[proftpd-dfsg] 01/04: New upstream version 1.3.6

Francesco Lovergine frankie at moszumanska.debian.org
Tue Feb 6 14:22:16 GMT 2018


This is an automated email from the git hooks/post-receive script.

frankie pushed a commit to branch master
in repository proftpd-dfsg.

commit 8eade2a0906ee4bb3c5bd8de9cd0ea8a8cb43481
Author: Francesco Paolo Lovergine <frankie at debian.org>
Date:   Tue Feb 6 13:58:03 2018 +0100

    New upstream version 1.3.6
---
 .gitattributes                                     |     2 +
 .github/ISSUE_TEMPLATE.md                          |    25 +
 .gitignore                                         |    84 +-
 .travis.yml                                        |    56 +-
 CREDITS                                            |    20 +-
 ChangeLog                                          |  6927 ++++-
 Make.rules.in                                      |    34 +-
 Makefile.in                                        |     4 +-
 NEWS                                               |   141 +-
 README                                             |   122 -
 README.md                                          |   100 +
 RELEASE_NOTES                                      |  1059 +-
 acconfig.h                                         |     3 -
 config.h.in                                        |   128 +-
 configure                                          |  9721 ++++---
 configure.in                                       |  1125 +-
 src/pidfile.c => contrib/dist/coverity/modeling.c  |    69 +-
 contrib/dist/rpm/proftpd.service                   |     2 +
 contrib/dist/rpm/proftpd.spec                      |   155 +-
 contrib/dist/systemd/README.systemd                |    25 +
 contrib/dist/systemd/proftpd.socket                |    16 +
 contrib/dist/systemd/proftpd at .service              |     7 +
 contrib/dist/travis/docker-rpmbuild.sh             |    30 +
 contrib/dist/vagrant/README.md                     |     9 +
 contrib/dist/vagrant/Vagrantfile                   |    21 +
 contrib/ftpasswd                                   |    49 +-
 contrib/ftpquota                                   |    26 +-
 contrib/{mod_snmp => mod_auth_otp}/Makefile.in     |    41 +-
 contrib/mod_auth_otp/auth-otp.8                    |    34 +
 contrib/mod_auth_otp/auth-otp.c                    |   290 +
 contrib/mod_auth_otp/base32.c                      |   177 +
 .../{mod_sftp/service.h => mod_auth_otp/base32.h}  |    21 +-
 contrib/mod_auth_otp/config.guess                  |  1530 ++
 contrib/mod_auth_otp/config.sub                    |  1779 ++
 contrib/{mod_sftp => mod_auth_otp}/configure       |   221 +-
 contrib/{mod_sftp => mod_auth_otp}/configure.in    |   139 +-
 contrib/mod_auth_otp/crypto.c                      |   128 +
 .../{mod_sftp/service.h => mod_auth_otp/crypto.h}  |    24 +-
 contrib/mod_auth_otp/db.c                          |   461 +
 contrib/mod_auth_otp/db.h                          |    60 +
 contrib/mod_auth_otp/install-sh                    |   250 +
 contrib/mod_auth_otp/mod_auth_otp.c                |  1069 +
 .../mod_auth_otp.h.in}                             |    72 +-
 contrib/mod_auth_otp/otp.c                         |   134 +
 contrib/{mod_sftp/kbdint.h => mod_auth_otp/otp.h}  |    37 +-
 contrib/mod_auth_otp/t/Makefile.in                 |    62 +
 contrib/mod_auth_otp/t/api/base32.c                |   139 +
 contrib/mod_auth_otp/t/api/otp.c                   |   262 +
 .../mod_auth_otp/t/api/stubs.c                     |    74 +-
 contrib/mod_auth_otp/t/api/tests.c                 |   121 +
 .../mkhome.h => contrib/mod_auth_otp/t/api/tests.h |    41 +-
 contrib/mod_ban.c                                  |   857 +-
 contrib/mod_copy.c                                 |   273 +-
 contrib/mod_ctrls_admin.c                          |   408 +-
 contrib/mod_deflate.c                              |   138 +-
 contrib/mod_digest.c                               |  2920 +++
 contrib/mod_dnsbl/Makefile.in                      |     2 +-
 contrib/mod_dnsbl/configure                        |    76 +-
 contrib/mod_dnsbl/configure.in                     |    25 +-
 contrib/mod_dnsbl/mod_dnsbl.c                      |   226 +-
 contrib/mod_dnsbl/mod_dnsbl.h.in                   |     5 +-
 contrib/mod_dynmasq.c                              |    49 +-
 contrib/mod_exec.c                                 |   375 +-
 contrib/mod_geoip.c                                |   404 +-
 contrib/mod_ifsession.c                            |   188 +-
 contrib/mod_ifversion.c                            |    15 +-
 contrib/mod_ldap.c                                 |   555 +-
 contrib/mod_load/Makefile.in                       |     2 +-
 contrib/mod_load/configure                         |    58 +-
 contrib/mod_load/configure.in                      |    52 +
 contrib/mod_load/mod_load.c                        |     5 +-
 contrib/mod_log_forensic.c                         |    71 +-
 contrib/mod_qos.c                                  |    26 +-
 contrib/mod_quotatab.c                             |   773 +-
 contrib/mod_quotatab.h                             |     7 +-
 contrib/mod_quotatab_file.c                        |    16 +-
 contrib/mod_quotatab_ldap.c                        |     6 +-
 contrib/mod_quotatab_radius.c                      |     6 +-
 contrib/mod_quotatab_sql.c                         |    27 +-
 contrib/mod_radius.c                               |  2250 +-
 contrib/mod_ratio.c                                |     8 +-
 contrib/mod_readme.c                               |    49 +-
 contrib/mod_rewrite.c                              |   616 +-
 contrib/mod_sftp/Makefile.in                       |    32 +-
 contrib/mod_sftp/agent.c                           |    20 +-
 contrib/mod_sftp/agent.h                           |    10 +-
 contrib/mod_sftp/auth-hostbased.c                  |    70 +-
 contrib/mod_sftp/auth-kbdint.c                     |    64 +-
 contrib/mod_sftp/auth-password.c                   |    20 +-
 contrib/mod_sftp/auth-publickey.c                  |   122 +-
 contrib/mod_sftp/auth.c                            |   634 +-
 contrib/mod_sftp/auth.h                            |    55 +-
 contrib/mod_sftp/blacklist.c                       |    21 +-
 contrib/mod_sftp/blacklist.h                       |     8 +-
 contrib/mod_sftp/channel.c                         |    29 +-
 contrib/mod_sftp/channel.h                         |     9 +-
 contrib/mod_sftp/cipher.c                          |   121 +-
 contrib/mod_sftp/cipher.h                          |    12 +-
 contrib/mod_sftp/compress.c                        |    60 +-
 contrib/mod_sftp/compress.h                        |    12 +-
 contrib/mod_sftp/configure                         |   202 +-
 contrib/mod_sftp/configure.in                      |    35 +-
 contrib/mod_sftp/crypto.c                          |   260 +-
 contrib/mod_sftp/crypto.h                          |    10 +-
 contrib/mod_sftp/date.c                            |     4 +-
 contrib/mod_sftp/date.h                            |    10 +-
 contrib/mod_sftp/disconnect.c                      |    19 +-
 contrib/mod_sftp/disconnect.h                      |    10 +-
 contrib/mod_sftp/display.c                         |    70 +-
 contrib/mod_sftp/display.h                         |     8 +-
 contrib/mod_sftp/fxp.c                             |  4693 ++--
 contrib/mod_sftp/fxp.h                             |    10 +-
 contrib/mod_sftp/interop.c                         |    66 +-
 contrib/mod_sftp/interop.h                         |    17 +-
 contrib/mod_sftp/kbdint.h                          |    10 +-
 contrib/mod_sftp/kex.c                             |  1229 +-
 contrib/mod_sftp/kex.h                             |    10 +-
 contrib/mod_sftp/keys.c                            |   993 +-
 contrib/mod_sftp/keys.h                            |    26 +-
 contrib/mod_sftp/keystore.c                        |    44 +-
 contrib/mod_sftp/keystore.h                        |    10 +-
 contrib/mod_sftp/mac.c                             |   172 +-
 contrib/mod_sftp/mac.h                             |    13 +-
 contrib/mod_sftp/misc.c                            |    99 +-
 contrib/mod_sftp/misc.h                            |    13 +-
 contrib/mod_sftp/mod_sftp.c                        |   638 +-
 contrib/mod_sftp/mod_sftp.h.in                     |    42 +-
 contrib/mod_sftp/msg.c                             |   127 +-
 contrib/mod_sftp/msg.h                             |    10 +-
 contrib/mod_sftp/packet.c                          |   139 +-
 contrib/mod_sftp/packet.h                          |    10 +-
 contrib/mod_sftp/rfc4716.c                         |    44 +-
 contrib/mod_sftp/rfc4716.h                         |    10 +-
 contrib/mod_sftp/scp.c                             |   269 +-
 contrib/mod_sftp/scp.h                             |    10 +-
 contrib/mod_sftp/service.c                         |     9 +-
 contrib/mod_sftp/service.h                         |     9 +-
 contrib/mod_sftp/session.c                         |     4 +-
 contrib/mod_sftp/session.h                         |    10 +-
 contrib/mod_sftp/ssh2.h                            |     6 +-
 contrib/mod_sftp/tap.c                             |    25 +-
 contrib/mod_sftp/tap.h                             |    10 +-
 contrib/mod_sftp/umac.c                            |   129 +-
 contrib/mod_sftp/umac.h                            |    55 +-
 contrib/mod_sftp/utf8.h                            |     4 +-
 contrib/mod_sftp_pam.c                             |    80 +-
 contrib/mod_sftp_sql.c                             |    27 +-
 contrib/mod_shaper.c                               |   122 +-
 contrib/mod_site_misc.c                            |   758 +-
 contrib/mod_snmp/Makefile.in                       |    10 +-
 contrib/mod_snmp/PROFTPD-MIB.txt                   |     2 -
 contrib/mod_snmp/agentx.h                          |    10 +-
 contrib/mod_snmp/asn1.c                            |    51 +-
 contrib/mod_snmp/asn1.h                            |    10 +-
 contrib/mod_snmp/configure                         |    76 +-
 contrib/mod_snmp/configure.in                      |     8 +-
 contrib/mod_snmp/db.c                              |     8 +-
 contrib/mod_snmp/db.h                              |     8 +-
 contrib/mod_snmp/mib.c                             |     5 +-
 contrib/mod_snmp/mib.h                             |    10 +-
 contrib/mod_snmp/mod_snmp.c                        |   303 +-
 contrib/mod_snmp/mod_snmp.h.in                     |     4 +-
 contrib/mod_snmp/msg.c                             |     5 +-
 contrib/mod_snmp/msg.h                             |    12 +-
 contrib/mod_snmp/notify.c                          |     7 +-
 contrib/mod_snmp/notify.h                          |    13 +-
 contrib/mod_snmp/packet.c                          |    16 +-
 contrib/mod_snmp/packet.h                          |    16 +-
 contrib/mod_snmp/pdu.c                             |     5 +-
 contrib/mod_snmp/pdu.h                             |    12 +-
 contrib/mod_snmp/smi.c                             |    17 +-
 contrib/mod_snmp/smi.h                             |    12 +-
 contrib/mod_snmp/stacktrace.c                      |    66 -
 contrib/mod_snmp/stacktrace.h                      |    34 -
 contrib/mod_snmp/uptime.c                          |     4 +-
 contrib/mod_snmp/uptime.h                          |    10 +-
 contrib/mod_sql.c                                  |  1503 +-
 contrib/mod_sql.h                                  |    12 +-
 contrib/mod_sql_mysql.c                            |   654 +-
 contrib/mod_sql_odbc.c                             |    96 +-
 contrib/mod_sql_passwd.c                           |   914 +-
 contrib/mod_sql_postgres.c                         |   568 +-
 contrib/mod_sql_sqlite.c                           |    47 +-
 contrib/mod_statcache.c                            |  2579 ++
 contrib/mod_tls.c                                  |  6924 ++++-
 contrib/mod_tls.h                                  |    69 +-
 contrib/mod_tls_fscache.c                          |   714 +
 contrib/mod_tls_memcache.c                         |  1691 +-
 contrib/mod_tls_redis.c                            |  1966 ++
 contrib/mod_tls_shmcache.c                         |  1890 +-
 contrib/mod_unique_id.c                            |    11 +-
 contrib/mod_wrap.c                                 |   132 +-
 contrib/mod_wrap2/Makefile.in                      |     2 +-
 contrib/mod_wrap2/configure                        |     2 +-
 contrib/mod_wrap2/configure.in                     |    19 +
 contrib/mod_wrap2/mod_wrap2.c                      |   144 +-
 contrib/mod_wrap2/mod_wrap2.h.in                   |    10 +-
 contrib/mod_wrap2_file.c                           |    24 +-
 contrib/mod_wrap2_redis.c                          |   481 +
 contrib/mod_wrap2_sql.c                            |    54 +-
 contrib/xferstats.holger-preiss                    |    39 +-
 doc/contrib/ftpasswd.html                          |    24 +-
 doc/contrib/ftpmail.html                           |    29 +-
 doc/contrib/ftpquota.html                          |    39 +-
 doc/contrib/index.html                             |    33 +-
 doc/contrib/mod_auth_otp.html                      |   469 +
 doc/contrib/mod_ban.html                           |   114 +-
 doc/contrib/mod_copy.html                          |    81 +-
 doc/contrib/mod_ctrls_admin.html                   |   130 +-
 doc/contrib/mod_deflate.html                       |    54 +-
 doc/contrib/mod_digest.html                        |   394 +
 doc/contrib/mod_dnsbl.html                         |    71 +-
 doc/contrib/mod_dynmasq.html                       |    34 +-
 doc/contrib/mod_exec.html                          |   100 +-
 doc/contrib/mod_geoip.html                         |   149 +-
 doc/contrib/mod_ifsession.html                     |    68 +-
 doc/contrib/mod_ifversion.html                     |    45 +-
 doc/contrib/mod_ldap.html                          |   610 +-
 doc/contrib/mod_load.html                          |    26 +-
 doc/contrib/mod_log_forensic.html                  |    56 +-
 doc/contrib/mod_qos.html                           |    39 +-
 doc/contrib/mod_quotatab.html                      |    52 +-
 doc/contrib/mod_quotatab_file.html                 |    16 +-
 doc/contrib/mod_quotatab_ldap.html                 |    12 +-
 doc/contrib/mod_quotatab_radius.html               |    12 +-
 doc/contrib/mod_quotatab_sql.html                  |    20 +-
 doc/contrib/mod_radius.html                        |   185 +-
 doc/contrib/mod_ratio.html                         |    81 +-
 doc/contrib/mod_readme.html                        |    31 +-
 doc/contrib/mod_rewrite.html                       |    68 +-
 doc/contrib/mod_sftp.html                          |   564 +-
 doc/contrib/mod_sftp_pam.html                      |    44 +-
 doc/contrib/mod_sftp_sql.html                      |    59 +-
 doc/contrib/mod_shaper.html                        |    51 +-
 doc/contrib/mod_site_misc.html                     |    65 +-
 doc/contrib/mod_snmp.html                          |   123 +-
 doc/contrib/mod_sql.html                           |   155 +-
 doc/contrib/mod_sql_odbc.html                      |    67 +-
 doc/contrib/mod_sql_passwd.html                    |   236 +-
 doc/contrib/mod_sql_sqlite.html                    |    36 +-
 doc/contrib/mod_statcache.html                     |   258 +
 doc/contrib/mod_tls.html                           |   578 +-
 doc/contrib/mod_tls_fscache.html                   |    94 +
 doc/contrib/mod_tls_memcache.html                  |    71 +-
 doc/contrib/mod_tls_redis.html                     |   129 +
 doc/contrib/mod_tls_shmcache.html                  |    75 +-
 doc/contrib/mod_unique_id.html                     |    49 +-
 doc/contrib/mod_wrap.html                          |    40 +-
 doc/contrib/mod_wrap2.html                         |    73 +-
 doc/contrib/mod_wrap2_file.html                    |    35 +-
 doc/contrib/mod_wrap2_redis.html                   |   145 +
 doc/contrib/mod_wrap2_sql.html                     |    81 +-
 doc/howto/ASCII.html                               |    14 +-
 doc/howto/AWS.html                                 |   532 +
 doc/howto/AuthFiles.html                           |    28 +-
 doc/howto/Authentication.html                      |    13 +-
 doc/howto/BCP.html                                 |    58 +-
 doc/howto/Chroot.html                              |    30 +-
 doc/howto/Classes.html                             |    28 +-
 doc/howto/Compiling.html                           |   103 +-
 doc/howto/ConfigFile.html                          |   240 +-
 doc/howto/ConfigurationTricks.html                 |    14 +-
 doc/howto/ConnectionACLs.html                      |    16 +-
 doc/howto/Controls.html                            |    27 +-
 doc/howto/CreateHome.html                          |    17 +-
 doc/howto/DNS.html                                 |    23 +-
 doc/howto/DSO.html                                 |    36 +-
 doc/howto/Debugging.html                           |    35 +-
 doc/howto/Directory.html                           |    51 +-
 doc/howto/DisplayFiles.html                        |    17 +-
 doc/howto/ECCN.html                                |     9 +-
 doc/howto/FTP.html                                 |    36 +-
 doc/howto/FXP.html                                 |    32 +-
 doc/howto/Filters.html                             |    13 +-
 doc/howto/Globbing.html                            |    22 +-
 doc/howto/KeepAlives.html                          |    18 +-
 doc/howto/Limit.html                               |    14 +-
 doc/howto/ListOptions.html                         |    23 +-
 doc/howto/LogLevels.html                           |    11 +-
 doc/howto/LogMessages.html                         |    17 +-
 doc/howto/Logging.html                             |   168 +-
 doc/howto/Memcache.html                            |    32 +-
 doc/howto/NAT.html                                 |    39 +-
 doc/howto/Nonroot.html                             |    23 +-
 doc/howto/Quotas.html                              |    73 +-
 doc/howto/Radius.html                              |    25 +-
 doc/howto/Redis.html                               |   207 +
 doc/howto/Regex.html                               |    21 +-
 doc/howto/Rewrite.html                             |    19 +-
 doc/howto/SQL.html                                 |   196 +-
 doc/howto/SSH.html                                 |    21 +-
 doc/howto/Scoreboard.html                          |    14 +-
 doc/howto/Sendfile.html                            |    18 +-
 doc/howto/ServerType.html                          |    11 +-
 doc/howto/Stopping.html                            |    17 +-
 doc/howto/TLS.html                                 |   180 +-
 doc/howto/Testing.html                             |    36 +-
 doc/howto/Timestamps.html                          |    30 +-
 doc/howto/Tracing.html                             |    20 +-
 doc/howto/Translations.html                        |    30 +-
 doc/howto/Umask.html                               |    15 +-
 doc/howto/Upgrade.html                             |    34 +-
 doc/howto/Versioning.html                          |    28 +-
 doc/howto/Vhost.html                               |   132 +-
 doc/howto/VirtualUsers.html                        |    20 +-
 doc/howto/ftpaccess.html                           |    64 +
 doc/howto/index.html                               |    32 +-
 doc/license.txt                                    |     2 +-
 doc/mod_sample.c                                   |     6 +-
 doc/modules/index.html                             |    14 +-
 doc/modules/mod_auth.html                          |   535 +-
 doc/modules/mod_auth_file.html                     |    39 +-
 doc/modules/mod_auth_pam.html                      |    34 +-
 doc/modules/mod_auth_unix.html                     |    98 +-
 doc/modules/mod_cap.html                           |    19 +-
 doc/modules/mod_core.html                          |  1836 +-
 doc/modules/mod_ctrls.html                         |    62 +-
 doc/modules/mod_delay.html                         |    86 +-
 doc/modules/mod_dso.html                           |    91 +-
 doc/modules/mod_facl.html                          |    35 +-
 doc/modules/mod_facts.html                         |    81 +-
 doc/modules/mod_ident.html                         |    49 +-
 doc/modules/mod_lang.html                          |   106 +-
 doc/modules/mod_log.html                           |   139 +-
 doc/modules/mod_ls.html                            |   165 +-
 doc/modules/mod_memcache.html                      |    47 +-
 doc/modules/mod_redis.html                         |   493 +
 doc/modules/mod_rlimit.html                        |    90 +-
 doc/modules/mod_site.html                          |    18 +-
 doc/modules/mod_xfer.html                          |   216 +-
 doc/utils/ftpasswd.html                            |    65 +-
 doc/utils/ftpcount.html                            |    18 +-
 doc/utils/ftpdctl.html                             |    16 +-
 doc/utils/ftpmail.html                             |    19 +-
 doc/utils/ftpquota.html                            |    40 +-
 doc/utils/ftpscrub.html                            |    11 +-
 doc/utils/ftpshut.html                             |    11 +-
 doc/utils/ftptop.html                              |    11 +-
 doc/utils/ftpwho.html                              |    13 +-
 doc/utils/index.html                               |    10 +-
 doc/utils/prxs.html                                |    19 +-
 include/ascii.h                                    |    27 +-
 include/auth.h                                     |    61 +-
 include/bindings.h                                 |    62 +-
 include/ccan-json.h                                |   120 +
 include/child.h                                    |     6 +-
 include/class.h                                    |    25 +-
 include/cmd.h                                      |    33 +-
 include/compat.h                                   |    55 +-
 include/conf.h                                     |    18 +-
 include/configdb.h                                 |   144 +
 include/ctrls.h                                    |     6 +-
 include/data.h                                     |    31 +-
 include/default_paths.h                            |    15 +-
 include/dirtree.h                                  |   148 +-
 include/display.h                                  |    14 +-
 include/encode.h                                   |    15 +-
 include/env.h                                      |     6 +-
 include/event.h                                    |     6 +-
 include/expr.h                                     |     8 +-
 include/feat.h                                     |     6 +-
 include/filter.h                                   |     8 +-
 include/fsio.h                                     |   202 +-
 include/ftp.h                                      |     9 +-
 include/{tpl.h => hanson-tpl.h}                    |     1 +
 include/help.h                                     |     6 +-
 include/ident.h                                    |     4 +-
 include/inet.h                                     |    28 +-
 include/json.h                                     |   153 +
 include/lastlog.h                                  |     8 +-
 include/libsupp.h                                  |     6 +-
 include/log.h                                      |    20 +-
 include/{mod_log.h => logfmt.h}                    |    19 +-
 include/memcache.h                                 |    13 +-
 include/mkhome.h                                   |     6 +-
 include/mod_ctrls.h                                |     5 +-
 include/modules.h                                  |    47 +-
 include/netacl.h                                   |    18 +-
 include/netaddr.h                                  |    41 +-
 include/netio.h                                    |    22 +-
 include/options.h                                  |    43 +-
 include/parser.h                                   |    64 +-
 include/pidfile.h                                  |    10 +-
 include/pool.h                                     |    14 +-
 include/pr-syslog.h                                |    12 +-
 include/privs.h                                    |     6 +-
 include/proctitle.h                                |     6 +-
 include/proftpd.h                                  |    34 +-
 include/redis.h                                    |   298 +
 include/regexp.h                                   |     6 +-
 include/response.h                                 |    13 +-
 include/rlimit.h                                   |     6 +-
 include/scoreboard.h                               |     8 +-
 include/session.h                                  |    13 +-
 include/sets.h                                     |     5 +-
 src/version.c => include/signals.h                 |    32 +-
 include/stash.h                                    |    25 +-
 include/str.h                                      |    67 +-
 include/support.h                                  |    40 +-
 include/table.h                                    |    43 +-
 include/throttle.h                                 |     6 +-
 include/timers.h                                   |     4 +-
 include/trace.h                                    |     6 +-
 include/utf8.h                                     |     6 +-
 include/var.h                                      |     6 +-
 include/version.h                                  |     6 +-
 include/xferlog.h                                  |    10 +-
 lib/Makefile.in                                    |     5 +-
 lib/ccan-json.c                                    |  1404 +
 lib/glibc-glob.c                                   |     2 +-
 lib/{tpl.c => hanson-tpl.c}                        |    28 +-
 lib/libcap/_makenames.c                            |     2 -
 lib/libcap/cap_alloc.c                             |     2 -
 lib/libcap/cap_extint.c                            |     2 -
 lib/libcap/cap_file.c                              |     2 -
 lib/libcap/cap_flag.c                              |     2 -
 lib/libcap/cap_proc.c                              |     2 -
 lib/libcap/cap_sys.c                               |     2 -
 lib/libcap/cap_text.c                              |     2 -
 lib/libcap/libcap.h                                |     2 -
 lib/libltdl/ltdl.c                                 |     4 +-
 lib/pr-syslog.c                                    |    94 +-
 lib/pr_fnmatch.c                                   |     6 +-
 lib/pr_fnmatch_loop.c                              |     2 +-
 lib/sstrncpy.c                                     |    20 +-
 locale/Makefile.in                                 |     2 +-
 locale/files.txt                                   |    61 +-
 modules/Makefile.in                                |     3 +
 modules/mod_auth.c                                 |  1208 +-
 modules/mod_auth_file.c                            |   400 +-
 modules/mod_auth_pam.c                             |   231 +-
 modules/mod_auth_unix.c                            |   722 +-
 modules/mod_cap.c                                  |   126 +-
 modules/mod_core.c                                 |  1828 +-
 modules/mod_ctrls.c                                |   195 +-
 modules/mod_delay.c                                |   524 +-
 modules/mod_dso.c                                  |    26 +-
 modules/mod_facl.c                                 |   454 +-
 modules/mod_facts.c                                |   660 +-
 modules/mod_ident.c                                |    65 +-
 modules/mod_lang.c                                 |   444 +-
 modules/mod_log.c                                  |   972 +-
 modules/mod_ls.c                                   |   675 +-
 modules/mod_memcache.c                             |    89 +-
 modules/mod_redis.c                                |  1942 ++
 modules/mod_rlimit.c                               |    46 +-
 modules/mod_site.c                                 |   292 +-
 modules/mod_xfer.c                                 |  1231 +-
 src/Makefile.in                                    |     5 +-
 src/ascii.c                                        |   180 +
 src/auth.c                                         |   932 +-
 src/bindings.c                                     |   297 +-
 src/child.c                                        |     6 +-
 src/class.c                                        |   101 +-
 src/cmd.c                                          |   129 +-
 src/configdb.c                                     |   965 +
 src/ctrls.c                                        |   190 +-
 src/data.c                                         |   594 +-
 src/dirtree.c                                      |  1479 +-
 src/display.c                                      |   104 +-
 src/encode.c                                       |    22 +-
 src/env.c                                          |    31 +-
 src/event.c                                        |    15 +-
 src/expr.c                                         |    13 +-
 src/feat.c                                         |    24 +-
 src/filter.c                                       |    22 +-
 src/fsio.c                                         |  7761 ++++--
 src/ftpdctl.c                                      |    34 +-
 src/help.c                                         |    55 +-
 src/ident.c                                        |     7 +-
 src/inet.c                                         |   609 +-
 src/json.c                                         |   844 +
 src/lastlog.c                                      |     8 +-
 src/log.c                                          |   226 +-
 src/main.c                                         |  1102 +-
 src/memcache.c                                     |   146 +-
 src/mkhome.c                                       |    18 +-
 src/modules.c                                      |    93 +-
 src/netacl.c                                       |   175 +-
 src/netaddr.c                                      |   325 +-
 src/netio.c                                        |   893 +-
 src/parser.c                                       |   848 +-
 src/pidfile.c                                      |    47 +-
 src/pool.c                                         |   119 +-
 src/privs.c                                        |    26 +-
 src/proctitle.c                                    |   119 +-
 src/proftpd.8.in                                   |     9 +-
 src/redis.c                                        |  5708 +++++
 src/regexp.c                                       |   107 +-
 src/response.c                                     |    74 +-
 src/rlimit.c                                       |    11 +-
 src/scoreboard.c                                   |   311 +-
 src/session.c                                      |    96 +-
 src/sets.c                                         |     6 +-
 src/signals.c                                      |   720 +
 src/stash.c                                        |   709 +-
 src/str.c                                          |   629 +-
 src/support.c                                      |   819 +-
 src/table.c                                        |   224 +-
 src/throttle.c                                     |     6 +-
 src/timers.c                                       |   110 +-
 src/trace.c                                        |   116 +-
 src/utf8.c                                         |     6 +-
 src/var.c                                          |    13 +-
 src/version.c                                      |     6 +-
 src/wtmp.c                                         |     6 +-
 src/xferlog.c                                      |    35 +-
 tests/Makefile.in                                  |    43 +-
 tests/api/array.c                                  |    63 +-
 tests/api/ascii.c                                  |   351 +
 tests/api/auth.c                                   |  1915 ++
 tests/api/class.c                                  |    42 +-
 tests/api/cmd.c                                    |   196 +-
 tests/api/configdb.c                               |   642 +
 tests/api/data.c                                   |  1097 +
 tests/api/display.c                                |   296 +
 tests/api/encode.c                                 |   303 +
 tests/api/env.c                                    |     8 +-
 tests/api/etc/str/utf8-space.txt                   |     1 +
 tests/api/event.c                                  |    90 +-
 tests/api/expr.c                                   |    12 +-
 tests/api/feat.c                                   |     6 +-
 tests/api/filter.c                                 |   164 +
 tests/api/fsio.c                                   |  4682 +++-
 tests/api/help.c                                   |   178 +
 tests/api/inet.c                                   |   815 +
 tests/api/json.c                                   |  1858 ++
 tests/api/misc.c                                   |  1193 +
 tests/api/modules.c                                |   408 +-
 tests/api/netacl.c                                 |   289 +-
 tests/api/netaddr.c                                |   766 +-
 tests/api/netio.c                                  |  1068 +-
 tests/api/parser.c                                 |   656 +
 tests/api/pidfile.c                                |   126 +
 tests/api/pool.c                                   |   243 +-
 tests/api/privs.c                                  |   185 +
 tests/api/redis.c                                  |  4475 ++++
 tests/api/regexp.c                                 |   247 +-
 tests/api/response.c                               |   277 +-
 tests/api/rlimit.c                                 |   191 +
 tests/api/scoreboard.c                             |  1398 +-
 tests/api/sets.c                                   |     6 +-
 tests/api/stash.c                                  |   411 +-
 tests/api/str.c                                    |   597 +-
 tests/api/stubs.c                                  |   137 +-
 tests/api/table.c                                  |   134 +-
 tests/api/tests.c                                  |    99 +-
 tests/api/tests.h                                  |    26 +-
 tests/api/timers.c                                 |    23 +-
 tests/api/trace.c                                  |   420 +
 tests/api/var.c                                    |     6 +-
 tests/api/version.c                                |     6 +-
 tests/t/commands/clnt.t                            |    11 +
 tests/t/config/anonallowrobots.t                   |    11 +
 tests/t/config/loginpasswordprompt.t               |    11 +
 tests/t/config/maxpasswordsize.t                   |    11 +
 tests/t/config/maxtransfersperhost.t               |    11 +
 tests/t/config/maxtransfersperuser.t               |    11 +
 tests/t/config/transferoptions.t                   |    11 +
 tests/t/config/virtualhost.t                       |    11 +
 tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key  |    12 +
 .../etc/modules/mod_auth_otp/ssh_host_dsa_key.pub  |     1 +
 tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key  |    27 +
 .../etc/modules/mod_auth_otp/ssh_host_rsa_key.pub  |     1 +
 tests/t/etc/modules/mod_geoip/GeoIP.dat            |   Bin 1084888 -> 754324 bytes
 tests/t/etc/modules/mod_geoip/GeoIPASNum.dat       |   Bin 3658209 -> 3899353 bytes
 tests/t/etc/modules/mod_geoip/GeoIPv6.dat          |   Bin 1232477 -> 1405637 bytes
 tests/t/etc/modules/mod_geoip/GeoLiteCity.dat      |   Bin 30751876 -> 15966443 bytes
 tests/t/etc/modules/mod_sftp/authorized_rsa_keys2  |    23 +
 .../etc/modules/mod_sftp/authorized_rsa_keys_no_nl |     9 +
 tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key  |    15 +
 .../etc/modules/mod_sftp/ssh_host_rsa1024_key.pub  |     1 +
 tests/t/etc/modules/mod_sftp/test_rsa2048_key2     |    27 +
 tests/t/etc/modules/mod_sftp/test_rsa2048_key2.pub |     1 +
 tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155  |    27 +
 .../etc/modules/mod_sftp/test_rsa_key_bug4155.pub  |     1 +
 tests/t/etc/modules/mod_sql_odbc/odbc.ini          |    15 +
 tests/t/etc/modules/mod_sql_odbc/odbcinst.ini      |    11 +
 .../etc/modules/mod_tls/tls-get-passphrase-once.pl |    22 +
 tests/t/lib/ProFTPD/TestSuite/FTP.pm               |    30 +-
 tests/t/lib/ProFTPD/TestSuite/Utils.pm             |   126 +-
 tests/t/lib/ProFTPD/Tests/Commands/APPE.pm         |  2189 +-
 tests/t/lib/ProFTPD/Tests/Commands/CLNT.pm         |   106 +
 tests/t/lib/ProFTPD/Tests/Commands/CWD.pm          |  1882 +-
 tests/t/lib/ProFTPD/Tests/Commands/DELE.pm         |   166 +-
 tests/t/lib/ProFTPD/Tests/Commands/FEAT.pm         |    72 +-
 tests/t/lib/ProFTPD/Tests/Commands/HOST.pm         |  1135 +-
 tests/t/lib/ProFTPD/Tests/Commands/LIST.pm         |  2760 +-
 tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm         |  1084 +-
 tests/t/lib/ProFTPD/Tests/Commands/MKD.pm          |  2032 +-
 tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm         |   964 +-
 tests/t/lib/ProFTPD/Tests/Commands/MLST.pm         |   734 +-
 tests/t/lib/ProFTPD/Tests/Commands/NLST.pm         |  2055 +-
 tests/t/lib/ProFTPD/Tests/Commands/OPTS.pm         |     4 +-
 tests/t/lib/ProFTPD/Tests/Commands/RETR.pm         |  1603 +-
 tests/t/lib/ProFTPD/Tests/Commands/RMD.pm          |  1614 +-
 tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm         |  1080 +-
 tests/t/lib/ProFTPD/Tests/Commands/RNTO.pm         |   631 +-
 tests/t/lib/ProFTPD/Tests/Commands/SIZE.pm         |  1182 +-
 tests/t/lib/ProFTPD/Tests/Commands/STAT.pm         |   940 +-
 tests/t/lib/ProFTPD/Tests/Commands/STOR.pm         |  2058 +-
 tests/t/lib/ProFTPD/Tests/Commands/STOU.pm         |   207 +-
 tests/t/lib/ProFTPD/Tests/Commands/TYPE.pm         |   135 +-
 .../ProFTPD/Tests/Config/AllowForeignAddress.pm    |   272 +-
 .../t/lib/ProFTPD/Tests/Config/AnonAllowRobots.pm  |   583 +
 .../ProFTPD/Tests/Config/AnonRejectPasswords.pm    |   291 +-
 tests/t/lib/ProFTPD/Tests/Config/AuthAliasOnly.pm  |   233 +-
 tests/t/lib/ProFTPD/Tests/Config/DefaultChdir.pm   |   126 +-
 tests/t/lib/ProFTPD/Tests/Config/DefaultRoot.pm    |    76 +-
 .../t/lib/ProFTPD/Tests/Config/Directory/Limits.pm |   161 -
 .../ProFTPD/Tests/Config/DisplayFileTransfer.pm    |   112 +-
 tests/t/lib/ProFTPD/Tests/Config/DisplayQuit.pm    |    86 +
 tests/t/lib/ProFTPD/Tests/Config/FactsOptions.pm   |     4 +-
 tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm   |   649 +-
 tests/t/lib/ProFTPD/Tests/Config/HideFiles.pm      |   182 +-
 tests/t/lib/ProFTPD/Tests/Config/Include.pm        |   116 +
 tests/t/lib/ProFTPD/Tests/Config/Limit/RMD.pm      |    32 +-
 tests/t/lib/ProFTPD/Tests/Config/ListOptions.pm    |   160 +-
 .../ProFTPD/Tests/Config/LoginPasswordPrompt.pm    |   215 +
 .../lib/ProFTPD/Tests/Config/MasqueradeAddress.pm  |   179 +
 tests/t/lib/ProFTPD/Tests/Config/MaxClients.pm     |   117 +
 .../t/lib/ProFTPD/Tests/Config/MaxPasswordSize.pm  |   184 +
 .../ProFTPD/Tests/Config/MaxTransfersPerHost.pm    |   236 +
 .../ProFTPD/Tests/Config/MaxTransfersPerUser.pm    |   236 +
 tests/t/lib/ProFTPD/Tests/Config/Order.pm          |   283 +-
 tests/t/lib/ProFTPD/Tests/Config/ServerIdent.pm    |   397 +-
 tests/t/lib/ProFTPD/Tests/Config/ShowSymlinks.pm   |    20 +-
 .../t/lib/ProFTPD/Tests/Config/TransferOptions.pm  |   271 +
 tests/t/lib/ProFTPD/Tests/Config/Umask.pm          |    15 +-
 tests/t/lib/ProFTPD/Tests/Config/VirtualHost.pm    |   139 +
 tests/t/lib/ProFTPD/Tests/HTTP.pm                  |     5 +-
 tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm   |  3082 ++-
 tests/t/lib/ProFTPD/Tests/Logins.pm                |   743 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp.pm  |  1938 ++
 .../lib/ProFTPD/Tests/Modules/mod_auth_otp/sftp.pm |  1283 +
 .../lib/ProFTPD/Tests/Modules/mod_ban/memcache.pm  |   259 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_copy.pm      |   334 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_ctrls.pm     |   103 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_delay.pm     |   704 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_digest.pm    |  8679 +++++++
 tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm      |   292 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_geoip.pm     |   183 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_geoip/sql.pm |   199 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm |   378 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm      |   711 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_ldap.pm      |    17 +
 .../lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm  |  1486 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_readme.pm    |   454 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_rlimit.pm    |   128 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm      | 25482 +++++++++++++------
 .../lib/ProFTPD/Tests/Modules/mod_sftp/rewrite.pm  |    14 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/sql.pm  |    25 +
 .../t/lib/ProFTPD/Tests/Modules/mod_sftp/wrap2.pm  |   173 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_pam.pm  |   216 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm  |   481 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_shaper.pm    |   144 +
 tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm      |  1438 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_site_misc.pm |  2059 +-
 tests/t/lib/ProFTPD/Tests/Modules/mod_snmp.pm      |   253 +-
 .../t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm  |  4039 ++-
 .../t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm  |  6750 +++--
 tests/t/lib/ProFTPD/Tests/Modules/mod_statcache.pm |  2569 ++
 tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm       |   654 +-
 .../t/lib/ProFTPD/Tests/Modules/mod_tls_fscache.pm |   235 +
 .../lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm  |   475 +-
 .../lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm  |   184 +-
 .../t/lib/ProFTPD/Tests/Modules/mod_wrap2_redis.pm |  2927 +++
 tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_sql.pm |   228 +-
 tests/t/lib/ProFTPD/Tests/SMTP.pm                  |     2 +-
 tests/t/lib/ProFTPD/Tests/SSH2.pm                  |   135 +
 tests/t/modules/mod_auth_otp.t                     |    11 +
 tests/t/modules/mod_auth_otp/sftp.t                |    11 +
 tests/t/modules/mod_digest.t                       |    11 +
 tests/t/modules/mod_dynmasq.t                      |    11 +
 tests/t/modules/mod_geoip/sql.t                    |    11 +
 tests/t/modules/mod_rlimit.t                       |    11 +
 tests/t/modules/mod_statcache.t                    |    11 +
 tests/t/modules/mod_tls_fscache.t                  |    11 +
 tests/t/modules/mod_wrap2_redis.t                  |    11 +
 tests/t/ssh2.t                                     |    11 +
 tests/tests.pl                                     |    54 +
 utils/Makefile.in                                  |     6 +-
 utils/ftpcount.c                                   |    35 +-
 utils/ftpscrub.c                                   |    17 +-
 utils/ftpshut.8.in                                 |     4 +-
 utils/ftpshut.c                                    |    74 +-
 utils/ftptop.c                                     |    18 +-
 utils/ftpwho.1.in                                  |     7 +-
 utils/ftpwho.c                                     |   320 +-
 utils/misc.c                                       |     3 +-
 utils/scoreboard.c                                 |    50 +-
 utils/utils.h                                      |     8 +-
 692 files changed, 206173 insertions(+), 49181 deletions(-)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fa18026
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.pl linguist-language=C
+*.pm linguist-language=C
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..54e81b5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,25 @@
+### What I Did
+
+Please describe what you did, including the clients you used, what errors
+or messages you saw, _etc_.
+
+### What I Expected/Wanted
+
+Please describe what behavior you wanted to achieve, what you expected to see.
+Sometimes the "bug" is simply that ProFTPD is doing something different than
+what you expected, and sometimes the desired behavior can be done using
+different approaches.  And sometimes, knowing what you expected to see helps
+us to better pinpoint the root cause of the problem/issue.
+
+### ProFTPD Version and Configuration
+
+Please help us reproduce the problem/issue you are encountering.  To do this,
+we need to know which version of ProFTPD you are using, how it was built,
+_etc_.  The following command is an easy way to get all of this information:
+
+    $ proftpd -V
+
+In addition, we need to see *all* of the ProFTPD configuration files you are
+using (*minus* any sensitive information like passwords, of course).  Armed
+with the version and configuration data, then, we can set up ProFTPD locally
+using the same configuration, and see what happens.
diff --git a/.gitignore b/.gitignore
index cdabacf..e435926 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
-.github
+.deps/
+.github/
+autom4te.cache
 config.log
 config.h
 config.cache
 config.status
-autom4te.cache
+cov-int
 proftpd
 proftpd.pc
 module-libs.txt
@@ -13,20 +15,98 @@ ftpcount
 ftptop
 ftpdctl
 ftpscrub
+contrib/mod_auth_otp/auth-otp
+contrib/mod_auth_otp/mod_auth_otp.h
+contrib/mod_dnsbl/mod_dnsbl.h
+contrib/mod_load/mod_load.h
+contrib/mod_sftp/mod_sftp.h
+contrib/mod_snmp/mod_snmp.h
+contrib/mod_wrap2/mod_wrap2.h
 include/buildstamp.h
 Makefile
 Make.rules
 Make.modules
+lib/libcap/_makenames
+lib/libcap/cap_names.h
+lib/libcap/cap_names.sed
+lib/libltdl/stamp-h1
+module-libs.txt
 modules/module_glue.c
 src/prxs
 src/*.[0-9]
 stamp-h
+tests/api-tests
+tests/api-tests.log
 utils/*.[0-9]
 *.a
+*.gcda
+*.gcno
 *.la
+*.lo
+*.log
 *.o
+*.orig
 *.patch
+*.rej
 *~
+*.swo
 *.swp
+.DS_Store
 .libs
 libtool
+
+# Ignore symlinks (e.g. from contrib/ to include/ or modules/) generated by
+# the build system.
+include/mod_auth_otp.h
+include/mod_digest.h
+include/mod_dnsbl.h
+include/mod_load.h
+include/mod_quotatab.h
+include/mod_sftp.h
+include/mod_snmp.h
+include/mod_sql.h
+include/mod_tls.h
+include/mod_wrap2.h
+modules/mod_ban.c
+modules/mod_copy.c
+modules/mod_ctrls_admin.c
+modules/mod_deflate.c
+modules/mod_digest.c
+modules/mod_dynmasq.c
+modules/mod_exec.c
+modules/mod_geoip.c
+modules/mod_ifsession.c
+modules/mod_ifversion.c
+modules/mod_ldap.c
+modules/mod_log_forensic.c
+modules/mod_qos.c
+modules/mod_quotatab.c
+modules/mod_quotatab_file.c
+modules/mod_quotatab_ldap.c
+modules/mod_quotatab_radius.c
+modules/mod_quotatab_sql.c
+modules/mod_radius.c
+modules/mod_ratio.c
+modules/mod_readme.c
+modules/mod_rewrite.c
+modules/mod_sftp_ldap.c
+modules/mod_sftp_pam.c
+modules/mod_sftp_sql.c
+modules/mod_shaper.c
+modules/mod_site_misc.c
+modules/mod_sql.c
+modules/mod_sql_mysql.c
+modules/mod_sql_odbc.c
+modules/mod_sql_passwd.c
+modules/mod_sql_postgres.c
+modules/mod_sql_sqlite.c
+modules/mod_statcache.c
+modules/mod_tls.c
+modules/mod_tls_fscache.c
+modules/mod_tls_memcache.c
+modules/mod_tls_shmcache.c
+modules/mod_unique_id.c
+modules/mod_wrap.c
+modules/mod_wrap2_file.c
+modules/mod_wrap2_redis.c
+modules/mod_wrap2_sql.c
diff --git a/.travis.yml b/.travis.yml
index c06191f..266aa61 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,43 +1,91 @@
-env: TRAVIS_CI=true
+env:
+  - PACKAGE_VERSION=1.3.6
+
 language: c
 
 compiler:
   - gcc
   - clang
 
+services:
+  - docker
+  - redis-server
+
 install:
+  # Need to add other repos for e.g. libsodium
+  - sudo add-apt-repository -y ppa:jbboehr/ppa
+  - sudo add-apt-repository -y ppa:chris-lea/libsodium
+  - echo "deb http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" | sudo tee -a /etc/apt/sources.list > /dev/null
+  - echo "deb-src http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" | sudo tee -a /etc/apt/sources.list > /dev/null
   - sudo apt-get update -qq
   # for unit tests
   - sudo apt-get install -y check
   # for mod_lang
   - sudo apt-get install -y gettext
+  # for xattr support
+  - sudo apt-get install -y libattr1-dev
+  # for mod_cap
+  - sudo apt-get install -y libcap-dev
   # for mod_geoip
   - sudo apt-get install -y libgeoip-dev
   # for mod_ldap
   - sudo apt-get install -y libldap2-dev libsasl2-dev
   # for memcache support
   - sudo apt-get install -y libmemcached-dev
+  # for redis support
+  - sudo apt-get install -y libhiredis-dev
   # for mod_sql_mysql
   - sudo apt-get install -y libmysqlclient-dev
   # for PAM support
   - sudo apt-get install -y libpam-dev
   # for mod_sql_postgres
   - sudo apt-get install -y libpq-dev
+  # for mod_sql_odbc
+  - sudo apt-get install -y unixodbc-dev
   # for OpenSSL support
   - sudo apt-get install -y libssl-dev
+  # for Sodium support
+  - sudo apt-get install -y --force-yes libsodium-dev
   # for mod_sql_sqlite
   - sudo apt-get install -y libsqlite3-dev sqlite3
+  # for mod_wrap
+  - sudo apt-get install -y libwrap0-dev
   # for PCRE support
   - sudo apt-get install -y libpcre3-dev
+  # for zlib support
+  - sudo apt-get install -y zlib1g-dev
   # for static code analysis
   - sudo apt-get install -y cppcheck rats
+  # for test code coverage
+  - sudo apt-get install -y lcov
+  - gem install coveralls-lcov
+  # for HTML validation
+  - sudo apt-get install -y tidy
+
+before_script:
+  - cd ${TRAVIS_BUILD_DIR}
+  - lcov --directory . --zerocounters
 
 script:
   # - find . -type f -name "*.c" -print | grep -v tests | xargs cppcheck 2>&1
   # - find . -type f -name "*.c" -print | grep -v tests | xargs rats --language=c
-  - ./configure --enable-ctrls --enable-facl --enable-memcache --enable-nls --enable-pcre --enable-tests --with-modules=mod_sql:mod_sql_mysql:mod_sql_postgres:mod_sql_sqlite:mod_sql_passwd:mod_sftp:mod_sftp_sql:mod_sftp_pam:mod_tls:mod_tls_shmcache:mod_tls_memcache:mod_ban:mod_copy:mod_ctrls_admin:mod_deflate:mod_dnsbl:mod_dynmasq:mod_exec:mod_facl:mod_geoip:mod_ifversion:mod_ldap:mod_load:mod_log_forensic:mod_quotatab:mod_quotatab_file:mod_quotatab_sql:mod_radius:mod_readme:mod_rewrite: [...]
+  - ./configure LIBS="-lodbc -lm -lrt -pthread" --enable-devel=coverage --enable-ctrls --enable-facl --enable-memcache --enable-nls --enable-pcre --enable-redis --enable-tests --with-modules=mod_sql:mod_sql_mysql:mod_sql_odbc:mod_sql_postgres:mod_sql_sqlite:mod_sql_passwd:mod_sftp:mod_sftp_sql:mod_sftp_pam:mod_tls:mod_tls_fscache:mod_tls_shmcache:mod_tls_memcache:mod_tls_redis:mod_ban:mod_copy:mod_ctrls_admin:mod_deflate:mod_dnsbl:mod_dynmasq:mod_exec:mod_facl:mod_geoip:mod_ifversion:mod [...]
   - make
-  - make check-api
+  - make TEST_VERBOSE=1 check-api
   - make clean
-  - ./configure --enable-ctrls --enable-dso --enable-facl --enable-memcache --enable-nls --enable-pcre --enable-tests --with-shared=mod_sql:mod_sql_mysql:mod_sql_postgres:mod_sql_sqlite:mod_sql_passwd:mod_sftp:mod_sftp_sql:mod_sftp_pam:mod_tls:mod_tls_shmcache:mod_tls_memcache:mod_ban:mod_copy:mod_ctrls_admin:mod_deflate:mod_dnsbl:mod_dynmasq:mod_exec:mod_facl:mod_geoip:mod_ifversion:mod_ldap:mod_load:mod_log_forensic:mod_quotatab:mod_quotatab_file:mod_quotatab_sql:mod_radius:mod_readme: [...]
+  - ./configure LIBS="-lodbc -lm -lrt -pthread" --enable-devel --enable-ctrls --enable-dso --enable-facl --enable-memcache --enable-nls --enable-pcre --enable-tests --with-shared=mod_sql:mod_sql_mysql:mod_sql_odbc:mod_sql_postgres:mod_sql_sqlite:mod_sql_passwd:mod_sftp:mod_sftp_sql:mod_sftp_pam:mod_tls:mod_tls_fscache:mod_tls_shmcache:mod_tls_memcache:mod_ban:mod_copy:mod_ctrls_admin:mod_deflate:mod_dnsbl:mod_dynmasq:mod_exec:mod_facl:mod_geoip:mod_ifversion:mod_ldap:mod_load:mod_log_for [...]
   - make
+  - (cd contrib/dist/travis && sudo docker run -e PACKAGE_VERSION=${PACKAGE_VERSION} -e TRAVIS_BRANCH=${TRAVIS_BRANCH} -v `pwd`:`pwd` -w `pwd` centos:centos7 /bin/bash `pwd`/docker-rpmbuild.sh)
+
+after_success:
+  - cd ${TRAVIS_BUILD_DIR}
+  # capture the test coverage info
+  - lcov --ignore-errors gcov,source --directory . --capture --output-file coverage.info
+  # filter out system and test code
+  - lcov --remove coverage.info 'lib/glibc-glob.*' 'lib/ccan-json.*' 'lib/hanson-tpl.*' 'lib/pr_fnmatch_loop.*' 'tests/*' '/usr/*' --output-file coverage.info
+  # debug before upload
+  - lcov --list coverage.info
+  # upload coverage info to coveralls
+  - coveralls-lcov coverage.info
+  # Run some validation on the docs
+  - for f in `/bin/ls doc/contrib/*.html doc/howto/*.html doc/modules/*.html doc/utils/*.html`; do echo "Processing $f"; tidy -errors -omit -q $f; done
diff --git a/CREDITS b/CREDITS
index ea3c9fb..f7b4219 100644
--- a/CREDITS
+++ b/CREDITS
@@ -17,6 +17,12 @@ beautification by scripts.  The fields are: name (N), email (E), web-address
 address (S).
 
 --
+N: TJ Saunders
+E: tj at castaglia.org
+W: http://www.castaglia.org/
+P: 1024/A511976A 69 7E 68 4D 16 68 D6 96  84 28 40 5C B7 8E 89 3F  A5 11 97 6A
+D: ProFTPD developer, current maintainer
+
 N: Jesse Sipprell
 E: jss at inflicted.net
 P: 1024/A2C5F611 75 AA 73 4F BE 30 7E 33  48 F4 25 C3 C6 D5 6E D0
@@ -32,25 +38,19 @@ N: Mark Lowes
 E: hamster at vom.org.uk
 W: http://www.korenwolf.net/
 P: 1024D/31A56701 0CA5 442E CED1 B6D3 2211  64FA BCB0 2153 31A5 6701
-D: Head documentation hacker and webmaster
-
-N: TJ Saunders
-E: tj at castaglia.org
-W: http://www.castaglia.org/
-P: 1024/A511976A 69 7E 68 4D 16 68 D6 96  84 28 40 5C B7 8E 89 3F  A5 11 97 6A
-D: ProFTPD developer, current maintainer
+D: Former project documentation hacker and webmaster
 
 N: John Morrissey
 E: jwm at horde.net
 W: http://horde.net/~jwm/
 P: 1024D/DB06E596 8D29 BFAE 4CD4 C73B E6FB  91D9 13F7 7892 DB06 E596
-D: ProFTPD developer
+D: Original mod_ldap author and former project developer
 
 N: Charles Seeger
 E: seeger at cise.ufl.edu
 W: http://www.cise.ufl.edu/~seeger/
 P: 1024/70D9EFA9 CD 9C F4 28 FD A4 C8 FF  2A 8A 0D DB E6 0B 17 CC
-D: ProFTPD developer
+D: Former project developer
 
 N: Daniel Roesen
 E: droesen at entire-systems.com
@@ -59,4 +59,4 @@ D: RPM packaging maintainer
 
 N: Andrew Houghton
 E: aah at acm.org
-D: SQL support maintainer
+D: Former mod_sql maintainer
diff --git a/ChangeLog b/ChangeLog
index c428735..2e936ee 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -5,733 +5,6779 @@
 
 2016-03-10  TJ Saunders <tj at castaglia.org>
 
+	* README.md: Update link to shields.io badge for latest release.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* README.md, contrib/dist/rpm/proftpd.spec, include/version.h: 
+	Updating files for 1.3.6rc3 development.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
 	* NEWS: Update NEWS with release date.
 
 2016-03-10  TJ Saunders <tj at castaglia.org>
 
-	* contrib/dist/rpm/proftpd.spec: Update rpm .spec file for 1.3.5b
-	release.
+	* include/version.h: Preparing for RC2 release.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/rfc4716.c,
+	tests/t/etc/modules/mod_sftp/authorized_rsa_keys_no_nl,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Reproduce, and fix,
+	the case where mod_sftp does not handle public key files which do
+	not end in a newline.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with fix for Bug#4230.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #226 from proftpd/tls-dhparam-ignored-bug4230 Bug#4230: TLSDHParamFile directive appears ignored because
+	unexpected…
 
 2016-03-10  TJ Saunders <tj at castaglia.org>
 
-	* RELEASE_NOTES: Preparing release notes for a 1.3.5b release.
+	* modules/mod_core.c,
+	tests/t/lib/ProFTPD/Tests/Config/DisplayQuit.pm: If DisplayQuit is
+	configured with a non-path, make sure we handle it properly.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c,
+	tests/t/lib/ProFTPD/Tests/Config/DisplayFileTransfer.pm: If the
+	configured DisplayFileTransfer file is not able to be handled
+	successfully, don't penalize/close the data transfer connection for
+	it.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_wrap.c: Sigh.  Declared right variable in wrong
+	location.  Trying again.
 
 2016-03-10  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c: Fix build errors; used wrong variable name, and
-	pushed without building.  Shame.
+	* : Merge pull request #228 from proftpd/json-no-stdbool-dependency Remove the JSON implementation's dependence on stdbool.h, on the
+	assu…
 
 2016-03-10  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, contrib/mod_tls.c: Backport of fix for Bug#4230 to 1.3.5
-	branch.
+	* doc/modules/mod_core.html: Fix the documented default
+	TimeoutLinger value.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_wrap.c: Address compiler warnings.
+
+2016-03-10  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_facl.c: Make sure the module-unload event listener is
+	conditionally compiled with the same conditions as its usage.
+
+2016-03-09  TJ Saunders <tj at castaglia.org>
+
+	* : commit f9e85236e76703303ecad3c16951ac623ec34e5a Author: TJ
+	Saunders <tj at castaglia.org> Date:   Wed Mar 9 16:10:11 2016 -0800
+
+2016-03-09  TJ Saunders <tj at castaglia.org>
+
+	* src/netacl.c, tests/api/netacl.c: Adding tests showing that we
+	properly handle "0.0.0.0/0" as a netacl expression.
+
+2016-03-08  TJ Saunders <tj at castaglia.org>
+
+	* : commit 35455b027ba9a315109035e117bd4890781ee489 Merge: 57ef0cd
+	d49387e Author: TJ Saunders <tj at castaglia.org> Date:   Tue Mar 8
+	23:04:55 2016 -0800
+
+2016-03-08  TJ Saunders <tj at castaglia.org>
+
+	* include/netacl.h, src/netacl.c, tests/api/netacl.c: Provide a
+	pr_netacl_get_str2() function for getting string versions of ACLs
+	without the added description that pr_netacl_get_str() adds.
+
+2016-03-08  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES: Fleshing out the release notes, as we're getting
+	very close to release.
+
+2016-03-08  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_log.html: Add docs for the added %{file-size} and
+	%{transfer-type} LogFormat variables.
+
+2016-03-08  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Bug#4230: TLSDHParamFile directive appears
+	ignored because unexpected DH is chosen.
+
+2016-03-04  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_auth_otp.html: Minor updates/fixes to the
+	mod_auth_otp docs.
+
+2016-03-04  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_auth_otp.html: Add required OpenSSL attributions.
+
+2016-03-04  TJ Saunders <tj at castaglia.org>
+
+	* include/ccan-json.h, lib/ccan-json.c: Attempt to fix/address
+	compiler errors on different platforms (e.g. HP-UX) by not reusing
+	the same name for a struct and its typedef.  Also avoid
+	re-typedef'ing uchar_t.
+
+2016-03-04  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c, contrib/mod_sftp/scp.c: Clear the internal
+	statcache in a few more places when handling SFTP, SCP.
+
+2016-03-04  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, modules/mod_site.c, modules/mod_xfer.c: More
+	places where the internal cache should be cleared first.
+
+2016-03-04  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Another place where we need to clear the
+	internal statcache.
+
+2016-02-29  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #223 from proftpd/core-str-hex2bin Add symmetric String API function for converting from hex string to
+	b…
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ctrls.c: If there are issues creating the
+	ControlsSocket, log them at the NOTICE level, rather than in a
+	ControlsLog (which not be opened anyway).
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_statcache.c, src/fsio.c: When the pr_fs_clear_cache2()
+	function is used to clear the statcache, generate an event for any
+	OTHER modules which might also be doing caching (e.g.
+	mod_statcache), so that they too can clear their cached stat data.
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_statcache.c: Make mod_statcache less intrusive when it
+	encounters an error when handling an FSIO callback; just return the
+	underlying OS call return value.
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_statcache.c: The FSIO open(2) callback in
+	mod_statcache was only clearing the cached value IFF open(2)
+	returned zero.  But open(2) returns zero OR GREATER on success.
+	Oops.
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Cache the close(2) errno value, to avoid it being
+	changed by later code in pr_fsio_close().
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_unique_id.html: Fix HTML markup.
+
+2016-02-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Add a fake 451 response code for aborted
+	SFTP file transfers.
+
+2016-02-23  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_statcache.c: Automatically disable negative caching
+	for SSH2 sessions.
 
 2016-02-23  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Adding mention of backport of fix for Bug#4227.
+	* NEWS: Add mention of fix for Bug#4227.
+
+2016-02-23  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #220 from proftpd/sftp-multi-init-bug4227 Bug#4227: Support SFTP clients that send multiple INIT requests.
 
 2016-02-22  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Bug#4227: Support SFTP clients that send
-	multiple INIT requests.
+	* contrib/mod_sftp/fxp.c: Only increment the bytes transferred
+	values, when handling an SFTP WRITE request, once the bytes have
+	been successfully written, per write(2) result.
 
-2016-01-26  TJ Saunders <tj at castaglia.org>
+2016-02-22  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS with backport of fix for Bug#4223.
+	* : Merge pull request #219 from proftpd/sftp-no-stat-on-read Attempt to mitigate the issue with mod_statcache's stat() causing
+	SFT…
 
-2016-01-26  TJ Saunders <tj at castaglia.org>
+2016-02-22  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_xfer.c: Bug#4223: Permissions on files uploaded via
-	STOU do not honor configured Umask.
+	* contrib/mod_statcache.c: Improve mod_statcache trace logging, to
+	better differentiate among locking-related log messages.
 
-2016-01-15  TJ Saunders <tj at castaglia.org>
+2016-02-21  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4217.
+	* RELEASE_NOTES, contrib/mod_tls.c, doc/contrib/mod_tls.html: Use a
+	directive name of TLSServerInfoFile, for consistency with other
+	mod_tls directives.
 
-2016-01-15  TJ Saunders <tj at castaglia.org>
+2016-02-20  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_auth.c: Address Bug#4217, as requested, to be slightly
-	more indicative, via response codes and messages, of the issue with
-	reauthentication.
+	* : Merge pull request #217 from proftpd/tls-serverinfo Add TLSServerInfo directive, for TLS ServerHello extensions
 
-2016-01-03  TJ Saunders <tj at castaglia.org>
+2016-02-20  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c: Per RFC 4217, Section 15.3, require
-	authentication before accepting the CCC command, to mitigate
-	anonymous clients from issuing AUTH/CCC in loops, tying up server
-	resources.
+	* RELEASE_NOTES, contrib/mod_tls.c, doc/contrib/mod_tls.html: 
+	Implement a TLSServerInfo directive, for configuring custom TLS
+	extension data (e.g. for SCT data).
 
-2015-12-17  TJ Saunders <tj at castaglia.org>
+2016-02-17  TJ Saunders <tj at castaglia.org>
 
-	* Make.rules.in, lib/Makefile.in, modules/Makefile.in,
-	src/Makefile.in, src/ftpscrub.c, utils/Makefile.in: Backport the fix
-	for the 'make dist' target to the 1.3.5 branch.
+	* contrib/mod_exec.c, contrib/mod_sftp/keys.c, contrib/mod_tls.c,
+	modules/mod_rlimit.c, src/rlimit.c: Don't log spurious log messages
+	if getrlimit(2) fails with ENOSYS/EPERM.
 
-2015-12-14  TJ Saunders <tj at castaglia.org>
+2016-02-16  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS with backport of fix for Bug#4212.
+	* contrib/mod_sftp/blacklist.c: Quell Coverity warning by setting an
+	upper bound on the number of records we are willing to scan/process
+	from the blacklist.dat file.
 
-2015-12-11  TJ Saunders <tj at castaglia.org>
+2016-02-16  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_facts.c: Make sure we handle the same issue for MLSD
-	commands, too.
+	* lib/libltdl/ltdl.c: Quell Coverity warning about possible resource
+	leak in libltdl.
 
-2015-12-11  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_core.c, modules/mod_ls.c, src/data.c: Make sure that
-	the port number logged in the "RootRevoke in effect" messages is
-	accurate.  Enforce the case where RootRevoke is on, but the client has ignored
-	our error responses to PORT/EPRT, and had requested a data transfer
-	anyway.  We do this by returning an error response for the data
-	transfer command (e.g. LIST, STOR, etc) before we even attempt to
-	make the connection.
+	* contrib/mod_sftp/keys.c, contrib/mod_tls.c: Ensure proper
+	NUL-termination of buffers in all cases.  Should make Coverity
+	happier.
 
-2015-12-11  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_ldap.c: Fix mod_ldap build issue when building against
-	e.g. OpenLDAP-2.3.x.
+	* contrib/mod_auth_otp/base32.c: Use signed data types, to prevent
+	underflow and bad shift operations.  Found by Coverity.
 
-2015-12-10  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_auth.c: Provide slightly more informative message
-	about active data transfers being affected by "RootRevoke on".
+	* contrib/mod_statcache.c: Add check for return value, per Coverity.
 
-2015-12-10  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* lib/tpl.c: Remove unused variables and prevent compiler warnings
-	on Mac OSX (and probably others).
+	* contrib/mod_auth_otp/mod_auth_otp.c: Fix issue discovered by
+	Coverity.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/kbdint.c: Remove now-unneeded cast.
+	* contrib/mod_digest.c: Fix issue detected by Coverity.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Updated NEWS with backport of fix for Bug#4210.
+	* doc/contrib/mod_auth_otp.html, doc/contrib/mod_digest.html,
+	doc/contrib/mod_statcache.html: Update docs of new modules to
+	mention where their source code is.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Correct the parameters to talk of
-	"extended attributes", not SFTP extensions.
+	* : Merge pull request #214 from proftpd/contrib-statcache Adding mod_statcache to the contrib/ directory.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Bug#4210 - Avoid unbounded SFTP extension
-	key/values.
+	* .gitignore, .travis.yml, contrib/mod_auth_otp/mod_auth_otp.c: 
+	Address build warnings, tweak the travis-ci setup.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Backport of fix for Bug#4209 to 1.3.5 branch.
+	* contrib/mod_auth_otp/Makefile.in: Trying some reordering, when
+	building mod_auth_otp as a DSO module.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-12  TJ Saunders <tj at castaglia.org>
 
-	* src/pool.c: Bug#4209 - Zero-length memory allocation possible,
-	with undefined results.
+	* contrib/mod_digest.c: Fix building of mod_digest as a shared
+	module.
 
-2015-11-28  TJ Saunders <tj at castaglia.org>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/kbdint.c, contrib/mod_sftp/mod_sftp.h.in,
-	contrib/mod_sftp_pam.c: Avoid possible signedness mismatch when
-	comparing the expected/received number of SSH keyboard-interactive
-	challenge responses.
+	* contrib/mod_auth_otp/Makefile.in: Back to the original.  Probably
+	won't work.
 
-2015-08-26  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, modules/mod_facts.c: Backport of fix for Bug#4202 to 1.3.5
-	branch.
+	* contrib/mod_auth_otp/Makefile.in: Still correcting paths.  Sigh.
 
-2015-08-18  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, contrib/mod_sftp/scp.c: Backporting the fix for Bug#4201 to
-	the 1.3.5 branch.
+	* contrib/mod_auth_otp/Makefile.in,
+	contrib/mod_auth_otp/mod_auth_otp.c: Still trying to get the
+	travis-ci builds passing.
 
-2015-08-15  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* configure, configure.in: Backporting configure changes from
-	master, for detecting when to add the -pthread flag, e.g. when using
-	OpenSSL on FreeBSD.
+	* contrib/mod_auth_otp/Makefile.in: Tweak, tweak.
 
-2015-08-15  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/utf8.c, src/encode.c, src/inet.c, src/support.c: 
-	Backporting fixes/changes for DARWIN12 from master to 1.3.5.
+	* contrib/mod_auth_otp/Makefile.in: Still working on fixing the
+	travis-ci build.
 
-2015-08-12  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* src/main.c: Backporting errno handling fix from master.
+	* contrib/mod_auth_otp/t/Makefile.in,
+	contrib/mod_auth_otp/t/api/base32.c,
+	contrib/mod_auth_otp/t/api/otp.c,
+	contrib/mod_auth_otp/t/api/stubs.c,
+	contrib/mod_auth_otp/t/api/tests.c,
+	contrib/mod_auth_otp/t/api/tests.h: Add missing API tests for
+	mod_auth_otp, breaking the travis-ci build.
 
-2015-08-12  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* src/main.c: Make sure to clear out any cached errno, too.
+	* .travis.yml: Add mod_auth_otp, mod_digest to the travis-ci builds.
 
-2015-08-12  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* src/main.c: Make sure to clear any possibly-cached errno value in
-	the main select() loop.
+	* : Merge pull request #213 from proftpd/contrib-auth-otp Adding mod_auth_otp to the contrib/ modules directory.
 
-2015-08-09  tjsaunders <tj at lyveminds.com>
+2016-02-11  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/kex.c: Backport a tweak from master, with regard
-	to the timing of logging of the NEWKEYS command.
+	* : Merge pull request #212 from proftpd/contrib-digest Adding mod_digest module to contrib modules.
 
-2015-08-06  tjsaunders <tj at lyveminds.com>
+2016-02-01  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, modules/mod_facts.c: Bug#4198 - MLSD/MLST fact type "cdir"
-	is incorrectly used for the current working directory.  Conflicts: 	modules/mod_facts.c
+	* contrib/mod_sql.c: Log a warning about using the Plaintext
+	SQLAuthType; it's a bad security policy, as it allows storing
+	passwords IN THE CLEAR in the database.
 
-2015-07-28  tjsaunders <tj at lyveminds.com>
+2016-02-01  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, contrib/mod_sftp/kex.c, contrib/mod_sftp/keys.c: Backport of
-	the fix for Bug#4097 to the 1.3.5 branch.
+	* RELEASE_NOTES, contrib/mod_sql.c, include/mod_log.h,
+	modules/mod_log.c, tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: 
+	Add a %{transfer-type} LogFormat variable; this is just one more
+	variable needed to make an ExtendedLog that contains the same
+	information as the fixed-format TransferLog.
 
-2015-07-25  tjsaunders <tj at lyveminds.com>
+2016-02-01  TJ Saunders <tj at castaglia.org>
 
-	* src/fsio.c, tests/api/fsio.c: If either of the arguments to
-	pr_fs_dircat() are empty strings, be consistent and append a "/"
-	character, just as is done when BOTH arguments are empty strings.
+	* contrib/mod_wrap.c, modules/mod_core.c: Update mod_wrap to
+	properly handle a HOST command.
 
-2015-07-25  tjsaunders <tj at lyveminds.com>
+2016-02-01  TJ Saunders <tj at castaglia.org>
 
-	* src/fsio.c, tests/api/fsio.c: Bug#4194: Add more checks for empty
-	paths in pr_fs_dircat().
+	* contrib/mod_quotatab.c, modules/mod_core.c: Update mod_quotatab to
+	Do The Right Thing(tm) in the face of HOST commands.
 
-2015-07-25  tjsaunders <tj at lyveminds.com>
+2016-02-01  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_wrap2/mod_wrap2.c, modules/mod_core.c: Update
+	mod_wrap2 to properly handle HOST commands.
+
+2016-02-01  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_radius.c, contrib/mod_rewrite.c, modules/mod_core.c: 
+	Update the mod_radius and mod_rewrite modules to properly
+	respond/handle HOST commands.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ldap.c, modules/mod_core.c: Make mod_ldap be
+	HOST-aware, and reset its settings as necessary.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_passwd.c, modules/mod_core.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm: Make sure that
+	mod_sql_passwd works properly with respect to HOST commands.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c: Update mod_sql so that it Does The Right
+	Thing(tm) for HOST commands.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* src/auth.c: Correct grammar in a trace log message; no functional
+	change.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Update the list of modules needing HOST
+	handling work.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth.c: Remove now-obsolete POST_CMD HOST handler; it
+	was replaced by the 'core.session-reinit' event listener.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth.c,
+	tests/t/lib/ProFTPD/Tests/Config/AnonAllowRobots.pm: Fix edge case
+	with implementation of Bug#4224, where we do NOT want any automatic
+	handling of "robots.txt" for non-anonymous logins.
+
+2016-01-31  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #210 from
+	proftpd/auth-anon-allow-robots-bug4224 Bug#4224: Prohibit FTP indexing by web crawlers via auto-generated
+	ro…
+
+2016-01-30  TJ Saunders <tj at castaglia.org>
+
+	* locale/files.txt: Updated files.txt.
+
+2016-01-30  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_quotatab.html: Fix markup.
+
+2016-01-29  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES, doc/modules/mod_auth.html,
+	modules/mod_auth.c, tests/t/config/anonallowrobots.t,
+	tests/t/lib/ProFTPD/Tests/Config/AnonAllowRobots.pm: Bug#4224:
+	Prohibit FTP indexing by web crawlers via auto-generated robots.txt.
+
+2016-01-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Properly shut down the TLS session on data
+	connections as well.
+
+2016-01-28  TJ Saunders <tj at castaglia.org>
+
+	* doc/rfc/rfc7151.txt: Adding RFC 7151 to our RFCs area, since we
+	now implement the HOST command.
+
+2016-01-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #209 from proftpd/exists2 Restore backward compatibility
+
+2016-01-28  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_copy.c, contrib/mod_sftp/fxp.c,
+	contrib/mod_sftp/mod_sftp.c, contrib/mod_sftp/scp.c,
+	contrib/mod_sql.c, contrib/mod_tls.c, include/support.h,
+	modules/mod_core.c, modules/mod_ls.c, modules/mod_xfer.c,
+	src/support.c, tests/api/misc.c: Restore backward compatibility by
+	restoring the Support API's exists/mode functions back to their
+	original signatures, and provide variants, which accept a pool, for
+	the chroot+symlink-aware functionality.
+
+2016-01-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #208 from proftpd/extlog-file-size-var-redux Use POST_CMD handlers for stashing the 'mod_xfer.file-size' note, so
+	…
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #207 from proftpd/extlog-file-size-var Add support for a %{file-size} LogFormat variable, which resolves to
+	the
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Config/AllowForeignAddress.pm: Add
+	regression test for a site-to-site transfer of a >2GB file.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for fix for Bug#4219.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #206 from
+	proftpd/fsio-chrooted-readlink-bug4219 Bug#4219: Better handling of symlinks when chrooted
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_site.c, tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm: 
+	Added regression tests for SITE CHMOD commands and symlinks,
+	chrooted and not.  All tests pass properly now.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_site.c, tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm: 
+	Added regression tests for the SITE CHGRP command and symlinks,
+	chrooted and not.  All tests pass now.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/scp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for SCP downloads to symlinks, chrooted and not.  Made sure
+	the tests pass as expected.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* : commit 5dc63cc755fbc7fde7757812678b4d02eb54a654 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Wed Jan 27 11:15:37 2016 -0800
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Add comment about where to find the definition
+	of unknown TLS extension IDs.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_mysql.c: If mod_sql_mysql fails to configure the
+	character set, just log the failure and do not treat it as a fatal
+	error, in order to better handle situations of client/server version
+	skew.
+
+2016-01-27  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/Logging.html: Mention that FreeBSD ftpd does NOT use the
+	xferlog(5) format.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* src/inet.c, src/support.c: Add Mac OSX-specific version macros for
+	the newer OSX versions as needed.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* : commit 662a109c354eb72b9f138e1b332ba3fd3e088b88 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Tue Jan 26 16:54:44 2016 -0800
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/STOU.pm: Add another regression
+	test for Bug#4223, for the chroot case.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c: Style nit; no functional change.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Add the dir_readlink() magic to the
+	check-file SFTP extension handling.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for SFTP RMDIR requests and symlinks, chrooted and not.
+	Ensured that all tests pass as expected.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for SFTP OPENDIR requests and symlinks, chrooted and not.
+	Made sure the tests pass as expected.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for SFTP SETSTAT requests and symlinks, chrooted and not.
+	Made sure that the tests pass properly.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for the SFTP STAT requests and symlinks, chrooted and not, and
+	made sure the tests pass.
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* : commit 9a17b90289f876339b4cf71f74329a2d5fa3c4f9 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Tue Jan 26 10:47:03 2016 -0800
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #205 from proftpd/xfer-stou-umask-bug4223 Bug#4223: Permissions on files uploaded via STOU do not honor
+	configured Umask
+
+2016-01-26  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c: Bug#4223: Permissions on files uploaded via
+	STOU do not honor configured Umask.
+
+2016-01-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for the SFTP OPEN requests and symlinks, chrooted and not.
+
+2016-01-25  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Added regression
+	tests for SFTP MKDIR requests and symlinks, chrooted and not.
+
+2016-01-25  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/RNTO.pm: Added regression tests
+	for the RNTO command, and symlinks.
+
+2016-01-25  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm: 
+	Adding regression tests for the RNFR command and symlinks, chrooted
+	and not.
+
+2016-01-24  TJ Saunders <tj at castaglia.org>
+
+	* : commit f8eded5488c03a40a02dbdb4056d6222e0a045a3 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Jan 24 20:24:44 2016 -0800
+
+2016-01-24  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #204 from proftpd/default-transfer-mode Move the processing of the DefaultTransferMode directive from
+	mod_aut…
+
+2016-01-24  TJ Saunders <tj at kiban.io>
+
+	* modules/mod_auth.c, modules/mod_core.c: Move the processing of the
+	DefaultTransferMode directive from mod_auth to mod_core.  The
+	directive handler is in mod_core, and the processing is more
+	properly done as a post-PASS step.
+
+2016-01-24  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, modules/mod_xfer.c,
+	tests/t/lib/ProFTPD/Tests/Commands/RMD.pm: Added regression tests
+	for RMD and symlinks, and made sure that symlinks could be handled
+	properly when chrooted.
+
+2016-01-24  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c, tests/t/lib/ProFTPD/Tests/Commands/RETR.pm: 
+	Added regression tests for RETR and symlinks, and updated the RETR
+	handling to use dir_readlink(), so that symlinks can be retrieved
+	when chrooted.
+
+2016-01-24  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, tests/t/lib/ProFTPD/Tests/Commands/CWD.pm: 
+	Added regression tests for the CWD and symlinks, and made sure that
+	CWD does The Right Thing(tm) with symlinks, chrooted and not.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* : commit e09990a3840ddca4413160c86ab0eff403c3a698 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Fri Jan 22 16:09:02 2016 -0800
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Fix log messages to be grammatically correct.
+
+2016-01-22  TJ Saunders <tj at kiban.io>
+
+	* src/str.c, tests/api/str.c: For better logging/debugging, have the
+	{uid,gid}2str() functions return "-1" for cases where the
+	uid_t/gid_t have that value, as they are used as sentinel values in
+	the core code.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/MKD.pm: Updated MKD regression
+	tests to use test_setup()/test_cleanup(), in preparation to adding
+	more symlink-related regression tests.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm: 
+	Update the handling of the MDTM command for chrooted symlinks, with
+	accompanying regression tests.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm: First, update the MDTM
+	regression tests to use test_setup() and test_cleanup().  Next, I'll
+	add regression tests for MDTM on symlinks (to files and
+	directories).
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_copy.c: Found another place needing updating for the
+	change of exists() signature.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c, contrib/mod_tls.c: Updated some contrib
+	modules, due to changes in the Support API signatures.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/APPE.pm: Make sure that the
+	destination path correctly lines up with the (adjusted) chroot on
+	MacOSX.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c, contrib/mod_sftp/mod_sftp.c,
+	contrib/mod_sftp/scp.c, include/support.h, modules/mod_core.c,
+	modules/mod_ls.c, modules/mod_xfer.c, src/support.c,
+	tests/api/misc.c, tests/t/lib/ProFTPD/Tests/Commands/APPE.pm: Added
+	more regression tests for the APPE command, for absolute destination
+	path symlinks and chroots, which required some refactoring of the
+	core library.
+
+2016-01-22  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/ListOptions.html, doc/modules/mod_facts.html,
+	doc/modules/mod_ls.html: Update docs to mention new
+	'NoAdjustedSymlinks' options for mod_facts, mod_ls.
+
+2016-01-22  TJ Saunders <tj at kiban.io>
+
+	* doc/howto/CreateHome.html: Updated links to the main docs for the
+	CreateHome directive.
+
+2016-01-21  TJ Saunders <tj at kiban.io>
+
+	* contrib/mod_sftp_pam.c: Now the mod_sftp_pam module will check for
+	the AuthPAM directive, in addition to SFTPPAMEngine, to better work,
+	as expected, with existing proftpd configurations that use AuthPAM.  See:
+	http://serverfault.com/questions/386294/proftpd-with-sftp-first-login-fails-seconds-succeeds
+
+2016-01-21  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/DELE.pm: Modernized some of the
+	DELE regression tests in preparation, but then realized that DELE's
+	handling of symlinks just does The Right Thing(tm), regardless of
+	chroots.
+
+2016-01-21  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/APPE.pm: Adding APPE regression
+	tests for symlinks, chrooted and not.
+
+2016-01-21  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c: Rename a variable to be more consistent with
+	usage elsewhere, i.e. using 'xerrno' as the errno holder, not
+	'ferrno'.
+
+2016-01-21  TJ Saunders <tj at kiban.io>
+
+	* include/ftp.h: Remove redundant redefinition of the C_HOST macro.
+
+2016-01-21  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/LIST.pm: Adding regression
+	tests for the LIST command, with regard to symlinks, ShowSymlinks,
+	and chroots.
+
+2016-01-21  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/NLST.pm: Added NLST regression
+	tests for symlinks, and symlinks when chrooted.
+
+2016-01-20  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c, tests/api/misc.c: Clean up the returned destination
+	paths, even when not chrooted.
+
+2016-01-20  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/STAT.pm: Updated the STAT
+	regression tests, and added missing tests for symlinks, both when
+	chrooted and not.
+
+2016-01-20  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Fix the broken unit tests.
+
+2016-01-20  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_facts.c, src/support.c, tests/api/misc.c,
+	tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/MLST.pm: Update mod_facts to use
+	dir_readlink() for MLST/MLSD commands.  Turns out that this fixes an
+	already-existing bug, where symlinks were not being handled well
+	when the session is chrooted anyway.
+
+2016-01-20  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Remove some distracting FSIO statcache trace logging
+	by explicitly setting errno to zero for success cases.
+
+2016-01-20  TJ Saunders <tj at castaglia.org>
+
+	* src/dirtree.c: Move some noisy AllowOverride logging to trace
+	logging, from debug logging.
+
+2016-01-19  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Update mod_sftp's
+	handling of the READLINK request to use the new chroot-aware
+	dir_readlink() function.
+
+2016-01-19  TJ Saunders <tj at castaglia.org>
+
+	* : commit 632cc9dd493e263f66fb42dd54809cb3f671b17b Merge: d55f831
+	5fd7508 Author: TJ Saunders <tj at castaglia.org> Date:   Tue Jan 19
+	09:56:04 2016 -0800
+
+2016-01-19  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: Make sure that
+	mod_sql can also handle the new %R, %{transfer-millisecs} variables
+	from Bug#4218.
+
+2016-01-19  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #202 from proftpd/privs-coverage Increase test coverage of the Privs API.
+
+2016-01-19  TJ Saunders <tj at castaglia.org>
+
+	* include/privs.h, src/privs.c, tests/api/privs.c: Attempt to
+	increase test coverage of the Privs API by providing a function for
+	setting the "am I running as nonroot?" flag, to be used by unit
+	tests.
+
+2016-01-19  TJ Saunders <tj at castaglia.org>
+
+	* include/support.h, src/support.c, tests/api/misc.c: Provide flags,
+	for handling relative destination symlink paths (or not).  Update
+	the unit tests to cover the new branches.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c, tests/api/misc.c: If dir_readlink() reads in a
+	relative path, and that path falls within the chroot, try to
+	emit/provide a relative path as output, in order to preserve the
+	principle of least surprise.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* include/support.h, src/support.c, tests/api/misc.c: First part of
+	Bug#4219: provide a support function which wraps pr_fsio_readlink(),
+	and does the chroot checking/adaptation.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Handle the edge case where a custom FSIO object does
+	not provide a readlink implementation, and that callback is thus
+	NULL.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_snmp.pm: Update the mod_snmp
+	tests to pass properly, when more contrib modules (e.g.  mod_tls,
+	mod_sftp, mod_ban) are present.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_snmp/mod_snmp.c: Remove some logically unneeded code,
+	per Coverity.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_snmp/PROFTPD-MIB.txt: Remove CVS tag, now that we're
+	in Git.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Ignore the Coverity cov-int directory, too.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ctrls.c: Avoid unnecessary (and redundant) null check,
+	per Coverity.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* src/dirtree.c: Catch/handle a possible null pointer dereference,
+	per Coverity.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_passwd.c: Pedantically check for possible null
+	values, per Coverity.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_fscache.c: Implement permission checks on the
+	directory for caching OCSP responses, and avoid world-writable
+	directories.
+
+2016-01-18  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_fscache.c: Be a little more strict when checking
+	permissions for cached OCSP responses, by calling open(2) on the
+	path, then using fstat(2), rather than just using lstat(2).  This
+	should make Coverity happier.  Hopefully.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Updates NEWS, release notes with mention of
+	Bug#4218.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #201 from
+	proftpd/extlog-response-transfer-millisecs-bug4218 Bug#4218: Support a LogFormat variable for logging command duration
+	in milliseconds
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_log.html, include/mod_log.h, modules/mod_log.c,
+	tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: Implement the
+	%{transfer-millisecs} LogFormat variable, with regression test.  In
+	doing so, the implementation of the %T LogFormat variable was made
+	much simpler.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: First step: add
+	missing regression test for the %T LogFormat variable, to be used as
+	a template for the next part.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Now we're playing with platform-specific
+	behaviors.  Whee!
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/str.c: Quell compiler warning about mismatched types.
+
+2016-01-17  TJ Saunders <tj at kiban.io>
+
+	* include/support.h, modules/mod_ls.c, src/support.c,
+	tests/api/misc.c: Still working out the kinks with the rewritten
+	get_name_max().
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: The rewriting of get_name_max() changed the test
+	expectations.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c: Rewrite the get_name_max() function to be more
+	legible, and avoid (hopefully) the rats' nest of ifdefs.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* src/netio.c: At least check if our stashing of the NetIO in the
+	notes fails, per Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ls.c: Avoid memory leak under error conditions; found
+	by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Avoid memory leak in edge case scenario; found
+	by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Avoid an unneeded loop, which avoids logically
+	dead code.  Found by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_dso.c: Avoid checking a pointer that we already know
+	to be null (due to earlier checks), per Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* src/log.c: Be more paranoid about checking for null pointers, per
+	Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_exec.c: It's possible for exec_subst_var() to be
+	called with a null cmd_rec, thus we need to guard against such
+	conditions.  Found by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Remove unused code, detected by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/coverity/modeling.c: Update the Coverity modeling file, to
+	squelch known false positives.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_fscache.c: Avoid passing a known null pointer, per
+	Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c: If pr_localtime() returns NULL, just bail, rather
+	than trying to skirt around the issue.  Found by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/interop.c: Avoid a possible null dereference,
+	found by Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* utils/ftpcount.c, utils/ftpscrub.c, utils/ftpwho.c: Add missing
+	break statements, per Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/keys.c: Remove extraneous null check, per
+	Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c: Avoid some dead/unreachable code, per Coverity.
+
+2016-01-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_memcache.c: Removing now-unused variables.
+
+2016-01-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #200 from proftpd/facts-media-type Update mod_facts to use mod_mime, when present/enabled, to provide
+	the
+
+2016-01-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #199 from
+	proftpd/quotatab-better-handling-of-default Tweak the lookup pattern mod_quotatab uses, with regard to the
+	QuotaD…
+
+2016-01-16  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/TLS.html: Adding FAQ about the TLS "unknown protocol"
+	error, from the forums.
+
+2016-01-15  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth.c: Make sure to validate the USER command first,
+	before any other special processing.
+
+2016-01-15  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for fix for Bug#4217.
+
+2016-01-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #198 from proftpd/auth-reauthenticate-bug4217 Bug#4217: Handle FTP re-authentication attempts better.
+
+2016-01-15  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth.c: Address Bug#4217, as requested, to be slightly
+	more indicative, via response codes and messages, of the issue with
+	reauthentication.
+
+2016-01-15  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Logins.pm: First part of working on
+	Bug#4217: make sure there is a regression test for the previous
+	behavior, for Bug#3736.
+
+2016-01-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #197 from proftpd/extlog-bug4067-redux Attempt to fix the exclusion LogFormat functionality introduced for
+	a…
+
+2016-01-14  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Fix bug where not providing all parameters for
+	FSCachePolicy might not have the expected behavior.
+
+2016-01-14  TJ Saunders <tj at castaglia.org>
+
+	* src/str.c: Quell a compiler warning about an unused function.
+
+2016-01-14  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in, contrib/mod_sftp/crypto.c,
+	contrib/mod_sql_postgres.c: Make sure that the build system
+	automatically enables OpenSSL support when mod_auth_otp or
+	mod_digest are requested.  Also update the lists of OpenSSL-using
+	modules elsewhere.
+
+2016-01-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Remove now-unused variable.
+
+2016-01-13  TJ Saunders <tj at castaglia.org>
+
+	* src/timers.c: Clean up stylistic nits, and leave more comments for
+	my future self.  This helped me track down a timer-related issue,
+	where the suggested "next timeout" calculation was too coarse, and
+	prone to cause unexpected behavior.  Hopefully better now.
+
+2016-01-13  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Set the module pointer for the TimeoutIdle
+	callback, for better trace logging.
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* src/data.c, src/netio.c: Fix the generation of network IO
+	read/write events, and refactor these events such that ALL of them
+	are generated by the NetIO API, not spread out amongst the NetIO and
+	data transfer routines.
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* src/auth.c: Include the number of items in the authcache, when we
+	flush it.
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* src/table.c: Minor simplification to the code for removing an
+	entry from a table.
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #196 from proftpd/sorted-feat Sorted FEAT response, for more professional look
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* src/table.c: Style nits; no functional change.
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Sort the FEAT response, for prettier, more
+	professional displays.
+
+2016-01-12  TJ Saunders <tj at castaglia.org>
+
+	* src/table.c: Style nits; no functional change.
+
+2016-01-11  TJ Saunders <tj at castaglia.org>
+
+	* src/response.c, tests/api/response.c: Minor improvement for
+	pr_response_send_async() by properly tracking the starting pointer
+	for sstrcat(), to avoid scanning bytes already concatenated.
+
+2016-01-11  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Make sure to include CLNT in the HELP output.
+
+2016-01-11  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #195 from proftpd/ssh2-on-ftp Detect SSH2 connections to an FTP port
+
+2016-01-10  TJ Saunders <tj at castaglia.org>
+
+	* : commit f70caf2b4c41f9a7fe4790d6d7a0c0650f35d6f9 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Jan 10 23:48:47 2016 -0800
+
+2016-01-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #194 from proftpd/support-clnt Support the CLNT command
+
+2016-01-10  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/FTP.html, include/cmd.h, include/ftp.h,
+	modules/mod_core.c, src/cmd.c, tests/api/cmd.c,
+	tests/t/commands/clnt.t, tests/t/lib/ProFTPD/TestSuite/FTP.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/CLNT.pm, tests/tests.pl: Support
+	the CLNT command.
+
+2016-01-10  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Ignoring a few more symlinks.
+
+2016-01-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_passwd.c, contrib/mod_tls.c,
+	contrib/mod_tls_memcache.c, contrib/mod_tls_shmcache.c,
+	include/str.h, src/str.c, tests/api/str.c: I decided that
+	pr_str_bin2hex() was a better function name.
+
+2016-01-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_passwd.c: Typo in comment; no functional change.
+
+2016-01-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #193 from proftpd/core-str-hex Add new `pr_str_hex()` function.
+
+2016-01-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c: Quell compiler warning.
+
+2016-01-08  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Adding a few more symlinks, generated by the build
+	system, to be ignored by git.
+
+2016-01-06  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Prevent infinite recursion, in lookup_file_fs(), by
+	using the fs object's readlink callback directly, rather than going
+	through the public API, which calls lookup_file_fs() -- hence the
+	recursion.
+
+2016-01-05  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #192 from proftpd/sql-odbc-issues SQL ODBC issues
+
+2016-01-05  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES, contrib/mod_sql_odbc.c,
+	doc/contrib/mod_sql_odbc.html,
+	tests/t/etc/modules/mod_sql_odbc/odbc.ini,
+	tests/t/etc/modules/mod_sql_odbc/odbcinst.ini,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm: While
+	investigating some mod_sql_odbc issues mentioned in the forums, I
+	discovered some cases where the ODBC API version needed to be
+	configurable.  Adding in the new config (and docs), and the
+	reproduction recipes.
+
+2016-01-05  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c: Minor nits; no functional change.
+
+2016-01-05  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Better name for
+	added testcase.
+
+2016-01-05  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Add regression test
+	ensuring that mod_sftp properly fails to login a client that fails
+	password authentication.
+
+2016-01-03  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm: Add regression test
+	for CCC before login.
+
+2016-01-03  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Per RFC 4217, Section 15.3, require
+	authentication before accepting the CCC command, to mitigate
+	anonymous clients from issuing AUTH/CCC in loops, tying up server
+	resources.
+
+2016-01-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #191 from proftpd/tls-verify-client-optional Implement "TLSVerifyClient optional", to replace functionality lost
+	when
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Mention fix for Bug#4214 in NEWS, release
+	notes.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #190 from
+	proftpd/lang-useencoding-per-user-bug4214 Bug#4214 - Allow UseEncoding to be set on a per-user basis.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in, modules/mod_lang.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm: Bug#4214 - Allow
+	UseEncoding to be set on a per-user basis.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* config.h.in, configure, configure.in, src/regexp.c: Add necessary
+	autoconf detection of pcre_free_study() function.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Apparently the travis-ci environment doesn't have
+	apt.  Weird.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Remove sending email notifications to -committers; it
+	doesn't appear to be working as expected.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Show the details of the libpcre3-dev package, to help
+	debug a build failure on travis-ci (but working locally).
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* src/regexp.c: Syntax tweak, more of a stylistic nit.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* src/regexp.c: We were missing an include of the PCRE headers.
+	Odd.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* src/auth.c: Quell compiler warning on travis-ci.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* src/regexp.c: Refactor some common code into a single routine, and
+	reduce needless duplication.
+
+2016-01-01  TJ Saunders <tj at castaglia.org>
+
+	* src/regexp.c: Make sure we explicitly call pcre_free_study(), to
+	free up any JIT-allocated memory used by pcre_study().
+
+2015-12-31  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Style nits; no functional change.
+
+2015-12-31  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes with mention of
+	Bug#4213.
+
+2015-12-31  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #189 from
+	proftpd/tls-nocertrequest-tlsoption-bug4213 Bug#4213 - Deprecate the NoCertRequest TLSOption.
+
+2015-12-27  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Updates NEWS, release notes with mention of
+	TLS session ticket support (Bug#4176).
+
+2015-12-27  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #188 from proftpd/tls-session-tickets-bug4176 TLS session ticket support (Bug#4176)
+
+2015-12-25  TJ Saunders <tj at castaglia.org>
+
+	* : commit 251611d8f6f579cb8eacc87ad525822b0fbed1c3 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Fri Dec 25 13:44:11 2015 -0800
+
+2015-12-25  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Add email notifications to the travis-ci builds.
+
+2015-12-25  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_tls.html: Documenting the TLS session ticket
+	support for Bug#4176.
+
+2015-12-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Bug#4176: Support for TLS session tickets.  Initial implementation, with some testing.  Needs documentation now.
+
+2015-12-23  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_sftp.html, doc/contrib/mod_tls.html: Remove some
+	confusing text about having multiple Options directives on the same
+	line.
+
+2015-12-23  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update news, release notes to mention
+	Bug#4200.
+
+2015-12-23  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #183 from proftpd/sql-tls-info-bug4200 Bug#4200 - Support TLS client configuration for SQL servers.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* include/compat.h: Provide a backward compatibility macro for
+	`_sql_make_cmd`, and remove some old backward compatibility macros
+	while there.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* : commit 39ee20cc2708bd8b7abd0c391e433f74858d2621 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Tue Dec 22 17:38:16 2015 -0800
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes with mention of
+	fix for Bug#4175.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #186 from proftpd/tls-ocsp-stapling-bug4175 Bug#4175: Support for OCSP stapling
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_memcache.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm: Implement an
+	OCSP response cache provider using mod_tls_memcache, with
+	accompanying regression tests.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_shmcache.c: Update the mod_tls_shmcache comments
+	to be more up-to-date.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_tls.html, doc/contrib/mod_tls_shmcache.html: 
+	Update the mod_tls_shmcache docs to mention its support for the
+	TLSStaplingCache directive.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_shmcache.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm: Implement an
+	OCSP response cache provider via SysV shared memory segments (using
+	mod_tls_shmcache), with accompanying tests.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_fscache.pm: Move one of
+	the OCSP stapling support routines to the mod_tls_fscache-specific
+	tests.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_fscache.c: Remove unused event listeners, and
+	update the logging of OpenSSL errors to include the finer-grained
+	data, if available.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: If the EnableDiags TLSOption is in effect,
+	print out the OCSP response we obtained from the TLSStaplingCache,
+	if any.  Also make sure that we do NOT add back to the
+	TLSStaplingCache a response that we just read from that cache.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_shmcache.c: Step #1 in adding OCSP cache support
+	to mod_tls_shmcache: rename all of the existing SSL session cache
+	variables/functions more clearly.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_fscache.pm,
+	tests/t/modules/mod_tls_fscache.t, tests/tests.pl: Split out the
+	mod_tls_fscache-related regression tests into their own test file,
+	just as was done for mod_tls_memcache and mod_tls_shmcache.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Ignore the new mod_tls_fscache symlinked into the
+	modules/ directory.
+
+2015-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_fscache.c: Finish implementing some of the
+	'ftpdctl ocspcache' actions for mod_tls_fscache.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_tls_fscache.html: Provide better example
+	configuration.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm: Remove development
+	cruft.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_tls.html, doc/contrib/mod_tls_fscache.html,
+	doc/contrib/mod_tls_memcache.html: Update the mod_tls docs for the
+	OCSP stapling support, and include the docs for the new
+	mod_tls_fscache module.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Implement a TLSStaplingResponder directive, for
+	sites which will need to proxy their OCSP queries.  Also remove
+	unnecessary defensive coding, after reading the OpenSSL sources.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_fscache.c: Be a little more careful with the OCSP
+	response files found on disk.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Refactor some of the previous OCSP code (for
+	checking client certificates' revocation status) to use the newer
+	code, and reduce the amount of code duplication.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Fix build errors, warnings caught by travis-ci.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm: Add
+	regression test demonstrating a mod_quotatab config that uses the
+	QuotaDefault directive.
+
+2015-12-21  TJ Saunders <tj at castaglia.org>
+
+	* : commit 8a9a719b595b31f061be46835a18f9b73c172625 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Mon Dec 21 15:24:27 2015 -0800
+
+2015-12-21  TJ Saunders <tj at kiban.io>
+
+	* contrib/mod_tls.c: Address Issue #185 by explicitly clearing the
+	passphrase list, even when there are no explicit passphrases for a
+	cert.
+
+2015-12-17  TJ Saunders <tj at castaglia.org>
+
+	* : commit 8787af68da750efcbe11ccee93df07a583e3ba89 Merge: f98a4fc
+	82a2fe1 Author: TJ Saunders <tj at castaglia.org> Date:   Thu Dec 17
+	14:06:01 2015 -0800
+
+2015-12-17  TJ Saunders <tj at castaglia.org>
+
+	* locale/files.txt: Updating the list of candidate files which might
+	contain localizable strings.
+
+2015-12-17  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #180 from proftpd/build-fix-dist-target Fix the 'make dist' target
+
+2015-12-17  TJ Saunders <tj at castaglia.org>
+
+	* : commit fd4afb392a2855a93ea2615f73215125e07db72c Author: TJ
+	Saunders <tj at castaglia.org> Date:   Thu Dec 17 13:34:26 2015 -0800
+
+2015-12-17  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #184 from proftpd/rpm-systemd-bug3661 Updated proftpd.spec file to include the systemd files, per
+	Bug#3661.
+
+2015-12-17  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Ignore symlinks generated by the build system, and
+	other auto-generated files.
+
+2015-12-16  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_memcache.c: Make the namespace for memcached
+	sessions more precise, as we might want to cache other objects (e.g.
+	OCSP responses) in the near future.
+
+2015-12-16  TJ Saunders <tj at castaglia.org>
+
+	* config.h.in, configure, configure.in, contrib/mod_tls.c: Initial
+	start of OCSP stapling implementation in mod_tls.  For this, we
+	either return a "tryLater" OCSP response, or none.  This is how it
+	starts.
+
+2015-12-15  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_sql_odbc.html: HTML fixups.
+
+2015-12-15  TJ Saunders <tj at castaglia.org>
+
+	* : commit d259494c8ebc9c2074ec95125d1e57a5b8db3074 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Tue Dec 15 16:51:56 2015 -0800
+
+2015-12-15  TJ Saunders <tj at castaglia.org>
+
+	* config.h.in, configure, configure.in, contrib/mod_sql.c,
+	contrib/mod_sql.h, contrib/mod_sql_mysql.c, contrib/mod_sql_odbc.c,
+	contrib/mod_sql_postgres.c, contrib/mod_sql_sqlite.c,
+	doc/contrib/mod_sql.html: Bug#4200 - Support TLS client
+	configuration for SQL servers.
+
+2015-12-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #182 from tkyf/fix_log_for_MaxHostsPerUser Fix log message on connection refused by MaxHostsPerUser limits
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c, contrib/mod_sftp/keys.c,
+	contrib/mod_sftp/mod_sftp.h.in: Make mod_sftp compile when
+	either/both DSA and DES are disabled in OpenSSL.
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c: Ensure that mod_sftp builds if RIPEMD
+	is disabled in OpenSSL.
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c: Make sure that mod_sftp can compile
+	against an OpenSSL which has had CAST support disabled.
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c, contrib/mod_sftp/mod_sftp.h.in: Make it
+	possible to build mod_sftp against an OpenSSL which has had Blowfish
+	support disabled.
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c: Make sure that mod_sftp compiles using
+	an OpenSSL that has had RC4 support disabled.
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for fix for Bug#4212.
+
+2015-12-14  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #181 from proftpd/rootrevoke-on-enforcement Bug#4212: Ensure that FTP data transfer commands fail appropriately
+	when "RootRevoke on" is in effect.
+
+2015-12-11  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, modules/mod_ls.c, src/data.c: Make sure that
+	the port number logged in the "RootRevoke in effect" messages is
+	accurate.  Enforce the case where RootRevoke is on, but the client has ignored
+	our error responses to PORT/EPRT, and had requested a data transfer
+	anyway.  We do this by returning an error response for the data
+	transfer command (e.g. LIST, STOR, etc) before we even attempt to
+	make the connection.
+
+2015-12-11  TJ Saunders <tj at castaglia.org>
+
+	* Make.rules.in, lib/Makefile.in, modules/Makefile.in,
+	src/Makefile.in, utils/Makefile.in: Define a MAKEDEPEND variable in
+	the top-level Make.rules, so that I can easily override what to use
+	as "makedepend" on platforms which don't easily have one (e.g. Mac
+	OSX).
+
+2015-12-11  TJ Saunders <tj at castaglia.org>
+
+	* src/ftpscrub.c: Remove duplicated 'ftpscrub.c' from the src/
+	directory; it is already in the utils/ directory (where it should
+	be).  I suspect that this file was gumming up the makedepend works.
+
+2015-12-11  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ldap.c: Fix mod_ldap build issue when building against
+	e.g. OpenLDAP-2.3.x.
+
+2015-12-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/ftpasswd: Address Debian ProFTPD Bug 796233; see:   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=796233
+
+2015-12-10  TJ Saunders <tj at castaglia.org>
+
+	* utils/ftpshut.8.in: Man page style nits.
+
+2015-12-10  TJ Saunders <tj at castaglia.org>
+
+	* src/dirtree.c: Reduce logging verbosity when parsing Include
+	directories.
+
+2015-12-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #178 from proftpd/contrib-systemd-bug3661 Bug#3661 - Support for systemd's "socket activation" mode.
+
+2015-12-10  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth.c: Provide slightly more informative message
+	about active data transfers being affected by "RootRevoke on".
+
+2015-12-09  TJ Saunders <tj at castaglia.org>
+
+	* : commit b82c2a02d2822d033f38026c53ee65579efce5b8 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Wed Dec 9 14:21:26 2015 -0800
+
+2015-12-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/dist/systemd/README.systemd,
+	contrib/dist/systemd/proftpd.socket,
+	contrib/dist/systemd/proftpd at .service: Bug#3661 - Support for
+	systemd's "socket activation" mode.
+
+2015-12-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #177 from proftpd/null-times-bug4136 Attempt to address/mitigate Bug#4136 by checking for null return
+	valu…
+
+2015-12-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c: Removing unused variable/context.
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES: Something more to mention in the release notes
+	(Bug#4057, that is).
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #176 from
+	proftpd/tls-memcache-using-json-bug4057 Bug#4057 - Support using JSON when storing TLS session information
+	in memcached
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ban.c: While working on Bug#4057, I encountered some
+	memory-handling issues with the work for Bug#4056; these are the
+	fixes.
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Another form of temporary file to ignore, please.
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, contrib/mod_tls_memcache.c,
+	doc/contrib/mod_tls_memcache.html, include/memcache.h,
+	src/memcache.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm: Bug#4057:
+	Support using JSON when storing TLS session information in
+	memcached.
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* : commit 6e34ab4c8132c3b8b87e60ab116e40a2b0aae3be Author: TJ
+	Saunders <tj at castaglia.org> Date:   Tue Dec 8 08:20:51 2015 -0800
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* src/signals.c: Minor tweaks (build warnings, error checking) for
+	obtaining stacktraces on Mac OSX.
+
+2015-12-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #175 from
+	proftpd/ban-memcache-using-json-bug4056 Bug#4056: Use JSON when storing ban information in memcached
+
+2015-12-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ban.c, include/memcache.h, src/memcache.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_ban/memcache.pm,
+	utils/ftpwho.c: Finish the regression tests for Bug#4056.  This also
+	involved adding some necessary cleanup (lessons learned!) in ftpwho.
+
+2015-12-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ban.c: Some cached ban rules might never expire; these
+	can be added using ftpdctl.  Make sure that mod_ban does not purge
+	the rules when checking for expired cache entries.
+
+2015-12-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ban.c, doc/contrib/mod_ban.html, src/memcache.c: 
+	Implement the code for supporting Bug#4056, and update the mod_ban
+	docs accordingly.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES: Flesh out the release notes, preparing for a
+	release.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Remove troublesome assertion, for now.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Trying to get this persnickety test to pass.
+	Sheesh.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Fix test failures by ensuring that localtime(3)
+	returns GMT data.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Adding test coverage of the check_shutmsg()
+	support routine.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* utils/ftpshut.c: Stylistic nits in the ftpshut utility; no
+	functional change.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/display.c: Properly test the SEND_NOW and NO_EOM display
+	flags.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c, tests/api/misc.c: More tests of the Support API;
+	I'm sure more coverage is needed.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/display.c: Cover more of the Display API via unit tests.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* include/support.h, src/main.c, src/support.c, tests/api/misc.c: 
+	Adding to the code coverage of the unit tests.
+
+2015-12-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/display.c: Tickle more of the code paths in the Display
+	API in the unit tests.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/misc.c: Fix broken test, seen by travis-ci.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c, tests/api/misc.c: More unit tests, for more code
+	coverage.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c, tests/api/misc.c: Filling out more unit tests, in
+	the name of code coverage.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* tests/Makefile.in, tests/api/misc.c, tests/api/tests.c,
+	tests/api/tests.h: Start adding unit tests for the support API
+	routines now pull in, to increase test coverage.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* src/support.c: Stylistic nits; no functional change.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* include/str.h, include/support.h, src/str.c, src/support.c,
+	tests/api/str.c: With the addition of the src/support.c file to the
+	unit tests, the test coverage has changed.  Thus work to get the
+	coverage back up to par, which involves some good refactoring.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* src/display.c: Stylistic tweaks; no functional change.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* include/display.h, src/display.c, tests/Makefile.in,
+	tests/api/display.c, tests/api/stubs.c, tests/api/tests.c,
+	tests/api/tests.h: Start working on unit tests for the Display API.
+
+2015-12-03  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES: Start updating release notes; more to be fleshed
+	out.
+
+2015-12-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #168 from
+	proftpd/login-timeout-exceeded-log-level Change the log level for various timeouts (TimeoutLogin,
+	TimeoutSessi…
+
+2015-11-30  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #172 from
+	proftpd/core-masqueradeaddress-resolved-later-bug4104 Bug#4104 - Handle MasqueradeAddress resolution errors due to startup
+	…
+
+2015-11-30  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/netaddr.c: Make sure to add a unit test for the specific
+	errno value, when a DNS name cannot be resolved (Bug#4104).
+
+2015-11-30  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for this feature.
+
+2015-11-30  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_dynmasq.c, tests/t/modules/mod_dynmasq.t,
+	tests/tests.pl: Add regression tests for the mod_dynmasq module,
+	ensuring that it handles the delayed-resolution DNS names for
+	MasqueradeAddress properly (Bug#4104).
+
+2015-11-30  TJ Saunders <tj at castaglia.org>
+
+	* src/session.c,
+	tests/t/lib/ProFTPD/Tests/Config/MasqueradeAddress.pm: Adding
+	regression test for Bug#4104.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_dynmasq.c, modules/mod_core.c, src/dirtree.c,
+	src/display.c, src/main.c, src/netaddr.c, src/session.c: Bug#4104 -
+	Handle MasqueradeAddress resolution errors due to startup sequences.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kbdint.c: Remove now-unneeded cast.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with fix for Bug#4210.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #171 from
+	proftpd/sftp-unbounded-extpair-bug4210 Bug#4210 - Avoid unbounded SFTP extended attribute key/values.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Bug#4210 - Avoid unbounded SFTP extension
+	key/values.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updating NEWS for fix for Bug#4209.
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #170 from
+	proftpd/core-zero-size-pool-malloc-bug4209 Bug#4209 - Zero-length memory allocation possible, with undefined
+	res…
+
+2015-11-28  TJ Saunders <tj at castaglia.org>
+
+	* src/pool.c: Bug#4209 - Zero-length memory allocation possible,
+	with undefined results.
+
+2015-11-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #169 from proftpd/tls-host-command Make the handling of the HOST command by mod_tls be more in line
+	with…
+
+2015-11-15  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Minor trace logging for debugging.
+
+2015-11-15  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_mysql.c: Stylistic tweaks; no functional change.
+
+2015-11-15  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql.c: Minor nit; no functional change.
+
+2015-11-15  TJ Saunders <tj at castaglia.org>
+
+	* src/netio.c: Minor nits; no functional change.
+
+2015-11-14  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth.c: Change the log level for various timeouts
+	(TimeoutLogin, TimeoutSession) from NOTICE to INFO, given that these
+	timeouts can be very frequent/common occurrences.
+
+2015-11-14  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/index.html: Fix link to LogLevels howto.
+
+2015-11-13  TJ Saunders <tj at castaglia.org>
+
+	* src/netio.c: If a stream has been marked as interruptible, then do
+	not retry e.g. poll() when the system call is interrupted; return
+	that error to the caller.
+
+2015-11-12  TJ Saunders <tj at castaglia.org>
+
+	* include/bindings.h, modules/mod_core.c, src/bindings.c: During the
+	implementing of HOST handling in mod_proxy, the integration tests
+	there stumbled across a minor bug in the handling of HOST commands
+	which pertain to several different vhosts on the same _address_, but
+	on different ports.  We were not properly differentiating among
+	these vhosts when looking for the one which had the requested HOST
+	(or not).
+
+2015-11-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #166 from
+	proftpd/sftp-publickey-failed-login-logging Add more logging, via the normal auth logging channels, for when
+	SFTP
+
+2015-11-10  TJ Saunders <tj at castaglia.org>
+
+	* : commit 100e7f7ca1780ce2cab8b489768b76816794999c Author: TJ
+	Saunders <tj at castaglia.org> Date:   Mon Nov 9 17:51:36 2015 -0800
+
+2015-11-09  TJ Saunders <tj at castaglia.org>
+
+	* src/inet.c: Ignore "Bad file descriptor" errors when toggling
+	Nagle on sockets.
+
+2015-11-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #165 from
+	proftpd/lang-segfault-postparse-bug4206 Attempt to Bug#4206 by speculatively fixing possible causes.  First,
+	…
+
+2015-10-28  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_lang.c: Attempt to Bug#4206 by speculatively fixing
+	possible causes.  First, reset a static string (allocated out of a
+	pool which is cleared on restart) to its default.  Next, remove some
+	(useless) #ifdefs which, if mod_lang is built as a DSO/shared
+	module, could cause problems on module loading IFF the system does
+	not have the expected header.
+
+2015-10-27  TJ Saunders <tj at castaglia.org>
+
+	* src/parser.c: Fix bug in parser, caused by refactoring.
+
+2015-10-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Fix compiler warning.
+
+2015-10-14  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #159 from proftpd/sftp-fsetstat-bug4204 Attempt to mitigate/fix Bug#4204 by ensuring that the response lists
+	are
+
+2015-10-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: When writing out a STATUS message, make
+	sure to use the pool from the SFTP request, rather than the
+	session's pool, as the latter could possibly be overused; this thus
+	counts as a memory leak.
+
+2015-10-13  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #163 from bverschueren/obsolete_timer_t_macro Remove HAVE_TIMER_T macro and timer_t detection mechanism
+
+2015-10-07  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Turns out that running the api-tests executable again
+	doesn't really increase the coverage of the Privs API as I'd
+	expected.  Oh well.
+
+2015-10-07  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: I think that the `api-tests` executable needs to be
+	run in the tests/ directory, so that the lcov stuff picks up the
+	files.
+
+2015-10-07  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Run just the privs-related unit tests as root, for
+	better test code coverage.
+
+2015-10-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Make sure that the errno value from a
+	failed pr_fsio_write() call is properly propagated/used.
+
+2015-10-07  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/privs.c: Remove unneeded setting of trace logging to
+	STDERR.
+
+2015-10-06  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/scoreboard.c: Attempt to fix spurious test failures.
+
+2015-10-06  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/scoreboard.c: Refactor the Scoreboard testing code to be
+	more in line with the rest of the styling.  Added coverage of more
+	fields when updating scoreboard entries.
+
+2015-10-05  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/scoreboard.c: Cover more of pr_scoreboard_entry_get(),
+	and note what needs to be done for pr_scoreboard_entry_update().
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* include/scoreboard.h, src/scoreboard.c, tests/api/scoreboard.c: 
+	Refactor the other portion of locking code in the Scoreboard API,
+	that for locking entries, into a common testable function.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* include/scoreboard.h, src/scoreboard.c, tests/api/scoreboard.c: 
+	Refactor some of the scoreboard locking code to avoid code
+	duplication, and make it possible to test via unit tests.  There's
+	more such refactoring to do here.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/auth.c: Tickle the "authenticated/authorized/checked via
+	RFC2228" code paths.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/auth.c, tests/api/data.c, tests/api/modules.c: Fixed
+	some typos, tried to cover a few more edge cases.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* src/modules.c, tests/api/data.c, tests/api/modules.c: Testing more
+	code paths.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/data.c, tests/api/stubs.c, tests/api/tests.h: Simulate
+	receiving control commands during a data transfer, for more test
+	coverage.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/data.c: Add test coverage for reading data via
+	pr_data_xfer().
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* src/data.c, tests/api/configdb.c, tests/api/data.c,
+	tests/api/netacl.c: Refactor some of the too-large pr_data_xfer()
+	function, to make it easier to read (and test).  Start working on
+	unit tests for this function, too.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/configdb.c: Much better exercising of the mergedown
+	functionality.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/configdb.c, tests/api/stash.c: Working on exercising
+	more of the config lookup code.
+
+2015-10-04  TJ Saunders <tj at castaglia.org>
+
+	* src/stash.c, tests/api/encode.c, tests/api/stash.c: Eking out ever
+	more test coverage, bit by bit.
+
+2015-10-03  TJ Saunders <tj at castaglia.org>
+
+	* src/auth.c, src/encode.c, tests/api/auth.c, tests/api/encode.c: 
+	Increased code coverage for the Auth and Encode APIs.
+
+2015-10-03  TJ Saunders <tj at castaglia.org>
+
+	* src/response.c, tests/api/response.c: The response unit tests
+	would fail if the TEST_VERBOSE environment variable was set; now
+	fixed.  Also added some sanity checking for null format strings for
+	pr_response_add() and pr_response_add_err(), for paranoia.
+
+2015-10-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #162 from
+	proftpd/tls-no-mlock-for-empty-passphrase If there is no passphrase needed for a certificate, then do not
+	bother
+
+2015-09-30  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #161 from
+	orthographic-pedant/spell_check/administrator Fixed typographical error, changed adminstrator to administrator in
+	README.
+
+2015-09-30  TJ Saunders <tj at castaglia.org>
+
+	* README.md: Add a few more badges.
+
+2015-09-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Attempt to mitigate/fix Bug#4204 by
+	ensuring that the response lists are cleared after each dispatching.
+
+2015-09-29  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #158 from proftpd/exec-max-fd-count Limit the maximum number of fds closed by mod_exec when executing an
+	…
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* src/configdb.c, tests/api/configdb.c: Tickling a few more branches
+	in the Config API.
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, src/inet.c, tests/api/inet.c: Removing unneeded code
+	paths.  Covering more of the Inet API with more testing.
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Cover more of the pr_fsio_access() code paths.
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* src/netaddr.c, tests/api/netaddr.c: Flesh out the remaining
+	Netaddr API unit tests.
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/netaddr.c: Cover the pr_netaddr_v6tov4() function in the
+	tests, too.
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/inet.c: Try covering more error paths in the Inet API.
+
+2015-09-28  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, tests/api/fsio.c: Trying to get coverage of the FSIO
+	API higher.
+
+2015-09-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_exec.c: Limit the maximum number of fds closed by
+	mod_exec when executing an external program, regardless of the
+	platform.
+
+2015-09-27  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Easy addition for covering of the --enable-devel
+	FSIO API.
+
+2015-09-27  TJ Saunders <tj at castaglia.org>
+
+	* src/configdb.c, tests/api/configdb.c: Covering more of the Config
+	API.
+
+2015-09-27  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, tests/api/fsio.c: Covering more of the FSIO API.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* src/regexp.c, tests/api/regexp.c: Increase coverage of POSIX regex
+	handling in the Regexp API.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/configdb.c: Cover a little more of the Config API.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* src/auth.c, tests/api/auth.c: Testing the handling of -1 UID/GIDs
+	turned up a small bug, where the Auth API was not setting errno
+	appropriately.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Test the cases where multiple FSes are
+	inserted/removed.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Cover more of the FSIO API, too.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/trace.c: Make sure we test the handling of the "DEFAULT"
+	trace channel, too.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* src/trace.c: If the channel name is null, then it cannot be
+	removed from the trace table of channels, can it?
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* src/trace.c, tests/api/stubs.c, tests/api/trace.c: Tickle a few
+	more code paths in the Trace API.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* src/log.c: Be more paranoid about handling of possible directories
+	as log files, including the "/" case.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/trace.c: More Trace test coverage.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/data.c: Fix unit test broken in travis-ci environment.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* include/data.h, src/data.c, tests/api/data.c: Attempt to write
+	tests for pr_data_sendfile(), covering more of its code paths.
+
+2015-09-26  TJ Saunders <tj at castaglia.org>
+
+	* src/netio.c, tests/api/event.c, tests/api/netio.c: Finish
+	implementing the NetIO unit tests.
+
+2015-09-25  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/event.c: One more tweak for the Event API unit test
+	coverage.
+
+2015-09-25  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/event.c: More coverage of the Event API code paths.
+
+2015-09-25  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/auth.c: Fix unit test failing in travis-ci environment.
+
+2015-09-25  TJ Saunders <tj at castaglia.org>
+
+	* config.h.in, configure, configure.in, include/fsio.h, src/fsio.c,
+	tests/api/fsio.c: Finish fleshing out the FSIO API unit tests.  This
+	required adding a new pr_fsio_fsync() function (and its own test),
+	for flushing data to disk for subsequent reading.
+
+2015-09-25  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, tests/api/fsio.c: More coverage for the FSIO API,
+	please.
+
+2015-09-25  TJ Saunders <tj at kiban.io>
+
+	* tests/api/fsio.c, tests/api/trace.c: Quell compiler warnings, fix
+	unit test failing in travis-ci environment.
+
+2015-09-25  TJ Saunders <tj at kiban.io>
+
+	* tests/api/fsio.c: Fix failing unit test.
+
+2015-09-25  TJ Saunders <tj at kiban.io>
+
+	* src/fsio.c, tests/api/fsio.c: Adding coverage for more functions
+	in the FSIO API; still more to be done here.
+
+2015-09-24  TJ Saunders <tj at kiban.io>
+
+	* tests/api/parser.c: When testing the function for parsing files,
+	load enough of a module's directive handling to test those paths,
+	too.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* src/configdb.c, tests/api/configdb.c, tests/api/regexp.c: More
+	test coverage, served up hot.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* src/parser.c, src/trace.c, tests/api/parser.c, tests/api/trace.c: 
+	Fix some of the Parser API tests, and try to cover more of the Trace
+	API.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* tests/api/netaddr.c: Use 'www.google.com' as a well-known DNS name
+	that has multiple IP addresses.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* src/parser.c, tests/api/netaddr.c, tests/api/parser.c: Filling in
+	more unit tests here and there, tickling more code paths, extending
+	coverage.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* tests/api/auth.c: Fix tests on travis-ci.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* include/modules.h, src/main.c, src/modules.c, tests/api/modules.c: 
+	Testing more of the Modules API.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* src/auth.c, tests/api/auth.c: More unit testing of the Auth API.
+	Fixed one bug with the handling of the FL_AUTH_MODULE caching flag
+	(yay unit tests!).
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* src/pool.c, tests/api/parser.c, tests/api/pool.c: Extend test
+	coverage of the Pool API.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* include/parser.h, modules/mod_auth_unix.c, modules/mod_core.c,
+	src/main.c, src/parser.c, tests/api/parser.c: Fixing up compiler
+	warnings.  Fixed the API for parsing a single line of text so that
+	it is actually _useful_ for callers outside of
+	pr_parser_parse_file().  Added more unit tests for parsing.
+
+2015-09-24  Castaglian <tj at kiban.io>
+
+	* src/auth.c, tests/api/auth.c: Extend test coverage of the Auth
+	API.
+
+2015-09-24  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Properly test the eviction of expired
+	fs.statcache entries.
+
+2015-09-24  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, tests/api/fsio.c, tests/api/tests.c: And yet more unit
+	tests for the FSIO API.
+
+2015-09-23  TJ Saunders <tj at castaglia.org>
+
+	* tests/Makefile.in, tests/api/privs.c, tests/api/tests.c,
+	tests/api/tests.h: Start working on unit tests for the Privs API
+	directly.
+
+2015-09-23  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/regexp.c: Flesh out more of the Regexp API unit tests.
+
+2015-09-23  Castaglian <tj at kiban.io>
+
+	* tests/api/data.c: The travis-ci environment yields slightly
+	different (but still OK) errno values.
+
+2015-09-23  Castaglian <tj at kiban.io>
+
+	* include/configdb.h, src/data.c, tests/api/data.c,
+	tests/api/stubs.c: Add some NULL checks, for e.g. unit tests.  Flesh
+	out more Data API unit tests.
+
+2015-09-22  TJ Saunders <tj at kiban.io>
+
+	* src/data.c, tests/api/data.c: Start working on unit tests for the
+	Data API.
+
+2015-09-22  TJ Saunders <tj at kiban.io>
+
+	* README.md, tests/api/fsio.c: Tweak README.  Fix unit test failing
+	only in travis-ci.
+
+2015-09-22  TJ Saunders <tj at kiban.io>
+
+	* src/fsio.c, tests/api/fsio.c: Fleshing out more FSIO tests; more
+	to come.
+
+2015-09-22  TJ Saunders <tj at kiban.io>
+
+	* tests/api/str.c: Fix failing test on this Yosemite MacOSX machine,
+	due to not NUL-terminating the source buffer in a sstrcat test.
+
+2015-09-22  TJ Saunders <tj at kiban.io>
+
+	* src/pool.c: Quell compiler warnings about pointer
+	arithmetic/casts; first seen on this version of Mac OSX (Yosemite).
+
+2015-09-21  TJ Saunders <tj at castaglia.org>
+
+	* README.md: Minor tweaks to README; no functional changes.
+
+2015-09-21  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Fix typo in travis-ci config file.
+
+2015-09-21  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml, src/inet.c, tests/api/configdb.c, tests/api/inet.c: 
+	Let's see/hope that these additions get us over the 50% mark for
+	test coverage.
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* src/parser.c, src/scoreboard.c, tests/api/cmd.c,
+	tests/api/event.c, tests/api/parser.c, tests/api/scoreboard.c,
+	tests/api/trace.c: Working toward getting at least 50% unit test
+	coverage.
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* src/netio.c, tests/api/netio.c, tests/api/timers.c: More unit
+	tests, Cooky!
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_lang.c, tests/Makefile.in, tests/api/configdb.c,
+	tests/api/encode.c, tests/api/parser.c, tests/api/tests.c,
+	tests/api/tests.h: Adding more unit tests to increase the test
+	coverage.
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* src/netio.c, tests/api/netio.c, tests/api/response.c: More work on
+	the unit tests, trying to expand on the test coverage.  Doing so
+	found a few small bugs in the NetIO code.  Yay Coveralls!
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* README.md: Add a badge for the Coveralls coverage status.
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Let's see how Coveralls does for us, shall we?
+
+2015-09-20  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in: Support a 'coverage' option for the
+	--enable-devel compile-time option, for enabling test coverage
+	generation/reporting.
+
+2015-09-11  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #154 from proftpd/ldap-libldap-tracing Enable more tracing of mod_ldap's operations by hooking into the
+	LDAP…
+
+2015-08-30  tjsaunders <tj at lyveminds.com>
+
+	* doc/license.txt: Update license header years.
+
+2015-08-30  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_ldap.html: Update the mod_ldap docs to actually
+	match the code now, folding in the changes mentioned in README.LDAP.
+
+2015-08-30  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_ldap.c: Make this directive handler name match the
+	directive name, for consistency.
+
+2015-08-26  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS for fix for Bug#4202.
+
+2015-08-26  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #152 from proftpd/facts-mlsd-crlf-bug4202 Bug#4202 - MLSD lines not properly terminated with CRLF.
+
+2015-08-26  tjsaunders <tj at lyveminds.com>
+
+	* : commit 62a4e24dcc37204168896ea35f379a1c0b952ff0 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Thu Aug 20 13:59:55 2015 -0700
+
+2015-08-20  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_xfer.c: There are a few conditions under which we
+	don't really need to log about failing to set the corking socket
+	option.
+
+2015-08-18  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS with mention of fix for Bug#4201.
+
+2015-08-18  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #150 from
+	proftpd/sftp-scp-hiddenstores-cleanup-bug4201 Bug#4201 - HiddenStores temporary files not removed when exceeding
+	qu…
+
+2015-08-17  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_dnsbl/configure, contrib/mod_dnsbl/configure.in,
+	contrib/mod_load/configure, contrib/mod_load/configure.in,
+	contrib/mod_sftp/configure, contrib/mod_sftp/configure.in,
+	contrib/mod_snmp/configure, contrib/mod_snmp/configure.in,
+	contrib/mod_wrap2/configure, contrib/mod_wrap2/configure.in: Reorder
+	the header/function checks in the module configure scripts, so that
+	they happen after processing --with-includes and --with-libraries
+	command-line options.  While this doesn't fix any current issue, it
+	does set a better example for future modules; this was a problem for
+	the e.g. mod_proxy configure script.
+
+2015-08-16  tjsaunders <tj at lyveminds.com>
+
+	* .gitattributes: Add manual overrides, to stop mis-representing the
+	language used for ProFTPD and its modules.
+
+2015-08-16  tjsaunders <tj at lyveminds.com>
+
+	* src/main.c: Add note to future self, about handling DisplayConnect
+	properly for denied connections.
+
+2015-08-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #151 from proftpd/extlog-speedup ExtendedLog speedup by avoiding redundant strlen(3) calls
+
+2015-08-16  tjsaunders <tj at lyveminds.com>
+
+	* lib/sstrncpy.c, modules/mod_log.c,
+	tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: Speed up
+	ExtendedLog writing by removing redundant strlen(3) calls on the
+	logged string, and instead simply tracking the return values from
+	snprintf et al.  Note that to do this, the sstrncpy() semantics needed to be
+	consistent, which means NOT using strcpy(3); we want sstrncpy() to
+	return the number of bytes actually copied, not what WOULD have been
+	copied.
+
+2015-08-16  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, modules/mod_log.c,
+	tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: Fixed some
+	regressions with handling of the DELE command's argument, and with
+	mod_tls tagging the PASS command as a SEC logging class (when it
+	should not).
+
+2015-08-16  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm: Checking in
+	regression test for Bug#4199.
+
+2015-08-15  tjsaunders <tj at lyveminds.com>
+
+	* configure, configure.in: Update the -pthread/OpenSSL check, to
+	catch regressions of Bug#3975 on e.g FreeBSD platforms, where the
+	`openssl version` command doesn't emit the expected flags.
+
+2015-08-15  tjsaunders <tj at lyveminds.com>
+
+	* : commit 006b1f73db9421ca653c5b545765ee6b6893ff6d Author:
+	tjsaunders <tj at lyveminds.com> Date:   Sat Aug 15 12:40:35 2015 -0700
+
+2015-08-15  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/scp.c: Make sure that we delete the HiddenStores
+	file on SCP upload when either write(2) or close(2) fails; we were
+	handling close(2) failures, but not write(2) failures properly.
+
+2015-08-15  tjsaunders <tj at lyveminds.com>
+
+	* : commit d99edbb036cba1269352e66f6c89b26c502b58dd Author:
+	tjsaunders <tj at lyveminds.com> Date:   Fri Aug 14 10:47:04 2015 -0700
+
+2015-08-13  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/interop.c, contrib/mod_sftp/interop.h,
+	contrib/mod_sftp/mod_sftp.c: Fix regression for SSH2 logins,
+	introduced by the changes for handling the optional 'comments' field
+	in the initial version exchange.  The initial version (exactly as
+	sent) need to be used in the session ID/hash; the previous changes
+	were trimming off the optional comment field, thus causing server
+	and client to calculate different session IDs.
+
+2015-08-13  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/scp.c: Bug#4201 - HiddenStores temporary files
+	not removed when exceeding quota using SCP.  Make sure to clean up the HiddenStores temporary file when there was
+	an error, either when writing OR when closing the HiddenStores file
+	(as when quota is exceeded).
+
+2015-08-13  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/kex.c: Remove some unnecessary casts; fix a minor
+	memory leak on error conditions.
+
+2015-08-12  tjsaunders <tj at lyveminds.com>
+
+	* src/main.c, src/signals.c: Clear out the EINTR errno value in a
+	few more places.
+
+2015-08-12  tjsaunders <tj at lyveminds.com>
+
+	* src/main.c: Make sure to clear out any cached errno, too.
+
+2015-08-12  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_facts.c, tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm: 
+	Checkpointing work on investigating a possible regression with
+	regard to MLSD line formatting, i.e.  missing proper CRLF
+	terminations.
+
+2015-08-12  tjsaunders <tj at lyveminds.com>
+
+	* src/main.c: Make sure to clear any possibly-cached errno value in
+	the main select() loop.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* src/timers.c: Style nit; no functional change.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* src/trace.c, tests/api/trace.c: Some tweaks to the Trace API,
+	trying to reduce redundancy.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* src/parser.c, tests/api/etc/str/utf8-space.txt, tests/api/str.c: 
+	If we fail to parse some configuration directives, check for any
+	non-ASCII characters in the directive name.  This recently tripped
+	me up with diagnosing a broken system; now check for, and log, such
+	things.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Config/HideFiles.pm: Update integration
+	test, broken by change for Bug#3990.
+
+2015-08-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #149 from proftpd/core-ptr-cast Address some pointer cast alignment issues by defining the
+	cmd_rec.ar…
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_cap.c: See what happens when you change your mind
+	about variable names partway through? Sigh.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_core.c: Quell a few more compiler warnings about
+	pointer casts.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* lib/tpl.c: Remove unused variables.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sql_passwd.c, modules/mod_cap.c: Fixing some build
+	issues/warnings.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* src/main.c: Quell another compiler warning about pointer casts.
+
+2015-08-10  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_copy.c, contrib/mod_deflate.c, contrib/mod_exec.c,
+	contrib/mod_ifsession.c, contrib/mod_ifversion.c,
+	contrib/mod_ldap.c, contrib/mod_log_forensic.c,
+	contrib/mod_quotatab.c, contrib/mod_quotatab_sql.c,
+	contrib/mod_radius.c, contrib/mod_readme.c, contrib/mod_rewrite.c,
+	contrib/mod_sftp/auth-hostbased.c, contrib/mod_sftp/auth-kbdint.c,
+	contrib/mod_sftp/auth-password.c,
+	contrib/mod_sftp/auth-publickey.c, contrib/mod_sftp/auth.c,
+	contrib/mod_sftp/fxp.c, contrib/mod_sftp/mod_sftp.c,
+	contrib/mod_sftp/scp.c, contrib/mod_sftp_sql.c,
+	contrib/mod_shaper.c, contrib/mod_site_misc.c,
+	contrib/mod_snmp/mod_snmp.c, contrib/mod_sql.c, contrib/mod_tls.c,
+	contrib/mod_wrap2/mod_wrap2.c, contrib/mod_wrap2_sql.c,
+	include/dirtree.h, include/proftpd.h, lib/json.c,
+	modules/mod_auth.c, modules/mod_auth_file.c,
+	modules/mod_auth_pam.c, modules/mod_core.c, modules/mod_ctrls.c,
+	modules/mod_dso.c, modules/mod_facts.c, modules/mod_log.c,
+	modules/mod_ls.c, modules/mod_site.c, modules/mod_xfer.c,
+	src/cmd.c, src/data.c, src/dirtree.c, src/main.c, src/parser.c,
+	tests/t/lib/ProFTPD/Tests/Config/Order.pm: Address some pointer cast
+	alignment issues by defining the cmd_rec.argv member to be "void
+	**", rather than "char **".  Void pointers are guaranteed to be
+	large enough to hold pointers to anything, where as char pointers
+	are not.  And we sometimes stash non-char data in the cmd_rec.argv
+	area.  This looks like a fair amount of code churn; mostly it's addressing
+	compiler warnings by providing explicit casts to char * in most
+	places (e.g. logging) where the variadic function cannot easily tell
+	the type.
+
+2015-08-09  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/kex.c: For some reason, the ordering in which the
+	logging of the NEWKEYS command makes a difference.  I suspect it's
+	the order/amount of memory read out of the pool.  By logging NEWKEYS
+	after setting the session keys, some rather strange behaviors are
+	alleviated.
+
+2015-08-09  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/auth.c: Fix possible segfault (due to null
+	pointer) when the SSH2 authentication method succeeds, but
+	subsequent login fails.
+
+2015-08-08  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/str.c: Slightly better demonstration of
+	pr_str_get_word()'s handling of quoted strings.
+
+2015-08-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #148 from proftpd/tls-shutdown-buffering When shutting down the TLS session, we need to send a 'close_notify'
+	…
+
+2015-08-07  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS for fix for Bug#4198.
+
+2015-08-07  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #147 from proftpd/facts-cdir-bug4198 Bug#4198 - MLSD/MLST fact type "cdir" is incorrectly used for the
+	current working directory.
+
+2015-08-06  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, doc/contrib/mod_tls.html: Provide an
+	environment variable for the requested SNI, if present.
+
+2015-08-06  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, doc/contrib/mod_tls.html: Provide an
+	environment variable for the requested SNI, if present.
+
+2015-08-06  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/MLST.pm: Checking in the
+	accompanying regression tests for Bug#4198.
+
+2015-08-06  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_facts.c: Bug#4198 - MLSD/MLST fact type "cdir" is
+	incorrectly used for the current working directory.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_xfer.c: Better handling of errno values when adjusting
+	process priorities.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/rlimit.c: Missed a couple of RLimit test cases needing
+	the conditional check.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_rewrite.c, contrib/mod_wrap2_file.c,
+	contrib/mod_wrap2_sql.c: Quell more compiler warnings about shadowed
+	global variables.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/filter.c: Add some additional assertions to quell
+	compiler warnings about unused variables (and make the tests
+	better).
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/rlimit.c: Fixing up platform-sensitive tests for e.g.
+	travis-ci builds.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/netio.c: Removing unused variable.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sql_postgres.c: Quell compiler warning about shadowed
+	global variable; stylistic nits.  No functional change.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_deflate.c: Forgot to update mod_deflate for the
+	changed NetIO API signature change.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* src/scoreboard.c: Quell compiler warning about unreached function
+	call.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* tests/Makefile.in, tests/api/env.c, tests/api/help.c,
+	tests/api/rlimit.c, tests/api/tests.c, tests/api/tests.h: Adding API
+	tests for the RLimit API.  Adding forgotten Help API tests.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* src/help.c: Fix the terminating condition for adding help items;
+	the previous end-of-loop condition was too fragile, and not
+	accurate.
+
+2015-08-05  tjsaunders <tj at lyveminds.com>
+
+	* src/help.c, src/response.c, tests/Makefile.in,
+	tests/api/response.c, tests/api/tests.c, tests/api/tests.h: Added
+	some tests for the Help API, which sussed out a few very minor
+	issues.
+
+2015-08-04  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix a few nits in the PSK handling; no
+	functional change.
+
+2015-08-04  tjsaunders <tj at lyveminds.com>
+
+	* src/dirtree.c: When checking limits, make sure that we actually
+	have an authenticated user/ group before checking for
+	per-user/per-group limits.  With modules such as mod_proxy, this may
+	not always be true.
+
+2015-08-03  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/mod_sftp.c: Make the ban messages emitted to SSH2
+	clients a little more descriptive.
+
+2015-08-03  tjsaunders <tj at lyveminds.com>
+
+	* src/log.c: If the DebugLevel directive is used to set the debug
+	level to anything higher than 0, then automatically assume that the
+	SyslogLevel is DEBUG, not NOTICE.  This helps in debugging
+	configurations.
+
+2015-08-03  tjsaunders <tj at lyveminds.com>
+
+	* src/trace.c: Turns out that the trace logging code needs the same
+	over-long log line handling fixes as the main logging code.
+
+2015-08-03  tjsaunders <tj at lyveminds.com>
+
+	* src/log.c: Better handling of over-long log lines.
+
+2015-08-03  tjsaunders <tj at lyveminds.com>
+
+	* src/log.c: Increase (and fix the handling of) the max log line
+	length.
+
+2015-08-03  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_tls.html: Some minor updates to the mod_tls docs.
+
+2015-08-02  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: When cleaning up OpenSSL-related code, mod_tls
+	needs to know that mod_proxy also uses OpenSSL (and thus should skip
+	cleanup if that module is in use).
+
+2015-07-31  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix logging of selected ALPN.
+
+2015-07-30  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, include/netio.h, src/netio.c: Allow modules
+	registering NetIO objects to customise the registered NetIO owner
+	name, if they wish.
+
+2015-07-28  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fixed a few nits in the cert-checking routine.
+
+2015-07-28  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: We retrieved the data connection SSL object too
+	early.
+
+2015-07-28  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix some timing-related trace logging.
+
+2015-07-28  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_tls.html: Mention, in the TLSSessionCache
+	description, that mod_tls aggressively prunes its session cache of
+	expired sessions by flushing the cache on each session exit.
+
+2015-07-28  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Handle the edge cases where TLS read/write
+	returns negative values; we don't want that to artificially affect
+	the total raw IO values.
+
+2015-07-28  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS with fix for Bug#4097.
+
+2015-07-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #45 from
+	proftpd/sftp-small-rsa-hostkey-rekeying-bug4097 Bug#4097 -  SSH rekey fails when using RSA hostkey smaller than 2048
+	bits.
+
+2015-07-27  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #145 from proftpd/ascii-oob-read-bug4195 Bug#4195 - Handle empty string inputs into ASCII CRLF translation
+	rou…
+
+2015-07-26  tjsaunders <tj at lyveminds.com>
+
+	* src/ascii.c: Bug#4195 - Handle empty string inputs into ASCII CRLF
+	translation routine.
+
+2015-07-25  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #144 from
+	proftpd/fsio-dircat-oob-read-bug4194 Bug#4194: Add more checks for empty paths in pr_fs_dircat().
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix broken build by including the necessary
+	OpenSSL header.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c, tests/api/fsio.c: If either of the arguments to
+	pr_fs_dircat() are empty strings, be consistent and append a "/"
+	character, just as is done when BOTH arguments are empty strings.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c, tests/api/fsio.c: Bug#4194: Add more checks for empty
+	paths in pr_fs_dircat().
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Better handling, when logging OpenSSL
+	diagnostics, of the protocol record callbacks.
+
+2015-07-25  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #143 from proftpd/tests-oob-read-bug4193 Bug#4193 - Out of bounds read in tests/api/str.c.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/ascii.c: Fix previous stupidity.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/ascii.c, tests/api/cmd.c: Quell some compiler warnings
+	in the unit tests, spotted while still trying to figure out why
+	travis-ci tests are failing (but they pass locally).
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/ascii.c: Triaging a failing unit test in travis-ci.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* tests/api/str.c: Bug#4193 - Out of bounds read in tests/api/str.c.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Tweaking/polishing the OpenSSL msg/info
+	callbacks.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_ban.html: Remove duplicate/extraneous slash in
+	example config.
+
+2015-07-25  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_ban.html: Minor tweak.
+
+2015-07-24  tjsaunders <tj at lyveminds.com>
+
+	* : commit 4776466b62889dee9a67171c6dbd317822191595 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Fri Jul 24 13:41:12 2015 -0700
+
+2015-07-24  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_log.c: Require a non-zero length LogFormat name.
+
+2015-07-24  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_log.c: Slightly better handling of using the default
+	LogFormat, without necessarily blocking configs that want to
+	use/override the "default" nickname themselves.
+
+2015-07-24  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_log.c: Fix the registration of the "default"
+	LogFormat.
+
+2015-07-24  tjsaunders <tj at lyveminds.com>
+
+	* doc/howto/TLS.html: Adding TLS FAQ, from forums, about
+	TLSVerifyDepth.
+
+2015-07-23  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_core.c: Better string handling, to make localization
+	easier.
+
+2015-07-23  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix the handling of OpenSSL's
+	SSL_SESS_CACHE_OFF value (which is 0x0, and thus not usable for the
+	bit-checking mod_tls was doing).  Enhance the trace logging around TLS handshaking.
+
+2015-07-23  tjsaunders <tj at lyveminds.com>
+
+	* src/inet.c: Be a little more paranoid in pr_inet_close(), and only
+	try to destroy a non-NULL pool pointer.  (Although destroy_pool()
+	does a NULL check, so this is more for clarity than anything else.)
+
+2015-07-22  tjsaunders <tj at lyveminds.com>
+
+	* include/netio.h, src/netio.c: Include the direction/mode of the
+	stream in the NetIO trace messages.
+
+2015-07-22  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_core.c: Tweaks to the handling of HOST commands in
+	relation to FTPS.  Stylistic nits.
+
+2015-07-22  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, include/cmd.h, src/cmd.c: Add missing
+	PR_CMD_HOST_ID value, and use it in e.g. mod_tls, to allow the HOST
+	command before AUTH is used.
+
+2015-07-22  tjsaunders <tj at lyveminds.com>
+
+	* src/netio.c: Add trace logging of which custom (or not) NetIO is
+	being used for the network operations.
+
+2015-07-21  tjsaunders <tj at lyveminds.com>
+
+	* src/pool.c: Allow unregistering of any/all cleanups by handling a
+	NULL callback value.
+
+2015-07-21  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Quell some compiler warnings, and propagate
+	errno appropriately in some cases.
+
+2015-07-20  tjsaunders <tj at lyveminds.com>
+
+	* src/response.c: Add some trace logging for when the Response API
+	is blocked, for debugging.
+
+2015-07-20  tjsaunders <tj at lyveminds.com>
+
+	* include/pool.h, src/pool.c, tests/api/array.c: Create an
+	array_cat2() function which provides a nice return value;
+	array_cat() then just becomes a void wrapper around array_cat2().
+
+2015-07-19  TJ Saunders <tj at castaglia.org>
+
+	* src/netaddr.c: If the caller wants to know all of the addresses
+	resolved via pr_netaddr_get_addr(), then actually walk all of the
+	struct addrinfo values provided by getaddrinfo(3) and provision the
+	caller-provided list.
+
+2015-07-19  TJ Saunders <tj at castaglia.org>
+
+	* src/table.c: Stylistic nits; no functional changes.
+
+2015-07-19  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_sftp.html: Minor tweaks/updates to the mod_sftp
+	docs.
+
+2015-07-18  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_core.html: Typos.
+
+2015-07-18  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_snmp.html: Update the mod_snmp docs as to where
+	the most recent code for mod_snmp can be found.
+
+2015-07-18  TJ Saunders <tj at castaglia.org>
+
+	* include/netaddr.h, src/ftpdctl.c, src/netaddr.c,
+	tests/api/netaddr.c: Add new Netaddr API functions for clearing
+	specific cached IP addresses, DNS names, as opposed to entire
+	caches.
+
+2015-07-17  tjsaunders <tj at lyveminds.com>
+
+	* doc/modules/mod_auth_unix.html: Add mod_auth_unix FAQ about
+	`passwd -d' and password-less logins.
+
+2015-07-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/mod_sftp.c: Style nit; no functional change.
+
+2015-07-17  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/kex.c: Add some trace logging into the function
+	which determines the DH size to request, for debugging.
+
+2015-07-17  tjsaunders <tj at lyveminds.com>
+
+	* : commit aabf243ce6b52e6817509922d4889dd7e6cd63d4 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Fri Jul 17 10:36:08 2015 -0700
+
+2015-07-17  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #142 from proftpd/mysql-utf8-bug4191 Bug#4191 - Use "utf8mb4" for MySQL, not just "utf8", if the version
+	of
+
+2015-07-16  tjsaunders <tj at lyveminds.com>
+
+	* include/log.h, include/options.h, src/xferlog.c: Make the default
+	mode for the TransferLog be a PR_TUNABLE option.
+
+2015-07-15  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_mysql.c: Bug#4191 - Use "utf8mb4" for MySQL, not
+	just "utf8", if the version of MySQL is new enough, for UTF8
+	encodings.
+
+2015-07-11  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c, contrib/mod_sftp/mod_sftp.c,
+	contrib/mod_sftp/mod_sftp.h.in, contrib/mod_tls.c: I was going to
+	provide configuration directives, for mod_tls and mod_sftp, to
+	configure the path to the OpenSSL .cnf file that should be used.
+	Due to the initialization timing, though, this is a path to do via
+	configuration directives.  Instead, I will simply document the use
+	of the SetEnv directive, and setting the OPENSSL_CONF environment
+	variable, in conjunction with adding calls to OPENSSL_config(3) in
+	these respective modules.
+
+2015-07-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/crypto.c, contrib/mod_tls.c: Use the OpenSSL
+	config file; it's probably used by the configured ENGINE.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in: Adding a few more compiler warnings, when
+	--enable-devel is used.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_auth_pam.c: Finally clean up compiler warnings which
+	had been bugging me for quite a while.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Quell some compiler warnings.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kex.c: When computing the size of the DH needed,
+	we also need to pay attention to the negotiated cipher block sizes,
+	not just the cipher key lengths.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kex.c: Remove now-unused macro value.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_sftp_sql.html: Remove now-unused keywords.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm: Minor tweaks
+	when testing out some use cases.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/keystore.c: Fix the trace log message when
+	handling SFTPAuthorizedUserKeys to properly include the store type.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c, contrib/mod_sftp/channel.c,
+	contrib/mod_sftp/service.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: When constructing the
+	cmd_recs, do it properly, so that ExtendedLog entries, e.g. for the
+	%r variable, log the request properly.  The cmd_recs allocated did
+	not set the correct cmd_rec.argc value.
+
+2015-07-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp_sql.c: Remove extraneous (and misleading) trace
+	logging.
+
+2015-07-02  TJ Saunders <tj at castaglia.org>
+
+	* src/dirtree.c: Found a place where we should have been using
+	"_bind_", not just "_bind".
+
+2015-07-01  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/TestSuite/Utils.pm: More testsuite help when
+	writing out subsections of e.g. <IfModule> config sections.
+
+2015-07-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #141 from proftpd/core-class-add-note Add a function for attaching notes to a class, e.g. from a config
+	dir…
+
+2015-07-01  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kex.c: Always make sure we're using this kex's
+	pool, not the overall Kex API pool, to prevent memory leaks.
+
+2015-06-30  tjsaunders <tj at lyveminds.com>
+
+	* : commit 98541b4a198b3f49721bd8a7d6d1894e52981623 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Tue Jun 30 10:06:54 2015 -0700
+
+2015-06-30  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/mod_sftp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Be more
+	defensive/picky about the client version string handling from
+	connecting SSH2 clients.
+
+2015-06-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Impose a limit of 512K on the size of a
+	single SFTP packet.  For comparison, OpenSSH imposes a 256K limit.
+
+2015-06-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Impose a limit (much higher than ever
+	expected) on the number of EXTENDED attributes that we'll read from
+	an SFTP client.
+
+2015-06-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Be more consistent about the handling of
+	errno values, especially across function calls.
+
+2015-06-29  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES: Update release notes to mention SFTP hardlink
+	extension support.
+
+2015-06-29  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #140 from proftpd/sftp-hardlink-extension Support the SFTP "hardlink at openssh.com" extension.
+
+2015-06-29  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES: Update release notes for LINK_COUNT SFTP STAT
+	support.
+
+2015-06-29  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #139 from proftpd/sftp-hardlink-stat-attr Support the SFTP STAT link-count attribute if clients are using a
+	new…
+
+2015-06-26  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in: More tweaks to the module build summary
+	display, to properly display shared modules.
+
+2015-06-26  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for Bug#4153.
+
+2015-06-26  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #137 from proftpd/sftp-multi-auth-bug4153 Bug#4153 - Support requiring multiple SSH authentication methods
+
+2015-06-26  tjsaunders <tj at lyveminds.com>
+
+	* : commit 250ec7f47628c1eb0b518b1311b709860c78ba0e Author:
+	tjsaunders <tj at lyveminds.com> Date:   Fri Jun 26 13:04:34 2015 -0700
+
+2015-06-26  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #138 from
+	proftpd/tls-protocol-exclusion-bug4189 Bug#4189 - Support protocol exclusion via TLSProtocol directive.
+
+2015-06-25  tjsaunders <tj at lyveminds.com>
+
+	* : commit d77feab0d434db2273bc16074f75106646a17d90 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Thu Jun 25 15:28:11 2015 -0700
+
+2015-06-25  tjsaunders <tj at lyveminds.com>
+
+	* : commit 4372ae9463913ca03ba24b6c15dd590699a5989d Author:
+	tjsaunders <tj at lyveminds.com> Date:   Thu Jun 25 15:26:52 2015 -0700
+
+2015-06-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/auth.c, contrib/mod_sftp/auth.h,
+	contrib/mod_sftp/mod_sftp.c: Guard against auth chains which are
+	unsupportable, i.e. chains that have "password" or "hostbased"
+	appearing multiple times.
+
+2015-06-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/auth-kbdint.c: Just as we require different
+	publickey authentications, we now require different
+	keyboard-interactive driver authentications in auth chains.
+
+2015-06-24  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.h: Found another place to use auth "chain"
+	instead of "list".
+
+2015-06-24  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c, contrib/mod_sftp/mod_sftp.c: Use "auth
+	chain" terminology more consistently throughout the code.
+
+2015-06-24  tjsaunders <tj at lyveminds.com>
+
+	* .gitignore: Ignore a few more file extensions.
+
+2015-06-24  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_sftp.html: Update mod_sftp SFTPAuthMethods
+	description for new auth chain syntax.
+
+2015-06-24  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/auth-hostbased.c,
+	contrib/mod_sftp/auth-kbdint.c, contrib/mod_sftp/auth-password.c,
+	contrib/mod_sftp/auth-publickey.c, contrib/mod_sftp/auth.c,
+	contrib/mod_sftp/auth.h, contrib/mod_sftp/mod_sftp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Implemented checking
+	of the configured auth chains at startup, to catch chains that use a
+	method (e.g. "hostbased") that cannot be supported (e.g.  no
+	SFTPAuthorizedHostKeys configured).  Implemented restriction that publickey authentication requests must
+	use different keys; duplicate keys will be rejected.  This supports
+	authentication chains such as "publickey+publickey".
+
+2015-06-24  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c, contrib/mod_sftp/auth.h,
+	contrib/mod_sftp/mod_sftp.c: Use the term "auth chain" rather than
+	"auth list", as "chain" is, in my mind, more accurate.
+
+2015-06-24  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c: Fix the handling of the next
+	authentication methods in the chain.
+
+2015-06-23  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c, contrib/mod_sftp/mod_sftp.c: Catch
+	unknown/unsupported authentication methods properly, and report them
+	properly, too.
+
+2015-06-23  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c: Catch other misconfigurations such as
+	double '+' characters in an SFTPAuthMethods list.
+
+2015-06-23  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c: Fix the check used for detecting trailing
+	'+' characters in a SFTPAuthMethods list.
+
+2015-06-23  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth.c, contrib/mod_sftp/auth.h,
+	contrib/mod_sftp/kbdint.h, contrib/mod_sftp/mod_sftp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Checking in the bulk
+	of the work needed for Bug#4153.  There is still a fair amount of
+	testing to be done, but the initial tests are looking good.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_auth.html, modules/mod_auth.c, src/regexp.c,
+	tests/t/lib/ProFTPD/Tests/Config/AnonRejectPasswords.pm: Make it
+	possible to use the AnonRejectPasswords directive to require
+	email-like anonymous passwords.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for fix for Bug#4139.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #134 from
+	proftpd/auth-allow-empty-password-bug4139 Bug#4139 - Support rejecting empty passwords.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ban.c, doc/contrib/mod_ban.html: Document the banning
+	of hosts using empty passwords.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES, doc/modules/mod_auth.html, modules/mod_auth.c,
+	tests/t/lib/ProFTPD/Tests/Logins.pm: Add integration tests and
+	documentation.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ban.c, contrib/mod_sftp/auth-password.c,
+	modules/mod_auth.c: Bug#4139 - Support rejecting empty passwords.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updating NEWS for Bug#4151.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #132 from
+	proftpd/data-ascii-conversion-bug4151 Bug#4151 - FTP ASCII mode conversion algorithm is painfully slow.
+
+2015-06-08  TJ Saunders <tj at castaglia.org>
+
+	* Make.rules.in, include/ascii.h, include/data.h, src/ascii.c,
+	src/data.c, tests/Makefile.in, tests/api/ascii.c, tests/api/data.c,
+	tests/api/tests.c, tests/api/tests.h,
+	tests/t/lib/ProFTPD/Tests/Commands/STOR.pm, utils/utils.h: Refactor
+	the ASCII CRLF handling code into its own API, unentangling it from
+	the implementation of the data transfers.  This makes it much easier
+	to unit test.  Added those unit tests, including a regression test for uploading a
+	file as ASCII.
+
+2015-06-05  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #133 from maytechnet/master Fix small copy/paste leftower
+
+2015-06-04  TJ Saunders <tj at castaglia.org>
+
+	* include/data.h, src/data.c, tests/api/data.c: Make the FTP ASCII
+	translation function symbols publicly visible, for accessing
+	directly for unit testing.  Removed misleading comment; history is what git is for.  Moved
+	potentially expensive pr_signals_handle() outside of the
+	byte-copying loop; we will handle any potential SIGSEGV signals
+	after the loop.
+
+2015-06-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/data.c: Not yet ready for these tests to be run.
+
+2015-06-04  TJ Saunders <tj at castaglia.org>
+
+	* tests/Makefile.in, tests/api/data.c, tests/api/stubs.c,
+	tests/api/tests.c, tests/api/tests.h: Start working on unit tests
+	for the Data API.  This adds the build glue to do so; now to fill in
+	the testcases.
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* src/data.c: Bug#4151 - FTP ASCII mode conversion algorithm is
+	painfully slow.
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updated NEWS for fix for Bug#4188.
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #131 from proftpd/geoip-multi-expr-bug4188 Bug#4188 - Support filtering based on country code and regional code
+	in
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for fix for Bug#4167.
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #98 from proftpd/core-crlf-in-path-bug4167 Bug#4167 - CR/LF characters are not supported in filenames.
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* src/main.c: Typo in comment; no functional change.
+
+2015-06-03  TJ Saunders <tj at castaglia.org>
+
+	* : commit 621000469e1f0b11486d2188f50eee87a33bb280 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Wed Jun 3 22:30:51 2015 -0700
+
+2015-06-02  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_geoip.html: Update the mod_geoip documentation
+	with FAQ describing the use case of Bug#4188.
+
+2015-06-02  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_geoip.c: Bug#4188 - Support filtering based on country
+	code and regional code in mod_geoip.
+
+2015-05-31  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/interop.c: Per this forums post, at least one
+	SSH2 client identifying itself as J2SSH does not support/handle
+	rekey requests properly:   https://forums.proftpd.org/smf/index.php/topic,11757.0.html Thus add it to the builtin interop lists as a non-rekeying client.
+
+2015-05-31  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updated NEWS for fix of Bug#4187.
+
+2015-05-31  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #129 from proftpd/geoip-table-loading-bug4187 Bug#4187 - mod_geoip does not load all of the GeoIPTables properly.
+
+2015-05-30  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_geoip.c: Bug#4187 - mod_geoip does not load all of the
+	GeoIPTables properly.
+
+2015-05-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #128 from proftpd/tls-adaptive-buffering Implement adaptive tuning of the TLS record/buffer size for data
+	tran…
+
+2015-05-27  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_dso.html: Adding mod_dso FAQ, from the online
+	forums.
+
+2015-05-27  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, contrib/dist/rpm/proftpd.spec, include/version.h: Update
+	files for rc2 status.
+
+2015-05-27  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, contrib/dist/rpm/proftpd.spec, include/version.h: Warming up
+	for a release of 1.3.6rc1.
+
+2015-05-27  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #80 from proftpd/core-schmod-cifs-bug4134 Bug#4134 - Unable to create folders using FTP on a CIFS mounted
+	share: "...
+
+2015-05-25  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES, contrib/mod_sftp/kex.c,
+	contrib/mod_sftp/mod_sftp.c, contrib/mod_sftp/mod_sftp.h.in,
+	doc/contrib/mod_sftp.html: Update mod_sftp to avoid weak DH groups,
+	per Bug#4184.
+
+2015-05-22  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_auth.c: Attempt to address Debian Bug#717235:   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=717235
+
+2015-05-22  tjsaunders <tj at lyveminds.com>
+
+	* configure, configure.in: Fix the build summary, so that it
+	displays modules, like mod_sftp or mod_snmp, which have their own
+	module directories.  Also skip the module listing if there are no
+	static (or no shared) modules to list in the summary.
+
+2015-05-22  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES: Mention the new ServerAlias directive in the
+	release notes, too.
+
+2015-05-22  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES, doc/modules/mod_core.html: Update NEWS,
+	release notes for HOST support.  Add docs for the new ServerAlias
+	directive.
+
+2015-05-22  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #126 from proftpd/core-host-cmd-bug3289 Bug#3289 - Support the HOST command
+
+2015-05-22  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/TLS.html: Tweaks to the TLS FAQ for FZ.
+
+2015-05-21  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/TLS.html: Add FAQs about FTPS connection issues with
+	FileZilla, lftp to the TLS howto.
+
+2015-05-21  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #125 from proftpd/tls-weak-dh-bug4184 Bug#4184 - Remove support for "weak" Diffie-Hellman groups.
+
+2015-05-12  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Nit.
+
+2015-05-12  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/HOST.pm: Adding regression test
+	showing successful login to name-based vhost.
+
+2015-05-04  TJ Saunders <tj at castaglia.org>
+
+	* : commit 8e6f14aa2f6141b632b48e1b063dde688cbeec62 Merge: 3b93982
+	64f28f1 Author: TJ Saunders <tj at castaglia.org> Date:   Mon May 4
+	19:34:33 2015 -0700
+
+2015-05-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #124 from proftpd/tls-ssl-buffer-sizing Tweak the SSL write buffer/record sizes during TLS handshakes, to
+	try to
+
+2015-04-30  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #123 from proftpd/tls-npn-alpn Support/use the ALPN/NPN TLS extensions in mod_tls
+
+2015-04-30  tjsaunders <tj at lyveminds.com>
+
+	* config.h.in, configure, configure.in, contrib/mod_tls.c: Add
+	support for the NPN and ALPN extensions to mod_tls, to support any
+	FTPS clients which might use these as heuristics for determining
+	when to use TLS False Start (e.g. Chrome/Firefox).
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/HOST.pm: Tweaks.
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Stylistic nits, compiler warnings; no
+	functional change.
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* src/bindings.c: Fix null pointer derefence encountered during
+	regression testing the HOST command.
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* doc/howto/Tracing.html, src/modules.c, src/trace.c: Add a 'module'
+	trace channel, for tracing the Module API.
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, modules/mod_core.c: Implement handling of the
+	TLS SNI extension, so that we can then double-check that (if used)
+	against the provided HOST command (and vice versa).
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* include/bindings.h, src/bindings.c: Fix bad merge.
+
+2015-04-29  tjsaunders <tj at lyveminds.com>
+
+	* : commit 2bde9ba89eb007778a940f1eae60f2fd22102195 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Wed Apr 29 14:46:09 2015 -0700
+
+2015-04-29  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #122 from proftpd/tls-session-reuse-bug4178 Bug#4178: When accepting a TLS connection for data transfers,
+	disable th...
+
+2015-04-28  tjsaunders <tj at lyveminds.com>
+
+	* src/data.c: Fix segfault due to null pointer dereference.
+
+2015-04-28  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Use the SSL options to work around a Safari
+	ECDSA bug.
+
+2015-04-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Enable SSL session cache flushing on session
+	exit, to keep that cache as groomed as possible.
+
+2015-04-27  TJ Saunders <tj at castaglia.org>
+
+	* .gitignore: Ignore a few more files/paths.
+
+2015-04-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Style nits; no functional change.
+
+2015-04-26  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Refactor the random PSK generation code into a
+	single helper function, for good style/DRY princicple.
+
+2015-04-26  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix the handling of the TLSECDHCurve directive.
+
+2015-04-26  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #121 from proftpd/tls-psk-support Bug#4174 - Support for TLS-PSK (pre-shared keys)
+
+2015-04-26  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES, doc/contrib/mod_tls.html: Adding
+	documentation for the new TLSPreSharedKey directive.
+
+2015-04-26  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Finish implementing TLS-PSK support.
+
+2015-04-25  tjsaunders <tj at lyveminds.com>
+
+	* : commit 569ebe9163e7d5cedcbf54215ced625a77aeab6b Merge: 9f1078f
+	46c8a0c Author: TJ Saunders <tj at castaglia.org> Date:   Sat Apr 25
+	19:30:49 2015 -0700
+
+2015-04-25  tjsaunders <tj at lyveminds.com>
+
+	* doc/contrib/mod_tls.html: Improve the TLSECDHCurve docs by
+	including the openssl command to use, to see the full list of
+	supported curves.
+
+2015-04-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Make sure that mod_tls compiles properly when
+	using OpenSSL-1.0.2 or later.
+
+2015-04-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c, doc/contrib/mod_tls.html: Better error
+	reporting if the configured curve name isn't known.  Add TLSOption
+	for disabling auto-ECDH negotiation behavior, as a future-proof
+	safety switch.
+
+2015-04-25  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES, contrib/mod_tls.c, doc/contrib/mod_tls.html: Add a
+	new TLSECDHCurve directive, for specifying the curve to use for
+	ECDHE ciphers.
+
+2015-04-25  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for Bug#3125.
+
+2015-04-25  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #119 from proftpd/facl-macosx-bug3125 Bug#3125 - Support for Mac OS X implementation of POSIX ACLs.
+
+2015-04-25  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Only log a trace message about adjusting the DH
+	parameter length if we indeed to adjust/change the length.
+
+2015-04-24  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Properly handle multiple TLSDHParamFiles, if
+	configured.  Adjust the DH parameter length to be the size of the RSA/DSA private
+	key, for greater strength, rather than using the given length (which
+	will only ever be 512 or 1024 bits, even for 2048-bit server
+	certs/keys).  If OpenSSL-1.0.2 or later are used, automatically negotiate the best
+	EC, rather than always falling back to the prime256v1 group.
+
+2015-04-22  TJ Saunders <tj at castaglia.org>
+
+	* src/privs.c: Change the logging priority based on errno
+	consistently.
+
+2015-04-20  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #118 from proftpd/core-stash-remove-symbol Add symbol type-specific remove functions, for use by modules (e.g.
+	mod_...
+
+2015-04-19  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/FXP.html, doc/modules/mod_core.html: Update references
+	in FXP howto to point to local mod_core docs, and add the missing
+	descriptions to the mod_core docs.
+
+2015-04-19  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/Directory.html, doc/modules/mod_core.html: Add
+	description of AllowOverride directive, and fix up the Directory
+	howto to point to that description.
+
+2015-04-19  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/rewrite.pm: Fix
+	mod_sftp+mod_rewrite tests for Mac OSX.
+
+2015-04-16  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/LogMessages.html: Fix nits in LogMessages doc.
+
+2015-04-15  tjsaunders <tj at lyveminds.com>
+
+	* README.md: Add links/badges for Travis, Coverity.
+
+2015-04-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #116 from rk295/patch-1 Changed the version string to reflect 1.3.6rc1
+
+2015-04-15  Gildásio Júnior <gildasio97 at gmail.com>
+
+	* README, README.md: Update and rename README to README.md 1. Use md extention because use markdown sintaxe; 2. Update in some sections with markdown "tags"
+
+2015-04-14  Robin Kearney <robin at kearney.co.uk>
+
+	* contrib/dist/rpm/proftpd.spec: Changed the version string to
+	reflect 1.3.6rc1
+
+2015-04-12  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Clear the statcache appropriately for
+	READLINK requests.  Add a note for my future self to NOT make the
+	mistake of using a resolved path (again) when making a call to
+	readlink(2).
+
+2015-04-12  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Fix a few more
+	tests for running on a Mac OSX.
+
+2015-04-12  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/sql.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_sql.pm: More fixing up
+	of tests.
+
+2015-04-12  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/FEAT.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/OPTS.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/SIZE.pm,
+	tests/t/lib/ProFTPD/Tests/Config/DefaultChdir.pm,
+	tests/t/lib/ProFTPD/Tests/Config/Directory/Limits.pm,
+	tests/t/lib/ProFTPD/Tests/Config/FactsOptions.pm,
+	tests/t/lib/ProFTPD/Tests/Config/Limit/RMD.pm,
+	tests/t/lib/ProFTPD/Tests/Config/MaxClients.pm,
+	tests/t/lib/ProFTPD/Tests/Config/ShowSymlinks.pm,
+	tests/t/lib/ProFTPD/Tests/Config/Umask.pm,
+	tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: Fixing up some of
+	the regression tests to either work on Mac OSX, or to match the
+	current code.  Still more tests need updating.
+
+2015-04-11  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_facts.c: Now that the statcaches have been separated
+	for stat(2) vs lstat(2), this second clearing of the statcaches is
+	no longer needed.
+
+2015-04-11  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: I found that we need to maintain two different
+	statcache tables: one for stat(2) data, and one for lstat(2) data.
+	The data can be different, for the same path, depending on whether
+	it's a symlink or not.
+
+2015-04-11  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_facts.c, src/dirtree.c: Running through the regression
+	tests has picked up a few necessary fixes.  Expect more.
+
+2015-04-10  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updated NEWS for Bug#4168.
+
+2015-04-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #110 from
+	proftpd/xfer-hiddenstores-races-bug4168 Bug#4168 - Race condition with HiddenStores and TimeoutIdle timeout.
+
+2015-04-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #114 from
+	proftpd/sftp-realpath-ctrl-byte-bug4170 Bug#4170 - Incorrect handling of control-byte field of
+	SSH_FXP_REALPATH ...
+
+2015-04-09  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/Radius.html: Update Radius howto to mention new
+	attributes supported by mod_radius.
+
+2015-04-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/ftpasswd, modules/mod_auth_file.c: Bug#4171 - Add checks
+	to mod_auth_file for crypt(3) failures due to the hash algorithm
+	used, and make ftpasswd check for times when --des/--md5 will not be
+	sufficient.
+
+2015-04-09  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/fxp.c: Try to implement more of the SFTP protocol
+	version 6 semantics for REALPATH's control byte.
+
+2015-04-09  tjsaunders <tj at lyveminds.com>
+
+	* : commit 8c1412153a13f51c63070c2ac67c7a922b1a9831 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Thu Apr 9 00:18:17 2015 -0700
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Fix copy/pasto, spotted by Coverity.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/kex.c: Remove unnecessary NULL check, per
+	Coverity.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* src/str.c, tests/api/str.c: Make sure that pr_str_is_fnmatch()
+	properly handles NULL arguments.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* lib/glibc-glob.c: Initialize the 'next' value explicitly, so that
+	things happen properly in the error handling code.  Found by
+	Coverity.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_radius.c: Handle the size where the size of an int may
+	not be exactly 4 bytes; on some platforms, it might actually be
+	larger.  So be specific about the number of bytes we copy for the
+	vendor ID of a VSA.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_site.c, tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm: 
+	Fix some typos in the handling of symbolic mode expressions for SITE
+	CHMOD commands, and start adding some symbolic SITE CHMOD regression
+	tests.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_delay.c: When shutting down and preserving the
+	DelayTable, make the handling a little more resilient, handling
+	(very unlikely but possible) error cases better, per Coverity.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_snmp/mod_snmp.c: Remove unnecessary NULL check, per
+	Coverity.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_core.c: Handle NULL return values from pr_expr_create,
+	and make Coverity happier.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* src/inet.c: Make the fd checks in pr_inet_openrw() a little less
+	brittle by checking for any valid-looking fd (i.e. > -1), rather
+	than just directly comparing against -1.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/coverity/modeling.c: Tell Coverity to ignore
+	rand(3)/random(3); we know what we're doing, and do not use it for
+	cases where cryptographically strong random numbers are needed.  So
+	please, Coverity, stop marking its use as defects.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/keys.c, contrib/mod_tls.c: Make sure that we
+	NUL-terminate the buffer we gave to read(2), for passphrase
+	collection, before doing anything else with the buffer.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c: Check for possible NULL value before calling glob(3),
+	lest it segfault on us.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c: Remove unnecessary NULL check, per Coverity.
+
+2015-04-08  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Bug#4170 - Incorrect handling of
+	control-byte field of SSH_FXP_REALPATH as bitmask rather than
+	enumeration.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/auth.c: Handle possibly (albeit unlikely) NULL
+	return value.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_radius.c: Slightly more paranoid parsing of RADIUS
+	variable formatted strings, as configured in the config file.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_ifsession.c: The <If...> sections using regexes might
+	be malformed; the configuration handler needs to handle this error
+	case properly.
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* : commit ac1a6a4dc9d34703dd5a438336cb53c0fed33d10 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Wed Apr 8 00:25:07 2015 -0700
+
+2015-04-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #112 from proftpd/coverity-defects Moew Coverity defects
+
+2015-04-08  tjsaunders <tj at lyveminds.com>
+
+	* contrib/coverity/modeling.c, lib/tpl.c, lib/tpl.h: Tweak the TPL
+	API a little, so that we can mark it for visibility by Coverity.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Quell warning about uninitialized variable
+	being used.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c: Better NULL checking.  Also clear the statcache in the
+	case of renames.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_exec.c, contrib/mod_radius.c, lib/tpl.c,
+	modules/mod_ls.c, modules/mod_site.c: Fixing up more issues spotted
+	by Coverity.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c: Fix dead code spotted by Coverity.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_snmp/packet.c: Removing some logically dead code
+	branches spotted by Coverity.
+
+2015-04-07  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #111 from proftpd/coverity-defects Quell some of the warnings/issues spotted by Coverity.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* : commit 6bb2e06513363a256802ebb250ff05a1d9fd3208 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Tue Apr 7 22:03:04 2015 -0700
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_core.c, modules/mod_xfer.c: Use PR_HANDLED, not
+	HANDLED.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_copy.c: Use PR_DECLINED, not DECLINED.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES: Tweaks/edits to release notes.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES: More fleshing out of the release notes.  We're
+	getting close to release-ready.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES: Start fleshing out the release notes, getting ready
+	for a release.
+
+2015-04-07  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c: Bug#4168 - Race condition with HiddenStores
+	and TimeoutIdle timeout.  Attempt to reduce/mitigate this race by blocking timers during
+	critical sections of transfer setup code, and handle some side
+	effects in the exit handler by using other criteria for determining
+	when/how to handle e.g. HiddenStores files.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/Makefile.in, contrib/mod_sftp/umac.c: Fix
+	building of umac128.{o, lo}, and the resulting symbol collision for
+	a DSO build of mod_sftp.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* src/inet.c: Fix compilation when the --disable-ipv6 compile-time
+	option is used.
+
+2015-04-07  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES: Updating NEWS, release notes for Bug#4169.
+
+2015-04-07  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #109 from
+	proftpd/copy-unauthenticated-copying Bug#4169 - Unauthenticated copying of files via SITE CPFR/CPTO
+	allowed by mod_copy
+
+2015-04-06  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, contrib/mod_sftp/fxp.c: Update NEWS with mention of fix for
+	Bug#4166.
+
+2015-04-06  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #97 from
+	proftpd/sftp-rekeying-memory-usage-bug4166 Bug#4166 - mod_sftp sessions consume large amounts of memory due to
+	reke...
+
+2015-04-05  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #108 from proftpd/sftp-umac-128 Support for "umac-128 at openssh.com" SSH MAC algorithm
+
+2015-04-05  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/Makefile.in, contrib/mod_sftp/crypto.c,
+	contrib/mod_sftp/mac.c, contrib/mod_sftp/umac.c,
+	contrib/mod_sftp/umac.h: Add support for the umac-128 at openssh.com
+	SSH MAC.
+
+2015-04-05  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kbdint.c: Fix memory leak when handling
+	'keyboard-interactive' SSH authentication.
+
+2015-04-05  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_memcache.c: Tone down the logging message emitted
+	when we try to register a handler and there's no memcache support
+	enabled in the build.
+
+2015-04-04  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes with mention of
+	Bug#4059.
+
+2015-04-04  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #107 from
+	proftpd/radius-addl-attributes-bug4059 Bug#4059 - Implement additional RADIUS attributes
+
+2015-04-03  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_radius.c, doc/contrib/mod_radius.html: Bug#4059 -
+	Implement additional RADIUS attributes.
+
+2015-04-03  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp_pam.c: Stylistic nit cleanup in mod_sftp_pam, no
+	functional changes.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES: Mention the new FSCachePolicy directive in the
+	release notes.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_sftp.html: Fix the spacing in the mod_sftp docs
+	referring to RFCs.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #106 from proftpd/site-misc-resp-code-setting Fix the setting of FTP response codes for the internal commands that
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #105 from proftpd/site-misc-extlog-resp-code Add regression test for ExtendedLog handling of the response code
+	for SITE commands
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Found a few more places in mod_sftp that
+	need to clear the statcache for accurate stat/lstat results.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* : commit f88556af8c9395873f75fc43e5f779648b5de0c3 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Thu Apr 2 11:25:48 2015 -0700
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/table.c: Add a unit test for executing pr_table_do() on
+	a table, with a callback that removes items from the table during
+	the iteration.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Clear the statcache when files are created/modified
+	via creat(2) and open(2) calls as well.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, tests/api/fsio.c: Clear the statcache when a file is
+	closed or unlinked.  Flesh out the pr_fsio_stat()/pr_fsio_lstat() unit tests to catch
+	regressions such as the previous statcache issue, where the
+	statcache was not being populated with the struct stat information.
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_site_misc.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_site_misc.pm: Adding
+	integration test demonstrating the proper handling of the %s
+	ExtendedLog variable for SITE commands, e.g. those provided by
+	mod_site_mis.c
+
+2015-04-02  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #104 from proftpd/core-fscacheoptions Add FSCachePolicy directive, for tuning statcache
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* : commit 36ed1cb9901e041e88b0a96e8fb4bd13defea24f Merge: b65700f
+	9d45a71 Author: TJ Saunders <tj at castaglia.org> Date:   Wed Apr 1
+	23:35:15 2015 -0700
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Set errno to the proper value in the case of
+	stat(2)/lstat(2) failure.
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/misc.c: Adding missing format string.
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ls.c: Remove debugging cruft.
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ls.c, src/fsio.c: Add missing memcpy(2) call that was
+	causing FTP directory listings to fail.
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ls.c, src/dirtree.c, src/fsio.c, src/support.c: 
+	Checking in some changes while I debug why the statcaching is
+	breaking FTP directory listings.
+
+2015-04-01  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_core.c: Start working on an FSCacheOptions directive,
+	for tuning the newly-added statcache.
+
+2015-04-01  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/fxp.c: Properly increment the st_size on SFTP
+	WRITE, taking into account possible overwrites of existing chunks of
+	the file.
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #100 from proftpd/fsio-stat-caching Change the FSIO API's statcache from a single entry to one using
+	tables.
+
+2015-04-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #101 from proftpd/sftp-fstat-on-write-perf Avoid an fstat(2) call per WRITE request, to see if it helps with
+	perfor...
+
+2015-04-01  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c, tests/api/fsio.c: Dealing with test fallout, and
+	quelling some compiler warnings spotted by travis-ci.
+
+2015-04-01  tjsaunders <tj at lyveminds.com>
+
+	* src/str.c: Remove some unused variables (spotted by travis-ci).
+
+2015-04-01  tjsaunders <tj at lyveminds.com>
+
+	* src/fsio.c: Make sure that pr_fsio_access() doesn't clear the
+	statcache when it doesn't have to; that policy should be up to the
+	caller, not to the implementation.
+
+2015-04-01  tjsaunders <tj at lyveminds.com>
+
+	* include/fsio.h, modules/mod_core.c, src/dirtree.c, src/fsio.c,
+	tests/api/fsio.c: Disable statcaching until the client has
+	authenticated, at which point we will start caching.
+
+2015-03-31  tjsaunders <tj at lyveminds.com>
+
+	* : commit fc5dda6054a982a858add1fa63addb48fbdbb9f4 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Tue Mar 31 23:33:53 2015 -0700
+
+2015-03-31  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_copy.c, contrib/mod_quotatab.c,
+	contrib/mod_rewrite.c, contrib/mod_sftp/fxp.c,
+	contrib/mod_sftp/misc.c, contrib/mod_site_misc.c,
+	contrib/mod_snmp/mod_snmp.c, contrib/mod_sql.c, include/fsio.h,
+	include/options.h, modules/mod_auth.c, modules/mod_core.c,
+	modules/mod_facl.c, modules/mod_facts.c, modules/mod_log.c,
+	modules/mod_ls.c, modules/mod_xfer.c, src/dirtree.c, src/fsio.c,
+	src/mkhome.c, src/support.c, src/table.c, tests/api/fsio.c: Change
+	the FSIO API's statcache from a single entry to one using tables.
+	This cache now holds up to a configurable size, with expiration, and
+	allows clearing of single entries (or the entire cache if needed).
+
+2015-03-31  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/fxp.c: Fix bug where we were allocating a cmd_rec
+	out of the wrong pool when handling REMOVE requests.
+
+2015-03-31  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/fxp.c: Slight logging tweaks; no functional
+	change.
+
+2015-03-31  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with mention of Bug#4157.
+
+2015-03-31  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #96 from proftpd/core-tracing-perf-bug4157 Bug#4157 - LIST/NLST of 1000s of files is slow on some platforms.
+
+2015-03-30  TJ Saunders <tj at castaglia.org>
+
+	* src/bindings.c: Remove now-unused variable.
+
+2015-03-30  TJ Saunders <tj at castaglia.org>
+
+	* include/bindings.h, src/bindings.c: Fix up the
+	ServerAlias/namebind handling which was causing spurious NOTICE
+	level log messages in 1.3.5.
+
+2015-03-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Make sure to set the curr_pkt pointer to
+	NULL in the case of an unknown request, too.
+
+2015-03-29  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_dso.c, src/auth.c, src/inet.c, src/netio.c: Add
+	missing tags on some pools, fix typos.  Nothing major.
+
+2015-03-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c: Found a place where we need to clear a
+	pointer to a packet explicitly. Failing to do so could mean that a
+	packet is reused, thus leading to leaking memory (as that packet
+	pool is never actually freed/reused).
+
+2015-03-28  TJ Saunders <tj at castaglia.org>
+
+	* include/support.h, modules/mod_ls.c, src/support.c: Additional
+	fixes for Bug#4157, trying to remove a redundant lstat(2) call
+	during directory listings that include symlinks.
+
+2015-03-28  TJ Saunders <tj at castaglia.org>
+
+	* : commit fd68ce118083fea2e1add58ae6e43413aa0b408e Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sat Mar 28 15:48:31 2015 -0700
+
+2015-03-28  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, src/netio.c, src/response.c,
+	tests/api/netio.c, tests/t/lib/ProFTPD/Tests/Commands/MKD.pm: 
+	Additional changes needed for handling embedded LFs in paths, for
+	Bug#4167.
+
+2015-03-28  TJ Saunders <tj at castaglia.org>
+
+	* include/netio.h, src/main.c, src/netio.c, src/str.c,
+	tests/api/netio.c, tests/t/lib/ProFTPD/Tests/Commands/MKD.pm: 
+	Bug#4167 - CR/LF characters are not supported in filenames.
+
+2015-03-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/fxp.c: Avoid an fstat(2) call per WRITE request,
+	to see if it helps with performance writing files on network
+	filesystems like NFS.
+
+2015-03-25  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Slightly better trace messages for SSL
+	WANT_READ/WANT_WRITE conditions.
+
+2015-03-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/mod_sftp.c: Only dump the memory pools if
+	mod_shaper is NOT present.
+
+2015-03-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kex.c: Additional changes for Bug#4166, to reduce
+	the memory leaked during an SSH2 rekeying.
+
+2015-03-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c, contrib/mod_sftp/kex.c,
+	contrib/mod_sftp/mod_sftp.c, modules/mod_xfer.c: Bug#4166 - mod_sftp
+	sessions consume large amounts of memory due to rekeying.  In order to debug this, I had to add code to mod_sftp which handles
+	the SIGUSR2 signal by dumping the memory pools of the process.
+	This, in turn, required some slight adjustments to a similar
+	handling of SIGUSR2 that mod_xfer does, but which was only needed if
+	mod_shaper is present.
+
+2015-03-20  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ls.c, src/fsio.c, src/trace.c: Bug#4157 - LIST/NLST of
+	1000s of files is slow on some platforms.
+
+2015-03-20  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for Bug#4164.
+
+2015-03-20  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #95 from proftpd/sql-uid-size-bug4164 Bug#4164 - mod_sql fails to read UID/GID values larger than 32 bits
+	from SQL table
+
+2015-03-16  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_exec.c, contrib/mod_sftp/fxp.c, contrib/mod_sql.c,
+	modules/mod_cap.c, modules/mod_facts.c, src/main.c: Updating more
+	places to use the new functions for stringifying UID/GID values.
+	Display the max supported UID/GID value in the -V output.
+
+2015-03-16  tjsaunders <tj at lyveminds.com>
+
+	* : commit 005b8a5cccfa75109f738f2ecbb763225d1a76c3 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Mon Mar 16 14:05:11 2015 -0700
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* src/auth.c, src/privs.c: Update more places in the core code to
+	log UID/GID values properly.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sql.c: Fix up a few more places in mod_sql where
+	proper stringification of UID/GID needs to use the new routines.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: The bug has
+	number; it's number is 4164.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: Run all of
+	the mod_sql_sqlite tests, please.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* src/auth.c, src/str.c, tests/api/str.c: Bug#4164: Start using the
+	new functions for ID/string conversion in more places; there's still
+	more here to do.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* : commit 9b215b1530bf4839ddce6505d70d9e79a9c541c5 Author:
+	tjsaunders <tj at lyveminds.com> Date:   Sun Mar 15 17:27:51 2015 -0700
+
+2015-03-15  TJ Saunders <tj at castaglia.org>
+
+	* configure: Updated configure.
+
+2015-03-15  TJ Saunders <tj at castaglia.org>
+
+	* config.h.in, configure.in: Check for the strtoll(3) function, too.
+
+2015-03-15  TJ Saunders <tj at castaglia.org>
+
+	* configure: Updated configure.
+
+2015-03-15  TJ Saunders <tj at castaglia.org>
+
+	* config.h.in, configure.in: Add autoconf checks for the size of
+	uid_t/gid_t, as part of handling Bug#4164.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for Bug#4163.
+
+2015-03-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #94 from
+	proftpd/tls-no-export-ciphers-bug4163 Bug#4163 - Remove support for EXPORT grade ciphers.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/fxp.c: Fix two places with incorrect comparisons
+	against the utf8ProtocolVersion SFTPOption, per comments in
+	Bug#4160.
+
+2015-03-15  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS for Bug#4160.
+
+2015-03-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #93 from
+	proftpd/sftp-realpath-nocheck-v6-response-bug4160 Bug#4160 - Malformed response to SSH_FXP_REALPATH with SFTP version
+	6.
+
+2015-03-14  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS with fix for Bug#4156.
+
+2015-03-14  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #91 from proftpd/core-ls-segfault-bug4156 Bug#4156 - Segfault handling LIST/NLST FTP command on Mac OS X.
+
+2015-03-03  tjsaunders <tj at lyveminds.com>
+
+	* NEWS, RELEASE_NOTES: Updates NEWS, release notes for Bug#4159.
+
+2015-03-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #92 from proftpd/xfer-ignore-ascii-bug4159 Bug#4159: Support ability to disable ASCII translation transparently
+	to ...
+
+2015-03-01  TJ Saunders <tj at castaglia.org>
+
+	* include/fsio.h, lib/sstrncpy.c, src/fsio.c, tests/api/fsio.c,
+	tests/api/str.c: Bug#4156 - Segfault handling LIST/NLST FTP command
+	on Mac OS X.
+
+2015-03-01  TJ Saunders <tj at castaglia.org>
+
+	* src/main.c: Include PR_TUNABLE_PATH_MAX in the `proftpd -V'
+	output.
+
+2014-12-25  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for JSON output of
+	ftpwho (Bug#4031).
+
+2015-02-27  TJ Saunders <tj at castaglia.org>
+
+	* lib/pr_fnmatch_loop.c: Backport glibc fix for a memory overread
+	for certain fnmatch patterns.
+
+2015-02-19  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp_sql.c: Fix additional issue related to Bug#4155,
+	where the buffer overrun could happen for bad key data.  This also
+	adds a log message for the future, pinpointing the root of the
+	problem.
+
+2015-02-19  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm: Remove stray
+	comment character.
+
+2015-02-17  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/NLST.pm: Add a regression test
+	for the NLST command and how the -a and -1 options will affect the
+	format of the NLST response.
+
+2015-02-17  tjsaunders <tj at lyveminds.com>
+
+	* modules/mod_ls.c: Stylistic nits; no functional change.
+
+2015-02-17  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm: Adding mod_exec
+	regression test demonstrating how to use mod_exec and mod_ifsession.
+
+2015-02-17  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS with fix for Bug#4155.
+
+2015-02-17  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #89 from
+	proftpd/sftp-sql-overlong-commented-keys-bug4155 Bug#4155 - SSH keys with too-long Comment headers aren't recognized
+	by mod_sftp_sql
+
+2015-02-17  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/etc/modules/mod_sftp/test_rsa2048_key2,
+	tests/t/etc/modules/mod_sftp/test_rsa2048_key2.pub,
+	tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155,
+	tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155.pub,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm: Adding
+	accompanying regression tests for Bug#4155.
+
+2015-02-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp_sql.c: Bug#4155 - SSH keys with too-long Comment
+	headers aren't recognized by mod_sftp_sql.
+
+2015-02-17  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #88 from proftpd/ls-option-parsing Fix random issue with LIST option parsing
+
+2015-02-17  tjsaunders <tj at lyveminds.com>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/LIST.pm: Adding a regression
+	test for the "LIST -a" parsing issue raised on GitHub.
+
+2015-02-04  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS for fix for Bug#4152.
+
+2015-02-04  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #87 from
+	proftpd/core-enotconn-logging-bug4152 Bug#4152 - Reduce logging of non-fatal "unable to open incoming
+	connection" errors
+
+2015-02-04  tjsaunders <tj at lyveminds.com>
+
+	* src/main.c: Bug#4152 - Reduce logging of non-fatal "unable to open
+	incoming connection" errors.
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sql_passwd.c: Correct the pointer check used in an
+	error case; spotted by Coverity.
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_tls.c: Stylistic nits; no functional change.
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_radius.c: Stylistic cleanup; no functional changes.
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* contrib/mod_sftp/keys.c, contrib/mod_tls.c: Address some Coverity
+	issues by ensuring that a buffer, given to OpenSSL, is
+	NUL-terminated before calling e.g. strlen(3) on the buffer.
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* src/data.c: Clear the "have dangling CR" flag on data reset as
+	well.
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* src/data.c: Reset any ASCII transformation state, possibly
+	leftover from previous transfers, when preparing for a data transfer
+	(Bug#4150).
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* src/data.c: Use the return value from xfrm_ascii_write() as part
+	of a trace-level log message (Bug#4149).
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* src/data.c: Update stale comment (Bug#4148).
+
+2015-02-03  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS for Bug#4145.
+
+2015-02-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #86 from
+	proftpd/authfile-symlink-segfault-bug4145 Bug#4145 - Segfault if AuthUserFile is a relative symlink.
+
+2014-12-25  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #84 from proftpd/utils-ftpwho-json-bug4031  Bug#4031 - Support JSON output format for ftpwho
+
+2014-12-25  TJ Saunders <tj at castaglia.org>
+
+	* utils/ftpwho.1.in: Update the ftpwho man page to mention the new
+	json option.
+
+2014-12-25  TJ Saunders <tj at castaglia.org>
+
+	* include/tpl.h: Include renamed file.
+
+2014-12-25  TJ Saunders <tj at castaglia.org>
+
+	* configure.in, include/memcache.h, lib/json.c, lib/json.h,
+	lib/tpl.h, utils/ftpwho.c, utils/utils.h: Bug#4031 - Support JSON
+	output format for ftpwho.
+
+2014-12-24  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Updating NEWS, release notes for fix for
+	Bug#4144.
+
+2014-12-24  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #83 from
+	proftpd/xfer-appe-with-hiddenstores-bug4144 Bug#4144 - Support APPE when HiddenStores are enabled.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Adding missing trace logging of some FSIO operations.
+	Stylistic cleanup.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml, tests/api/fsio.c: Set the TRAVIS_CI environment
+	variable when it is being used to e.g. run the API tests.  Then use
+	this in the fsio_access() tests, to avoid running the tests which
+	run afoul of the travis-ci environment (and causing unexpected
+	false-positive test failures).
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: For the fsio_access() tests, use chmod(2)
+	directly on the created directory, rather than futzing with the
+	umask.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Comment out more of the fsio_access() tests, for
+	now.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* src/scoreboard.c: Quell compiler warning, found by travis-ci.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Comment out some of the just-added tests for
+	fsio_access(), pending investigation into travis-ci's umask et al.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/inet.c: Removing more unused variables; no functional
+	change.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/auth.c: Remove unused variable; no functional change.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* tests/api/fsio.c: Fixup use of variables; no functional change.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kex.c: Quell another compiler warning, found by
+	travis-ci.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_site_misc.c: Fix printf(3) format warning found by
+	travis-ci.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c, tests/api/fsio.c: Add unit test for the fsio_access()
+	function, as part of tracking down the culprit for Bug#4141.
+
+2014-12-22  TJ Saunders <tj at castaglia.org>
+
+	* src/dirtree.c: Add trace logging of various conditions under which
+	we'll hide a file, for debugging/triage.
+
+2014-12-20  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls_memcache.c: Reduce the severity of the "unable to
+	register memcache SSL session cache" message, from NOTICE to DEBUG;
+	it causes more confusion than aid.
+
+2014-12-17  tjsaunders <tj at lyveminds.com>
+
+	* RELEASE_NOTES, contrib/mod_ban.c, doc/contrib/mod_ban.html: Add
+	support to mod_ban, for banning on "bad protocol" detection.
+
+2014-12-15  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with fix for Bug#4143.
+
+2014-12-15  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #81 from proftpd/core-reject-http-smtp Bug#4143 - HTTPS/FTPS protocol confusion leads to XSS
+
+2014-12-15  tjsaunders <tj at lyveminds.com>
+
+	* src/cmd.c, src/main.c: Update the comments with the appropriate
+	bug number.
+
+2014-12-14  TJ Saunders <tj at castaglia.org>
+
+	* include/cmd.h, include/dirtree.h, include/session.h, src/cmd.c,
+	src/main.c, tests/t/http.t, tests/t/lib/ProFTPD/Tests/HTTP.pm,
+	tests/t/lib/ProFTPD/Tests/SMTP.pm, tests/t/smtp.t: If we detect HTTP
+	or SMTP commands, drop the connection.  This prevents proftpd from
+	being used as a staging server for nefarious attacks on other
+	protocol servers.
+
+2014-12-14  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Fix regression test
+	for Bug#4097 to run using current mod_sftp code.
+
+2014-12-14  TJ Saunders <tj at castaglia.org>
+
+	* : commit 1ba73d3d85bb837ae08eb1fd15d81d087385fc60 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Dec 14 18:57:16 2014 -0800
+
+2014-12-14  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Fixed quite a few
+	failing regression tests; many of them were failing (on Mac OSX) due
+	to tmp filesystem fun.
+
+2014-12-13  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_sftp.html: Add another mod_sftp FAQ about warnings
+	about reading certain config files.
+
+2014-12-09  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Remove the EPERM exclusion.
+
+2014-12-09  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Ignore EPERM, in the case where we see ENOENT.
+
+2014-12-09  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: If we encounter an ENOENT from fchmod(2) when securely
+	changing mode of a temporary directory, as from a network filesystem
+	such as CIFS, then we should probably ignore any EACCES errors said
+	filesystem might return as well.
+
+2014-12-09  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Make sure the fchmod(2) call fails, using ENOENT,
+	before deciding to fall back to chmod(2).
+
+2014-12-09  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_site.c: Fix build warnings about missing printf
+	arguments.
+
+2014-12-09  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Bug#4134 - Unable to create folders using FTP on a
+	CIFS mounted share: "No such file or directory".  The underlying cause of this bug is that, on a CIFS mount (perhaps
+	because of the mount options used), fchmod(2) on the temporary
+	directory returns ENOENT.  Weird.  So we address this by doing a) a
+	fallback call to chmod(2) on the path, and b) ignoring ENOENT.
+
+2014-12-08  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for fix for Bug#4140.
+
+2014-12-08  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #79 from
+	proftpd/sftp-readlink-symlinks-bug4140 Bug#4140 - SFTP READLINK requests to symlinks to directories fail.
+
+2014-12-08  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/fxp.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Bug#4140 - SFTP
+	READLINK requests to symlinks to directories fail.
+
+2014-12-07  TJ Saunders <tj at castaglia.org>
+
+	* src/auth.c: Log the value for TZ, if it is already present; this
+	will help with debugging issues such as Bug#4113.
+
+2014-12-06  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Updated NEWS, release notes for Bug#4138
+	changes.
+
+2014-12-06  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #78 from
+	proftpd/sql-passwd-salt-encodings-bug4138 Bug#4138 - Support for hex-encoded salts in mod_sql_passwd.
+
+2014-12-06  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_passwd.c, doc/contrib/mod_sql_passwd.html,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm: Bug#4138 -
+	Support for hex-encoded salts in mod_sql_passwd.
+
+2014-12-01  tjsaunders <tj at lyveminds.com>
+
+	* NEWS: Update NEWS with fix for Bug#4137.
+
+2014-12-01  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #76 from
+	proftpd/geoip-denyfilter-precedence-bug4137 Bug#4137 - GeoIPDenyFilter incorrectly takes precedence over
+	GeoIPAllowF...
+
+2014-11-30  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #77 from proftpd/ctrls-openbsd-portability Issue #75: Add configure-time check for struct sockpeercred (as used
+	on
+
+2014-11-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_geoip.c: Bug#4137 - GeoIPDenyFilter incorrectly takes
+	precedence over GeoIPAllowFilter.
+
+2014-11-16  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updated NEWS for fix for Bug#4133.
+
+2014-11-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #73 from
+	proftpd/ldap-users-missing-uid-filter-bug4133 Bug#4133 - LDAPUsers directive does not honor
+	uid-number-filter-templat...
+
+2014-11-16  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/keys.c: Make sure that
+	sftp_keys_clear_ecdsa_hostkey() has proper semantics, and will
+	return ENOENT if ECC is supported, but there are no ECDSA hostkeys
+	configured.
+
+2014-11-16  TJ Saunders <tj at castaglia.org>
+
+	* RELEASE_NOTES, doc/contrib/mod_sftp.html: Update docs, release
+	notes for new SFTPHostKey flags.
+
+2014-11-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #72 from
+	proftpd/sftp-disabling-global-hostkeys Modify the SFTPHostKey directive such that it can be used to
+	unload/clea...
+
+2014-11-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/log.c: Remove misleading comment; no functional change.
+
+2014-11-13  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #71 from proftpd/sftp-wrap2-deny-msg Make sure that mod_sftp properly resolves any %u variables in a
+	WrapDeny...
+
+2014-11-11  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #70 from proftpd/sftp-allowfilter-redux Following up on a forums post, make sure that if AllowFilter is used
+	on
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c: Update notes of which modules have been
+	reworked, which need reworking.
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_xfer.c: Fix merge errors causing broken build.
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* : commit 7c4787492b879207a6052058bf9dd11c8c2f48dd Merge: bdaefba
+	eef26a0 Author: TJ Saunders <tj at castaglia.org> Date:   Mon Nov 10
+	10:24:31 2014 -0800
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm: Add regression
+	test demonstrating use of the new key fingerprint environment
+	variables.
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/auth-publickey.c, doc/contrib/mod_sftp.html: 
+	Populate some environment variables that can be used for
+	logging/tracking the publickey used for user authentication, via key
+	fingerprints.
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for changes for
+	Bug#4060.
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #68 from proftpd/ls-opt-U-bug4060 Bug#4060 fix
+
+2014-11-10  TJ Saunders <tj at castaglia.org>
+
+	* doc/howto/ListOptions.html, modules/mod_ls.c,
+	tests/t/lib/ProFTPD/Tests/Commands/LIST.pm: Bug#4060 - Support
+	unsorted LIST entries (-U) to decrease memory/CPU usage for large
+	directory listings.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #67 from proftpd/xfer-note-file-offset Add support for %{file-offset} ExtendedLog variable, for tracking
+	resumed uploads/downloads
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* : commit 8d0d90490407d67d8da7bd1543a5f4625b5797af Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Nov 9 22:38:25 2014 -0800
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #66 from
+	proftpd/lang-require-valid-encoding-bug4125 Bug#4125 - mod_lang should provide way to reject illegally-encoded
+	filenames.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_lang.c: Copy-pasto.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* include/encode.h, modules/mod_lang.c, src/encode.c, src/fsio.c: 
+	Forgot to actually wire up the handling of the RequireValidEncoding
+	LangOption to the propagation of decoding errors; without this, all
+	illegal encodings would be rejected.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_copy.c, contrib/mod_site_misc.c,
+	doc/modules/mod_lang.html, include/encode.h, include/fsio.h,
+	include/session.h, modules/mod_core.c, modules/mod_facts.c,
+	modules/mod_lang.c, modules/mod_ls.c, modules/mod_site.c,
+	modules/mod_xfer.c, src/dirtree.c, src/encode.c, src/fsio.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm: Bug#4125 - mod_lang
+	should provide way to reject illegall-encoded filenames.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with mention of changes for Bug#4058.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #65 from proftpd/trace-timing-channel-bug4058 Bug#4058 -  Create a 'timing' trace channel, for timing-related data
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_log_forensic.c, contrib/mod_sftp/packet.c,
+	contrib/mod_tls.c, include/proftpd.h, include/support.h,
+	modules/mod_auth.c, src/main.c, src/support.c: Add supporting
+	functions for easier timestamping for a 'timing' channel, and start
+	implementing some of the more important timings to capture.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, contrib/mod_sftp/configure: Update NEWS, regenerate mod_sftp
+	configure, for fix for Bug#4131.
+
+2014-11-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #64 from proftpd/sftp-autoconf-sha2-detection Fix mod_sftp's autoconf script to properly detect OpenSSL support
+	for SH...
+
+2014-11-06  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #63 from proftpd/sqlpasswd-dual-salt-support Modify mod_sql_passwd so that both global and per-user salts can be
+	supp...
+
+2014-11-06  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, release notes for Bug#4130.
+
+2014-11-06  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #62 from
+	proftpd/site-utime-multi-timestamps-bug4130 Bug#4130 - Support the 3-timestamp form of SITE UTIME.
+
+2014-11-03  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* NEWS: Update NEWS for fix for Bug#4129.
+
+2014-11-03  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #61 from
+	proftpd/sql-negative-cached-ids-bug4129 Bug#4129 - mod_sql caches incorrect UID/GID when name cannot be
+	retrieve...
+
+2014-11-02  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* tests/t/lib/ProFTPD/Tests/Commands/RMD.pm: Add regression test
+	demonstrating deletion of a directory (empty) whose name contains
+	spaces.
+
+2014-10-27  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with fix for Bug#4124.
+
+2014-10-27  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #60 from
+	proftpd/xfer-deleteabortestores-off-bug4124 Bug#4124 - DeleteAbortedStores defaults to "on" for all transfers,
+	not j...
+
+2014-10-24  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/scoreboard.c: Make sure we handle possible (albeit unlikey)
+	lseek(2) errors here, too.
+
+2014-10-24  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* utils/scoreboard.c: If lseek(2) fails, make sure we handle it
+	properly.
+
+2014-10-24  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/tpl.c: Slightly better (paranoid) handling of fatal messages
+	in TPL.
+
+2014-10-24  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/stash.c, tests/api/stash.c: Per Coverity, check that our name
+	comparison is doing what we expect.  Also ensure that the names used
+	in stash tables are at least one character long; no empty strings.
+
+2014-10-23  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* : commit 0ebf3f8a96aeaaa83bc1250fadc337d626a09186 Author:
+	tjsaunders <tjsaunders at blackpearlsystems.com> Date:   Thu Oct 23
+	08:50:32 2014 -0700
+
+2014-10-23  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #58 from proftpd/tls-protocol-version-bug4116 Bug#4116 - Report exact SSL/TLS protocol version used in client
+	connecti...
+
+2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/memcache.c: Check for a null pointer again, as was done above.
+
+2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/tpl.c: Clean up the varargs in this error case.
+
+2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/tpl.c: Tell compilers (and static code analyzers) that we
+	explicitly do not care about the return values in this code branch.
+
+2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/main.c: If we still have trouble opening the ScoreboardFile in
+	inetd mode, after deleting it, then fail to start up.
+
+2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_shaper.c: Check for an improbable error when deleting
+	the ShaperTable on shutdown, per Coverity's suggestions.
+
+2014-10-21  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_tls.c: More refactoring to the TLS version setting in
+	mod_tls, to make sure that the intended behavior, a la Bug#4114, is
+	enforced properly in all cases. This code is much simpler to
+	maintain now.
+
+2014-10-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Got the order of the default ciphersuites
+	wrong, for Bug#4114.
+
+2014-10-21  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_xfer.c: Remove some unneeded code, and check that the
+	configured Max{Retrieve,Store}FileSize is greater than zero.
+
+2014-10-21  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/scoreboard.c, utils/scoreboard.c: If lseek(2) fails, make sure
+	to clean up after ourselves before leaving (i.e.  unlocking the
+	scoreboard, and closing the fd).
+
+2014-10-19  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for Bug#4035.
+
+2014-10-19  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #53 from
+	proftpd/xfer-hiddenstores-not-renamed-bug4035 Bug#4035 - HiddenStores file not renamed every time
+
+2014-10-19  TJ Saunders <tj at castaglia.org>
+
+	* : commit e3a87649a7bafa593b68b36fcc0b9eb2d1045eee Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Oct 19 12:13:49 2014 -0700
+
+2014-10-19  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #51 from
+	proftpd/sftp-too-small-buffer-bug4112 Bug#4112 - Failure to connect using mod_sftp sometimes due to
+	too-small buffers
+
+2014-10-19  TJ Saunders <tj at castaglia.org>
+
+	* : commit 70c583005687a2cbba6807f9f83a504895eedb47 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Oct 19 12:08:38 2014 -0700
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/log.c: If we fail to open a syslog socket, log the error.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c: Remove redundant variable.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_core.c: Ignore errors from setting trace levels here,
+	when restarting.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/pr-syslog.c, src/log.c: More cases where we are not concerned
+	with errors; be explicit about it.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c: Check (or ignore) errors from pr_inet_set_block().
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_tls.c: Fix a case where, when secure site-to-site
+	transfers are used, mod_tls would ignore a possible problem when
+	matching the server cert's CN.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/tpl.c: The tpl code needed some va_end() calls before
+	returning early from a variadic function.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/misc.c: Log in other places here when
+	stat(2)/fstat(2) fail.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/misc.c: Log if stat(2) fails here.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/data.c: We're not really concerned with a fcntl(2) error here.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c: Some controls could elicit large responses.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c: Put a bound on the number of controls response
+	arguments, too.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c: Put a bound on the max number of controls request
+	arguments.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_ctrls.c: Another place where we can check before
+	calling close() on an fd.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_ctrls.c: Make sure the fd is a valid fd value before
+	passing it to close(2).
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/scoreboard.c: More handling of lseek(2) errors.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* utils/scoreboard.c: Handle possible lseek(2) error when scrubbing
+	scoreboard via ftpscrub.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/netaddr.c: More paranoid error handling, prompted by Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_quotatab_file.c: Handle (rare) cases where lseek(2)
+	might return an error.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_core.c: Coverity found another place where possibly
+	badly formatted Trace settings could trigger a null pointer
+	dereference.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/keystore.c: Avoid possible null pointer
+	dereferences (unlikely) when handling badly configured
+	SFTPAuthorized{Host,User}Keys parameters.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_core.c: Avoid possible null pointer dereference, per
+	Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_wrap2/mod_wrap2.c: Paranoidly check for NULL from
+	strchr(3); this case should've been caught in the directive handler.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: Avoid possible (but unlikely) null pointer
+	dereference.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c: Watch for short reads when reading in a controls
+	request.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* doc/howto/Tracing.html, src/log.c, src/trace.c: Handle the case
+	where opening a syslog socket might fail.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: Remove unreachable code, found by
+	Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_xfer.c: Remove code which is logically
+	dead/unreachable, per Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/memcache.c: A few more missing break statements found by
+	Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_core.c: Handle the EISDIR case first; the following
+	code would handle it automatically, but we want to handle EISDIR as
+	a special case.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/event.c: Coverity's analysis correctly identified some
+	dead/unreachable code.  Clean it up here.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_xfer.c: Remove redundant check, pointed out by
+	Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/scoreboard.c: Remove some code which would never be reached,
+	pointed out by Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* utils/ftpshut.c: Adding missing break statement, and make the code
+	clearer.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/memcache.c: More missing break statements found by Coverity.
+
+2014-10-18  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_memcache.c: Add missing break statements, per
+	Coverity.
+
+2014-10-17  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c: Refactor the protocol-enabling bits into a
+	common function, which makes the protocol setting code easier to
+	read/maintain.
+
+2014-10-16  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* NEWS, RELEASE_NOTES, doc/contrib/mod_tls.html: Update mod_tls
+	docs, NEWS, and release notes for Bug#4114.
+
+2014-10-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #56 from proftpd/tls-sslv3-disabled-bug4114 Bug#4114 - mod_tls should not support SSLv3 by default.
+
+2014-10-16  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_tls.c: Bug#4114 - mod_tls should not support SSLv3 by
+	default.
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c: Better error handling (and reporting).
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: Add
+	regression test demonstrating setting the UID/GID columns for
+	SQLUserInfo to NULL.
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sql.c: Always log the default UID/GID, not just when
+	"SQLAuthenticate groups" is in effect.  Sheesh.
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/bindings.c, src/inet.c, src/netaddr.c: In handling some
+	Coverity-found issues, I think we were propagating some errors too
+	eagerly.  Fix these, along with the trace logging added for tracking
+	down the cause.
+
+2014-10-14  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #55 from proftpd/coverity-defects Coverity defects
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_delay.c: Check for fcntl(2) return values, and log
+	them (via trace logging).
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: If there are errors setting a file handle
+	as blocking, log them via trace logging.
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_tls.c: Log (via trace messages) when putting a socket
+	into nonblocking/blocking mode results in errors.  Make it clear
+	that setting other socket options can be ignored if they fail.
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/stash.c: Minor edge case found by Coverity.
+
+2014-10-14  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/coverity/modeling.c: More Coverity modeling hints.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_tls.c: More places where we need to log if there's an
+	issue stashing a note in a table, especially these SSL notes.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_shaper.c: Per Coverity, avoid possible divide-by-zero
+	situation.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/coverity/modeling.c: Model pr_session_disconnect() as a
+	killpath.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sql_passwd.c: If we have an error reading the salt,
+	then don't proceed to trying to manipulate the buffer containing
+	said salt.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/timers.c: Remove redundant null check.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: Fix more places in mod_sftp where checking
+	xerrno for EOF was unnecessary.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: As it is not possible for xerrno to be EOF
+	in this case, remove the unneeded tertiary operator/check for that
+	condition.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_snmp/mod_snmp.c: Add missing break statement
+	(copy-pasto).
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/dirtree.c: Add missing break statement.  Oops.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_radius.c: Make it clearer what the expected
+	usage/return value of the RADIUS_IS_VAR macro is.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/str.c: Remove some redundant checks: an int cannot be larger
+	than INT_MAX, by definition.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/fsio.c: Restructure a function to remove dead code.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/cmd.c: Log if we have a problem caching our "displayable"
+	string of a command in its notes table.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/display.c, src/display.c: Make sure that
+	pr_fsio_fstat() succeeds before setting the IO block size hint on
+	the file handle.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/misc.c: Another place where we weren't checking
+	pr_fsio_fstat()'s return value.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: Be sure to check the pr_table_add() return
+	value, per Coverity.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_xfer.c: Per Coverity's prodding, check the return
+	value of pr_fsio_stat().
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/coverity/modeling.c: Model setenv(3) as a TAINTED_DATA
+	sink.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: Another copy-pasto found by Coverity.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/fxp.c: Copy-pasto, found by Coverity.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/keys.c: Copy-pasto, found by Coverity.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/packet.c: If mod_sftp is interrupted when reading
+	from a socket, then retry that read(2) call again, rather than just
+	looping infinitely.
+
+2014-10-12  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_ldap.c: Remove unnecessary tertiary operator.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_delay.c: Use proper blocks, especially when dealing
+	with macro expansion, to make clear what is happening (and what is
+	in scope).
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/kex.c: Be consistent with the datatype used for
+	the hostkey length throughout the KEX API.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* modules/mod_delay.c: If mod_delay has issues blocking/unblocking
+	signals, log the reason why via trace logging.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_copy.c: Log, via trace logging, if we fail to add a
+	session note.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_copy.c: Log, via trace logging, a should-never-fail
+	error.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/pr-syslog.c: If we have a problem sending a message via
+	syslog, try (at least) to report it via STDERR.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/fsio.c: Prompted by Coverity, log (rather than ignoring)
+	return values.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/agent.c: Per Coverity, at least log the fcntl(2)
+	error when trying to set CLOEXEC on an fd opened to talk to an SSH
+	agent.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/netio.c: Explicitly ignore fcntl(2) return values here.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c: Per Coverity, make sure that we check for, and handle
+	errors, when calling getsockopt(2).
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c: At least log, via trace logging, if fcntl(F_SETOWN)
+	fails.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/bindings.c, src/inet.c: Propagate -- and check for -- more
+	return values, prompted by Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/rfc4716.c: Be more paranoid about checking return
+	values, prompted by Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/data.c: Explicitly ignore the fcntl(2) return value for data
+	sockets.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c: If setting a socket back to blocking mode fails,
+	propagate that error to the caller, just as we do when setting the
+	socket to nonblocking mode.  Found by Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c, src/log.c: Explicitly ignore the fcntl(2) return
+	value when opening a log fd; there is no existing log fd that we can
+	use at that point for reporting the error.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/dirtree.c: If we encounter an error in opening a socket, make
+	sure that we return from that function early, as appropriate (found
+	by Coverity).
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* utils/ftptop.c: Fix a possible (albeit unlikely) resource leak
+	identified by Coverity.
+
+2014-10-11  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #54 from proftpd/coverity-defects Coverity defects
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/display.c, src/display.c: Fix error spotted by
+	Coverity, where we might exceed the array of supported units for
+	Display variables.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* lib/pr-syslog.c: Avoid possible buffer overrun on exceedingly log
+	syslog-destined log messages, per Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/keys.c, contrib/mod_sftp/keys.h: Ensure matching
+	datatypes, on platforms where uint32_t and size_t are not the same
+	size; this should address an issue spotted by Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_shaper.c: Initialize variable, per Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_snmp/mod_snmp.c: Initialize variable, per Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/keys.c: Initialize variable, per Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/stash.c: Initialize variable value, per Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp_sql.c: Be pedanticly cautious, prompted by
+	Coverity, in making sure that buffer indices are within the range we
+	assume.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/main.c: Avoid possible descriptor leak, found by Coverity, by
+	using the existing pr_fs_get_usable_fd() function, rather than
+	multiple different approaches.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_snmp/mod_snmp.c: Coverity found a "leaked" file
+	descriptor in mod_snmp; we were not returning the opened socket to
+	the caller when opening the listening socket.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/netaddr.c: Fix a case, found by Coverity, where we would not
+	properly free an addrinfo.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c: Fix error case where a socket opened by init_conn()
+	was not properly closed on error.  This leak was identified by
+	Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/ctrls.c: If there are errors when opening a Controls socket,
+	make sure that the socket is closed, otherwise we have a descriptor
+	leak.  This was identified by Coverity.
+
+2014-10-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/env.c: Fix memory leak identified by Coverity: setenv(3)
+	already makes a copy of its arguments, which means that we do not
+	need to do it.
+
+2014-10-10  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_sftp/keys.c, contrib/mod_tls.c: Make sure that when
+	prompting the user for a passphrase for a certificate, we ensure
+	that the buffer holding the passphrase is NUL-terminated.
+
+2014-10-10  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* utils/scoreboard.c: If we fail to obtain a lock on the scoreboard
+	and return the error, make sure we close the fd on the way out, lest
+	we leak an fd.
+
+2014-10-10  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/netaddr.c: Fix case where we might reference a buffer that has
+	fallen out of scope.
+
+2014-10-10  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_radius.c: Fix minor socket descriptor leak found by
+	Coverity.
+
+2014-10-09  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* src/inet.c, tests/Makefile.in, tests/api/inet.c,
+	tests/api/stubs.c, tests/api/tests.c, tests/api/tests.h: Start
+	working on unit tests for the Inet API.
+
+2014-10-09  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* include/filter.h, src/filter.c, tests/Makefile.in,
+	tests/api/filter.c, tests/api/tests.c, tests/api/tests.h: Adding
+	unit tests for the Filter API.
+
+2014-10-08  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* doc/howto/BCP.html, doc/modules/mod_auth.html: Minor doc updates,
+	especially a caution for guarding the /proc filesystem.
+
+2014-10-07  TJ Saunders <tj at castaglia.org>
+
+	* include/options.h: Make the default lingering close timeout be 10
+	secs, rather than 30 secs; the latter is rather long to wait for
+	lost/wandering data to make its way to a client.
+
+2014-10-07  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS for Bug#4110.
+
+2014-10-07  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #52 from proftpd/core-solaris-conslog-bug4110 Bug#4110 - proftpd on Solaris should use /dev/conslog instead of
+	/dev/lo...
+
+2014-10-05  TJ Saunders <tj at castaglia.org>
+
+	* tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm: Added more
+	regression tests, and tweaked code to add more logging, and to
+	handle additional error cases.  This should help remove edge cases
+	where HiddenStores are not properly deleted on error.
+
+2014-10-05  TJ Saunders <tj at castaglia.org>
+
+	* include/pr-syslog.h, lib/pr-syslog.c: Bug#4110 - proftpd on
+	Solaris should use /dev/conslog instead of /dev/log.
+
+2014-10-05  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/cipher.c, contrib/mod_sftp/kex.c,
+	contrib/mod_sftp/mac.c: Increase buffer sizes where we write out
+	mpints during the key exchange process, for Bug#4112.
+
+2014-10-05  TJ Saunders <tj at castaglia.org>
+
+	* : commit d2e0461cc51145c71dc1d8a4b48b669a39f5fe1a Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Oct 5 10:04:22 2014 -0700
+
+2014-10-05  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_ldap.html: Update mod_ldap docs to mention "ldap"
+	trace logging channel, courtesy of Bug#4107.
+
+2014-10-05  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #50 from proftpd/ldap-more-tracing-bug4107 Bug#4107 - LDAPLog can fill disk on a busy server.  Start moving
+	some of...
+
+2014-09-28  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* contrib/mod_tls.c: Initial work on PSK support; needs testing.
+
+2014-09-28  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_ldap.c: Bug#4107 - LDAPLog can fill disk on a busy
+	server.  Start moving some of the mod_ldap logging into TraceLog.
+
+2014-09-28  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updated NEWS for fix of Bug#4109.
+
+2014-09-28  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #49 from proftpd/inet-ipv6-tclass-bug4109 Bug#4109 - setsockopt() call for IPV6_TCLASS should use
+	IPPROTO_IPV6.
+
+2014-09-23  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with fix for Bug#4108.
+
+2014-09-23  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #48 from
+	proftpd/tls-data-conn-handshake-delay-bug4108 Bug#4108 - SSL handshakes for data connections sometimes stall for
+	3-30 ...
+
+2014-09-21  TJ Saunders <tj at castaglia.org>
+
+	* : commit cf83753a6564b84f6962f60f2f0b49b4c7a89a97 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Sep 21 21:51:56 2014 -0700
+
+2014-09-21  TJ Saunders <tj at castaglia.org>
+
+	* .travis.yml: Use --enable-devel when building via travis-ci.
+
+2014-09-21  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_deflate.c, modules/mod_memcache.c: Fix build
+	warnings/breakage.
+
+2014-09-21  TJ Saunders <tj at castaglia.org>
+
+	* : commit d2f5d99fef7a30b3772b48ee4b3a400bf7ef9a65 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Sun Sep 21 16:53:03 2014 -0700
+
+2014-09-18  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_facts.html: Slightly better anchor name.
+
+2014-09-18  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_facts.html: Add FAQ about disabling mod_facts
+	module entirely.
+
+2014-09-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/cipher.c, contrib/mod_sftp/mac.c: Increase cipher
+	and MAC buffer sizes, as a (somewhat educated) guess as to what
+	might be causing the issues reported here:   https://forums.proftpd.org/smf/index.php/topic,11611.0.html
+
+2014-09-10  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_tls.html: Doc typo.
+
+2014-09-07  TJ Saunders <tj at castaglia.org>
+
+	* NEWS, contrib/mod_sftp/display.c: Make sure the fix for Bug#4094
+	is applied to mod_sftp's handling of Display variables as well.
+
+2014-09-07  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/keys.c: When loading the hostkey data, stop using
+	the scratch buffer, and start using the provided pool.  In limited
+	tests, this appears to help with rekeying.
+
+2014-09-07  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_rlimit.html: Add a FAQ section to mod_rlimit, for
+	the RLimitChroot directive.
+
+2014-09-07  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_core.c, modules/mod_xfer.c: Start moving toward using
+	'core.module-reset' event handlers, rather than a POST_CMD HOST
+	handler, for preparing a module for a changed main_server pointer
+	due to a HOST command.
+
+2014-09-04  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* NEWS, RELEASE_NOTES: Update NEWS, RELEASE_NOTES for fix for
+	Bug#4098.
+
+2014-09-04  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #44 from proftpd/sftp-hostkey-perms-bug4098 Bug#4098 -  mod_sftp unable to use SFTPHostKey due to being group
+	readable in CentOS 7.
+
+2014-09-03  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/keys.c, tests/t/lib/ProFTPD/TestSuite/Utils.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Add
+	integration/regression test for Bug#4098, and improve the log
+	message when insecure permissions on hostkey files are found.
+
+2014-08-30  TJ Saunders <tj at castaglia.org>
+
+	* : commit 3e1a5851a7e2c7bcb711328225ffa9898780f2ec Merge: cd85317
+	a8cc506 Author: TJ Saunders <tj at castaglia.org> Date:   Fri Aug 29
+	08:51:04 2014 -0700
+
+2014-08-29  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Updating NEWS.
+
+2014-08-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/mod_sftp.c, contrib/mod_sftp/mod_sftp.h.in: Make
+	sure that if zlib support is lacking, mod_sftp does not to try
+	negotiate compression during the key exchange.
+
+2014-08-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/Makefile.in: Make sure to add -lz, when needed,
+	to the building of a DSO mod_sftp.
+
+2014-08-29  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/Makefile.in, contrib/mod_sftp/compress.c,
+	contrib/mod_sftp/configure, contrib/mod_sftp/configure.in: Improve
+	the detection of zlib support, and handling of the results, for
+	mod_sftp.
+
+2014-08-28  TJ Saunders <tj at castaglia.org>
+
+	* Make.rules.in, configure, configure.in,
+	contrib/mod_sftp/Makefile.in, contrib/mod_sftp/compress.c,
+	contrib/mod_sftp/configure, contrib/mod_sftp/configure.in,
+	contrib/mod_sftp/mod_sftp.c, contrib/mod_sftp/mod_sftp.h.in,
+	contrib/mod_tls_memcache.c, contrib/mod_tls_shmcache.c: Bug#4102 -
+	Failure to build mod_tls when using static libcrypto due to libdl
+	linker errors.  In the course of fixing this issue, I also fixed problems with
+	configure's detection of EC support in OpenSSL, and make it possible
+	for mod_sftp to build without zlib support.
+
+2014-08-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/configure, contrib/mod_sftp/configure.in: Fix
+	another place where the libdl linker flag may need to come earlier
+	in the list.
+
+2014-08-27  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in: Bug#4102 - Failure to build mod_tls when
+	using static libcrypto due to libdl linker errors.
+
+2014-08-27  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/keys.c, contrib/mod_sftp/mod_sftp.c,
+	contrib/mod_sftp/mod_sftp.h.in: Bug#4098 - mod_sftp unable to use
+	SFTPHostKey due to being group readable in CentOS 7.
+
+2014-08-19  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sftp/kex.c,
+	tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key,
+	tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key.pub,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Bug#4097 - SSH rekey
+	fails when using RSA hostkey smaller than 2048 bits.
+
+2014-08-16  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #41 from proftpd/core-smkdir-only-when-needed Only use the mkdtemp(3) route in pr_fsio_smkdir() if root privs are
+	need...
+
+2014-08-14  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_tls.c, include/bindings.h, modules/mod_core.c,
+	src/bindings.c, tests/t/lib/ProFTPD/TestSuite/FTP.pm,
+	tests/t/lib/ProFTPD/Tests/Commands/HOST.pm: More work on handling
+	the implications of HOST commands in various modules; much more work
+	to be done here.
+
+2014-08-14  TJ Saunders <tj at castaglia.org>
+
+	* include/ftp.h, modules/mod_core.c,
+	tests/t/lib/ProFTPD/TestSuite/FTP.pm: Create branch for finishing
+	work on supporting the HOST command (Bug#3289).
+
+2014-08-14  TJ Saunders <tj at castaglia.org>
+
+	* src/fsio.c: Only use the mkdtemp(3) route in pr_fsio_smkdir() if
+	root privs are needed for setting the desired ownership, i.e.
+	UserOwner/GroupOwner are in effect.
+
+2014-08-13  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_log.html: Update mod_log docs with a FAQ section,
+	including a FAQ on how to get the ExtendedLog to include the reason
+	why a session was ended.
+
+2014-08-13  TJ Saunders <tj at castaglia.org>
+
+	* modules/mod_ctrls.c, src/ctrls.c: Stylistic nits; no functional
+	change.
+
+2014-08-13  TJ Saunders <tj at castaglia.org>
+
+	* configure: Regenerated configure script.
+
+2014-08-13  TJ Saunders <tj at castaglia.org>
+
+	* configure.in: Adding build summary for static and shared modules;
+	more info will be added in this section.
+
+2014-08-13  TJ Saunders <tj at castaglia.org>
+
+	* : commit e423841e2d07f72d766baff5fddf094db343ff38 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Wed Aug 13 06:42:53 2014 -0700
+
+2014-08-13  TJ Saunders <tj at castaglia.org>
+
+	* configure.in: Fixed handling of too-many/misplaced colons in
+	--with-shared argument.
+
+2014-08-12  TJ Saunders <tj at castaglia.org>
+
+	* configure, configure.in, src/fsio.c: Revert previous configure
+	script change; it broke the build for shared modules.  Need to
+	reexamine why that feedback was given on GitHub.  Also fix a typo
+	causing a compiler warning.
+
+2014-08-12  TJ Saunders <tj at castaglia.org>
+
+	* configure: Regenerated configure script.
+
+2014-08-12  TJ Saunders <tj at castaglia.org>
+
+	* configure.in: Review feedback from GitHub.
+
+2014-08-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* NEWS: Update NEWS for fix for Bug#4094.
+
+2014-08-11  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #38 from
+	proftpd/core-display-avail-space-bug4094 Bug#4094 - Available space on file system using %f displays wrong
+	value.
+
+2014-08-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+
+	* NEWS: Update NEWS for changes for Bug#4030.
+
+2014-08-11  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #37 from proftpd/auth-negative-cache-bug4030 Bug#4030 - Cache negative/failed Auth API name/ID lookups.
+
+2014-08-11  TJ Saunders <tj at castaglia.org>
+
+	* include/auth.h, src/auth.c, tests/api/auth.c: Bug#4030 - Cache
+	negative/failed Auth API name/ID lookups.
+
+2014-08-10  TJ Saunders <tj at castaglia.org>
+
+	* NEWS: Update NEWS with fix for Bug#4020.
+
+2014-08-10  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #36 from proftpd/delay-delayonevent-bug4020 Bug#4020 - Add minimum delay options to mod_delay functionality
+
+2014-08-10  TJ Saunders <tj at castaglia.org>
+
+	* doc/modules/mod_delay.html, modules/mod_delay.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_delay.pm: Bug#4020 - Add
+	minimum delay options to mod_delay functionality.
+
+2014-08-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #35 from proftpd/fsio-getpipebuf-bug4050 Bug#4050 - Use of PIPE_BUF causes build failure on platforms without
+	it.
+
+2014-08-09  TJ Saunders <tj at castaglia.org>
+
+	* doc/contrib/mod_copy.html, doc/contrib/mod_deflate.html,
+	doc/contrib/mod_dnsbl.html, doc/contrib/mod_geoip.html,
+	doc/contrib/mod_ifsession.html, doc/contrib/mod_rewrite.html,
+	doc/contrib/mod_sftp.html, doc/contrib/mod_snmp.html,
+	doc/contrib/mod_sql.html, doc/contrib/mod_sql_passwd.html,
+	doc/contrib/mod_tls.html, doc/contrib/mod_tls_memcache.html,
+	doc/contrib/mod_tls_shmcache.html, doc/modules/mod_auth_file.html,
+	doc/modules/mod_auth_pam.html, doc/modules/mod_auth_unix.html,
+	doc/modules/mod_ctrls.html, doc/modules/mod_delay.html,
+	doc/modules/mod_dso.html, doc/modules/mod_facl.html,
+	doc/modules/mod_ident.html, doc/modules/mod_xfer.html,
+	modules/mod_auth_pam.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm: More
+	documentation followup/cleanup for Bug#4054.
+
+2014-08-09  TJ Saunders <tj at castaglia.org>
+
+	* contrib/mod_sql_passwd.c, contrib/mod_tls_memcache.c,
+	contrib/mod_tls_shmcache.c, doc/contrib/mod_copy.html,
+	doc/contrib/mod_deflate.html, doc/contrib/mod_dnsbl.html,
+	doc/contrib/mod_geoip.html, doc/contrib/mod_ifsession.html,
+	doc/contrib/mod_rewrite.html, doc/contrib/mod_sftp.html,
+	doc/contrib/mod_snmp.html, doc/contrib/mod_sql.html,
+	doc/contrib/mod_sql_passwd.html, doc/contrib/mod_tls.html,
+	doc/contrib/mod_tls_memcache.html,
+	doc/contrib/mod_tls_shmcache.html: Bug#4054 - Ensure that module
+	documentation mentions Trace channel names where applicable.
+
+2014-08-09  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #34 from proftpd/core-serverident-var-version Support %{version} variable for ServerIdent
+
+2014-08-09  TJ Saunders <tj at castaglia.org>
 
-	* tests/api/str.c: Bug#4193 - Out of bounds read in tests/api/str.c.
+	* : Merge pull request #33 from proftpd/sftp-missing-payload-bug4093 Bug#4093 - Improve mod_sftp handling of missing packet payloads.
 
-2015-07-24  tjsaunders <tj at lyveminds.com>
+2014-08-09  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_log.c: More backporting of fixes from master, with
-	regard to using the "default" LogFormat, whilst still allowing
-	configs to override that using "default" as a nickname if they wish.
+	* src/session.c, tests/t/lib/ProFTPD/Tests/Config/ServerIdent.pm: 
+	Support a %{version} variable in the ServerIdent value.
 
-2015-07-24  tjsaunders <tj at lyveminds.com>
+2014-08-09  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_log.c: Backport the fix for looking/using the default
-	LogFormat, from master.
+	* modules/mod_ctrls.c: Clarify the mod_ctrls message when chown(2)
+	fails to include the local socket path.
 
-2015-07-17  TJ Saunders <tj at castaglia.org>
+2014-08-09  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/keys.c, contrib/mod_sftp/mod_sftp.c: Fix handling
-	of just ECDSA SSH host keys in mod_sftp; backported the code from
-	master.
+	* modules/mod_log.c: Fix handling of ExtendedLogs which log to
+	syslog.
 
-2015-07-17  tjsaunders <tj at lyveminds.com>
+2014-08-08  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4191.
+	* tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: Adding another
+	regression test for Bug#4067, proving that an ExtendedLog using just
+	the READ or WRITE classes properly logs SFTP uploads and downloads,
+	without needing to explicitly include the SFTP class.
 
-2015-07-15  TJ Saunders <tj at castaglia.org>
+2014-08-07  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* contrib/mod_sql_mysql.c: Bug#4191 - Use "utf8mb4" for MySQL, not
-	just "utf8", if the version of MySQL is new enough, for UTF8
-	encodings.
+	* RELEASE_NOTES: Minor note, for future fleshing out, about
+	ExtendedLog functionality changes.
 
-2015-05-31  TJ Saunders <tj at castaglia.org>
+2014-08-07  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Updating NEWS for backport of fix for Bug#4187.
+	* src/inet.c: Avoid log spam on FreeBSD 10, which is reported to
+	indicate that the IPV6_TCLASS socket option is not usable on IPv6
+	sockets via EINVAL, rather than the more appropriate ENOPROTOOPT.
+	Sigh.
 
-2015-05-30  TJ Saunders <tj at castaglia.org>
+2014-07-30  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_geoip.c: Bug#4187 - mod_geoip does not load all of the
-	GeoIPTables properly.
+	* contrib/mod_sftp/auth-hostbased.c, contrib/mod_sftp/blacklist.c,
+	contrib/mod_sftp/keys.c, contrib/mod_sftp/keystore.c,
+	contrib/mod_sftp/msg.c: Bug#4093 - Improve mod_sftp handling of
+	missing packet payloads.
 
-2015-05-27  tjsaunders <tj at lyveminds.com>
+2014-07-28  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* include/version.h: Update files for maint status.
+	* src/bindings.c: Ignore "unable to accept an incoming connection:
+	Software caused connection abort" messages, as they are almost
+	always caused by TCP probes/health checks from e.g. naive load
+	balancers.
 
-2015-05-27  tjsaunders <tj at lyveminds.com>
+2014-07-28  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS, contrib/dist/rpm/proftpd.spec: Warming up for pending 1.3.5a
-	maint release.
+	* NEWS: Updated NEWS for fix of Bug#4091.
 
-2015-04-29  tjsaunders <tj at lyveminds.com>
+2014-07-28  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4178.
+	* : Merge pull request #32 from
+	proftpd/core-privs-error-logging-bug4091 Bug#4091 - Log "Operation not permitted" privs errors at NOTICE
+	rather t...
 
-2015-04-29  tjsaunders <tj at lyveminds.com>
+2014-07-27  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c: Bug#4178: When accepting a TLS connection for
-	data transfers, disable the storing of session IDs in the session
-	cache; we want to require that all data connections reuse the
-	session ID from the control connection, not add their own sessions.
-	If the NoSessionReuseRequired option is used, then it doesn't matter
-	whether we add data connection session IDs to the cache or not; if
-	that option is NOT used, then we only want sessions in the cache
-	that have been added for control connections.  But we DO want to support lookup of session IDs in the session cache
-	for data connections (to enforce session reuse).  Thus we toggle the
-	OpenSSL SSL_SESS_CACHE_NO_INTERNAL_STORE flag for the duration of a
-	data connection TLS handshake.
+	* src/privs.c: Bug#4091 - Log "Operation not permitted" privs errors
+	at NOTICE rather than ERROR.
 
-2015-04-28  tjsaunders <tj at lyveminds.com>
+2014-07-25  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* src/data.c: Fix segfault due to null pointer dereference.  Conflicts: 	src/data.c
+	* NEWS, contrib/mod_wrap2_file.c: Update NEWS for fix of Bug#4090.
 
-2015-04-22  TJ Saunders <tj at castaglia.org>
+2014-07-25  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_copy.c, include/cmd.h: Fix broken build by removing
-	references to function that has not been backported to the 1.3.5
-	branch.
+	* : Merge pull request #31 from proftpd/wrap2-file-ipv6-addr-bug4090 Bug#4090 - mod_wrap2_file does not support IPv6 addresses properly.
 
-2015-04-22  TJ Saunders <tj at castaglia.org>
+2014-07-25  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* src/privs.c: Change the logging priority based on errno
-	consistently.
+	* NEWS: Updated NEWS with fix for Bug#4089.
 
-2015-04-07  tjsaunders <tj at lyveminds.com>
+2014-07-25  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_copy.c, modules/mod_core.c, modules/mod_xfer.c: 
-	Updating modules to use the PR_ prefixed macros.
+	* : Merge pull request #30 from
+	proftpd/sftp-auth-multiple-attempts-bug4089 Bug#4089 - mod_sftp does not allow multiple attempts using a given
 
-2015-04-07  tjsaunders <tj at lyveminds.com>
+2014-07-19  TJ Saunders <tj at castaglia.org>
 
-	* RELEASE_NOTES: Update release notes for pending maint release.
+	* NEWS: Update NEWS for fix for Bug#4087.
 
-2015-04-07  tjsaunders <tj at lyveminds.com>
+2014-07-19  TJ Saunders <tj at castaglia.org>
 
-	* src/inet.c: Backporting fix from master, for compiling when the
-	--disable-ipv6 compile-time option is used.
+	* : Merge pull request #29 from
+	proftpd/sftp-maxloginattempts-none-bug4087 Bug#4087 - mod_sftp does not handle "MaxLoginAttempts none"
+	properly.
 
-2015-04-07  tjsaunders <tj at lyveminds.com>
+2014-07-17  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4169.
+	* contrib/mod_sftp/mod_sftp.h.in: Keep the mod_sftp version string
+	format consistently using three digits, and use "1.0.0" instead of
+	"1.0".
 
-2015-04-06  TJ Saunders <tj at castaglia.org>
+2014-07-17  TJ Saunders <tj at castaglia.org>
 
-	* RELEASE_NOTES, contrib/mod_copy.c, doc/contrib/mod_copy.html,
-	tests/t/lib/ProFTPD/Tests/Modules/mod_copy.pm: Turns out that
-	mod_copy inadvertently allows unauthenticated copying.  Worse, it
-	does not provide an Engine directive for disabling the module in
-	builds that have it enabled.  Conflicts: 	RELEASE_NOTES 	doc/contrib/mod_copy.html
+	* contrib/mod_sftp/fxp.c, contrib/mod_sftp/mod_sftp.h.in: Fix
+	SFTPLog message if LINK request fails.  Bump mod_sftp version, due
+	to fixed LINK handling (and other fixes).
 
-2015-04-05  TJ Saunders <tj at castaglia.org>
+2014-07-16  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/mac.c: Fix typo/bug, backported from master.
+	* contrib/mod_sftp/auth.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Bug#4087 - mod_sftp
+	does not handle "MaxLoginAttempts none" properly.
 
-2015-04-05  TJ Saunders <tj at castaglia.org>
+2014-07-16  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/kbdint.c: Backport fix for memory leak when
-	handling 'keyboard-interactive' SSH authentication.
+	* tests/t/etc/modules/mod_sftp/authorized_rsa_keys2,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Adding regression
+	test of publickey SSH authentication, where the key is stored as the
+	2nd key in the file.
 
-2015-04-02  TJ Saunders <tj at castaglia.org>
+2014-07-09  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_ls.c: Backporting fix from master, with regard to
-	ASCII translation of emitted directory listings.
+	* contrib/mod_sftp_pam.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_pam.pm: Add louder
+	logging for when mod_sftp_pam denies a login, along with a
+	regression test that tickles this behavior.
 
-2015-03-31  tjsaunders <tj at lyveminds.com>
+2014-07-08  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Backporting fix from the investigation for
-	Bug#4166, to prevent memory leaking during handling of SFTP REMOVE
-	requests.
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: Add
+	regression test for SQLUserInfo which returns NULL for the uid/gid
+	columns; this ensures that a) mod_sql doesn't segfault on such
+	return values, and b) that the SQLDefaultUID/SQLDefaultGID values
+	are used.
 
-2015-03-31  tjsaunders <tj at lyveminds.com>
+2014-07-06  TJ Saunders <tj at castaglia.org>
 
-	* src/bindings.c: Make namebind create/open failures less noisy, as
-	part of backporting part of the fix from master.
+	* NEWS: Updated NEWS with fix of Bug#4081.
 
-2015-03-16  TJ Saunders <tj at castaglia.org>
+2014-07-06  TJ Saunders <tj at castaglia.org>
 
-	* tests/api/fsio.c: Fix the FSIO unit tests for 1.3.5.
+	* : Merge pull request #28 from
+	proftpd/sftp-symlink-rel-path-bug4081 Bug#4081 - Not possible to create relative symlinks with SFTP.
 
-2015-03-15  tjsaunders <tj at lyveminds.com>
+2014-07-05  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS with backport of fix for Bug#4160.
+	* contrib/mod_sql.c, modules/mod_log.c: Add support for the
+	%{file-offset} variable to mod_sql (and the missing %{file-modified}
+	variable).  Fixed formatting/printing of %{file-offset} for
+	ExtendedLog.
 
-2015-03-15  tjsaunders <tj at lyveminds.com>
+2014-07-05  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Bug#4160 - Malformed response to
-	SSH_FXP_REALPATH with SFTP version 6.
+	* NEWS: Updating NEWS for Bug#4084.
 
-2015-03-14  tjsaunders <tj at lyveminds.com>
+2014-07-05  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4156.
+	* : Merge pull request #27 from
+	proftpd/ls-nlst-glob-recursion-bug4084 Bug#4084 - "NLST *" returns files from subdirectories.
 
-2015-03-01  TJ Saunders <tj at castaglia.org>
+2014-07-05  TJ Saunders <tj at castaglia.org>
 
-	* include/fsio.h, lib/sstrncpy.c, src/fsio.c, tests/api/fsio.c,
-	tests/api/str.c: Bug#4156 - Segfault handling LIST/NLST FTP command
-	on Mac OS X.  Conflicts: 	tests/api/fsio.c
+	* modules/mod_ls.c, tests/t/lib/ProFTPD/Tests/Commands/NLST.pm: 
+	Bug#4084 - "NLST *" returns files from subdirectories.
 
-2015-02-27  TJ Saunders <tj at castaglia.org>
+2014-07-05  TJ Saunders <tj at castaglia.org>
 
-	* lib/pr_fnmatch_loop.c: Backport glibc fix for a memory overread
-	for certain fnmatch patterns.
+	* tests/t/config/maxtransfersperhost.t,
+	tests/t/config/maxtransfersperuser.t,
+	tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerHost.pm,
+	tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerUser.pm,
+	tests/tests.pl: Adding regression tests for the MaxTransfersPerHost
+	and MaxTransfersPerUser directives.
 
-2015-02-19  TJ Saunders <tj at castaglia.org>
+2014-07-03  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp_sql.c: Fix additional issue related to Bug#4155,
-	where the buffer overrun could happen for bad key data.  This also
-	adds a log message for the future, pinpointing the root of the
-	problem.
+	* doc/modules/mod_auth.html, doc/modules/mod_xfer.html: Fill in
+	missing descriptions for the MaxClients* and MaxTransfers*
+	directives.
 
-2015-02-17  tjsaunders <tj at lyveminds.com>
+2014-07-02  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4155.
+	* NEWS: Updating NEWS for Bug#4083.
 
-2015-02-17  TJ Saunders <tj at castaglia.org>
+2014-07-02  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp_sql.c: No, it should be "Subject: ", not
-	"Comment: "; these are 2 different headers defined by RFC 4716.
+	* : Merge pull request #26 from
+	proftpd/sql-defaulthomedir-empty-home-bug4083 Bug#4083 - Using SQLDefaultHomedir with null home results in "No
+	such user"
 
-2015-02-17  TJ Saunders <tj at castaglia.org>
+2014-07-02  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp_sql.c: Bug#4155 - SSH keys with too-long Comment
-	headers aren't recognized by mod_sftp_sql.  Conflicts: 	contrib/mod_sftp_sql.c
+	* contrib/mod_sql.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: Bug#4083 -
+	Using SQLDefaultHomedir with null home results in "No such user".
 
-2015-02-17  tjsaunders <tj at lyveminds.com>
+2014-06-26  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_ls.c: Backporting fix from master branch.
+	* tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm: Adding
+	regression test for particular mod_ifsession config, based on
+	reports from the forums.
 
-2015-02-04  tjsaunders <tj at lyveminds.com>
+2014-06-26  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS with backport of fix for Bug#4152.
+	* modules/mod_log.c: Fix bug in the handling of ExtendedLog class
+	name parsing, introduced as part of the change for Bug#4067.
 
-2015-02-04  tjsaunders <tj at lyveminds.com>
+2014-06-24  TJ Saunders <tj at castaglia.org>
 
-	* src/main.c: Change the ordering, so that we can short-circuit the
-	checks faster.
+	* contrib/mod_sftp/fxp.c, contrib/mod_sftp/scp.c,
+	modules/mod_xfer.c: Stash the file-offset note for SCP transfers as
+	well.  Fix up misleading comments.
 
-2015-02-04  tjsaunders <tj at lyveminds.com>
+2014-06-24  TJ Saunders <tj at castaglia.org>
 
-	* src/main.c: Bug#4152 - Reduce logging of non-fatal "unable to open
-	incoming connection" errors.
+	* contrib/mod_sftp/fxp.c, include/mod_log.h, modules/mod_log.c,
+	modules/mod_xfer.c: Add support for a new %{file-offset} LogFormat
+	variable, to store the file offset at which a file transfer (upload
+	or download) starts.
 
-2015-02-03  tjsaunders <tj at lyveminds.com>
+2014-06-24  TJ Saunders <tj at castaglia.org>
 
-	* src/data.c: Clear the "have dangling CR" flag on data reset as
-	well.
+	* : Merge pull request #25 from proftpd/core-cmd-errno-note Stash an errno value for all commands, for use by e.g. auditing
+	modules
 
-2015-02-03  tjsaunders <tj at lyveminds.com>
+2014-06-23  TJ Saunders <tj at castaglia.org>
 
-	* src/data.c: Reset any ASCII transformation state, possibly
-	leftover from previous transfers, when preparing for a data transfer
-	(Bug#4150).
+	* : commit 7d818268492254c9ee32f5ef4ab24edc04fe2a7e Author:
+	tjsaunders <tjsaunders at blackpearlsystems.com> Date:   Mon Jun 23
+	09:34:29 2014 -0700
 
-2015-02-03  tjsaunders <tj at lyveminds.com>
+2014-06-23  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS with backport of fix for Bug#4145.
+	* : Merge pull request #24 from proftpd/sftp-symlink-order-bug4080 Bug#4080 - mod_sftp does not implement SFTP LINK request properly.
 
-2015-02-03  TJ Saunders <tj at castaglia.org>
+2014-06-22  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_auth_file.c,
-	tests/t/lib/ProFTPD/Tests/Modules/mod_auth_file.pm: Bug#4145 -
-	Segfault if AuthUserFile is a relative symlink.
+	* contrib/mod_sftp/fxp.c, src/fsio.c, tests/api/fsio.c: Bug#4080 -
+	mod_sftp does not implement SFTP LINK request properly.
 
-2014-12-15  TJ Saunders <tj at castaglia.org>
+2014-06-20  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Update NEWS with backport of fix for Bug#4143.
+	* doc/modules/mod_rlimit.html: Adding clarifying comments about
+	RLimitChroot.
 
-2014-12-15  tjsaunders <tj at lyveminds.com>
+2014-06-20  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* src/cmd.c, src/main.c: Update the comments with the appropriate
-	bug number.
+	* NEWS: Update NEWS for fix for Bug#4079.
 
-2014-12-14  TJ Saunders <tj at castaglia.org>
+2014-06-20  TJ Saunders <tj at castaglia.org>
 
-	* include/cmd.h, include/dirtree.h, include/session.h, src/cmd.c,
-	src/main.c, tests/t/http.t, tests/t/lib/ProFTPD/Tests/HTTP.pm,
-	tests/t/lib/ProFTPD/Tests/SMTP.pm, tests/t/smtp.t: If we detect HTTP
-	or SMTP commands, drop the connection.  This prevents proftpd from
-	being used as a staging server for nefarious attacks on other
-	protocol servers.  Conflicts: 	include/cmd.h
+	* : Merge pull request #23 from proftpd/sftp-space-available-bug4079 Bug#4079 - Invalid response encoding for SFTP space-available
+	request.
 
-2014-12-08  TJ Saunders <tj at castaglia.org>
+2014-06-20  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4140.
+	* contrib/mod_sftp/fxp.c: Bug#4079 - Invalid response encoding for
+	SFTP space-available request.
 
-2014-12-08  TJ Saunders <tj at castaglia.org>
+2014-06-19  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c,
-	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Bug#4140 - SFTP
-	READLINK requests to symlinks to directories fail.
+	* modules/mod_xfer.c: Add trace message of when REST offset exceeds
+	file size, containg the requested offset and the file size in
+	question.
 
-2014-12-08  TJ Saunders <tj at castaglia.org>
+2014-06-19  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Remove duplicate entry in NEWS.
+	* modules/mod_xfer.c, src/netio.c,
+	tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm: Adding regression
+	tests for the behavior reported in Bug#4035, and tweaking the code
+	to handle these cases.
 
-2014-12-01  tjsaunders <tj at lyveminds.com>
+2014-06-19  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Backported fix for Bug#4137.
+	* : Merge pull request #21 from
+	proftpd/exec-execenable-per-directory-bug4076 Bug#4076 - Ability to disable mod_exec on a per-directory basis.
 
-2014-12-01  TJ Saunders <tj at castaglia.org>
+2014-06-19  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_geoip.c: Add some additional logging, to help clarify
-	things.
+	* : commit 37569a94b3156be8f732741774702bb3fe5136be Author:
+	tjsaunders <tjsaunders at blackpearlsystems.com> Date:   Thu Jun 19
+	18:36:35 2014 -0700
 
-2014-11-29  TJ Saunders <tj at castaglia.org>
+2014-06-19  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_geoip.c: Bug#4137 - GeoIPDenyFilter incorrectly takes
-	precedence over GeoIPAllowFilter.
+	* : Merge pull request #20 from proftpd/shaper-log-sighup-bug4077 Bug#4077 - ShaperLog not closed/reopened on SIGHUP, causing log
+	rotation
 
-2014-11-29  TJ Saunders <tj at castaglia.org>
+2014-06-19  TJ Saunders <tj at castaglia.org>
 
-	* config.h.in, configure, configure.in, src/ctrls.c: Issue #75: Add
-	configure-time check for struct sockpeercred (as used on OpenBSD
-	systems), and update the Controls API to use this.
+	* : commit f214df3fd415cb9ce2cb6fd74bd85caa5b0ecc46 Author: TJ
+	Saunders <tj at castaglia.org> Date:   Thu Jun 19 07:54:37 2014 -0700
 
-2014-11-16  TJ Saunders <tj at castaglia.org>
+2014-06-18  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4133.
+	* : Merge pull request #22 from
+	proftpd/auth-addl-failure-reasons-bug4070 Bug#4070 - Support wider range of authentication failures.
 
-2014-11-16  TJ Saunders <tj at castaglia.org>
+2014-06-18  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_ldap.c: Bug#4133 -  LDAPUsers directive does not honor
-	uid-number-filter-template parameter.
+	* : commit 5027925f5133e3d911eb7af27672fe45bda2818d Author: TJ
+	Saunders <tj at castaglia.org> Date:   Wed Jun 18 21:50:26 2014 -0700
 
-2014-11-09  TJ Saunders <tj at castaglia.org>
+2014-06-18  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, contrib/mod_sftp/configure: Update NEWS, regenerate mod_sftp
-	configure, for backport of fix for Bug#4131.
+	* contrib/mod_ldap.c, src/auth.c,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_ldap.pm: Something shifted
+	that is causing mod_ldap login troubles; making some tweaks to
+	hopefully narrow down the issue.
 
-2014-11-07  TJ Saunders <tj at castaglia.org>
+2014-06-18  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/configure.in: Fix mod_sftp's autoconf script to
-	properly detect OpenSSL support for SHA2.
+	* doc/modules/mod_facts.html: Mention, in the mod_facts docs, how to
+	build that module as a shared module.
 
-2014-11-03  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-06-18  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4129.
+	* src/stash.c: Address more travis-ci compiler warnings.
 
-2014-11-03  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-06-18  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* contrib/mod_sql.c: Bug#4129 - mod_sql caches incorrect UID/GID
-	when name cannot be retrieved.
+	* src/pidfile.c: Remember to set errno if opening the PidFile fails.
+	This fixes a compiler warning detected by travis-ci.
 
-2014-10-27  TJ Saunders <tj at castaglia.org>
+2014-06-18  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4124.
+	* NEWS: Update NEWS with mention of Bug#4073.
 
-2014-10-27  TJ Saunders <tj at castaglia.org>
+2014-06-18  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_xfer.c: Bug#4124 - DeleteAbortedStores defaults to
-	"on" for all transfers, not just HiddenStores.  Conflicts: 	modules/mod_xfer.c
+	* : Merge pull request #19 from
+	proftpd/tls-implicitssl-download-bug4073 Bug#4073 - Polycom VOIP phones unable to use FTPS data transfers.
 
-2014-10-23  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Updates NEWS with backport of fix for Bug#4116.
+	* include/auth.h, modules/mod_auth.c, modules/mod_auth_pam.c: 
+	Bug#4070 - Support wider range of authentication failures.
 
-2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c: Bug#4116 - Report exact SSL/TLS protocol
-	version used in client connections.
+	* contrib/mod_exec.c, doc/contrib/mod_exec.html,
+	tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm: Bug#4076 - Ability to
+	disable mod_exec on a per-directory basis.
 
-2014-10-22  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, RELEASE_NOTES, contrib/mod_tls.c: Backport the fixes for
-	Bug#4114 to the 1.3.5 branch.
+	* include/cmd.h, src/cmd.c, tests/api/cmd.c: Start working on
+	stashing the errno value for every command in the cmd->notes table.
+	This will allow modules (such as audit modules for Bug#3807) access
+	to that errno value, for tracking/logging.
 
-2014-10-19  TJ Saunders <tj at castaglia.org>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of Bug#4112.
+	* contrib/mod_shaper.c: Bug#4077 - ShaperLog not closed/reopened on
+	SIGHUP, causing log rotation problems.
 
-2014-10-05  TJ Saunders <tj at castaglia.org>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/cipher.c, contrib/mod_sftp/kex.c,
-	contrib/mod_sftp/mac.c: Increase buffer sizes where we write out
-	mpints during the key exchange process, for Bug#4112.
+	* contrib/mod_tls.c: Remove unnecessary comment.
 
-2014-09-14  TJ Saunders <tj at castaglia.org>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/cipher.c, contrib/mod_sftp/mac.c: Increase cipher
-	and MAC buffer sizes, as a (somewhat educated) guess as to what
-	might be causing the issues reported here:   https://forums.proftpd.org/smf/index.php/topic,11611.0.html
+	* contrib/mod_tls.c, modules/mod_xfer.c: Bug#4073 - Polycom VOIP
+	phones unable to use FTPS data transfers.
 
-2014-09-28  TJ Saunders <tj at castaglia.org>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4109.
+	* : Merge pull request #18 from
+	proftpd/ls-stat-response-codes-bug3990 Fix regression for Bug#3990
 
-2014-09-28  TJ Saunders <tj at castaglia.org>
+2014-06-17  TJ Saunders <tj at castaglia.org>
 
-	* src/inet.c: Bug#4109 - setsockopt() call for IPV6_TCLASS should
-	use IPPROTO_IPV6.
+	* modules/mod_ls.c, tests/t/lib/ProFTPD/Tests/Commands/STAT.pm: Fix
+	the handling of the response code for STAT commands, fixing a
+	regression caused by the previous commits for Bug#3990.
 
-2014-09-23  TJ Saunders <tj at castaglia.org>
+2014-05-30  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS, RELEASE_NOTES: Update news, release notes for backported fix
-	for Bug#4108.
+	* NEWS: Update NEWS for Bug#4063.
 
-2014-09-23  TJ Saunders <tj at castaglia.org>
+2014-05-30  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c: Bug#4108 - SSL handshakes for data connections
-	sometimes stall for 3-30 seconds.
+	* : Merge pull request #16 from
+	proftpd/fsio-smkdir-cifs-eacces-bug4063 Bug#4063 -  Unable to create directory on NFS/CIFS partition:
+	Permission...
 
-2014-08-11  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-30  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4094.
+	* : Merge pull request #17 from
+	proftpd/ls-nlst-opt-a-wrong-directory-bug4069 Bug#4069 - NLST -a shows / directory instead of the current
+	directory.
 
-2014-08-11  TJ Saunders <tj at castaglia.org>
+2014-05-30  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* doc/howto/DisplayFiles.html, src/display.c,
-	tests/t/lib/ProFTPD/Tests/Config/DisplayConnect.pm: Bug#4094 -
-	Available space on file system using %f displays wrong value.
+	* modules/mod_ls.c: Bug#4069 - NLST -a shows / directory instead of
+	the current directory.
 
-2014-07-28  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-29  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4091.
+	* src/fsio.c: Bug#4063 -  Unable to create directory on NFS/CIFS
+	partition: Permission denied.
 
-2014-07-28  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-29  TJ Saunders <tj at castaglia.org>
 
-	* src/privs.c: Reduce the scope of the logging changes to just those
-	PRIVS calls related to the effective (rather than the real) IDs.
+	* : Merge pull request #15 from proftpd/auth-anon-maxclients-bug4068 Bug#4068 - MaxClients directive doesn't work for <Anonymous>
+	sessions.
 
-2014-07-27  TJ Saunders <tj at castaglia.org>
+2014-05-29  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* src/privs.c: Bug#4091 - Log "Operation not permitted" privs errors
-	at NOTICE rather than ERROR.
+	* : commit cd89bc454ba0e3655b01a79ca32cc342595bdd46 Author:
+	tjsaunders <tjsaunders at blackpearlsystems.com> Date:   Thu May 29
+	09:57:31 2014 -0700
 
-2014-07-25  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-29  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, contrib/mod_wrap2_file.c: Update NEWS for backport of fix
-	for Bug#4090.
+	* : Merge pull request #14 from proftpd/sftp-extlog-class-bug4067 ExtendedLog classes for SSH, SFTP (Bug#4067)
 
-2014-07-25  TJ Saunders <tj at castaglia.org>
+2014-05-29  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* contrib/mod_wrap2/mod_wrap2.c, contrib/mod_wrap2_file.c,
-	tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_file.pm: Bug#4090 -
-	mod_wrap2_file does not support IPv6 addresses properly.
+	* modules/mod_log.c,
+	tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm: Better
+	implementation of logging class exclusion, and tested with
+	regression tests, including the new SSH and SFTP logging classes.
 
-2014-07-25  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-28  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Updated NEWS for backport of fix for Bug#4089.
+	* NEWS, RELEASE_NOTES, contrib/mod_sftp/auth.c,
+	contrib/mod_sftp/channel.c, contrib/mod_sftp/fxp.c,
+	contrib/mod_sftp/kex.c, contrib/mod_sftp/service.c,
+	include/modules.h, modules/mod_log.c: Bug#4067 - Create ExtendedLog
+	class for SFTP requests.
 
-2014-07-25  TJ Saunders <tj at castaglia.org>
+2014-05-28  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* contrib/mod_sftp/auth-hostbased.c,
-	contrib/mod_sftp/auth-publickey.c: Bug#4089 - mod_sftp does not
-	allow multiple attempts using a given authentication method.
+	* doc/modules/mod_core.html: Update ServerIdent description to
+	mention the supported variables, and add description for the
+	ServerName directive.
 
-2014-07-19  TJ Saunders <tj at castaglia.org>
+2014-05-28  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4087.
+	* NEWS, doc/contrib/mod_sftp.html: Update NEWS, mod_sftp docs for
+	Bug#4065.
 
-2014-07-16  TJ Saunders <tj at castaglia.org>
+2014-05-28  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/auth.c,
-	tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm: Bug#4087 - mod_sftp
-	does not handle "MaxLoginAttempts none" properly.
+	* : Merge pull request #13 from
+	proftpd/sftp-banner-environ-variable-bug4065 Bug#4065 - mod_sftp should provide the SSH client banner as
+	environment variable, for logging
 
-2014-07-02  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-27  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* NEWS: Backport of fix for Bug#4083 to 1.3.5 branch.
+	* contrib/mod_sftp/mod_sftp.c: Bug#4065 - mod_sftp should provide
+	the SSH client banner as environment variable, for logging.
 
-2014-07-02  TJ Saunders <tj at castaglia.org>
+2014-05-27  tjsaunders <tjsaunders at blackpearlsystems.com>
 
-	* contrib/mod_sql.c: Fix handling of SQLEngine logic in mod_sql,
-	which was contributing to test failures when regressing Bug#4083.
+	* doc/contrib/mod_sftp.html: Improved formatting for mod_sftp
+	environment variables.
 
-2014-07-02  TJ Saunders <tj at castaglia.org>
+2014-05-27  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sql.c,
-	tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm: Bug#4083 -
-	Using SQLDefaultHomedir with null home results in "No such user".
+	* : Merge pull request #12 from
+	proftpd/scoreboard-entry-locking-bug3823 Attempt to mitigate possible cause for Bug#3823 by replacing use of
+	F_SE...
 
-2014-06-20  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-27  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Backport of fix for Bug#4079 to 1.3.5 branch.
+	* src/scoreboard.c: Attempt to mitigate possible cause for Bug#3823
+	by replacing use of F_SETLKW when updating scoreboard entries to
+	F_SETLK, and handling interruptions.  This should reduce any
+	(potentially long) blocking behaviors.
 
-2014-06-20  TJ Saunders <tj at castaglia.org>
+2014-05-26  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Use sftp_msg_write_int() for 32-bit
-	values; write_long() is for 64-bit values.
+	* : Merge pull request #11 from proftpd/signals-refactoring Refactor the signal-handling code out of src/main.c, and into its
+	own so...
 
-2014-06-20  TJ Saunders <tj at castaglia.org>
+2014-05-25  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_sftp/fxp.c: Bug#4079 - Invalid response encoding for
-	SFTP space-available request.
+	* include/conf.h: Previous merge forgot to include the new configdb
+	header.
 
-2014-06-19  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-25  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Updating NEWS for backport of fix for Bug#4077 to 1.3.5
-	branch.
+	* : Merge pull request #10 from proftpd/dirtree-refactoring Refactor the src/dirtree.c file into smaller chunks, one for the
+	pr_conf...
 
-2014-06-17  TJ Saunders <tj at castaglia.org>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_shaper.c: Bug#4077 - ShaperLog not closed/reopened on
-	SIGHUP, causing log rotation problems.
+	* : Merge pull request #9 from
+	proftpd/hiddenstores-pid-variable-bug4062 Bug#4062 - Support PID variable in HiddenStores filename.
 
-2014-06-19  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Backport of fix for Bug#4073 to 1.3.5 branch.
+	* include/log.h, src/log.c, src/main.c: Fix slight regression in
+	logging behavior due to changes for Bug#3983.  If the -d
+	command-line option is used, then make sure the default syslog level
+	is DEBUG, so that the requested debug logging appears.
 
-2014-06-18  TJ Saunders <tj at castaglia.org>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_xfer.c: The heuristic for disabling sendfile support
-	when implicit FTPS is used does not need to include the port number;
-	having TLS negotiated at the time of session initialization is
-	enough.
+	* modules/mod_xfer.c,
+	tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm: Bug#4062 - Support
+	PID variable in HiddenStores filename.
 
-2014-06-17  TJ Saunders <tj at castaglia.org>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c: Remove unnecessary comment.
+	* modules/mod_auth_file.c: Sprinkle more signal handling in
+	potentially long while() loops when handling AuthUserFiles and
+	AuthGroupFiles.
 
-2014-06-17  TJ Saunders <tj at castaglia.org>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* contrib/mod_tls.c, modules/mod_xfer.c: Bug#4073 - Polycom VOIP
-	phones unable to use FTPS data transfers.
+	* modules/mod_auth_file.c: Fix ordering of handlers in
+	mod_auth_file; no functional change.
 
-2014-05-30  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Update NEWS for backport of fix for Bug#4063.
+	* modules/mod_auth_file.c, modules/mod_auth_unix.c: Use better trace
+	channel names in the provided auth modules.
 
-2014-05-29  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* src/fsio.c: Bug#4063 -  Unable to create directory on NFS/CIFS
-	partition: Permission denied.
+	* modules/mod_auth_file.c: Comment typo fixed.  No functional
+	change.
 
-2014-05-30  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-24  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, modules/mod_ls.c: Update NEWS for backport of fix for
-	Bug#4069.
+	* : Merge pull request #8 from
+	proftpd/snmp-multiple-agent-addrs-bug4061 Bug#4061 - SNMPAgent should support multiple addresses, including
+	IPv6
 
-2014-05-30  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-23  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_ls.c: Bug#4069 - NLST -a shows / directory instead of
-	the current directory.
+	* : Merge pull request #7 from proftpd/aix-build-errors Fix AIX build issues, as reported in the forums; see:
 
-2014-05-29  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-21  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Updated NEWS for backport of fix for Bug#4068.
+	* : Merge pull request #6 from proftpd/symtab-hash-comparison When storing symbols in the symbol table hash ("stash"), stop
+	storing al...
 
-2014-05-29  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-21  TJ Saunders <tj at castaglia.org>
 
-	* modules/mod_auth.c: Bug#4068 - MaxClients directive doesn't work
-	for <Anonymous> sessions.
+	* contrib/mod_ctrls_admin.c, contrib/mod_quotatab_ldap.c,
+	contrib/mod_quotatab_radius.c, contrib/mod_quotatab_sql.c,
+	contrib/mod_ratio.c, contrib/mod_sftp_sql.c,
+	contrib/mod_sql_passwd.c, contrib/mod_wrap2_sql.c,
+	include/dirtree.h, include/stash.h, src/auth.c, src/cmd.c,
+	src/main.c, src/modules.c, src/parser.c, src/stash.c,
+	tests/api/stash.c: When storing symbols in the symbol table hash
+	("stash"), stop storing all symbol types (CMD, CONF, AUTH, and HOOK)
+	all in the same table.  Instead, have different tables for each
+	different type.  This makes it faster when looking up (by name) a
+	symbol of a given type; less symbol names to grovel through and
+	compare.  And speaking of symbol comparisons, this change also stores the hash
+	value (of the symbol name) as part of the symbol data.  Thus when
+	doing symbol lookups, the hash value is compared FIRST, and only if
+	that value matches will the more in-depth comparison (by name
+	length, name content) be done.  The overall effect should be a net gain in speed (less latency when
+	dispatching e.g. commands, auth lookups, parsing), and a drop in CPU
+	(due to fewer string comparisons hopefully).
+
+2014-05-20  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #5 from proftpd/parser-pidfile-unit-tests Add unit tests for the Pidfile API, and start working on tests for
+	the
+
+2014-05-19  TJ Saunders <tj at castaglia.org>
+
+	* : Merge pull request #4 from proftpd/trace-unit-tests Adding unit tests for the Trace API; fixed some minor bugs found
+	while
 
-2014-05-27  TJ Saunders <tj at castaglia.org>
+2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* src/scoreboard.c: Attempt to mitigate possible cause for Bug#3823
-	by replacing use of F_SETLKW when updating scoreboard entries to
-	F_SETLK, and handling interruptions.  This should reduce any
-	(potentially long) blocking behaviors.
+	* .gitignore: A few more files for git to ignore.
 
-2014-05-23  tjsaunders <tjsaunders at blackpearlsystems.com>
+2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* include/conf.h, src/fsio.c: Fix AIX build issues, as reported in
-	the forums; see:   https://forums.proftpd.org/smf/index.php/topic,11548.0.html
+	* src/main.c: Stylistic nit; no functional change.
 
 2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* src/fsio.c: Fix error spotted by travis-ci's clang build.
+	* : Merge pull request #3 from proftpd/clang-warnings Clean up some warnings spotted by travis-ci's clang build.
 
 2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, modules/mod_ls.c,
-	tests/t/lib/ProFTPD/Tests/Commands/STAT.pm: Revert the changes for
-	Bug#3990 on the 1.3.5 branch; they were mistakenly merged there.
+	* : Merge pull request #2 from proftpd/radius-timeout-parsing Update mod_radius to use pr_str_get_duration() for parsing timeout
+	strin...
 
 2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* NEWS, src/log.c: Revert change for Bug#3983 from 1.3.5 branch; it
-	was mistakenly merged there.
+	* contrib/mod_radius.c: Update mod_radius to use
+	pr_str_get_duration() for parsing timeout strings for the
+	RadiusAuthServer and RadiusAcctServer directives.  Fixed stylistic
+	nits while there.
 
 2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* Makefile.in: Clean up git files (and travis-ci files) when
-	preparing for a release.
+	* Makefile.in: Clean up the .git/ directory, and .gitignore and
+	.travis.yml files, when preparing a release.
 
 2014-05-18  TJ Saunders <tj at castaglia.org>
 
-	* : commit a6179ff80274079de89da21de3d68865dd209ebf Author: TJ
-	Saunders <tj at castaglia.org> Date:   Sun May 18 17:15:33 2014 -0700
+	* .cvsignore, contrib/.cvsignore, contrib/mod_dnsbl/.cvsignore,
+	contrib/mod_load/.cvsignore, contrib/mod_sftp/.cvsignore,
+	contrib/mod_snmp/.cvsignore, contrib/mod_wrap2/.cvsignore,
+	include/.cvsignore, lib/.cvsignore, lib/libcap/.cvsignore,
+	locale/.cvsignore, modules/.cvsignore, src/.cvsignore,
+	tests/.cvsignore, tests/api/.cvsignore, tests/array.c,
+	tests/class.c, tests/env.c, tests/event.c, tests/expr.c,
+	tests/feat.c, tests/modules.c, tests/netacl.c, tests/netaddr.c,
+	tests/pool.c, tests/regexp.c, tests/scoreboard.c, tests/sets.c,
+	tests/str.c, tests/stubs.c, tests/table.c, tests/tests.c,
+	tests/tests.h, tests/timers.c, tests/var.c, tests/version.c,
+	utils/.cvsignore: Clean up more files from the cvs2git conversion.
 
 2014-05-18  TJ Saunders <tj at castaglia.org>
 
@@ -862,13 +6908,8 @@
 
 2014-05-17  TJ Saunders <tj at castaglia.org>
 
-	* : commit 03aa76e1b15908a509849249e83b0979558b4537 Author: TJ
-	Saunders <tj at castaglia.org> Date:   Sat May 17 17:59:10 2014 -0700
-
-2014-05-17  TJ Saunders <tj at castaglia.org>
-
-	* po/Makefile.in: Remove unused po/ directory, which came back with
-	the cvs2git conversion.
+	* src/netaddr.c: Avoid null pointer dereference for interfaces with
+	no addresses.
 
 2014-05-17  TJ Saunders <tj at castaglia.org>
 
@@ -877,22 +6918,14 @@
 
 2014-05-17  TJ Saunders <tj at castaglia.org>
 
-	* NEWS: Backport of fix for Bug#4055 to 1.3.5 branch.
-
-2014-05-17  TJ Saunders <tj at castaglia.org>
-
-	* : commit 355d16baf192ec86cd015ec27b9b9f2846e9ded3 Author: TJ
-	Saunders <tj at castaglia.org> Date:   Sat May 17 17:46:20 2014 -0700
+	* NEWS: Bug#4055 - "error setting listen fd IPV6_TCLASS: Protocol
+	not available" log message.
 
 2014-05-17  TJ Saunders <tj at castaglia.org>
 
-	* : commit 9f2bdb71a310ad3017d17c4a643acb0f3ebd2bae Author: TJ
-	Saunders <tj at castaglia.org> Date:   Sat May 17 08:50:52 2014 -0700
-
-2014-05-16  tjsaunders <tjsaunders at blackpearlsystems.com>
-
-	* .gitignore: Ignore the generated man page files via .gitignore,
-	too.
+	* : commit 3fd34befa71cdc41bc1375b243a18e8837ee76d0 Author:
+	tjsaunders <tjsaunders at blackpearlsystems.com> Date:   Fri May 16
+	09:53:25 2014 -0700
 
 2014-05-16  tjsaunders <tjsaunders at blackpearlsystems.com>
 
diff --git a/Make.rules.in b/Make.rules.in
index 32796d7..55330b4 100644
--- a/Make.rules.in
+++ b/Make.rules.in
@@ -18,7 +18,7 @@ CC=@CC@
 PLATFORM=@OSREL@ @OSTYPE@
 LDFLAGS=@LDFLAGS@ @LIBDIRS@
 LIBEXECDIR=@LIBEXECDIR@
-LIBS=@LIBS@ @LIBRARIES@
+LIBS=@LIBS@ @LIBRARIES@ @LIBADD_DL@
 LIBTOOL=@LIBTOOL@
 MAKEDEPEND=makedepend -Y
 RANLIB=@RANLIB@
@@ -63,23 +63,25 @@ DEFINES=$(PLATFORM)
 # options.
 MODULE_LIBS_FILE=$(top_builddir)/module-libs.txt
 
-OBJS=main.o timers.o sets.o pool.o privs.o str.o table.o regexp.o dirtree.o \
-     expr.o support.o netaddr.o inet.o child.o parser.o log.o lastlog.o \
-     xferlog.o bindings.o netacl.o class.o scoreboard.o help.o feat.o netio.o \
-     cmd.o response.o data.o modules.o stash.o display.o auth.o fsio.o \
-     mkhome.o ctrls.o event.o var.o throttle.o session.o trace.o encode.o \
-     proctitle.o filter.o pidfile.o env.o version.o rlimit.o wtmp.o memcache.o
+OBJS=main.o timers.o sets.o pool.o privs.o str.o table.o regexp.o configdb.o \
+     dirtree.o expr.o signals.o support.o netaddr.o inet.o child.o parser.o \
+     log.o lastlog.o xferlog.o bindings.o netacl.o class.o scoreboard.o help.o \
+     feat.o netio.o cmd.o response.o ascii.o data.o modules.o stash.o \
+     display.o auth.o fsio.o mkhome.o ctrls.o event.o var.o throttle.o \
+     session.o trace.o encode.o proctitle.o filter.o pidfile.o env.o version.o \
+     rlimit.o wtmp.o json.o memcache.o redis.o
 
 BUILD_OBJS=src/main.o src/timers.o src/sets.o src/pool.o src/privs.o src/str.o \
-           src/table.o src/regexp.o src/dirtree.o src/expr.o src/support.o \
-           src/netaddr.o src/inet.o src/child.o src/parser.o src/log.o \
-           src/lastlog.o src/xferlog.o src/bindings.o src/netacl.o src/class.o \
-           src/scoreboard.o src/help.o src/feat.o src/netio.o src/cmd.o \
-           src/response.o src/data.o src/modules.o src/stash.o src/display.o \
-           src/auth.o src/fsio.o src/mkhome.o src/ctrls.o src/event.o \
-           src/var.o src/throttle.o src/session.o src/trace.o src/encode.o \
-           src/proctitle.o src/filter.o src/pidfile.o src/env.o src/version.o \
-           src/rlimit.o src/wtmp.o src/memcache.o
+           src/table.o src/regexp.o src/configdb.o src/dirtree.o src/expr.o \
+           src/signals.o src/support.o src/netaddr.o src/inet.o src/child.o \
+           src/parser.o src/log.o src/lastlog.o src/xferlog.o src/bindings.o \
+           src/netacl.o src/class.o src/scoreboard.o src/help.o src/feat.o \
+           src/netio.o src/cmd.o src/response.o src/ascii.o src/data.o \
+           src/modules.o src/stash.o src/display.o src/auth.o src/fsio.o \
+           src/mkhome.o src/ctrls.o src/event.o src/var.o src/throttle.o \
+           src/session.o src/trace.o src/encode.o src/proctitle.o src/filter.o \
+           src/pidfile.o src/env.o src/version.o src/rlimit.o src/wtmp.o \
+           src/json.o src/memcache.o src/redis.o
 
 SHARED_MODULE_DIRS=@SHARED_MODULE_DIRS@
 SHARED_MODULE_LIBS=@SHARED_MODULE_LIBS@
diff --git a/Makefile.in b/Makefile.in
index 6929976..d1c2e96 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -207,6 +207,9 @@ clean:
 
 distclean: clean
 	cd lib/ && $(MAKE) distclean
+	cd modules/ && $(MAKE) distclean
+	cd src/ && $(MAKE) distclean
+	cd utils/ && $(MAKE) distclean
 	rm -f Makefile Make.modules Make.rules \
 	      contrib/Makefile include/Makefile lib/Makefile \
 	      locale/Makefile modules/Makefile src/Makefile \
@@ -218,7 +221,6 @@ distclean: clean
 spec:
 	# RPM needs this in the top-level directory in order to support '-t'
 	mv -f contrib/dist/rpm/proftpd.spec .
-	cat proftpd.spec | sed 's/global proftpd_version.*/global proftpd_version\t$(RELEASE_VERSION)/' | sed 's/global proftpd_cvs_version_main.*/global proftpd_cvs_version_main\t$(RELEASE_VERSION)/' | sed 's/global release_cand_version.*/global release_cand_version\t$(RC_VERSION)/' > /tmp/proftpd-build-spec.tmp && mv /tmp/proftpd-build-spec.tmp proftpd.spec
 
 dist: depend distclean spec
 	rm -rf `find . -name CVS`
diff --git a/NEWS b/NEWS
index 2734c74..7ba4d16 100644
--- a/NEWS
+++ b/NEWS
@@ -6,35 +6,84 @@
     http://bugs.proftpd.org/show_bug.cgi?id=N
 
   where `N' is the bug number.
+
+  If the issue listed below mentions "Issue" instead of "Bug", the number
+  there references a GitHub issue, which can be found using a URL like:
+
+    https://github.com/proftpd/proftpd/issues/N
+
+  where `N' is the issue number.
 -----------------------------------------------------------------------------
 
-1.3.5e - Released 09-Apr-2017
+1.3.6 - Released 09-Apr-2017
 --------------------------------
-- Bug 4287 - SFTP clients using umac-64 at openssh.com digest fail to connect.
-- Bug 4288 - SFTP rekeying failure with ProFTPD 1.3.5d, caused by null
-  pointer dereference.
+- Bug 4284 - SITE UTIME not working with group permissions.
+- Bug 4289 - LDAPSearchScope does not alter search scope as expected.  When
+  the LDAPServer directive is used with LDAP URLs, the LDAPSearchScope should
+  not be used; the handler was failing to handle this case properly.
+- Bug 4285 - In AIX, log failed logins so that user accounts can be locked by
+  the OS after multiple failed login attempts.
+- Added mod_wrap2_redis to the contrib/ modules directory.
 - Bug 4295 - AllowChrootSymlinks off does not check entire DefaultRoot path
   for symlinks (CVE-2017-7418).
+- Bug 4299 - TimeoutLogin not working for SFTP connections.
 
-1.3.5d - Released 15-Jan-2017
+1.3.6rc4 - Released 15-Jan-2017
 --------------------------------
 - Bug 4283 - All FTP logins treated as anonymous logins again.  This is a
   regression of Bug#3307.
 
-1.3.5c - Released 14-Jan-2017
+1.3.6rc3 - Released 14-Jan-2017
 --------------------------------
+- Bug 4222 - Add support for curve25519-sha256 at libssh.org key exchange.
+- Bug 4186 - ProFTPD creates name-based vhost when it should not.
+- Bug 4233 - Support enforcing minimum key lengths for SFTP/SCP.
+- Bug 4235 - Recursive SCP uploads of directories fail with "No such file or
+  directory".
+- Bug 4154 - Support for scrypt in mod_sql_passwd.  This also includes
+  support for Argon2, assuming use of libsodium-1.0.9 or later.
+- Bug 4237 - Corrupted ASCII uploads.  The refactoring work for Bug#4151 had
+  introduced a bug in the handling of ASCII uploads, now fixed.
+- Bug 4220 - Support reading geoip filters from SQL databases.
+- Bug 4242 - Using mod_memcache results in segfault (signal 11).  Caused by
+  a bug in libmemcached-1.0.18 and earlier.
+- Bug 4244 - Long-running sessions consume memory continuously.
+- Bug 4248 - ALLO command failed unexpectedly.
+- Bug 4247 - ProFTPD runs out of memory when listing very large directories
+  (e.g. over 400GB).
 - Bug 4254 - SSH rekey during authentication can cause issues with clients.
 - Bug 4257 - Recursive SCP uploads of multiple directories not handled properly.
+- Bug 4252 - Clients sometimes receive extra 450 response on ABOR.
 - Bug 4259 - LIST returns different results for file, depending on path syntax.
-- Bug 4255 - "AuthAliasOnly on" in server config breaks anonymous logins.
+- Bug 4255 - "AuthAliasOnly on" in server config breaks anonymous logins. 
+- Bug 4240 - Support OpenSSL 1.1.x API.
+- Bug 4262 - SITE CPTO returns 250 even when the copy operation fails due to
+  the user's quota being exceeded.
+- Bug 4263 - SITE CPTO terminated by TimeoutIdle.
+- Bug 4265 - Omit version information from ServerIdent banner by default.
+- Bug 4264 - Support larger fixed DH groups in SSH key exchange.
+- Bug 3849 - Allow wildcarded directory names in Include patterns.
+- Bug 3662 - Allow SIZE command while in ASCII mode via build-time option.
 - Bug 4272 - CapabilitiesEngine directive not honored for <IfUser>/<IfGroup>
   sections.
-- Bug 4275 - Support OpenSSL 1.1.x API.
+- Bug 4267 - NLST should allow for sorted output.  The ListOptions directive
+  now supports a SortedNLST flag for such use cases.
+- Bug 4260 - Restarting server fails when using password-protected SSL
+  certificates and no TLSPassPhraseProvider.
+- Bug 4216 - SSH rekeying causes invalid packet due to interrupting timer.
 - Bug 4278 - Memory leak when mod_facl is used.
 
-1.3.5b - Released 10-Mar-2016
+1.3.6rc2 - Released 10-Mar-2016
 --------------------------------
 - Bug 4187 - mod_geoip does not load all of the GeoIPTables properly.
+- Bug 4167 - CR/LF characters are not supported in filenames.
+- Bug 4188 - Support filtering based on country code and regional code in
+  mod_geoip.
+- Bug 4151 - FTP ASCII mode conversion algorithm is painfully slow.
+- Bug 4139 - Support rejecting empty passwords.  See new AllowEmptyPasswords
+  directive.
+- Bug 4189 - Support protocol exclusion via TLSProtocol directive.
+- Bug 4153 - Support requiring multiple SSH authentication methods.
 - Bug 4191 - "Incorrect string value" reported by mod_sql_mysql for some UTF8
   characters.
 - Bug 4097 - SSH rekey fails when using RSA hostkey smaller than 2048 bits.
@@ -45,69 +94,129 @@
 - Bug 4202 - MLSD lines not properly terminated with CRLF.
 - Bug 4209 - Zero-length memory allocation possible, with undefined results.
 - Bug 4210 - Avoid unbounded SFTP extended attribute key/values.
+- Bug 4104 - Handle MasqueradeAddress resolution errors due to startup
+  sequencing.
+- Bug 4056 - Support using JSON when storing ban information in memcached.
+- Bug 4057 - Support using JSON when storing TLS session information in
+  memcached.
 - Bug 4212 - Ensure that FTP data transfer commands fail appropriately when
   "RootRevoke on" is in effect.
+- Bug 4175 - Support for OCSP stapling.
+- Bug 4176 - Support for TLS session tickets.
+- Bug 4200 - Support TLS client configuration for SQL servers.
+- Bug 4213 - Deprecate the NoCertRequest TLSOption.
+- Bug 4214 - Allow UseEncoding to be set on a per-user basis.
 - Bug 4217 - Handle FTP re-authentication attempts better.
+- Bug 4218 - Support a LogFormat variable for logging command duration in
+  milliseconds.
 - Bug 4223 - Permissions on files uploaded via STOU do not honor configured
   Umask.
+- Bug 4219 - Better handling of symlinks when chrooted.
+- Bug 4224 - Prohibit FTP indexing by web crawlers via auto-generated
+  robots.txt.
+- Added mod_auth_otp, mod_digest, mod_statcache to the contrib/ modules
+  directory.
 - Bug 4227 - Support SFTP clients that send multiple INIT requests.
 - Bug 4230 - TLSDHParamFile directive appears ignored because unexpected DH is
   chosen.
-- Bug 4242 - Using mod_memcache results in segfault (signal 11).  Caused by
-  a bug in libmemcached-1.0.18 and earlier.
-- Bug 4248 - ALLO command failed unexpectedly.
-- Bug 4247 - ProFTPD runs out of memory when listing very large directories
-  (e.g. over 400GB).
 
-1.3.5a - Released 27-May-2015
+1.3.6rc1 - Released 27-May-2015
 --------------------------------
 - Bug 4055 - "error setting listen fd IPV6_TCLASS: Protocol not available" log
   message.
 - Bug 3944 - Session closed if active data transfer fails due to "Address
   already in use" error.
+- Bug 3983 - Change default SyslogLevel to be NOTICE rather than DEBUG.
+- Bug 3990 - Use 213 response code for STAT on a file.  STAT on a directory
+  now results in a 212 response code as well, per RFC 959.
+- Bug 4061 - SNMPAgent should support multiple addresses, including IPv6
+  addresses.
+- Bug 4062 - Support PID variable in HiddenStores filename.
+- Bug 4065 - mod_sftp should provide the SSH client banner as environment
+  variable, for logging.
+- Bug 4067 - Create ExtendedLog class for SFTP requests.
 - Bug 4068 - MaxClients directive doesn't work for <Anonymous> sessions.
 - Bug 4069 - NLST -a shows / directory instead of the current directory.
 - Bug 4063 - Unable to create directory on NFS/CIFS partition: Permission
   denied.
 - Bug 4073 - Polycom VOIP phones unable to use FTPS data transfers.
+- Bug 4070 - Support wider range of causes of authentication failure.
 - Bug 4077 - ShaperLog not closed/reopened on SIGHUP, causing log rotation
   problems.
+- Bug 4076 - Ability to disable mod_exec on a per-directory basis.
 - Bug 4079 - Invalid response encoding for SFTP space-available request.
+- Bug 4080 - mod_sftp does not implement SFTP LINK request properly.
 - Bug 4083 - Using SQLDefaultHomedir with null home results in "No such user".
+- Bug 4084 - "NLST *" returns files from subdirectories.
+- Bug 4081 - Not possible to create relative symlinks with SFTP.
 - Bug 4087 - mod_sftp does not handle "MaxLoginAttempts none" properly.
 - Bug 4089 - mod_sftp does not allow multiple attempts using a given
   authentication method.
 - Bug 4090 - mod_wrap2_file does not support IPv6 addresses properly.
 - Bug 4091 - Log "Operation not permitted" privs errors at NOTICE rather than
   ERROR.
+- Bug 4093 - Improve mod_sftp handling of missing packet payloads.
+- Bug 4050 - Use of PIPE_BUF causes build failure on platforms without it.
+- Bug 4020 - Add minimum delay options to mod_delay functionality.
+- Bug 4030 - Cache negative/failed Auth API name/ID lookups.
 - Bug 4094 - Available space on file system using %f displays wrong value.
+- Bug 4012 - Failure to build mod_tls when using static libcrypto due to libdl
+  linker errors.
+- Bug 4098 - mod_sftp unable to use SFTPHostKey due to being group readable in
+  CentOS 7.
 - Bug 4108 - SSL handshakes for data connections sometimes stall for 3-30
   seconds.
 - Bug 4109 - setsockopt() call for IPV6_TCLASS should use IPPROTO_IPV6.
+- Bug 4110 - proftpd on Solaris should use /dev/conslog instead of /dev/log.
+- Bug 4114 - mod_tls should not support SSLv3 by default.
 - Bug 4112 - Failure to connect using mod_sftp sometimes due to too-small
   buffers.
-- Bug 4114 - mod_tls should not support SSLv3 by default.
+- Bug 4035 - HiddenStores file not renamed every time.
 - Bug 4116 - Report exact SSL/TLS protocol version used in client connections.
 - Bug 4124 - DeleteAbortedStores defaults to "on" for all transfers, not just
   HiddenStores.
 - Bug 4129 - mod_sql caches incorrect UID/GID when name cannot be retrieved.
+- Bug 4130 - Support the 3-timestamp form of SITE UTIME.
 - Bug 4131 - mod_sftp's autoconf script does not detect OpenSSL SHA2 support.
+- Bug 4058 - Create a 'timing' trace channel, for timing-related data.
+- Bug 4125 - mod_lang should provide way to reject illegally-encoded filenames.
+- Bug 4060 - Support unsorted LIST entries (-U) to decrease memory/CPU usage
+  for large directory listings.
 - Bug 4133 - LDAPUsers directive does not honor uid-number-filter-template
   parameter.
 - Bug 4137 - GeoIPDenyFilter incorrectly takes precedence over GeoIPAllowFilter.
+- Bug 4138 - Support for hex-encoded salts in mod_sql_passwd.
 - Bug 4140 - SFTP READLINK requests to symlinks to directories fail.
 - Bug 4143 - HTTPS/FTPS protocol confusion leads to XSS.
+- Bug 4144 - Support APPE when HiddenStores are enabled.
+- Bug 4031 - Support JSON output format for ftpwho.
 - Bug 4145 - Segfault if AuthUserFile is a relative symlink.
 - Bug 4152 - Reduce logging of non-fatal "unable to open incoming connection"
   errors.
 - Bug 4155 - SSH keys with too-long Comment headers aren't recognized by
   mod_sftp_sql.
+- Bug 4159 - Support ability to disable ASCII translation transparently to FTP
+  clients.
 - Bug 4156 - Segfault handling LIST/NLST FTP command on Mac OS X.
 - Bug 4160 - Malformed response to SSH_FXP_REALPATH with SFTP version 6.
+- Bug 4163 - Remove support for EXPORT grade ciphers.
+- Bug 4164 - mod_sql fails to read UID/GID values larger than 32 bits from SQL
+  tables.
+- Bug 4157 - LIST/NLST of 1000s of files is slow on some platforms.
+- Bug 4059 - Implement additional RADIUS attributes.
+- Bug 4166 - mod_sftp sessions consume large amounts of memory due to rekeying.
 - Bug 4169 - Unauthenticated copying of files via SITE CPFR/CPTO allowed by
   mod_copy.
+- Bug 4170 - Incorrect handling of control-byte field of SSH_FXP_REALPATH as
+  bitmask rather than enumeration for SFTP protocol version 6.
+- Bug 4168 - Race condition with HiddenStores and TimeoutIdle timeout, causing
+  hidden file not to be cleaned up properly.
+- Bug 3125 - Support for Mac OS X implementation of POSIX ACLs.
+- Bug 4174 - Support for TLS-PSK (pre-shared keys).
 - Bug 4178 - TLS session reuse requirement for data connections not properly
   enforced.
+- Bug 4184 - Remove support for "weak" Diffie-Hellman groups.
+- Bug 3289 - Support the HOST command.
 
 1.3.5 - Released 15-May-2014
 --------------------------------
diff --git a/README b/README
deleted file mode 100644
index 8c48c4d..0000000
--- a/README
+++ /dev/null
@@ -1,122 +0,0 @@
-
-                        ProFTPD 1.3.x README
-                        ==================
-
-Introduction
-------------
-
-ProFTPD is a highly configurable FTP daemon for Unix and Unix-like
-operating systems.  See the README.ports file for more details about
-the platforms on which ProFTPD in known or thought to build and run.
-
-ProFTPD grew from a desire for a secure and configurable FTP server.
-It was inspired by a significant admiration of the Apache web server.
-Unlike most other Unix ftp servers, it has not been derived from the old
-BSD ftpd code base, but is a completely new design and implementation.
-
-ProFTPD's extensive configurability provides systems adminstrators great
-flexibility in user authentication and access controls, including virtual
-ftp users and easy chroot() ftp sessions for individual users.
-
-ProFTPD is popular with many service providers for delivering update
-access to user web pages, without resorting to Unix shell accounts.
-
-ProFTPD powers many well-known, high-volume anonymous FTP sites,
-including debian.org, kernel.org, redhat.com and sourceforge.net.
-
-ProFTPD is bundled with several Linux distributions, including
-Conectiva and Trustix.
-
-
-Latest Release
---------------
-
-     ftp://ftp.proftpd.org/distrib/
-     http://www.proftpd.org/
-
-     + see RELEASE_NOTES for an overview of the changes in this release
-
-Major Features
---------------
-
-    o A single main configuration file, with directives and directive groups
-      patterned after those of the Apache web server. 
-
-    o Per directory ".ftpaccess" configuration similar to Apache's ".htaccess". 
-
-    o Designed to run either as a stand-alone server or from inetd.
-
-    o Multiple virtual FTP servers and anonymous FTP services. 
-
-    o Multiple passwd files.
-
-    o Shadow password support, including support for expired accounts.
-
-    o Multiple authentication methods, including PAM, LDAP and SQL.
-
-    o Virtual users.
-
-    o ProFTPD never executes any external program at any time.
-      There is no SITE EXEC command, and all file and directory listings
-      are generated internally, without using an external ls command.
-
-    o Anonymous FTP and other chroot directories do not require any specific
-      directory structure, executable programs or other system files. 
-
-    o Modular architecture with an API that facilitates well structured
-      extensions to meet user needs.
-
-    o Visibility of directories or files controlled based on Unix style
-      permissions or user/group ownership. 
-
-    o Logging and utmp/wtmp support.  Logging is compatible with wu-ftpd,
-      and extended, customizable logging is available.
-
-    o If supported by the capabilities the host system, it can run as a
-      non-privileged user in stand-alone mode, thwarting attacks aimed at
-      exploiting "root" privileges.
-
-    o GPL source license.  The source code is available to audit. 
-
-
-
-Documentation
--------------
-
-        The doc/ directory
-
-        http://www.proftpd.org/docs/
-
-
-Installation Overview
----------------------
-
-For detailed installation instructions, see the INSTALL file in the root
-directory of the source distribution.
-
-The ProFTPD source distribution is designed to be configured using the
-GNU autotools, so compiling and installing follows the familiar command
-sequence of './configure ; make ; make install'.  However, a significant
-portion of ProFTPD's configurability is done at compile time, so it is
-highly recommended that you read INSTALL and all the README.* files that
-pertain to your platform and desired features before building the sources.
-
-ProFTPD uses a single configuration file.  A few examples are included in
-the sample-configurations subdirectory of the source distribution.
-
-On most systems, the inetd or xinetd configuration must be changed,
-either to remove the current ftpd entry to run ProFTPD standalone,
-or to change the current ftpd entry to use the proftpd daemon.
-
-Questions
----------
-
-If you have questions, please ask them on the appropriate mailing lists:
-
-  http://www.proftpd.org/lists.html
-
-If you don't understand the documentation, please tell us, so we can explain it
-better.  The general idea is: if you need to ask for help, then something needs
-to be fixed so you (and others) don't need to ask for help.  Asking questions
-helps us to know what needs to be documented, described, and/or fixed.
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f6b5f33
--- /dev/null
+++ b/README.md
@@ -0,0 +1,100 @@
+ProFTPD 1.3.x README
+====================
+
+Status
+------
+[![Build Status](https://travis-ci.org/proftpd/proftpd.svg?branch=master)](https://travis-ci.org/proftpd/proftpd)
+[![Coverage Status](https://coveralls.io/repos/proftpd/proftpd/badge.svg?branch=master&service=github)](https://coveralls.io/github/proftpd/proftpd?branch=master)
+[![Coverity Scan Status](https://scan.coverity.com/projects/198/badge.svg)](https://scan.coverity.com/projects/198)
+[![Release](https://img.shields.io/badge/release-1.3.5d-brightgreen.svg)](https://github.com/proftpd/proftpd/releases/latest)
+[![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg)
+
+Introduction
+------------
+
+ProFTPD is a highly configurable FTP daemon for Unix and Unix-like
+operating systems.  See the _**README.ports**_ file for more details about
+the platforms on which ProFTPD in known or thought to build and run.
+
+ProFTPD grew from a desire for a secure and configurable FTP server.
+It was inspired by a significant admiration of the Apache web server.
+Unlike most other Unix FTP servers, it has not been derived from the old
+BSD `ftpd` code base, but is a completely new design and implementation.
+
+ProFTPD's extensive configurability provides systems administrators great
+flexibility in user authentication and access controls, including virtual
+users and easy `chroot()` FTP sessions for individual users.
+
+ProFTPD is popular with many service providers for delivering update
+access to user web pages, without resorting to Unix shell accounts.
+
+Latest Release
+--------------
+
+- ftp://ftp.proftpd.org/distrib/source/
+- http://www.proftpd.org/
+
+>see _**RELEASE_NOTES**_ for an overview of the changes in this release.
+
+Major Features
+--------------
+
+- A single main configuration file, with directives and directive groups patterned after those of the Apache web server. 
+
+- Per directory ".ftpaccess" configuration similar to Apache's ".htaccess". 
+
+- Designed to run either as a stand-alone server or from `inetd`/`xinetd`.
+
+- Multiple virtual FTP servers and anonymous FTP services. 
+
+- Multiple password files.
+
+- Shadow password support, including support for expired accounts.
+
+- Multiple authentication methods, including PAM, LDAP, SQL, and RADIUS.
+
+- Virtual users.
+
+- ProFTPD never executes any external program at any time. There is no `SITE EXEC` command, and all file and directory listings are generated internally, without using an external ls command.
+
+- Anonymous FTP and other chroot directories do not require any specific directory structure, executable programs or other system files. 
+
+- Modular architecture with an API that facilitates well structured extensions to meet user needs.
+
+- Visibility of directories or files controlled based on Unix style permissions or user/group ownership. 
+
+- Logging and utmp/wtmp support.  Logging is compatible with `wu-ftpd`, and extended, customizable logging is available.
+
+- If supported by the capabilities the host system, it can run as a non-privileged user in stand-alone mode, thwarting attacks aimed at exploiting "root" privileges.
+
+- GPLv2 source license.  The source code is available to audit.
+
+Documentation
+-------------
+
+- The [doc/](doc/) directory
+- http://www.proftpd.org/docs/
+
+Installation Overview
+---------------------
+
+For detailed installation instructions, see the _**INSTALL**_ file in the root directory of the source distribution.
+
+The ProFTPD source distribution is designed to be configured using the GNU autotools, so compiling and installing follows the familiar command sequence of
+
+    $ ./configure
+    $ make
+    $ make install
+
+However, a significant portion of ProFTPD's configurability is done at compile time, so it is highly recommended that you read _**INSTALL**_ and all of the _**README.***_ files that pertain to your platform and desired features before building the sources.
+
+ProFTPD uses a single configuration file.  A few examples are included in the [sample-configurations/](sample-configurations/) subdirectory of the source distribution.
+
+On most systems, the `inetd` or `xinetd` configuration must be changed, either to remove the current ftpd entry to run ProFTPD standalone, or to change the current ftpd entry to use the proftpd daemon.
+
+Questions
+---------
+
+If you have questions, please ask them on the appropriate [mailing lists](http://www.proftpd.org/lists.html).
+
+If you don't understand the documentation, please tell us, so we can explain it better.  The general idea is: if you need to ask for help, then something needs to be fixed so you (and others) don't need to ask for help.  Asking questions helps us to know what needs to be documented, described, and/or fixed.
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 21a22ad..fad64d6 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -1,641 +1,726 @@
-                    1.3.5 Release Notes
+                    1.3.6 Release Notes
                   ------------------------
 
 This file contains a description of the major changes to ProFTPD for the
-1.3.5 release cycle, from the 1.3.5rc1 release to the 1.3.5 maintenance
+1.3.6 release cycle, from the 1.3.6rc1 release to the 1.3.6 maintenance
 releases.  More information on these changes can be found in the NEWS and
 ChangeLog files.
 
-1.3.5e
+1.3.6
 ---------
-  + Fixed SFTP issue with umac-64 at openssh.com digest/MAC.
-  + Fixed regression with mod_sftp rekeying.
-  + Backported fix for "AllowChrootSymlinks off" checking each component
-    for symlinks (CVE-2017-7418).
 
+  + Support for using Redis for caching, logging; see the doc/howto/Redis.html
+    documentation.
+  + Fixed mod_sql_postgres SSL support (Issue #415).
+  + Support building against LibreSSL instead of OpenSSL (Issue #361).
+  + Better support on AIX for login restraictions (Bug #4285).
+  + TimeoutLogin (and other timeouts) were not working properly for SFTP
+    connections (Bug#4299).
+  + Handling of the SIGILL and SIGINT signals, by the daemon process, now causes
+    the child processes to be terminated as well (Issue #461).
+  + RPM .spec file naming changed to conform to Fedora guidelines.
+  + Fix for "AllowChrootSymlinks off" checking each component for symlinks
+    (CVE-2017-7418).
 
-1.3.5d
----------
 
-  + Fixed regression where all normal FTP users were handled as anonymous
-    users.
+  + New Modules:
 
+    mod_redis, mod_tls_redis, mod_wrap2_redis
+      With Redis now supported as a caching mechanism, similar to Memcache,
+      there are now Redis-using modules: mod_redis (for configuring the Redis
+      connection information), mod_tls_redis (for caching SSL sessions and
+      OCSP information using Redis), and mod_wrap2_redis (for using ACLs stored
+      in Redis).
 
-1.3.5c
----------
+      See the respective module documentation for details:
+        doc/modules/mod_redis.html, doc/contrib/mod_tls_redis.html, and
+        doc/contrib/mod_wrap2_redis.html.
 
-  + Support for OpenSSL 1.1.x API changes.
-  + Fixed memory leak when mod_facl is used.
-  + Fixed handling of CapabilitiesEngine in <IfUser>/<IfGroup> sections.
 
+  + Changed Modules:
 
-1.3.5b
----------
+    mod_ban
+      The mod_ban module's BanCache directive can now use Redis-based caching;
+      see doc/contrib/mod_ban.html#BanCache.
 
-  + SSH RSA hostkeys smaller than 2048 bits now work properly.
-  + MLSD response lines are now properly CRLF terminated.
-  + Fixed selection of DH groups from TLSDHParamFile.
 
+  + New Configuration Directives
 
-1.3.5a
----------
+    SQLPasswordArgon2, SQLPasswordScrypt
+      The key lengths for Argon2 and Scrypt-based passwords are now configurable
+      via these new directives; previously, the key length had been hardcoded
+      to be 32 bytes, which is not interoperable with all other implementations
+      (Issue #454).
 
-  + Fixed "stalled" SSL/TLS handshakes for data transfers.
-  + Fixed handling of SSH keys with overlong Comment headers in mod_sftp_sql.
+      See doc/contrib/mod_sql_passwd.html#SQLPasswordArgon2,
+        doc/contrib/mod_sql_passwd.html#SQLPasswordScrypt for details.
 
-  + Changed Modules
 
-    mod_tls
-      By default, mod_tls will no longer support SSLv3 connections.  In
-      order to support SSLv3 connections (for sites that need to), you must
-      explicitly configure this via the TLSProtocol directive, e.g.:
+  + Changed Configuration Directives
 
-        TLSProtocol SSLv3 TLSv1 ...
+    AllowChrootSymlinks
+      When "AllowChrootSymlinks off" was used, only the last portion of the
+      DefaultRoot path would be checked to see if it was a symlink.  Now,
+      each component of the DefaultRoot path will be checked to see if it is
+      a symlink when "AllowChrootSymlinks off" is used.
 
+    Include
+      The Include directive can now be used within a <Limit> section, e.g.:
 
-  + New Configuration Directives
+        <Limit LOGIN>
+          Include /path/to/allowed.txt
+          DenyAll
+        </Limit>
 
-    CopyEngine
-      The mod_copy module is enabled by default; there may be cases where
-      the module should be disabled, without requiring a rebuild of the
-      server.  Thus mod_copy now supports a CopyEngine directive; see
-      doc/contrib/mod_copy.html#CopyEngine.
 
+  + API Changes
 
-  + Changed Configuration Directives
-
-    DeleteAbortedStores
-      The DeleteAbortedStores directive, for Bug#3917, was meant to be
-      enabled, but only when HiddenStores was in effect.  Unfortunately,
-      the fix caused a regression, as DeleteAbortedStores was enabled for
-      ALL transfers inadvertently.  The desired behavior, of enabling
-      DeleteAbortedStores only when HiddenStores is in effect, has been
-      properly implemented now.
+    A new JSON API has been added, for use by third-party modules.
 
 
-1.3.5
+1.3.6rc4
 ---------
 
-  + TLS 1.1/1.2 configuration now works properly.
-
-  + New Configuration Directives
+  + Fixed regression where all normal FTP users were handled as anonymous
+    users.
 
-    RLimitChroot
-      When proftpd chroots a session (e.g. via DefaultRoot or <Anonymous>),
-      certain attacks become possible, such as the "Roaring Beast" attack:
 
-        http://auscert.org.au/15286
-        https://auscert.org.au/15526
+1.3.6rc3
+---------
 
-      To help mitigate these attacks, proftpd now rejects any attempt to do
-      a write of any kind to paths under /etc and /lib, when the session is
-      chrooted to a path other than "/".
+  + Support for OpenSSL 1.1.x API changes.
+  + mod_sftp uses SHA256 for publickey fingerprints by default, rather than
+    SHA1 or MD5.
+  + mod_sql_passwd supports Scrypt and Argon2 password hashing algorithms
+    when libsodium is detected in the header/library directories.
+  + mod_sftp now supports extended attributes (xattrs) via SFTP.
+  + Fixed bug in filesystem free space calculation causing ALLO to fail
+    unexpectedly.
+  + Default FTP/SSH2 banner changed to no longer include version information.
+  + New -X command-line option for disabling forking, to aid debugging.
+  + Logging of stacktraces now enabled by default; this also means that the
+    installed executables do not have symbols stripped at install time.
+  + Fixed memory leak when the mod_facl module is used.
+  + Support for UNIX.ownername and UNIX.groupname MLSD/MLST facts.
+  + Use initgroups(3) for group membership discovery by default, as it is
+    faster/more performant on most systems.  For the previous behavior, use
+    the NoInitgroups AuthUnixOption.
+
+
+  + Changed Modules:
+
+    mod_ctrls_admin
+      The mod_ctrls_admin module supports a new "config" ftpdctl action, which
+      can be used to dynamically change configuration directives in the
+      running daemon.  See doc/contrib/mod_ctrls_admin.html#config for more
+      details.
+  
 
-      If these restrictions cause problems for any sites, this guard can be
-      disabled via the new RLimitChroot directive, e.g.:
+  + New Configuration Directives:
 
-        RLimitChroot off
+    CopyOptions
+      The mod_copy module now supports a CopyOptions directive, for tweaking
+      particular behaviors such as how to handle copy failures.  See the
+      documentation at doc/contrib/mod_copy.html#CopyOptions for details.
 
-      See doc/modules/mod_rlimit.html#RLimitChroot for more information.
+    DigestDefaultAlgorithm
+      The mod_digest module uses the SHA1 algorithm for digests by default.
+      Use the DigestDefaultAlgorithm to change this default as needed; see
+      doc/contrib/mod_digest.html#DigestDefaultAlgorithm.
 
+    FSOptions
+      The --enable-xattr compile option enables support for extended attributes.
+      There will be cases where this support needs to be disabled at runtime;
+      use this option for doing so.  See doc/modules/mod_core.html#FSOptions
+      for more information.
 
-  + Changed Configuration Directives
+    IncludeOptions
+      The Include directive now supports wildcarded directory names.  However,
+      this behavior may not be desirable or have other consequences.  Use
+      the new IncludeOptions directive for altering this behavior, such as
+      ignoring temporary files:
+
+        IncludeOptions IgnoreTempFiles
+
+      For other options, see the documentation at
+      doc/modules/mod_core.html#IncludeOptions.
+
+    MaxPasswordSize
+      A denial-of-service attack can be mounted by causing excessive CPU
+      usage via very long passwords processed via the crypt(3) function.
+      To mitigate such resource consumption attacks, ProFTPD now imposes
+      a maximum limit on the password length; that limit can be configured
+      via this directive, documented at
+      doc/modules/mod_auth.html#MaxPasswordSize.
+
+    SFTPKeyLimits
+      As cryptographic attacks grow more sophisticated, shorter keys become
+      more at risk of compromise.  The SFTPKeyLimits directive provides the
+      administrator a way to set stronger policies by imposing a minimum key
+      length for the various types of keys: RSA, DSA, ECC.  For more
+      information, see doc/contrib/mod_sftp.html#SFTPKeyLimits.
+
+    SQLPasswordCost
+      When libsodium support is detected by the build system, memory-hard
+      algorithms such as scrypt and Argon2 are supported by the mod_sql_passwd
+      module.  These algorithms have tunable "costs"; the new SQLPasswordCost
+      directive is for such tunings; see
+      doc/contrib/mod_sql_passwd.html#SQLPasswordCost.
+
+
+  + Changed Configuration Directives:
+
+    AuthUnixOption
+      ProFTPD will now try to use the initgroups(3) library function for
+      group discovery by default.  If this change of behavior leads to
+      issues, the previous behavior can be restored using this new
+      NoInitgroups AuthUnixOption.  See
+      doc/modules/mod_auth_unix.html#AuthUnixOptions for details.
+
+    CapabilitiesEngine
+      The CapabilitiesEngine directive of the mod_cap module was not being
+      properly honored when used within <IfUser> or <IfGroup> sections; this
+      has now been fixed.
 
-    SFTPOptions AllowInsecureLogin
-      Some SFTP clients may wish to use the 'none' cipher, and/or 'none' digest,
-      for testing purposes.  For example, disabling the cipher and digest can
-      be used for testing the raw transfer speed over SFTP.
+    FactsOptions
+      The MLSD/MLST responses now include the UNIX.ownername and UNIX.groupname
+      facts, for providing the textual labels.  Some FTP clients may not
+      handle these facts; use this new NoNames FactsOption value to have
+      these new name facts not included in the MLSD/MLST responses by default.
+      The documentation at doc/modules/mod_facts.html#FactsOptions contains
+      more information.
 
-      mod_sftp, by default, will not allow connections which attempt to use the
-      'none' cipher or 'none' digest, even if these are explicitly enabled via
-      the SFTPCiphers and SFTPDigests directive, as use of these algorithms
-      disables the security protections on the transferred data (such as
-      username/password).
+    GeoIPAllowFilter, GeoIPDenyFilter
+      The GeoIPAllowFilter and GeoIPDenyFilter directives of the mod_geoip
+      module now support reading the filter data on a per-user basis, and
+      support reading the filter data from SQL databases via a SQLNamedQuery.
+      See the doc/contrib/mod_geoip.html#GeoIPAllowFilter documentation for
+      details and examples.
 
-      Thus to explicitly allow usage for these insecure algorithms, use:
+    Include
+      The Include directive now supported wildcarded directory names, e.g.:
 
-        SFTPOptions AllowInsecureLogin
+        Include /path/to/sites/*/proftpd.conf
 
-      See doc/contrib/mod_sftp.html#SFTPOptions for details.
+      Previously wildcards were only allowed as part of the filename.  For
+      more details, see doc/modules/mod_core.html#Include.
 
-    SQLPasswordPBKDF2 sql://
-      The mod_sql_passwd module now supports retrieval of PBKDF2 parameters,
-      such as algorithm, iteration count, and output length, on a per-user
-      basis, via a SQLNamedQuery, in addition to staticly configured
-      parameters.
+    ListOptions
+      The mod_ls module returns file names for the NLST command in the order
+      in which the names are provided by the underlying filesystem via the
+      readdir(2) function.  For many filesystems, the names are not ordered.
+      However, some legacy FTP applications may assume/expect that the NLST
+      names are orded.  For such cases, use the new SortedNLST ListOption; see
+      doc/modules/mod_ls.html#ListOptions.
 
-      See doc/contrib/mod_sql_passwd.html#SQLPasswordPBKDF2 for details.
+    MasqueradeAddress
+      The MasqueradeAddress directive is now allowed in <Global> sections,
+      to ease configuration for name-based virtual hosts.
 
+    SFTPKeyExchanges
+      The mod_sftp module now supports the "curve25519-sha256 at libssh.org"
+      SSH key exchange algorithm, when compiled with the libsodium library.
 
-1.3.5rc4
----------
+      In addition, the following key exchange algorithms are supported:
 
-  + Fixed mod_sftp/mod_sftp_pam memory allocation (CVE-2013-4359).
-  + Added support for SSCN FTP command, for secure site-to-site (FXP) transfers.
-  + Added support for Elliptic Curve (EC) suites to mod_tls.
-  + Fixed handling of LDFLAGS settings for module builds
-  + Changed log levels on many log messages, in order to have more consistently
-    applied log levels when logging.
+        diffie-hellman-group14-sha256
+        diffie-hellman-group16-sha512
+        diffie-hellman-group18-sha512
 
-  + New Modules
+    SFTPOptions
+      There are several new SFTPOptions supported:
 
-    mod_dnsbl
-      The mod_dnsbl module can be used to provide connection access control
-      using DNS "blacklists", i.e. denying connections from clients which
-      are known "bad" addresses/networks.  See doc/contrib/mod_dnsbl.html
-      for more information.
+        IgnoreFIFOs
+        IgnoreSFTPSetExtendedAttributes
+        IgnoreSFTPUploadExtendedAttributes
 
+      The doc/contrib/mod_sftp.html#SFTPOptions documentation covers these
+      new options in greater detail.
 
-  + New Configuration Directives
 
-    LDAPLog
-      The mod_ldap module now supports the LDAPLog directive, for directing
-      all of the mod_ldap logging to a file.  More details can be found at
-      doc/contrib/mod_ldap.html#LDAPLog.
+  + API Changes
 
-    TLSECCertificateFile, TLSECCertificateKeyFile
-      The mod_tls module now supports Elliptic Curve (EC) cryptography.
-      These ciphers and algorithms require an EC certificate/key, which
-      are configured with these new directives.  See the
-      doc/contrib/mod_tls.html#TLSECCertificateFile and
-      doc/contrib/mod_tls.html#TLSECCertificateKeyFile for more information.
+    Many of the core API have been const-ified.  This will cause build
+    warnings in many existing third-party modules.
 
-    TLSVerifyServer
-      The mod_tls module now supports secure FXP (site-to-site) transfers
-      via the SSCN FTP command.  Since mod_tls now acts as a client for
-      part of these transfers, there is a new TLSVerifyServer directive
-      that controls how mod_tls will verify the certificate of the remote
-      server.  See doc/contrib/mod_tls.html#TLSVerifyServer for details.
 
+1.3.6rc2
+---------
 
-  + Changed Configuration Directives
+  + SSH RSA hostkeys smaller than 2048 bits now work properly.
+  + MLSD response lines are now properly CRLF terminated.
+  + Faster/more efficient FTP ASCII mode conversion code.
+  + Support for OCSP stapling.
+  + Support for the common CLNT FTP command.
+  + Fixed selection of DH groups from TLSDHParamFile.
 
-    CapabilitiesSet
-      Some sites use directories with the SGID bit set, and to preserve the
-      SGID bit on directories created in these directories, on Linux systems,
-      the mod_cap module either needs to be disabled, OR it needs to support
-      the CAP_FSETID capability.
+  + New Modules:
 
-      The CapabilitiesSet directive now handles the CAP_FSETID capability,
-      which is not enabled by default.  To enable its use, you would use the
-      following in your proftpd.conf:
+    mod_auth_otp
+      The mod_auth_otp module supports one-time passwords using the HOTP and
+      TOTP algorithms.  This OTP support is compatible with e.g. Google
+      Authenticator and other implementations.  The mod_auth_otp module can
+      thus be used to provide multi-factor authentication (MFA, 2FA) for
+      both FTP and SFTP logins.  For more information, see the module
+      documentation at doc/contrib/mod_auth_otp.html.
 
-        <IfModule mod_cap.c>
-          CapabilitiesSet +CAP_FSETID
-        </IfModule>
+    mod_digest
+      The mod_digest module implements the HASH FTP command, per the IETF
+      Draft.  This module also implements previous FTP commands that achieve
+      similar functionality, e.g. XCRC, XMD5, XSHA1, and others.  See the
+      full module documentation at doc/contrib/mod_digest.html.
 
-      See doc/contrib/mod_cap.html#CapabilitiesSet for more information.
+    mod_statcache
+      Some sites often act as "mirrors", and/or have clients which perform
+      numerous, repeated, and expensive directory listings.  These directory
+      listings cause repetitive (and often redundant) lookups of the per-file
+      details via the stat(2) and lstat(2) system calls.  The mod_statcache
+      module caches this "stat" information, such that the cached data can
+      be shared/reused across multiple FTP sessions.  See
+      doc/contrib/mod_statcache.html for details.
 
-    ExecOnEvent
-      Executed commands can now be executed with the privileges of the
-      logged-in user using the '~' notation in the ExecOnEvent directive;
-      see doc/contrib/mod_exec.html#ExecOnEvent for details.
 
-      The ExecOnEvent directive is now supported in the <VirtualHost>
-      and <Global> contexts, as well as the "server config" default context.
+  + New Configuration Directives:
 
-    LogFormat, SQLLog
-      The %s LogFormat variable is now properly resolved for SFTP transfers.
+    AllowEmptyPasswords
+      By default, proftpd allows for empty passwords to be used for
+      authentication.  Some sites may wish to enforce the requirement that
+      passwords not be empty; this can now be accomplished via the
+      AllowEmptyPasswords directive, e.g.:
 
-      The %D/%d LogFormat variables are now properly resolved for directory
-      listings, for both FTP and SFTP sessions.
+        # Require non-empty passwords
+        AllowEmptyPasswords on
 
-      There is a new LogFormat variable, %{basename}, for logging the last
-      component of a path, <i>i.e.</i> just the file/directory name.
+      See doc/modules/mod_auth.html#AllowEmptyPasswords for details.
+      Note that this applies to mod_sftp password-based logins as well.
 
-    PersistentPasswd 
-      The PersistentPasswd directive now defaults to 'off'.  See
-      doc/modules/mod_auth_unix.html#PersistentPasswd for more information.
+    AnonAllowRobots
+      Some web crawlers, notably Google, will automatically index any
+      FTP sites with anonymous logins.  This behavior is now automatically
+      prohibited, using auto-generated robots.txt files for <Anonymous>
+      logins.  Use the new AnonAllowRobots directive to allow indexing again;
+      see doc/modules/mod_auth.html#AnonAllowRobots for details.
 
-    SFTPOptions
-      To prevent users from changing the timestamp of their files
-      uploaded via SCP, the SFTPOptions directive
-      (doc/contrib/mod_sftp.html#SFTPOptions) now supports an
-      IgnoreSCPUploadTimes parameter.
+    SQLODBCVersion
+      The mod_sql_odbc module defaults to using ODBC API version 3.  However,
+      some ODBC drivers may themselves expect older versions of the ODBC
+      API.  This can now be configured using e.g. "SQLODBCVersion ODBCv2";
+      see doc/contrib/mod_sql_odbc.html#SQLODBCVersion for more information.
 
-    SQLOptions
-      In some environments (e.g. those with very strict policies such as
-      SELinux, or perhaps memory-constrained environments), mod_sql needs
-      to be told to not try to read any database-specific configuration
-      files.  For this, the SQLOptions directive now supports the
-      IgnoreConfigFile parameter; see doc/contrib/mod_sql.html#SQLOptions.
+    TLSSessionTickets
+      The mod_tls module now supports TLS session tickets, per RFC 5077.
+      See doc/contrib/mod_tls.html#TLSSessionTicketKeys for more details.
 
+    TLSStapling
+      The mod_tls module now supports OCSP stapling by caching OCSP responses
+      for server certificates, and stapling those OCSP responses in the
+      SSL/TLS handshake data.  This OCSP stapling functionality is configuring
+      using new directives:
 
-  + Changed Utilities
+        TLSStaplingCache
+        TLSStaplingOptions
+        TLSStaplingResponder
+        TLSStaplingTimeout
 
-    ftpasswd
-      The ftpasswd utility has two new command-line options, --lock and
-      --unlock, for "administratively" locking/unlocking specific accounts,
-      akin to the functionality offered by passwd(1).
+      See the doc/contrib/mod_tls.html documentation for more information.
 
-      The ftpasswd utility also now checks for concurrent modifications:
-      only one process can be modifying the target files at a time.
 
+  + Changed Modules
 
-  + New Documentation
+    The mod_sftp module now supports the LINK_COUNT attribute in SFTP STAT
+    requests, for clients using SFTP protocol version 6.  It also supports the
+    "hardlink at openssh.com" SFTP extension.
 
-    We went through ProFTPD source code with an eye toward logged messages,
-    wanting to make sure that similar types of messages were consistently
-    logged at the same level.  With this, we can now provide documentation
-    which describes the messages logged at each log level, along with
-    a catalog of the most common log messages and what they mean.  See
-    doc/howto/LogLevels.html and doc/howto/LogMessages.html, respsectively.
+    The mod_tls_memcache and mod_tls_shmcache modules now provide OCSP
+    responder caches.
 
-    A new howto for the mod_radius module, including some example FreeRADIUS
-    configurations, has been added; see doc/howto/Radius.html.
 
- 
-1.3.5rc3
----------
+  + Changed Configuration Directives:
 
-  + Fixed mod_sql "SQLAuthType Backend" MySQL issues
-  + HideUser/HideGroup now work as expected for virtual users
+    BanCacheOptions
+      When storing ban rules in memcached, the mod_ban module can now
+      format the rules as JSON, allowing for better interoperability with
+      other applications interested in those ban rules.  To use JSON,
+      use "BanCacheOptions UseJSON"; see
+      doc/contrib/mod_ban.html#BanCacheOptions for more information.
 
-  + New Modules
+    BanOnEvent
+      The mod_ban module can now be configured to ban clients which attempt
+      to use empty passwords, when the new AllowEmptyPasswords directive is
+      set to "false", via the new EmptyPassword BanOnEvent rule.  See
+      doc/contrib/mod_ban.html#BanOnEvent for more information.
 
-    mod_snmp
-      The mod_snmp module is intended to collect various state information
-      and expose them via SNMP counters and gauges.  Currently only
-      SNMPv1/SNMPv2 are supported.  See doc/contrib/mod_snmp.html for more
+    LogFormat
+      The mod_log module supports several new LogFormat variables.  The
+      %R variable can be used for logging command response time, in
+      milliseconds, which is useful for measuring response time/latency.
+      Data transfer durations are separately tracked using the new
+      %{transfer-millisecs} variable.  For logging the size of the file
+      after data transfer (e.g. after upload), use the %{file-size} variable.
+      And for logging whether the file transfer used binary or ASCII mode,
+      use the %{transfer-type} variable.
+
+    MasqueradeAddress
+      Sometimes the DNS name configured for MasqueradeAddress directives
+      cannot be resolved when ProFTPD starts up, such as when the network
+      interface on the host machine has not yet come up.  This would cause
+      ProFTPD to fail to start.
+
+      The MasqueradeAddress directive now handles these resolution errors,
+      and will attempt to resolve the DNS name later, when clients connect;
+      this allows ProFTPD to start up properly in such cases.
+
+    SFTPAuthMethods
+      The mod_sftp module now supports requiring multiple authentication
+      methods for a single login.  Some sites, for example, wish for clients
+      to authenticate via publickey AND password.  This is supported via
+      the SFTPAuthMethods directive, e.g.:
+
+        # Require both publickey and password authentication, in any order
+        SFTPAuthMethods publickey+password password+publickey
+
+      The doc/contrib/mod_sftp.html#SFTPAuthMethods documentation has more
       information.
 
+    SFTPExtensions
+      The mod_sftp module now supports the "hardlink at openssh.com" SFTP
+      extension.  To disable support for this extension, use:
 
-  + New Configuration Directives
+        SFTPExtensions -hardlink
 
-    SQLUserPrimaryKey, SQLGroupPrimaryKey
-      The mod_sql module now has directives for specifying primary key
-      columns for user/group data; these can be used for storing user/group
-      values in tables which require foreign key constraints.  See
-      doc/howto/SQL.html#SQLPrimaryKeys for a more detailed description
-      and use cases for these directives.
+      See doc/contrib/mod_sftp.html#SFTPExtensions for details.
 
-    SQLPasswordPBKDF2
-      The mod_sql_passwd module now supports handling passwords encrypted
-      using the PBKDF2 algorithm.  See
-      doc/contrib/mod_sql_passwd.html#SQLPasswordPBKDF2 for more information.
+    SQLConnectInfo, SQLNamedConnectInfo
+      The mod_sql module can now support using SSL/TLS for connections to
+      backend databases, if supported by the database-specific module,
+      by configuring the necessary SSL/TLS details using the SQLConnectInfo
+      and SQLNamedConnectInfo directives.  See their respective documentation,
+      doc/contrib/mod_sql.html#SQLConnectInfo and
+      doc/contrib/mod_sql.html#SQLNamedConnectInfo for more information.
 
+    TLSOptions
+      The NoCertRequest TLSOption is now deprecated.  The mod_tls module,
+      rather than requesting client certificates every time, will only do
+      so if "TLSVerifyClient on" or "TLSVerifyClient optional" is configured.
+      This brings the mod_tls behavior more in line with Apache behavior,
+      and slightly reduces the SSL/TLS handshake latency.
 
-  + Changed Configuration Directives
+    TLSProtocol
+      The mod_tls module now supports excluding of particular SSL/TLS
+      versions via the TLSProtocol directive, e.g.:
 
-    DeleteAbortedStores
-      To preserve the principle of least surprise, the behavior of the
-      DeleteAbortedStores directive has been changed slightly.  Specifically,
-      DeleteAbortedStores is automatically enabled now whenever
-      "HiddenStores on" is configured.
+        # Support all TLS versions except SSLv3
+        TLSProtocol ALL -SSLv3
 
-    LogFormat, SQLLog
-      The LogFormat and SQLLog directives now supports a %g variable, for
-      logging the name of the primary group of the logged-in user.
-      See doc/modules/mod_log.html#LogFormat.
+      The doc/contrib/mod_tls.html#TLSProtocol documentation has more
+      details.
 
-    SFTPDigests
-      The mod_sftp module now supports UMAC as an SSH digest algorithm,
-      using the digest name as used by OpenSSH, i.e. "umac-64 at openssh".
-      Support for this digest is automatically enabled where supported.
-      See doc/contrib/mod_sftp.html#SFTPDigests for details.
+    TLSSessionCache
+      When storing SSL session data in memcached, the mod_tls module can now
+      format the session data using JSON, allowing for better interoperability
+      with other applications interested in that session data.  To use JSON,
+      use "TLSSessionCache memcache:/json".  See
+      doc/contrib/mod_tls_memcache.html for more information.
 
-    SFTPExtensions fsync
-      The mod_sftp module now supports the custom "fsync at openssh"
-      SFTP extension, for handling fsync requests from SFTP clients that
-      need to ensure that any buffered uploaded data has been flushed out
-      to the backing store on the server.  See
-      doc/contrib/mod_sftp.html#SFTPExtensions for details.    
+    UseEncoding
+      Different users/clients may wish to use different filename encodings
+      for their files.  The UseEncoding directive can now be set
+      per-user/group/class, by now working with mod_ifsession sections as
+      expected.
 
 
-1.3.5rc2
+1.3.6rc1
 ---------
 
-  + New Modules
-
-    mod_log_forensic
-      The mod_log_forensic module is intended to collect logging information
-      while a session runs, but only to write that log information out to
-      a file when certain conditions happen, and thus to avoid logging
-      redundant information for "normal" sessions.  See
-      doc/contrib/mod_log_forensic.html for more information.
-
-    mod_rlimit
-      The handling of the RLimitCPU, RLimitMemory, and RLimitOpenFiles
-      directives were refactored into a new mod_rlimit module.  This module
-      also automatically sets the RLIMIT_NPROC resource limit for session
-      processes (as a protective measure).  See doc/modules/mod_rlimit.html
-      for details.
-
-
-  + New Configuration Directives
-
-    GeoIPPolicy
-      The GeoIPPolicy directive for the mod_geoip module is used to configure
-      the default allow/deny policy of the module, when evaluating any
-      configured GeoIPFilters.  See doc/contrib/mod_geoip.html#GeoIPPolicy.
-
-    TLSMasqueradeAddress
-      There are particular situations where MasqueradeAddress functionality
-      is needed, but *only for FTPS connections*.  For these situations,
-      use the new TLSMasqueradeAddress directive; see
-      doc/contrib/mod_tls.html#TLSMasqueradeAddress for details.
-
-    TLSUserName
-      The mod_tls module can now be configured to authenticate users based
-      on contents of a client-provided certificate (assuming the client
-      provided one).  By using:
-
-       TLSVerifyClient on
-
-      and the new TLSUserName directive, mod_tls may verify the user
-      without requiring a password.  See doc/contrib/mod_tls.html#TLSUserName.
-
-
-  + Changed Configuration Directives
+  + Support the HOST command (see RFC 7151).
+  + Changed the default SyslogLevel to be NOTICE, rather than DEBUG.
+  + Fixed "stalled" SSL/TLS handshakes for data transfers.
+  + ftpwho now supports JSON output format.
+  + Fixed handling of SSH keys with overlong Comment headers in mod_sftp_sql.
+  + Changed handling of logging of SFTP sessions to ExtendedLogs; see the
+    notes below on the ExtendedLog directive.
 
-    AuthUserFile, AuthGroupFile
-      The mod_auth_file module is now more strict about the permissions
-      on configured AuthUserFile and AuthGroupFile, and about the permissions
-      of the directories containing these files.  These restrictions are to
-      prevent accidental (or malicious) altering of these files by any
-      user on the system.  See
-      doc/modules/mod_auth_file.html#AuthFilePermissions for a discussion
-      of these new restrictions.
+  + Changed Modules
 
-    BanOnEvent TLSHandshake
-      There is a particular type of Denial-of-Service attack where malicious
-      clients request just enough of an SSL/TLS handshake to cause the server
-      to perform the expensive decryption operations; if this happens
-      frequently enough, the server will consume much of its CPU, thus
-      starving other clients.  See:
+    mod_facl
+      The mod_facl module now supports the MacOSX flavour of POSIX ACLs.
 
-        http://vincent.bernat.im/en/blog/2011-ssl-dos-mitigation.html
-        http://www.thc.org/thc-ssl-dos/
+    mod_radius
+      The mod_radius module has added support for the following RADIUS
+      attributes:
 
-      The BanOnEvent directive now supports a TLSHandshake rule, to impose
-      bans on such clients.
+        Acct-Terminate-Cause
+        Event-Timestamp
+        Idle-Timeout
+        Message-Authenticator
+        Reply-Message
+        Session-Timeout
 
-    HiddenStores
-      The default "." suffix used for HiddenStores filenames can now be
-      customised.  See doc/modules/mod_xfer.html#HiddenStores.
+    mod_site_misc
+      The SITE UTIME command now supports the 3-timestamp variant:
 
-    LogFormat
-      The LogFormat directive now supports variables for timestamps with
-      millisecond and microsecond granularity, via %{millisecs} and
-      %{microsecs}, respectively.  In addition, a new shorthand variable,
-      %{iso8601}, is supported.  See doc/modules/mod_log.html#LogFormat.
+        SITE UTIME path atime mtime ctime
 
-      Important note: the new %{iso8601} timestamp format is *also* now the
-      timestamp format used for the default system logging that proftpd uses.
-
-    TLSProtocol
-      Almost all of the mod_tls configuration directives can be configured
-      on a per-<VirtualHost> basis, except for the TLSProtocol directive.
-      This has now been corrected, such that TLSProtocol can be set on
-      a per-<VirtualHost> basis (and is also allowed in <Global> sections).
-
-    TLSSessionCache
-      The default SSL session cache timeout, when no explicit timeout has
-      been configured, has been increased to be slightly longer than the
-      default control channel renegotiation timeout.  This means that the
-      out-of-the-box mod_tls behavior will be better for FTPS clients that
-      have long-lived (i.e. on the order of multiple hours) sessions.
+      where each timestamp is expressed as "YYYYMMDDhhmmss".
 
+    mod_sql
+      The mod_sql module would previously only support 32-bit UID/GID
+      values, due to its use of the atoi(3) C library function for parsing
+      result set values into IDs.  This has been fixed; mod_sql now
+      properly supports 64-bit UID/GID values.
 
-  + Logging Format changes
-
-    Note that the logging format for SystemLog, TraceLog, and all per-module
-    log files has changed.  Specifically, the log lines now have an ISO-8601
-    timestamp format, so that they now look like:
+    mod_tls
+      By default, mod_tls will no longer support SSLv3 connections.  In
+      order to support SSLv3 connections (for sites that need to), you must
+      explicitly configure this via the TLSProtocol directive, e.g.:
 
-      2013-01-31 15:33:03,832
+        TLSProtocol SSLv3 TLSv1 ...
 
-    instead of:
+      In addition, mod_tls will no longer support EXPORT ciphers.
 
-      Jan 31 15:33:03
 
-    The new format does not use textual month abbreviations, and includes
-    milliseconds in the timestamp, for finer-grained logging granularity.
+  + New Configuration Directives:
 
-    NOTE: This WILL have impact on sites using DenyHosts, banhosts, fail2ban,
-    depending on the configured log recognition patterns used by those
-    applications.
+    CopyEngine
+      The mod_copy module is enabled by default; there may be cases where
+      the module should be disabled, without requiring a rebuild of the
+      server.  Thus mod_copy now supports a CopyEngine directive; see
+      doc/contrib/mod_copy.html#CopyEngine.
 
+    DelayOnEvent
+      There are sites which wish to use mod_delay for administratively
+      adding delays to connections as e.g. brute force attack deterrents.
+      To support these use cases, the mod_delay module has a new DelayOnEvent
+      directive.  Using this directive, sites can configure something like
+      the following, for forcing a minimum login delay and a failed login
+      delay:
 
-1.3.5rc1
----------
+        <IfModule mod_delay.c>
+          # Configure successful logins to be delayed by 2 secs
+          DelayOnEvent PASS 2000ms
 
-  + Added support for SHA-256, SHA-512 password hashes to the ftpasswd tool
-  + Many mod_sftp bugfixes
-  + Added Japanese and Spanish translations
+          # Configure failed logins to be delayed by 5 secs
+          DelayOnEvent FailedLogin 5s
+        </IfModule>
 
-  + New Modules
+      See doc/modules/mod_delay.html#DelayOnEvent for more information.
+
+    ExecEnable
+      Some sites using mod_exec need to configure a command to be executed,
+      but wish to "blacklist" certain directories where that command should
+      not be executed.  To handle configurations like this, the mod_exec
+      module has a new ExecEnable directive; see
+      doc/contrib/mod_exec.html#ExecEnable for details.
+
+    FSCachePolicy
+      ProFTPD has long maintained a cache of the last stat data for a file.
+      However, for performance reasons, this cache size needs to be larger,
+      and to be enhanced to handle expiration, etc.  To tune the size
+      and expiration settings of this filesystem data cache, use the new
+      FSCachePolicy directive; see doc/modules/mod_core.html#FSCachePolicy
+      for details.
 
-    mod_geoip
-      The mod_geoip module provides lookup of geographic information based
-      on the IP address of the connecting client.  This information can
-      be logged, and used to drop the connection if necessary.  See
-      doc/contrib/mod_geoip.html for complete information.
+    LangOptions
+      Currently proftpd tries to cope with various filename/character
+      encodings used by FTP clients; if it cannot decode the filename, it
+      will use the sent filename as-is.  This behavior can cause problems
+      for downstream resources that then attempt to deal with these
+      filenames, and fail.  To make proftpd be more strict about the
+      encoding it accepts, use the new LangOption directive:
+
+        LangOptions RequireValidEncoding
+
+      See doc/modules/mod_lang.html#LangOptions.
+
+    RadiusOptions
+      The mod_radius module now supports/handles additional RADIUS
+      attributes.  Some of these attribute may need to be ignore for
+      some sites; others may wish to e.g. enforce stronger security by
+      requiring the use of the Message-Authenticator attribute.  Thus
+      mod_radius has a new RadiusOptions directive; see
+      doc/contrib/mod_radius.html#RadiusOptions for details.
+
+    ServerAlias
+      Supporting true name-based virtual hosting means needing to associate
+      names with the IP-based virtual hosts.  The ServerAlias directive is
+      used to do this association; see doc/modules/mod_core.html#ServerAlias
+      for details.
 
+    SQLPasswordSaltEncoding
+      In order to handle binary data for salts, the mod_sql_password module
+      now supports handling of this data as base64- or hex-encoded data
+      via the new SQLPasswordSaltEncoding directive; see
+      doc/contrib/mod_sql_passwd.html#SQLPasswordSaltEncoding for details.
+
+    TLSECDHCurve
+      When an FTPS client uses an ECDHE cipher, mod_tls currently will use
+      the X9.62 <code>prime256v1</code> curve.  Some sites may, however,
+      wish to use other curves for ECDHE ciphers.  These sites may now use
+      the new TLSECDHCurve directive to configure the curve; see
+      doc/contrib/mod_tls.html#TLSECDHCurve for details.
+
+    TLSNextProtocol
+      Newer TLS clients use the ALPN (Application Layer Protocol Negotiation)
+      extension (or its earlier incarnation NPN (Next Protocol Negotiation)
+      for determining the protocol that will be used over the SSL/TLS
+      session.  The support for these extensions can be used by clients for
+      enabling other behaviors/optimizations, such as TLS False Start.
+      This directive can be used to disable mod_tls' use of the ALPN/NPN
+      extensions as needed; see doc/contrib/mod_tls.html#TLSNextProtocol
+      for more details.
+
+    TLSPreSharedKey
+      Some sites may find that using pre-shared keys (PSK) is preferable
+      for their TLS needs.  TLS clients in embedded or low power environments
+      may find PSK to be less computationally expensive.  The mod_tls module
+      now supports pre-shared keys via its TLSPreSharedKey directive; see
+      doc/contrib/mod_tls.html#TLSPreSharedKey for details.
+
+    TransferOptions
+      There are some broken (<i>e.g.</i> old/mainframe) FTP clients that
+      will upload files, containing CRLF sequences, as ASCII data, but
+      these clients expect these CRLF sequences to be left as-is by the
+      FTP server.  To handle these broken clients, there is a new
+      TransferOptions directive; see doc/modules/mod_xfer.html#TransferOptions
+      for more information.
 
-  + Changed Modules
 
-    mod_sftp
-      The mod_sftp module now supports use of Elliptic Curve Cryptography
-      (ECC).  This includes ECDSA host keys and user keys, and the ECDH
-      key exchange method.
+  + Changed Configuration Directives
 
-      Improved FIPS support in mod_sftp.
+    BanOnEvent BadProtocol
+      Some clients (malicious or unintentional) may send HTTP or SMTP commands
+      to ProFTPD.  ProFTPD now detects these "bad protocol" messages, and
+      mod_ban can now ban clients that repeatedly do this via its BanOnEvent
+      rules.  See doc/contrib/mod_ban.html#BanOnEvent for details.
 
-      The mod_sftp module now honors the MaxStoreFileSize directive.
+    DelayTable none
+      If the mod_delay module is used to enforce minimum delays, and not
+      use its DelayTable for "learning" the best delay, then the DelayTable
+      is not needed/used to all.  The DelayTable directive can now be used
+      to tell mod_delay to not even open/lock on that table, using:
 
+        DelayTable none
 
-  + New Configuration Directives:
+      See doc/modules/mod_delay.html#DelayTable.
 
-    AllowChrootSymlinks
-      By default, when chrooting a process (via the DefaultRoot directive
-      or for <Anonymous> logins), proftpd will follow a symlink.  Some sites
-      wish to restrict this; for these sites, there is the new
-      AllowChrootSymlinks directive.  See
-      doc/modules/mod_auth.html#AllowChrootSymlinks for details.
-
-    CapabilitiesRootRevoke
-      The mod_cap module now completely drops root privileges; the retained
-      Linux capabilities should suffice.  To revert to the previous behavior,
-      use this new CapabilitiesRootRevoke directive; see
-      doc/modules/mod_cap.html#CapabilitiesRootRevoke.
-
-    <IfAuthenticated>
-      There are times when a site might wish to have directives that apply
-      <b>only</b> to sessions once the user has been authenticated, and not
-      before then.  The mod_ifsession module now provides this ability via
-      the <IfAuthenticated> section; see
-      doc/contrib/mod_ifsession.html#IfAuthenticated.
+    DeleteAbortedStores
+      The DeleteAbortedStores directive, for Bug#3917, was meant to be
+      enabled, but only when HiddenStores was in effect.  Unfortunately,
+      the fix caused a regression, as DeleteAbortedStores was enabled for
+      ALL transfers inadvertently.  The desired behavior, of enabling
+      DeleteAbortedStores only when HiddenStores is in effect, has been
+      properly implemented now.
 
-    FactsOptions
-      The FactsOptions directive is used to tweak the outputs used by
-      the mod_facts module for the MLSD/MLST output.  One such option is
-      UseSlink, which provides better compatibility with the output expected
-      e.g. by FileZilla.  The doc/modules/mod_facts.html#FactsOptions
-      description has more details.
-
-    QuotaDefault
-      The QuotaDefault directive is used to provide a "default" quota,
-      via the mod_quotatab module, if mod_quotatab is unable to find a
-      quota for the logging-in user.  See
-      doc/contrib/mod_quotatab.html#QuotaDefault for details.
-
-    RewriteMaxReplace
-      The RewriteMaxReplace directive is used to change the number of
-      replacements that mod_rewrite will do when rewriting commands.  See
-      doc/contrib/mod_rewrite.html#RewriteMaxReplace for more details.
-
-    TLSServerCipherPreference
-      This directive can be used to configure mod_tls so that the server
-      ciphersuites are preferred, rather than preferring the client
-      ciphersuites.  See doc/contrib/mod_tls.html#TLSServerCipherPreference
-      for more information.
+    ExtendedLog
+      When an ExtendedLog is used for logging mod_sftp requests, the log
+      file will contain both the SFTP requests AND the internal FTP commands
+      to which mod_sftp will map the SFTP requests; this can lead to some
+      VERY verbose log files.
 
+      For greater control over SFTP logging, the ExtendedLog directive now
+      supports two new log classes: SSH, and SFTP.  In addition, it supports
+      the '!' prefix, for excluding certain log classes from a given
+      ExtendedLog.  For example:
 
-  + Changed Configuration Directives
+        LogFormat ftp "..." ALL,EXIT,!SSH,!SFTP
+        ExtendedLog /path/to/extended-ftp.log ftp
 
-    AllowFilter, DenyFilter
-      The "[NC]" (or "[nocase]") case-insensitive flags for the PathAllowFilter
-      and PathDenyFilter directives have now been extended to apply to the
-      AllowFilter and DenyFilter directives as well.  For example:
+        LogFormat sftp "..." SSH,SFTP
+        ExtendedLog /path/to/extended-sftp.log sftp
 
-        AllowFilter \.html [NC]
-        DenyFilter \.jpg$ [nocase]
+      NOTE that existing ExtendedLogs that expect to see the SFTP requests will
+      no longer do so; adding the "SFTP" logging class to such ExtendedLogs is
+      now necessary.
 
-    CreateHome
-      When creating a home directory via the CreateHome directive, proftpd
-      will automatically use root privileges, in order to create the
-      necessary directories with configured ownership and permissions.
-      However, for some sites which use e.g. NFS, use of root privileges
-      will cause problems.  For such situations, the CreateHome directive
-      now supports a NoRootPrivs parameter; see doc/howto/CreateHome.html
-      for examples.
+    HiddenStores
+      Some sites may experience HiddenStores filename collisions when
+      uploading FTP clients get disconnected, and/or use multiple concurrent
+      sessions for uploading.  To help avoid collisions, the HiddenStores
+      directive now supports the %P variable, for adding the session PID
+      to the generated HiddenStore name.
+
+      In other cases, some sites may have FTP clients that want to use the
+      APPE FTP command, but cannot do so when HiddenStores is in effect.
+      These FTP clients are often outside of the site's control, and not
+      easily changed.  ProFTPD now automatically disables the HiddenStores
+      functionality when an APPE FTP command is used; the APPE command
+      can only function on an existing file anyway, and thus there is no
+      loss of functionality with this policy change.
 
     ListOptions
-      The mod_ls module now supports the -1 option (list one file per line),
-      which can also be configured via the ListOptions directive.
+      When ProFTPD generates a directory listing for FTP commands such as
+      LIST or NLST, it will automatically sort the names lexicographically.
+      For very wide directories, this can cause more memory/CPU usage, in
+      order to sort all of these names.  This sorting can now be disabled
+      via ListOptions, using:
 
-      The ListOptions directive now supports new "LISTOnly" and "NLSTOnly"
-      keywords, for applying the restrictions only to the LIST (or NLST)
-      FTP commands.
+        ListOptions -U
 
-    LogFormat
-      The LogFormat directive can now handle logging variables of the format
-      %{note:<name>}, where <name> refers to an internal "note" name.  The
-      supported names vary depending on the various modules.  In most cases,
-      these note names are not useful for general purpose logging, however.
-
-    RewriteCondition, RewriteRule
-      The RewriteCondition and RewriteRule directives of the mod_rewrite
-      module now support time-related variables; see
-      doc/contrib/mod_rewrite.html#RewriteCondition for more information.
-
-    RootRevoke
-      The RootRevoke directive now supports a "UseNonCompliantActiveTransfers"
-      parameter, for dropping root privileges BUT still allowing active
-      data transfers by using a non-standard source port for the active
-      transfer.  See doc/modules/mod_auth.html#RootRevoke for more information.
-
-    ScoreboardFile
-      Some sites have found that maintaining the ScoreboardFile can lead to
-      a fair amount of overhead.  Those sites who wish to remove this overhead
-      can now disable use of the ScoreboardFile altogether by using:
-
-        ScoreboardFile off
-
-      Note that doing so means that certain configuration directives (e.g.
-      MaxClients) and certain tools (e.g. ftptop, ftpwho, ftpcount) will no
-      longer work.  See doc/modules/mod_core.html#ScoreboardFile for details.
-
-    ServerIdent
-      The ServerIdent directive is now honored by the mod_sftp module.
-      For example, you can configure mod_sftp to not provide any version
-      information at all, or to provide a different server name/token
-      altogether; see doc/contrib/mod_sftp.html#SFTPVersionId.
+      See doc/modules/mod_ls.html#ListOptions for more information.
 
     SFTPDigests
-      The SFTPDigests directive (see doc/contrib/mod_sftp.html#SFTPDigests)
-      now handles/supports the SHA-256 and SHA-512 digest algorithms.
+      The mod_sftp module now supports the umac-128 at openssh.com MAC
+      algorithm; see doc/contrib/mod_sftp.html#SFTPDigests.
 
     SFTPHostKey
-      The mod_sftp module can now load its host keys from an SSH agent
-      process such as OpenSSH ssh-agent(1) tool; see
-      doc/contrib/mod_sftp.html#SFTPHostKey for details.
+      Large hosting sites often use a <Global> section for centralizing
+      configuration of a large number of <VirtualHost> sections in their
+      proftpd.conf.  Sometimes, such sites will want to disable use of
+      particular SSH hostkeys for a given <VirtualHost>.  To support this
+      use case, the SFTPHostKey directive can be used to ignore any
+      globally-configured RSA, DSA, or ECDSA SSH hostkey, e.g.:
+
+        SFTPHostKey NoRSA
+        SFTPHostKey NoDSA
+
+      See doc/contrib/mod_sftp.html#SFTPHostKey for details.
 
     SFTPOptions
-      To prevent users from changing the ownership of their files via SFTP,
-      the SFTPOptions directive (doc/contrib/mod_sftp.html#SFTPOptions) now
-      supports an IgnoreSFTPSetOwners parameter.
-
-    SocketOptions
-      The SocketOptions directive (see doc/modules/mod_core.html#SocketOptions)
-      now supports a new "keepalive" parameter, for tuning the TCP keepalive
-      behavior.  A more detailed explanation of TCP keepalives, and other
-      forms of keepalive, is provided in doc/howto/KeepAlives.html.
-
-    SQLLog, LogFormat
-      The LogFormat and SQLLog directives support two new logging variables:
-      %{transfer-status} and %{transfer-failure}.  The %{transfer-status}
-      variable indicates the status of a data transfer: "success", "failed",
-      "cancelled", "timeout", or "-" (if not applicable).  The
-      %{transfer-failure} variable indicates the reason for the data transfer
-      failure, or "-" if there was no failure.
-
-    SystemLog
-      The SystemLog can now be disabled via <IfClass>, e.g. for disabling
-      logging for specific clients.
-
-    TLSCipherSuite
-      The default SSL/TLS ciphersuites offered/used by mod_tls have changed
-      from "ALL:!ADH" to a more secure ordering of "DEFAULT:!ADH:!EXPORT:!DES".
+      Some OS distributions insist that their SSH hostkeys be group-readable.
+      In order to allow mod_sftp, which has more strict SSH hostkey permission
+      policies, to use those SSH hostkeys, the SFTPOptions directive can
+      now be used to relax this permission policy as needed:
 
-    TLSProtocol
-      When compiled using OpenSSL 1.0.1 or later, the TLSProtocol directive
-      can be used to specify TLS versionf "TLSv1.1" and "TLSv1.2".
+        SFTPOptions InsecureHostKeyPerms
 
-    UseEncoding
-      When the UseEncoding directive (doc/modules/mod_lang.html#UseEncoding)
-      is used to specify encodings, it used to require that the client use
-      these encodings, and not allow clients to request different encodings.
-      This requirement is now related.  To preserve the old behavior, a
-      new "strict" keyword is supported; the directive documentation covers
-      this in more details.
+      See doc/contrib/mod_sftp.html#SFTPOptions for details.
 
-    <VirtualHost>, MasqueradeAddress, DefaultAddress
-      All of these directives can now take an interface name, in addition
-      to DNS names or IP addresses, to be resolved to an IP address.  This
-      is useful in cases where the same proftpd.conf needs to be deployed or
-      shared across a cluster; each machine has the same interface name which
-      handles different IP addresses.
+      Small Diffie-Hellman groups are subject to cryptographic weaknesses;
+      see https://weakdh.org.  Thus mod_sftp now avoids the use of weak
+      DH groups by default; the AllowWeakDH SFTPOption is used to re-enable
+      such support for clients that require it.
+      See doc/contrib/mod_sftp.html#SFTPOptions.
 
-      Examples:
+    SNMPAgent
+      Some sites may wish to have mod_snmp listening on multiple
+      addresses (e.g. on multi-homed servers), including IPv6 addresses.
+      The SNMPAgent directive now supports this, e.g.:
 
-        DefaultAddress lo0
-        MasqueradeAddress eth0
+        <IfModule mod_snmp.c>
+          SNMPAgent master 127.0.0.1:1161 10.0.1.2:1161 [::]
+        </IfModule>
+
+      See doc/contrib/mod_snmp.html#SNMPAgent for more information.
 
-        <VirtualHost eth0>
-          ...
-        </VirtualHost>
+    TLSOptions
+      Small Diffie-Hellman groups are subject to cryptographic weaknesses;
+      see https://weakdh.org.  Thus mod_tls now avoids the use of weak
+      DH groups by default; the AllowWeakDH TLSOption is used to re-enable
+      such support for clients that require it.
+      See doc/contrib/mod_tls.html#TLSOptions.
 
 
   + Changed Command Handling
 
-    PORT/EPRT
-      When handling PORT and EPRT FTP commands from clients, proftpd now
-      checks for RFC 1918 addresses in those commands; these are non-publicly
-      routable IP addresses, and should NOT be being sent by clients.  When
-      proftpd detects these RFC1918 addresses being used for a WAN server
-      address, then proftpd will ignore the RFC1918 address, and instead
-      use the IP address of the connecting client.  This change should help
-      interoperability of data transfers for some FTP clients.
+    When handling the STAT FTP command, ProFTPD now follows RFC 959 more
+    closely, and will use the 213 response code for STAT commands on files,
+    and the 212 response code for STAT commands on directories.  Previously,
+    ProFTPD would respond to all STAT commands using the 211 response code.
 
 
-  + API Changes
+  + Changed Utilities
 
-    The session.class struct member has been renamed to session.conn_class.
-    This was done to allow for support of C++ modules.  Similarly, the
-    cmd_rec.class struct member has been renamed to cmd_rec.cmd_class,
-    and cmdtable.class renamed to cmdtable.cmd_class.  See Bug#3079 for
-    details.
+    The ftpwho command-line utility can now emit its data as JSON,
+    for easier parsing/reuse in other utilities.  To request JSON, use:
 
-Last Updated: $Date: 2014-05-09 14:52:12 $
+      $ ftpwho -o json
diff --git a/acconfig.h b/acconfig.h
index b543ec7..5dc6ba7 100644
--- a/acconfig.h
+++ b/acconfig.h
@@ -60,9 +60,6 @@
 /* Define if you have the <syslog.h> header file. */
 #undef HAVE_SYSLOG_H
 
-/* Define if you already have a typedef for timer_t */
-#undef HAVE_TIMER_T
-
 /* Define if your struct utmp has ut_host */
 #undef HAVE_UT_UT_HOST
 
diff --git a/config.h.in b/config.h.in
index 017ac00..f2f3b62 100644
--- a/config.h.in
+++ b/config.h.in
@@ -52,7 +52,10 @@
 /* Define if you have Linux sendfile() semantics. */
 #undef HAVE_LINUX_SENDFILE
 
-/* Define if you have Mac OSX sendfile() semantics.  */
+/* Define if you have MacOSX POSIX ACLs. */
+#undef HAVE_MACOSX_POSIX_ACL
+
+/* Define if you have MacOSX sendfile() semantics.  */
 #undef HAVE_MACOSX_SENDFILE
 
 /* Define if you have Solaris POSIX ACLs. */
@@ -118,9 +121,6 @@
 /* Define if you have the <syslog.h> header file. */
 #undef HAVE_SYSLOG_H
 
-/* Define if you already have a typedef for timer_t */
-#undef HAVE_TIMER_T
-
 /* Define if you have the tzname global variable.  */
 #undef HAVE_TZNAME
 
@@ -252,6 +252,15 @@
 /* The number of bytes in a pointer to a void.  */
 #undef SIZEOF_VOID_P
 
+/* The number of bytes in a uid_t.  */
+#undef SIZEOF_UID_T
+
+/* The number of bytes in a gid_t.  */
+#undef SIZEOF_GID_T
+
+/* Define if you have the authenticate function.  */
+#undef HAVE_AUTHENTICATE
+
 /* Define if you have the backtrace function.  */
 #undef HAVE_BACKTRACE
 
@@ -270,6 +279,18 @@
 /* Define if you have the endprotoent function.  */
 #undef HAVE_ENDPROTOENT
 
+/* Define if you have the extattr_delete_link function.  */
+#undef HAVE_EXTATTR_DELETE_LINK
+
+/* Define if you have the extattr_delete_link function.  */
+#undef HAVE_EXTATTR_GET_LINK
+
+/* Define if you have the extattr_delete_link function.  */
+#undef HAVE_EXTATTR_LIST_LINK
+
+/* Define if you have the extattr_delete_link function.  */
+#undef HAVE_EXTATTR_SET_LINK
+
 /* Define if you have the fconvert function.  */
 #undef HAVE_FCONVERT
 
@@ -297,6 +318,9 @@
 /* Define if you have the freeaddrinfo function.  */
 #undef HAVE_FREEADDRINFO
 
+/* Define if you have the fsync function.  */
+#undef HAVE_FSYNC
+
 /* Define if you have the futimes function.  */
 #undef HAVE_FUTIMES
 
@@ -315,6 +339,9 @@
 /* Define if you have the getgrouplist function.  */
 #undef HAVE_GETGROUPLIST
 
+/* Define if you have the getgroups function.  */
+#undef HAVE_GETGROUPS
+
 /* Define if you have the getgrset function.  */
 #undef HAVE_GETGRSET
 
@@ -369,15 +396,39 @@
 /* Define if you have the inet_pton function.  */
 #undef HAVE_INET_PTON
 
+/* Define if you have the initgroups function.  */
+#undef HAVE_INITGROUPS
+
+/* Define if you have the lgetxattr function.  */
+#undef HAVE_LGETXATTR
+
+/* Define if you have the llistxattr function.  */
+#undef HAVE_LLISTXATTR
+
+/* Define if you have the loginfailed function.  */
+#undef HAVE_LOGINFAILED
+
 /* Define if you have the loginrestrictions function.  */
 #undef HAVE_LOGINRESTRICTIONS
 
+/* Define if you have the loginsuccess function.  */
+#undef HAVE_LOGINSUCCESS
+
+/* Define if you have the lremovexattr function.  */
+#undef HAVE_LREMOVEXATTR
+
+/* Define if you have the lsetxattr function.  */
+#undef HAVE_LSETXATTR
+
 /* Define if you have the memcpy function.  */
 #undef HAVE_MEMCPY
 
 /* Define if you have the mempcpy function.  */
 #undef HAVE_MEMPCPY
 
+/* Define if you have the memset_s function.  */
+#undef HAVE_MEMSET_S
+
 /* Define if you have the mkdir function.  */
 #undef HAVE_MKDIR
 
@@ -399,6 +450,9 @@
 /* Define if you have the munlockall function.  */
 #undef HAVE_MUNLOCKALL
 
+/* Define if you have the mysql_get_option function.  */
+#undef HAVE_MYSQL_GET_OPTION
+
 /* Define if you have the MySQL make_scrambled_password function.  */
 #undef HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD
 
@@ -411,18 +465,39 @@
 /* Define if you have the MySQL my_make_scrambled_password_323 function.  */
 #undef HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD_323
 
+/* Define if you have the MySQL mysql_get_ssl_cipher function.  */
+#undef HAVE_MYSQL_MYSQL_GET_SSL_CIPHER
+
+/* Define if you have the MySQL mysql_ssl_set function.  */
+#undef HAVE_MYSQL_MYSQL_SSL_SET
+
 /* Define if you have the nl_langinfo function.  */
 #undef HAVE_NL_LANGINFO
 
 /* Define if you have the pathconf function.  */
 #undef HAVE_PATHCONF
 
+/* Define if you have the PCRE pcre_free_study function.  */
+#undef HAVE_PCRE_PCRE_FREE_STUDY
+
 /* Define if you have the perm_copy_fd function.  */
 #undef HAVE_PERM_COPY_FD
 
+/* Define if you have the posix_fadvise function.  */
+#undef HAVE_POSIX_FADVISE
+
 /* Define if you have the Postgres PQescapeStringConn function.  */
 #undef HAVE_POSTGRES_PQESCAPESTRINGCONN
 
+/* Define if you have the Postgres PQgetssl function.  */
+#undef HAVE_POSTGRES_PQGETSSL
+
+/* Define if you have the Postgres PQinitOpenSSL function.  */
+#undef HAVE_POSTGRES_PQINITOPENSSL
+
+/* Define if you have the prctl function.  */
+#undef HAVE_PRCTL
+
 /* Define if you have the pstat function.  */
 #undef HAVE_PSTAT
 
@@ -513,6 +588,9 @@
 /* Define if you have the strtol function.  */
 #undef HAVE_STRTOL
 
+/* Define if you have the strtoll function.  */
+#undef HAVE_STRTOLL
+
 /* Define if you have the strtoull function.  */
 #undef HAVE_STRTOULL
 
@@ -534,6 +612,9 @@
 /* Define if you have the <arpa/inet.h> header file.  */
 #undef HAVE_ARPA_INET_H
 
+/* Define if you have the <attr/xattr.h> header file.  */
+#undef HAVE_ATTR_XATTR_H
+
 /* Define if you have the <bstring.h> header file.  */
 #undef HAVE_BSTRING_H
 
@@ -597,6 +678,9 @@
 /* Define if you have the <linux/capability.h> header file.  */
 #undef HAVE_LINUX_CAPABILITY_H
 
+/* Define if you have the <linux/prctl.h> header file.  */
+#undef HAVE_LINUX_PRCTL_H
+
 /* Define if you have the <locale.h> header file.  */
 #undef HAVE_LOCALE_H
 
@@ -663,6 +747,9 @@
 /* Define if you have the <signal.h> header file.  */
 #undef HAVE_SIGNAL_H
 
+/* Define if you have the <sodium.h> header file.  */
+#undef HAVE_SODIUM_H
+
 /* Define if you have the <string.h> header file.  */
 #undef HAVE_STRING_H
 
@@ -675,9 +762,15 @@
 /* Define if you have the <sys/acl.h> header file.  */
 #undef HAVE_SYS_ACL_H
 
+/* Define if you have the <sys/audit.h> header file.  */
+#undef HAVE_SYS_AUDIT_H
+
 /* Define if you have the <sys/dir.h> header file.  */
 #undef HAVE_SYS_DIR_H
 
+/* Define if you have the <sys/extattr.h> header file.  */
+#undef HAVE_SYS_EXTATTR_H
+
 /* Define if you have the <sys/file.h> header file.  */
 #undef HAVE_SYS_FILE_H
 
@@ -750,6 +843,9 @@
 /* Define if you have <sys/wait.h> that is POSIX.1 compatible.  */
 #undef HAVE_SYS_WAIT_H
 
+/* Define if you have the <sys/xattr.h> header file.  */
+#undef HAVE_SYS_XATTR_H
+
 /* Define if you have the <termios.h> header file.  */
 #undef HAVE_TERMIOS_H
 
@@ -936,9 +1032,12 @@
 /* Define if largefile support is desired.  */
 #undef PR_USE_LARGEFILES
 
-/* Define if memcache support is desired.. */
+/* Define if Memcache support is desired.. */
 #undef PR_USE_MEMCACHE
 
+/* Define if Redis support is desired.. */
+#undef PR_USE_REDIS
+
 /* Define if the %llu format should be used.  */
 #undef HAVE_LLU
 
@@ -960,9 +1059,17 @@
 /* Define if using nonblocking open of log files.  */
 #undef PR_USE_NONBLOCKING_LOG_OPEN
 
+/* Define if Sodiumm support, if available, should be used.  */
+#undef PR_USE_SODIUM
+
 /* Define if OpenSSL support, if available, should be used.  */
 #undef PR_USE_OPENSSL
 
+/* Define if OpenSSL Application Layer Protocol Negotiation (ALPN) support,
+ * if available, should be used.
+ */
+#undef PR_USE_OPENSSL_ALPN
+
 /* Define if OpenSSL Elliptic Curve Cryptography (ECC) support, if available,
  * should be used.
  */
@@ -973,6 +1080,14 @@
  */
 #undef PR_USE_OPENSSL_FIPS
 
+/* Define if OpenSSL Next Protocol Negotiation (NPN) support, if available,
+ * should be used.
+ */
+#undef PR_USE_OPENSSL_NPN
+
+/* Define if OpenSSL OCSP support, if available, should be used.  */
+#undef PR_USE_OPENSSL_OCSP
+
 /* Define if using PCRE support.  */
 #undef PR_USE_PCRE
 
@@ -994,6 +1109,9 @@
 /* Define if using trace support.  */
 #undef PR_USE_TRACE
 
+/* Define if using xattr support.  */
+#undef PR_USE_XATTR
+
 /* Tunable parameters */
 #undef PR_TUNABLE_BUFFER_SIZE
 #undef PR_TUNABLE_NEW_POOL_SIZE
diff --git a/configure b/configure
index 0b4c591..ff849a2 100755
--- a/configure
+++ b/configure
@@ -1516,7 +1516,7 @@ Optional Features:
   --enable-autoshadow     enable run-time auto-detection of shadowed passwords
                           (requires shadow)
 
-  --enable-auth-pam       enable PAM support (default=yes)
+  --enable-auth-pam       enable PAM support (default=auto)
 
   --enable-builtin-getaddrinfo
                           force use of builtin getaddrinfo (default=no)
@@ -1548,6 +1548,8 @@ Optional Features:
 
   --enable-pcre           enable use of PCRE for POSIX regular expressions
                           rather than the system library (default=no)
+  --enable-redis          enable support for Redis (default=no)
+
   --enable-force-setpassent
                           force use of setpassent (default=no on FreeBSD)
 
@@ -1555,6 +1557,8 @@ Optional Features:
 
   --enable-openssl        enable OpenSSL support (default=no)
 
+  --enable-sodium         enable Sodium support (default=auto)
+
   --disable-sendfile      disable sendfile support (default=no)
 
   --enable-shadow         force compilation of shadowed password support
@@ -1565,6 +1569,8 @@ Optional Features:
 
   --disable-trace         disable trace support (default=no)
 
+  --disable-xattr         disable extended attribute support (default=auto)
+
   --enable-devel          enable developer-only code (default=no)
 
   --enable-buffer-size    tune the the size (in bytes) of internal buffers
@@ -1581,7 +1587,7 @@ Optional Features:
                           set how often (in loops) the mod_xfer module updates
                           the scoreboard (default=10)
 
-  --disable-strip         do not strip debugging symbols from installed code
+  --enable-strip          strip debugging symbols from installed code
                           (default=no)
 
   --enable-timeout-ident  set the default timeout (in secs) for RFC931
@@ -1604,6 +1610,10 @@ Optional Features:
                           set the default timeout (in secs) for stalled
                           transfers (default=3600)
 
+  --enable-parser-buffer-size
+                          tune the the size (in bytes) of parser buffers
+                          (default=4096 bytes)
+
   --enable-transfer-buffer-size
                           tune the the size (in bytes) of data transfer
                           buffers (default=OS dependent)
@@ -3885,13 +3895,13 @@ if test "${lt_cv_nm_interface+set}" = set; then
 else
   lt_cv_nm_interface="BSD nm"
   echo "int some_variable = 0;" > conftest.$ac_ext
-  (eval echo "\"\$as_me:3888: $ac_compile\"" >&5)
+  (eval echo "\"\$as_me:3898: $ac_compile\"" >&5)
   (eval "$ac_compile" 2>conftest.err)
   cat conftest.err >&5
-  (eval echo "\"\$as_me:3891: $NM \\\"conftest.$ac_objext\\\"\"" >&5)
+  (eval echo "\"\$as_me:3901: $NM \\\"conftest.$ac_objext\\\"\"" >&5)
   (eval "$NM \"conftest.$ac_objext\"" 2>conftest.err > conftest.out)
   cat conftest.err >&5
-  (eval echo "\"\$as_me:3894: output\"" >&5)
+  (eval echo "\"\$as_me:3904: output\"" >&5)
   cat conftest.out >&5
   if $GREP 'External.*some_variable' conftest.out > /dev/null; then
     lt_cv_nm_interface="MS dumpbin"
@@ -5113,7 +5123,7 @@ ia64-*-hpux*)
   ;;
 *-*-irix6*)
   # Find out which ABI we are using.
-  echo '#line 5116 "configure"' > conftest.$ac_ext
+  echo '#line 5126 "configure"' > conftest.$ac_ext
   if { (eval echo "$as_me:$LINENO: \"$ac_compile\"") >&5
   (eval $ac_compile) 2>&5
   ac_status=$?
@@ -6963,11 +6973,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:6966: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:6976: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>conftest.err)
    ac_status=$?
    cat conftest.err >&5
-   echo "$as_me:6970: \$? = $ac_status" >&5
+   echo "$as_me:6980: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s "$ac_outfile"; then
      # The compiler can only warn and ignore the option if not recognized
      # So say no if there are warnings other than the usual output.
@@ -7302,11 +7312,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:7305: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:7315: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>conftest.err)
    ac_status=$?
    cat conftest.err >&5
-   echo "$as_me:7309: \$? = $ac_status" >&5
+   echo "$as_me:7319: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s "$ac_outfile"; then
      # The compiler can only warn and ignore the option if not recognized
      # So say no if there are warnings other than the usual output.
@@ -7407,11 +7417,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:7410: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:7420: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>out/conftest.err)
    ac_status=$?
    cat out/conftest.err >&5
-   echo "$as_me:7414: \$? = $ac_status" >&5
+   echo "$as_me:7424: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s out/conftest2.$ac_objext
    then
      # The compiler can only warn and ignore the option if not recognized
@@ -7462,11 +7472,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:7465: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:7475: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>out/conftest.err)
    ac_status=$?
    cat out/conftest.err >&5
-   echo "$as_me:7469: \$? = $ac_status" >&5
+   echo "$as_me:7479: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s out/conftest2.$ac_objext
    then
      # The compiler can only warn and ignore the option if not recognized
@@ -10231,7 +10241,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 10234 "configure"
+#line 10244 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -10327,7 +10337,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 10330 "configure"
+#line 10340 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -14000,11 +14010,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:14003: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:14013: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>conftest.err)
    ac_status=$?
    cat conftest.err >&5
-   echo "$as_me:14007: \$? = $ac_status" >&5
+   echo "$as_me:14017: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s "$ac_outfile"; then
      # The compiler can only warn and ignore the option if not recognized
      # So say no if there are warnings other than the usual output.
@@ -14099,11 +14109,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:14102: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:14112: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>out/conftest.err)
    ac_status=$?
    cat out/conftest.err >&5
-   echo "$as_me:14106: \$? = $ac_status" >&5
+   echo "$as_me:14116: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s out/conftest2.$ac_objext
    then
      # The compiler can only warn and ignore the option if not recognized
@@ -14151,11 +14161,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:14154: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:14164: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>out/conftest.err)
    ac_status=$?
    cat out/conftest.err >&5
-   echo "$as_me:14158: \$? = $ac_status" >&5
+   echo "$as_me:14168: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s out/conftest2.$ac_objext
    then
      # The compiler can only warn and ignore the option if not recognized
@@ -15598,6 +15608,10 @@ fi
 
 LDFLAGS="-L\$(top_srcdir)/lib $LDFLAGS"
 
+if test $ac_cv_c_compiler_gnu = yes; then
+    LDFLAGS="$LDFLAGS -rdynamic"
+fi
+
 # Record the current CPPFLAGS, LDFLAGS, and LIBS here
 ac_orig_cppflags="$CPPFLAGS"
 ac_orig_ldflags="$LDFLAGS"
@@ -15707,167 +15721,24 @@ fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 
-  CFLAGS="$fullCFLAGS"
-fi
-
-if test x$exec_prefix = xNONE ; then
-  exec_prefix=$prefix
-fi
-
-if test x$prefix = xNONE ; then
-  prefix=/usr/local
-  bindir=/usr/local/bin
-  datadir=/usr/local/share
-  libexecdir=/usr/local/libexec
-  sbindir=/usr/local/sbin
-fi
-
-BINDIR=`eval echo $bindir`
-
-DATADIR=`eval echo $datadir`
-
-INCLUDEDIR=`eval echo $includedir`
-
-LIBEXECDIR=`eval echo $libexecdir`
-
-LOCALSTATEDIR=`eval echo $localstatedir`
-
-PREFIX=`eval echo $prefix`
-
-SBINDIR=`eval echo $sbindir`
-
-SYSCONFDIR=`eval echo $sysconfdir`
-
-
-LIB_DEPS="\"\""
-
-
-
-
-
-LIB_OBJS="pr_fnmatch.o sstrncpy.o strsep.o vsnprintf.o glibc-glob.o glibc-hstrerror.o glibc-mkstemp.o pr-syslog.o pwgrent.o tpl.o"
-
-
-# Check whether --with-getopt was given.
-if test "${with_getopt+set}" = set; then
-  withval=$with_getopt;
-    if test "$withval" != "no" ; then
-
-for ac_func in getopt
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
+    { echo "$as_me:$LINENO: checking whether the C compiler accepts -fno-omit-frame-pointer" >&5
+echo $ECHO_N "checking whether the C compiler accepts -fno-omit-frame-pointer... $ECHO_C" >&6; }
+  CFLAGS="-fno-omit-frame-pointer"
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
-
-#undef $ac_func
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
 
 int
 main ()
 {
-return $ac_func ();
+
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-	eval "$as_ac_var=no"
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
-_ACEOF
-
-for ac_header in getopt.h
-do
-as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  { echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-else
-  # Is the header compilable?
-{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
-echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-$ac_includes_default
-#include <$ac_header>
-_ACEOF
 rm -f conftest.$ac_objext
 if { (ac_try="$ac_compile"
 case "(($ac_try" in
@@ -15885,105 +15756,295 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_header_compiler=yes
+  { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }; fullCFLAGS="$fullCFLAGS $CFLAGS"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_header_compiler=no
+	{ echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
-echo "${ECHO_T}$ac_header_compiler" >&6; }
 
-# Is the header present?
-{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
-echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <$ac_header>
-_ACEOF
-if { (ac_try="$ac_cpp conftest.$ac_ext"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } >/dev/null && {
-	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       }; then
-  ac_header_preproc=yes
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
+  CFLAGS="-g2 $fullCFLAGS"
+fi
 
-  ac_header_preproc=no
+if test x$exec_prefix = xNONE ; then
+  exec_prefix=$prefix
 fi
 
-rm -f conftest.err conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
-echo "${ECHO_T}$ac_header_preproc" >&6; }
+if test x$prefix = xNONE ; then
+  prefix=/usr/local
+  bindir=/usr/local/bin
+  datadir=/usr/local/share
+  libexecdir=/usr/local/libexec
+  sbindir=/usr/local/sbin
+fi
 
-# So?  What about this header?
-case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
-  yes:no: )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
-echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
-    ac_header_preproc=yes
-    ;;
-  no:yes:* )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
-echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
-echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
-echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
-echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
-echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+BINDIR=`eval echo $bindir`
 
-    ;;
-esac
-{ echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  eval "$as_ac_Header=\$ac_header_preproc"
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
+DATADIR=`eval echo $datadir`
 
-fi
-if test `eval echo '${'$as_ac_Header'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
-_ACEOF
+INCLUDEDIR=`eval echo $includedir`
 
-fi
+LIBEXECDIR=`eval echo $libexecdir`
 
-done
+LOCALSTATEDIR=`eval echo $localstatedir`
 
+PREFIX=`eval echo $prefix`
 
-for ac_func in getopt_long
+SBINDIR=`eval echo $sbindir`
+
+SYSCONFDIR=`eval echo $sysconfdir`
+
+
+LIB_DEPS="\"\""
+
+
+
+
+
+LIB_OBJS="pr_fnmatch.o sstrncpy.o strsep.o vsnprintf.o glibc-glob.o glibc-hstrerror.o glibc-mkstemp.o pr-syslog.o pwgrent.o hanson-tpl.o ccan-json.o"
+
+
+# Check whether --with-getopt was given.
+if test "${with_getopt+set}" = set; then
+  withval=$with_getopt;
+    if test "$withval" != "no" ; then
+
+for ac_func in getopt
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $ac_func innocuous_$ac_func
+
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
+
+#undef $ac_func
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $ac_func ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$ac_func || defined __stub___$ac_func
+choke me
+#endif
+
+int
+main ()
+{
+return $ac_func ();
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  eval "$as_ac_var=yes"
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	eval "$as_ac_var=no"
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+fi
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+_ACEOF
+
+for ac_header in getopt.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <$ac_header>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_header_compiler=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <$ac_header>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+
+done
+
+
+for ac_func in getopt_long
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -16842,6 +16903,12 @@ echo "$as_me: error: use --enable-nls instead of --with-modules=mod_lang for NLS
 echo "$as_me: error: use --enable-memcache instead of --with-modules=mod_memcache for Memcache support" >&2;}
    { (exit 1); exit 1; }; }
           fi
+
+          if test x"$amodule" = xmod_redis ; then
+            { { echo "$as_me:$LINENO: error: use --enable-redis instead of --with-modules=mod_redis for Redis support" >&5
+echo "$as_me: error: use --enable-redis instead of --with-modules=mod_redis for Redis support" >&2;}
+   { (exit 1); exit 1; }; }
+          fi
         done
 
         # Trim off any leading/trailing colons, and collapse double-colons
@@ -16873,7 +16940,15 @@ if test x"$enable_memcache" = xyes; then
   ac_build_static_modules="$ac_build_static_modules modules/mod_memcache.o"
 fi
 
-ac_unshareable_modules="mod_auth mod_rlimit mod_auth_unix mod_core mod_dso mod_ls mod_xfer mod_log mod_site mod_cap mod_ctrls"
+if test x"$enable_redis" = xyes; then
+  # Yes, we DO want mod_redis AFTER the other modules in the static
+  # module list. Otherwise, the module load ordering will be such that
+  # Redis support will not work as expected.
+  ac_static_modules="$ac_static_modules mod_redis.o"
+  ac_build_static_modules="$ac_build_static_modules modules/mod_redis.o"
+fi
+
+ac_unshareable_modules="mod_auth mod_rlimit mod_auth_unix mod_core mod_dso mod_ls mod_xfer mod_log mod_site mod_cap mod_ctrls mod_memcache mod_redis"
 
 
 # Check whether --with-shared was given.
@@ -16890,10 +16965,11 @@ echo "$as_me: error: --with-shared parameter missing required colon-separated li
         # Trim off any leading/trailing colons, and collapse double-colons
         # into single colons; these are common typos.
         shared_modules=`echo "$withval" | sed 's/::/:/g'| sed 's/^://' | sed 's/:$//' | sed -e 's/:/ /g'`;
+        ac_shared_modules=`echo "$withval" | sed 's/::/:/g'| sed 's/^://' | sed 's/:$//' | sed -e 's/:/.la /g'`.la;
 
         # First double-check that the given list does not contain any
         # unshareable modules
-        for amodule in $shared_modules; do
+        for amodule in $pr_shared_modules; do
           for smodule in $ac_unshareable_modules; do
             if test x"$amodule" = x"$smodule"; then
               { { echo "$as_me:$LINENO: error: cannot build $amodule as a shared module" >&5
@@ -16903,10 +16979,23 @@ echo "$as_me: error: cannot build $amodule as a shared module" >&2;}
           done
         done
 
-        ac_shared_modules=`echo "$withval" | sed -e 's/:/.la /g'`.la ;
         for amodule in $ac_shared_modules; do
           ac_build_shared_modules="modules/$amodule $ac_build_shared_modules"
         done
+
+        { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Werror=implicit-function-declaration" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Werror=implicit-function-declaration... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Werror=implicit-function-declaration conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Werror=implicit-function-declaration"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
       fi
     fi
 
@@ -18056,7 +18145,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 18059 "configure"
+#line 18148 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -19674,6 +19763,107 @@ cat >>confdefs.h <<\_ACEOF
 _ACEOF
 
       ac_build_addl_libs="$ac_build_addl_libs -lpcreposix -lpcre"
+
+      # Check for other PCRE-specific functionality here
+      saved_ldflags="$LDFLAGS"
+      saved_libs="$LIBS"
+      saved_cppflags="$CPPFLAGS"
+
+      # fiddle with CPPFLAGS, LDFLAGS
+      CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+      LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+            LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+      LIBS="$LIBS -lpcre -lpcreposix"
+
+      { echo "$as_me:$LINENO: checking for PCRE's pcre_free_study" >&5
+echo $ECHO_N "checking for PCRE's pcre_free_study... $ECHO_C" >&6; }
+      cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+          # include <stddef.h>
+          #endif
+          #ifdef HAVE_STDLIB_H
+          # include <stdlib.h>
+          #endif
+          #ifdef HAVE_SYS_TYPES_H
+          # include <sys/types.h>
+          #endif
+          #include <pcre.h>
+
+int
+main ()
+{
+
+          pcre_extra *extra = NULL;
+          pcre_free_study(extra);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+          { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_PCRE_PCRE_FREE_STUDY 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+          { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+      # restore CPPFLAGS, LDFLAGS
+      CPPFLAGS="$saved_cppflags"
+      LDFLAGS="$saved_ldflags"
+      LIBS="$saved_libs"
+    fi
+
+fi
+
+
+# Check whether --enable-redis was given.
+if test "${enable_redis+set}" = set; then
+  enableval=$enable_redis;  if test x"$enableval" = xyes ; then
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_REDIS 1
+_ACEOF
+
     fi
 
 fi
@@ -19691,15 +19881,21 @@ if test "${enable_ipv6+set}" = set; then
 fi
 
 
+pr_use_openssl=""
 # Check whether --enable-openssl was given.
 if test "${enable_openssl+set}" = set; then
-  enableval=$enable_openssl;  if test x"$enableval" = xyes ; then
+  enableval=$enable_openssl;  if test x"$enableval" = xno ; then
+      pr_use_openssl="no"
+    fi
+
+fi
 
-cat >>confdefs.h <<\_ACEOF
-#define PR_USE_OPENSSL 1
-_ACEOF
 
-      ac_build_addl_libs="$ac_build_addl_libs -lssl -lcrypto"
+pr_use_sodium=""
+# Check whether --enable-sodium was given.
+if test "${enable_sodium+set}" = set; then
+  enableval=$enable_sodium;  if test x"$enableval" = xno ; then
+      pr_use_sodium="no"
     fi
 
 fi
@@ -20463,7 +20659,13 @@ if test "${enable_trace+set}" = set; then
 fi
 
 
-pr_devel_cflags="-g -O0 -Wcast-align -Wchar-subscripts -Winline -Wstrict-prototypes -Wmissing-declarations -Wnested-externs -Wpointer-arith -Wshadow -Wundef"
+# Check whether --enable-xattr was given.
+if test "${enable_xattr+set}" = set; then
+  enableval=$enable_xattr;
+fi
+
+
+pr_devel_cflags="-g3 -O0 -Wcast-align -Wchar-subscripts -Winline -Wstrict-prototypes -Wmissing-declarations -Wnested-externs -Wpointer-arith -Wshadow -Wundef"
 pr_devel_libs=""
 
 # Check whether --enable-devel was given.
@@ -20472,11 +20674,137 @@ if test "${enable_devel+set}" = set; then
     if test x"$enableval" != xno ; then
       devel="yes"
 
+      # Additional warnings but only for developer mode.  Note that
+      # -Wconversion is a bit noisy at the moment, thus why we
+      # selectively choose which warnings to enable.
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wdangling-else" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wdangling-else... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wdangling-else conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wdangling-else"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wextra" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wextra... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wextra conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wextra"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Werror=implicit-function-declaration" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Werror=implicit-function-declaration... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Werror=implicit-function-declaration conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Werror=implicit-function-declaration"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Winit-self" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Winit-self... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Winit-self conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Winit-self"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wno-missing-field-initializers" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wno-missing-field-initializers... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wno-missing-field-initializers conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wno-missing-field-initializers"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wno-unused-parameter" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wno-unused-parameter... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wno-unused-parameter conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wno-unused-parameter"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wnull-dereference" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wnull-dereference... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wnull-dereference conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wnull-dereference"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wstrict-prototypes" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wstrict-prototypes... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wstrict-prototypes conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wstrict-prototypes"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+      { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -fdelete-null-pointer-checks" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -fdelete-null-pointer-checks... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -fdelete-null-pointer-checks conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -fdelete-null-pointer-checks"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+
 
       if test `echo $enableval | grep -c coredump` = "1" ; then
         pr_devel_cflags="-DPR_DEVEL_COREDUMP $pr_devel_cflags"
       fi
 
+      if test `echo $enableval | grep -c coverage` = "1" ; then
+        pr_devel_cflags="-DPR_DEVEL_COVERAGE --coverage $pr_devel_cflags"
+        pr_devel_libs="--coverage $pr_devel_libs"
+      fi
+
       if test `echo $enableval | grep -c nodaemon` = "1" ; then
         pr_devel_cflags="-DPR_DEVEL_NO_DAEMON $pr_devel_cflags"
       fi
@@ -20490,232 +20818,15 @@ if test "${enable_devel+set}" = set; then
         pr_devel_libs="-pg $pr_devel_libs"
       fi
 
-      if test `echo $enableval | grep -c stacktrace` = "1"; then
-        pr_devel_cflags="-DPR_DEVEL_STACK_TRACE $pr_devel_cflags"
-
-        # On FreeBSD, the libexecinfo port is needed for the backtrace(3)
-        # function; we thus also need to check for the libexecinfo library
-
-{ echo "$as_me:$LINENO: checking for backtrace in -lexecinfo" >&5
-echo $ECHO_N "checking for backtrace in -lexecinfo... $ECHO_C" >&6; }
-if test "${ac_cv_lib_execinfo_backtrace+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-lexecinfo  $LIBS"
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char backtrace ();
-int
-main ()
-{
-return backtrace ();
-  ;
-  return 0;
-}
-_ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  ac_cv_lib_execinfo_backtrace=yes
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-	ac_cv_lib_execinfo_backtrace=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_lib_execinfo_backtrace" >&5
-echo "${ECHO_T}$ac_cv_lib_execinfo_backtrace" >&6; }
-if test $ac_cv_lib_execinfo_backtrace = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_LIBEXECINFO 1
-_ACEOF
-
-  LIBS="-lexecinfo $LIBS"
-
-fi
-
-
-        # Some libcs need the execinfo.h header for their backtrace symbols,
-        # and some (like Solaris) want ucontext.h.  Check for those headers
-        # here.
-
-
-for ac_header in execinfo.h ucontext.h
-do
-as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  { echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-else
-  # Is the header compilable?
-{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
-echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-$ac_includes_default
-#include <$ac_header>
-_ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_header_compiler=yes
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-	ac_header_compiler=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
-echo "${ECHO_T}$ac_header_compiler" >&6; }
-
-# Is the header present?
-{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
-echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <$ac_header>
-_ACEOF
-if { (ac_try="$ac_cpp conftest.$ac_ext"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } >/dev/null && {
-	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       }; then
-  ac_header_preproc=yes
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-  ac_header_preproc=no
-fi
-
-rm -f conftest.err conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
-echo "${ECHO_T}$ac_header_preproc" >&6; }
-
-# So?  What about this header?
-case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
-  yes:no: )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
-echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
-    ac_header_preproc=yes
-    ;;
-  no:yes:* )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
-echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
-echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
-echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
-echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
-echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
-
-    ;;
-esac
-{ echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  eval "$as_ac_Header=\$ac_header_preproc"
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-
-fi
-if test `eval echo '${'$as_ac_Header'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
-_ACEOF
-
-fi
+      if test `echo $enableval | grep -c sanitize` = "1" ; then
+        pr_devel_cflags="-fsanitize=address $pr_devel_cflags"
+        pr_devel_libs="-fsanitize=address $pr_devel_libs"
 
-done
-
-
-        # Make sure that we can find the backtrace(3) and backtrace_symbols(3)
-        # functions
-        { echo "$as_me:$LINENO: checking for backtrace" >&5
-echo $ECHO_N "checking for backtrace... $ECHO_C" >&6; }
+        # Determine whether we need to link with libasan (gcc) or not (clang)
+        { echo "$as_me:$LINENO: checking whether the C compiler accepts -lasan" >&5
+echo $ECHO_N "checking whether the C compiler accepts -lasan... $ECHO_C" >&6; }
+        saved_ldflags=$LDFLAGS
+        LDFLAGS="-lasan $LDFLAGS"
         cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -20723,22 +20834,13 @@ cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 
-            #include <stddef.h>
-            #include <stdlib.h>
-            #ifdef HAVE_EXECINFO_H
-            # include <execinfo.h>
-            #endif
-            #ifdef HAVE_UCONTEXT_H
-            # include <ucontext.h>
-            #endif
 
 int
 main ()
 {
 
-            void **syms = NULL;
-            int res, nsyms = 0;
-            res = backtrace(syms, nsyms);
+            int i;
+            i = 7;
 
   ;
   return 0;
@@ -20765,11 +20867,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 
             { echo "$as_me:$LINENO: result: yes" >&5
 echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_BACKTRACE 1
-_ACEOF
-
+            pr_devel_libs="-lasan $pr_devel_libs"
 
 else
   echo "$as_me: failed program was:" >&5
@@ -20784,81 +20882,10 @@ fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
-
-        { echo "$as_me:$LINENO: checking for backtrace_symbols" >&5
-echo $ECHO_N "checking for backtrace_symbols... $ECHO_C" >&6; }
-        cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-
-            #include <stddef.h>
-            #include <stdlib.h>
-            #ifdef HAVE_EXECINFO_H
-            # include <execinfo.h>
-            #endif
-            #ifdef HAVE_UCONTEXT_H
-            # include <ucontext.h>
-            #endif
-
-int
-main ()
-{
-
-            void **syms = NULL;
-            int nsyms = 0;
-            char **res;
-            res = backtrace_symbols(syms, nsyms);
-
-  ;
-  return 0;
-}
-_ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-
-            { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_BACKTRACE_SYMBOLS 1
-_ACEOF
-
-
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-
-            { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-
-
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-
+        LDFLAGS=$saved_ldflags
       fi
 
+
       if test `echo $enableval | grep -c timing` = "1"; then
         pr_devel_cflags="-DPR_DEVEL_TIMING $pr_devel_cflags"
       fi
@@ -20958,12 +20985,12 @@ _ACEOF
 fi
 
 
-keepsyms="no"
+keepsyms="yes"
 # Check whether --enable-strip was given.
 if test "${enable_strip+set}" = set; then
   enableval=$enable_strip;
-    if test x"$enableval" = xno ; then
-      keepsyms="yes"
+    if test x"$enableval" = xyes ; then
+      keepsyms="no"
     fi
 
 
@@ -21102,6 +21129,28 @@ _ACEOF
 fi
 
 
+# Check whether --enable-parser-buffer-size was given.
+if test "${enable_parser_buffer_size+set}" = set; then
+  enableval=$enable_parser_buffer_size;
+    if test "$enableval" = "yes" || test "$enableval" = "no" ; then
+      { echo "$as_me:$LINENO: WARNING: parser buffer size defaulting to regular buffer size" >&5
+echo "$as_me: WARNING: parser buffer size defaulting to regular buffer size" >&2;}
+
+cat >>confdefs.h <<_ACEOF
+#define PR_TUNABLE_PARSER_BUFFER_SIZE 4096
+_ACEOF
+
+    else
+
+cat >>confdefs.h <<_ACEOF
+#define PR_TUNABLE_PARSER_BUFFER_SIZE $enableval
+_ACEOF
+
+    fi
+
+fi
+
+
 # Check whether --enable-transfer-buffer-size was given.
 if test "${enable_transfer_buffer_size+set}" = set; then
   enableval=$enable_transfer_buffer_size;
@@ -21890,7 +21939,8 @@ fi
 
 
 
-for ac_header in krb.h login.h prot.h usersec.h
+
+for ac_header in krb.h login.h prot.h usersec.h sys/audit.h
 do
 as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
 if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
@@ -22399,6 +22449,196 @@ fi
 done
 
 
+{ echo "$as_me:$LINENO: checking for AIX authenticate" >&5
+echo $ECHO_N "checking for AIX authenticate... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+  #include <sys/types.h>
+  #ifdef HAVE_USERSEC_H
+  # include <usersec.h>
+  #endif
+
+int
+main ()
+{
+
+    (void) authenticate(NULL, NULL, NULL, NULL);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_AUTHENTICATE 1
+_ACEOF
+
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+
+
+{ echo "$as_me:$LINENO: checking for AIX loginfailed" >&5
+echo $ECHO_N "checking for AIX loginfailed... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+  #include <sys/types.h>
+  #ifdef HAVE_USERSEC_H
+  # include <usersec.h>
+  #endif
+
+int
+main ()
+{
+
+    (void) loginfailed(NULL, NULL, NULL, 0);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LOGINFAILED 1
+_ACEOF
+
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+
+{ echo "$as_me:$LINENO: checking for AIX loginsuccess" >&5
+echo $ECHO_N "checking for AIX loginsuccess... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+  #include <sys/types.h>
+  #ifdef HAVE_USERSEC_H
+  # include <usersec.h>
+  #endif
+
+int
+main ()
+{
+
+    (void) loginsuccess(NULL, NULL, NULL, NULL);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LOGINSUCCESS 1
+_ACEOF
+
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+
 
 if test x"$install_user" = x; then
   if test "x$target_os" = "xcygwin"; then
@@ -22908,7 +23148,8 @@ fi
 
 
 
-for ac_header in fcntl.h signal.h sys/ioctl.h sys/prctl.h sys/resource.h sys/time.h junistd.h memory.h
+
+for ac_header in fcntl.h signal.h linux/prctl.h sys/ioctl.h sys/prctl.h sys/resource.h sys/time.h junistd.h memory.h
 do
 as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
 if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
@@ -26817,56 +27058,6 @@ _ACEOF
 
 
 
-{ echo "$as_me:$LINENO: checking for timer_t" >&5
-echo $ECHO_N "checking for timer_t... $ECHO_C" >&6; }
-if test "${pr_cv_header_timer_t+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <sys/types.h>
-
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP ".*typedef.*timer_t;" >/dev/null 2>&1; then
-  pr_cv_header_timer_t="yes"
-else
-  cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <limits.h>
-
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP ".*typedef.*timer_t;" >/dev/null 2>&1; then
-  pr_cv_header_timer_t="yes"
-else
-  pr_cv_header_timer_t="no"
-fi
-rm -f conftest*
-
-fi
-rm -f conftest*
-
-fi
-{ echo "$as_me:$LINENO: result: $pr_cv_header_timer_t" >&5
-echo "${ECHO_T}$pr_cv_header_timer_t" >&6; }
-
-if test "$pr_cv_header_timer_t" = "yes"; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_TIMER_T 1
-_ACEOF
-
-fi
-
 { echo "$as_me:$LINENO: checking whether time.h and sys/time.h may both be included" >&5
 echo $ECHO_N "checking whether time.h and sys/time.h may both be included... $ECHO_C" >&6; }
 if test "${ac_cv_header_time+set}" = set; then
@@ -30635,10 +30826,9 @@ cat >>confdefs.h <<_ACEOF
 _ACEOF
 
 
-
-{ echo "$as_me:$LINENO: checking for mode_t" >&5
-echo $ECHO_N "checking for mode_t... $ECHO_C" >&6; }
-if test "${ac_cv_type_mode_t+set}" = set; then
+{ echo "$as_me:$LINENO: checking for uid_t" >&5
+echo $ECHO_N "checking for uid_t... $ECHO_C" >&6; }
+if test "${ac_cv_type_uid_t+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -30648,7 +30838,7 @@ cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 $ac_includes_default
-typedef mode_t ac__type_new_;
+typedef uid_t ac__type_new_;
 int
 main ()
 {
@@ -30677,48 +30867,172 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_type_mode_t=yes
+  ac_cv_type_uid_t=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_type_uid_t=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_type_uid_t" >&5
+echo "${ECHO_T}$ac_cv_type_uid_t" >&6; }
+
+# The cast to long int works around a bug in the HP C Compiler
+# version HP92453-01 B.11.11.23709.GP, which incorrectly rejects
+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.
+# This bug is HP SR number 8606223364.
+{ echo "$as_me:$LINENO: checking size of uid_t" >&5
+echo $ECHO_N "checking size of uid_t... $ECHO_C" >&6; }
+if test "${ac_cv_sizeof_uid_t+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  if test "$cross_compiling" = yes; then
+  # Depending upon the size, compute the lo and hi bounds.
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef uid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) >= 0)];
+test_array [0] = 0
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_lo=0 ac_mid=0
+  while :; do
+    cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef uid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) <= $ac_mid)];
+test_array [0] = 0
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_hi=$ac_mid; break
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_type_mode_t=no
+	ac_lo=`expr $ac_mid + 1`
+			if test $ac_lo -le $ac_mid; then
+			  ac_lo= ac_hi=
+			  break
+			fi
+			ac_mid=`expr 2 '*' $ac_mid + 1`
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_type_mode_t" >&5
-echo "${ECHO_T}$ac_cv_type_mode_t" >&6; }
-if test $ac_cv_type_mode_t = yes; then
-  :
+  done
 else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-cat >>confdefs.h <<_ACEOF
-#define mode_t mode_t
+	cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
 _ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef uid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) < 0)];
+test_array [0] = 0
 
-fi
-
-{ echo "$as_me:$LINENO: checking for ino_t" >&5
-echo $ECHO_N "checking for ino_t... $ECHO_C" >&6; }
-if test "${ac_cv_type_ino_t+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_hi=-1 ac_mid=-1
+  while :; do
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 $ac_includes_default
-typedef ino_t ac__type_new_;
+   typedef uid_t ac__type_sizeof_;
 int
 main ()
 {
-if ((ac__type_new_ *) 0)
-  return 0;
-if (sizeof (ac__type_new_))
-  return 0;
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) >= $ac_mid)];
+test_array [0] = 0
+
   ;
   return 0;
 }
@@ -30740,53 +31054,49 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_type_ino_t=yes
+  ac_lo=$ac_mid; break
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_type_ino_t=no
+	ac_hi=`expr '(' $ac_mid ')' - 1`
+			if test $ac_mid -le $ac_hi; then
+			  ac_lo= ac_hi=
+			  break
+			fi
+			ac_mid=`expr 2 '*' $ac_mid`
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_type_ino_t" >&5
-echo "${ECHO_T}$ac_cv_type_ino_t" >&6; }
-if test $ac_cv_type_ino_t = yes; then
-  :
+  done
 else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-cat >>confdefs.h <<_ACEOF
-#define ino_t ino_t
-_ACEOF
+	ac_lo= ac_hi=
+fi
 
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
 
-{ echo "$as_me:$LINENO: checking for intptr_t" >&5
-echo $ECHO_N "checking for intptr_t... $ECHO_C" >&6; }
-if test "${ac_cv_type_intptr_t+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+# Binary search between lo and hi bounds.
+while test "x$ac_lo" != "x$ac_hi"; do
+  ac_mid=`expr '(' $ac_hi - $ac_lo ')' / 2 + $ac_lo`
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-  #if HAVE_INTTYPES_H
-  # include <inttypes.h>
-  #endif
-
-
-typedef intptr_t ac__type_new_;
+$ac_includes_default
+   typedef uid_t ac__type_sizeof_;
 int
 main ()
 {
-if ((ac__type_new_ *) 0)
-  return 0;
-if (sizeof (ac__type_new_))
-  return 0;
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) <= $ac_mid)];
+test_array [0] = 0
+
   ;
   return 0;
 }
@@ -30808,30 +31118,122 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_type_intptr_t=yes
+  ac_hi=$ac_mid
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_type_intptr_t=no
+	ac_lo=`expr '(' $ac_mid ')' + 1`
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_type_intptr_t" >&5
-echo "${ECHO_T}$ac_cv_type_intptr_t" >&6; }
-if test $ac_cv_type_intptr_t = yes; then
+done
+case $ac_lo in
+?*) ac_cv_sizeof_uid_t=$ac_lo;;
+'') if test "$ac_cv_type_uid_t" = yes; then
+     { { echo "$as_me:$LINENO: error: cannot compute sizeof (uid_t)
+See \`config.log' for more details." >&5
+echo "$as_me: error: cannot compute sizeof (uid_t)
+See \`config.log' for more details." >&2;}
+   { (exit 77); exit 77; }; }
+   else
+     ac_cv_sizeof_uid_t=0
+   fi ;;
+esac
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef uid_t ac__type_sizeof_;
+static long int longval () { return (long int) (sizeof (ac__type_sizeof_)); }
+static unsigned long int ulongval () { return (long int) (sizeof (ac__type_sizeof_)); }
+#include <stdio.h>
+#include <stdlib.h>
+int
+main ()
+{
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_INTPTR_T 1
+  FILE *f = fopen ("conftest.val", "w");
+  if (! f)
+    return 1;
+  if (((long int) (sizeof (ac__type_sizeof_))) < 0)
+    {
+      long int i = longval ();
+      if (i != ((long int) (sizeof (ac__type_sizeof_))))
+	return 1;
+      fprintf (f, "%ld\n", i);
+    }
+  else
+    {
+      unsigned long int i = ulongval ();
+      if (i != ((long int) (sizeof (ac__type_sizeof_))))
+	return 1;
+      fprintf (f, "%lu\n", i);
+    }
+  return ferror (f) || fclose (f) != 0;
+
+  ;
+  return 0;
+}
 _ACEOF
+rm -f conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && { ac_try='./conftest$ac_exeext'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; }; then
+  ac_cv_sizeof_uid_t=`cat conftest.val`
+else
+  echo "$as_me: program exited with status $ac_status" >&5
+echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
+( exit $ac_status )
+if test "$ac_cv_type_uid_t" = yes; then
+     { { echo "$as_me:$LINENO: error: cannot compute sizeof (uid_t)
+See \`config.log' for more details." >&5
+echo "$as_me: error: cannot compute sizeof (uid_t)
+See \`config.log' for more details." >&2;}
+   { (exit 77); exit 77; }; }
+   else
+     ac_cv_sizeof_uid_t=0
+   fi
+fi
+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext conftest.$ac_objext conftest.$ac_ext
 fi
+rm -f conftest.val
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_sizeof_uid_t" >&5
+echo "${ECHO_T}$ac_cv_sizeof_uid_t" >&6; }
 
 
-{ echo "$as_me:$LINENO: checking for socklen_t" >&5
-echo $ECHO_N "checking for socklen_t... $ECHO_C" >&6; }
-if test "${ac_cv_type_socklen_t+set}" = set; then
+
+cat >>confdefs.h <<_ACEOF
+#define SIZEOF_UID_T $ac_cv_sizeof_uid_t
+_ACEOF
+
+
+{ echo "$as_me:$LINENO: checking for gid_t" >&5
+echo $ECHO_N "checking for gid_t... $ECHO_C" >&6; }
+if test "${ac_cv_type_gid_t+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -30840,17 +31242,8 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_NETDB_H
-    # include <netdb.h>
-    #endif
-    #include <sys/socket.h>
-
-
-typedef socklen_t ac__type_new_;
+$ac_includes_default
+typedef gid_t ac__type_new_;
 int
 main ()
 {
@@ -30879,50 +31272,30 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_type_socklen_t=yes
+  ac_cv_type_gid_t=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_type_socklen_t=no
+	ac_cv_type_gid_t=no
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_type_socklen_t" >&5
-echo "${ECHO_T}$ac_cv_type_socklen_t" >&6; }
-if test $ac_cv_type_socklen_t = yes; then
-
-cat >>confdefs.h <<_ACEOF
-#define HAVE_SOCKLEN_T 1
-_ACEOF
-
-
-else
-  cat >>confdefs.h <<\_ACEOF
-#define socklen_t int
-_ACEOF
-
-fi
-
+{ echo "$as_me:$LINENO: result: $ac_cv_type_gid_t" >&5
+echo "${ECHO_T}$ac_cv_type_gid_t" >&6; }
 
-
-for ac_header in utmp.h
-do
-as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  { echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+# The cast to long int works around a bug in the HP C Compiler
+# version HP92453-01 B.11.11.23709.GP, which incorrectly rejects
+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.
+# This bug is HP SR number 8606223364.
+{ echo "$as_me:$LINENO: checking size of gid_t" >&5
+echo $ECHO_N "checking size of gid_t... $ECHO_C" >&6; }
+if test "${ac_cv_sizeof_gid_t+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
 else
-  # Is the header compilable?
-{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
-echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+  if test "$cross_compiling" = yes; then
+  # Depending upon the size, compute the lo and hi bounds.
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -30930,7 +31303,16 @@ cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 $ac_includes_default
-#include <$ac_header>
+   typedef gid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) >= 0)];
+test_array [0] = 0
+
+  ;
+  return 0;
+}
 _ACEOF
 rm -f conftest.$ac_objext
 if { (ac_try="$ac_compile"
@@ -30949,135 +31331,979 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_header_compiler=yes
+  ac_lo=0 ac_mid=0
+  while :; do
+    cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef gid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) <= $ac_mid)];
+test_array [0] = 0
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_hi=$ac_mid; break
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_header_compiler=no
+	ac_lo=`expr $ac_mid + 1`
+			if test $ac_lo -le $ac_mid; then
+			  ac_lo= ac_hi=
+			  break
+			fi
+			ac_mid=`expr 2 '*' $ac_mid + 1`
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
-echo "${ECHO_T}$ac_header_compiler" >&6; }
+  done
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-# Is the header present?
-{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
-echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+	cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <$ac_header>
+$ac_includes_default
+   typedef gid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) < 0)];
+test_array [0] = 0
+
+  ;
+  return 0;
+}
 _ACEOF
-if { (ac_try="$ac_cpp conftest.$ac_ext"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
   cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } >/dev/null && {
-	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       }; then
-  ac_header_preproc=yes
+       } && test -s conftest.$ac_objext; then
+  ac_hi=-1 ac_mid=-1
+  while :; do
+    cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef gid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) >= $ac_mid)];
+test_array [0] = 0
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_lo=$ac_mid; break
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-  ac_header_preproc=no
+	ac_hi=`expr '(' $ac_mid ')' - 1`
+			if test $ac_mid -le $ac_hi; then
+			  ac_lo= ac_hi=
+			  break
+			fi
+			ac_mid=`expr 2 '*' $ac_mid`
 fi
 
-rm -f conftest.err conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
-echo "${ECHO_T}$ac_header_preproc" >&6; }
-
-# So?  What about this header?
-case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
-  yes:no: )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
-echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
-    ac_header_preproc=yes
-    ;;
-  no:yes:* )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
-echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
-echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
-echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
-echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
-echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
-
-    ;;
-esac
-{ echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+  done
 else
-  eval "$as_ac_Header=\$ac_header_preproc"
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
+	ac_lo= ac_hi=
 fi
-if test `eval echo '${'$as_ac_Header'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
-_ACEOF
- have_utmp=1
-else
-  have_utmp=0
-fi
-
-done
 
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
 
-if test $have_utmp; then
-  { echo "$as_me:$LINENO: checking whether struct utmp has ut_user" >&5
-echo $ECHO_N "checking whether struct utmp has ut_user... $ECHO_C" >&6; }
-if test "${pr_cv_header_utmaxtype+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+# Binary search between lo and hi bounds.
+while test "x$ac_lo" != "x$ac_hi"; do
+  ac_mid=`expr '(' $ac_hi - $ac_lo ')' / 2 + $ac_lo`
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <utmp.h>
+$ac_includes_default
+   typedef gid_t ac__type_sizeof_;
+int
+main ()
+{
+static int test_array [1 - 2 * !(((long int) (sizeof (ac__type_sizeof_))) <= $ac_mid)];
+test_array [0] = 0
 
+  ;
+  return 0;
+}
 _ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP " *ut_user.*;" >/dev/null 2>&1; then
-  pr_cv_header_utmaxtype="yes"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_hi=$ac_mid
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_lo=`expr '(' $ac_mid ')' + 1`
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+done
+case $ac_lo in
+?*) ac_cv_sizeof_gid_t=$ac_lo;;
+'') if test "$ac_cv_type_gid_t" = yes; then
+     { { echo "$as_me:$LINENO: error: cannot compute sizeof (gid_t)
+See \`config.log' for more details." >&5
+echo "$as_me: error: cannot compute sizeof (gid_t)
+See \`config.log' for more details." >&2;}
+   { (exit 77); exit 77; }; }
+   else
+     ac_cv_sizeof_gid_t=0
+   fi ;;
+esac
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+   typedef gid_t ac__type_sizeof_;
+static long int longval () { return (long int) (sizeof (ac__type_sizeof_)); }
+static unsigned long int ulongval () { return (long int) (sizeof (ac__type_sizeof_)); }
+#include <stdio.h>
+#include <stdlib.h>
+int
+main ()
+{
+
+  FILE *f = fopen ("conftest.val", "w");
+  if (! f)
+    return 1;
+  if (((long int) (sizeof (ac__type_sizeof_))) < 0)
+    {
+      long int i = longval ();
+      if (i != ((long int) (sizeof (ac__type_sizeof_))))
+	return 1;
+      fprintf (f, "%ld\n", i);
+    }
+  else
+    {
+      unsigned long int i = ulongval ();
+      if (i != ((long int) (sizeof (ac__type_sizeof_))))
+	return 1;
+      fprintf (f, "%lu\n", i);
+    }
+  return ferror (f) || fclose (f) != 0;
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && { ac_try='./conftest$ac_exeext'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; }; then
+  ac_cv_sizeof_gid_t=`cat conftest.val`
+else
+  echo "$as_me: program exited with status $ac_status" >&5
+echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+( exit $ac_status )
+if test "$ac_cv_type_gid_t" = yes; then
+     { { echo "$as_me:$LINENO: error: cannot compute sizeof (gid_t)
+See \`config.log' for more details." >&5
+echo "$as_me: error: cannot compute sizeof (gid_t)
+See \`config.log' for more details." >&2;}
+   { (exit 77); exit 77; }; }
+   else
+     ac_cv_sizeof_gid_t=0
+   fi
+fi
+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext conftest.$ac_objext conftest.$ac_ext
+fi
+rm -f conftest.val
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_sizeof_gid_t" >&5
+echo "${ECHO_T}$ac_cv_sizeof_gid_t" >&6; }
+
+
+
+cat >>confdefs.h <<_ACEOF
+#define SIZEOF_GID_T $ac_cv_sizeof_gid_t
+_ACEOF
+
+
+
+{ echo "$as_me:$LINENO: checking for mode_t" >&5
+echo $ECHO_N "checking for mode_t... $ECHO_C" >&6; }
+if test "${ac_cv_type_mode_t+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+typedef mode_t ac__type_new_;
+int
+main ()
+{
+if ((ac__type_new_ *) 0)
+  return 0;
+if (sizeof (ac__type_new_))
+  return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_type_mode_t=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_type_mode_t=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_type_mode_t" >&5
+echo "${ECHO_T}$ac_cv_type_mode_t" >&6; }
+if test $ac_cv_type_mode_t = yes; then
+  :
+else
+
+cat >>confdefs.h <<_ACEOF
+#define mode_t mode_t
+_ACEOF
+
+fi
+
+{ echo "$as_me:$LINENO: checking for ino_t" >&5
+echo $ECHO_N "checking for ino_t... $ECHO_C" >&6; }
+if test "${ac_cv_type_ino_t+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+typedef ino_t ac__type_new_;
+int
+main ()
+{
+if ((ac__type_new_ *) 0)
+  return 0;
+if (sizeof (ac__type_new_))
+  return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_type_ino_t=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_type_ino_t=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_type_ino_t" >&5
+echo "${ECHO_T}$ac_cv_type_ino_t" >&6; }
+if test $ac_cv_type_ino_t = yes; then
+  :
+else
+
+cat >>confdefs.h <<_ACEOF
+#define ino_t ino_t
+_ACEOF
+
+fi
+
+{ echo "$as_me:$LINENO: checking for intptr_t" >&5
+echo $ECHO_N "checking for intptr_t... $ECHO_C" >&6; }
+if test "${ac_cv_type_intptr_t+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+  #if HAVE_INTTYPES_H
+  # include <inttypes.h>
+  #endif
+
+
+typedef intptr_t ac__type_new_;
+int
+main ()
+{
+if ((ac__type_new_ *) 0)
+  return 0;
+if (sizeof (ac__type_new_))
+  return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_type_intptr_t=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_type_intptr_t=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_type_intptr_t" >&5
+echo "${ECHO_T}$ac_cv_type_intptr_t" >&6; }
+if test $ac_cv_type_intptr_t = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_INTPTR_T 1
+_ACEOF
+
+fi
+
+
+{ echo "$as_me:$LINENO: checking for socklen_t" >&5
+echo $ECHO_N "checking for socklen_t... $ECHO_C" >&6; }
+if test "${ac_cv_type_socklen_t+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_NETDB_H
+    # include <netdb.h>
+    #endif
+    #include <sys/socket.h>
+
+
+typedef socklen_t ac__type_new_;
+int
+main ()
+{
+if ((ac__type_new_ *) 0)
+  return 0;
+if (sizeof (ac__type_new_))
+  return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_type_socklen_t=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_type_socklen_t=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_type_socklen_t" >&5
+echo "${ECHO_T}$ac_cv_type_socklen_t" >&6; }
+if test $ac_cv_type_socklen_t = yes; then
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_SOCKLEN_T 1
+_ACEOF
+
+
+else
+  cat >>confdefs.h <<\_ACEOF
+#define socklen_t int
+_ACEOF
+
+fi
+
+
+
+for ac_header in utmp.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <$ac_header>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_header_compiler=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <$ac_header>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+ have_utmp=1
+else
+  have_utmp=0
+fi
+
+done
+
+
+if test $have_utmp; then
+  { echo "$as_me:$LINENO: checking whether struct utmp has ut_user" >&5
+echo $ECHO_N "checking whether struct utmp has ut_user... $ECHO_C" >&6; }
+if test "${pr_cv_header_utmaxtype+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <utmp.h>
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP " *ut_user.*;" >/dev/null 2>&1; then
+  pr_cv_header_utmaxtype="yes"
+else
+  pr_cv_header_utmaxtype="no"
+fi
+rm -f conftest*
+
+fi
+{ echo "$as_me:$LINENO: result: $pr_cv_header_utmaxtype" >&5
+echo "${ECHO_T}$pr_cv_header_utmaxtype" >&6; }
+  { echo "$as_me:$LINENO: checking whether struct utmp has ut_host" >&5
+echo $ECHO_N "checking whether struct utmp has ut_host... $ECHO_C" >&6; }
+if test "${pr_cv_header_ut_host+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <utmp.h>
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP " *ut_host.*;" >/dev/null 2>&1; then
+  pr_cv_header_ut_host="yes"
+else
+  pr_cv_header_ut_host="no"
+fi
+rm -f conftest*
+
+fi
+{ echo "$as_me:$LINENO: result: $pr_cv_header_ut_host" >&5
+echo "${ECHO_T}$pr_cv_header_ut_host" >&6; }
+  { echo "$as_me:$LINENO: checking whether struct utmp has ut_exit" >&5
+echo $ECHO_N "checking whether struct utmp has ut_exit... $ECHO_C" >&6; }
+if test "${pr_cv_header_ut_exit+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <utmp.h>
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP " *ut_exit.*;" >/dev/null 2>&1; then
+  pr_cv_header_ut_exit="yes"
+else
+  pr_cv_header_ut_exit="no"
+fi
+rm -f conftest*
+
+fi
+{ echo "$as_me:$LINENO: result: $pr_cv_header_ut_exit" >&5
+echo "${ECHO_T}$pr_cv_header_ut_exit" >&6; }
+  if test "$pr_cv_header_utmaxtype" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_UTMAXTYPE 1
+_ACEOF
+
+  fi
+  if test "$pr_cv_header_ut_host" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_UT_UT_HOST 1
+_ACEOF
+
+  fi
+  if test "$pr_cv_header_ut_exit" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_UT_UT_EXIT 1
+_ACEOF
+
+  fi
+fi
+
+if test "$have_syslog_h" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SYSLOG_H 1
+_ACEOF
+
+
+  { echo "$as_me:$LINENO: checking whether syslog.h defines LOG_CRON" >&5
+echo $ECHO_N "checking whether syslog.h defines LOG_CRON... $ECHO_C" >&6; }
+if test "${pr_cv_header_syslog_log_cron+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+#include <syslog.h>
+#ifdef LOG_CRON
+  yes
+#endif
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "yes" >/dev/null 2>&1; then
+  pr_cv_header_syslog_log_cron="yes"
+else
+  pr_cv_header_syslog_log_cron="no"
+fi
+rm -f conftest*
+
+fi
+{ echo "$as_me:$LINENO: result: $pr_cv_header_syslog_log_cron" >&5
+echo "${ECHO_T}$pr_cv_header_syslog_log_cron" >&6; }
+
+  { echo "$as_me:$LINENO: checking whether syslog.h defines LOG_FTP" >&5
+echo $ECHO_N "checking whether syslog.h defines LOG_FTP... $ECHO_C" >&6; }
+if test "${pr_cv_header_syslog_log_ftp+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
 else
-  pr_cv_header_utmaxtype="no"
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+#include <syslog.h>
+#ifdef LOG_FTP
+  yes
+#endif
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "yes" >/dev/null 2>&1; then
+  pr_cv_header_syslog_log_ftp="yes"
+else
+  pr_cv_header_syslog_log_ftp="no"
 fi
 rm -f conftest*
 
 fi
-{ echo "$as_me:$LINENO: result: $pr_cv_header_utmaxtype" >&5
-echo "${ECHO_T}$pr_cv_header_utmaxtype" >&6; }
-  { echo "$as_me:$LINENO: checking whether struct utmp has ut_host" >&5
-echo $ECHO_N "checking whether struct utmp has ut_host... $ECHO_C" >&6; }
-if test "${pr_cv_header_ut_host+set}" = set; then
+{ echo "$as_me:$LINENO: result: $pr_cv_header_syslog_log_ftp" >&5
+echo "${ECHO_T}$pr_cv_header_syslog_log_ftp" >&6; }
+
+  if test "$pr_cv_header_syslog_log_cron" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LOG_CRON 1
+_ACEOF
+
+  fi
+  if test "$pr_cv_header_syslog_log_ftp" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LOG_FTP 1
+_ACEOF
+
+  fi
+fi
+
+for dirfd in d_fd dd_fd __dd_fd ; do
+	{ echo "$as_me:$LINENO: checking for $dirfd in DIR structure" >&5
+echo $ECHO_N "checking for $dirfd in DIR structure... $ECHO_C" >&6; }
+	cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+#include <stdio.h>
+#include <dirent.h>
+
+int
+main ()
+{
+
+DIR *dirp;
+int i = dirp->$dirfd;
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  eval ac_cv_struct_dir_$dirfd=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+	if test "`eval echo x'$''ac_cv_struct_dir_'$dirfd`" = xyes ; then
+		ucdirfd=`echo $dirfd | tr 'abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'`
+		cat >>confdefs.h <<_ACEOF
+#define HAVE_STRUCT_DIR_$ucdirfd 1
+_ACEOF
+
+		{ echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+		break
+	else
+		{ echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+	fi
+done
+
+
+
+# The Ultrix 4.2 mips builtin alloca declared by alloca.h only works
+# for constant arguments.  Useless!
+{ echo "$as_me:$LINENO: checking for working alloca.h" >&5
+echo $ECHO_N "checking for working alloca.h... $ECHO_C" >&6; }
+if test "${ac_cv_working_alloca_h+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -31086,23 +32312,150 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <utmp.h>
+#include <alloca.h>
+int
+main ()
+{
+char *p = (char *) alloca (2 * sizeof (int));
+			  if (p) return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_working_alloca_h=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_working_alloca_h=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_working_alloca_h" >&5
+echo "${ECHO_T}$ac_cv_working_alloca_h" >&6; }
+if test $ac_cv_working_alloca_h = yes; then
 
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_ALLOCA_H 1
 _ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP " *ut_host.*;" >/dev/null 2>&1; then
-  pr_cv_header_ut_host="yes"
+
+fi
+
+{ echo "$as_me:$LINENO: checking for alloca" >&5
+echo $ECHO_N "checking for alloca... $ECHO_C" >&6; }
+if test "${ac_cv_func_alloca_works+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
 else
-  pr_cv_header_ut_host="no"
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#ifdef __GNUC__
+# define alloca __builtin_alloca
+#else
+# ifdef _MSC_VER
+#  include <malloc.h>
+#  define alloca _alloca
+# else
+#  ifdef HAVE_ALLOCA_H
+#   include <alloca.h>
+#  else
+#   ifdef _AIX
+ #pragma alloca
+#   else
+#    ifndef alloca /* predefined by HP cc +Olibcalls */
+char *alloca ();
+#    endif
+#   endif
+#  endif
+# endif
+#endif
+
+int
+main ()
+{
+char *p = (char *) alloca (1);
+				    if (p) return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_func_alloca_works=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_func_alloca_works=no
 fi
-rm -f conftest*
 
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $pr_cv_header_ut_host" >&5
-echo "${ECHO_T}$pr_cv_header_ut_host" >&6; }
-  { echo "$as_me:$LINENO: checking whether struct utmp has ut_exit" >&5
-echo $ECHO_N "checking whether struct utmp has ut_exit... $ECHO_C" >&6; }
-if test "${pr_cv_header_ut_exit+set}" = set; then
+{ echo "$as_me:$LINENO: result: $ac_cv_func_alloca_works" >&5
+echo "${ECHO_T}$ac_cv_func_alloca_works" >&6; }
+
+if test $ac_cv_func_alloca_works = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_ALLOCA 1
+_ACEOF
+
+else
+  # The SVR3 libPW and SVR4 libucb both contain incompatible functions
+# that cause trouble.  Some versions do not even contain alloca or
+# contain a buggy version.  If you still want to use their alloca,
+# use ar to extract alloca.o from them instead of compiling alloca.c.
+
+ALLOCA=\${LIBOBJDIR}alloca.$ac_objext
+
+cat >>confdefs.h <<\_ACEOF
+#define C_ALLOCA 1
+_ACEOF
+
+
+{ echo "$as_me:$LINENO: checking whether \`alloca.c' needs Cray hooks" >&5
+echo $ECHO_N "checking whether \`alloca.c' needs Cray hooks... $ECHO_C" >&6; }
+if test "${ac_cv_os_cray+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -31111,146 +32464,344 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <utmp.h>
+#if defined CRAY && ! defined CRAY2
+webecray
+#else
+wenotbecray
+#endif
 
 _ACEOF
 if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP " *ut_exit.*;" >/dev/null 2>&1; then
-  pr_cv_header_ut_exit="yes"
+  $EGREP "webecray" >/dev/null 2>&1; then
+  ac_cv_os_cray=yes
 else
-  pr_cv_header_ut_exit="no"
+  ac_cv_os_cray=no
 fi
 rm -f conftest*
 
 fi
-{ echo "$as_me:$LINENO: result: $pr_cv_header_ut_exit" >&5
-echo "${ECHO_T}$pr_cv_header_ut_exit" >&6; }
-  if test "$pr_cv_header_utmaxtype" = "yes"; then
+{ echo "$as_me:$LINENO: result: $ac_cv_os_cray" >&5
+echo "${ECHO_T}$ac_cv_os_cray" >&6; }
+if test $ac_cv_os_cray = yes; then
+  for ac_func in _getb67 GETB67 getb67; do
+    as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $ac_func innocuous_$ac_func
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_UTMAXTYPE 1
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
+
+#undef $ac_func
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $ac_func ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$ac_func || defined __stub___$ac_func
+choke me
+#endif
+
+int
+main ()
+{
+return $ac_func ();
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  eval "$as_ac_var=yes"
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	eval "$as_ac_var=no"
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+fi
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+
+cat >>confdefs.h <<_ACEOF
+#define CRAY_STACKSEG_END $ac_func
+_ACEOF
+
+    break
+fi
+
+  done
+fi
+
+{ echo "$as_me:$LINENO: checking stack direction for C alloca" >&5
+echo $ECHO_N "checking stack direction for C alloca... $ECHO_C" >&6; }
+if test "${ac_cv_c_stack_direction+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  if test "$cross_compiling" = yes; then
+  ac_cv_c_stack_direction=0
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+int
+find_stack_direction ()
+{
+  static char *addr = 0;
+  auto char dummy;
+  if (addr == 0)
+    {
+      addr = &dummy;
+      return find_stack_direction ();
+    }
+  else
+    return (&dummy > addr) ? 1 : -1;
+}
+
+int
+main ()
+{
+  return find_stack_direction () < 0;
+}
+_ACEOF
+rm -f conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && { ac_try='./conftest$ac_exeext'
+  { (case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); }; }; then
+  ac_cv_c_stack_direction=1
+else
+  echo "$as_me: program exited with status $ac_status" >&5
+echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+( exit $ac_status )
+ac_cv_c_stack_direction=-1
+fi
+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext conftest.$ac_objext conftest.$ac_ext
+fi
+
+
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_c_stack_direction" >&5
+echo "${ECHO_T}$ac_cv_c_stack_direction" >&6; }
+
+cat >>confdefs.h <<_ACEOF
+#define STACK_DIRECTION $ac_cv_c_stack_direction
+_ACEOF
+
+
+fi
+
+
+old_LDFLAGS=$LDFLAGS
+LDFLAGS="$LDFLAGS -L/usr/ucblib/"
+
+{ echo "$as_me:$LINENO: checking for alloca in -lucb" >&5
+echo $ECHO_N "checking for alloca in -lucb... $ECHO_C" >&6; }
+if test "${ac_cv_lib_ucb_alloca+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lucb  $LIBS"
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char alloca ();
+int
+main ()
+{
+return alloca ();
+  ;
+  return 0;
+}
 _ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_lib_ucb_alloca=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-  fi
-  if test "$pr_cv_header_ut_host" = "yes"; then
+	ac_cv_lib_ucb_alloca=no
+fi
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_UT_UT_HOST 1
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_lib_ucb_alloca" >&5
+echo "${ECHO_T}$ac_cv_lib_ucb_alloca" >&6; }
+if test $ac_cv_lib_ucb_alloca = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_LIBUCB 1
 _ACEOF
 
-  fi
-  if test "$pr_cv_header_ut_exit" = "yes"; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_UT_UT_EXIT 1
-_ACEOF
+  LIBS="-lucb $LIBS"
 
-  fi
 fi
 
-if test "$have_syslog_h" = "yes"; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_SYSLOG_H 1
-_ACEOF
-
+LDFLAGS=$old_LDFLAGS
 
-  { echo "$as_me:$LINENO: checking whether syslog.h defines LOG_CRON" >&5
-echo $ECHO_N "checking whether syslog.h defines LOG_CRON... $ECHO_C" >&6; }
-if test "${pr_cv_header_syslog_log_cron+set}" = set; then
+if test $ac_cv_c_compiler_gnu = yes; then
+    { echo "$as_me:$LINENO: checking whether $CC needs -traditional" >&5
+echo $ECHO_N "checking whether $CC needs -traditional... $ECHO_C" >&6; }
+if test "${ac_cv_prog_gcc_traditional+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
+    ac_pattern="Autoconf.*'x'"
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-#include <syslog.h>
-#ifdef LOG_CRON
-  yes
-#endif
-
+#include <sgtty.h>
+Autoconf TIOCGETP
 _ACEOF
 if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "yes" >/dev/null 2>&1; then
-  pr_cv_header_syslog_log_cron="yes"
+  $EGREP "$ac_pattern" >/dev/null 2>&1; then
+  ac_cv_prog_gcc_traditional=yes
 else
-  pr_cv_header_syslog_log_cron="no"
+  ac_cv_prog_gcc_traditional=no
 fi
 rm -f conftest*
 
-fi
-{ echo "$as_me:$LINENO: result: $pr_cv_header_syslog_log_cron" >&5
-echo "${ECHO_T}$pr_cv_header_syslog_log_cron" >&6; }
 
-  { echo "$as_me:$LINENO: checking whether syslog.h defines LOG_FTP" >&5
-echo $ECHO_N "checking whether syslog.h defines LOG_FTP... $ECHO_C" >&6; }
-if test "${pr_cv_header_syslog_log_ftp+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+  if test $ac_cv_prog_gcc_traditional = no; then
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-#include <syslog.h>
-#ifdef LOG_FTP
-  yes
-#endif
-
+#include <termio.h>
+Autoconf TCGETA
 _ACEOF
 if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "yes" >/dev/null 2>&1; then
-  pr_cv_header_syslog_log_ftp="yes"
-else
-  pr_cv_header_syslog_log_ftp="no"
+  $EGREP "$ac_pattern" >/dev/null 2>&1; then
+  ac_cv_prog_gcc_traditional=yes
 fi
 rm -f conftest*
 
-fi
-{ echo "$as_me:$LINENO: result: $pr_cv_header_syslog_log_ftp" >&5
-echo "${ECHO_T}$pr_cv_header_syslog_log_ftp" >&6; }
-
-  if test "$pr_cv_header_syslog_log_cron" = "yes"; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_LOG_CRON 1
-_ACEOF
-
   fi
-  if test "$pr_cv_header_syslog_log_ftp" = "yes"; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_LOG_FTP 1
-_ACEOF
-
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_prog_gcc_traditional" >&5
+echo "${ECHO_T}$ac_cv_prog_gcc_traditional" >&6; }
+  if test $ac_cv_prog_gcc_traditional = yes; then
+    CC="$CC -traditional"
   fi
 fi
 
-for dirfd in d_fd dd_fd __dd_fd ; do
-	{ echo "$as_me:$LINENO: checking for $dirfd in DIR structure" >&5
-echo $ECHO_N "checking for $dirfd in DIR structure... $ECHO_C" >&6; }
-	cat >conftest.$ac_ext <<_ACEOF
+{ echo "$as_me:$LINENO: checking return type of signal handlers" >&5
+echo $ECHO_N "checking return type of signal handlers... $ECHO_C" >&6; }
+if test "${ac_cv_type_signal+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-#include <stdio.h>
-#include <dirent.h>
+#include <sys/types.h>
+#include <signal.h>
 
 int
 main ()
 {
-
-DIR *dirp;
-int i = dirp->$dirfd;
-
+return *(signal (0, 0)) (0) == 1;
   ;
   return 0;
 }
@@ -31272,37 +32823,31 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  eval ac_cv_struct_dir_$dirfd=yes
+  ac_cv_type_signal=int
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-
+	ac_cv_type_signal=void
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-	if test "`eval echo x'$''ac_cv_struct_dir_'$dirfd`" = xyes ; then
-		ucdirfd=`echo $dirfd | tr 'abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'`
-		cat >>confdefs.h <<_ACEOF
-#define HAVE_STRUCT_DIR_$ucdirfd 1
-_ACEOF
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_type_signal" >&5
+echo "${ECHO_T}$ac_cv_type_signal" >&6; }
 
-		{ echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-		break
-	else
-		{ echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-	fi
-done
+cat >>confdefs.h <<_ACEOF
+#define RETSIGTYPE $ac_cv_type_signal
+_ACEOF
 
 
 
-# The Ultrix 4.2 mips builtin alloca declared by alloca.h only works
-# for constant arguments.  Useless!
-{ echo "$as_me:$LINENO: checking for working alloca.h" >&5
-echo $ECHO_N "checking for working alloca.h... $ECHO_C" >&6; }
-if test "${ac_cv_working_alloca_h+set}" = set; then
+for ac_func in vprintf
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -31311,12 +32856,41 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <alloca.h>
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $ac_func innocuous_$ac_func
+
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
+
+#undef $ac_func
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $ac_func ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$ac_func || defined __stub___$ac_func
+choke me
+#endif
+
 int
 main ()
 {
-char *p = (char *) alloca (2 * sizeof (int));
-			  if (p) return 0;
+return $ac_func ();
   ;
   return 0;
 }
@@ -31339,30 +32913,28 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_working_alloca_h=yes
+  eval "$as_ac_var=yes"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_working_alloca_h=no
+	eval "$as_ac_var=no"
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_working_alloca_h" >&5
-echo "${ECHO_T}$ac_cv_working_alloca_h" >&6; }
-if test $ac_cv_working_alloca_h = yes; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_ALLOCA_H 1
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
-fi
-
-{ echo "$as_me:$LINENO: checking for alloca" >&5
-echo $ECHO_N "checking for alloca... $ECHO_C" >&6; }
-if test "${ac_cv_func_alloca_works+set}" = set; then
+{ echo "$as_me:$LINENO: checking for _doprnt" >&5
+echo $ECHO_N "checking for _doprnt... $ECHO_C" >&6; }
+if test "${ac_cv_func__doprnt+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -31371,32 +32943,41 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#ifdef __GNUC__
-# define alloca __builtin_alloca
+/* Define _doprnt to an innocuous variant, in case <limits.h> declares _doprnt.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define _doprnt innocuous__doprnt
+
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char _doprnt (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
 #else
-# ifdef _MSC_VER
-#  include <malloc.h>
-#  define alloca _alloca
-# else
-#  ifdef HAVE_ALLOCA_H
-#   include <alloca.h>
-#  else
-#   ifdef _AIX
- #pragma alloca
-#   else
-#    ifndef alloca /* predefined by HP cc +Olibcalls */
-char *alloca ();
-#    endif
-#   endif
-#  endif
-# endif
+# include <assert.h>
+#endif
+
+#undef _doprnt
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char _doprnt ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub__doprnt || defined __stub____doprnt
+choke me
 #endif
 
 int
 main ()
 {
-char *p = (char *) alloca (1);
-				    if (p) return 0;
+return _doprnt ();
   ;
   return 0;
 }
@@ -31419,71 +33000,51 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_func_alloca_works=yes
+  ac_cv_func__doprnt=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_func_alloca_works=no
+	ac_cv_func__doprnt=no
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_func_alloca_works" >&5
-echo "${ECHO_T}$ac_cv_func_alloca_works" >&6; }
-
-if test $ac_cv_func_alloca_works = yes; then
+{ echo "$as_me:$LINENO: result: $ac_cv_func__doprnt" >&5
+echo "${ECHO_T}$ac_cv_func__doprnt" >&6; }
+if test $ac_cv_func__doprnt = yes; then
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_ALLOCA 1
+#define HAVE_DOPRNT 1
 _ACEOF
 
-else
-  # The SVR3 libPW and SVR4 libucb both contain incompatible functions
-# that cause trouble.  Some versions do not even contain alloca or
-# contain a buggy version.  If you still want to use their alloca,
-# use ar to extract alloca.o from them instead of compiling alloca.c.
+fi
 
-ALLOCA=\${LIBOBJDIR}alloca.$ac_objext
+fi
+done
 
-cat >>confdefs.h <<\_ACEOF
-#define C_ALLOCA 1
-_ACEOF
 
 
-{ echo "$as_me:$LINENO: checking whether \`alloca.c' needs Cray hooks" >&5
-echo $ECHO_N "checking whether \`alloca.c' needs Cray hooks... $ECHO_C" >&6; }
-if test "${ac_cv_os_cray+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#if defined CRAY && ! defined CRAY2
-webecray
-#else
-wenotbecray
-#endif
 
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "webecray" >/dev/null 2>&1; then
-  ac_cv_os_cray=yes
-else
-  ac_cv_os_cray=no
-fi
-rm -f conftest*
 
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_os_cray" >&5
-echo "${ECHO_T}$ac_cv_os_cray" >&6; }
-if test $ac_cv_os_cray = yes; then
-  for ac_func in _getb67 GETB67 getb67; do
-    as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+for ac_func in bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo fsync futimes getifaddrs getpgid getpgrp mkdtemp nl_langinfo
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
 echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
 if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
@@ -31567,125 +33128,214 @@ ac_res=`eval echo '${'$as_ac_var'}'`
 	       { echo "$as_me:$LINENO: result: $ac_res" >&5
 echo "${ECHO_T}$ac_res" >&6; }
 if test `eval echo '${'$as_ac_var'}'` = yes; then
-
-cat >>confdefs.h <<_ACEOF
-#define CRAY_STACKSEG_END $ac_func
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
-    break
-fi
-
-  done
 fi
+done
 
-{ echo "$as_me:$LINENO: checking stack direction for C alloca" >&5
-echo $ECHO_N "checking stack direction for C alloca... $ECHO_C" >&6; }
-if test "${ac_cv_c_stack_direction+set}" = set; then
+{ echo "$as_me:$LINENO: checking for gai_strerror" >&5
+echo $ECHO_N "checking for gai_strerror... $ECHO_C" >&6; }
+if test "${ac_cv_func_gai_strerror+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
-  if test "$cross_compiling" = yes; then
-  ac_cv_c_stack_direction=0
-else
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-$ac_includes_default
-int
-find_stack_direction ()
-{
-  static char *addr = 0;
-  auto char dummy;
-  if (addr == 0)
-    {
-      addr = &dummy;
-      return find_stack_direction ();
-    }
-  else
-    return (&dummy > addr) ? 1 : -1;
-}
+/* Define gai_strerror to an innocuous variant, in case <limits.h> declares gai_strerror.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define gai_strerror innocuous_gai_strerror
+
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char gai_strerror (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
+
+#undef gai_strerror
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char gai_strerror ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_gai_strerror || defined __stub___gai_strerror
+choke me
+#endif
 
 int
 main ()
 {
-  return find_stack_direction () < 0;
+return gai_strerror ();
+  ;
+  return 0;
 }
 _ACEOF
-rm -f conftest$ac_exeext
+rm -f conftest.$ac_objext conftest$ac_exeext
 if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>&5
-  ac_status=$?
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && { ac_try='./conftest$ac_exeext'
-  { (case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_try") 2>&5
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); }; }; then
-  ac_cv_c_stack_direction=1
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_func_gai_strerror=yes
 else
-  echo "$as_me: program exited with status $ac_status" >&5
-echo "$as_me: failed program was:" >&5
+  echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-( exit $ac_status )
-ac_cv_c_stack_direction=-1
+	ac_cv_func_gai_strerror=no
 fi
-rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext conftest.$ac_objext conftest.$ac_ext
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 fi
+{ echo "$as_me:$LINENO: result: $ac_cv_func_gai_strerror" >&5
+echo "${ECHO_T}$ac_cv_func_gai_strerror" >&6; }
+if test $ac_cv_func_gai_strerror = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_GAI_STRERROR 1
+_ACEOF
 
+else
+  LIB_OBJS="$LIB_OBJS glibc-gai_strerror.o"
 
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_c_stack_direction" >&5
-echo "${ECHO_T}$ac_cv_c_stack_direction" >&6; }
 
-cat >>confdefs.h <<_ACEOF
-#define STACK_DIRECTION $ac_cv_c_stack_direction
+
+{ echo "$as_me:$LINENO: checking for iconv" >&5
+echo $ECHO_N "checking for iconv... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
 _ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #ifdef HAVE_ICONV_H
+    # include <iconv.h>
+    #endif
+
+int
+main ()
+{
 
+    size_t res, in_len = 0, out_len = 0;
+    const char *in = NULL;
+    char *out = NULL;
+    res = iconv((iconv_t)-1, &in, &in_len, &out, &out_len);
 
-fi
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
 
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
-old_LDFLAGS=$LDFLAGS
-LDFLAGS="$LDFLAGS -L/usr/ucblib/"
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_ICONV 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
 
-{ echo "$as_me:$LINENO: checking for alloca in -lucb" >&5
-echo $ECHO_N "checking for alloca in -lucb... $ECHO_C" >&6; }
-if test "${ac_cv_lib_ucb_alloca+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-lucb  $LIBS"
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+{ echo "$as_me:$LINENO: checking for dirfd" >&5
+echo $ECHO_N "checking for dirfd... $ECHO_C" >&6; }
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_STDIO_H
+    # include <stdio.h>
+    #endif
+    #ifdef HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_DIRENT_H
+    # include <dirent.h>
+    #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char alloca ();
 int
 main ()
 {
-return alloca ();
+
+    DIR *dirh = NULL;
+    int fd;
+
+    fd = dirfd(dirh);
+
   ;
   return 0;
 }
@@ -31708,111 +33358,74 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_lib_ucb_alloca=yes
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_lib_ucb_alloca=no
-fi
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_lib_ucb_alloca" >&5
-echo "${ECHO_T}$ac_cv_lib_ucb_alloca" >&6; }
-if test $ac_cv_lib_ucb_alloca = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_LIBUCB 1
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_DIRFD 1
 _ACEOF
 
-  LIBS="-lucb $LIBS"
 
-fi
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-LDFLAGS=$old_LDFLAGS
 
-if test $ac_cv_c_compiler_gnu = yes; then
-    { echo "$as_me:$LINENO: checking whether $CC needs -traditional" >&5
-echo $ECHO_N "checking whether $CC needs -traditional... $ECHO_C" >&6; }
-if test "${ac_cv_prog_gcc_traditional+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-    ac_pattern="Autoconf.*'x'"
-  cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <sgtty.h>
-Autoconf TIOCGETP
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "$ac_pattern" >/dev/null 2>&1; then
-  ac_cv_prog_gcc_traditional=yes
-else
-  ac_cv_prog_gcc_traditional=no
-fi
-rm -f conftest*
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
 
-  if test $ac_cv_prog_gcc_traditional = no; then
-    cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <termio.h>
-Autoconf TCGETA
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "$ac_pattern" >/dev/null 2>&1; then
-  ac_cv_prog_gcc_traditional=yes
 fi
-rm -f conftest*
 
-  fi
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_prog_gcc_traditional" >&5
-echo "${ECHO_T}$ac_cv_prog_gcc_traditional" >&6; }
-  if test $ac_cv_prog_gcc_traditional = yes; then
-    CC="$CC -traditional"
-  fi
-fi
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
-{ echo "$as_me:$LINENO: checking return type of signal handlers" >&5
-echo $ECHO_N "checking return type of signal handlers... $ECHO_C" >&6; }
-if test "${ac_cv_type_signal+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+{ echo "$as_me:$LINENO: checking for getaddrinfo" >&5
+echo $ECHO_N "checking for getaddrinfo... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <sys/types.h>
-#include <signal.h>
+ #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_STDIO_H
+    # include <stdio.h>
+    #endif
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_NETDB_H
+    # include <netdb.h>
+    #endif
 
 int
 main ()
 {
-return *(signal (0, 0)) (0) == 1;
+
+    getaddrinfo(NULL, NULL, NULL, NULL);
+
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -31821,27 +33434,40 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_type_signal=int
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_GETADDRINFO 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_type_signal=void
-fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_type_signal" >&5
-echo "${ECHO_T}$ac_cv_type_signal" >&6; }
 
-cat >>confdefs.h <<_ACEOF
-#define RETSIGTYPE $ac_cv_type_signal
-_ACEOF
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
 
 
-for ac_func in vprintf
+
+
+
+
+
+
+for ac_func in getcwd getenv getgrouplist getgroups getgrset gethostbyname2 gethostname getnameinfo
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -31931,9 +33557,21 @@ if test `eval echo '${'$as_ac_var'}'` = yes; then
 #define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
-{ echo "$as_me:$LINENO: checking for _doprnt" >&5
-echo $ECHO_N "checking for _doprnt... $ECHO_C" >&6; }
-if test "${ac_cv_func__doprnt+set}" = set; then
+fi
+done
+
+
+
+
+
+
+
+for ac_func in gettimeofday hstrerror inet_aton inet_ntop inet_pton initgroups
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -31942,12 +33580,12 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define _doprnt to an innocuous variant, in case <limits.h> declares _doprnt.
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
    For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define _doprnt innocuous__doprnt
+#define $ac_func innocuous_$ac_func
 
 /* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char _doprnt (); below.
+    which can conflict with char $ac_func (); below.
     Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
     <limits.h> exists even on freestanding compilers.  */
 
@@ -31957,7 +33595,7 @@ cat >>conftest.$ac_ext <<_ACEOF
 # include <assert.h>
 #endif
 
-#undef _doprnt
+#undef $ac_func
 
 /* Override any GCC internal prototype to avoid an error.
    Use char because int might match the return type of a GCC
@@ -31965,18 +33603,18 @@ cat >>conftest.$ac_ext <<_ACEOF
 #ifdef __cplusplus
 extern "C"
 #endif
-char _doprnt ();
+char $ac_func ();
 /* The GNU C library defines this for functions which it implements
     to always fail with ENOSYS.  Some functions are actually named
     something starting with __ and the normal name is an alias.  */
-#if defined __stub__doprnt || defined __stub____doprnt
+#if defined __stub_$ac_func || defined __stub___$ac_func
 choke me
 #endif
 
 int
 main ()
 {
-return _doprnt ();
+return $ac_func ();
   ;
   return 0;
 }
@@ -31999,47 +33637,30 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_func__doprnt=yes
+  eval "$as_ac_var=yes"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_func__doprnt=no
+	eval "$as_ac_var=no"
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_func__doprnt" >&5
-echo "${ECHO_T}$ac_cv_func__doprnt" >&6; }
-if test $ac_cv_func__doprnt = yes; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_DOPRNT 1
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
-
-fi
 done
 
 
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-for ac_func in bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo futimes getifaddrs getpgid getpgrp mkdtemp nl_langinfo
+for ac_func in loginrestrictions
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -32132,9 +33753,21 @@ _ACEOF
 fi
 done
 
-{ echo "$as_me:$LINENO: checking for gai_strerror" >&5
-echo $ECHO_N "checking for gai_strerror... $ECHO_C" >&6; }
-if test "${ac_cv_func_gai_strerror+set}" = set; then
+
+
+
+
+
+
+
+
+
+for ac_func in memcpy mempcpy memset_s mkdir mkstemp mlock mlockall munlock munlockall
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -32143,12 +33776,12 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define gai_strerror to an innocuous variant, in case <limits.h> declares gai_strerror.
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
    For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define gai_strerror innocuous_gai_strerror
+#define $ac_func innocuous_$ac_func
 
 /* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char gai_strerror (); below.
+    which can conflict with char $ac_func (); below.
     Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
     <limits.h> exists even on freestanding compilers.  */
 
@@ -32158,7 +33791,7 @@ cat >>conftest.$ac_ext <<_ACEOF
 # include <assert.h>
 #endif
 
-#undef gai_strerror
+#undef $ac_func
 
 /* Override any GCC internal prototype to avoid an error.
    Use char because int might match the return type of a GCC
@@ -32166,18 +33799,18 @@ cat >>conftest.$ac_ext <<_ACEOF
 #ifdef __cplusplus
 extern "C"
 #endif
-char gai_strerror ();
+char $ac_func ();
 /* The GNU C library defines this for functions which it implements
     to always fail with ENOSYS.  Some functions are actually named
     something starting with __ and the normal name is an alias.  */
-#if defined __stub_gai_strerror || defined __stub___gai_strerror
+#if defined __stub_$ac_func || defined __stub___$ac_func
 choke me
 #endif
 
 int
 main ()
 {
-return gai_strerror ();
+return $ac_func ();
   ;
   return 0;
 }
@@ -32200,123 +33833,90 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_func_gai_strerror=yes
+  eval "$as_ac_var=yes"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_func_gai_strerror=no
+	eval "$as_ac_var=no"
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_func_gai_strerror" >&5
-echo "${ECHO_T}$ac_cv_func_gai_strerror" >&6; }
-if test $ac_cv_func_gai_strerror = yes; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_GAI_STRERROR 1
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
-else
-  LIB_OBJS="$LIB_OBJS glibc-gai_strerror.o"
-
 fi
+done
 
 
-{ echo "$as_me:$LINENO: checking for iconv" >&5
-echo $ECHO_N "checking for iconv... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
- #include <stdlib.h>
-    #include <sys/types.h>
-    #ifdef HAVE_ICONV_H
-    # include <iconv.h>
-    #endif
-
-int
-main ()
-{
 
-    size_t res, in_len = 0, out_len = 0;
-    const char *in = NULL;
-    char *out = NULL;
-    res = iconv((iconv_t)-1, &in, &in_len, &out, &out_len);
 
-  ;
-  return 0;
-}
-_ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
 
-    { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_ICONV 1
-_ACEOF
 
 
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
 
 
-    { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
 
 
-fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
 
-{ echo "$as_me:$LINENO: checking for dirfd" >&5
-echo $ECHO_N "checking for dirfd... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+for ac_func in pathconf posix_fadvise prctl putenv regcomp rmdir select setgroups socket statfs strchr strcoll strerror
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $ac_func innocuous_$ac_func
 
-    #include <stdio.h>
-    #include <sys/types.h>
-    #if HAVE_DIRENT_H
-    # include <dirent.h>
-    #endif
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
 
-int
-main ()
-{
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
 
-    DIR *dirh = NULL;
-    int fd;
+#undef $ac_func
 
-    fd = dirfd(dirh);
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $ac_func ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$ac_func || defined __stub___$ac_func
+choke me
+#endif
 
+int
+main ()
+{
+return $ac_func ();
   ;
   return 0;
 }
@@ -32339,55 +33939,88 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
+  eval "$as_ac_var=yes"
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-    { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
+	eval "$as_ac_var=no"
+fi
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_DIRFD 1
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+fi
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
+fi
+done
 
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
 
 
-    { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
 
 
-fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
 
-{ echo "$as_me:$LINENO: checking for getaddrinfo" >&5
-echo $ECHO_N "checking for getaddrinfo... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+
+
+
+
+
+for ac_func in strlcat strlcpy strsep strtod strtof strtol strtoll strtoull setprotoent setspent endprotoent
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $ac_func innocuous_$ac_func
 
-    #include <stdio.h>
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_NETDB_H
-    # include <netdb.h>
-    #endif
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
+
+#undef $ac_func
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $ac_func ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$ac_func || defined __stub___$ac_func
+choke me
+#endif
 
 int
 main ()
 {
-
-    getaddrinfo(NULL, NULL, NULL, NULL);
-
+return $ac_func ();
   ;
   return 0;
 }
@@ -32410,37 +34043,32 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-
-    { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_GETADDRINFO 1
-_ACEOF
-
-
+  eval "$as_ac_var=yes"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-
-    { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-
-
+	eval "$as_ac_var=no"
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
+fi
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+_ACEOF
 
+fi
+done
 
+# __snprintf and __vsnprintf are only on solaris and _really_ broken there.
 
 
-
-
-
-
-for ac_func in getcwd getenv getgrouplist getgrset gethostbyname2 gethostname getnameinfo
+for ac_func in vsnprintf snprintf
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -32533,12 +34161,11 @@ _ACEOF
 fi
 done
 
+if test x"$ac_cv_func_vsnprintf" != xyes || test x"$ac_cv_func_snprintf" != xyes
+then
 
 
-
-
-
-for ac_func in gettimeofday hstrerror inet_aton inet_ntop inet_pton
+for ac_func in fconvert fcvt
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -32632,7 +34259,154 @@ fi
 done
 
 
-for ac_func in loginrestrictions
+for ac_header in floatingpoint.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <$ac_header>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_header_compiler=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <$ac_header>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+
+done
+
+fi
+
+
+
+
+
+
+
+for ac_func in setsid setgroupent seteuid setegid setenv setpgid siginterrupt
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -32728,12 +34502,7 @@ done
 
 
 
-
-
-
-
-
-for ac_func in memcpy mempcpy mkdir mkstemp mlock mlockall munlock munlockall
+for ac_func in tzset uname unsetenv
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -32827,22 +34596,9 @@ fi
 done
 
 
-
-
-
-
-
-
-
-
-
-
-for ac_func in pathconf putenv regcomp rmdir select setgroups socket statfs strchr strcoll strerror
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+{ echo "$as_me:$LINENO: checking for setpassent" >&5
+echo $ECHO_N "checking for setpassent... $ECHO_C" >&6; }
+if test "${ac_cv_func_setpassent+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -32851,12 +34607,12 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+/* Define setpassent to an innocuous variant, in case <limits.h> declares setpassent.
    For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
+#define setpassent innocuous_setpassent
 
 /* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
+    which can conflict with char setpassent (); below.
     Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
     <limits.h> exists even on freestanding compilers.  */
 
@@ -32866,7 +34622,7 @@ cat >>conftest.$ac_ext <<_ACEOF
 # include <assert.h>
 #endif
 
-#undef $ac_func
+#undef setpassent
 
 /* Override any GCC internal prototype to avoid an error.
    Use char because int might match the return type of a GCC
@@ -32874,18 +34630,18 @@ cat >>conftest.$ac_ext <<_ACEOF
 #ifdef __cplusplus
 extern "C"
 #endif
-char $ac_func ();
+char setpassent ();
 /* The GNU C library defines this for functions which it implements
     to always fail with ENOSYS.  Some functions are actually named
     something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
+#if defined __stub_setpassent || defined __stub___setpassent
 choke me
 #endif
 
 int
 main ()
 {
-return $ac_func ();
+return setpassent ();
   ;
   return 0;
 }
@@ -32908,39 +34664,39 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
+  ac_cv_func_setpassent=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
+	ac_cv_func_setpassent=no
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+{ echo "$as_me:$LINENO: result: $ac_cv_func_setpassent" >&5
+echo "${ECHO_T}$ac_cv_func_setpassent" >&6; }
+if test $ac_cv_func_setpassent = yes; then
+  case "$target_os:$enable_force_setpassent" in
+  *freebsd[23]\.[0-3]*:)
+    { echo "$as_me:$LINENO: WARNING: Disabling broken FreeBSD setpassent()" >&5
+echo "$as_me: WARNING: Disabling broken FreeBSD setpassent()" >&2;}
+        ;;
+  *:no) ;;
+  *)
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SETPASSENT 1
 _ACEOF
-
+ ;;
+esac
 fi
-done
-
-
-
-
-
-
-
 
 
+if test x"$enable_ctrls" = xyes; then
 
 
-for ac_func in strlcat strlcpy strsep strtod strtof strtol strtoull setprotoent setspent endprotoent
+for ac_func in getpeereid getpeerucred
 do
 as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
 { echo "$as_me:$LINENO: checking for $ac_func" >&5
@@ -33033,15 +34789,26 @@ _ACEOF
 fi
 done
 
-# __snprintf and __vsnprintf are only on solaris and _really_ broken there.
+  ac_static_modules="$ac_static_modules mod_ctrls.o"
+  ac_build_static_modules="$ac_build_static_modules modules/mod_ctrls.o"
+fi
+
+if test x"$enable_nls" = xyes; then
+  ac_static_modules="$ac_static_modules mod_lang.o"
+  ac_build_static_modules="$ac_build_static_modules modules/mod_lang.o"
 
+    if test x"$ifsession_requested" = xtrue; then
+    ac_static_modules=`echo "$ac_static_modules" | sed -e 's/mod_ifsession\.o//g'`
+    ac_static_modules="$ac_static_modules mod_ifsession.o"
 
-for ac_func in vsnprintf snprintf
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+    ac_build_static_modules=`echo "$ac_build_static_modules" | sed -e 's/modules\/mod_ifsession\.o//g'`
+    ac_build_static_modules="$ac_build_static_modules modules/mod_ifsession.o";
+  fi
+fi
+
+{ echo "$as_me:$LINENO: checking for struct cmsgcred.cmcred_uid" >&5
+echo $ECHO_N "checking for struct cmsgcred.cmcred_uid... $ECHO_C" >&6; }
+if test "${ac_cv_member_struct_cmsgcred_cmcred_uid+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -33050,53 +34817,286 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
 
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_SYS_UIO_H
+    # include <sys/uio.h>
+    #endif
 
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
 
-#undef $ac_func
+int
+main ()
+{
+static struct cmsgcred ac_aggr;
+if (ac_aggr.cmcred_uid)
+return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_cmsgcred_cmcred_uid=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_SYS_UIO_H
+    # include <sys/uio.h>
+    #endif
+
+
+int
+main ()
+{
+static struct cmsgcred ac_aggr;
+if (sizeof ac_aggr.cmcred_uid)
+return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_cmsgcred_cmcred_uid=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_member_struct_cmsgcred_cmcred_uid=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_cmsgcred_cmcred_uid" >&5
+echo "${ECHO_T}$ac_cv_member_struct_cmsgcred_cmcred_uid" >&6; }
+if test $ac_cv_member_struct_cmsgcred_cmcred_uid = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STRUCT_CMSGCRED 1
+_ACEOF
+
+fi
+
+
+{ echo "$as_me:$LINENO: checking for struct sockcred.sc_uid" >&5
+echo $ECHO_N "checking for struct sockcred.sc_uid... $ECHO_C" >&6; }
+if test "${ac_cv_member_struct_sockcred_sc_uid+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_SYS_UIO_H
+    # include <sys/uio.h>
+    #endif
+
+
+int
+main ()
+{
+static struct sockcred ac_aggr;
+if (ac_aggr.sc_uid)
+return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_sockcred_sc_uid=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_SYS_UIO_H
+    # include <sys/uio.h>
+    #endif
+
+
+int
+main ()
+{
+static struct sockcred ac_aggr;
+if (sizeof ac_aggr.sc_uid)
+return 0;
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_sockcred_sc_uid=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_member_struct_sockcred_sc_uid=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_sockcred_sc_uid" >&5
+echo "${ECHO_T}$ac_cv_member_struct_sockcred_sc_uid" >&6; }
+if test $ac_cv_member_struct_sockcred_sc_uid = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STRUCT_SOCKCRED 1
+_ACEOF
+
+fi
+
+
+# See:
+#   https://github.com/proftpd/proftpd/issues/75
+{ echo "$as_me:$LINENO: checking for struct sockpeercred.uid" >&5
+echo $ECHO_N "checking for struct sockpeercred.uid... $ECHO_C" >&6; }
+if test "${ac_cv_member_struct_sockpeercred_uid+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_SYS_UIO_H
+    # include <sys/uio.h>
+    #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
 
 int
 main ()
 {
-return $ac_func ();
+static struct sockpeercred ac_aggr;
+if (ac_aggr.uid)
+return 0;
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -33105,95 +35105,48 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_sockpeercred_uid=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
-_ACEOF
-
-fi
-done
-
-if test x"$ac_cv_func_vsnprintf" != xyes || test x"$ac_cv_func_snprintf" != xyes
-then
-
-
-for ac_func in fconvert fcvt
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+	cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
 
-#undef $ac_func
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_SYS_UIO_H
+    # include <sys/uio.h>
+    #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
 
 int
 main ()
 {
-return $ac_func ();
+static struct sockpeercred ac_aggr;
+if (sizeof ac_aggr.uid)
+return 0;
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -33202,55 +35155,63 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_sockpeercred_uid=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
+	ac_cv_member_struct_sockpeercred_uid=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_sockpeercred_uid" >&5
+echo "${ECHO_T}$ac_cv_member_struct_sockpeercred_uid" >&6; }
+if test $ac_cv_member_struct_sockpeercred_uid = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STRUCT_SOCKPEERCRED 1
 _ACEOF
 
 fi
-done
 
 
-for ac_header in floatingpoint.h
-do
-as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  { echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+{ echo "$as_me:$LINENO: checking for struct sockaddr_in.sin_len" >&5
+echo $ECHO_N "checking for struct sockaddr_in.sin_len... $ECHO_C" >&6; }
+if test "${ac_cv_member_struct_sockaddr_in_sin_len+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
 else
-  # Is the header compilable?
-{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
-echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+  cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-$ac_includes_default
-#include <$ac_header>
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_NETINET_IN_H
+    # include <netinet/in.h>
+    #endif
+
+
+int
+main ()
+{
+static struct sockaddr_in ac_aggr;
+if (ac_aggr.sin_len)
+return 0;
+  ;
+  return 0;
+}
 _ACEOF
 rm -f conftest.$ac_objext
 if { (ac_try="$ac_compile"
@@ -33269,117 +35230,83 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_header_compiler=yes
+  ac_cv_member_struct_sockaddr_in_sin_len=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_header_compiler=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
-echo "${ECHO_T}$ac_header_compiler" >&6; }
-
-# Is the header present?
-{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
-echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+	cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <$ac_header>
+
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_SOCKET_H
+    # include <sys/socket.h>
+    #endif
+    #if HAVE_NETINET_IN_H
+    # include <netinet/in.h>
+    #endif
+
+
+int
+main ()
+{
+static struct sockaddr_in ac_aggr;
+if (sizeof ac_aggr.sin_len)
+return 0;
+  ;
+  return 0;
+}
 _ACEOF
-if { (ac_try="$ac_cpp conftest.$ac_ext"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
   cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } >/dev/null && {
-	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       }; then
-  ac_header_preproc=yes
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_sockaddr_in_sin_len=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-  ac_header_preproc=no
-fi
-
-rm -f conftest.err conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
-echo "${ECHO_T}$ac_header_preproc" >&6; }
-
-# So?  What about this header?
-case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
-  yes:no: )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
-echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
-    ac_header_preproc=yes
-    ;;
-  no:yes:* )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
-echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
-echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
-echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
-echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
-echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
-
-    ;;
-esac
-{ echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  eval "$as_ac_Header=\$ac_header_preproc"
+	ac_cv_member_struct_sockaddr_in_sin_len=no
 fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
 
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-if test `eval echo '${'$as_ac_Header'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
-_ACEOF
 
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
+{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_sockaddr_in_sin_len" >&5
+echo "${ECHO_T}$ac_cv_member_struct_sockaddr_in_sin_len" >&6; }
+if test $ac_cv_member_struct_sockaddr_in_sin_len = yes; then
 
-done
+cat >>confdefs.h <<\_ACEOF
+#define SIN_LEN 1
+_ACEOF
 
 fi
 
 
-
-
-
-
-
-for ac_func in setsid setgroupent seteuid setegid setenv setpgid siginterrupt
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+{ echo "$as_me:$LINENO: checking for struct statfs.f_fstypename" >&5
+echo $ECHO_N "checking for struct statfs.f_fstypename... $ECHO_C" >&6; }
+if test "${ac_cv_member_struct_statfs_f_fstypename+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -33388,53 +35315,39 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
 
-#undef $ac_func
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_PARAM_H
+    # include <sys/param.h>
+    #endif
+    #if HAVE_SYS_MOUNT_H
+    # include <sys/mount.h>
+    #endif
+    #if HAVE_SYS_VFS_H
+    # include <sys/vfs.h>
+    #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
 
 int
 main ()
 {
-return $ac_func ();
+static struct statfs ac_aggr;
+if (ac_aggr.f_fstypename)
+return 0;
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -33443,94 +35356,51 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_statfs_f_fstypename=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
-_ACEOF
-
-fi
-done
-
-
-
-
-for ac_func in tzset uname unsetenv
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+	cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
 
-#undef $ac_func
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_PARAM_H
+    # include <sys/param.h>
+    #endif
+    #if HAVE_SYS_MOUNT_H
+    # include <sys/mount.h>
+    #endif
+    #if HAVE_SYS_VFS_H
+    # include <sys/vfs.h>
+    #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
 
 int
 main ()
 {
-return $ac_func ();
+static struct statfs ac_aggr;
+if (sizeof ac_aggr.f_fstypename)
+return 0;
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -33539,34 +35409,34 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_statfs_f_fstypename=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
+	ac_cv_member_struct_statfs_f_fstypename=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_statfs_f_fstypename" >&5
+echo "${ECHO_T}$ac_cv_member_struct_statfs_f_fstypename" >&6; }
+if test $ac_cv_member_struct_statfs_f_fstypename = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STATFS_F_FSTYPENAME 1
 _ACEOF
 
 fi
-done
 
 
-{ echo "$as_me:$LINENO: checking for setpassent" >&5
-echo $ECHO_N "checking for setpassent... $ECHO_C" >&6; }
-if test "${ac_cv_func_setpassent+set}" = set; then
+{ echo "$as_me:$LINENO: checking for struct statfs.f_type" >&5
+echo $ECHO_N "checking for struct statfs.f_type... $ECHO_C" >&6; }
+if test "${ac_cv_member_struct_statfs_f_type+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   cat >conftest.$ac_ext <<_ACEOF
@@ -33575,53 +35445,39 @@ _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define setpassent to an innocuous variant, in case <limits.h> declares setpassent.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define setpassent innocuous_setpassent
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char setpassent (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
-
-#undef setpassent
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char setpassent ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_setpassent || defined __stub___setpassent
-choke me
-#endif
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_PARAM_H
+    # include <sys/param.h>
+    #endif
+    #if HAVE_SYS_MOUNT_H
+    # include <sys/mount.h>
+    #endif
+    #if HAVE_SYS_VFS_H
+    # include <sys/vfs.h>
+    #endif
+
 
 int
 main ()
 {
-return setpassent ();
+static struct statfs ac_aggr;
+if (ac_aggr.f_type)
+return 0;
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -33630,101 +35486,51 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  ac_cv_func_setpassent=yes
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_statfs_f_type=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_func_setpassent=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_func_setpassent" >&5
-echo "${ECHO_T}$ac_cv_func_setpassent" >&6; }
-if test $ac_cv_func_setpassent = yes; then
-  case "$target_os:$enable_force_setpassent" in
-  *freebsd[23]\.[0-3]*:)
-    { echo "$as_me:$LINENO: WARNING: Disabling broken FreeBSD setpassent()" >&5
-echo "$as_me: WARNING: Disabling broken FreeBSD setpassent()" >&2;}
-        ;;
-  *:no) ;;
-  *)
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_SETPASSENT 1
-_ACEOF
- ;;
-esac
-fi
-
-
-if test x"$enable_ctrls" = xyes; then
-
-
-for ac_func in getpeereid getpeerucred
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+	cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
 
-#undef $ac_func
+    #if HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
+    #if HAVE_SYS_PARAM_H
+    # include <sys/param.h>
+    #endif
+    #if HAVE_SYS_MOUNT_H
+    # include <sys/mount.h>
+    #endif
+    #if HAVE_SYS_VFS_H
+    # include <sys/vfs.h>
+    #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
 
 int
 main ()
 {
-return $ac_func ();
+static struct statfs ac_aggr;
+if (sizeof ac_aggr.f_type)
+return 0;
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -33733,68 +35539,69 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
+       } && test -s conftest.$ac_objext; then
+  ac_cv_member_struct_statfs_f_type=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
+	ac_cv_member_struct_statfs_f_type=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
-_ACEOF
 
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-done
+{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_statfs_f_type" >&5
+echo "${ECHO_T}$ac_cv_member_struct_statfs_f_type" >&6; }
+if test $ac_cv_member_struct_statfs_f_type = yes; then
 
-  ac_static_modules="$ac_static_modules mod_ctrls.o"
-  ac_build_static_modules="$ac_build_static_modules modules/mod_ctrls.o"
-fi
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STATFS_F_TYPE 1
+_ACEOF
 
-if test x"$enable_nls" = xyes; then
-  ac_static_modules="$ac_static_modules mod_lang.o"
-  ac_build_static_modules="$ac_build_static_modules modules/mod_lang.o"
 fi
 
-{ echo "$as_me:$LINENO: checking for struct cmsgcred.cmcred_uid" >&5
-echo $ECHO_N "checking for struct cmsgcred.cmcred_uid... $ECHO_C" >&6; }
-if test "${ac_cv_member_struct_cmsgcred_cmcred_uid+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
+
+if test x"$enable_largefile" = xno; then
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_LARGEFILES 0
+_ACEOF
+
 else
-  cat >conftest.$ac_ext <<_ACEOF
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_LARGEFILES 1
+_ACEOF
+
+fi
+
+{ echo "$as_me:$LINENO: checking whether struct addrinfo is defined" >&5
+echo $ECHO_N "checking whether struct addrinfo is defined... $ECHO_C" >&6; }
+ cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_SYS_UIO_H
-    # include <sys/uio.h>
-    #endif
-
+ #include <stdio.h>
+   #ifdef HAVE_UNISTD_H
+   # include <unistd.h>
+   #endif
+   #include <sys/types.h>
+   #include <sys/socket.h>
+   #include <netdb.h>
 
 int
 main ()
 {
-static struct cmsgcred ac_aggr;
-if (ac_aggr.cmcred_uid)
-return 0;
+do {
+   struct addrinfo a;
+   (void) a.ai_flags;
+  } while(0)
+
   ;
   return 0;
 }
@@ -33816,35 +35623,48 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_cmsgcred_cmcred_uid=yes
+  { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STRUCT_ADDRINFO 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	cat >conftest.$ac_ext <<_ACEOF
+	{ echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+
+{ echo "$as_me:$LINENO: checking whether struct sockaddr_storage is defined" >&5
+echo $ECHO_N "checking whether struct sockaddr_storage is defined... $ECHO_C" >&6; }
+ cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_SYS_UIO_H
-    # include <sys/uio.h>
-    #endif
-
+ #include <stdio.h>
+   #ifdef HAVE_UNISTD_H
+   # include <unistd.h>
+   #endif
+   #include <sys/types.h>
+   #include <sys/socket.h>
+   #include <netdb.h>
 
 int
 main ()
 {
-static struct cmsgcred ac_aggr;
-if (sizeof ac_aggr.cmcred_uid)
-return 0;
+do {
+   struct sockaddr_storage ss;
+  } while(0)
+
   ;
   return 0;
 }
@@ -33866,35 +35686,28 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_cmsgcred_cmcred_uid=yes
+  { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_STRUCT_SS 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_member_struct_cmsgcred_cmcred_uid=no
-fi
+	{ echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_cmsgcred_cmcred_uid" >&5
-echo "${ECHO_T}$ac_cv_member_struct_cmsgcred_cmcred_uid" >&6; }
-if test $ac_cv_member_struct_cmsgcred_cmcred_uid = yes; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_STRUCT_CMSGCRED 1
-_ACEOF
-
-fi
 
 
-{ echo "$as_me:$LINENO: checking for struct sockcred.sc_uid" >&5
-echo $ECHO_N "checking for struct sockcred.sc_uid... $ECHO_C" >&6; }
-if test "${ac_cv_member_struct_sockcred_sc_uid+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
+  { echo "$as_me:$LINENO: checking whether ss_family is defined" >&5
+echo $ECHO_N "checking whether ss_family is defined... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -33902,23 +35715,20 @@ cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_SYS_UIO_H
-    # include <sys/uio.h>
-    #endif
-
+    #include <stdio.h>
+    #include <unistd.h>
+    #include <sys/types.h>
+    #include <sys/socket.h>
 
 int
 main ()
 {
-static struct sockcred ac_aggr;
-if (ac_aggr.sc_uid)
-return 0;
+
+    do {
+     struct sockaddr_storage a;
+     (void) a.ss_family;
+    } while(0)
+
   ;
   return 0;
 }
@@ -33940,35 +35750,45 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_sockcred_sc_uid=yes
+
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SS_FAMILY 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	cat >conftest.$ac_ext <<_ACEOF
+
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+    { echo "$as_me:$LINENO: checking whether __ss_family is defined" >&5
+echo $ECHO_N "checking whether __ss_family is defined... $ECHO_C" >&6; }
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_SYS_UIO_H
-    # include <sys/uio.h>
-    #endif
-
+      #include <stdio.h>
+      #include <unistd.h>
+      #include <sys/types.h>
+      #include <sys/socket.h>
 
 int
 main ()
 {
-static struct sockcred ac_aggr;
-if (sizeof ac_aggr.sc_uid)
-return 0;
+
+      do {
+       struct sockaddr_storage a;
+       (void) a.__ss_family;
+      } while(0)
+
   ;
   return 0;
 }
@@ -33990,37 +35810,34 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_sockcred_sc_uid=yes
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE___SS_FAMILY 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_member_struct_sockcred_sc_uid=no
-fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_sockcred_sc_uid" >&5
-echo "${ECHO_T}$ac_cv_member_struct_sockcred_sc_uid" >&6; }
-if test $ac_cv_member_struct_sockcred_sc_uid = yes; then
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_STRUCT_SOCKCRED 1
-_ACEOF
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 
 fi
 
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 
-# See:
-#   https://github.com/proftpd/proftpd/issues/75
-{ echo "$as_me:$LINENO: checking for struct sockpeercred.uid" >&5
-echo $ECHO_N "checking for struct sockpeercred.uid... $ECHO_C" >&6; }
-if test "${ac_cv_member_struct_sockpeercred_uid+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
+
+  { echo "$as_me:$LINENO: checking whether ss_len is defined" >&5
+echo $ECHO_N "checking whether ss_len is defined... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -34028,23 +35845,20 @@ cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_SYS_UIO_H
-    # include <sys/uio.h>
-    #endif
-
+    #include <stdio.h>
+    #include <unistd.h>
+    #include <sys/types.h>
+    #include <sys/socket.h>
 
 int
 main ()
 {
-static struct sockpeercred ac_aggr;
-if (ac_aggr.uid)
-return 0;
+
+    do {
+     struct sockaddr_storage a;
+     (void) a.ss_len;
+    } while(0)
+
   ;
   return 0;
 }
@@ -34066,35 +35880,45 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_sockpeercred_uid=yes
+
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SS_LEN 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	cat >conftest.$ac_ext <<_ACEOF
+
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+    { echo "$as_me:$LINENO: checking whether __ss_len is defined" >&5
+echo $ECHO_N "checking whether __ss_len is defined... $ECHO_C" >&6; }
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_SYS_UIO_H
-    # include <sys/uio.h>
-    #endif
-
+      #include <stdio.h>
+      #include <unistd.h>
+      #include <sys/types.h>
+      #include <sys/socket.h>
 
 int
 main ()
 {
-static struct sockpeercred ac_aggr;
-if (sizeof ac_aggr.uid)
-return 0;
+
+      do {
+       struct sockaddr_storage a;
+       (void) a.__ss_len;
+      } while(0)
+
   ;
   return 0;
 }
@@ -34116,121 +35940,210 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
        } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_sockpeercred_uid=yes
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE___SS_LEN 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_member_struct_sockpeercred_uid=no
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_sockpeercred_uid" >&5
-echo "${ECHO_T}$ac_cv_member_struct_sockpeercred_uid" >&6; }
-if test $ac_cv_member_struct_sockpeercred_uid = yes; then
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_STRUCT_SOCKPEERCRED 1
-_ACEOF
 
-fi
 
 
-{ echo "$as_me:$LINENO: checking for struct sockaddr_in.sin_len" >&5
-echo $ECHO_N "checking for struct sockaddr_in.sin_len... $ECHO_C" >&6; }
-if test "${ac_cv_member_struct_sockaddr_in_sin_len+set}" = set; then
+for ac_header in sys/acl.h acl/libacl.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
 else
-  cat >conftest.$ac_ext <<_ACEOF
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+$ac_includes_default
+#include <$ac_header>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_NETINET_IN_H
-    # include <netinet/in.h>
-    #endif
-
+	ac_header_compiler=no
+fi
 
-int
-main ()
-{
-static struct sockaddr_in ac_aggr;
-if (ac_aggr.sin_len)
-return 0;
-  ;
-  return 0;
-}
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <$ac_header>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
   cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_sockaddr_in_sin_len=yes
+       }; then
+  ac_header_preproc=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	cat >conftest.$ac_ext <<_ACEOF
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+
+done
+
+{ echo "$as_me:$LINENO: checking for perm_copy_fd in -lacl" >&5
+echo $ECHO_N "checking for perm_copy_fd in -lacl... $ECHO_C" >&6; }
+if test "${ac_cv_lib_acl_perm_copy_fd+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lacl  $LIBS"
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
 
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_SOCKET_H
-    # include <sys/socket.h>
-    #endif
-    #if HAVE_NETINET_IN_H
-    # include <netinet/in.h>
-    #endif
-
-
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char perm_copy_fd ();
 int
 main ()
 {
-static struct sockaddr_in ac_aggr;
-if (sizeof ac_aggr.sin_len)
-return 0;
+return perm_copy_fd ();
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34239,75 +36152,77 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_sockaddr_in_sin_len=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_lib_acl_perm_copy_fd=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_member_struct_sockaddr_in_sin_len=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+	ac_cv_lib_acl_perm_copy_fd=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_sockaddr_in_sin_len" >&5
-echo "${ECHO_T}$ac_cv_member_struct_sockaddr_in_sin_len" >&6; }
-if test $ac_cv_member_struct_sockaddr_in_sin_len = yes; then
+{ echo "$as_me:$LINENO: result: $ac_cv_lib_acl_perm_copy_fd" >&5
+echo "${ECHO_T}$ac_cv_lib_acl_perm_copy_fd" >&6; }
+if test $ac_cv_lib_acl_perm_copy_fd = yes; then
 
 cat >>confdefs.h <<\_ACEOF
-#define SIN_LEN 1
+#define HAVE_LIBACL 1
 _ACEOF
 
-fi
 
+fi
 
-{ echo "$as_me:$LINENO: checking for struct statfs.f_fstypename" >&5
-echo $ECHO_N "checking for struct statfs.f_fstypename... $ECHO_C" >&6; }
-if test "${ac_cv_member_struct_statfs_f_fstypename+set}" = set; then
+{ echo "$as_me:$LINENO: checking which POSIX ACL implementation to use" >&5
+echo $ECHO_N "checking which POSIX ACL implementation to use... $ECHO_C" >&6; }
+if test "${pr_cv_func_facl+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
-  cat >conftest.$ac_ext <<_ACEOF
+  pr_cv_func_facl="none"
+
+    if test "$pr_cv_func_facl" = "none"; then
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_PARAM_H
-    # include <sys/param.h>
-    #endif
-    #if HAVE_SYS_MOUNT_H
-    # include <sys/mount.h>
-    #endif
-    #if HAVE_SYS_VFS_H
-    # include <sys/vfs.h>
-    #endif
-
+ #include <sys/types.h>
+        #ifdef HAVE_SYS_ACL_H
+        # include <sys/acl.h>
+        #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
 
 int
 main ()
 {
-static struct statfs ac_aggr;
-if (ac_aggr.f_fstypename)
-return 0;
+ acl_permset_t permset;
+        /* On BSD, ACL_READ_DATA is a #define. */
+        #ifdef ACL_READ_DATA
+        acl_perm_t perm = ACL_READ_DATA;
+        #else
+        # error "ACL_READ_DATA not #defined on this platform"
+        #endif /* ACL_READ_DATA */
+        (void)acl_get_perm_np(permset, perm);
+
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34316,51 +36231,59 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_statfs_f_fstypename=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_facl="BSD"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	cat >conftest.$ac_ext <<_ACEOF
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+  fi
+
+    if test "$pr_cv_func_facl" = "none"; then
+    old_ldflags=$LDFLAGS
+    old_libs=$LIBS
+    new_ldflags=`echo "$LDFLAGS" | sed -e 's/-L\$(top_srcdir)\/lib//g'`
+    LDFLAGS="$new_ldflags"
+    LIBS="-lacl $LIBS"
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_PARAM_H
-    # include <sys/param.h>
-    #endif
-    #if HAVE_SYS_MOUNT_H
-    # include <sys/mount.h>
-    #endif
-    #if HAVE_SYS_VFS_H
-    # include <sys/vfs.h>
-    #endif
-
+ #include <sys/types.h>
+        #ifdef HAVE_SYS_ACL_H
+        # include <sys/acl.h>
+        #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
 
 int
 main ()
 {
-static struct statfs ac_aggr;
-if (sizeof ac_aggr.f_fstypename)
-return 0;
+ acl_permset_t permset;
+        acl_perm_t perm;
+        (void)acl_get_perm(permset, perm);
+
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34369,75 +36292,57 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_statfs_f_fstypename=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_facl="Linux"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_member_struct_statfs_f_fstypename=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_statfs_f_fstypename" >&5
-echo "${ECHO_T}$ac_cv_member_struct_statfs_f_fstypename" >&6; }
-if test $ac_cv_member_struct_statfs_f_fstypename = yes; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_STATFS_F_FSTYPENAME 1
-_ACEOF
 
 fi
 
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+    LDFLAGS=$old_ldflags
+    LIBS=$old_libs
+  fi
 
-{ echo "$as_me:$LINENO: checking for struct statfs.f_type" >&5
-echo $ECHO_N "checking for struct statfs.f_type... $ECHO_C" >&6; }
-if test "${ac_cv_member_struct_statfs_f_type+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+    if test "$pr_cv_func_facl" = "none"; then
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_PARAM_H
-    # include <sys/param.h>
-    #endif
-    #if HAVE_SYS_MOUNT_H
-    # include <sys/mount.h>
-    #endif
-    #if HAVE_SYS_VFS_H
-    # include <sys/vfs.h>
-    #endif
-
+ #include <sys/types.h>
+        #ifdef HAVE_SYS_ACL_H
+        # include <sys/acl.h>
+        #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
 
 int
 main ()
 {
-static struct statfs ac_aggr;
-if (ac_aggr.f_type)
-return 0;
+ acl_entry_t ae;
+        /* On Mac, ACL_READ_DATA is an enum value. */
+        acl_perm_t perm = ACL_READ_DATA;
+        (void)acl_get_qualifier(ae);
+
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34446,51 +36351,63 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_statfs_f_type=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_facl="MacOSX"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	cat >conftest.$ac_ext <<_ACEOF
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+  fi
+
+    if test "$pr_cv_func_facl" = "none"; then
+    old_ldflags=$LDFLAGS
+    LDFLAGS="-lsec $LDFLAGS"
+    cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #if HAVE_SYS_TYPES_H
-    # include <sys/types.h>
-    #endif
-    #if HAVE_SYS_PARAM_H
-    # include <sys/param.h>
-    #endif
-    #if HAVE_SYS_MOUNT_H
-    # include <sys/mount.h>
-    #endif
-    #if HAVE_SYS_VFS_H
-    # include <sys/vfs.h>
-    #endif
-
+ #ifdef HAVE_STDDEF_H
+        # include <stddef.h>
+        #endif
+        #ifdef HAVE_STDLIB_H
+        # include <stdlib.h>
+        #endif
+        #ifdef HAVE_SYS_TYPES_H
+        # include <sys/types.h>
+        #endif
+        #ifdef HAVE_SYS_ACL_H
+        # include <sys/acl.h>
+        #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
 
 int
 main ()
 {
-static struct statfs ac_aggr;
-if (sizeof ac_aggr.f_type)
-return 0;
+ aclent_t ae;
+        (void)aclcheck(&ae,0,NULL);
+
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34499,144 +36416,137 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_cv_member_struct_statfs_f_type=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_facl="Solaris"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_member_struct_statfs_f_type=no
-fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+    LDFLAGS=$old_ldflags
+  fi
+
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_member_struct_statfs_f_type" >&5
-echo "${ECHO_T}$ac_cv_member_struct_statfs_f_type" >&6; }
-if test $ac_cv_member_struct_statfs_f_type = yes; then
+{ echo "$as_me:$LINENO: result: $pr_cv_func_facl" >&5
+echo "${ECHO_T}$pr_cv_func_facl" >&6; }
+
+if test "$pr_cv_func_facl" != "none"; then
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_STATFS_F_TYPE 1
+#define HAVE_POSIX_ACL 1
 _ACEOF
 
-fi
 
+  case "$pr_cv_func_facl" in
+    "BSD")
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_BSD_POSIX_ACL 1
+_ACEOF
+
+      ;;
 
-if test x"$enable_largefile" = xno; then
+    "Linux")
 
 cat >>confdefs.h <<\_ACEOF
-#define PR_USE_LARGEFILES 0
+#define HAVE_LINUX_POSIX_ACL 1
 _ACEOF
 
-else
+      ;;
+
+    "MacOSX")
 
 cat >>confdefs.h <<\_ACEOF
-#define PR_USE_LARGEFILES 1
+#define HAVE_MACOSX_POSIX_ACL 1
 _ACEOF
 
-fi
+      ;;
 
-{ echo "$as_me:$LINENO: checking whether struct addrinfo is defined" >&5
-echo $ECHO_N "checking whether struct addrinfo is defined... $ECHO_C" >&6; }
- cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
+    "Solaris")
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LIBSEC 1
 _ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
- #include <stdio.h>
-   #ifdef HAVE_UNISTD_H
-   # include <unistd.h>
-   #endif
-   #include <sys/types.h>
-   #include <sys/socket.h>
-   #include <netdb.h>
 
-int
-main ()
-{
-do {
-   struct addrinfo a;
-   (void) a.ai_flags;
-  } while(0)
 
-  ;
-  return 0;
-}
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SOLARIS_POSIX_ACL 1
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
+
+      ;;
+  esac
+fi
+
+if test x"$enable_facl" = xyes ; then
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_STRUCT_ADDRINFO 1
+#define PR_USE_FACL 1
 _ACEOF
 
 
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
+  case "$pr_cv_func_facl" in
+    "Linux")
+      ac_build_addl_libs="-lacl $ac_build_addl_libs"
+      ;;
 
-	{ echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
+    "Solaris")
+      ac_build_addl_libs="-lsec $ac_build_addl_libs"
+      ;;
+  esac
+fi
+
+if test x"$enable_ipv6" != xno ; then
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_IPV6 1
+_ACEOF
 
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+if test x"$enable_sendfile" != xno ; then
+  { echo "$as_me:$LINENO: checking which sendfile() implementation to use" >&5
+echo $ECHO_N "checking which sendfile() implementation to use... $ECHO_C" >&6; }
+if test "${pr_cv_func_sendfile+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  pr_cv_func_sendfile="none"
 
-{ echo "$as_me:$LINENO: checking whether struct sockaddr_storage is defined" >&5
-echo $ECHO_N "checking whether struct sockaddr_storage is defined... $ECHO_C" >&6; }
- cat >conftest.$ac_ext <<_ACEOF
+        if test "$pr_cv_func_sendfile" = "none"; then
+      cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <stdio.h>
-   #ifdef HAVE_UNISTD_H
-   # include <unistd.h>
-   #endif
-   #include <sys/types.h>
-   #include <sys/socket.h>
-   #include <netdb.h>
+ #include <sys/types.h>
+          #include <sys/sendfile.h>
+          #include <unistd.h>
 
 int
 main ()
 {
-do {
-   struct sockaddr_storage ss;
-  } while(0)
+ int i;
+          off_t o;
+          size_t c;
+          (void)sendfile(i,i,&o,c);
 
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34645,62 +36555,52 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_STRUCT_SS 1
-_ACEOF
-
-
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_sendfile="Linux"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	{ echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
 
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+    fi
 
-  { echo "$as_me:$LINENO: checking whether ss_family is defined" >&5
-echo $ECHO_N "checking whether ss_family is defined... $ECHO_C" >&6; }
-  cat >conftest.$ac_ext <<_ACEOF
+        if test "$pr_cv_func_sendfile" = "none"; then
+      cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #include <stdio.h>
-    #include <unistd.h>
-    #include <sys/types.h>
-    #include <sys/socket.h>
+ #include <sys/types.h>
+          #include <sys/socket.h>
+          #include <sys/uio.h>
 
 int
 main ()
 {
-
-    do {
-     struct sockaddr_storage a;
-     (void) a.ss_family;
-    } while(0)
+ int i;
+          off_t o;
+          size_t n;
+          struct sf_hdtr h;
+          (void)sendfile(i,i,o,n,&h,&o,i);
 
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34709,58 +36609,50 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-
-    { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_SS_FAMILY 1
-_ACEOF
-
-
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_sendfile="BSD"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
-    { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-    { echo "$as_me:$LINENO: checking whether __ss_family is defined" >&5
-echo $ECHO_N "checking whether __ss_family is defined... $ECHO_C" >&6; }
-    cat >conftest.$ac_ext <<_ACEOF
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+    fi
+
+        if test "$pr_cv_func_sendfile" = "none"; then
+      cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-      #include <stdio.h>
-      #include <unistd.h>
-      #include <sys/types.h>
-      #include <sys/socket.h>
+ #include <sys/types.h>
+          #include <sys/socket.h>
 
 int
 main ()
 {
-
-      do {
-       struct sockaddr_storage a;
-       (void) a.__ss_family;
-      } while(0)
+ uint f;
+          int h;
+          struct sf_parms p;
+          (void)send_file(&(h),&(p),f);
 
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34769,68 +36661,53 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-
-      { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE___SS_FAMILY 1
-_ACEOF
-
-
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_sendfile="AIX"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
-      { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+    fi
 
-  { echo "$as_me:$LINENO: checking whether ss_len is defined" >&5
-echo $ECHO_N "checking whether ss_len is defined... $ECHO_C" >&6; }
-  cat >conftest.$ac_ext <<_ACEOF
+        if test "$pr_cv_func_sendfile" = "none"; then
+      old_ldflags=$LDFLAGS
+      LDFLAGS="-lsendfile $LDFLAGS"
+      cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-    #include <stdio.h>
-    #include <unistd.h>
-    #include <sys/types.h>
-    #include <sys/socket.h>
+ #include <sys/types.h>
+          #include <sys/sendfile.h>
+          #include <unistd.h>
 
 int
 main ()
 {
-
-    do {
-     struct sockaddr_storage a;
-     (void) a.ss_len;
-    } while(0)
+ int i;
+          off_t o;
+          size_t c;
+          (void)sendfile(i,i,&o,c);
 
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34839,58 +36716,52 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-
-    { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_SS_LEN 1
-_ACEOF
-
-
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_sendfile="Solaris"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
-    { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-    { echo "$as_me:$LINENO: checking whether __ss_len is defined" >&5
-echo $ECHO_N "checking whether __ss_len is defined... $ECHO_C" >&6; }
-    cat >conftest.$ac_ext <<_ACEOF
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+      LDFLAGS=$old_ldflags
+    fi
+
+        if test "$pr_cv_func_sendfile" = "none"; then
+      cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-      #include <stdio.h>
-      #include <unistd.h>
-      #include <sys/types.h>
-      #include <sys/socket.h>
+ #include <sys/types.h>
+          #include <sys/socket.h>
+          #include <sys/uio.h>
 
 int
 main ()
 {
-
-      do {
-       struct sockaddr_storage a;
-       (void) a.__ss_len;
-      } while(0)
+ int i;
+          off_t o, n;
+          struct sf_hdtr h;
+          (void)sendfile(i,i,o,&n,&h,i);
 
   ;
   return 0;
 }
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -34899,36 +36770,44 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-
-      { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE___SS_LEN 1
-_ACEOF
-
-
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pr_cv_func_sendfile="MacOSX"
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
-      { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+    fi
 
 fi
+{ echo "$as_me:$LINENO: result: $pr_cv_func_sendfile" >&5
+echo "${ECHO_T}$pr_cv_func_sendfile" >&6; }
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
 
+  if test x"$pr_cv_func_sendfile" != x"none"; then
+            if test x"$pr_cv_func_sendfile" != x"AIX"; then
 
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SENDFILE 1
+_ACEOF
 
 
-for ac_header in sys/acl.h acl/libacl.h
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_SENDFILE 1
+_ACEOF
+
+    fi
+  fi
+
+  case "$pr_cv_func_sendfile" in
+    "Linux")
+
+for ac_header in sys/sendfile.h
 do
 as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
 if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
@@ -35067,43 +36946,61 @@ fi
 
 done
 
-{ echo "$as_me:$LINENO: checking which POSIX ACL implementation to use" >&5
-echo $ECHO_N "checking which POSIX ACL implementation to use... $ECHO_C" >&6; }
-if test "${pr_cv_func_facl+set}" = set; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LINUX_SENDFILE 1
+_ACEOF
+
+      ;;
+
+    "BSD")
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_BSD_SENDFILE 1
+_ACEOF
+
+      ;;
+
+    "AIX")
+      { echo "$as_me:$LINENO: WARNING: ** AIX sendfile support automatically disabled **" >&5
+echo "$as_me: WARNING: ** AIX sendfile support automatically disabled **" >&2;}
+      ;;
+
+    "Solaris")
+
+for ac_header in sys/sendfile.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
 else
-  pr_cv_func_facl="none"
-
-    if test "$pr_cv_func_facl" = "none"; then
-    cat >conftest.$ac_ext <<_ACEOF
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-        #ifdef HAVE_SYS_ACL_H
-        # include <sys/acl.h>
-        #endif
-
-int
-main ()
-{
- acl_entry_t ae;
-        (void)acl_get_qualifier(ae);
-
-  ;
-  return 0;
-}
+$ac_includes_default
+#include <$ac_header>
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -35112,107 +37009,161 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  pr_cv_func_facl="BSD"
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-
+	ac_header_compiler=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-  fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
 
-    if test "$pr_cv_func_facl" = "none"; then
-    old_ldflags=$LDFLAGS
-    LDFLAGS="-lacl $LDFLAGS"
-    cat >conftest.$ac_ext <<_ACEOF
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-        #ifdef HAVE_SYS_ACL_H
-        # include <sys/acl.h>
-        #endif
-
-int
-main ()
-{
- acl_entry_t ae;
-        (void)acl_get_qualifier(ae);
-
-  ;
-  return 0;
-}
+#include <$ac_header>
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+if { (ac_try="$ac_cpp conftest.$ac_ext"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
   cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  pr_cv_func_facl="Linux"
+       }; then
+  ac_header_preproc=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
 
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
 fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-    LDFLAGS=$old_ldflags
-  fi
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
 
-    if test "$pr_cv_func_facl" = "none"; then
-    old_ldflags=$LDFLAGS
-    LDFLAGS="-lsec $LDFLAGS"
-    cat >conftest.$ac_ext <<_ACEOF
+fi
+
+done
+
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SOLARIS_SENDFILE 1
+_ACEOF
+
+      ac_build_addl_libs="-lsendfile $ac_build_addl_libs"
+      ;;
+
+    "MacOSX")
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_MACOSX_SENDFILE 1
+_ACEOF
+
+      ;;
+  esac
+fi
+
+if test x"$enable_trace" != xno ; then
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_TRACE 1
+_ACEOF
+
+fi
+
+if test x"$enable_xattr" != xno ; then
+  # On Free/Net/OpenBSD, it's sys/extattr.h
+  if test "${ac_cv_header_sys_extattr_h+set}" = set; then
+  { echo "$as_me:$LINENO: checking for sys/extattr.h" >&5
+echo $ECHO_N "checking for sys/extattr.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_sys_extattr_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_sys_extattr_h" >&5
+echo "${ECHO_T}$ac_cv_header_sys_extattr_h" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking sys/extattr.h usability" >&5
+echo $ECHO_N "checking sys/extattr.h usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-        #ifdef HAVE_SYS_ACL_H
-        # include <sys/acl.h>
-        #endif
-
-int
-main ()
-{
- aclent_t ae;
-        (void)aclcheck(&ae,0,NULL);
-
-  ;
-  return 0;
-}
+$ac_includes_default
+#include <sys/extattr.h>
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -35221,77 +37172,136 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  pr_cv_func_facl="Solaris"
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-    LDFLAGS=$old_ldflags
-  fi
-
+	ac_header_compiler=no
 fi
-{ echo "$as_me:$LINENO: result: $pr_cv_func_facl" >&5
-echo "${ECHO_T}$pr_cv_func_facl" >&6; }
 
-if test "$pr_cv_func_facl" != "none"; then
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_POSIX_ACL 1
+# Is the header present?
+{ echo "$as_me:$LINENO: checking sys/extattr.h presence" >&5
+echo $ECHO_N "checking sys/extattr.h presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <sys/extattr.h>
 _ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
+  ac_header_preproc=no
+fi
 
-  case "$pr_cv_func_facl" in
-    "BSD")
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_BSD_POSIX_ACL 1
-_ACEOF
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: sys/extattr.h: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: sys/extattr.h: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h: present but cannot be compiled" >&5
+echo "$as_me: WARNING: sys/extattr.h: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: sys/extattr.h:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: sys/extattr.h: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: sys/extattr.h:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: sys/extattr.h: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/extattr.h: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: sys/extattr.h: in the future, the compiler will take precedence" >&2;}
 
-      ;;
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for sys/extattr.h" >&5
+echo $ECHO_N "checking for sys/extattr.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_sys_extattr_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_cv_header_sys_extattr_h=$ac_header_preproc
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_sys_extattr_h" >&5
+echo "${ECHO_T}$ac_cv_header_sys_extattr_h" >&6; }
 
-    "Linux")
+fi
+if test $ac_cv_header_sys_extattr_h = yes; then
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_LIBACL 1
+#define HAVE_SYS_EXTATTR_H 1
 _ACEOF
 
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_LINUX_POSIX_ACL 1
+#define PR_USE_XATTR 1
 _ACEOF
 
-      { echo "$as_me:$LINENO: checking for perm_copy_fd in -lacl" >&5
-echo $ECHO_N "checking for perm_copy_fd in -lacl... $ECHO_C" >&6; }
-if test "${ac_cv_lib_acl_perm_copy_fd+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-lacl  $LIBS"
-cat >conftest.$ac_ext <<_ACEOF
+
+     { echo "$as_me:$LINENO: checking for extattr_delete_link" >&5
+echo $ECHO_N "checking for extattr_delete_link... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char perm_copy_fd ();
 int
 main ()
 {
-return perm_copy_fd ();
+
+         int res;
+         int namespace = 0;
+         const char *path = NULL, name = NULL;
+         res = extattr_delete_link(path, namespace, name);
+
   ;
   return 0;
 }
@@ -35314,97 +37324,60 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_lib_acl_perm_copy_fd=yes
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-	ac_cv_lib_acl_perm_copy_fd=no
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-{ echo "$as_me:$LINENO: result: $ac_cv_lib_acl_perm_copy_fd" >&5
-echo "${ECHO_T}$ac_cv_lib_acl_perm_copy_fd" >&6; }
-if test $ac_cv_lib_acl_perm_copy_fd = yes; then
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_PERM_COPY_FD 1
-_ACEOF
-
-fi
-
-      ;;
-
-    "Solaris")
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_LIBSEC 1
-_ACEOF
-
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_SOLARIS_POSIX_ACL 1
-_ACEOF
-
-      ;;
-  esac
-fi
 
-if test x"$enable_facl" = xyes ; then
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define PR_USE_FACL 1
+#define HAVE_EXTATTR_DELETE_LINK 1
 _ACEOF
 
 
-  case "$pr_cv_func_facl" in
-    "Linux")
-      ac_build_addl_libs="-lacl $ac_build_addl_libs"
-      ;;
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-    "Solaris")
-      ac_build_addl_libs="-lsec $ac_build_addl_libs"
-      ;;
-  esac
-fi
 
-if test x"$enable_ipv6" != xno ; then
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
-cat >>confdefs.h <<\_ACEOF
-#define PR_USE_IPV6 1
-_ACEOF
 
 fi
 
-if test x"$enable_sendfile" != xno ; then
-  { echo "$as_me:$LINENO: checking which sendfile() implementation to use" >&5
-echo $ECHO_N "checking which sendfile() implementation to use... $ECHO_C" >&6; }
-if test "${pr_cv_func_sendfile+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  pr_cv_func_sendfile="none"
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
-        if test "$pr_cv_func_sendfile" = "none"; then
-      cat >conftest.$ac_ext <<_ACEOF
+     { echo "$as_me:$LINENO: checking for extattr_get_link" >&5
+echo $ECHO_N "checking for extattr_get_link... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-          #include <sys/sendfile.h>
-          #include <unistd.h>
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
 
 int
 main ()
 {
- int i;
-          off_t o;
-          size_t c;
-          (void)sendfile(i,i,&o,c);
+
+         ssize_t res;
+         int namespace = 0;
+         const char *path = NULL, name = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = extattr_get_link(path, namespace, name, val, sz);
 
   ;
   return 0;
@@ -35428,37 +37401,60 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  pr_cv_func_sendfile="Linux"
+
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_EXTATTR_GET_LINK 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
-    fi
 
-        if test "$pr_cv_func_sendfile" = "none"; then
-      cat >conftest.$ac_ext <<_ACEOF
+     { echo "$as_me:$LINENO: checking for extattr_list_link" >&5
+echo $ECHO_N "checking for extattr_list_link... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-          #include <sys/socket.h>
-          #include <sys/uio.h>
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
 
 int
 main ()
 {
- int i;
-          off_t o;
-          size_t n;
-          struct sf_hdtr h;
-          (void)sendfile(i,i,o,n,&h,&o,i);
+
+         ssize_t res;
+         int namespace = 0;
+         const char *path = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = extattr_list_link(path, namespace, val, sz);
 
   ;
   return 0;
@@ -35482,35 +37478,60 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  pr_cv_func_sendfile="BSD"
+
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_EXTATTR_LIST_LINK 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
-    fi
 
-        if test "$pr_cv_func_sendfile" = "none"; then
-      cat >conftest.$ac_ext <<_ACEOF
+     { echo "$as_me:$LINENO: checking for extattr_set_link" >&5
+echo $ECHO_N "checking for extattr_set_link... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-          #include <sys/socket.h>
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
 
 int
 main ()
 {
- uint f;
-          int h;
-          struct sf_parms p;
-          (void)send_file(&(h),&(p),f);
+
+         int res;
+         int namespace = 0;
+         const char *path = NULL, name = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = extattr_set_link(path, namespace, name, val, sz);
 
   ;
   return 0;
@@ -35534,51 +37555,63 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  pr_cv_func_sendfile="AIX"
+
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_EXTATTR_SET_LINK 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
-    fi
 
-        if test "$pr_cv_func_sendfile" = "none"; then
-      old_ldflags=$LDFLAGS
-      LDFLAGS="-lsendfile $LDFLAGS"
-      cat >conftest.$ac_ext <<_ACEOF
+fi
+
+
+
+  # On Linux/MacOSX, it's sys/xattr.h
+  if test "${ac_cv_header_sys_xattr_h+set}" = set; then
+  { echo "$as_me:$LINENO: checking for sys/xattr.h" >&5
+echo $ECHO_N "checking for sys/xattr.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_sys_xattr_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_sys_xattr_h" >&5
+echo "${ECHO_T}$ac_cv_header_sys_xattr_h" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking sys/xattr.h usability" >&5
+echo $ECHO_N "checking sys/xattr.h usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-          #include <sys/sendfile.h>
-          #include <unistd.h>
-
-int
-main ()
-{
- int i;
-          off_t o;
-          size_t c;
-          (void)sendfile(i,i,&o,c);
-
-  ;
-  return 0;
-}
+$ac_includes_default
+#include <sys/xattr.h>
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_compile") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -35587,98 +37620,107 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  pr_cv_func_sendfile="Solaris"
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-
+	ac_header_compiler=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-      LDFLAGS=$old_ldflags
-    fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
 
-        if test "$pr_cv_func_sendfile" = "none"; then
-      cat >conftest.$ac_ext <<_ACEOF
+# Is the header present?
+{ echo "$as_me:$LINENO: checking sys/xattr.h presence" >&5
+echo $ECHO_N "checking sys/xattr.h presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
- #include <sys/types.h>
-          #include <sys/socket.h>
-          #include <sys/uio.h>
-
-int
-main ()
-{
- int i;
-          off_t o, n;
-          struct sf_hdtr h;
-          (void)sendfile(i,i,o,&n,&h,i);
-
-  ;
-  return 0;
-}
+#include <sys/xattr.h>
 _ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
+if { (ac_try="$ac_cpp conftest.$ac_ext"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
   cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-  pr_cv_func_sendfile="Mac OSX"
+       }; then
+  ac_header_preproc=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-
+  ac_header_preproc=no
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-    fi
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
 
-fi
-{ echo "$as_me:$LINENO: result: $pr_cv_func_sendfile" >&5
-echo "${ECHO_T}$pr_cv_func_sendfile" >&6; }
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: sys/xattr.h: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: sys/xattr.h: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h: present but cannot be compiled" >&5
+echo "$as_me: WARNING: sys/xattr.h: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: sys/xattr.h:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: sys/xattr.h: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: sys/xattr.h:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: sys/xattr.h: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sys/xattr.h: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: sys/xattr.h: in the future, the compiler will take precedence" >&2;}
 
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for sys/xattr.h" >&5
+echo $ECHO_N "checking for sys/xattr.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_sys_xattr_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_cv_header_sys_xattr_h=$ac_header_preproc
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_sys_xattr_h" >&5
+echo "${ECHO_T}$ac_cv_header_sys_xattr_h" >&6; }
 
-  if test x"$pr_cv_func_sendfile" != x"none"; then
-            if test x"$pr_cv_func_sendfile" != x"AIX"; then
+fi
+if test $ac_cv_header_sys_xattr_h = yes; then
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_SENDFILE 1
+#define HAVE_SYS_XATTR_H 1
 _ACEOF
 
 
 cat >>confdefs.h <<\_ACEOF
-#define PR_USE_SENDFILE 1
+#define PR_USE_XATTR 1
 _ACEOF
 
-    fi
-  fi
-
-  case "$pr_cv_func_sendfile" in
-    "Linux")
 
-for ac_header in sys/sendfile.h
+for ac_header in attr/xattr.h
 do
 as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
 if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
@@ -35818,60 +37860,122 @@ fi
 done
 
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_LINUX_SENDFILE 1
+     # Some platforms need libattr for extended attributes
+
+{ echo "$as_me:$LINENO: checking for setxattr in -lattr" >&5
+echo $ECHO_N "checking for setxattr in -lattr... $ECHO_C" >&6; }
+if test "${ac_cv_lib_attr_setxattr+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lattr  $LIBS"
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
 _ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
 
-      ;;
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char setxattr ();
+int
+main ()
+{
+return setxattr ();
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_lib_attr_setxattr=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-    "BSD")
+	ac_cv_lib_attr_setxattr=no
+fi
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_BSD_SENDFILE 1
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_lib_attr_setxattr" >&5
+echo "${ECHO_T}$ac_cv_lib_attr_setxattr" >&6; }
+if test $ac_cv_lib_attr_setxattr = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_LIBATTR 1
 _ACEOF
 
-      ;;
+  LIBS="-lattr $LIBS"
 
-    "AIX")
-      { echo "$as_me:$LINENO: WARNING: ** AIX sendfile support automatically disabled **" >&5
-echo "$as_me: WARNING: ** AIX sendfile support automatically disabled **" >&2;}
-      ;;
+fi
 
-    "Solaris")
 
-for ac_header in sys/sendfile.h
-do
-as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  { echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-else
-  # Is the header compilable?
-{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
-echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+     { echo "$as_me:$LINENO: checking for lgetxattr" >&5
+echo $ECHO_N "checking for lgetxattr... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-$ac_includes_default
-#include <$ac_header>
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
+
+int
+main ()
+{
+
+         ssize_t res;
+         const char *path = NULL, *name = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = lgetxattr(path, name, val, sz);
+
+  ;
+  return 0;
+}
 _ACEOF
-rm -f conftest.$ac_objext
-if { (ac_try="$ac_compile"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_compile") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
@@ -35880,127 +37984,258 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
   (exit $ac_status); } && {
 	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       } && test -s conftest.$ac_objext; then
-  ac_header_compiler=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LGETXATTR 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_header_compiler=no
+
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
 
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
-echo "${ECHO_T}$ac_header_compiler" >&6; }
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
-# Is the header present?
-{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
-echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
+     { echo "$as_me:$LINENO: checking for llistxattr" >&5
+echo $ECHO_N "checking for llistxattr... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-#include <$ac_header>
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
+
+int
+main ()
+{
+
+         ssize_t res;
+         const char *path = NULL;
+         char *names = NUL;
+         size_t namessz = 0;
+         res = llistxattr(path, names, namessz);
+
+  ;
+  return 0;
+}
 _ACEOF
-if { (ac_try="$ac_cpp conftest.$ac_ext"
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
 case "(($ac_try" in
   *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
   *) ac_try_echo=$ac_try;;
 esac
 eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  (eval "$ac_link") 2>conftest.er1
   ac_status=$?
   grep -v '^ *+' conftest.er1 >conftest.err
   rm -f conftest.er1
   cat conftest.err >&5
   echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } >/dev/null && {
-	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
 	 test ! -s conftest.err
-       }; then
-  ac_header_preproc=yes
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LLISTXATTR 1
+_ACEOF
+
+
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-  ac_header_preproc=no
+
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
 
-rm -f conftest.err conftest.$ac_ext
-{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
-echo "${ECHO_T}$ac_header_preproc" >&6; }
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
-# So?  What about this header?
-case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
-  yes:no: )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
-echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
-    ac_header_preproc=yes
-    ;;
-  no:yes:* )
-    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
-echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
-echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
-echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
-echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
-echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
-    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
-echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+     { echo "$as_me:$LINENO: checking for lremovexattr" >&5
+echo $ECHO_N "checking for lremovexattr... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
 
-    ;;
+int
+main ()
+{
+
+         ssize_t res;
+         const char *path = NULL, *name = NULL;
+         res = lremovexattr(path, name);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
 esac
-{ echo "$as_me:$LINENO: checking for $ac_header" >&5
-echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
-if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_LREMOVEXATTR 1
+_ACEOF
+
+
 else
-  eval "$as_ac_Header=\$ac_header_preproc"
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
 fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
 
-fi
-if test `eval echo '${'$as_ac_Header'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+     { echo "$as_me:$LINENO: checking for lsetxattr" >&5
+echo $ECHO_N "checking for lsetxattr... $ECHO_C" >&6; }
+     cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
 _ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
 
-fi
+int
+main ()
+{
 
-done
+         int res, flags = 0;
+         const char *path = NULL, *name = NULL;
+         const void *val = NULL;
+         res = lsetxattr(path, name, val, flags);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
 
+         { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_SOLARIS_SENDFILE 1
+#define HAVE_LSETXATTR 1
 _ACEOF
 
-      ac_build_addl_libs="-lsendfile $ac_build_addl_libs"
-      ;;
 
-    "Mac OSX")
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+         { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_MACOSX_SENDFILE 1
-_ACEOF
 
-      ;;
-  esac
 fi
 
-if test x"$enable_trace" != xno ; then
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+fi
 
-cat >>confdefs.h <<\_ACEOF
-#define PR_USE_TRACE 1
-_ACEOF
 
 fi
 
@@ -36697,29 +38932,495 @@ echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
 if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
-  eval "$as_ac_Header=\$ac_header_preproc"
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+
+done
+
+{ echo "$as_me:$LINENO: checking for setproctitle in -lutil" >&5
+echo $ECHO_N "checking for setproctitle in -lutil... $ECHO_C" >&6; }
+if test "${ac_cv_lib_util_setproctitle+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lutil  $LIBS"
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char setproctitle ();
+int
+main ()
+{
+return setproctitle ();
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  ac_cv_lib_util_setproctitle=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_cv_lib_util_setproctitle=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_lib_util_setproctitle" >&5
+echo "${ECHO_T}$ac_cv_lib_util_setproctitle" >&6; }
+if test $ac_cv_lib_util_setproctitle = yes; then
+  cat >>confdefs.h <<\_ACEOF
+#define HAVE_SETPROCTITLE 1
+_ACEOF
+
+		ac_cv_func_setproctitle="yes" ; LIBS="$LIBS -lutil"
+fi
+
+
+if test "$ac_cv_func_setproctitle" = "yes"; then
+  cat >>confdefs.h <<\_ACEOF
+#define PF_ARGV_TYPE PF_ARGV_NONE
+_ACEOF
+
+else
+  pf_argv_set="no"
+
+
+for ac_header in sys/pstat.h
+do
+as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  { echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking $ac_header usability" >&5
+echo $ECHO_N "checking $ac_header usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <$ac_header>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_header_compiler=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking $ac_header presence" >&5
+echo $ECHO_N "checking $ac_header presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <$ac_header>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: $ac_header: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: $ac_header: present but cannot be compiled" >&5
+echo "$as_me: WARNING: $ac_header: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: $ac_header:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: $ac_header: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: $ac_header:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: $ac_header: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: $ac_header: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: $ac_header: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for $ac_header" >&5
+echo $ECHO_N "checking for $ac_header... $ECHO_C" >&6; }
+if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  eval "$as_ac_Header=\$ac_header_preproc"
+fi
+ac_res=`eval echo '${'$as_ac_Header'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+
+fi
+if test `eval echo '${'$as_ac_Header'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+ have_pstat_h="yes"
+else
+  have_pstat_h="no"
+fi
+
+done
+
+  if test "$have_pstat_h" = "yes"; then
+
+for ac_func in pstat
+do
+as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
+{ echo "$as_me:$LINENO: checking for $ac_func" >&5
+echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
+if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $ac_func innocuous_$ac_func
+
+/* System header to define __stub macros and hopefully few prototypes,
+    which can conflict with char $ac_func (); below.
+    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
+    <limits.h> exists even on freestanding compilers.  */
+
+#ifdef __STDC__
+# include <limits.h>
+#else
+# include <assert.h>
+#endif
+
+#undef $ac_func
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $ac_func ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$ac_func || defined __stub___$ac_func
+choke me
+#endif
+
+int
+main ()
+{
+return $ac_func ();
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  eval "$as_ac_var=yes"
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	eval "$as_ac_var=no"
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+fi
+ac_res=`eval echo '${'$as_ac_var'}'`
+	       { echo "$as_me:$LINENO: result: $ac_res" >&5
+echo "${ECHO_T}$ac_res" >&6; }
+if test `eval echo '${'$as_ac_var'}'` = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+done
+
+
+    if test "$ac_cv_func_pstat" = "yes"; then
+	cat >>confdefs.h <<\_ACEOF
+#define PF_ARGV_TYPE PF_ARGV_PSTAT
+_ACEOF
+
+    else
+	cat >>confdefs.h <<\_ACEOF
+#define PF_ARGV_TYPE PF_ARGV_WRITEABLE
+_ACEOF
+
+    fi
+
+    pf_argv_set="yes"
+  fi
+
+  if test "$pf_argv_set" = "no"; then
+    cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <sys/exec.h>
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "#define.*PS_STRINGS.*" >/dev/null 2>&1; then
+  have_psstrings="yes"
+else
+  have_psstrings="no"
+fi
+rm -f conftest*
+
+    if test "$have_psstrings" = "yes"; then
+	cat >>confdefs.h <<\_ACEOF
+#define PF_ARGV_TYPE PF_ARGV_PSSTRINGS
+_ACEOF
+
+	pf_argv_set="yes"
+    fi
+  fi
+
+  if test "$pf_argv_set" = "no"; then
+    { echo "$as_me:$LINENO: checking whether __progname and __progname_full are available" >&5
+echo $ECHO_N "checking whether __progname and __progname_full are available... $ECHO_C" >&6; }
+if test "${pf_cv_var_progname+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+extern char *__progname, *__progname_full;
+int
+main ()
+{
+__progname = "foo"; __progname_full = "foo bar";
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+  pf_cv_var_progname="yes"
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	pf_cv_var_progname="no"
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+fi
+{ echo "$as_me:$LINENO: result: $pf_cv_var_progname" >&5
+echo "${ECHO_T}$pf_cv_var_progname" >&6; }
+
+    if test "$pf_cv_var_progname" = "yes"; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE___PROGNAME 1
+_ACEOF
+
+    fi
+
+    { echo "$as_me:$LINENO: checking which argv replacement method to use" >&5
+echo $ECHO_N "checking which argv replacement method to use... $ECHO_C" >&6; }
+if test "${pf_cv_argv_type+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+#if defined(__GNU_HURD__)
+  yes
+#endif
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "yes" >/dev/null 2>&1; then
+  pf_cv_argv_type="new"
+else
+  pf_cv_argv_type="writeable"
 fi
-ac_res=`eval echo '${'$as_ac_Header'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
+rm -f conftest*
 
 fi
-if test `eval echo '${'$as_ac_Header'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
+{ echo "$as_me:$LINENO: result: $pf_cv_argv_type" >&5
+echo "${ECHO_T}$pf_cv_argv_type" >&6; }
+
+    if test "$pf_cv_argv_type" = "new"; then
+	cat >>confdefs.h <<\_ACEOF
+#define PF_ARGV_TYPE PF_ARGV_NEW
+_ACEOF
+
+	pf_argv_set="yes"
+    fi
+
+    if test "$pf_argv_set" = "no"; then
+	cat >>confdefs.h <<\_ACEOF
+#define PF_ARGV_TYPE PF_ARGV_WRITEABLE
 _ACEOF
 
+    fi
+  fi
 fi
 
-done
 
-{ echo "$as_me:$LINENO: checking for setproctitle in -lutil" >&5
-echo $ECHO_N "checking for setproctitle in -lutil... $ECHO_C" >&6; }
-if test "${ac_cv_lib_util_setproctitle+set}" = set; then
+{ echo "$as_me:$LINENO: checking for backtrace in -lexecinfo" >&5
+echo $ECHO_N "checking for backtrace in -lexecinfo... $ECHO_C" >&6; }
+if test "${ac_cv_lib_execinfo_backtrace+set}" = set; then
   echo $ECHO_N "(cached) $ECHO_C" >&6
 else
   ac_check_lib_save_LIBS=$LIBS
-LIBS="-lutil  $LIBS"
+LIBS="-lexecinfo  $LIBS"
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -36733,11 +39434,11 @@ cat >>conftest.$ac_ext <<_ACEOF
 #ifdef __cplusplus
 extern "C"
 #endif
-char setproctitle ();
+char backtrace ();
 int
 main ()
 {
-return setproctitle ();
+return backtrace ();
   ;
   return 0;
 }
@@ -36760,39 +39461,33 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  ac_cv_lib_util_setproctitle=yes
+  ac_cv_lib_execinfo_backtrace=yes
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
-	ac_cv_lib_util_setproctitle=no
+	ac_cv_lib_execinfo_backtrace=no
 fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 LIBS=$ac_check_lib_save_LIBS
 fi
-{ echo "$as_me:$LINENO: result: $ac_cv_lib_util_setproctitle" >&5
-echo "${ECHO_T}$ac_cv_lib_util_setproctitle" >&6; }
-if test $ac_cv_lib_util_setproctitle = yes; then
-  cat >>confdefs.h <<\_ACEOF
-#define HAVE_SETPROCTITLE 1
+{ echo "$as_me:$LINENO: result: $ac_cv_lib_execinfo_backtrace" >&5
+echo "${ECHO_T}$ac_cv_lib_execinfo_backtrace" >&6; }
+if test $ac_cv_lib_execinfo_backtrace = yes; then
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_LIBEXECINFO 1
 _ACEOF
 
-		ac_cv_func_setproctitle="yes" ; LIBS="$LIBS -lutil"
-fi
+  LIBS="-lexecinfo $LIBS"
 
+fi
 
-if test "$ac_cv_func_setproctitle" = "yes"; then
-  cat >>confdefs.h <<\_ACEOF
-#define PF_ARGV_TYPE PF_ARGV_NONE
-_ACEOF
 
-else
-  pf_argv_set="no"
 
 
-for ac_header in sys/pstat.h
+for ac_header in execinfo.h ucontext.h
 do
 as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
 if { as_var=$as_ac_Header; eval "test \"\${$as_var+set}\" = set"; }; then
@@ -36926,64 +39621,42 @@ if test `eval echo '${'$as_ac_Header'}'` = yes; then
   cat >>confdefs.h <<_ACEOF
 #define `echo "HAVE_$ac_header" | $as_tr_cpp` 1
 _ACEOF
- have_pstat_h="yes"
-else
-  have_pstat_h="no"
+
 fi
 
 done
 
-  if test "$have_pstat_h" = "yes"; then
 
-for ac_func in pstat
-do
-as_ac_var=`echo "ac_cv_func_$ac_func" | $as_tr_sh`
-{ echo "$as_me:$LINENO: checking for $ac_func" >&5
-echo $ECHO_N "checking for $ac_func... $ECHO_C" >&6; }
-if { as_var=$as_ac_var; eval "test \"\${$as_var+set}\" = set"; }; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+{ echo "$as_me:$LINENO: checking for backtrace" >&5
+echo $ECHO_N "checking for backtrace... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-/* Define $ac_func to an innocuous variant, in case <limits.h> declares $ac_func.
-   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
-#define $ac_func innocuous_$ac_func
-
-/* System header to define __stub macros and hopefully few prototypes,
-    which can conflict with char $ac_func (); below.
-    Prefer <limits.h> to <assert.h> if __STDC__ is defined, since
-    <limits.h> exists even on freestanding compilers.  */
-
-#ifdef __STDC__
-# include <limits.h>
-#else
-# include <assert.h>
-#endif
 
-#undef $ac_func
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $ac_func ();
-/* The GNU C library defines this for functions which it implements
-    to always fail with ENOSYS.  Some functions are actually named
-    something starting with __ and the normal name is an alias.  */
-#if defined __stub_$ac_func || defined __stub___$ac_func
-choke me
-#endif
+    #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_EXECINFO_H
+    # include <execinfo.h>
+    #endif
+    #ifdef HAVE_UCONTEXT_H
+    # include <ucontext.h>
+    #endif
 
 int
 main ()
 {
-return $ac_func ();
+
+    void **syms = NULL;
+    int res, nsyms = 0;
+    res = backtrace(syms, nsyms);
+
   ;
   return 0;
 }
@@ -37006,88 +39679,60 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  eval "$as_ac_var=yes"
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
 
-	eval "$as_ac_var=no"
-fi
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-fi
-ac_res=`eval echo '${'$as_ac_var'}'`
-	       { echo "$as_me:$LINENO: result: $ac_res" >&5
-echo "${ECHO_T}$ac_res" >&6; }
-if test `eval echo '${'$as_ac_var'}'` = yes; then
-  cat >>confdefs.h <<_ACEOF
-#define `echo "HAVE_$ac_func" | $as_tr_cpp` 1
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_BACKTRACE 1
 _ACEOF
 
-fi
-done
-
 
-    if test "$ac_cv_func_pstat" = "yes"; then
-	cat >>confdefs.h <<\_ACEOF
-#define PF_ARGV_TYPE PF_ARGV_PSTAT
-_ACEOF
-
-    else
-	cat >>confdefs.h <<\_ACEOF
-#define PF_ARGV_TYPE PF_ARGV_WRITEABLE
-_ACEOF
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-    fi
 
-    pf_argv_set="yes"
-  fi
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
-  if test "$pf_argv_set" = "no"; then
-    cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
-#include <sys/exec.h>
 
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "#define.*PS_STRINGS.*" >/dev/null 2>&1; then
-  have_psstrings="yes"
-else
-  have_psstrings="no"
 fi
-rm -f conftest*
 
-    if test "$have_psstrings" = "yes"; then
-	cat >>confdefs.h <<\_ACEOF
-#define PF_ARGV_TYPE PF_ARGV_PSSTRINGS
-_ACEOF
-
-	pf_argv_set="yes"
-    fi
-  fi
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
-  if test "$pf_argv_set" = "no"; then
-    { echo "$as_me:$LINENO: checking whether __progname and __progname_full are available" >&5
-echo $ECHO_N "checking whether __progname and __progname_full are available... $ECHO_C" >&6; }
-if test "${pf_cv_var_progname+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
+{ echo "$as_me:$LINENO: checking for backtrace_symbols" >&5
+echo $ECHO_N "checking for backtrace_symbols... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-extern char *__progname, *__progname_full;
+
+    #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_EXECINFO_H
+    # include <execinfo.h>
+    #endif
+    #ifdef HAVE_UCONTEXT_H
+    # include <ucontext.h>
+    #endif
+
 int
 main ()
 {
-__progname = "foo"; __progname_full = "foo bar";
+
+    void **syms = NULL;
+    int nsyms = 0;
+    char **res;
+    res = backtrace_symbols(syms, nsyms);
+
   ;
   return 0;
 }
@@ -37110,74 +39755,29 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 	 test ! -s conftest.err
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
-  pf_cv_var_progname="yes"
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-	pf_cv_var_progname="no"
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-fi
-{ echo "$as_me:$LINENO: result: $pf_cv_var_progname" >&5
-echo "${ECHO_T}$pf_cv_var_progname" >&6; }
 
-    if test "$pf_cv_var_progname" = "yes"; then
+    { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE___PROGNAME 1
-_ACEOF
-
-    fi
-
-    { echo "$as_me:$LINENO: checking which argv replacement method to use" >&5
-echo $ECHO_N "checking which argv replacement method to use... $ECHO_C" >&6; }
-if test "${pf_cv_argv_type+set}" = set; then
-  echo $ECHO_N "(cached) $ECHO_C" >&6
-else
-  cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
+#define HAVE_BACKTRACE_SYMBOLS 1
 _ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
 
-#if defined(__GNU_HURD__)
-  yes
-#endif
 
-_ACEOF
-if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
-  $EGREP "yes" >/dev/null 2>&1; then
-  pf_cv_argv_type="new"
 else
-  pf_cv_argv_type="writeable"
-fi
-rm -f conftest*
-
-fi
-{ echo "$as_me:$LINENO: result: $pf_cv_argv_type" >&5
-echo "${ECHO_T}$pf_cv_argv_type" >&6; }
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-    if test "$pf_cv_argv_type" = "new"; then
-	cat >>confdefs.h <<\_ACEOF
-#define PF_ARGV_TYPE PF_ARGV_NEW
-_ACEOF
 
-	pf_argv_set="yes"
-    fi
+    { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
 
-    if test "$pf_argv_set" = "no"; then
-	cat >>confdefs.h <<\_ACEOF
-#define PF_ARGV_TYPE PF_ARGV_WRITEABLE
-_ACEOF
 
-    fi
-  fi
 fi
 
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
 { echo "$as_me:$LINENO: checking whether printf supports %llu format" >&5
 echo $ECHO_N "checking whether printf supports %llu format... $ECHO_C" >&6; };
 if test "$cross_compiling" = yes; then
@@ -37268,7 +39868,6 @@ my_shared_modules=`echo "$ac_shared_modules" | sed -e 's/\.la//g'`;
 all_modules="$my_core_modules $my_static_modules $my_shared_modules";
 
 pr_use_mysql="no"
-pr_use_openssl="no"
 pr_use_postgres="no"
 
 { echo "$as_me:$LINENO: checking for duplicate module build requests" >&5
@@ -37276,14 +39875,38 @@ echo $ECHO_N "checking for duplicate module build requests... $ECHO_C" >&6; }
 for i in $all_modules; do
   once=no;
 
-    if test x"$i" = x"mod_tls"; then
-    pr_use_openssl=yes
+      if test x"$i" = x"mod_auth_otp"; then
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+  elif test x"$i" = x"mod_digest"; then
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+  elif test x"$i" = x"mod_tls"; then
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
 
   elif test x"$i" = x"mod_sftp"; then
-    pr_use_openssl=yes
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+    if test x"$pr_use_sodium" = x ; then
+      pr_use_sodium=yes
+    fi
 
   elif test x"$i" = x"mod_sql_passwd"; then
-    pr_use_openssl=yes
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+    if test x"$pr_use_sodium" = x ; then
+      pr_use_sodium=yes
+    fi
   fi
 
   for j in $all_modules; do
@@ -37325,74 +39948,292 @@ echo "$as_me: error: duplicate build request for $j -- aborting" >&2;}
               ac_build_addl_libdirs="$ac_build_addl_libdirs $my_libdir"
             fi
 
-            if test x"$my_libdir" = x"-pthread"; then
-              LIBS="$LIBS -pthread"
-            fi
-          done
-        fi
-      fi
-    fi
+            if test x"$my_libdir" = x"-pthread"; then
+              LIBS="$LIBS -pthread"
+            fi
+          done
+        fi
+      fi
+    fi
+
+  elif test x"$i" = x"mod_sql_postgres"; then
+    pr_use_postgres="yes"
+
+    if test x"$pg_config" != xno; then
+      if `$pg_config 2>/dev/null 1>&2`; then
+        # pg_config --includedir gives path, no -I prefix
+        pg_includes=`$pg_config --includedir 2>/dev/null`
+        if test ! -z "$pg_includes"; then
+          ac_build_addl_includes="$ac_build_addl_includes -I$pg_includes"
+        fi
+
+        # pg_config --libdir gives path, no -L prefix
+        pg_libdirs=`$pg_config --libdir 2>/dev/null`
+        if test ! -z "pg_libdirs"; then
+          ac_build_addl_libdirs="$ac_build_addl_libdirs -L$pg_libdirs"
+        fi
+
+        # I suspect that we will also need to look for -pthread here
+        # (a la Bug#3702), as a forums post about proftpd+postgres describes
+        # the same "Alarm clock" symptom as seen with MySQL; see:
+        #
+        #  http://forums.proftpd.org/smf/index.php/topic,1424.0.html
+
+        # pg_config --libs gives libs, with -l prefixes
+        pg_libs=`$pg_config --libs 2>/dev/null`
+        if test ! -z "$pg_libs"; then
+          for pg_lib in $pg_lib; do
+            if test x"$pg_lib" = x"-pthread"; then
+              LIBS="$LIBS -pthread"
+            fi
+          done
+        fi
+      fi
+    fi
+  fi
+done
+{ echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+if test x"$pr_use_mysql" = xyes; then
+    saved_cppflags="$CPPFLAGS"
+  saved_ldflags="$LDFLAGS"
+  saved_libs="$LIBS"
+
+    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+  LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="$LIBS -lm -lmysqlclient -lz"
+
+  { echo "$as_me:$LINENO: checking for mysql_get_option" >&5
+echo $ECHO_N "checking for mysql_get_option... $ECHO_C" >&6; }
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <mysql.h>
+
+int
+main ()
+{
+
+      (void) mysql_get_option(NULL, 0, NULL);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_MYSQL_GET_OPTION 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+
+  { echo "$as_me:$LINENO: checking for MySQL's make_scrambled_password" >&5
+echo $ECHO_N "checking for MySQL's make_scrambled_password... $ECHO_C" >&6; }
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <mysql.h>
+
+int
+main ()
+{
+
+      char output[32];
+      char *input = NULL;
+      (void) make_scrambled_password(output, input);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+  { echo "$as_me:$LINENO: checking for MySQL's make_scrambled_password_323" >&5
+echo $ECHO_N "checking for MySQL's make_scrambled_password_323... $ECHO_C" >&6; }
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <mysql.h>
+
+int
+main ()
+{
+
+      char output[32];
+      char *input = NULL;
+      (void) make_scrambled_password_323(output, input);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
 
-  elif test x"$i" = x"mod_sql_postgres"; then
-    pr_use_postgres="yes"
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD_323 1
+_ACEOF
 
-    if test x"$pg_config" != xno; then
-      if `$pg_config 2>/dev/null 1>&2`; then
-        # pg_config --includedir gives path, no -I prefix
-        pg_includes=`$pg_config --includedir 2>/dev/null`
-        if test ! -z "$pg_includes"; then
-          ac_build_addl_includes="$ac_build_addl_includes -I$pg_includes"
-        fi
 
-        # pg_config --libdir gives path, no -L prefix
-        pg_libdirs=`$pg_config --libdir 2>/dev/null`
-        if test ! -z "pg_libdirs"; then
-          ac_build_addl_libdirs="$ac_build_addl_libdirs -L$pg_libdirs"
-        fi
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
-        # I suspect that we will also need to look for -pthread here
-        # (a la Bug#3702), as a forums post about proftpd+postgres describes
-        # the same "Alarm clock" symptom as seen with MySQL; see:
-        #
-        #  http://forums.proftpd.org/smf/index.php/topic,1424.0.html
 
-        # pg_config --libs gives libs, with -l prefixes
-        pg_libs=`$pg_config --libs 2>/dev/null`
-        if test ! -z "$pg_libs"; then
-          for pg_lib in $pg_lib; do
-            if test x"$pg_lib" = x"-pthread"; then
-              LIBS="$LIBS -pthread"
-            fi
-          done
-        fi
-      fi
-    fi
-  fi
-done
-{ echo "$as_me:$LINENO: result: no" >&5
+      { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
 
-if test x"$pr_use_mysql" = xyes; then
-    saved_cppflags="$CPPFLAGS"
-  saved_ldflags="$LDFLAGS"
-  saved_libs="$LIBS"
-
-    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
-  LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
 
-    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-  LIBS="$LIBS -lm -lmysqlclient -lz"
+fi
 
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
 
-  { echo "$as_me:$LINENO: checking for MySQL's make_scrambled_password" >&5
-echo $ECHO_N "checking for MySQL's make_scrambled_password... $ECHO_C" >&6; }
+  # For Bug#3669 in the RedHat case, we need to check for
+  # my_make_scrambled_password.
+  { echo "$as_me:$LINENO: checking for MySQL's my_make_scrambled_password" >&5
+echo $ECHO_N "checking for MySQL's my_make_scrambled_password... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -37407,7 +40248,8 @@ main ()
 
       char output[32];
       char *input = NULL;
-      (void) make_scrambled_password(output, input);
+      size_t inputlen = 0;
+      (void) my_make_scrambled_password(output, input, inputlen);
 
   ;
   return 0;
@@ -37436,7 +40278,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD 1
+#define HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD 1
 _ACEOF
 
 
@@ -37454,15 +40296,17 @@ fi
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 
-  { echo "$as_me:$LINENO: checking for MySQL's make_scrambled_password_323" >&5
-echo $ECHO_N "checking for MySQL's make_scrambled_password_323... $ECHO_C" >&6; }
+  { echo "$as_me:$LINENO: checking for MySQL's my_make_scrambled_password_323" >&5
+echo $ECHO_N "checking for MySQL's my_make_scrambled_password_323... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -37477,7 +40321,8 @@ main ()
 
       char output[32];
       char *input = NULL;
-      (void) make_scrambled_password_323(output, input);
+      size_t inputlen = 0;
+      (void) my_make_scrambled_password_323(output, input, inputlen);
 
   ;
   return 0;
@@ -37506,7 +40351,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD_323 1
+#define HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD_323 1
 _ACEOF
 
 
@@ -37524,17 +40369,17 @@ fi
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 
-  # For Bug#3669 in the RedHat case, we need to check for
-  # my_make_scrambled_password.
-  { echo "$as_me:$LINENO: checking for MySQL's my_make_scrambled_password" >&5
-echo $ECHO_N "checking for MySQL's my_make_scrambled_password... $ECHO_C" >&6; }
+  { echo "$as_me:$LINENO: checking for MySQL's mysql_ssl_set" >&5
+echo $ECHO_N "checking for MySQL's mysql_ssl_set... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -37547,10 +40392,8 @@ int
 main ()
 {
 
-      char output[32];
-      char *input = NULL;
-      size_t inputlen = 0;
-      (void) my_make_scrambled_password(output, input, inputlen);
+      MYSQL *mysql = NULL;
+      (void) mysql_ssl_set(mysql, NULL, NULL, NULL, NULL, NULL);
 
   ;
   return 0;
@@ -37579,7 +40422,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD 1
+#define HAVE_MYSQL_MYSQL_SSL_SET 1
 _ACEOF
 
 
@@ -37597,15 +40440,17 @@ fi
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 
-  { echo "$as_me:$LINENO: checking for MySQL's my_make_scrambled_password_323" >&5
-echo $ECHO_N "checking for MySQL's my_make_scrambled_password_323... $ECHO_C" >&6; }
+  { echo "$as_me:$LINENO: checking for MySQL's mysql_get_ssl_cipher" >&5
+echo $ECHO_N "checking for MySQL's mysql_get_ssl_cipher... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -37618,10 +40463,8 @@ int
 main ()
 {
 
-      char output[32];
-      char *input = NULL;
-      size_t inputlen = 0;
-      (void) my_make_scrambled_password_323(output, input, inputlen);
+      MYSQL *mysql = NULL;
+      (void) mysql_get_ssl_cipher(mysql);
 
   ;
   return 0;
@@ -37650,7 +40493,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD_323 1
+#define HAVE_MYSQL_MYSQL_GET_SSL_CIPHER 1
 _ACEOF
 
 
@@ -37756,7 +40599,7 @@ echo $ECHO_N "checking whether linking with OpenSSL functions succeeds... $ECHO_
   saved_libs="$LIBS"
 
     LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-  LIBS="$LIBS -lcrypto"
+  LIBS="-lcrypto $LIBS"
 
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
@@ -37810,6 +40653,7 @@ echo "${ECHO_T}no" >&6; }
 
       { echo "$as_me:$LINENO: checking whether linking with OpenSSL functions requires -ldl" >&5
 echo $ECHO_N "checking whether linking with OpenSSL functions requires -ldl... $ECHO_C" >&6; }
+      LIBS="-lcrypto -ldl $LIBS"
       cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -37872,7 +40716,7 @@ echo $ECHO_N "checking whether linking with OpenSSL functions requires -lz... $E
       saved_libs="$LIBS"
 
             LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-      LIBS="$LIBS -lcrypto -lz"
+      LIBS="-lcrypto -lz $LIBS"
 
       cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
@@ -37916,24 +40760,451 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
        } && test -s conftest$ac_exeext &&
        $as_test_x conftest$ac_exeext; then
 
-          { echo "$as_me:$LINENO: result: yes" >&5
+          { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+          LIBS="$saved_libs -lz"
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+          { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+          LIBS="$saved_libs"
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+
+  { echo "$as_me:$LINENO: checking whether OpenSSL has complete ECC support" >&5
+echo $ECHO_N "checking whether OpenSSL has complete ECC support... $ECHO_C" >&6; }
+  saved_libs="$LIBS"
+
+    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto $LIBS"
+
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+      #include <openssl/ec.h>
+      #include <openssl/ecdh.h>
+      #include <openssl/ecdsa.h>
+      #include <openssl/evp.h>
+      #include <openssl/objects.h>
+      #include <openssl/opensslv.h>
+      #if OPENSSL_VERSION_NUMBER < 0x0090807f /* 0.9.8g */
+      # error "OpenSSL < 0.9.8g has unreliable ECC code"
+      #endif
+
+int
+main ()
+{
+
+      EC_KEY *e = EC_KEY_new_by_curve_name(NID_secp521r1);
+      const EVP_MD *m = EVP_sha512();
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_OPENSSL_ECC 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+  LIBS="$saved_libs"
+
+  { echo "$as_me:$LINENO: checking whether OpenSSL has ALPN support" >&5
+echo $ECHO_N "checking whether OpenSSL has ALPN support... $ECHO_C" >&6; }
+  saved_libs="$LIBS"
+
+    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto -lssl $LIBS"
+
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+      #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #include <openssl/ssl.h>
+
+int
+main ()
+{
+
+      SSL_CTX *ctx = NULL;
+      SSL_CTX_set_alpn_select_cb(ctx, NULL, NULL);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_OPENSSL_ALPN 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+  LIBS="$saved_libs"
+
+  { echo "$as_me:$LINENO: checking whether OpenSSL has NPN support" >&5
+echo $ECHO_N "checking whether OpenSSL has NPN support... $ECHO_C" >&6; }
+  saved_libs="$LIBS"
+
+    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto -lssl $LIBS"
+
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+      #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #include <openssl/ssl.h>
+
+int
+main ()
+{
+
+      SSL_CTX *ctx = NULL;
+      SSL_CTX_set_next_protos_advertised_cb(ctx, NULL, NULL);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_OPENSSL_NPN 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+  LIBS="$saved_libs"
+
+  { echo "$as_me:$LINENO: checking whether OpenSSL has OCSP support" >&5
+echo $ECHO_N "checking whether OpenSSL has OCSP support... $ECHO_C" >&6; }
+  saved_libs="$LIBS"
+
+    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto -lssl $LIBS"
+
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+
+      #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #include <openssl/ssl.h>
+      #include <openssl/ocsp.h>
+
+int
+main ()
+{
+
+      SSL_CTX *ctx = NULL;
+      SSL_CTX_set_tlsext_status_cb(ctx, NULL);
+      SSL_CTX_set_tlsext_status_arg(ctx, NULL);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_OPENSSL_OCSP 1
+_ACEOF
+
+
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+
+      { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+
+
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
+      conftest$ac_exeext conftest.$ac_ext
+  LIBS="$saved_libs"
+
+  pr_use_pthread_for_openssl="no"
+  if test x"$openssl_cmdline" != xno; then
+    if `$openssl_cmdline version 2>/dev/null 1>&2`; then
+      openssl_cflags=`$openssl_cmdline version -f 2>/dev/null`
+      if test ! -z "$openssl_cflags"; then
+        # Look for the -pthread flag, indicating that this OpenSSL was built
+        # with threads support (see Bug#3795)
+        for openssl_cflag in $openssl_cflags; do
+          if test x"$openssl_cflag" = x"-pthread"; then
+            pr_use_pthread_for_openssl="yes"
+          fi
+        done
+
+        if test x"$pr_use_pthread_for_openssl" = xno ; then
+          # If we're on FreeBSD, AND if OpenSSL is being used, AND if
+          # openssl version -f shows no flags, then ASSUME that we do need
+          # the -pthread flag, to avoid regressions of Bug#3795.
+          if test `echo $ostype | grep -c FREEBSD` != "0" ; then
+            pr_use_pthread_for_openssl="yes"
+          fi
+        fi
+      fi
+    fi
+  else
+    # If we're on FreeBSD, AND if OpenSSL is being used, then ASSUME that we
+    # do need the -pthread flag, to avoid regressions of Bug#3795.
+    if test `echo $ostype | grep -c DFREEBSD` != "0" ; then
+      pr_use_pthread_for_openssl="yes"
+    fi
+  fi
+
+  if test x"$pr_use_pthread_for_openssl" = xyes ; then
+    LIBS="$LIBS -pthread"
+  fi
+fi
+
+if test x"$pr_use_postgres" = xyes; then
+  # Check for other Postgres-specific functionality here
+  saved_ldflags="$LDFLAGS"
+  saved_libs="$LIBS"
+  saved_cppflags="$CPPFLAGS"
+
+  # fiddle with CPPFLAGS, LDFLAGS
+  CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+  LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="$LIBS -lm -lpq"
+
+  { echo "$as_me:$LINENO: checking for Postgres's PQescapeStringConn" >&5
+echo $ECHO_N "checking for Postgres's PQescapeStringConn... $ECHO_C" >&6; }
+  cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <libpq-fe.h>
+
+int
+main ()
+{
+
+      char *input = NULL, *output = NULL;
+      size_t inputlen = 0;
+      PGconn *postgres = NULL;
+      int pgerr = 0;
+      PQescapeStringConn(postgres, output, input, inputlen, &pgerr);
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.$ac_objext conftest$ac_exeext
+if { (ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_link") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext &&
+       $as_test_x conftest$ac_exeext; then
+
+      { echo "$as_me:$LINENO: result: yes" >&5
 echo "${ECHO_T}yes" >&6; }
-          LIBS="$saved_libs -lz"
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_POSTGRES_PQESCAPESTRINGCONN 1
+_ACEOF
+
 
 else
   echo "$as_me: failed program was:" >&5
 sed 's/^/| /' conftest.$ac_ext >&5
 
 
-          { echo "$as_me:$LINENO: result: no" >&5
+      { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
-          LIBS="$saved_libs"
-
-
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
 
 
 fi
@@ -37941,36 +41212,31 @@ fi
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
 
-  { echo "$as_me:$LINENO: checking whether linking with OpenSSL has complete ECC support" >&5
-echo $ECHO_N "checking whether linking with OpenSSL has complete ECC support... $ECHO_C" >&6; }
-  saved_libs="$LIBS"
-
-    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-  LIBS="$LIBS -lcrypto"
-
+  { echo "$as_me:$LINENO: checking for Postgres's PQgetssl" >&5
+echo $ECHO_N "checking for Postgres's PQgetssl... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
 cat confdefs.h >>conftest.$ac_ext
 cat >>conftest.$ac_ext <<_ACEOF
 /* end confdefs.h.  */
-
-      #include <openssl/ec.h>
-      #include <openssl/ecdh.h>
-      #include <openssl/ecdsa.h>
-      #include <openssl/evp.h>
-      #include <openssl/objects.h>
-      #include <openssl/opensslv.h>
-      #if OPENSSL_VERSION_NUMBER < 0x0090807f /* 0.9.8g */
-      # error "OpenSSL < 0.9.8g has unreliable ECC code"
+ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
       #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <libpq-fe.h>
 
 int
 main ()
 {
 
-      EC_KEY *e = EC_KEY_new_by_curve_name(NID_secp521r1);
-      const EVP_MD *m = EVP_sha512();
+      const PGconn *pg = NULL;
+      (void) PQgetssl(pg);
 
   ;
   return 0;
@@ -37999,7 +41265,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define PR_USE_OPENSSL_ECC 1
+#define HAVE_POSTGRES_PQGETSSL 1
 _ACEOF
 
 
@@ -38016,60 +41282,13 @@ fi
 
 rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
       conftest$ac_exeext conftest.$ac_ext
-  LIBS="$saved_libs"
-
-  pr_use_pthread_for_openssl="no"
-  if test x"$openssl_cmdline" != xno; then
-    if `$openssl_cmdline version 2>/dev/null 1>&2`; then
-      openssl_cflags=`$openssl_cmdline version -f 2>/dev/null`
-      if test ! -z "$openssl_cflags"; then
-        # Look for the -pthread flag, indicating that this OpenSSL was built
-        # with threads support (see Bug#3795)
-        for openssl_cflag in $openssl_cflags; do
-          if test x"$openssl_cflag" = x"-pthread"; then
-            pr_use_pthread_for_openssl="yes"
-          fi
-        done
 
-        if test x"$pr_use_pthread_for_openssl" = xno ; then
-          # If we're on FreeBSD, AND if OpenSSL is being used, AND if
-          # openssl version -f shows no flags, then ASSUME that we do need
-          # the -pthread flag, to avoid regressions of Bug#3795.
-          if test `echo $ostype | grep -c FREEBSD` != "0" ; then
-            pr_use_pthread_for_openssl="yes"
-          fi
-        fi
-      fi
-    fi
-  else
-    # If we're on FreeBSD, AND if OpenSSL is being used, then ASSUME that we
-    # do need the -pthread flag, to avoid regressions of Bug#3795.
-    if test `echo $ostype | grep -c DFREEBSD` != "0" ; then
-      pr_use_pthread_for_openssl="yes"
-    fi
-  fi
-
-  if test x"$pr_use_pthread_for_openssl" = xyes ; then
-    LIBS="$LIBS -pthread"
-  fi
-fi
-
-if test x"$pr_use_postgres" = xyes; then
-  # Check for other Postgres-specific functionality here
-  saved_ldflags="$LDFLAGS"
-  saved_libs="$LIBS"
-  saved_cppflags="$CPPFLAGS"
-
-  { echo "$as_me:$LINENO: checking for Postgres's PQescapeStringConn" >&5
-echo $ECHO_N "checking for Postgres's PQescapeStringConn... $ECHO_C" >&6; }
-
-  # fiddle with CPPFLAGS, LDFLAGS
-  CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
-  LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
-
-    LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-  LIBS="$LIBS -lm -lpq"
+  # restore CPPFLAGS, LDFLAGS
+  CPPFLAGS="$saved_cppflags"
+  LDFLAGS="$saved_ldflags"
 
+  { echo "$as_me:$LINENO: checking for Postgres's PQinitOpenSSL" >&5
+echo $ECHO_N "checking for Postgres's PQinitOpenSSL... $ECHO_C" >&6; }
   cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
 _ACEOF
@@ -38089,11 +41308,8 @@ int
 main ()
 {
 
-      char *input = NULL, *output = NULL;
-      size_t inputlen = 0;
-      PGconn *postgres = NULL;
-      int pgerr = 0;
-      PQescapeStringConn(postgres, output, input, inputlen, &pgerr);
+      int init_ssl = 0, init_crypto = 0;
+      PQinitOpenSSL(init_ssl, init_crypto);
 
   ;
   return 0;
@@ -38122,7 +41338,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
 echo "${ECHO_T}yes" >&6; }
 
 cat >>confdefs.h <<\_ACEOF
-#define HAVE_POSTGRES_PQESCAPESTRINGCONN 1
+#define HAVE_POSTGRES_PQINITOPENSSL 1
 _ACEOF
 
 
@@ -38146,6 +41362,150 @@ rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
   LIBS="$saved_libs"
 fi
 
+if test x"$pr_use_sodium" = xyes; then
+  if test "${ac_cv_header_sodium_h+set}" = set; then
+  { echo "$as_me:$LINENO: checking for sodium.h" >&5
+echo $ECHO_N "checking for sodium.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_sodium_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_sodium_h" >&5
+echo "${ECHO_T}$ac_cv_header_sodium_h" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking sodium.h usability" >&5
+echo $ECHO_N "checking sodium.h usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <sodium.h>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_header_compiler=no
+fi
+
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking sodium.h presence" >&5
+echo $ECHO_N "checking sodium.h presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <sodium.h>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+  ac_header_preproc=no
+fi
+
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
+
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: sodium.h: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: sodium.h: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sodium.h: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: sodium.h: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: sodium.h: present but cannot be compiled" >&5
+echo "$as_me: WARNING: sodium.h: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sodium.h:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: sodium.h:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sodium.h: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: sodium.h: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sodium.h:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: sodium.h:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sodium.h: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: sodium.h: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: sodium.h: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: sodium.h: in the future, the compiler will take precedence" >&2;}
+
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for sodium.h" >&5
+echo $ECHO_N "checking for sodium.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_sodium_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_cv_header_sodium_h=$ac_header_preproc
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_sodium_h" >&5
+echo "${ECHO_T}$ac_cv_header_sodium_h" >&6; }
+
+fi
+if test $ac_cv_header_sodium_h = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SODIUM_H 1
+_ACEOF
+
+
+cat >>confdefs.h <<\_ACEOF
+#define PR_USE_SODIUM 1
+_ACEOF
+
+     ac_build_addl_libs="$ac_build_addl_libs -lsodium"
+
+fi
+
+
+fi
+
 for module in $ac_shared_modules ; do
   moduledir=`echo "$module" | sed -e 's/\.la$//'`;
 
@@ -38216,16 +41576,16 @@ echo "$as_me: error: specified archive '$thearch' does not match expected module
     done
 
         for thelib in $srclib $inclib; do
-      dup="no"
+      dup="xno"
 
       for somelib in $ac_addl_libs $LIBS; do
         if test "$thelib" = "$somelib"; then
-          dup="yes"
+          dup="xyes"
           break
         fi
       done
 
-      if test "$dup" = "no"; then
+      if test "$dup" = x"no"; then
         addonlibs="$addonlibs $thelib"
       fi
     done
@@ -38314,16 +41674,16 @@ for module in $ac_static_modules; do
     fi
 
         for thelib in $srclib $inclib; do
-      dup="no"
+      dup="xno"
 
       for somelib in $ac_addl_libs $LIBS; do
         if test "$thelib" = "$somelib"; then
-          dup="yes"
+          dup="xyes"
           break
         fi
       done
 
-      if test "$dup" = "no"; then
+      if test "$dup" = x"no"; then
         addonlibs="$addonlibs $thelib"
       fi
     done
@@ -38454,7 +41814,33 @@ _ACEOF
 
   if test x"$GCC" = xyes; then
 
-                { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wfloat-equal" >&5
+                { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wcomment" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wcomment... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wcomment conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wcomment"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+    { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wdeclaration-after-statement" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wdeclaration-after-statement... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wdeclaration-after-statement conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wdeclaration-after-statement"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+    { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wfloat-equal" >&5
 echo $ECHO_N "checking whether ${CC-cc} accepts -Wfloat-equal... $ECHO_C" >&6; }
    echo 'void f(){}' > conftest.c
    if test -z "`${CC-cc} -c -Wfloat-equal conftest.c 2>&1`"; then
@@ -38519,6 +41905,19 @@ echo "${ECHO_T}no" >&6; }
    fi
    rm -f conftest*
 
+    { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wmissing-braces" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wmissing-braces... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wmissing-braces conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wmissing-braces"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
     { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wpointer-to-int-cast" >&5
 echo $ECHO_N "checking whether ${CC-cc} accepts -Wpointer-to-int-cast... $ECHO_C" >&6; }
    echo 'void f(){}' > conftest.c
@@ -38545,6 +41944,32 @@ echo "${ECHO_T}no" >&6; }
    fi
    rm -f conftest*
 
+    { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wstrict-overflow" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wstrict-overflow... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wstrict-overflow conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wstrict-overflow"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
+    { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wswitch" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -Wswitch... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -Wswitch conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -Wswitch"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
     { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -Wunreachable-code" >&5
 echo $ECHO_N "checking whether ${CC-cc} accepts -Wunreachable-code... $ECHO_C" >&6; }
    echo 'void f(){}' > conftest.c
@@ -38558,6 +41983,19 @@ echo "${ECHO_T}no" >&6; }
    fi
    rm -f conftest*
 
+    { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -fstack-protector" >&5
+echo $ECHO_N "checking whether ${CC-cc} accepts -fstack-protector... $ECHO_C" >&6; }
+   echo 'void f(){}' > conftest.c
+   if test -z "`${CC-cc} -c -fstack-protector conftest.c 2>&1`"; then
+     { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+     CFLAGS="$CFLAGS -fstack-protector"
+   else
+     { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+   fi
+   rm -f conftest*
+
     { echo "$as_me:$LINENO: checking whether ${CC-cc} accepts -fstack-protector-all" >&5
 echo $ECHO_N "checking whether ${CC-cc} accepts -fstack-protector-all... $ECHO_C" >&6; }
    echo 'void f(){}' > conftest.c
@@ -41300,3 +44738,26 @@ for moduledir in $ac_shared_module_dirs $ac_static_module_dirs; do
     fi
   fi
 done
+
+# Display a summary of what modules will be compiled
+echo
+echo "--------------"
+echo "Build Summary"
+echo "--------------"
+if test ! -z "$my_static_modules"; then
+  echo "Building the following static modules:"
+  for amodule in $my_static_modules; do
+   echo "  $amodule"
+  done
+fi
+
+if test ! -z "$my_shared_modules"; then
+  echo
+  echo "Building the following shared modules:"
+  for amodule in $my_shared_modules; do
+    echo "  $amodule"
+  done
+fi
+
+echo
+echo "--------------"
diff --git a/configure.in b/configure.in
index 8c43891..16832b9 100644
--- a/configure.in
+++ b/configure.in
@@ -1,7 +1,7 @@
 dnl ProFTPD - FTP server daemon
 dnl Copyright (c) 1997, 1998 Public Flood Software
 dnl Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
-dnl Copyright (c) 2001-2016 The ProFTPD Project team
+dnl Copyright (c) 2001-2017 The ProFTPD Project team
 dnl
 dnl This program is free software; you can redistribute it and/or modify
 dnl it under the terms of the GNU General Public License as published by
@@ -47,7 +47,7 @@ AC_SUBST(BUILD_OPTS)
 AC_SUBST(OSREL)
 AC_SUBST(OSTYPE)
 
-dnl MacOS X requires -traditional-cpp; autodetecting it isn't impossible
+dnl MacOSX requires -traditional-cpp; autodetecting it isn't impossible
 dnl AFAIK, since assuming the need for -traditional-cpp breaks the build
 dnl on other OSes. -jwm, 30 Jan 2001
 if test "$OSTYPE" = "-DRHAPSODY5"; then
@@ -63,7 +63,7 @@ top_builddir=.
 LT_INIT([dlopen])
 
 dnl If LT_INIT provides an empty value for CONFIG_SHELL for some reason (it
-dnl happens on my Mac OSX 10.5 machine, for example), have a fallback.
+dnl happens on my MacOSX 10.5 machine, for example), have a fallback.
 if test x"$CONFIG_SHELL" = x; then
   CONFIG_SHELL="$SHELL"
 fi
@@ -89,6 +89,11 @@ fi
 
 LDFLAGS="-L\$(top_srcdir)/lib $LDFLAGS"
 
+if test $ac_cv_prog_gcc = yes; then
+  dnl Make sure to use the -rdynamic linker flag, for stacktraces
+  LDFLAGS="$LDFLAGS -rdynamic"
+fi
+
 # Record the current CPPFLAGS, LDFLAGS, and LIBS here
 ac_orig_cppflags="$CPPFLAGS"
 ac_orig_ldflags="$LDFLAGS"
@@ -119,7 +124,14 @@ if test $ac_cv_prog_gcc = yes; then
     AC_MSG_RESULT(yes); fullCFLAGS="$fullCFLAGS $CFLAGS",
     AC_MSG_RESULT(no))
 
-  CFLAGS="$fullCFLAGS"
+  dnl test for -fno-omit-frame-pointer
+  AC_MSG_CHECKING([whether the C compiler accepts -fno-omit-frame-pointer])
+  CFLAGS="-fno-omit-frame-pointer"
+  AC_TRY_COMPILE(,,
+    AC_MSG_RESULT(yes); fullCFLAGS="$fullCFLAGS $CFLAGS",
+    AC_MSG_RESULT(no))
+
+  CFLAGS="-g2 $fullCFLAGS"
 fi
 
 dnl We substitute these in the man page templates.
@@ -166,7 +178,7 @@ dnl problems for certain builds of proftpd, e.g. ProFTPD and MySQL (which
 dnl itself similarly bundles a getopt implementation).  Thus, we need to
 dnl support/handle --without-getopt, to disable use of ProFTPD's getopt.
 dnl I don't know if MySQL supports any similar option.
-LIB_OBJS="pr_fnmatch.o sstrncpy.o strsep.o vsnprintf.o glibc-glob.o glibc-hstrerror.o glibc-mkstemp.o pr-syslog.o pwgrent.o tpl.o"
+LIB_OBJS="pr_fnmatch.o sstrncpy.o strsep.o vsnprintf.o glibc-glob.o glibc-hstrerror.o glibc-mkstemp.o pr-syslog.o pwgrent.o hanson-tpl.o ccan-json.o"
 
 AC_ARG_WITH(getopt,
   [AC_HELP_STRING(
@@ -370,6 +382,10 @@ AC_ARG_WITH(modules,
           if test x"$amodule" = xmod_memcache ; then
             AC_MSG_ERROR([use --enable-memcache instead of --with-modules=mod_memcache for Memcache support])
           fi
+
+          if test x"$amodule" = xmod_redis ; then
+            AC_MSG_ERROR([use --enable-redis instead of --with-modules=mod_redis for Redis support])
+          fi
         done
 
         # Trim off any leading/trailing colons, and collapse double-colons
@@ -402,8 +418,17 @@ if test x"$enable_memcache" = xyes; then
   ac_build_static_modules="$ac_build_static_modules modules/mod_memcache.o"
 fi
 
+dnl Redis
+if test x"$enable_redis" = xyes; then
+  # Yes, we DO want mod_redis AFTER the other modules in the static
+  # module list. Otherwise, the module load ordering will be such that
+  # Redis support will not work as expected.
+  ac_static_modules="$ac_static_modules mod_redis.o"
+  ac_build_static_modules="$ac_build_static_modules modules/mod_redis.o"
+fi
+
 dnl List of modules which are not allowed to be built as DSOs
-ac_unshareable_modules="mod_auth mod_rlimit mod_auth_unix mod_core mod_dso mod_ls mod_xfer mod_log mod_site mod_cap mod_ctrls"
+ac_unshareable_modules="mod_auth mod_rlimit mod_auth_unix mod_core mod_dso mod_ls mod_xfer mod_log mod_site mod_cap mod_ctrls mod_memcache mod_redis"
 
 AC_ARG_WITH(shared,
   [AC_HELP_STRING(
@@ -420,10 +445,11 @@ AC_ARG_WITH(shared,
         # Trim off any leading/trailing colons, and collapse double-colons
         # into single colons; these are common typos.
         shared_modules=`echo "$withval" | sed 's/::/:/g'| sed 's/^://' | sed 's/:$//' | sed -e 's/:/ /g'`;
+        ac_shared_modules=`echo "$withval" | sed 's/::/:/g'| sed 's/^://' | sed 's/:$//' | sed -e 's/:/.la /g'`.la;
 
         # First double-check that the given list does not contain any
         # unshareable modules
-        for amodule in $shared_modules; do
+        for amodule in $pr_shared_modules; do
           for smodule in $ac_unshareable_modules; do
             if test x"$amodule" = x"$smodule"; then
               AC_MSG_ERROR([cannot build $amodule as a shared module])
@@ -431,10 +457,11 @@ AC_ARG_WITH(shared,
           done
         done
 
-        ac_shared_modules=`echo "$withval" | sed -e 's/:/.la /g'`.la ;
         for amodule in $ac_shared_modules; do
           ac_build_shared_modules="modules/$amodule $ac_build_shared_modules"
         done
+
+        PR_CHECK_CC_OPT(Werror=implicit-function-declaration)
       fi
     fi
   ])
@@ -603,7 +630,7 @@ dnl PAM support.
 AC_ARG_ENABLE(auth-pam,
   [AC_HELP_STRING(
     [--enable-auth-pam],
-    [enable PAM support (default=yes)])
+    [enable PAM support (default=auto)])
   ],
   [
     if test "$enableval" = "no"; then
@@ -804,6 +831,61 @@ AC_ARG_ENABLE(pcre,
     if test x"$enableval" = xyes ; then
       AC_DEFINE(PR_USE_PCRE, 1, [Define if using PCRE support.])
       ac_build_addl_libs="$ac_build_addl_libs -lpcreposix -lpcre"
+
+      # Check for other PCRE-specific functionality here
+      saved_ldflags="$LDFLAGS"
+      saved_libs="$LIBS"
+      saved_cppflags="$CPPFLAGS"
+
+      # fiddle with CPPFLAGS, LDFLAGS
+      CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+      LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+      dnl Splice out -lsupp, since that library hasn't been built yet
+      LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+      LIBS="$LIBS -lpcre -lpcreposix"
+
+      AC_MSG_CHECKING([for PCRE's pcre_free_study])
+      AC_TRY_LINK(
+        [ #ifdef HAVE_STDDEF_H
+          # include <stddef.h>
+          #endif
+          #ifdef HAVE_STDLIB_H
+          # include <stdlib.h>
+          #endif
+          #ifdef HAVE_SYS_TYPES_H
+          # include <sys/types.h>
+          #endif
+          #include <pcre.h>
+        ],
+        [
+          pcre_extra *extra = NULL;
+          pcre_free_study(extra);
+        ],
+        [
+          AC_MSG_RESULT(yes)
+          AC_DEFINE(HAVE_PCRE_PCRE_FREE_STUDY, 1, [Define if you have PCRE's pcre_free_study])
+        ],
+        [
+          AC_MSG_RESULT(no)
+        ]
+      )
+
+      # restore CPPFLAGS, LDFLAGS
+      CPPFLAGS="$saved_cppflags"
+      LDFLAGS="$saved_ldflags"
+      LIBS="$saved_libs"
+    fi
+  ])
+
+dnl Redis support
+AC_ARG_ENABLE(redis,
+  [AC_HELP_STRING(
+    [--enable-redis],
+    [enable support for Redis (default=no)])
+  ],
+  [ if test x"$enableval" = xyes ; then
+      AC_DEFINE(PR_USE_REDIS, 1, [Define if using Redis support.])
     fi
   ])
 
@@ -822,14 +904,26 @@ AC_ARG_ENABLE(ipv6,
   ])
 
 dnl OpenSSL support
+pr_use_openssl=""
 AC_ARG_ENABLE(openssl,
   [AC_HELP_STRING(
     [--enable-openssl],
     [enable OpenSSL support (default=no)])
   ],
-  [ if test x"$enableval" = xyes ; then
-      AC_DEFINE(PR_USE_OPENSSL, 1, [Define if using OpenSSL support.])
-      ac_build_addl_libs="$ac_build_addl_libs -lssl -lcrypto"
+  [ if test x"$enableval" = xno ; then
+      pr_use_openssl="no"
+    fi
+  ])
+
+dnl Sodium support
+pr_use_sodium=""
+AC_ARG_ENABLE(sodium,
+  [AC_HELP_STRING(
+    [--enable-sodium],
+    [enable Sodium support (default=auto)])
+  ],
+  [ if test x"$enableval" = xno ; then
+      pr_use_sodium="no"
     fi
   ])
 
@@ -913,8 +1007,15 @@ AC_ARG_ENABLE(trace,
     [disable trace support (default=no)])
   ])
 
+dnl Extended attribute (xattr) support
+AC_ARG_ENABLE(xattr,
+  [AC_HELP_STRING(
+    [--disable-xattr],
+    [disable extended attribute support (default=auto)])
+  ])
+
 dnl Enable developer code
-pr_devel_cflags="-g -O0 -Wcast-align -Wchar-subscripts -Winline -Wstrict-prototypes -Wmissing-declarations -Wnested-externs -Wpointer-arith -Wshadow -Wundef"
+pr_devel_cflags="-g3 -O0 -Wcast-align -Wchar-subscripts -Winline -Wstrict-prototypes -Wmissing-declarations -Wnested-externs -Wpointer-arith -Wshadow -Wundef"
 pr_devel_libs=""
 
 AC_ARG_ENABLE(devel,
@@ -926,12 +1027,30 @@ AC_ARG_ENABLE(devel,
     if test x"$enableval" != xno ; then
       devel="yes"
 
+      # Additional warnings but only for developer mode.  Note that
+      # -Wconversion is a bit noisy at the moment, thus why we
+      # selectively choose which warnings to enable.
+      PR_CHECK_CC_OPT(Wdangling-else)
+      PR_CHECK_CC_OPT(Wextra)
+      PR_CHECK_CC_OPT(Werror=implicit-function-declaration)
+      PR_CHECK_CC_OPT(Winit-self)
+      PR_CHECK_CC_OPT(Wno-missing-field-initializers)
+      PR_CHECK_CC_OPT(Wno-unused-parameter)
+      PR_CHECK_CC_OPT(Wnull-dereference)
+      PR_CHECK_CC_OPT(Wstrict-prototypes)
+      PR_CHECK_CC_OPT(fdelete-null-pointer-checks)
+
       dnl Check to see if specific developer flags were requested
 
       if test `echo $enableval | grep -c coredump` = "1" ; then
         pr_devel_cflags="-DPR_DEVEL_COREDUMP $pr_devel_cflags"
       fi
 
+      if test `echo $enableval | grep -c coverage` = "1" ; then
+        pr_devel_cflags="-DPR_DEVEL_COVERAGE --coverage $pr_devel_cflags"
+        pr_devel_libs="--coverage $pr_devel_libs"
+      fi
+
       if test `echo $enableval | grep -c nodaemon` = "1" ; then
         pr_devel_cflags="-DPR_DEVEL_NO_DAEMON $pr_devel_cflags"
       fi
@@ -945,75 +1064,35 @@ AC_ARG_ENABLE(devel,
         pr_devel_libs="-pg $pr_devel_libs"
       fi
 
-      if test `echo $enableval | grep -c stacktrace` = "1"; then
-        pr_devel_cflags="-DPR_DEVEL_STACK_TRACE $pr_devel_cflags"
-
-        # On FreeBSD, the libexecinfo port is needed for the backtrace(3)
-        # function; we thus also need to check for the libexecinfo library
-        AC_CHECK_LIB(execinfo, backtrace)
+      if test `echo $enableval | grep -c sanitize` = "1" ; then
+        pr_devel_cflags="-fsanitize=address $pr_devel_cflags"
+        pr_devel_libs="-fsanitize=address $pr_devel_libs"
 
-        # Some libcs need the execinfo.h header for their backtrace symbols,
-        # and some (like Solaris) want ucontext.h.  Check for those headers
-        # here.
-        AC_CHECK_HEADERS(execinfo.h ucontext.h)
-
-        # Make sure that we can find the backtrace(3) and backtrace_symbols(3)
-        # functions
-        AC_MSG_CHECKING([for backtrace])
-        AC_TRY_LINK(
-          [
-            #include <stddef.h>
-            #include <stdlib.h>
-            #ifdef HAVE_EXECINFO_H
-            # include <execinfo.h>
-            #endif
-            #ifdef HAVE_UCONTEXT_H
-            # include <ucontext.h>
-            #endif
-          ],
-          [
-            void **syms = NULL;
-            int res, nsyms = 0;
-            res = backtrace(syms, nsyms);
-          ],
-          [
-            AC_MSG_RESULT(yes)
-            AC_DEFINE(HAVE_BACKTRACE, 1, [Define if you have backtrace])
-          ],
-          [
-            AC_MSG_RESULT(no)
-          ]
-        )
-
-        AC_MSG_CHECKING([for backtrace_symbols])
+        # Determine whether we need to link with libasan (gcc) or not (clang)
+        AC_MSG_CHECKING([whether the C compiler accepts -lasan])
+        saved_ldflags=$LDFLAGS
+        LDFLAGS="-lasan $LDFLAGS"
         AC_TRY_LINK(
           [
-            #include <stddef.h>
-            #include <stdlib.h>
-            #ifdef HAVE_EXECINFO_H
-            # include <execinfo.h>
-            #endif
-            #ifdef HAVE_UCONTEXT_H
-            # include <ucontext.h>
-            #endif
           ],
           [
-            void **syms = NULL;
-            int nsyms = 0;
-            char **res;
-            res = backtrace_symbols(syms, nsyms);
+            int i;
+            i = 7;
           ],
           [
             AC_MSG_RESULT(yes)
-            AC_DEFINE(HAVE_BACKTRACE_SYMBOLS, 1, [Define if you have backtrace_symbols])
+            pr_devel_libs="-lasan $pr_devel_libs"
           ],
           [
             AC_MSG_RESULT(no)
           ]
         )
-
+        LDFLAGS=$saved_ldflags
       fi
 
+      dnl Here is where we WOULD check for the stacktrace developer option.
+      dnl However, as of Issue 276, stacktraces are now enabled by default.
+
       if test `echo $enableval | grep -c timing` = "1"; then
         pr_devel_cflags="-DPR_DEVEL_TIMING $pr_devel_cflags"
       fi
@@ -1086,15 +1165,15 @@ AC_ARG_ENABLE(scoreboard-updates,
     fi
   ])
 
-keepsyms="no"
+keepsyms="yes"
 AC_ARG_ENABLE(strip,
   [AC_HELP_STRING(
-    [--disable-strip],
-    [do not strip debugging symbols from installed code (default=no)])
+    [--enable-strip],
+    [strip debugging symbols from installed code (default=no)])
   ],
   [
-    if test x"$enableval" = xno ; then
-      keepsyms="yes"
+    if test x"$enableval" = xyes ; then
+      keepsyms="no"
     fi
   ]
 )
@@ -1195,6 +1274,22 @@ AC_ARG_ENABLE(timeout-stalled,
     fi
   ])
 
+AC_ARG_ENABLE(parser-buffer-size,
+  [AC_HELP_STRING(
+    [--enable-parser-buffer-size],
+    [tune the the size (in bytes) of parser buffers (default=4096 bytes)])
+  ],
+  [
+    if test "$enableval" = "yes" || test "$enableval" = "no" ; then
+      AC_MSG_WARN(parser buffer size defaulting to regular buffer size)
+      AC_DEFINE_UNQUOTED(PR_TUNABLE_PARSER_BUFFER_SIZE, 4096,
+      [Default buffer size])
+    else
+      AC_DEFINE_UNQUOTED(PR_TUNABLE_PARSER_BUFFER_SIZE, $enableval,
+      [Specified parser buffer size])
+    fi
+  ])
+
 AC_ARG_ENABLE(transfer-buffer-size,
   [AC_HELP_STRING(
     [--enable-transfer-buffer-size],
@@ -1265,7 +1360,7 @@ if test "$pr_cv_var__pw_stayopen" = "yes"; then
   AC_DEFINE(HAVE__PW_STAYOPEN, 1, [Define if you have __pw_stayopen variable.])
 fi
 
-AC_CHECK_HEADERS(krb.h login.h prot.h usersec.h)
+AC_CHECK_HEADERS(krb.h login.h prot.h usersec.h sys/audit.h)
 
 dnl HP-UX's hpsecurity.h can multiply define MAXINT and confuse configure
 AC_CHECK_HEADERS(hpsecurity.h, [
@@ -1287,6 +1382,64 @@ AC_CHECK_HEADERS(hpsecurity.h, [
   ])
 ])
 
+dnl AIX's "lastlog" support is done via specific functions, rather than a
+dnl struct (Bug#4285).
+AC_MSG_CHECKING(for AIX authenticate)
+AC_TRY_COMPILE([
+  #include <sys/types.h>
+  #ifdef HAVE_USERSEC_H
+  # include <usersec.h>
+  #endif
+  ],
+  [
+    (void) authenticate(NULL, NULL, NULL, NULL);
+  ],
+  [
+    AC_DEFINE(HAVE_AUTHENTICATE, 1, [Define if you have the AIX authenticate function])
+    AC_MSG_RESULT(yes)
+  ], [
+    AC_MSG_RESULT(no)
+  ]
+)
+
+
+AC_MSG_CHECKING(for AIX loginfailed)
+AC_TRY_COMPILE([
+  #include <sys/types.h>
+  #ifdef HAVE_USERSEC_H
+  # include <usersec.h>
+  #endif
+  ],
+  [
+    (void) loginfailed(NULL, NULL, NULL, 0);
+  ],
+  [
+    AC_DEFINE(HAVE_LOGINFAILED, 1, [Define if you have the AIX loginfailed function])
+    AC_MSG_RESULT(yes)
+  ], [
+    AC_MSG_RESULT(no)
+  ]
+)
+
+AC_MSG_CHECKING(for AIX loginsuccess)
+AC_TRY_COMPILE([
+  #include <sys/types.h>
+  #ifdef HAVE_USERSEC_H
+  # include <usersec.h>
+  #endif
+  ],
+  [
+    (void) loginsuccess(NULL, NULL, NULL, NULL);
+  ],
+  [
+    AC_DEFINE(HAVE_LOGINSUCCESS, 1, [Define if you have the AIX loginsuccess function]
+)
+    AC_MSG_RESULT(yes)
+  ], [
+    AC_MSG_RESULT(no)
+  ]
+)
+
 dnl Checks for installation user/group
 
 if test x"$install_user" = x; then
@@ -1311,7 +1464,7 @@ dnl Checks for header files.
 AC_HEADER_DIRENT
 AC_HEADER_STDC
 AC_HEADER_SYS_WAIT
-AC_CHECK_HEADERS(fcntl.h signal.h sys/ioctl.h sys/prctl.h sys/resource.h sys/time.h junistd.h memory.h)
+AC_CHECK_HEADERS(fcntl.h signal.h linux/prctl.h sys/ioctl.h sys/prctl.h sys/resource.h sys/time.h junistd.h memory.h)
 if test x"$force_shadow" != xno ; then
   AC_CHECK_HEADERS(shadow.h,
     [ if test "$use_shadow" = "" && test -f /etc/shadow ; then
@@ -1523,17 +1676,6 @@ AC_TYPE_MODE_T
 AC_TYPE_OFF_T
 AC_TYPE_GETGROUPS
 
-dnl Check to see if timer_t is already defined
-AC_CACHE_CHECK([for timer_t], pr_cv_header_timer_t,
-	AC_EGREP_HEADER([.*typedef.*timer_t;],sys/types.h,
-			pr_cv_header_timer_t="yes",
-	AC_EGREP_HEADER([.*typedef.*timer_t;],limits.h,
-			pr_cv_header_timer_t="yes",pr_cv_header_timer_t="no")))
-
-if test "$pr_cv_header_timer_t" = "yes"; then
-  AC_DEFINE(HAVE_TIMER_T, 1, [Define if you have the timer_t type])
-fi
-
 AC_HEADER_TIME
 AC_STRUCT_TM
 
@@ -1546,6 +1688,8 @@ AC_CHECK_SIZEOF(size_t, 0)
 AC_CHECK_SIZEOF(time_t, 0)
 AC_CHECK_SIZEOF(char *, 0)
 AC_CHECK_SIZEOF(void *, 0)
+AC_CHECK_SIZEOF(uid_t, 0)
+AC_CHECK_SIZEOF(gid_t, 0)
 
 dnl Check for generic typedefs
 AC_CHECK_TYPE(mode_t, mode_t)
@@ -1676,7 +1820,8 @@ LDFLAGS=$old_LDFLAGS
 AC_PROG_GCC_TRADITIONAL
 AC_TYPE_SIGNAL
 AC_FUNC_VPRINTF
-AC_CHECK_FUNCS(bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo futimes getifaddrs getpgid getpgrp mkdtemp nl_langinfo)
+
+AC_CHECK_FUNCS(bcopy crypt fdatasync fgetgrent fgetpwent fgetspent flock fpathconf freeaddrinfo fsync futimes getifaddrs getpgid getpgrp mkdtemp nl_langinfo)
 AC_CHECK_FUNC(gai_strerror,
   AC_DEFINE(HAVE_GAI_STRERROR, 1,
     [Define if you have the gai_strerror() function]),
@@ -1685,8 +1830,15 @@ AC_CHECK_FUNC(gai_strerror,
 
 AC_MSG_CHECKING([for iconv])
 AC_TRY_LINK(
-  [ #include <stdlib.h>
-    #include <sys/types.h>
+  [ #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
     #ifdef HAVE_ICONV_H
     # include <iconv.h>
     #endif
@@ -1708,9 +1860,18 @@ AC_TRY_LINK(
 
 AC_MSG_CHECKING([for dirfd])
 AC_TRY_LINK(
-  [
-    #include <stdio.h>
-    #include <sys/types.h>
+  [ #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_STDIO_H
+    # include <stdio.h>
+    #endif
+    #ifdef HAVE_SYS_TYPES_H
+    # include <sys/types.h>
+    #endif
     #if HAVE_DIRENT_H
     # include <dirent.h>
     #endif
@@ -1733,8 +1894,15 @@ AC_TRY_LINK(
 dnl getaddrinfo is a #define on Tru64 Unix.  How annoying.
 AC_MSG_CHECKING([for getaddrinfo])
 AC_TRY_LINK(
-  [
-    #include <stdio.h>
+  [ #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_STDIO_H
+    # include <stdio.h>
+    #endif
     #if HAVE_SYS_TYPES_H
     # include <sys/types.h>
     #endif
@@ -1757,12 +1925,12 @@ AC_TRY_LINK(
   ]
 )
 
-AC_CHECK_FUNCS(getcwd getenv getgrouplist getgrset gethostbyname2 gethostname getnameinfo)
-AC_CHECK_FUNCS(gettimeofday hstrerror inet_aton inet_ntop inet_pton)
+AC_CHECK_FUNCS(getcwd getenv getgrouplist getgroups getgrset gethostbyname2 gethostname getnameinfo)
+AC_CHECK_FUNCS(gettimeofday hstrerror inet_aton inet_ntop inet_pton initgroups)
 AC_CHECK_FUNCS(loginrestrictions)
-AC_CHECK_FUNCS(memcpy mempcpy mkdir mkstemp mlock mlockall munlock munlockall)
-AC_CHECK_FUNCS(pathconf putenv regcomp rmdir select setgroups socket statfs strchr strcoll strerror)
-AC_CHECK_FUNCS(strlcat strlcpy strsep strtod strtof strtol strtoull setprotoent setspent endprotoent)
+AC_CHECK_FUNCS(memcpy mempcpy memset_s mkdir mkstemp mlock mlockall munlock munlockall)
+AC_CHECK_FUNCS(pathconf posix_fadvise prctl putenv regcomp rmdir select setgroups socket statfs strchr strcoll strerror)
+AC_CHECK_FUNCS(strlcat strlcpy strsep strtod strtof strtol strtoll strtoull setprotoent setspent endprotoent)
 # __snprintf and __vsnprintf are only on solaris and _really_ broken there.
 AC_CHECK_FUNCS(vsnprintf snprintf)
 if test x"$ac_cv_func_vsnprintf" != xyes || test x"$ac_cv_func_snprintf" != xyes
@@ -1795,6 +1963,15 @@ dnl NLS/LANG support
 if test x"$enable_nls" = xyes; then
   ac_static_modules="$ac_static_modules mod_lang.o"
   ac_build_static_modules="$ac_build_static_modules modules/mod_lang.o"
+
+  dnl Make sure that mod_ifsession, if present in the list, appears at the end.
+  if test x"$ifsession_requested" = xtrue; then
+    ac_static_modules=`echo "$ac_static_modules" | sed -e 's/mod_ifsession\.o//g'`
+    ac_static_modules="$ac_static_modules mod_ifsession.o"
+
+    ac_build_static_modules=`echo "$ac_build_static_modules" | sed -e 's/modules\/mod_ifsession\.o//g'`
+    ac_build_static_modules="$ac_build_static_modules modules/mod_ifsession.o";
+  fi
 fi
 
 dnl Controls-related checks
@@ -1907,6 +2084,9 @@ PR_CHECK_SS_LEN
 dnl POSIX ACL checks.  Always perform these, in case the administrator
 dnl wants to use mod_facl.
 AC_CHECK_HEADERS(sys/acl.h acl/libacl.h)
+AC_CHECK_LIB(acl, perm_copy_fd,
+  [AC_DEFINE(HAVE_LIBACL, 1, [Define if you have libacl])]
+)
 AC_CACHE_CHECK(
   [which POSIX ACL implementation to use],
   pr_cv_func_facl,
@@ -1919,26 +2099,61 @@ AC_CACHE_CHECK(
         #ifdef HAVE_SYS_ACL_H
         # include <sys/acl.h>
         #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
       ],
-      [ acl_entry_t ae;
-        (void)acl_get_qualifier(ae);
+      [ acl_permset_t permset;
+        /* On BSD, ACL_READ_DATA is a #define. */
+        #ifdef ACL_READ_DATA
+        acl_perm_t perm = ACL_READ_DATA;
+        #else
+        # error "ACL_READ_DATA not #defined on this platform"
+        #endif /* ACL_READ_DATA */
+        (void)acl_get_perm_np(permset, perm);
       ], pr_cv_func_facl="BSD")
   fi
 
   dnl Linux.
   if test "$pr_cv_func_facl" = "none"; then
     old_ldflags=$LDFLAGS
-    LDFLAGS="-lacl $LDFLAGS"
+    old_libs=$LIBS
+    new_ldflags=`echo "$LDFLAGS" | sed -e 's/-L\$(top_srcdir)\/lib//g'`
+    LDFLAGS="$new_ldflags"
+    LIBS="-lacl $LIBS"
     AC_TRY_LINK(
       [ #include <sys/types.h>
         #ifdef HAVE_SYS_ACL_H
         # include <sys/acl.h>
         #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
       ],
-      [ acl_entry_t ae;
-        (void)acl_get_qualifier(ae);
+      [ acl_permset_t permset;
+        acl_perm_t perm;
+        (void)acl_get_perm(permset, perm);
       ], pr_cv_func_facl="Linux")
     LDFLAGS=$old_ldflags
+    LIBS=$old_libs
+  fi
+
+  dnl MacOSX.
+  if test "$pr_cv_func_facl" = "none"; then
+    AC_TRY_LINK(
+      [ #include <sys/types.h>
+        #ifdef HAVE_SYS_ACL_H
+        # include <sys/acl.h>
+        #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
+      ],
+      [ acl_entry_t ae;
+        /* On Mac, ACL_READ_DATA is an enum value. */
+        acl_perm_t perm = ACL_READ_DATA;
+        (void)acl_get_qualifier(ae);
+      ], pr_cv_func_facl="MacOSX")
   fi
 
   dnl Solaris.
@@ -1946,10 +2161,21 @@ AC_CACHE_CHECK(
     old_ldflags=$LDFLAGS
     LDFLAGS="-lsec $LDFLAGS"
     AC_TRY_LINK(
-      [ #include <sys/types.h>
+      [ #ifdef HAVE_STDDEF_H
+        # include <stddef.h>
+        #endif
+        #ifdef HAVE_STDLIB_H
+        # include <stdlib.h>
+        #endif
+        #ifdef HAVE_SYS_TYPES_H
+        # include <sys/types.h>
+        #endif
         #ifdef HAVE_SYS_ACL_H
         # include <sys/acl.h>
         #endif
+        #ifdef HAVE_ACL_LIBACL_H
+        # include <acl/libacl.h>
+        #endif
       ],
       [ aclent_t ae;
         (void)aclcheck(&ae,0,NULL);
@@ -1969,11 +2195,13 @@ if test "$pr_cv_func_facl" != "none"; then
       ;;
 
     "Linux")
-      AC_DEFINE(HAVE_LIBACL, 1, [Define if you have libacl])
       AC_DEFINE(HAVE_LINUX_POSIX_ACL, 1,
         [Define if you have Linux-flavoured POSIX ACL support])
-      AC_CHECK_LIB(acl, perm_copy_fd,
-        [AC_DEFINE(HAVE_PERM_COPY_FD, 1, [Define if you have perm_copy_fd])])
+      ;;
+
+    "MacOSX")
+      AC_DEFINE(HAVE_MACOSX_POSIX_ACL, 1,
+        [Define if you have MacOSX-flavoured POSIX ACL support])
       ;;
 
     "Solaris")
@@ -2081,7 +2309,7 @@ if test x"$enable_sendfile" != xno ; then
           off_t o, n;
           struct sf_hdtr h;
           (void)sendfile(i,i,o,&n,&h,i);
-        ], pr_cv_func_sendfile="Mac OSX")
+        ], pr_cv_func_sendfile="MacOSX")
     fi
   )
 
@@ -2120,9 +2348,9 @@ if test x"$enable_sendfile" != xno ; then
       ac_build_addl_libs="-lsendfile $ac_build_addl_libs"
       ;;
 
-    "Mac OSX")
+    "MacOSX")
       AC_DEFINE(HAVE_MACOSX_SENDFILE, 1,
-        [Define if using Mac OSX sendfile support.])
+        [Define if using MacOSX sendfile support.])
       ;;
   esac
 fi
@@ -2132,6 +2360,272 @@ if test x"$enable_trace" != xno ; then
   AC_DEFINE(PR_USE_TRACE, 1, [Define for trace support])
 fi
 
+dnl Extended attribute checks
+if test x"$enable_xattr" != xno ; then
+  # On Free/Net/OpenBSD, it's sys/extattr.h
+  AC_CHECK_HEADER(sys/extattr.h,
+    [AC_DEFINE(HAVE_SYS_EXTATTR_H, 1, [Define if sys/extattr.h is present.])
+     AC_DEFINE(PR_USE_XATTR, 1, [Define if using xattr support.])
+
+     AC_MSG_CHECKING([for extattr_delete_link])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
+       ],
+       [
+         int res;
+         int namespace = 0;
+         const char *path = NULL, name = NULL;
+         res = extattr_delete_link(path, namespace, name);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_EXTATTR_DELETE_LINK, 1, [Define if you have extattr_delete_link])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+
+     AC_MSG_CHECKING([for extattr_get_link])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
+       ],
+       [
+         ssize_t res;
+         int namespace = 0;
+         const char *path = NULL, name = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = extattr_get_link(path, namespace, name, val, sz);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_EXTATTR_GET_LINK, 1, [Define if you have extattr_get_link])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+
+     AC_MSG_CHECKING([for extattr_list_link])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
+       ],
+       [
+         ssize_t res;
+         int namespace = 0;
+         const char *path = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = extattr_list_link(path, namespace, val, sz);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_EXTATTR_LIST_LINK, 1, [Define if you have extattr_list_link])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+
+     AC_MSG_CHECKING([for extattr_set_link])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_EXTATTR_H
+         # include <sys/extattr.h>
+         #endif
+       ],
+       [
+         int res;
+         int namespace = 0;
+         const char *path = NULL, name = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = extattr_set_link(path, namespace, name, val, sz);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_EXTATTR_SET_LINK, 1, [Define if you have extattr_set_link])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+   ])
+
+  # On Linux/MacOSX, it's sys/xattr.h
+  AC_CHECK_HEADER(sys/xattr.h,
+    [AC_DEFINE(HAVE_SYS_XATTR_H, 1, [Define if sys/xattr.h is present.])
+     AC_DEFINE(PR_USE_XATTR, 1, [Define if using xattr support.])
+     AC_CHECK_HEADERS(attr/xattr.h)
+
+     # Some platforms need libattr for extended attributes
+     AC_CHECK_LIB(attr, setxattr)
+
+     AC_MSG_CHECKING([for lgetxattr])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
+       ],
+       [
+         ssize_t res;
+         const char *path = NULL, *name = NULL;
+         void *val = NULL;
+         size_t sz = 0;
+         res = lgetxattr(path, name, val, sz);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_LGETXATTR, 1, [Define if you have lgetxattr])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+
+     AC_MSG_CHECKING([for llistxattr])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
+       ],
+       [
+         ssize_t res;
+         const char *path = NULL;
+         char *names = NUL;
+         size_t namessz = 0;
+         res = llistxattr(path, names, namessz);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_LLISTXATTR, 1, [Define if you have llistxattr])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+
+     AC_MSG_CHECKING([for lremovexattr])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
+       ],
+       [
+         ssize_t res;
+         const char *path = NULL, *name = NULL;
+         res = lremovexattr(path, name);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_LREMOVEXATTR, 1, [Define if you have lremovexattr])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+
+     AC_MSG_CHECKING([for lsetxattr])
+     AC_TRY_LINK(
+       [ #ifdef HAVE_STDDEF_H
+         # include <stddef.h>
+         #endif
+         #ifdef HAVE_STDLIB_H
+         # include <stdlib.h>
+         #endif
+         #ifdef HAVE_SYS_TYPES_H
+         # include <sys/types.h>
+         #endif
+         #ifdef HAVE_SYS_XATTR_H
+         # include <sys/xattr.h>
+         #endif
+       ],
+       [
+         int res, flags = 0;
+         const char *path = NULL, *name = NULL;
+         const void *val = NULL;
+         res = lsetxattr(path, name, val, flags);
+       ],
+       [
+         AC_MSG_RESULT(yes)
+         AC_DEFINE(HAVE_LSETXATTR, 1, [Define if you have lsetxattr])
+       ],
+       [
+         AC_MSG_RESULT(no)
+       ]
+     )
+   ])
+fi
+
 dnl Custom-rolled macro for checking return type of setgrent(3)
 PR_FUNC_SETGRENT_VOID
 
@@ -2262,6 +2756,72 @@ else
   fi
 fi
 
+dnl Check for stacktrace support
+dnl On FreeBSD, the libexecinfo port is needed for the backtrace(3) function;
+dnl we thus also need to check for the libexecinfo library
+AC_CHECK_LIB(execinfo, backtrace)
+
+dnl Some libcs need the execinfo.h header for their backtrace symbols, and
+dnl some (like Solaris) want ucontext.h.  Check for those headers here.
+AC_CHECK_HEADERS(execinfo.h ucontext.h)
+
+dnl Make sure that we can find the backtrace(3) and backtrace_symbols(3)
+dnl functions
+AC_MSG_CHECKING([for backtrace])
+AC_TRY_LINK(
+  [
+    #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_EXECINFO_H
+    # include <execinfo.h>
+    #endif
+    #ifdef HAVE_UCONTEXT_H
+    # include <ucontext.h>
+    #endif
+  ], [
+    void **syms = NULL;
+    int res, nsyms = 0;
+    res = backtrace(syms, nsyms);
+  ], [
+    AC_MSG_RESULT(yes)
+    AC_DEFINE(HAVE_BACKTRACE, 1, [Define if you have backtrace])
+  ], [
+    AC_MSG_RESULT(no)
+  ]
+)
+
+AC_MSG_CHECKING([for backtrace_symbols])
+AC_TRY_LINK(
+  [
+    #ifdef HAVE_STDDEF_H
+    # include <stddef.h>
+    #endif
+    #ifdef HAVE_STDLIB_H
+    # include <stdlib.h>
+    #endif
+    #ifdef HAVE_EXECINFO_H
+    # include <execinfo.h>
+    #endif
+    #ifdef HAVE_UCONTEXT_H
+    # include <ucontext.h>
+    #endif
+  ], [
+    void **syms = NULL;
+    int nsyms = 0;
+    char **res;
+    res = backtrace_symbols(syms, nsyms);
+  ], [
+    AC_MSG_RESULT(yes)
+    AC_DEFINE(HAVE_BACKTRACE_SYMBOLS, 1, [Define if you have backtrace_symbols])
+  ], [
+    AC_MSG_RESULT(no)
+  ]
+)
+
 dnl Run a small test program to see if the host's printf(3) family can
 dnl actually handle the %llu format.
 AC_MSG_CHECKING([whether printf supports %llu format]);
@@ -2304,22 +2864,46 @@ my_shared_modules=`echo "$ac_shared_modules" | sed -e 's/\.la//g'`;
 all_modules="$my_core_modules $my_static_modules $my_shared_modules";
 
 pr_use_mysql="no"
-pr_use_openssl="no"
 pr_use_postgres="no"
 
 AC_MSG_CHECKING([for duplicate module build requests])
 for i in $all_modules; do
   once=no;
 
-  dnl Make sure the OpenSSL define is set if mod_tls or mod_sftp are being used
-  if test x"$i" = x"mod_tls"; then
-    pr_use_openssl=yes
+  dnl Make sure the OpenSSL define is set if mod_tls, mod_sftp, or other
+  dnl whitelisted modules are being used
+  if test x"$i" = x"mod_auth_otp"; then
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+  elif test x"$i" = x"mod_digest"; then
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+  elif test x"$i" = x"mod_tls"; then
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
 
   elif test x"$i" = x"mod_sftp"; then
-    pr_use_openssl=yes
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+    if test x"$pr_use_sodium" = x ; then
+      pr_use_sodium=yes
+    fi
 
   elif test x"$i" = x"mod_sql_passwd"; then
-    pr_use_openssl=yes
+    if test x"$pr_use_openssl" = x ; then
+      pr_use_openssl=yes
+    fi
+
+    if test x"$pr_use_sodium" = x ; then
+      pr_use_sodium=yes
+    fi
   fi
 
   for j in $all_modules; do
@@ -2420,12 +3004,39 @@ if test x"$pr_use_mysql" = xyes; then
   LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
   LIBS="$LIBS -lm -lmysqlclient -lz"
 
+  AC_MSG_CHECKING([for mysql_get_option])
+  AC_TRY_LINK(
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <mysql.h>
+    ],
+    [
+      (void) mysql_get_option(NULL, 0, NULL);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(HAVE_MYSQL_GET_OPTION, 1, [Define if you have the mysql_get_option function])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+
   dnl For Bug#3669, we need to check for make_scrambled_password_323.
   dnl While we're at it, check for other variants as well.
 
   AC_MSG_CHECKING([for MySQL's make_scrambled_password])
   AC_TRY_LINK(
-    [
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -2450,7 +3061,9 @@ if test x"$pr_use_mysql" = xyes; then
 
   AC_MSG_CHECKING([for MySQL's make_scrambled_password_323])
   AC_TRY_LINK(
-    [
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -2477,7 +3090,9 @@ if test x"$pr_use_mysql" = xyes; then
   # my_make_scrambled_password.
   AC_MSG_CHECKING([for MySQL's my_make_scrambled_password])
   AC_TRY_LINK(
-    [
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -2503,7 +3118,9 @@ if test x"$pr_use_mysql" = xyes; then
 
   AC_MSG_CHECKING([for MySQL's my_make_scrambled_password_323])
   AC_TRY_LINK(
-    [
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -2527,6 +3144,58 @@ if test x"$pr_use_mysql" = xyes; then
     ]
   )
 
+  AC_MSG_CHECKING([for MySQL's mysql_ssl_set])
+  AC_TRY_LINK(
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <mysql.h>
+    ],
+    [
+      MYSQL *mysql = NULL;
+      (void) mysql_ssl_set(mysql, NULL, NULL, NULL, NULL, NULL);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(HAVE_MYSQL_MYSQL_SSL_SET, 1, [Define if you have MySQL's mysql_ssl_set function])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+
+  AC_MSG_CHECKING([for MySQL's mysql_get_ssl_cipher])
+  AC_TRY_LINK(
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <mysql.h>
+    ],
+    [
+      MYSQL *mysql = NULL;
+      (void) mysql_get_ssl_cipher(mysql);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(HAVE_MYSQL_MYSQL_GET_SSL_CIPHER, 1, [Define if you have MySQL's mysql_get_ssl_cipher function])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+
   dnl Restore CPPFLAGS, LDFLAGS
   CPPFLAGS="$saved_cppflags"
   LDFLAGS="$saved_ldflags"
@@ -2566,7 +3235,7 @@ if test x"$pr_use_openssl" = xyes; then
 
   dnl Splice out -lsupp, since that library hasn't been built yet
   LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-  LIBS="$LIBS -lcrypto"
+  LIBS="-lcrypto $LIBS"
 
   AC_TRY_LINK(
     [
@@ -2583,6 +3252,7 @@ if test x"$pr_use_openssl" = xyes; then
       AC_MSG_RESULT(no)
 
       AC_MSG_CHECKING([whether linking with OpenSSL functions requires -ldl])
+      LIBS="-lcrypto -ldl $LIBS"
       AC_TRY_LINK(
         [
           #include <openssl/evp.h>
@@ -2605,7 +3275,7 @@ if test x"$pr_use_openssl" = xyes; then
 
       dnl Splice out -lsupp, since that library hasn't been built yet
       LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-      LIBS="$LIBS -lcrypto -lz"
+      LIBS="-lcrypto -lz $LIBS"
 
       AC_TRY_LINK(
         [
@@ -2630,12 +3300,12 @@ if test x"$pr_use_openssl" = xyes; then
     ]
   )
 
-  AC_MSG_CHECKING([whether linking with OpenSSL has complete ECC support])
+  AC_MSG_CHECKING([whether OpenSSL has complete ECC support])
   saved_libs="$LIBS"
 
   dnl Splice out -lsupp, since that library hasn't been built yet
   LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-  LIBS="$LIBS -lcrypto"
+  LIBS="-lcrypto $LIBS"
 
   AC_TRY_LINK(
     [
@@ -2663,6 +3333,101 @@ if test x"$pr_use_openssl" = xyes; then
   )
   LIBS="$saved_libs"
 
+  AC_MSG_CHECKING([whether OpenSSL has ALPN support])
+  saved_libs="$LIBS"
+
+  dnl Splice out -lsupp, since that library hasn't been built yet
+  LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto -lssl $LIBS"
+
+  AC_TRY_LINK(
+    [
+      #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #include <openssl/ssl.h>
+    ],
+    [
+      SSL_CTX *ctx = NULL;
+      SSL_CTX_set_alpn_select_cb(ctx, NULL, NULL);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(PR_USE_OPENSSL_ALPN, 1, [Define if your OpenSSL supports ALPN])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+  LIBS="$saved_libs"
+
+  AC_MSG_CHECKING([whether OpenSSL has NPN support])
+  saved_libs="$LIBS"
+
+  dnl Splice out -lsupp, since that library hasn't been built yet
+  LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto -lssl $LIBS"
+
+  AC_TRY_LINK(
+    [
+      #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #include <openssl/ssl.h>
+    ],
+    [
+      SSL_CTX *ctx = NULL;
+      SSL_CTX_set_next_protos_advertised_cb(ctx, NULL, NULL);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(PR_USE_OPENSSL_NPN, 1, [Define if your OpenSSL supports NPN])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+  LIBS="$saved_libs"
+
+  AC_MSG_CHECKING([whether OpenSSL has OCSP support])
+  saved_libs="$LIBS"
+
+  dnl Splice out -lsupp, since that library hasn't been built yet
+  LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
+  LIBS="-lcrypto -lssl $LIBS"
+
+  AC_TRY_LINK(
+    [
+      #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #include <openssl/ssl.h>
+      #include <openssl/ocsp.h>
+    ],
+    [
+      SSL_CTX *ctx = NULL;
+      SSL_CTX_set_tlsext_status_cb(ctx, NULL);
+      SSL_CTX_set_tlsext_status_arg(ctx, NULL);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(PR_USE_OPENSSL_OCSP, 1, [Define if your OpenSSL supports OCSP])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+  LIBS="$saved_libs"
+
   pr_use_pthread_for_openssl="no"
   if test x"$openssl_cmdline" != xno; then
     if `$openssl_cmdline version 2>/dev/null 1>&2`; then
@@ -2705,8 +3470,6 @@ if test x"$pr_use_postgres" = xyes; then
   saved_libs="$LIBS"
   saved_cppflags="$CPPFLAGS"
 
-  AC_MSG_CHECKING([for Postgres's PQescapeStringConn])
-
   # fiddle with CPPFLAGS, LDFLAGS
   CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
   LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
@@ -2715,8 +3478,11 @@ if test x"$pr_use_postgres" = xyes; then
   LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
   LIBS="$LIBS -lm -lpq"
 
+  AC_MSG_CHECKING([for Postgres's PQescapeStringConn])
   AC_TRY_LINK(
-    [
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
       #ifdef HAVE_STDLIB_H
       # include <stdlib.h>
       #endif
@@ -2741,12 +3507,74 @@ if test x"$pr_use_postgres" = xyes; then
     ]
   )
 
+  AC_MSG_CHECKING([for Postgres's PQgetssl])
+  AC_TRY_LINK(
+    [ #ifdef HAVE_STDDEF_H
+      # include <stddef.h>
+      #endif
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <libpq-fe.h>
+    ],
+    [
+      const PGconn *pg = NULL;
+      (void) PQgetssl(pg);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(HAVE_POSTGRES_PQGETSSL, 1, [Define if you have Postgres's PQgetssl])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+
+  # restore CPPFLAGS, LDFLAGS
+  CPPFLAGS="$saved_cppflags"
+  LDFLAGS="$saved_ldflags"
+
+  AC_MSG_CHECKING([for Postgres's PQinitOpenSSL])
+  AC_TRY_LINK(
+    [
+      #ifdef HAVE_STDLIB_H
+      # include <stdlib.h>
+      #endif
+      #ifdef HAVE_SYS_TYPES_H
+      # include <sys/types.h>
+      #endif
+      #include <libpq-fe.h>
+    ],
+    [
+      int init_ssl = 0, init_crypto = 0;
+      PQinitOpenSSL(init_ssl, init_crypto);
+    ],
+    [
+      AC_MSG_RESULT(yes)
+      AC_DEFINE(HAVE_POSTGRES_PQINITOPENSSL, 1, [Define if you have Postgres's PQinitOpenSSL])
+    ],
+    [
+      AC_MSG_RESULT(no)
+    ]
+  )
+
   # restore CPPFLAGS, LDFLAGS
   CPPFLAGS="$saved_cppflags"
   LDFLAGS="$saved_ldflags"
   LIBS="$saved_libs"
 fi
 
+if test x"$pr_use_sodium" = xyes; then
+  AC_CHECK_HEADER(sodium.h,
+    [AC_DEFINE(HAVE_SODIUM_H, 1, [Define if sodium.h is present.])
+     AC_DEFINE(PR_USE_SODIUM, 1, [Define if using Sodium support.])
+     ac_build_addl_libs="$ac_build_addl_libs -lsodium"
+    ])
+fi
+
 for module in $ac_shared_modules ; do
   moduledir=`echo "$module" | sed -e 's/\.la$//'`;
 
@@ -2818,16 +3646,16 @@ for module in $ac_static_modules ; do
 
     dnl Test for duplicate libraries, just in case.
     for thelib in $srclib $inclib; do
-      dup="no"
+      dup="xno"
 
       for somelib in $ac_addl_libs $LIBS; do
         if test "$thelib" = "$somelib"; then
-          dup="yes"
+          dup="xyes"
           break
         fi
       done
 	
-      if test "$dup" = "no"; then
+      if test "$dup" = x"no"; then
         addonlibs="$addonlibs $thelib"
       fi
     done
@@ -2914,16 +3742,16 @@ for module in $ac_static_modules; do
 
     dnl Test for duplicate libraries, just in case.
     for thelib in $srclib $inclib; do
-      dup="no"
+      dup="xno"
 
       for somelib in $ac_addl_libs $LIBS; do
         if test "$thelib" = "$somelib"; then
-          dup="yes"
+          dup="xyes"
           break
         fi
       done
 	
-      if test "$dup" = "no"; then
+      if test "$dup" = x"no"; then
         addonlibs="$addonlibs $thelib"
       fi
     done
@@ -3031,14 +3859,20 @@ if test x"$devel" = xyes ; then
     dnl Some C compilers (e.g. older gcc versions) may not accept these
     dnl options.  Check if they are supported.  They will be added to
     dnl CFLAGS if supported.
+    PR_CHECK_CC_OPT(Wcomment)
+    PR_CHECK_CC_OPT(Wdeclaration-after-statement)
     PR_CHECK_CC_OPT(Wfloat-equal)
     PR_CHECK_CC_OPT(Wformat)
     PR_CHECK_CC_OPT(Wformat-security)
     PR_CHECK_CC_OPT(Wimplicit-function-declaration)
     PR_CHECK_CC_OPT(Wmaybe-uninitialized)
+    PR_CHECK_CC_OPT(Wmissing-braces)
     PR_CHECK_CC_OPT(Wpointer-to-int-cast)
     PR_CHECK_CC_OPT(Wstack-protector)
+    PR_CHECK_CC_OPT(Wstrict-overflow)
+    PR_CHECK_CC_OPT(Wswitch)
     PR_CHECK_CC_OPT(Wunreachable-code)
+    PR_CHECK_CC_OPT(fstack-protector)
     PR_CHECK_CC_OPT(fstack-protector-all)
   fi
 
@@ -3051,7 +3885,7 @@ else
     INSTALL_STRIP=""
 
   else 
-    dnl Make sure to strip symbols from non-developer object files.
+    dnl Make sure to strip symbols from object files.
     INSTALL_STRIP="-s"
   fi
 fi
@@ -3148,3 +3982,26 @@ for moduledir in $ac_shared_module_dirs $ac_static_module_dirs; do
     fi
   fi
 done
+
+# Display a summary of what modules will be compiled
+echo
+echo "--------------"
+echo "Build Summary"
+echo "--------------"
+if test ! -z "$my_static_modules"; then
+  echo "Building the following static modules:"
+  for amodule in $my_static_modules; do
+   echo "  $amodule"
+  done
+fi
+
+if test ! -z "$my_shared_modules"; then
+  echo
+  echo "Building the following shared modules:"
+  for amodule in $my_shared_modules; do
+    echo "  $amodule"
+  done
+fi
+
+echo
+echo "--------------"
diff --git a/src/pidfile.c b/contrib/dist/coverity/modeling.c
similarity index 52%
copy from src/pidfile.c
copy to contrib/dist/coverity/modeling.c
index 9b8f33c..7be8ec6 100644
--- a/src/pidfile.c
+++ b/contrib/dist/coverity/modeling.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2009 The ProFTPD Project team
+ * Copyright (c) 2014-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,43 +22,52 @@
  * OpenSSL in the source distribution.
  */
 
-/* Pidfile management
- * $Id: pidfile.c,v 1.5 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Coverity modeling file. */
+
+typedef struct module_struct module;
+typedef struct {} pr_table_t;
+typedef struct {} *lt_dlhandle;
 
-#include "conf.h"
-#include "privs.h"
+/* ProFTPD functions. */
+void pr_session_disconnect(module *m, int reason_code, const char *details) {
+  __coverity_panic__();
+}
 
-static const char *pidfile_path = PR_PID_FILE_PATH;
+int pr_table_add(pr_table_t *tab, const char *key_data, void *value_data,
+    size_t value_datasz) {
+  /* ignore */
+}
 
-void pr_pidfile_write(void) {
-  FILE *fh = NULL;
-  const char *path = NULL;
+/* libltdl functions. */
 
-  path = get_param_ptr(main_server->conf, "PidFile", FALSE);
-  if (path != NULL &&
-      *path) {
-    pidfile_path = pstrdup(permanent_pool, path);
+/* Resource leak false positive. */
+lt_dlhandle lt_dlopenext(const char *filename) {
+  /* ignore */
+}
 
-  } else {
-    path = pidfile_path;
-  }
+/* libc functions. */
 
-  PRIVS_ROOT
-  fh = fopen(path, "w");
-  PRIVS_RELINQUISH
+int mkstemp(char *template) {
+  /* ignore */
+}
 
-  if (fh == NULL) {
-    fprintf(stderr, "error opening PidFile '%s': %s\n", path, strerror(errno));
-    exit(1);
-  }
+int rand(void) {
+  /* ignore */
+}
+
+long random(void) {
+  /* ignore */
+}
+
+int setenv(const char *key, const char *value, int overwrite) {
+  __coverity_tainted_data_sink__(key);
+  __coverity_tainted_data_sink__(value);
+}
 
-  fprintf(fh, "%lu\n", (unsigned long) getpid());
-  if (fclose(fh) < 0) {
-    fprintf(stderr, "error writing PidFile '%s': %s\n", path, strerror(errno));
-  }
+char *strerror(int errnum) {
+  /* ignore */
 }
 
-int pr_pidfile_remove(void) {
-  return unlink(pidfile_path);
+void tpl_fatal(char *fmt, ...) {
+  __coverity_panic__();
 }
diff --git a/contrib/dist/rpm/proftpd.service b/contrib/dist/rpm/proftpd.service
index 7749af3..c2fd401 100644
--- a/contrib/dist/rpm/proftpd.service
+++ b/contrib/dist/rpm/proftpd.service
@@ -8,6 +8,8 @@ PIDFile = /run/proftpd/proftpd.pid
 Environment = PROFTPD_OPTIONS=
 EnvironmentFile = -/etc/sysconfig/proftpd
 ExecStart = /usr/sbin/proftpd $PROFTPD_OPTIONS
+ExecStartPost = touch /var/lock/subsys/proftpd
+ExecStopPost = rm -f /var/lock/subsys/proftpd
 ExecReload = /bin/kill -HUP $MAINPID
 
 [Install]
diff --git a/contrib/dist/rpm/proftpd.spec b/contrib/dist/rpm/proftpd.spec
index bc8251f..8d6d862 100644
--- a/contrib/dist/rpm/proftpd.spec
+++ b/contrib/dist/rpm/proftpd.spec
@@ -4,6 +4,9 @@
 #   mod_auth_pam
 #   mod_ban
 #   mod_ctrls_admin
+#   mod_deflate
+#   mod_dnsbl
+#   mod_dynmasq
 #   mod_exec
 #   mod_facl
 #   mod_ifsession
@@ -18,22 +21,30 @@
 #   mod_rewrite
 #   mod_shaper
 #   mod_site_misc
+#   mod_snmp
 #   mod_sql
 #   mod_sql_passwd
 #   mod_wrap2
 #   mod_wrap2_file
+#   mod_wrap2_redis
 #   mod_wrap2_sql
+#   mod_unique_id
 #
 # Dynamic modules with additional build or runtime dependencies, not built by default
 #
+#   mod_auth_otp (needs openssl [--with ssl])
+#   mod_digest (needs openssl [--with ssl])
+#   mod_geoip (needs geoip [--with geoip])
 #   mod_ldap (needs openldap [--with ldap])
 #   mod_quotatab_ldap (needs openldap [--with ldap])
 #   mod_sftp (needs openssl [--with ssl])
 #   mod_sftp_pam (needs openssl [--with ssl])
 #   mod_sftp_sql (needs openssl [--with ssl])
 #   mod_sql_mysql (needs mysql client libraries [--with mysql])
+#   mod_sql_sqlite (needs sqlite libraries [--with sqlite])
 #   mod_sql_postgres (needs postgresql client libraries [--with postgresql])
 #   mod_tls (needs openssl [--with ssl])
+#   mod_tls_fscache (needs openssl [--with ssl])
 #   mod_tls_shmcache (needs openssl [--with ssl])
 #   mod_wrap (needs tcp_wrappers [--with wrap])
 #
@@ -41,19 +52,28 @@
 # RHEL5 and clones don't have suitably recent versions of pcre/libmemcached
 # so use --with rhel5 to inhibit those features when using --with everything
 
-%global proftpd_version           1.3.5
+%global proftpd_version			1.3.6
 
-# When doing a stable or maint release, this line is to be commented out.
-# When doing an RC, define it to be e.g. 'rc2'.
-#
-# NOTE: rpmbuild is really bloody stupid, and CANNOT handle a leading '#'
-# character followed by a '%' character.  
-%global release_cand_version      e
+# rc_version should be incremented for each RC release, and reset back to 1
+# AFTER each stable release.
+%global rc_version			5
+
+# release_version should be incremented for each maint release, and reset back
+# to 1 BEFORE starting new release cycle.
+%global release_version			1
+
+%if %(echo %{proftpd_version} | grep rc >/dev/null 2>&1 && echo 1 || echo 0)
+%global rpm_version %(echo %{proftpd_version} | sed -e 's/rc.*//')
+%global rpm_release 0.%{rc_version}.%(echo %{proftpd_version} | sed -e 's/.*rc/rc/')
+%else
+%global rpm_version %{proftpd_version}
+%global rpm_release %{release_version}
+%endif
 
-%global usecvsversion             0%{?_with_cvs:1}
+%global usecvsversion             	0%{?_with_cvs:1}
 
-%global proftpd_cvs_version_main  1.3.5
-%global proftpd_cvs_version_date  20150527
+%global proftpd_cvs_version_main	1.3.6
+%global proftpd_cvs_version_date  	20150527
 
 # Spec default assumes that a gzipped tarball is used, since nightly CVS builds,
 # release candidates and stable/maint releases are all available in that form;
@@ -67,6 +87,7 @@
 # --with rhel5 inhibits features not available on RHEL5 and clones
 # --with rhel6 inhibits features not available on RHEL6 and clones
 %if 0%{?_with_everything:1}
+%global _with_geoip 1
 %global _with_ldap 1
 %if 0%{!?_with_rhel5:1} && 0%{!?_with_rhel6:1}
 %global _with_memcache 1
@@ -75,11 +96,18 @@
 %if 0%{!?_with_rhel5:1}
 %global _with_pcre 1
 %endif
+%global _with_redis 1
+%global _with_sqlite 1
 %global _with_postgresql 1
 %global _with_ssl 1
 %global _with_wrap 1
 %endif
 #
+# --with geoip (for mod_geoip)
+%if 0%{?_with_geoip:1}
+BuildRequires: geoip-devel
+%endif
+#
 # --with ldap (for mod_ldap, mod_quotatab_ldap)
 %if 0%{?_with_ldap:1}
 BuildRequires: openldap-devel
@@ -105,19 +133,28 @@ BuildRequires: pcre-devel >= 7.0
 BuildRequires: postgresql-devel
 %endif
 #
-# --with ssl (for mod_sftp, mod_sftp_pam, mod_sftp_sql, mod_sql_passwd, mod_tls, mod_tls_shmcache)
+# --with redis (for mod_redis, mod_tls_redis)
+%if 0%{?_with_redis:1}
+BuildRequires: hiredis
+%endif
+# --with ssl (for mod_auth_otp, mod_digest, mod_sftp, mod_sftp_pam, mod_sftp_sql, mod_sql_passwd, mod_tls, mod_tls_fscache, mod_tls_shmcache)
 %if 0%{?_with_ssl:1}
 BuildRequires: openssl-devel
 %endif
 #
+# --with-sqlite (for mod_sql_sqlite)
+%if 0%{?_with_sqlite:1}
+BuildRequires: sqlite-devel
+%endif
+#
 # --with wrap (for mod_wrap)
 %if 0%{?_with_wrap:1}
 # This header file might be in package tcp_wrappers or tcp_wrappers-devel
 BuildRequires: /usr/include/tcpd.h
 %endif
 
-# Assume init is systemd if /run/lock exists, else SysV
-%global use_systemd %([ -d /run/lock ] && echo 1 || echo 0)
+# Assume init is systemd if /run/console exists, else SysV
+%global use_systemd %([ -d /run/console ] && echo 1 || echo 0)
 
 # rundir is /run/proftpd under systemd, else %%{_localstatedir}/run/proftpd
 %if %{use_systemd}
@@ -138,9 +175,9 @@ Version:                %{proftpd_cvs_version_main}
 Release:                0.1.cvs%{proftpd_cvs_version_date}%{?dist}
 Source0:                ftp://ftp.proftpd.org/devel/source/proftpd-cvs-%{proftpd_cvs_version_date}.tar%{srcext}
 %else
-Version:                %{proftpd_version}
-Release:                %{?release_cand_version:0.}1%{?release_cand_version:.%{release_cand_version}}%{?dist}
-Source0:                ftp://ftp.proftpd.org/distrib/source/proftpd-%{version}%{?release_cand_version}.tar%{srcext}
+Version:                %{rpm_version}
+Release:                %{rpm_release}%{?dist}
+Source0:                ftp://ftp.proftpd.org/distrib/source/proftpd-%{proftpd_version}.tar%{srcext}
 %endif
 BuildRoot:              %{_tmppath}/%{name}-%{version}-root
 Requires:               pam >= 0.99, /sbin/chkconfig
@@ -176,6 +213,16 @@ Modules requiring additional dependencies such as mod_sql_mysql, mod_ldap,
 etc. are in separate sub-packages so as not to inconvenience users that
 do not need that functionality.
 
+%if 0%{?_with_geoip:1}
+%package geoip
+Summary:        ProFTPD - Modules relying on GeoIP
+Group:          System Environment/Daemons
+Requires:       proftpd = %{version}-%{release}
+
+%description geoip
+This optional package contains the modules using GeoIP.
+%endif
+
 %if 0%{?_with_ldap:1}
 %package ldap
 Summary:        ProFTPD - Modules relying on LDAP
@@ -196,6 +243,16 @@ Requires:       proftpd = %{version}-%{release}
 This optional package contains the modules using MySQL.
 %endif
 
+%if 0%{?_with_sqlite:1}
+%package sqlite
+Summary:        ProFTPD - Modules relying on SQLite
+Group:          System Environment/Daemons
+Requires:       proftpd = %{version}-%{release}
+
+%description sqlite
+This optional package contains the modules using SQLite.
+%endif
+
 %if 0%{?_with_postgresql:1}
 %package postgresql
 Summary:        ProFTPD - Modules relying on PostgreSQL
@@ -229,12 +286,15 @@ Requires:       pkgconfig
 Requires:       pam-devel
 Requires:       ncurses-devel
 Requires:       zlib-devel
+%{?_with_geoip:Requires:      geoip-devel}
 %{?_with_ldap:Requires:       openldap-devel}
 %{?_with_memcache:Requires:   libmemcached-devel >= 0.41}
 %{?_with_mysql:Requires:      mysql-devel}
 %{?_with_pcre:Requires:       pcre-devel >= 7.0}
 %{?_with_postgresql:Requires: postgresql-devel}
+%{?_with_redis:Requires:      hiredis}
 %{?_with_ssl:Requires:        openssl-devel}
+%{?_with_sqlite:Requires:     sqlite-devel}
 %{?_with_wrap:Requires:       /usr/include/tcpd.h}
 
 %description devel
@@ -260,7 +320,7 @@ ProFTPD server:
 %if %{usecvsversion}
 %setup -q -n %{name}-%{proftpd_cvs_version_main}
 %else
-%setup -q -n %{name}-%{version}%{?release_cand_version}
+%setup -q -n %{name}-%{proftpd_version}
 %endif
 
 # Avoid documentation name conflicts
@@ -281,6 +341,9 @@ fi
 STANDARD_MODULE_LIST="  mod_auth_pam            \
                         mod_ban                 \
                         mod_ctrls_admin         \
+                        mod_deflate             \
+                        mod_dnsbl               \
+                        mod_dynmasq             \
                         mod_exec                \
                         mod_facl                \
                         mod_load                \
@@ -294,12 +357,18 @@ STANDARD_MODULE_LIST="  mod_auth_pam            \
                         mod_rewrite             \
                         mod_shaper              \
                         mod_site_misc           \
+                        mod_snmp                \
                         mod_sql                 \
                         mod_wrap2               \
                         mod_wrap2_file          \
-                        mod_wrap2_sql           "
+                        mod_wrap2_redis         \
+                        mod_wrap2_sql           \
+                        mod_unique_id           "
 
 OPTIONAL_MODULE_LIST="                          \
+%{?_with_ssl:           mod_auth_otp}           \
+%{?_with_ssl:           mod_digest}             \
+%{?_with_geoip:         mod_geoip}              \
 %{?_with_ldap:          mod_ldap}               \
 %{?_with_ldap:          mod_quotatab_ldap}      \
 %{?_with_ssl:           mod_sftp}               \
@@ -307,10 +376,13 @@ OPTIONAL_MODULE_LIST="                          \
 %{?_with_ssl:           mod_sftp_sql}           \
 %{?_with_mysql:         mod_sql_mysql}          \
 %{?_with_ssl:           mod_sql_passwd}         \
+%{?_with_sqlite:        mod_sql_sqlite}         \
 %{?_with_postgresql:    mod_sql_postgres}       \
 %{?_with_ssl:           mod_tls}                \
+%{?_with_ssl:           mod_tls_fscache}        \
 %{?_with_ssl:           mod_tls_shmcache}       \
 %{?_with_ssl:%{?_with_memcache:mod_tls_memcache}} \
+%{?_with_ssl:%{?_with_redis:mod_tls_redis}}     \
 %{?_with_wrap:          mod_wrap}               "
 
 MODULE_LIST=$(echo ${STANDARD_MODULE_LIST} ${OPTIONAL_MODULE_LIST} mod_ifsession | tr -s '[:space:]' ':' | sed 's/:$//')
@@ -326,6 +398,7 @@ MODULE_LIST=$(echo ${STANDARD_MODULE_LIST} ${OPTIONAL_MODULE_LIST} mod_ifsession
         --enable-nls \
         %{?_with_memcache:--enable-memcache} \
         %{?_with_pcre:--enable-pcre} \
+        %{?_with_redis:--enable-redis} \
         %{?_with_ssl:--enable-openssl} \
         --enable-shadow \
         --with-lastlog \
@@ -352,10 +425,14 @@ install -p -m 644 contrib/dist/rpm/proftpd.pam %{buildroot}/etc/pam.d/proftpd
 install -m 644 contrib/dist/rpm/basic-pam.conf %{buildroot}/etc/proftpd.conf
 
 %if %{use_systemd}
-# Systemd unit file
+# Systemd unit files
 mkdir -p %{buildroot}%{_unitdir}
 install -p -m 644 contrib/dist/rpm/proftpd.service \
     %{buildroot}%{_unitdir}/proftpd.service
+install -p -m 644 contrib/dist/systemd/proftpd at .service \
+    %{buildroot}%{_unitdir}/proftpd at .service
+install -p -m 644 contrib/dist/systemd/proftpd.socket \
+    %{buildroot}%{_unitdir}/proftpd.socket
 # Ensure /run/proftpd exists
 mkdir -p %{buildroot}%{_sysconfdir}/tmpfiles.d
 install -p -m 644 contrib/dist/rpm/proftpd-tmpfs.conf \
@@ -440,16 +517,23 @@ rm -rf %{_builddir}/%{name}-%{version}
 
 %files -f proftpd.lang
 %{_bindir}/ftpdctl
+%{?_with_ssl:%{_sbindir}/auth-otp}
 %{_sbindir}/ftpscrub
 %{_sbindir}/ftpshut
 %{_sbindir}/in.proftpd
 %{_sbindir}/proftpd
 %dir %{_libexecdir}/proftpd/
+%{?_with_ssl:%{_libexecdir}/proftpd/mod_auth_otp.so}
 %{_libexecdir}/proftpd/mod_auth_pam.so
 %{_libexecdir}/proftpd/mod_ban.so
 %{_libexecdir}/proftpd/mod_ctrls_admin.so
+%{_libexecdir}/proftpd/mod_deflate.so
+%{?_with_ssl:%{_libexecdir}/proftpd/mod_digest.so}
+%{_libexecdir}/proftpd/mod_dnsbl.so
+%{_libexecdir}/proftpd/mod_dynmasq.so
 %{_libexecdir}/proftpd/mod_exec.so
 %{_libexecdir}/proftpd/mod_facl.so
+%{?_with_geoip:%{_libexecdir}/proftpd/mod_geoip.so}
 %{_libexecdir}/proftpd/mod_ifsession.so
 %{_libexecdir}/proftpd/mod_load.so
 %{_libexecdir}/proftpd/mod_quotatab.so
@@ -465,14 +549,19 @@ rm -rf %{_builddir}/%{name}-%{version}
 %{?_with_ssl:%{_libexecdir}/proftpd/mod_sftp_sql.so}
 %{_libexecdir}/proftpd/mod_shaper.so
 %{_libexecdir}/proftpd/mod_site_misc.so
+%{_libexecdir}/proftpd/mod_snmp.so
 %{_libexecdir}/proftpd/mod_sql.so
 %{?_with_ssl:%{_libexecdir}/proftpd/mod_sql_passwd.so}
 %{?_with_ssl:%{_libexecdir}/proftpd/mod_tls.so}
+%{?_with_ssl:%{_libexecdir}/proftpd/mod_tls_fscache.so}
 %{?_with_ssl:%{?_with_memcache:%{_libexecdir}/proftpd/mod_tls_memcache.so}}
+%{?_with_ssl:%{?_with_redis:%{_libexecdir}/proftpd/mod_tls_redis.so}}
 %{?_with_ssl:%{_libexecdir}/proftpd/mod_tls_shmcache.so}
 %{_libexecdir}/proftpd/mod_wrap2.so
 %{_libexecdir}/proftpd/mod_wrap2_file.so
+%{_libexecdir}/proftpd/mod_wrap2_redis.so
 %{_libexecdir}/proftpd/mod_wrap2_sql.so
+%{_libexecdir}/proftpd/mod_unique_id.so
 %exclude %{_libexecdir}/proftpd/*.a
 %exclude %{_libexecdir}/proftpd/*.la
 %dir %{rundir}/
@@ -480,6 +569,8 @@ rm -rf %{_builddir}/%{name}-%{version}
 %dir %{_localstatedir}/ftp/pub/
 %if %{use_systemd}
 %{_unitdir}/proftpd.service
+%{_unitdir}/proftpd at .service
+%{_unitdir}/proftpd.socket
 %{_sysconfdir}/tmpfiles.d/proftpd.conf
 %else
 %{_sysconfdir}/rc.d/init.d/proftpd
@@ -490,11 +581,13 @@ rm -rf %{_builddir}/%{name}-%{version}
 %config(noreplace) %{_sysconfdir}/pam.d/proftpd
 %config(noreplace) %{_sysconfdir}/logrotate.d/proftpd
 %config(noreplace) %{_sysconfdir}/xinetd.d/proftpd
+%config(noreplace) %{_sysconfdir}/PROFTPD-MIB.txt
 
-%doc COPYING CREDITS ChangeLog NEWS README RELEASE_NOTES
+%doc COPYING CREDITS ChangeLog NEWS README.md RELEASE_NOTES
 %doc README.DSO README.modules README.IPv6 README.PAM
 %doc README.capabilities README.classes README.controls README.facl
 %doc contrib/README.contrib contrib/README.ratio
+%doc contrib/dist/systemd/README.systemd
 %doc doc/* sample-configurations/
 %{_mandir}/man5/proftpd.conf.5*
 %{_mandir}/man5/xferlog.5*
@@ -502,6 +595,7 @@ rm -rf %{_builddir}/%{name}-%{version}
 %{_mandir}/man8/ftpscrub.8*
 %{_mandir}/man8/ftpshut.8*
 %{_mandir}/man8/proftpd.8*
+%{?_with_ssl:%{_mandir}/man8/auth-otp.8*}
 
 %if 0%{?_with_ldap:1}
 %files ldap
@@ -520,6 +614,11 @@ rm -rf %{_builddir}/%{name}-%{version}
 %{_libexecdir}/proftpd/mod_sql_postgres.so
 %endif
 
+%if 0%{?_with_sqlite:1}
+%files sqlite
+%{_libexecdir}/proftpd/mod_sql_sqlite.so
+%endif
+
 %if 0%{?_with_wrap:1}
 %files wrap
 %{_libexecdir}/proftpd/mod_wrap.so
@@ -546,6 +645,12 @@ rm -rf %{_builddir}/%{name}-%{version}
 %{_mandir}/man1/ftpwho.1*
 
 %changelog
+* Fri Dec 11 2015 Paul Howarth <paul at city-fan.org>
+- Include systemd unit files for native inetd operation (bug 3661)
+- Use /run/console rather than /run/lock for systemd detection, because the
+  'mock' build tool may create /run/lock itself
+- Fix bogus dates in spec changelog
+
 * Fri Jun 28 2013 Paul Howarth <paul at city-fan.org>
 - Support arbitrary tarball compression types using %%{srcext} macro
 - Package proftpd.conf manpage
@@ -573,7 +678,7 @@ rm -rf %{_builddir}/%{name}-%{version}
   - Create new utils subpackage
   - Lots of minor fixes
 
-* Mon Sep 11 2007 Philip Prindeville <philipp_subx at redfish-solutions.com>
+* Mon Sep 10 2007 Philip Prindeville <philipp_subx at redfish-solutions.com>
 - Cleaned up the .spec file to work with more recent releases of RPM.  Moved
   header files into separate component.
 
@@ -617,16 +722,16 @@ rm -rf %{_builddir}/%{name}-%{version}
   For details see http://bugs.proftpd.org/show_bug.cgi?id=1048
 - release: 1.2.1-2
 
-* Wed Mar 01 2001 Daniel Roesen <droesen at entire-systems.com>
+* Thu Mar 01 2001 Daniel Roesen <droesen at entire-systems.com>
 - Update to 1.2.1
 - release: 1.2.1-1
 
-* Wed Feb 27 2001 Daniel Roesen <droesen at entire-systems.com>
+* Tue Feb 27 2001 Daniel Roesen <droesen at entire-systems.com>
 - added "Obsoletes: proftpd-core" to make migration to new RPMs easier.
   Thanks to Sébastien Prud'homme <prudhomme at easy-flying.com> for the hint.
 - release: 1.2.0-3
 
-* Wed Feb 26 2001 Daniel Roesen <droesen at entire-systems.com>
+* Mon Feb 26 2001 Daniel Roesen <droesen at entire-systems.com>
 - cleaned up .spec formatting (cosmetics)
 - fixed CFLAGS (fixes /etc/shadow support)
 - included COPYING, CREDITS, ChangeLog and NEWS
@@ -639,7 +744,7 @@ rm -rf %{_builddir}/%{name}-%{version}
 - removed /ftp/ftpusers from package management. Deinstalling ProFTPD
   should _not_ result in removal of this file.
 
-* Thu Oct 03 1999 O.Elliyasa <osman at Cable.EU.org>
+* Sun Oct 03 1999 O.Elliyasa <osman at Cable.EU.org>
 - Multi package creation.
   Created core, standalone, inetd (&doc) package creations.
   Added startup script for init.d
diff --git a/contrib/dist/systemd/README.systemd b/contrib/dist/systemd/README.systemd
new file mode 100644
index 0000000..2874302
--- /dev/null
+++ b/contrib/dist/systemd/README.systemd
@@ -0,0 +1,25 @@
+Using proftpd with systemd's built-in inetd functionality
+---------------------------------------------------------
+
+On systemd-based systems, you can use systemd's built-in inetd
+functionality to start proftpd on demand. Note that this is not
+full "socket activation" where systemd would start a single
+long-running process to handle the current and subsequent
+connections, but a simple inetd equivalent where a new instance
+of proftpd is run for each new incoming connection, each session
+lasting until its connection drops. This makes it best suited to
+low-volume applications.
+
+1. Install the files proftpd.socket and proftpd at .service from the
+   contrib/dist/systemd directory into either the system unit directory
+   (usually /usr/lib/systemd/system - recommended for downstream
+   packagers) or the local unit directory (/etc/systemd/system -
+   recommended for admins installing from source locally).
+
+2. Set "ServerType inetd" in /etc/proftpd.conf
+
+3. Run "systemctl enable proftpd.socket" as root to enable
+   listening for connections at boot time.
+
+4. Run "systemctl start proftpd.socket" as root to start
+   listening for connections immediately.
diff --git a/contrib/dist/systemd/proftpd.socket b/contrib/dist/systemd/proftpd.socket
new file mode 100644
index 0000000..6fc2c49
--- /dev/null
+++ b/contrib/dist/systemd/proftpd.socket
@@ -0,0 +1,16 @@
+# Note that to use proftpd via this socket file, you will need:
+#
+#   ServerType inetd
+#
+# in /etc/proftpd.conf
+
+[Unit]
+Description=ProFTPD FTP Server Activation Socket
+Conflicts=proftpd.service
+
+[Socket]
+ListenStream=21
+Accept=true
+
+[Install]
+WantedBy=sockets.target
diff --git a/contrib/dist/systemd/proftpd at .service b/contrib/dist/systemd/proftpd at .service
new file mode 100644
index 0000000..fc0bf9c
--- /dev/null
+++ b/contrib/dist/systemd/proftpd at .service
@@ -0,0 +1,7 @@
+[Unit]
+Description=ProFTPD FTP Server
+After=local-fs.target
+
+[Service]
+ExecStart=-/usr/sbin/in.proftpd
+StandardInput=socket
diff --git a/contrib/dist/travis/docker-rpmbuild.sh b/contrib/dist/travis/docker-rpmbuild.sh
new file mode 100644
index 0000000..d4efb2a
--- /dev/null
+++ b/contrib/dist/travis/docker-rpmbuild.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+BRANCH=${TRAVIS_BRANCH:-master}
+VERSION=${PACKAGE_VERSION:-1.3.6rc5}
+
+# Make sure that the necessary packages/tools are installed
+yum install -y gcc make git rpm-build imake
+
+# These are for the basic proftpd build
+yum install -y gettext pkgconfig pam-devel ncurses-devel zlib-devel libacl-devel libcap-devel
+
+# And these are for --with everything
+yum install -y openldap-devel libmemcached-devel mysql-devel pcre-devel postgresql-devel openssl-devel tcp_wrappers-devel sqlite-devel geoip-devel
+
+# Install the EPEL repo, for the Redis RPMs
+yum install -y wget
+wget -q -r --no-parent -A 'epel-release-*.rpm' http://dl.fedoraproject.org/pub/epel/7/x86_64/e
+rpm -Uvh dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-*.rpm
+yum install -y hiredis-devel
+
+rm -fr rpm/
+mkdir rpm/
+cd rpm/
+git clone -q -b ${BRANCH} --depth 10 https://github.com/proftpd/proftpd.git proftpd-${VERSION}
+cd proftpd-${VERSION}/
+./configure
+make dist
+cd ..
+tar zcf proftpd-${VERSION}.tar.gz proftpd-${VERSION}
+rpmbuild -ta proftpd-${VERSION}.tar.gz --with everything
diff --git a/contrib/dist/vagrant/README.md b/contrib/dist/vagrant/README.md
new file mode 100644
index 0000000..7a8ace6
--- /dev/null
+++ b/contrib/dist/vagrant/README.md
@@ -0,0 +1,9 @@
+# Example Vagrantfile for ProFTPD
+
+For folks wanting to develop/test ProFTPD using Vagrant, here is an example
+`Vagrantfile`, which installs the `proftpd-basic` package on an Ubuntu 14.04
+box.  The installed server can be reached using:
+
+    $ ftp vagrant at 192.168.21.21
+
+where the password is the same as the username.
diff --git a/contrib/dist/vagrant/Vagrantfile b/contrib/dist/vagrant/Vagrantfile
new file mode 100644
index 0000000..f034ef1
--- /dev/null
+++ b/contrib/dist/vagrant/Vagrantfile
@@ -0,0 +1,21 @@
+Vagrant.configure("2") do |config|
+  config.vm.box = "ubuntu/trusty64"
+  config.vm.box_check_update = false
+
+  config.vm.provider "virtualbox" do |vb|
+    vb.gui = false
+    vb.memory = "2048"
+  end
+
+  config.vm.provision "shell", inline: <<-SHELL
+    apt-get update
+    apt-get install -y debconf-utils
+    export DEBIAN_FRONTEND=noninteractive
+    echo "proftpd-basic shared/proftpd/inetd_or_standalone select standalone" | debconf-set-selections 
+    apt-get install -y git gcc make lftp proftpd
+  SHELL
+
+  # Create a private network, which allows host-only access to the machine
+  # using a specific IP.
+  config.vm.network "private_network", ip: "192.168.21.21"
+end
diff --git a/contrib/ftpasswd b/contrib/ftpasswd
index c42c789..de2e20d 100755
--- a/contrib/ftpasswd
+++ b/contrib/ftpasswd
@@ -1,6 +1,6 @@
 #!/usr/bin/env perl
 # ---------------------------------------------------------------------------
-# Copyright (C) 2000-2013 TJ Saunders <tj at castaglia.org>
+# Copyright (C) 2000-2015 TJ Saunders <tj at castaglia.org>
 #
 # 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
@@ -20,8 +20,6 @@
 # files suitable for use with proftpd's AuthUserFile directive, in passwd(5)
 # format, or AuthGroupFile, in group(5) format.  The idea is somewhat similar
 # to Apache's htpasswd program.
-#
-#  $Id: ftpasswd,v 1.22 2013-12-10 17:26:30 castaglia Exp $
 # ---------------------------------------------------------------------------
 
 use strict;
@@ -80,6 +78,26 @@ usage() if (defined($opts{'h'}));
 
 version() if (defined($opts{'version'}));
 
+# Per Bug#4171, check if we are on a Linux system, AND it has the
+# /proc/sys/crypto/fips_enabled file, AND that entry says that FIPS mode is
+# enabled.  If these conditions are met, then neither DES or MD5 will work.
+#
+# If either --des or --md5 are specified, OR if no hash is specified, we
+# need to check.
+if ((defined($opts{'des'}) || defined($opts{'md5'})) ||
+     !defined($opts{'des'}) && !defined($opts{'md5'}) &&
+     !defined($opts{'sha256'}) && !defined($opts{'sha512'})) {
+  if (open(my $fh, "< /proc/sys/crypto/fips_enabled")) {
+    my $fips_enabled = <$fh>;
+    close($fh);
+
+    chomp($fips_enabled);
+    if ($fips_enabled) {
+      die "$program: FIPS mode enabled on your system (see /proc/sys/crypto/fips_enabled), thus --des and --md5 will not be supported.  Use --sha256 or --sha512.\n"
+    }
+  }
+}
+
 # check if "use-cracklib" was given as an option, and whether a path
 # to other dictionary files was given.
 if (defined($opts{'use-cracklib'})) {
@@ -452,17 +470,31 @@ sub get_passwd {
 
   } else {
 
-    # prompt for the password to be used
+    # Install a SIGINT handler, for cases where Ctrl-C may be used to abort
+    # the prompt.
+    $SIG{INT} = sub {
+      # Restore the file permissions.
+      unless (chmod(0440, $output_file)) {
+        print STDERR "$program: unable to set permissions on $output_file: $!";
+      }
+
+      # Restore the terminal echo behavior, too.
+      system "stty echo";
+
+      exit 1;
+    };
+
+    # Prompt for the password to be used
     system "stty -echo";
     print STDOUT "\nPassword: ";
 
-    # open the tty for reading (is this portable?)
+    # Open the tty for reading (is this portable?)
     open(TTY, "/dev/tty") or die "$program: unable to open /dev/tty: $!\n";
     chomp($passwd = <TTY>);
     print STDOUT "\n";
     system "stty echo";
 
-    # prompt again, to make sure the user typed in the password correctly
+    # Prompt again, to make sure the user typed in the password correctly
     system "stty -echo";
     print STDOUT "Re-type password: ";
     chomp($passwd2 = <TTY>);
@@ -470,6 +502,9 @@ sub get_passwd {
     system "stty echo";
     close(TTY);
 
+    # Restore default SIGINT handling
+    $SIG{INT} = 'DEFAULT';
+
     if ($passwd2 ne $passwd) {
       print STDOUT "Passwords do not match.  Please try again.\n";
       return get_passwd(name => $name);
@@ -832,7 +867,7 @@ sub open_output_file {
 
   if (-f $output_file) {
     # make sure we can write/update the file first
-    unless (chmod 0644, $output_file) {
+    unless (chmod(0644, $output_file)) {
       die "$program: unable to set permissions on $output_file to 0644: $!\n";
     }
 
diff --git a/contrib/ftpquota b/contrib/ftpquota
index add5be0..e641722 100755
--- a/contrib/ftpquota
+++ b/contrib/ftpquota
@@ -1,6 +1,6 @@
 #!/usr/bin/env perl
 # -------------------------------------------------------------------------
-# Copyright (C) 2000-2013 TJ Saunders <tj at castaglia.org>
+# Copyright (C) 2000-2017 TJ Saunders <tj at castaglia.org>
 #
 # 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
@@ -15,8 +15,6 @@
 # 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.
-#
-#  $Id: ftpquota,v 1.6 2013-04-23 23:25:20 castaglia Exp $
 # -------------------------------------------------------------------------
 
 use strict;
@@ -1032,25 +1030,35 @@ usage: $program [options]
  The following options are used to specify specific quota limits:
 
   --Bu                 Specifies the limit of the number of bytes that may be
-  --bytes-upload       uploaded.  Defaults to -1 (unlimited).
+  --bytes-upload       uploaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero is treated as
+                       "unlimited".
 
   --Bd                 Specifies the limit of the number of bytes that may be
-  --bytes-download     downloaded.  Defaults to -1 (unlimited).
+  --bytes-download     downloaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero is treated as
+                       "unlimited".
 
   --Bx                 Specifies the limit of the number of bytes that may be
   --bytes-xfer         transferred.  Note that this total includes uploads,
                        downloads, AND directory listings.  Defaults to
-                       -1 (unlimited).
+                       -1 (unlimited).  Note that any value value less than or
+                       equal to zero is treated as "unlimited".
 
   --Fu                 Specifies the limit of the number of files that may be
-  --files-upload       uploaded.  Defaults to -1 (unlimited).
+  --files-upload       uploaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero is treated as
+                       "unlimited".
 
   --Fd                 Specifies the limit of the number of files that may be
-  --files-download     downloaded.  Defaults to -1 (unlimited).
+  --files-download     downloaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero is treated as
+                       "unlimited".
 
   --Fx                 Specifies the limit of the number of files that may be
   --files-xfer         transferred, including uploads and downloads.  Defaults
-                       to -1 (unlimited).
+                       to -1 (unlimited).  Note that any value less than or
+                       equal to zero is treated as "unlimited".
 
   -L                   Specifies the type of limit, "hard" or "soft", of
   --limit-type         the bytes limits.  If "hard", any uploaded files that
diff --git a/contrib/mod_snmp/Makefile.in b/contrib/mod_auth_otp/Makefile.in
similarity index 53%
copy from contrib/mod_snmp/Makefile.in
copy to contrib/mod_auth_otp/Makefile.in
index 1274e13..6570784 100644
--- a/contrib/mod_snmp/Makefile.in
+++ b/contrib/mod_auth_otp/Makefile.in
@@ -2,19 +2,22 @@ top_builddir=../..
 top_srcdir=../..
 srcdir=@srcdir@
 
-include $(top_srcdir)/Make.rules
+include ../../Make.rules
 
 .SUFFIXES: .la .lo
 
+EXEEXT=@EXEEXT@
 SHARED_CFLAGS=-DPR_SHARED_MODULE
 SHARED_LDFLAGS=-avoid-version -export-dynamic -module
 VPATH=@srcdir@
 
-MODULE_NAME=mod_snmp
-MODULE_OBJS=mod_snmp.o stacktrace.o asn1.o smi.o pdu.o msg.o db.o mib.o \
-  packet.o uptime.o notify.o
-SHARED_MODULE_OBJS=mod_snmp.lo stacktrace.lo asn1.lo smi.lo pdu.lo msg.lo \
-  db.lo mib.lo packet.lo uptime.lo notify.lo
+MODULE_NAME=mod_auth_otp
+MODULE_OBJS=mod_auth_otp.o base32.o otp.o crypto.o db.o
+SHARED_MODULE_OBJS=mod_auth_otp.lo base32.lo otp.lo crypto.lo db.lo
+UTILS_LIBS=@UTILS_LIBS@
+UTILS_OBJS=base32.o otp.o crypto.o auth-otp.o
+UTILS_API_OBJS=../../src/pool.o \
+  ../../src/str.o
 
 # Necessary redefinitions
 INCLUDES=-I. -I../.. -I../../include @INCLUDES@
@@ -30,21 +33,35 @@ LDFLAGS=-L../../lib @LDFLAGS@
 shared: $(SHARED_MODULE_OBJS)
 	$(LIBTOOL) --mode=link --tag=CC $(CC) -o $(MODULE_NAME).la $(SHARED_MODULE_OBJS) -rpath $(LIBEXECDIR) $(LDFLAGS) $(SHARED_LDFLAGS) $(SHARED_MODULE_LIBS) `cat $(MODULE_NAME).c | grep '$$Libraries:' | sed -e 's/^.*\$$Libraries: \(.*\)\\$$/\1/'`
 
-static: $(MODULE_OBJS)
+static: $(MODULE_OBJS) auth-otp$(EXEEXT)
 	$(AR) rc $(MODULE_NAME).a $(MODULE_OBJS)
 	$(RANLIB) $(MODULE_NAME).a
 
-install: install-misc
+auth-otp$(EXEEXT): $(UTILS_OBJS)
+	$(CC) $(LDFLAGS) -o $@ $(UTILS_OBJS) $(UTILS_API_OBJS) $(UTILS_LIBS)
+
+install: install-man install-utils
 	if [ -f $(MODULE_NAME).la ] ; then \
 		$(LIBTOOL) --mode=install --tag=CC $(INSTALL_BIN) $(MODULE_NAME).la $(DESTDIR)$(LIBEXECDIR) ; \
 	fi
 
-install-misc:
-	$(INSTALL) -o $(INSTALL_USER) -g $(INSTALL_GROUP) -m 0644 PROFTPD-MIB.txt $(DESTDIR)$(sysconfdir)/PROFTPD-MIB.txt
+# BSD install -d doesn't work, so ...
+$(DESTDIR)$(mandir) $(DESTDIR)$(mandir)/man1 $(DESTDIR)$(mandir)/man5 $(DESTDIR)$(mandir)/man8:
+	@if [ ! -d $@ ]; then \
+		mkdir -p $@; \
+		chown $(INSTALL_USER):$(INSTALL_GROUP) $@; \
+		chmod 0755 $@; \
+	fi
+
+install-man: $(DESTDIR)$(mandir) $(DESTDIR)$(mandir)/man8
+	$(INSTALL_MAN) auth-otp.8 $(DESTDIR)$(mandir)/man8
+
+install-utils: $(DESTDIR)$(sbindir) auth-otp$(EXEEXT)
+	$(INSTALL_BIN) auth-otp$(EXEEXT) $(DESTDIR)$(sbindir)/auth-otp$(EXEEXT)
 
 clean:
 	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).a $(MODULE_NAME).la *.o *.lo .libs/*.o
 
 dist: clean
-	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log
-	-$(RM) -r .git/ CVS/ RCS/
+	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log *.gcda *.gcno
+	-$(RM) -r CVS/ RCS/
diff --git a/contrib/mod_auth_otp/auth-otp.8 b/contrib/mod_auth_otp/auth-otp.8
new file mode 100644
index 0000000..b878fdd
--- /dev/null
+++ b/contrib/mod_auth_otp/auth-otp.8
@@ -0,0 +1,34 @@
+.TH auth-otp 8 "January 2016"
+.\" Process with
+.\" groff -man -Tascii auth-otp.8 
+.\"
+.SH NAME
+auth\-otp \- generate HOTP/TOTP secrets for mod_auth_otp ProFTPD module
+.SH SYNOPSIS
+.B auth\-otp
+.SH DESCRIPTION
+The
+.BI auth\-otp
+command provides a way to generate the initial HOTP or TOTP secrets for
+use by the mod_auth_otp ProFTPD module.
+.SH OPTIONS
+.TP 12
+.B \-h,\--help
+Display a short usage description, including all available options.
+.TP 12
+.B \-q,\--quiet
+Display only the generated secret, not anything else.
+.TP
+.B \-v,\--verbose
+Reports additional information while generating secrets.
+.SH FILES
+.PD 0
+.B /usr/local/sbin/auth\-otp
+.PD
+.SH AUTHORS
+.PP
+ProFTPD is written and maintained by a number of people, full credits
+can be found on
+.BR http://www.proftpd.org/credits.html
+.PD
+.PP
diff --git a/contrib/mod_auth_otp/auth-otp.c b/contrib/mod_auth_otp/auth-otp.c
new file mode 100644
index 0000000..872b0d8
--- /dev/null
+++ b/contrib/mod_auth_otp/auth-otp.c
@@ -0,0 +1,290 @@
+/*
+ * auth-otp: HOTP/TOTP tool for ProFTPD mod_auth_otp module
+ * Copyright 2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project 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.
+ */
+
+#include "config.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#ifdef HAVE_GETOPT_H
+#  include <getopt.h>
+#else
+#  include "../lib/getopt.h"
+#endif /* !HAVE_GETOPT_H */
+
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif
+
+#ifdef HAVE_NETINET_IN_H
+# include <netinet/in.h>
+#endif
+
+#include "mod_auth_otp.h"
+#include "pool.h"
+#include "base32.h"
+#include "crypto.h"
+#include "otp.h"
+
+static int quiet = FALSE, verbose = FALSE;
+
+/* Necessary stubs. */
+
+int auth_otp_logfd = -1;
+pool *auth_otp_pool = NULL;
+
+void pr_alarms_block(void) {
+}
+
+void pr_alarms_unblock(void) {
+}
+
+void pr_log_pri(int prio, const char *fmt, ...) {
+  if (verbose) {
+    va_list msg;
+
+    fprintf(stderr, "PRI%d: ", prio);
+
+    va_start(msg, fmt);
+    vfprintf(stderr, fmt, msg);
+    va_end(msg);
+
+    fprintf(stderr, "\n");
+  }
+}
+
+int pr_log_writefile(int fd, const char *prefix, const char *fmt, ...) {
+  if (verbose) {
+    va_list msg;
+
+    fprintf(stderr, "%s: ", prefix);
+
+    va_start(msg, fmt);
+    vfprintf(stderr, fmt, msg);
+    va_end(msg);
+
+    fprintf(stderr, "\n");
+  }
+
+  return 0;
+}
+
+void pr_memscrub(void *ptr, size_t ptrlen) {
+  if (ptr == NULL ||
+      ptrlen == 0) {
+    return;
+  }
+
+  OPENSSL_cleanse(ptr, ptrlen);
+}
+
+module *pr_module_get(const char *name) {
+  errno = ENOENT;
+  return NULL;
+}
+
+void pr_signals_handle(void) {
+}
+
+int pr_trace_msg(const char *name, int level, const char *fmt, ...) {
+  if (verbose) {
+    va_list msg;
+
+    fprintf(stderr, "<%s:%d>: ", name, level);
+
+    va_start(msg, fmt);
+    vfprintf(stderr, fmt, msg);
+    va_end(msg);
+
+    fprintf(stderr, "\n");
+  }
+
+  return 0;
+}
+
+static struct option_help {
+  const char *long_opt, *short_opt, *desc;
+} opts_help[] = {
+  { "--help",	"-h",	NULL },
+  { "--quiet",	"-q",	NULL },
+  { "--verbose","-v",	NULL },
+  { NULL }
+};
+
+#ifdef HAVE_GETOPT_LONG
+static struct option opts[] = {
+  { "help",    0, NULL, 'h' },
+  { "quiet",   0, NULL, 'q' },
+  { "verbose", 0, NULL, 'v' },
+  { NULL,      0, NULL, 0   }
+};
+#endif /* HAVE_GETOPT_LONG */
+
+static void show_usage(const char *progname, int exit_code) {
+  struct option_help *h = NULL;
+
+  printf("usage: %s [options]\n", progname);
+  for (h = opts_help; h->long_opt; h++) {
+#ifdef HAVE_GETOPT_LONG
+    printf("  %s, %s\n", h->short_opt, h->long_opt);
+#else /* HAVE_GETOPT_LONG */
+    printf("  %s\n", h->short_opt);
+#endif
+    if (h->desc == NULL) {
+      printf("    display %s usage\n", progname);
+
+    } else {
+      printf("    %s\n", h->desc);
+    }
+  }
+
+  exit(exit_code);
+}
+
+static int generate_code(pool *p, const char *key, size_t key_len) {
+  int res;
+  unsigned int code;
+
+  res = auth_otp_hotp(p, (const unsigned char *) key, key_len, 0, &code);
+  if (res < 0) {
+    return -1;
+  }
+
+  return (int) code;
+}
+
+static const char *generate_secret(pool *p) {
+  int res;
+  unsigned char encoded[18], rnd[32];
+  size_t encoded_len;
+  const char *secret;
+  const unsigned char *ptr;
+
+  if (RAND_bytes(rnd, sizeof(rnd)) != 1) {
+    fprintf(stderr, "Error obtaining %lu bytes of random data:\n",
+      (unsigned long) sizeof(rnd));
+    ERR_print_errors_fp(stderr);
+    errno = EPERM;
+    return NULL;
+  }
+
+  encoded_len = sizeof(encoded);
+  ptr = encoded;
+  res = auth_otp_base32_encode(p, rnd, sizeof(rnd), &ptr, &encoded_len);
+  if (res < 0) {
+    return NULL;
+  }
+
+  secret = pstrndup(p, (const char *) ptr, encoded_len);
+  return secret;
+}
+
+int main(int argc, char **argv) {
+  int c = 0;
+  char *ptr, *progname = *argv;
+  const char *cmdopts = "hqv", *secret = NULL;
+
+  ptr = strrchr(progname, '/');
+  if (ptr != NULL) {
+    progname = ptr+1;
+  }
+
+  opterr = 0;
+  while ((c =
+#ifdef HAVE_GETOPT_LONG
+	 getopt_long(argc, argv, cmdopts, opts, NULL)
+#else /* HAVE_GETOPT_LONG */
+	 getopt(argc, argv, cmdopts)
+#endif /* HAVE_GETOPT_LONG */
+	 ) != -1) {
+    switch (c) {
+      case 'h':
+        show_usage(progname, 0);
+        break;
+
+      case 'q':
+        verbose = FALSE;
+        quiet = TRUE;
+        break;
+
+      case 'v':
+        quiet = FALSE;
+        verbose = TRUE;
+        break;
+
+      case '?':
+        fprintf(stderr, "unknown option: %c\n", (char) optopt);
+        show_usage(progname, 1);
+        break;
+    }
+  }
+
+  auth_otp_pool = make_sub_pool(NULL);
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  OPENSSL_config(NULL);
+#endif /* prior to OpenSSL-1.1.x */
+  ERR_load_crypto_strings();
+  OpenSSL_add_all_algorithms();
+
+  secret = generate_secret(auth_otp_pool);  
+  if (secret == NULL) {
+    return 1;
+  }
+
+  if (quiet) {
+    fprintf(stdout, "%s\n", secret);
+
+  } else {
+    int code;
+
+    code = generate_code(auth_otp_pool, secret, strlen(secret));
+    if (code < 0) {
+      fprintf(stderr, "%s: error generating verification code: %s\n", progname,
+        strerror(errno));
+      destroy_pool(auth_otp_pool);
+      return 1;
+    }
+
+    fprintf(stdout, "-------------------------------------------------\n");
+    fprintf(stdout, "Your new secret key is: %s\n\n", secret);
+    fprintf(stdout, "To add this key to your SQL table, you might use:\n\n");
+    fprintf(stdout, "  INSERT INTO auth_otp (secret, counter) VALUES ('%s', 0);\n\n",
+      secret);
+    fprintf(stdout, "Your verification code is: %06d\n", code);
+    fprintf(stdout, "-------------------------------------------------\n");
+  }
+
+  ERR_free_strings();
+  EVP_cleanup();
+  RAND_cleanup();
+
+  destroy_pool(auth_otp_pool);
+  return 0;
+}
diff --git a/contrib/mod_auth_otp/base32.c b/contrib/mod_auth_otp/base32.c
new file mode 100644
index 0000000..bf5cf56
--- /dev/null
+++ b/contrib/mod_auth_otp/base32.c
@@ -0,0 +1,177 @@
+/*
+ * ProFTPD - mod_auth_otp base32 implementation
+ * Copyright (c) 2015-2016 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.
+ */
+
+#include "mod_auth_otp.h"
+#include "base32.h"
+
+/* Note that this base32 implementation does NOT emit the padding characters,
+ * as an "optimization".
+ *
+ * The base32 encoded values are used for interoperability with e.g. Google
+ * Authenticator, for entering into the app via human interaction.  To
+ * reduce the friction, then, the padding characters are omitted.
+ */
+
+static const unsigned char base32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+int auth_otp_base32_encode(pool *p, const unsigned char *raw,
+    size_t raw_len, const unsigned char **encoded, size_t *encoded_len) {
+  unsigned char *buf;
+  size_t buflen, bufsz;
+
+  if (p == NULL ||
+      raw == NULL ||
+      encoded == NULL ||
+      encoded_len == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  bufsz = (raw_len * 8) / 5 + 5;
+  buf = palloc(p, bufsz);
+  buflen = 0;
+
+  if (raw_len > 0) {
+    int d, i;
+    int bits_rem = 0;
+
+    d = raw[0];
+    i = 1;
+    bits_rem = 8;
+
+    while ((buflen < bufsz) &&
+           (bits_rem > 0 || (size_t) i < raw_len)) {
+      int j;
+
+      pr_signals_handle();
+
+      if (bits_rem < 5) {
+        if ((size_t) i < raw_len) {
+          d <<= 8;
+          d |= raw[i++] & 0xff;
+          bits_rem += 8;
+
+        } else {
+          int padding;
+
+          padding = 5 - bits_rem;
+          d <<= padding;
+          bits_rem += padding;
+        }
+      }
+
+      j = 0x1f & (d >> (bits_rem - 5));
+      bits_rem -= 5;
+      buf[buflen++] = base32[j];
+    }
+  }
+
+  if (buflen < bufsz) {
+    buf[buflen] = '\0';
+  }
+
+  *encoded = buf;
+  *encoded_len = buflen;
+  return 0;
+}
+
+int auth_otp_base32_decode(pool *p, const unsigned char *encoded,
+    size_t encoded_len, const unsigned char **raw, size_t *raw_len) {
+  register const unsigned char *ptr;
+  int d;
+  unsigned char *buf;
+  size_t buflen, bufsz;
+  int bits_rem;
+
+  if (p == NULL ||
+      encoded == NULL ||
+      raw == NULL ||
+      raw_len == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (encoded_len == 0) {
+    /* We were given an empty string; make sure we allocate at least one
+     * character, for the NUL.
+     */
+    encoded_len = 1;
+  }
+
+  bufsz = encoded_len;
+  buf = palloc(p, bufsz);
+  buflen = 0;
+
+  bits_rem = 0;
+  d = 0;
+
+  for (ptr = encoded; buflen < bufsz && *ptr; ++ptr) {
+    char c;
+
+    pr_signals_handle();
+
+    c = *ptr;
+
+    /* Per RFC 4648 recommendations, skip linefeeds and other similar
+     * characters in decoding.
+     */
+    if (c == ' ' ||
+        c == '\t' ||
+        c == '\r' ||
+        c == '\n' ||
+        c == '-') {
+      continue;
+    }
+
+    d <<= 5;
+
+    if ((c >= 'A' && c <= 'Z') ||
+        (c >= 'a' && c <= 'z')) {
+      c = (c & 0x1f) - 1;
+
+    } else if (c >= '2' && c <= '7') {
+      c -= ('2' - 26);
+
+    } else {
+      /* Invalid character. */
+      errno = EPERM;
+      return -1;
+    }
+
+    d |= c;
+    bits_rem += 5;
+    if (bits_rem >= 8) {
+      buf[buflen++] = (d >> (bits_rem - 8));
+      bits_rem -= 8;
+    }
+  }
+
+  if (buflen < bufsz) {
+    buf[buflen] = '\0';
+  }
+
+  *raw = buf;
+  *raw_len = buflen;
+  return 0;
+}
diff --git a/contrib/mod_sftp/service.h b/contrib/mod_auth_otp/base32.h
similarity index 67%
copy from contrib/mod_sftp/service.h
copy to contrib/mod_auth_otp/base32.h
index 0be2b36..bfc29e2 100644
--- a/contrib/mod_sftp/service.h
+++ b/contrib/mod_auth_otp/base32.h
@@ -1,6 +1,6 @@
 /*
- * ProFTPD - mod_sftp services (service)
- * Copyright (c) 2008-2011 TJ Saunders
+ * ProFTPD - mod_auth_otp base32 routines
+ * Copyright (c) 2015-2016 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
@@ -20,18 +20,17 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: service.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
+#ifndef MOD_AUTH_OTP_BASE32_H
+#define MOD_AUTH_OTP_BASE32_H
 
-#ifndef MOD_SFTP_SERVICE_H
-#define MOD_SFTP_SERVICE_H
+#include "mod_auth_otp.h"
 
-#include "packet.h"
+int auth_otp_base32_encode(pool *p, const unsigned char *raw,
+  size_t raw_len, const unsigned char **encoded, size_t *encoded_len);
 
-int sftp_service_handle(struct ssh2_packet *);
-int sftp_service_init(void);
+int auth_otp_base32_decode(pool *p, const unsigned char *encoded,
+  size_t encoded_len, const unsigned char **raw, size_t *raw_len);
 
-#endif
+#endif /* MOD_AUTH_OTP_BASE32_H */
diff --git a/contrib/mod_auth_otp/config.guess b/contrib/mod_auth_otp/config.guess
new file mode 100755
index 0000000..d622a44
--- /dev/null
+++ b/contrib/mod_auth_otp/config.guess
@@ -0,0 +1,1530 @@
+#! /bin/sh
+# Attempt to guess a canonical system name.
+#   Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999,
+#   2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+#   2011, 2012 Free Software Foundation, Inc.
+
+timestamp='2012-02-10'
+
+# This file 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, see <http://www.gnu.org/licenses/>.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+
+# Originally written by Per Bothner.  Please send patches (context
+# diff format) to <config-patches at gnu.org> and include a ChangeLog
+# entry.
+#
+# This script attempts to guess a canonical system name similar to
+# config.sub.  If it succeeds, it prints the system name on stdout, and
+# exits with 0.  Otherwise, it exits with 1.
+#
+# You can get the latest version of this script from:
+# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION]
+
+Output the configuration name of the system \`$me' is run on.
+
+Operation modes:
+  -h, --help         print this help, then exit
+  -t, --time-stamp   print date of last modification, then exit
+  -v, --version      print version number, then exit
+
+Report bugs and patches to <config-patches at gnu.org>."
+
+version="\
+GNU config.guess ($timestamp)
+
+Originally written by Per Bothner.
+Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000,
+2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012
+Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+  case $1 in
+    --time-stamp | --time* | -t )
+       echo "$timestamp" ; exit ;;
+    --version | -v )
+       echo "$version" ; exit ;;
+    --help | --h* | -h )
+       echo "$usage"; exit ;;
+    -- )     # Stop option processing
+       shift; break ;;
+    - )	# Use stdin as input.
+       break ;;
+    -* )
+       echo "$me: invalid option $1$help" >&2
+       exit 1 ;;
+    * )
+       break ;;
+  esac
+done
+
+if test $# != 0; then
+  echo "$me: too many arguments$help" >&2
+  exit 1
+fi
+
+trap 'exit 1' 1 2 15
+
+# CC_FOR_BUILD -- compiler used by this script. Note that the use of a
+# compiler to aid in system detection is discouraged as it requires
+# temporary files to be created and, as you can see below, it is a
+# headache to deal with in a portable fashion.
+
+# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still
+# use `HOST_CC' if defined, but it is deprecated.
+
+# Portable tmp directory creation inspired by the Autoconf team.
+
+set_cc_for_build='
+trap "exitcode=\$?; (rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null) && exit \$exitcode" 0 ;
+trap "rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null; exit 1" 1 2 13 15 ;
+: ${TMPDIR=/tmp} ;
+ { tmp=`(umask 077 && mktemp -d "$TMPDIR/cgXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" ; } ||
+ { test -n "$RANDOM" && tmp=$TMPDIR/cg$$-$RANDOM && (umask 077 && mkdir $tmp) ; } ||
+ { tmp=$TMPDIR/cg-$$ && (umask 077 && mkdir $tmp) && echo "Warning: creating insecure temp directory" >&2 ; } ||
+ { echo "$me: cannot create a temporary directory in $TMPDIR" >&2 ; exit 1 ; } ;
+dummy=$tmp/dummy ;
+tmpfiles="$dummy.c $dummy.o $dummy.rel $dummy" ;
+case $CC_FOR_BUILD,$HOST_CC,$CC in
+ ,,)    echo "int x;" > $dummy.c ;
+	for c in cc gcc c89 c99 ; do
+	  if ($c -c -o $dummy.o $dummy.c) >/dev/null 2>&1 ; then
+	     CC_FOR_BUILD="$c"; break ;
+	  fi ;
+	done ;
+	if test x"$CC_FOR_BUILD" = x ; then
+	  CC_FOR_BUILD=no_compiler_found ;
+	fi
+	;;
+ ,,*)   CC_FOR_BUILD=$CC ;;
+ ,*,*)  CC_FOR_BUILD=$HOST_CC ;;
+esac ; set_cc_for_build= ;'
+
+# This is needed to find uname on a Pyramid OSx when run in the BSD universe.
+# (ghazi at noc.rutgers.edu 1994-08-24)
+if (test -f /.attbin/uname) >/dev/null 2>&1 ; then
+	PATH=$PATH:/.attbin ; export PATH
+fi
+
+UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown
+UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown
+UNAME_SYSTEM=`(uname -s) 2>/dev/null`  || UNAME_SYSTEM=unknown
+UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown
+
+# Note: order is significant - the case branches are not exclusive.
+
+case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in
+    *:NetBSD:*:*)
+	# NetBSD (nbsd) targets should (where applicable) match one or
+	# more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*,
+	# *-*-netbsdecoff* and *-*-netbsd*.  For targets that recently
+	# switched to ELF, *-*-netbsd* would select the old
+	# object file format.  This provides both forward
+	# compatibility and a consistent mechanism for selecting the
+	# object file format.
+	#
+	# Note: NetBSD doesn't particularly care about the vendor
+	# portion of the name.  We always set it to "unknown".
+	sysctl="sysctl -n hw.machine_arch"
+	UNAME_MACHINE_ARCH=`(/sbin/$sysctl 2>/dev/null || \
+	    /usr/sbin/$sysctl 2>/dev/null || echo unknown)`
+	case "${UNAME_MACHINE_ARCH}" in
+	    armeb) machine=armeb-unknown ;;
+	    arm*) machine=arm-unknown ;;
+	    sh3el) machine=shl-unknown ;;
+	    sh3eb) machine=sh-unknown ;;
+	    sh5el) machine=sh5le-unknown ;;
+	    *) machine=${UNAME_MACHINE_ARCH}-unknown ;;
+	esac
+	# The Operating System including object format, if it has switched
+	# to ELF recently, or will in the future.
+	case "${UNAME_MACHINE_ARCH}" in
+	    arm*|i386|m68k|ns32k|sh3*|sparc|vax)
+		eval $set_cc_for_build
+		if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \
+			| grep -q __ELF__
+		then
+		    # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout).
+		    # Return netbsd for either.  FIX?
+		    os=netbsd
+		else
+		    os=netbsdelf
+		fi
+		;;
+	    *)
+		os=netbsd
+		;;
+	esac
+	# The OS release
+	# Debian GNU/NetBSD machines have a different userland, and
+	# thus, need a distinct triplet. However, they do not need
+	# kernel version information, so it can be replaced with a
+	# suitable tag, in the style of linux-gnu.
+	case "${UNAME_VERSION}" in
+	    Debian*)
+		release='-gnu'
+		;;
+	    *)
+		release=`echo ${UNAME_RELEASE}|sed -e 's/[-_].*/\./'`
+		;;
+	esac
+	# Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM:
+	# contains redundant information, the shorter form:
+	# CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used.
+	echo "${machine}-${os}${release}"
+	exit ;;
+    *:OpenBSD:*:*)
+	UNAME_MACHINE_ARCH=`arch | sed 's/OpenBSD.//'`
+	echo ${UNAME_MACHINE_ARCH}-unknown-openbsd${UNAME_RELEASE}
+	exit ;;
+    *:ekkoBSD:*:*)
+	echo ${UNAME_MACHINE}-unknown-ekkobsd${UNAME_RELEASE}
+	exit ;;
+    *:SolidBSD:*:*)
+	echo ${UNAME_MACHINE}-unknown-solidbsd${UNAME_RELEASE}
+	exit ;;
+    macppc:MirBSD:*:*)
+	echo powerpc-unknown-mirbsd${UNAME_RELEASE}
+	exit ;;
+    *:MirBSD:*:*)
+	echo ${UNAME_MACHINE}-unknown-mirbsd${UNAME_RELEASE}
+	exit ;;
+    alpha:OSF1:*:*)
+	case $UNAME_RELEASE in
+	*4.0)
+		UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'`
+		;;
+	*5.*)
+		UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'`
+		;;
+	esac
+	# According to Compaq, /usr/sbin/psrinfo has been available on
+	# OSF/1 and Tru64 systems produced since 1995.  I hope that
+	# covers most systems running today.  This code pipes the CPU
+	# types through head -n 1, so we only detect the type of CPU 0.
+	ALPHA_CPU_TYPE=`/usr/sbin/psrinfo -v | sed -n -e 's/^  The alpha \(.*\) processor.*$/\1/p' | head -n 1`
+	case "$ALPHA_CPU_TYPE" in
+	    "EV4 (21064)")
+		UNAME_MACHINE="alpha" ;;
+	    "EV4.5 (21064)")
+		UNAME_MACHINE="alpha" ;;
+	    "LCA4 (21066/21068)")
+		UNAME_MACHINE="alpha" ;;
+	    "EV5 (21164)")
+		UNAME_MACHINE="alphaev5" ;;
+	    "EV5.6 (21164A)")
+		UNAME_MACHINE="alphaev56" ;;
+	    "EV5.6 (21164PC)")
+		UNAME_MACHINE="alphapca56" ;;
+	    "EV5.7 (21164PC)")
+		UNAME_MACHINE="alphapca57" ;;
+	    "EV6 (21264)")
+		UNAME_MACHINE="alphaev6" ;;
+	    "EV6.7 (21264A)")
+		UNAME_MACHINE="alphaev67" ;;
+	    "EV6.8CB (21264C)")
+		UNAME_MACHINE="alphaev68" ;;
+	    "EV6.8AL (21264B)")
+		UNAME_MACHINE="alphaev68" ;;
+	    "EV6.8CX (21264D)")
+		UNAME_MACHINE="alphaev68" ;;
+	    "EV6.9A (21264/EV69A)")
+		UNAME_MACHINE="alphaev69" ;;
+	    "EV7 (21364)")
+		UNAME_MACHINE="alphaev7" ;;
+	    "EV7.9 (21364A)")
+		UNAME_MACHINE="alphaev79" ;;
+	esac
+	# A Pn.n version is a patched version.
+	# A Vn.n version is a released version.
+	# A Tn.n version is a released field test version.
+	# A Xn.n version is an unreleased experimental baselevel.
+	# 1.2 uses "1.2" for uname -r.
+	echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[PVTX]//' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'`
+	# Reset EXIT trap before exiting to avoid spurious non-zero exit code.
+	exitcode=$?
+	trap '' 0
+	exit $exitcode ;;
+    Alpha\ *:Windows_NT*:*)
+	# How do we know it's Interix rather than the generic POSIX subsystem?
+	# Should we change UNAME_MACHINE based on the output of uname instead
+	# of the specific Alpha model?
+	echo alpha-pc-interix
+	exit ;;
+    21064:Windows_NT:50:3)
+	echo alpha-dec-winnt3.5
+	exit ;;
+    Amiga*:UNIX_System_V:4.0:*)
+	echo m68k-unknown-sysv4
+	exit ;;
+    *:[Aa]miga[Oo][Ss]:*:*)
+	echo ${UNAME_MACHINE}-unknown-amigaos
+	exit ;;
+    *:[Mm]orph[Oo][Ss]:*:*)
+	echo ${UNAME_MACHINE}-unknown-morphos
+	exit ;;
+    *:OS/390:*:*)
+	echo i370-ibm-openedition
+	exit ;;
+    *:z/VM:*:*)
+	echo s390-ibm-zvmoe
+	exit ;;
+    *:OS400:*:*)
+	echo powerpc-ibm-os400
+	exit ;;
+    arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*)
+	echo arm-acorn-riscix${UNAME_RELEASE}
+	exit ;;
+    arm:riscos:*:*|arm:RISCOS:*:*)
+	echo arm-unknown-riscos
+	exit ;;
+    SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*)
+	echo hppa1.1-hitachi-hiuxmpp
+	exit ;;
+    Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*)
+	# akee at wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE.
+	if test "`(/bin/universe) 2>/dev/null`" = att ; then
+		echo pyramid-pyramid-sysv3
+	else
+		echo pyramid-pyramid-bsd
+	fi
+	exit ;;
+    NILE*:*:*:dcosx)
+	echo pyramid-pyramid-svr4
+	exit ;;
+    DRS?6000:unix:4.0:6*)
+	echo sparc-icl-nx6
+	exit ;;
+    DRS?6000:UNIX_SV:4.2*:7* | DRS?6000:isis:4.2*:7*)
+	case `/usr/bin/uname -p` in
+	    sparc) echo sparc-icl-nx7; exit ;;
+	esac ;;
+    s390x:SunOS:*:*)
+	echo ${UNAME_MACHINE}-ibm-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+	exit ;;
+    sun4H:SunOS:5.*:*)
+	echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+	exit ;;
+    sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*)
+	echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+	exit ;;
+    i86pc:AuroraUX:5.*:* | i86xen:AuroraUX:5.*:*)
+	echo i386-pc-auroraux${UNAME_RELEASE}
+	exit ;;
+    i86pc:SunOS:5.*:* | i86xen:SunOS:5.*:*)
+	eval $set_cc_for_build
+	SUN_ARCH="i386"
+	# If there is a compiler, see if it is configured for 64-bit objects.
+	# Note that the Sun cc does not turn __LP64__ into 1 like gcc does.
+	# This test works for both compilers.
+	if [ "$CC_FOR_BUILD" != 'no_compiler_found' ]; then
+	    if (echo '#ifdef __amd64'; echo IS_64BIT_ARCH; echo '#endif') | \
+		(CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | \
+		grep IS_64BIT_ARCH >/dev/null
+	    then
+		SUN_ARCH="x86_64"
+	    fi
+	fi
+	echo ${SUN_ARCH}-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+	exit ;;
+    sun4*:SunOS:6*:*)
+	# According to config.sub, this is the proper way to canonicalize
+	# SunOS6.  Hard to guess exactly what SunOS6 will be like, but
+	# it's likely to be more like Solaris than SunOS4.
+	echo sparc-sun-solaris3`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+	exit ;;
+    sun4*:SunOS:*:*)
+	case "`/usr/bin/arch -k`" in
+	    Series*|S4*)
+		UNAME_RELEASE=`uname -v`
+		;;
+	esac
+	# Japanese Language versions have a version number like `4.1.3-JL'.
+	echo sparc-sun-sunos`echo ${UNAME_RELEASE}|sed -e 's/-/_/'`
+	exit ;;
+    sun3*:SunOS:*:*)
+	echo m68k-sun-sunos${UNAME_RELEASE}
+	exit ;;
+    sun*:*:4.2BSD:*)
+	UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null`
+	test "x${UNAME_RELEASE}" = "x" && UNAME_RELEASE=3
+	case "`/bin/arch`" in
+	    sun3)
+		echo m68k-sun-sunos${UNAME_RELEASE}
+		;;
+	    sun4)
+		echo sparc-sun-sunos${UNAME_RELEASE}
+		;;
+	esac
+	exit ;;
+    aushp:SunOS:*:*)
+	echo sparc-auspex-sunos${UNAME_RELEASE}
+	exit ;;
+    # The situation for MiNT is a little confusing.  The machine name
+    # can be virtually everything (everything which is not
+    # "atarist" or "atariste" at least should have a processor
+    # > m68000).  The system name ranges from "MiNT" over "FreeMiNT"
+    # to the lowercase version "mint" (or "freemint").  Finally
+    # the system name "TOS" denotes a system which is actually not
+    # MiNT.  But MiNT is downward compatible to TOS, so this should
+    # be no problem.
+    atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*)
+	echo m68k-atari-mint${UNAME_RELEASE}
+	exit ;;
+    atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*)
+	echo m68k-atari-mint${UNAME_RELEASE}
+	exit ;;
+    *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*)
+	echo m68k-atari-mint${UNAME_RELEASE}
+	exit ;;
+    milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*)
+	echo m68k-milan-mint${UNAME_RELEASE}
+	exit ;;
+    hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*)
+	echo m68k-hades-mint${UNAME_RELEASE}
+	exit ;;
+    *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*)
+	echo m68k-unknown-mint${UNAME_RELEASE}
+	exit ;;
+    m68k:machten:*:*)
+	echo m68k-apple-machten${UNAME_RELEASE}
+	exit ;;
+    powerpc:machten:*:*)
+	echo powerpc-apple-machten${UNAME_RELEASE}
+	exit ;;
+    RISC*:Mach:*:*)
+	echo mips-dec-mach_bsd4.3
+	exit ;;
+    RISC*:ULTRIX:*:*)
+	echo mips-dec-ultrix${UNAME_RELEASE}
+	exit ;;
+    VAX*:ULTRIX*:*:*)
+	echo vax-dec-ultrix${UNAME_RELEASE}
+	exit ;;
+    2020:CLIX:*:* | 2430:CLIX:*:*)
+	echo clipper-intergraph-clix${UNAME_RELEASE}
+	exit ;;
+    mips:*:*:UMIPS | mips:*:*:RISCos)
+	eval $set_cc_for_build
+	sed 's/^	//' << EOF >$dummy.c
+#ifdef __cplusplus
+#include <stdio.h>  /* for printf() prototype */
+	int main (int argc, char *argv[]) {
+#else
+	int main (argc, argv) int argc; char *argv[]; {
+#endif
+	#if defined (host_mips) && defined (MIPSEB)
+	#if defined (SYSTYPE_SYSV)
+	  printf ("mips-mips-riscos%ssysv\n", argv[1]); exit (0);
+	#endif
+	#if defined (SYSTYPE_SVR4)
+	  printf ("mips-mips-riscos%ssvr4\n", argv[1]); exit (0);
+	#endif
+	#if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD)
+	  printf ("mips-mips-riscos%sbsd\n", argv[1]); exit (0);
+	#endif
+	#endif
+	  exit (-1);
+	}
+EOF
+	$CC_FOR_BUILD -o $dummy $dummy.c &&
+	  dummyarg=`echo "${UNAME_RELEASE}" | sed -n 's/\([0-9]*\).*/\1/p'` &&
+	  SYSTEM_NAME=`$dummy $dummyarg` &&
+	    { echo "$SYSTEM_NAME"; exit; }
+	echo mips-mips-riscos${UNAME_RELEASE}
+	exit ;;
+    Motorola:PowerMAX_OS:*:*)
+	echo powerpc-motorola-powermax
+	exit ;;
+    Motorola:*:4.3:PL8-*)
+	echo powerpc-harris-powermax
+	exit ;;
+    Night_Hawk:*:*:PowerMAX_OS | Synergy:PowerMAX_OS:*:*)
+	echo powerpc-harris-powermax
+	exit ;;
+    Night_Hawk:Power_UNIX:*:*)
+	echo powerpc-harris-powerunix
+	exit ;;
+    m88k:CX/UX:7*:*)
+	echo m88k-harris-cxux7
+	exit ;;
+    m88k:*:4*:R4*)
+	echo m88k-motorola-sysv4
+	exit ;;
+    m88k:*:3*:R3*)
+	echo m88k-motorola-sysv3
+	exit ;;
+    AViiON:dgux:*:*)
+	# DG/UX returns AViiON for all architectures
+	UNAME_PROCESSOR=`/usr/bin/uname -p`
+	if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ]
+	then
+	    if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \
+	       [ ${TARGET_BINARY_INTERFACE}x = x ]
+	    then
+		echo m88k-dg-dgux${UNAME_RELEASE}
+	    else
+		echo m88k-dg-dguxbcs${UNAME_RELEASE}
+	    fi
+	else
+	    echo i586-dg-dgux${UNAME_RELEASE}
+	fi
+	exit ;;
+    M88*:DolphinOS:*:*)	# DolphinOS (SVR3)
+	echo m88k-dolphin-sysv3
+	exit ;;
+    M88*:*:R3*:*)
+	# Delta 88k system running SVR3
+	echo m88k-motorola-sysv3
+	exit ;;
+    XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3)
+	echo m88k-tektronix-sysv3
+	exit ;;
+    Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD)
+	echo m68k-tektronix-bsd
+	exit ;;
+    *:IRIX*:*:*)
+	echo mips-sgi-irix`echo ${UNAME_RELEASE}|sed -e 's/-/_/g'`
+	exit ;;
+    ????????:AIX?:[12].1:2)   # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX.
+	echo romp-ibm-aix     # uname -m gives an 8 hex-code CPU id
+	exit ;;               # Note that: echo "'`uname -s`'" gives 'AIX '
+    i*86:AIX:*:*)
+	echo i386-ibm-aix
+	exit ;;
+    ia64:AIX:*:*)
+	if [ -x /usr/bin/oslevel ] ; then
+		IBM_REV=`/usr/bin/oslevel`
+	else
+		IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE}
+	fi
+	echo ${UNAME_MACHINE}-ibm-aix${IBM_REV}
+	exit ;;
+    *:AIX:2:3)
+	if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then
+		eval $set_cc_for_build
+		sed 's/^		//' << EOF >$dummy.c
+		#include <sys/systemcfg.h>
+
+		main()
+			{
+			if (!__power_pc())
+				exit(1);
+			puts("powerpc-ibm-aix3.2.5");
+			exit(0);
+			}
+EOF
+		if $CC_FOR_BUILD -o $dummy $dummy.c && SYSTEM_NAME=`$dummy`
+		then
+			echo "$SYSTEM_NAME"
+		else
+			echo rs6000-ibm-aix3.2.5
+		fi
+	elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then
+		echo rs6000-ibm-aix3.2.4
+	else
+		echo rs6000-ibm-aix3.2
+	fi
+	exit ;;
+    *:AIX:*:[4567])
+	IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'`
+	if /usr/sbin/lsattr -El ${IBM_CPU_ID} | grep ' POWER' >/dev/null 2>&1; then
+		IBM_ARCH=rs6000
+	else
+		IBM_ARCH=powerpc
+	fi
+	if [ -x /usr/bin/oslevel ] ; then
+		IBM_REV=`/usr/bin/oslevel`
+	else
+		IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE}
+	fi
+	echo ${IBM_ARCH}-ibm-aix${IBM_REV}
+	exit ;;
+    *:AIX:*:*)
+	echo rs6000-ibm-aix
+	exit ;;
+    ibmrt:4.4BSD:*|romp-ibm:BSD:*)
+	echo romp-ibm-bsd4.4
+	exit ;;
+    ibmrt:*BSD:*|romp-ibm:BSD:*)            # covers RT/PC BSD and
+	echo romp-ibm-bsd${UNAME_RELEASE}   # 4.3 with uname added to
+	exit ;;                             # report: romp-ibm BSD 4.3
+    *:BOSX:*:*)
+	echo rs6000-bull-bosx
+	exit ;;
+    DPX/2?00:B.O.S.:*:*)
+	echo m68k-bull-sysv3
+	exit ;;
+    9000/[34]??:4.3bsd:1.*:*)
+	echo m68k-hp-bsd
+	exit ;;
+    hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*)
+	echo m68k-hp-bsd4.4
+	exit ;;
+    9000/[34678]??:HP-UX:*:*)
+	HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'`
+	case "${UNAME_MACHINE}" in
+	    9000/31? )            HP_ARCH=m68000 ;;
+	    9000/[34]?? )         HP_ARCH=m68k ;;
+	    9000/[678][0-9][0-9])
+		if [ -x /usr/bin/getconf ]; then
+		    sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null`
+		    sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null`
+		    case "${sc_cpu_version}" in
+		      523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0
+		      528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1
+		      532)                      # CPU_PA_RISC2_0
+			case "${sc_kernel_bits}" in
+			  32) HP_ARCH="hppa2.0n" ;;
+			  64) HP_ARCH="hppa2.0w" ;;
+			  '') HP_ARCH="hppa2.0" ;;   # HP-UX 10.20
+			esac ;;
+		    esac
+		fi
+		if [ "${HP_ARCH}" = "" ]; then
+		    eval $set_cc_for_build
+		    sed 's/^		//' << EOF >$dummy.c
+
+		#define _HPUX_SOURCE
+		#include <stdlib.h>
+		#include <unistd.h>
+
+		int main ()
+		{
+		#if defined(_SC_KERNEL_BITS)
+		    long bits = sysconf(_SC_KERNEL_BITS);
+		#endif
+		    long cpu  = sysconf (_SC_CPU_VERSION);
+
+		    switch (cpu)
+			{
+			case CPU_PA_RISC1_0: puts ("hppa1.0"); break;
+			case CPU_PA_RISC1_1: puts ("hppa1.1"); break;
+			case CPU_PA_RISC2_0:
+		#if defined(_SC_KERNEL_BITS)
+			    switch (bits)
+				{
+				case 64: puts ("hppa2.0w"); break;
+				case 32: puts ("hppa2.0n"); break;
+				default: puts ("hppa2.0"); break;
+				} break;
+		#else  /* !defined(_SC_KERNEL_BITS) */
+			    puts ("hppa2.0"); break;
+		#endif
+			default: puts ("hppa1.0"); break;
+			}
+		    exit (0);
+		}
+EOF
+		    (CCOPTS= $CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null) && HP_ARCH=`$dummy`
+		    test -z "$HP_ARCH" && HP_ARCH=hppa
+		fi ;;
+	esac
+	if [ ${HP_ARCH} = "hppa2.0w" ]
+	then
+	    eval $set_cc_for_build
+
+	    # hppa2.0w-hp-hpux* has a 64-bit kernel and a compiler generating
+	    # 32-bit code.  hppa64-hp-hpux* has the same kernel and a compiler
+	    # generating 64-bit code.  GNU and HP use different nomenclature:
+	    #
+	    # $ CC_FOR_BUILD=cc ./config.guess
+	    # => hppa2.0w-hp-hpux11.23
+	    # $ CC_FOR_BUILD="cc +DA2.0w" ./config.guess
+	    # => hppa64-hp-hpux11.23
+
+	    if echo __LP64__ | (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) |
+		grep -q __LP64__
+	    then
+		HP_ARCH="hppa2.0w"
+	    else
+		HP_ARCH="hppa64"
+	    fi
+	fi
+	echo ${HP_ARCH}-hp-hpux${HPUX_REV}
+	exit ;;
+    ia64:HP-UX:*:*)
+	HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'`
+	echo ia64-hp-hpux${HPUX_REV}
+	exit ;;
+    3050*:HI-UX:*:*)
+	eval $set_cc_for_build
+	sed 's/^	//' << EOF >$dummy.c
+	#include <unistd.h>
+	int
+	main ()
+	{
+	  long cpu = sysconf (_SC_CPU_VERSION);
+	  /* The order matters, because CPU_IS_HP_MC68K erroneously returns
+	     true for CPU_PA_RISC1_0.  CPU_IS_PA_RISC returns correct
+	     results, however.  */
+	  if (CPU_IS_PA_RISC (cpu))
+	    {
+	      switch (cpu)
+		{
+		  case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break;
+		  case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break;
+		  case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break;
+		  default: puts ("hppa-hitachi-hiuxwe2"); break;
+		}
+	    }
+	  else if (CPU_IS_HP_MC68K (cpu))
+	    puts ("m68k-hitachi-hiuxwe2");
+	  else puts ("unknown-hitachi-hiuxwe2");
+	  exit (0);
+	}
+EOF
+	$CC_FOR_BUILD -o $dummy $dummy.c && SYSTEM_NAME=`$dummy` &&
+		{ echo "$SYSTEM_NAME"; exit; }
+	echo unknown-hitachi-hiuxwe2
+	exit ;;
+    9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:* )
+	echo hppa1.1-hp-bsd
+	exit ;;
+    9000/8??:4.3bsd:*:*)
+	echo hppa1.0-hp-bsd
+	exit ;;
+    *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*)
+	echo hppa1.0-hp-mpeix
+	exit ;;
+    hp7??:OSF1:*:* | hp8?[79]:OSF1:*:* )
+	echo hppa1.1-hp-osf
+	exit ;;
+    hp8??:OSF1:*:*)
+	echo hppa1.0-hp-osf
+	exit ;;
+    i*86:OSF1:*:*)
+	if [ -x /usr/sbin/sysversion ] ; then
+	    echo ${UNAME_MACHINE}-unknown-osf1mk
+	else
+	    echo ${UNAME_MACHINE}-unknown-osf1
+	fi
+	exit ;;
+    parisc*:Lites*:*:*)
+	echo hppa1.1-hp-lites
+	exit ;;
+    C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*)
+	echo c1-convex-bsd
+	exit ;;
+    C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*)
+	if getsysinfo -f scalar_acc
+	then echo c32-convex-bsd
+	else echo c2-convex-bsd
+	fi
+	exit ;;
+    C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*)
+	echo c34-convex-bsd
+	exit ;;
+    C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*)
+	echo c38-convex-bsd
+	exit ;;
+    C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*)
+	echo c4-convex-bsd
+	exit ;;
+    CRAY*Y-MP:*:*:*)
+	echo ymp-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+	exit ;;
+    CRAY*[A-Z]90:*:*:*)
+	echo ${UNAME_MACHINE}-cray-unicos${UNAME_RELEASE} \
+	| sed -e 's/CRAY.*\([A-Z]90\)/\1/' \
+	      -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \
+	      -e 's/\.[^.]*$/.X/'
+	exit ;;
+    CRAY*TS:*:*:*)
+	echo t90-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+	exit ;;
+    CRAY*T3E:*:*:*)
+	echo alphaev5-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+	exit ;;
+    CRAY*SV1:*:*:*)
+	echo sv1-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+	exit ;;
+    *:UNICOS/mp:*:*)
+	echo craynv-cray-unicosmp${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/'
+	exit ;;
+    F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*)
+	FUJITSU_PROC=`uname -m | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'`
+	FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'`
+	FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'`
+	echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}"
+	exit ;;
+    5000:UNIX_System_V:4.*:*)
+	FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'`
+	FUJITSU_REL=`echo ${UNAME_RELEASE} | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/ /_/'`
+	echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}"
+	exit ;;
+    i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*)
+	echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE}
+	exit ;;
+    sparc*:BSD/OS:*:*)
+	echo sparc-unknown-bsdi${UNAME_RELEASE}
+	exit ;;
+    *:BSD/OS:*:*)
+	echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE}
+	exit ;;
+    *:FreeBSD:*:*)
+	UNAME_PROCESSOR=`/usr/bin/uname -p`
+	case ${UNAME_PROCESSOR} in
+	    amd64)
+		echo x86_64-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;;
+	    *)
+		echo ${UNAME_PROCESSOR}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` ;;
+	esac
+	exit ;;
+    i*:CYGWIN*:*)
+	echo ${UNAME_MACHINE}-pc-cygwin
+	exit ;;
+    *:MINGW*:*)
+	echo ${UNAME_MACHINE}-pc-mingw32
+	exit ;;
+    i*:MSYS*:*)
+	echo ${UNAME_MACHINE}-pc-msys
+	exit ;;
+    i*:windows32*:*)
+	# uname -m includes "-pc" on this system.
+	echo ${UNAME_MACHINE}-mingw32
+	exit ;;
+    i*:PW*:*)
+	echo ${UNAME_MACHINE}-pc-pw32
+	exit ;;
+    *:Interix*:*)
+	case ${UNAME_MACHINE} in
+	    x86)
+		echo i586-pc-interix${UNAME_RELEASE}
+		exit ;;
+	    authenticamd | genuineintel | EM64T)
+		echo x86_64-unknown-interix${UNAME_RELEASE}
+		exit ;;
+	    IA64)
+		echo ia64-unknown-interix${UNAME_RELEASE}
+		exit ;;
+	esac ;;
+    [345]86:Windows_95:* | [345]86:Windows_98:* | [345]86:Windows_NT:*)
+	echo i${UNAME_MACHINE}-pc-mks
+	exit ;;
+    8664:Windows_NT:*)
+	echo x86_64-pc-mks
+	exit ;;
+    i*:Windows_NT*:* | Pentium*:Windows_NT*:*)
+	# How do we know it's Interix rather than the generic POSIX subsystem?
+	# It also conflicts with pre-2.0 versions of AT&T UWIN. Should we
+	# UNAME_MACHINE based on the output of uname instead of i386?
+	echo i586-pc-interix
+	exit ;;
+    i*:UWIN*:*)
+	echo ${UNAME_MACHINE}-pc-uwin
+	exit ;;
+    amd64:CYGWIN*:*:* | x86_64:CYGWIN*:*:*)
+	echo x86_64-unknown-cygwin
+	exit ;;
+    p*:CYGWIN*:*)
+	echo powerpcle-unknown-cygwin
+	exit ;;
+    prep*:SunOS:5.*:*)
+	echo powerpcle-unknown-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'`
+	exit ;;
+    *:GNU:*:*)
+	# the GNU system
+	echo `echo ${UNAME_MACHINE}|sed -e 's,[-/].*$,,'`-unknown-gnu`echo ${UNAME_RELEASE}|sed -e 's,/.*$,,'`
+	exit ;;
+    *:GNU/*:*:*)
+	# other systems with GNU libc and userland
+	echo ${UNAME_MACHINE}-unknown-`echo ${UNAME_SYSTEM} | sed 's,^[^/]*/,,' | tr '[A-Z]' '[a-z]'``echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`-gnu
+	exit ;;
+    i*86:Minix:*:*)
+	echo ${UNAME_MACHINE}-pc-minix
+	exit ;;
+    aarch64:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    aarch64_be:Linux:*:*)
+	UNAME_MACHINE=aarch64_be
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    alpha:Linux:*:*)
+	case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in
+	  EV5)   UNAME_MACHINE=alphaev5 ;;
+	  EV56)  UNAME_MACHINE=alphaev56 ;;
+	  PCA56) UNAME_MACHINE=alphapca56 ;;
+	  PCA57) UNAME_MACHINE=alphapca56 ;;
+	  EV6)   UNAME_MACHINE=alphaev6 ;;
+	  EV67)  UNAME_MACHINE=alphaev67 ;;
+	  EV68*) UNAME_MACHINE=alphaev68 ;;
+	esac
+	objdump --private-headers /bin/sh | grep -q ld.so.1
+	if test "$?" = 0 ; then LIBC="libc1" ; else LIBC="" ; fi
+	echo ${UNAME_MACHINE}-unknown-linux-gnu${LIBC}
+	exit ;;
+    arm*:Linux:*:*)
+	eval $set_cc_for_build
+	if echo __ARM_EABI__ | $CC_FOR_BUILD -E - 2>/dev/null \
+	    | grep -q __ARM_EABI__
+	then
+	    echo ${UNAME_MACHINE}-unknown-linux-gnu
+	else
+	    if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \
+		| grep -q __ARM_PCS_VFP
+	    then
+		echo ${UNAME_MACHINE}-unknown-linux-gnueabi
+	    else
+		echo ${UNAME_MACHINE}-unknown-linux-gnueabihf
+	    fi
+	fi
+	exit ;;
+    avr32*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    cris:Linux:*:*)
+	echo ${UNAME_MACHINE}-axis-linux-gnu
+	exit ;;
+    crisv32:Linux:*:*)
+	echo ${UNAME_MACHINE}-axis-linux-gnu
+	exit ;;
+    frv:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    hexagon:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    i*86:Linux:*:*)
+	LIBC=gnu
+	eval $set_cc_for_build
+	sed 's/^	//' << EOF >$dummy.c
+	#ifdef __dietlibc__
+	LIBC=dietlibc
+	#endif
+EOF
+	eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^LIBC'`
+	echo "${UNAME_MACHINE}-pc-linux-${LIBC}"
+	exit ;;
+    ia64:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    m32r*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    m68*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    mips:Linux:*:* | mips64:Linux:*:*)
+	eval $set_cc_for_build
+	sed 's/^	//' << EOF >$dummy.c
+	#undef CPU
+	#undef ${UNAME_MACHINE}
+	#undef ${UNAME_MACHINE}el
+	#if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL)
+	CPU=${UNAME_MACHINE}el
+	#else
+	#if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB)
+	CPU=${UNAME_MACHINE}
+	#else
+	CPU=
+	#endif
+	#endif
+EOF
+	eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep '^CPU'`
+	test x"${CPU}" != x && { echo "${CPU}-unknown-linux-gnu"; exit; }
+	;;
+    or32:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    padre:Linux:*:*)
+	echo sparc-unknown-linux-gnu
+	exit ;;
+    parisc64:Linux:*:* | hppa64:Linux:*:*)
+	echo hppa64-unknown-linux-gnu
+	exit ;;
+    parisc:Linux:*:* | hppa:Linux:*:*)
+	# Look for CPU level
+	case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in
+	  PA7*) echo hppa1.1-unknown-linux-gnu ;;
+	  PA8*) echo hppa2.0-unknown-linux-gnu ;;
+	  *)    echo hppa-unknown-linux-gnu ;;
+	esac
+	exit ;;
+    ppc64:Linux:*:*)
+	echo powerpc64-unknown-linux-gnu
+	exit ;;
+    ppc:Linux:*:*)
+	echo powerpc-unknown-linux-gnu
+	exit ;;
+    s390:Linux:*:* | s390x:Linux:*:*)
+	echo ${UNAME_MACHINE}-ibm-linux
+	exit ;;
+    sh64*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    sh*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    sparc:Linux:*:* | sparc64:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    tile*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    vax:Linux:*:*)
+	echo ${UNAME_MACHINE}-dec-linux-gnu
+	exit ;;
+    x86_64:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    xtensa*:Linux:*:*)
+	echo ${UNAME_MACHINE}-unknown-linux-gnu
+	exit ;;
+    i*86:DYNIX/ptx:4*:*)
+	# ptx 4.0 does uname -s correctly, with DYNIX/ptx in there.
+	# earlier versions are messed up and put the nodename in both
+	# sysname and nodename.
+	echo i386-sequent-sysv4
+	exit ;;
+    i*86:UNIX_SV:4.2MP:2.*)
+	# Unixware is an offshoot of SVR4, but it has its own version
+	# number series starting with 2...
+	# I am not positive that other SVR4 systems won't match this,
+	# I just have to hope.  -- rms.
+	# Use sysv4.2uw... so that sysv4* matches it.
+	echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION}
+	exit ;;
+    i*86:OS/2:*:*)
+	# If we were able to find `uname', then EMX Unix compatibility
+	# is probably installed.
+	echo ${UNAME_MACHINE}-pc-os2-emx
+	exit ;;
+    i*86:XTS-300:*:STOP)
+	echo ${UNAME_MACHINE}-unknown-stop
+	exit ;;
+    i*86:atheos:*:*)
+	echo ${UNAME_MACHINE}-unknown-atheos
+	exit ;;
+    i*86:syllable:*:*)
+	echo ${UNAME_MACHINE}-pc-syllable
+	exit ;;
+    i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.[02]*:*)
+	echo i386-unknown-lynxos${UNAME_RELEASE}
+	exit ;;
+    i*86:*DOS:*:*)
+	echo ${UNAME_MACHINE}-pc-msdosdjgpp
+	exit ;;
+    i*86:*:4.*:* | i*86:SYSTEM_V:4.*:*)
+	UNAME_REL=`echo ${UNAME_RELEASE} | sed 's/\/MP$//'`
+	if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then
+		echo ${UNAME_MACHINE}-univel-sysv${UNAME_REL}
+	else
+		echo ${UNAME_MACHINE}-pc-sysv${UNAME_REL}
+	fi
+	exit ;;
+    i*86:*:5:[678]*)
+	# UnixWare 7.x, OpenUNIX and OpenServer 6.
+	case `/bin/uname -X | grep "^Machine"` in
+	    *486*)	     UNAME_MACHINE=i486 ;;
+	    *Pentium)	     UNAME_MACHINE=i586 ;;
+	    *Pent*|*Celeron) UNAME_MACHINE=i686 ;;
+	esac
+	echo ${UNAME_MACHINE}-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}${UNAME_VERSION}
+	exit ;;
+    i*86:*:3.2:*)
+	if test -f /usr/options/cb.name; then
+		UNAME_REL=`sed -n 's/.*Version //p' </usr/options/cb.name`
+		echo ${UNAME_MACHINE}-pc-isc$UNAME_REL
+	elif /bin/uname -X 2>/dev/null >/dev/null ; then
+		UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')`
+		(/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486
+		(/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \
+			&& UNAME_MACHINE=i586
+		(/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \
+			&& UNAME_MACHINE=i686
+		(/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \
+			&& UNAME_MACHINE=i686
+		echo ${UNAME_MACHINE}-pc-sco$UNAME_REL
+	else
+		echo ${UNAME_MACHINE}-pc-sysv32
+	fi
+	exit ;;
+    pc:*:*:*)
+	# Left here for compatibility:
+	# uname -m prints for DJGPP always 'pc', but it prints nothing about
+	# the processor, so we play safe by assuming i586.
+	# Note: whatever this is, it MUST be the same as what config.sub
+	# prints for the "djgpp" host, or else GDB configury will decide that
+	# this is a cross-build.
+	echo i586-pc-msdosdjgpp
+	exit ;;
+    Intel:Mach:3*:*)
+	echo i386-pc-mach3
+	exit ;;
+    paragon:*:*:*)
+	echo i860-intel-osf1
+	exit ;;
+    i860:*:4.*:*) # i860-SVR4
+	if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then
+	  echo i860-stardent-sysv${UNAME_RELEASE} # Stardent Vistra i860-SVR4
+	else # Add other i860-SVR4 vendors below as they are discovered.
+	  echo i860-unknown-sysv${UNAME_RELEASE}  # Unknown i860-SVR4
+	fi
+	exit ;;
+    mini*:CTIX:SYS*5:*)
+	# "miniframe"
+	echo m68010-convergent-sysv
+	exit ;;
+    mc68k:UNIX:SYSTEM5:3.51m)
+	echo m68k-convergent-sysv
+	exit ;;
+    M680?0:D-NIX:5.3:*)
+	echo m68k-diab-dnix
+	exit ;;
+    M68*:*:R3V[5678]*:*)
+	test -r /sysV68 && { echo 'm68k-motorola-sysv'; exit; } ;;
+    3[345]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0 | SDS2:*:4.0:3.0 | SHG2:*:4.0:3.0 | S7501*:*:4.0:3.0)
+	OS_REL=''
+	test -r /etc/.relid \
+	&& OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+	/bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+	  && { echo i486-ncr-sysv4.3${OS_REL}; exit; }
+	/bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+	  && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;;
+    3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*)
+	/bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+	  && { echo i486-ncr-sysv4; exit; } ;;
+    NCR*:*:4.2:* | MPRAS*:*:4.2:*)
+	OS_REL='.3'
+	test -r /etc/.relid \
+	    && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+	/bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+	    && { echo i486-ncr-sysv4.3${OS_REL}; exit; }
+	/bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+	    && { echo i586-ncr-sysv4.3${OS_REL}; exit; }
+	/bin/uname -p 2>/dev/null | /bin/grep pteron >/dev/null \
+	    && { echo i586-ncr-sysv4.3${OS_REL}; exit; } ;;
+    m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*)
+	echo m68k-unknown-lynxos${UNAME_RELEASE}
+	exit ;;
+    mc68030:UNIX_System_V:4.*:*)
+	echo m68k-atari-sysv4
+	exit ;;
+    TSUNAMI:LynxOS:2.*:*)
+	echo sparc-unknown-lynxos${UNAME_RELEASE}
+	exit ;;
+    rs6000:LynxOS:2.*:*)
+	echo rs6000-unknown-lynxos${UNAME_RELEASE}
+	exit ;;
+    PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.[02]*:*)
+	echo powerpc-unknown-lynxos${UNAME_RELEASE}
+	exit ;;
+    SM[BE]S:UNIX_SV:*:*)
+	echo mips-dde-sysv${UNAME_RELEASE}
+	exit ;;
+    RM*:ReliantUNIX-*:*:*)
+	echo mips-sni-sysv4
+	exit ;;
+    RM*:SINIX-*:*:*)
+	echo mips-sni-sysv4
+	exit ;;
+    *:SINIX-*:*:*)
+	if uname -p 2>/dev/null >/dev/null ; then
+		UNAME_MACHINE=`(uname -p) 2>/dev/null`
+		echo ${UNAME_MACHINE}-sni-sysv4
+	else
+		echo ns32k-sni-sysv
+	fi
+	exit ;;
+    PENTIUM:*:4.0*:*)	# Unisys `ClearPath HMP IX 4000' SVR4/MP effort
+			# says <Richard.M.Bartel at ccMail.Census.GOV>
+	echo i586-unisys-sysv4
+	exit ;;
+    *:UNIX_System_V:4*:FTX*)
+	# From Gerald Hewes <hewes at openmarket.com>.
+	# How about differentiating between stratus architectures? -djm
+	echo hppa1.1-stratus-sysv4
+	exit ;;
+    *:*:*:FTX*)
+	# From seanf at swdc.stratus.com.
+	echo i860-stratus-sysv4
+	exit ;;
+    i*86:VOS:*:*)
+	# From Paul.Green at stratus.com.
+	echo ${UNAME_MACHINE}-stratus-vos
+	exit ;;
+    *:VOS:*:*)
+	# From Paul.Green at stratus.com.
+	echo hppa1.1-stratus-vos
+	exit ;;
+    mc68*:A/UX:*:*)
+	echo m68k-apple-aux${UNAME_RELEASE}
+	exit ;;
+    news*:NEWS-OS:6*:*)
+	echo mips-sony-newsos6
+	exit ;;
+    R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*)
+	if [ -d /usr/nec ]; then
+		echo mips-nec-sysv${UNAME_RELEASE}
+	else
+		echo mips-unknown-sysv${UNAME_RELEASE}
+	fi
+	exit ;;
+    BeBox:BeOS:*:*)	# BeOS running on hardware made by Be, PPC only.
+	echo powerpc-be-beos
+	exit ;;
+    BeMac:BeOS:*:*)	# BeOS running on Mac or Mac clone, PPC only.
+	echo powerpc-apple-beos
+	exit ;;
+    BePC:BeOS:*:*)	# BeOS running on Intel PC compatible.
+	echo i586-pc-beos
+	exit ;;
+    BePC:Haiku:*:*)	# Haiku running on Intel PC compatible.
+	echo i586-pc-haiku
+	exit ;;
+    SX-4:SUPER-UX:*:*)
+	echo sx4-nec-superux${UNAME_RELEASE}
+	exit ;;
+    SX-5:SUPER-UX:*:*)
+	echo sx5-nec-superux${UNAME_RELEASE}
+	exit ;;
+    SX-6:SUPER-UX:*:*)
+	echo sx6-nec-superux${UNAME_RELEASE}
+	exit ;;
+    SX-7:SUPER-UX:*:*)
+	echo sx7-nec-superux${UNAME_RELEASE}
+	exit ;;
+    SX-8:SUPER-UX:*:*)
+	echo sx8-nec-superux${UNAME_RELEASE}
+	exit ;;
+    SX-8R:SUPER-UX:*:*)
+	echo sx8r-nec-superux${UNAME_RELEASE}
+	exit ;;
+    Power*:Rhapsody:*:*)
+	echo powerpc-apple-rhapsody${UNAME_RELEASE}
+	exit ;;
+    *:Rhapsody:*:*)
+	echo ${UNAME_MACHINE}-apple-rhapsody${UNAME_RELEASE}
+	exit ;;
+    *:Darwin:*:*)
+	UNAME_PROCESSOR=`uname -p` || UNAME_PROCESSOR=unknown
+	case $UNAME_PROCESSOR in
+	    i386)
+		eval $set_cc_for_build
+		if [ "$CC_FOR_BUILD" != 'no_compiler_found' ]; then
+		  if (echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | \
+		      (CCOPTS= $CC_FOR_BUILD -E - 2>/dev/null) | \
+		      grep IS_64BIT_ARCH >/dev/null
+		  then
+		      UNAME_PROCESSOR="x86_64"
+		  fi
+		fi ;;
+	    unknown) UNAME_PROCESSOR=powerpc ;;
+	esac
+	echo ${UNAME_PROCESSOR}-apple-darwin${UNAME_RELEASE}
+	exit ;;
+    *:procnto*:*:* | *:QNX:[0123456789]*:*)
+	UNAME_PROCESSOR=`uname -p`
+	if test "$UNAME_PROCESSOR" = "x86"; then
+		UNAME_PROCESSOR=i386
+		UNAME_MACHINE=pc
+	fi
+	echo ${UNAME_PROCESSOR}-${UNAME_MACHINE}-nto-qnx${UNAME_RELEASE}
+	exit ;;
+    *:QNX:*:4*)
+	echo i386-pc-qnx
+	exit ;;
+    NEO-?:NONSTOP_KERNEL:*:*)
+	echo neo-tandem-nsk${UNAME_RELEASE}
+	exit ;;
+    NSE-?:NONSTOP_KERNEL:*:*)
+	echo nse-tandem-nsk${UNAME_RELEASE}
+	exit ;;
+    NSR-?:NONSTOP_KERNEL:*:*)
+	echo nsr-tandem-nsk${UNAME_RELEASE}
+	exit ;;
+    *:NonStop-UX:*:*)
+	echo mips-compaq-nonstopux
+	exit ;;
+    BS2000:POSIX*:*:*)
+	echo bs2000-siemens-sysv
+	exit ;;
+    DS/*:UNIX_System_V:*:*)
+	echo ${UNAME_MACHINE}-${UNAME_SYSTEM}-${UNAME_RELEASE}
+	exit ;;
+    *:Plan9:*:*)
+	# "uname -m" is not consistent, so use $cputype instead. 386
+	# is converted to i386 for consistency with other x86
+	# operating systems.
+	if test "$cputype" = "386"; then
+	    UNAME_MACHINE=i386
+	else
+	    UNAME_MACHINE="$cputype"
+	fi
+	echo ${UNAME_MACHINE}-unknown-plan9
+	exit ;;
+    *:TOPS-10:*:*)
+	echo pdp10-unknown-tops10
+	exit ;;
+    *:TENEX:*:*)
+	echo pdp10-unknown-tenex
+	exit ;;
+    KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*)
+	echo pdp10-dec-tops20
+	exit ;;
+    XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*)
+	echo pdp10-xkl-tops20
+	exit ;;
+    *:TOPS-20:*:*)
+	echo pdp10-unknown-tops20
+	exit ;;
+    *:ITS:*:*)
+	echo pdp10-unknown-its
+	exit ;;
+    SEI:*:*:SEIUX)
+	echo mips-sei-seiux${UNAME_RELEASE}
+	exit ;;
+    *:DragonFly:*:*)
+	echo ${UNAME_MACHINE}-unknown-dragonfly`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`
+	exit ;;
+    *:*VMS:*:*)
+	UNAME_MACHINE=`(uname -p) 2>/dev/null`
+	case "${UNAME_MACHINE}" in
+	    A*) echo alpha-dec-vms ; exit ;;
+	    I*) echo ia64-dec-vms ; exit ;;
+	    V*) echo vax-dec-vms ; exit ;;
+	esac ;;
+    *:XENIX:*:SysV)
+	echo i386-pc-xenix
+	exit ;;
+    i*86:skyos:*:*)
+	echo ${UNAME_MACHINE}-pc-skyos`echo ${UNAME_RELEASE}` | sed -e 's/ .*$//'
+	exit ;;
+    i*86:rdos:*:*)
+	echo ${UNAME_MACHINE}-pc-rdos
+	exit ;;
+    i*86:AROS:*:*)
+	echo ${UNAME_MACHINE}-pc-aros
+	exit ;;
+    x86_64:VMkernel:*:*)
+	echo ${UNAME_MACHINE}-unknown-esx
+	exit ;;
+esac
+
+#echo '(No uname command or uname output not recognized.)' 1>&2
+#echo "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" 1>&2
+
+eval $set_cc_for_build
+cat >$dummy.c <<EOF
+#ifdef _SEQUENT_
+# include <sys/types.h>
+# include <sys/utsname.h>
+#endif
+main ()
+{
+#if defined (sony)
+#if defined (MIPSEB)
+  /* BFD wants "bsd" instead of "newsos".  Perhaps BFD should be changed,
+     I don't know....  */
+  printf ("mips-sony-bsd\n"); exit (0);
+#else
+#include <sys/param.h>
+  printf ("m68k-sony-newsos%s\n",
+#ifdef NEWSOS4
+	"4"
+#else
+	""
+#endif
+	); exit (0);
+#endif
+#endif
+
+#if defined (__arm) && defined (__acorn) && defined (__unix)
+  printf ("arm-acorn-riscix\n"); exit (0);
+#endif
+
+#if defined (hp300) && !defined (hpux)
+  printf ("m68k-hp-bsd\n"); exit (0);
+#endif
+
+#if defined (NeXT)
+#if !defined (__ARCHITECTURE__)
+#define __ARCHITECTURE__ "m68k"
+#endif
+  int version;
+  version=`(hostinfo | sed -n 's/.*NeXT Mach \([0-9]*\).*/\1/p') 2>/dev/null`;
+  if (version < 4)
+    printf ("%s-next-nextstep%d\n", __ARCHITECTURE__, version);
+  else
+    printf ("%s-next-openstep%d\n", __ARCHITECTURE__, version);
+  exit (0);
+#endif
+
+#if defined (MULTIMAX) || defined (n16)
+#if defined (UMAXV)
+  printf ("ns32k-encore-sysv\n"); exit (0);
+#else
+#if defined (CMU)
+  printf ("ns32k-encore-mach\n"); exit (0);
+#else
+  printf ("ns32k-encore-bsd\n"); exit (0);
+#endif
+#endif
+#endif
+
+#if defined (__386BSD__)
+  printf ("i386-pc-bsd\n"); exit (0);
+#endif
+
+#if defined (sequent)
+#if defined (i386)
+  printf ("i386-sequent-dynix\n"); exit (0);
+#endif
+#if defined (ns32000)
+  printf ("ns32k-sequent-dynix\n"); exit (0);
+#endif
+#endif
+
+#if defined (_SEQUENT_)
+    struct utsname un;
+
+    uname(&un);
+
+    if (strncmp(un.version, "V2", 2) == 0) {
+	printf ("i386-sequent-ptx2\n"); exit (0);
+    }
+    if (strncmp(un.version, "V1", 2) == 0) { /* XXX is V1 correct? */
+	printf ("i386-sequent-ptx1\n"); exit (0);
+    }
+    printf ("i386-sequent-ptx\n"); exit (0);
+
+#endif
+
+#if defined (vax)
+# if !defined (ultrix)
+#  include <sys/param.h>
+#  if defined (BSD)
+#   if BSD == 43
+      printf ("vax-dec-bsd4.3\n"); exit (0);
+#   else
+#    if BSD == 199006
+      printf ("vax-dec-bsd4.3reno\n"); exit (0);
+#    else
+      printf ("vax-dec-bsd\n"); exit (0);
+#    endif
+#   endif
+#  else
+    printf ("vax-dec-bsd\n"); exit (0);
+#  endif
+# else
+    printf ("vax-dec-ultrix\n"); exit (0);
+# endif
+#endif
+
+#if defined (alliant) && defined (i860)
+  printf ("i860-alliant-bsd\n"); exit (0);
+#endif
+
+  exit (1);
+}
+EOF
+
+$CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null && SYSTEM_NAME=`$dummy` &&
+	{ echo "$SYSTEM_NAME"; exit; }
+
+# Apollos put the system type in the environment.
+
+test -d /usr/apollo && { echo ${ISP}-apollo-${SYSTYPE}; exit; }
+
+# Convex versions that predate uname can use getsysinfo(1)
+
+if [ -x /usr/convex/getsysinfo ]
+then
+    case `getsysinfo -f cpu_type` in
+    c1*)
+	echo c1-convex-bsd
+	exit ;;
+    c2*)
+	if getsysinfo -f scalar_acc
+	then echo c32-convex-bsd
+	else echo c2-convex-bsd
+	fi
+	exit ;;
+    c34*)
+	echo c34-convex-bsd
+	exit ;;
+    c38*)
+	echo c38-convex-bsd
+	exit ;;
+    c4*)
+	echo c4-convex-bsd
+	exit ;;
+    esac
+fi
+
+cat >&2 <<EOF
+$0: unable to guess system type
+
+This script, last modified $timestamp, has failed to recognize
+the operating system you are using. It is advised that you
+download the most up to date version of the config scripts from
+
+  http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD
+and
+  http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD
+
+If the version you run ($0) is already up to date, please
+send the following data and any information you think might be
+pertinent to <config-patches at gnu.org> in order to provide the needed
+information to handle your system.
+
+config.guess timestamp = $timestamp
+
+uname -m = `(uname -m) 2>/dev/null || echo unknown`
+uname -r = `(uname -r) 2>/dev/null || echo unknown`
+uname -s = `(uname -s) 2>/dev/null || echo unknown`
+uname -v = `(uname -v) 2>/dev/null || echo unknown`
+
+/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null`
+/bin/uname -X     = `(/bin/uname -X) 2>/dev/null`
+
+hostinfo               = `(hostinfo) 2>/dev/null`
+/bin/universe          = `(/bin/universe) 2>/dev/null`
+/usr/bin/arch -k       = `(/usr/bin/arch -k) 2>/dev/null`
+/bin/arch              = `(/bin/arch) 2>/dev/null`
+/usr/bin/oslevel       = `(/usr/bin/oslevel) 2>/dev/null`
+/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null`
+
+UNAME_MACHINE = ${UNAME_MACHINE}
+UNAME_RELEASE = ${UNAME_RELEASE}
+UNAME_SYSTEM  = ${UNAME_SYSTEM}
+UNAME_VERSION = ${UNAME_VERSION}
+EOF
+
+exit 1
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/contrib/mod_auth_otp/config.sub b/contrib/mod_auth_otp/config.sub
new file mode 100755
index 0000000..59bb593
--- /dev/null
+++ b/contrib/mod_auth_otp/config.sub
@@ -0,0 +1,1779 @@
+#! /bin/sh
+# Configuration validation subroutine script.
+#   Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999,
+#   2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+#   2011, 2012 Free Software Foundation, Inc.
+
+timestamp='2012-04-18'
+
+# This file is (in principle) common to ALL GNU software.
+# The presence of a machine in this file suggests that SOME GNU software
+# can handle that machine.  It does not imply ALL GNU software can.
+#
+# This file 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, see <http://www.gnu.org/licenses/>.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+
+# Please send patches to <config-patches at gnu.org>.  Submit a context
+# diff and a properly formatted GNU ChangeLog entry.
+#
+# Configuration subroutine to validate and canonicalize a configuration type.
+# Supply the specified configuration type as an argument.
+# If it is invalid, we print an error message on stderr and exit with code 1.
+# Otherwise, we print the canonical config type on stdout and succeed.
+
+# You can get the latest version of this script from:
+# http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD
+
+# This file is supposed to be the same for all GNU packages
+# and recognize all the CPU types, system types and aliases
+# that are meaningful with *any* GNU software.
+# Each package is responsible for reporting which valid configurations
+# it does not support.  The user should be able to distinguish
+# a failure to support a valid configuration from a meaningless
+# configuration.
+
+# The goal of this file is to map all the various variations of a given
+# machine specification into a single specification in the form:
+#	CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM
+# or in some cases, the newer four-part form:
+#	CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM
+# It is wrong to echo any other type of specification.
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION] CPU-MFR-OPSYS
+       $0 [OPTION] ALIAS
+
+Canonicalize a configuration name.
+
+Operation modes:
+  -h, --help         print this help, then exit
+  -t, --time-stamp   print date of last modification, then exit
+  -v, --version      print version number, then exit
+
+Report bugs and patches to <config-patches at gnu.org>."
+
+version="\
+GNU config.sub ($timestamp)
+
+Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000,
+2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012
+Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+  case $1 in
+    --time-stamp | --time* | -t )
+       echo "$timestamp" ; exit ;;
+    --version | -v )
+       echo "$version" ; exit ;;
+    --help | --h* | -h )
+       echo "$usage"; exit ;;
+    -- )     # Stop option processing
+       shift; break ;;
+    - )	# Use stdin as input.
+       break ;;
+    -* )
+       echo "$me: invalid option $1$help"
+       exit 1 ;;
+
+    *local*)
+       # First pass through any local machine types.
+       echo $1
+       exit ;;
+
+    * )
+       break ;;
+  esac
+done
+
+case $# in
+ 0) echo "$me: missing argument$help" >&2
+    exit 1;;
+ 1) ;;
+ *) echo "$me: too many arguments$help" >&2
+    exit 1;;
+esac
+
+# Separate what the user gave into CPU-COMPANY and OS or KERNEL-OS (if any).
+# Here we must recognize all the valid KERNEL-OS combinations.
+maybe_os=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'`
+case $maybe_os in
+  nto-qnx* | linux-gnu* | linux-android* | linux-dietlibc | linux-newlib* | \
+  linux-uclibc* | uclinux-uclibc* | uclinux-gnu* | kfreebsd*-gnu* | \
+  knetbsd*-gnu* | netbsd*-gnu* | \
+  kopensolaris*-gnu* | \
+  storm-chaos* | os2-emx* | rtmk-nova*)
+    os=-$maybe_os
+    basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`
+    ;;
+  android-linux)
+    os=-linux-android
+    basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'`-unknown
+    ;;
+  *)
+    basic_machine=`echo $1 | sed 's/-[^-]*$//'`
+    if [ $basic_machine != $1 ]
+    then os=`echo $1 | sed 's/.*-/-/'`
+    else os=; fi
+    ;;
+esac
+
+### Let's recognize common machines as not being operating systems so
+### that things like config.sub decstation-3100 work.  We also
+### recognize some manufacturers as not being operating systems, so we
+### can provide default operating systems below.
+case $os in
+	-sun*os*)
+		# Prevent following clause from handling this invalid input.
+		;;
+	-dec* | -mips* | -sequent* | -encore* | -pc532* | -sgi* | -sony* | \
+	-att* | -7300* | -3300* | -delta* | -motorola* | -sun[234]* | \
+	-unicom* | -ibm* | -next | -hp | -isi* | -apollo | -altos* | \
+	-convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\
+	-c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \
+	-harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \
+	-apple | -axis | -knuth | -cray | -microblaze)
+		os=
+		basic_machine=$1
+		;;
+	-bluegene*)
+		os=-cnk
+		;;
+	-sim | -cisco | -oki | -wec | -winbond)
+		os=
+		basic_machine=$1
+		;;
+	-scout)
+		;;
+	-wrs)
+		os=-vxworks
+		basic_machine=$1
+		;;
+	-chorusos*)
+		os=-chorusos
+		basic_machine=$1
+		;;
+	-chorusrdb)
+		os=-chorusrdb
+		basic_machine=$1
+		;;
+	-hiux*)
+		os=-hiuxwe2
+		;;
+	-sco6)
+		os=-sco5v6
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-sco5)
+		os=-sco3.2v5
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-sco4)
+		os=-sco3.2v4
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-sco3.2.[4-9]*)
+		os=`echo $os | sed -e 's/sco3.2./sco3.2v/'`
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-sco3.2v[4-9]*)
+		# Don't forget version if it is 3.2v4 or newer.
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-sco5v6*)
+		# Don't forget version if it is 3.2v4 or newer.
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-sco*)
+		os=-sco3.2v2
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-udk*)
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-isc)
+		os=-isc2.2
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-clix*)
+		basic_machine=clipper-intergraph
+		;;
+	-isc*)
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'`
+		;;
+	-lynx*178)
+		os=-lynxos178
+		;;
+	-lynx*5)
+		os=-lynxos5
+		;;
+	-lynx*)
+		os=-lynxos
+		;;
+	-ptx*)
+		basic_machine=`echo $1 | sed -e 's/86-.*/86-sequent/'`
+		;;
+	-windowsnt*)
+		os=`echo $os | sed -e 's/windowsnt/winnt/'`
+		;;
+	-psos*)
+		os=-psos
+		;;
+	-mint | -mint[0-9]*)
+		basic_machine=m68k-atari
+		os=-mint
+		;;
+esac
+
+# Decode aliases for certain CPU-COMPANY combinations.
+case $basic_machine in
+	# Recognize the basic CPU types without company name.
+	# Some are omitted here because they have special meanings below.
+	1750a | 580 \
+	| a29k \
+	| aarch64 | aarch64_be \
+	| alpha | alphaev[4-8] | alphaev56 | alphaev6[78] | alphapca5[67] \
+	| alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] | alpha64pca5[67] \
+	| am33_2.0 \
+	| arc | arm | arm[bl]e | arme[lb] | armv[2345] | armv[345][lb] | avr | avr32 \
+        | be32 | be64 \
+	| bfin \
+	| c4x | clipper \
+	| d10v | d30v | dlx | dsp16xx \
+	| epiphany \
+	| fido | fr30 | frv \
+	| h8300 | h8500 | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \
+	| hexagon \
+	| i370 | i860 | i960 | ia64 \
+	| ip2k | iq2000 \
+	| le32 | le64 \
+	| lm32 \
+	| m32c | m32r | m32rle | m68000 | m68k | m88k \
+	| maxq | mb | microblaze | mcore | mep | metag \
+	| mips | mipsbe | mipseb | mipsel | mipsle \
+	| mips16 \
+	| mips64 | mips64el \
+	| mips64octeon | mips64octeonel \
+	| mips64orion | mips64orionel \
+	| mips64r5900 | mips64r5900el \
+	| mips64vr | mips64vrel \
+	| mips64vr4100 | mips64vr4100el \
+	| mips64vr4300 | mips64vr4300el \
+	| mips64vr5000 | mips64vr5000el \
+	| mips64vr5900 | mips64vr5900el \
+	| mipsisa32 | mipsisa32el \
+	| mipsisa32r2 | mipsisa32r2el \
+	| mipsisa64 | mipsisa64el \
+	| mipsisa64r2 | mipsisa64r2el \
+	| mipsisa64sb1 | mipsisa64sb1el \
+	| mipsisa64sr71k | mipsisa64sr71kel \
+	| mipstx39 | mipstx39el \
+	| mn10200 | mn10300 \
+	| moxie \
+	| mt \
+	| msp430 \
+	| nds32 | nds32le | nds32be \
+	| nios | nios2 \
+	| ns16k | ns32k \
+	| open8 \
+	| or32 \
+	| pdp10 | pdp11 | pj | pjl \
+	| powerpc | powerpc64 | powerpc64le | powerpcle \
+	| pyramid \
+	| rl78 | rx \
+	| score \
+	| sh | sh[1234] | sh[24]a | sh[24]aeb | sh[23]e | sh[34]eb | sheb | shbe | shle | sh[1234]le | sh3ele \
+	| sh64 | sh64le \
+	| sparc | sparc64 | sparc64b | sparc64v | sparc86x | sparclet | sparclite \
+	| sparcv8 | sparcv9 | sparcv9b | sparcv9v \
+	| spu \
+	| tahoe | tic4x | tic54x | tic55x | tic6x | tic80 | tron \
+	| ubicom32 \
+	| v850 | v850e | v850e1 | v850e2 | v850es | v850e2v3 \
+	| we32k \
+	| x86 | xc16x | xstormy16 | xtensa \
+	| z8k | z80)
+		basic_machine=$basic_machine-unknown
+		;;
+	c54x)
+		basic_machine=tic54x-unknown
+		;;
+	c55x)
+		basic_machine=tic55x-unknown
+		;;
+	c6x)
+		basic_machine=tic6x-unknown
+		;;
+	m6811 | m68hc11 | m6812 | m68hc12 | m68hcs12x | picochip)
+		basic_machine=$basic_machine-unknown
+		os=-none
+		;;
+	m88110 | m680[12346]0 | m683?2 | m68360 | m5200 | v70 | w65 | z8k)
+		;;
+	ms1)
+		basic_machine=mt-unknown
+		;;
+
+	strongarm | thumb | xscale)
+		basic_machine=arm-unknown
+		;;
+	xgate)
+		basic_machine=$basic_machine-unknown
+		os=-none
+		;;
+	xscaleeb)
+		basic_machine=armeb-unknown
+		;;
+
+	xscaleel)
+		basic_machine=armel-unknown
+		;;
+
+	# We use `pc' rather than `unknown'
+	# because (1) that's what they normally are, and
+	# (2) the word "unknown" tends to confuse beginning users.
+	i*86 | x86_64)
+	  basic_machine=$basic_machine-pc
+	  ;;
+	# Object if more than one company name word.
+	*-*-*)
+		echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2
+		exit 1
+		;;
+	# Recognize the basic CPU types with company name.
+	580-* \
+	| a29k-* \
+	| aarch64-* | aarch64_be-* \
+	| alpha-* | alphaev[4-8]-* | alphaev56-* | alphaev6[78]-* \
+	| alpha64-* | alpha64ev[4-8]-* | alpha64ev56-* | alpha64ev6[78]-* \
+	| alphapca5[67]-* | alpha64pca5[67]-* | arc-* \
+	| arm-*  | armbe-* | armle-* | armeb-* | armv*-* \
+	| avr-* | avr32-* \
+	| be32-* | be64-* \
+	| bfin-* | bs2000-* \
+	| c[123]* | c30-* | [cjt]90-* | c4x-* \
+	| clipper-* | craynv-* | cydra-* \
+	| d10v-* | d30v-* | dlx-* \
+	| elxsi-* \
+	| f30[01]-* | f700-* | fido-* | fr30-* | frv-* | fx80-* \
+	| h8300-* | h8500-* \
+	| hppa-* | hppa1.[01]-* | hppa2.0-* | hppa2.0[nw]-* | hppa64-* \
+	| hexagon-* \
+	| i*86-* | i860-* | i960-* | ia64-* \
+	| ip2k-* | iq2000-* \
+	| le32-* | le64-* \
+	| lm32-* \
+	| m32c-* | m32r-* | m32rle-* \
+	| m68000-* | m680[012346]0-* | m68360-* | m683?2-* | m68k-* \
+	| m88110-* | m88k-* | maxq-* | mcore-* | metag-* | microblaze-* \
+	| mips-* | mipsbe-* | mipseb-* | mipsel-* | mipsle-* \
+	| mips16-* \
+	| mips64-* | mips64el-* \
+	| mips64octeon-* | mips64octeonel-* \
+	| mips64orion-* | mips64orionel-* \
+	| mips64r5900-* | mips64r5900el-* \
+	| mips64vr-* | mips64vrel-* \
+	| mips64vr4100-* | mips64vr4100el-* \
+	| mips64vr4300-* | mips64vr4300el-* \
+	| mips64vr5000-* | mips64vr5000el-* \
+	| mips64vr5900-* | mips64vr5900el-* \
+	| mipsisa32-* | mipsisa32el-* \
+	| mipsisa32r2-* | mipsisa32r2el-* \
+	| mipsisa64-* | mipsisa64el-* \
+	| mipsisa64r2-* | mipsisa64r2el-* \
+	| mipsisa64sb1-* | mipsisa64sb1el-* \
+	| mipsisa64sr71k-* | mipsisa64sr71kel-* \
+	| mipstx39-* | mipstx39el-* \
+	| mmix-* \
+	| mt-* \
+	| msp430-* \
+	| nds32-* | nds32le-* | nds32be-* \
+	| nios-* | nios2-* \
+	| none-* | np1-* | ns16k-* | ns32k-* \
+	| open8-* \
+	| orion-* \
+	| pdp10-* | pdp11-* | pj-* | pjl-* | pn-* | power-* \
+	| powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* \
+	| pyramid-* \
+	| rl78-* | romp-* | rs6000-* | rx-* \
+	| sh-* | sh[1234]-* | sh[24]a-* | sh[24]aeb-* | sh[23]e-* | sh[34]eb-* | sheb-* | shbe-* \
+	| shle-* | sh[1234]le-* | sh3ele-* | sh64-* | sh64le-* \
+	| sparc-* | sparc64-* | sparc64b-* | sparc64v-* | sparc86x-* | sparclet-* \
+	| sparclite-* \
+	| sparcv8-* | sparcv9-* | sparcv9b-* | sparcv9v-* | sv1-* | sx?-* \
+	| tahoe-* \
+	| tic30-* | tic4x-* | tic54x-* | tic55x-* | tic6x-* | tic80-* \
+	| tile*-* \
+	| tron-* \
+	| ubicom32-* \
+	| v850-* | v850e-* | v850e1-* | v850es-* | v850e2-* | v850e2v3-* \
+	| vax-* \
+	| we32k-* \
+	| x86-* | x86_64-* | xc16x-* | xps100-* \
+	| xstormy16-* | xtensa*-* \
+	| ymp-* \
+	| z8k-* | z80-*)
+		;;
+	# Recognize the basic CPU types without company name, with glob match.
+	xtensa*)
+		basic_machine=$basic_machine-unknown
+		;;
+	# Recognize the various machine names and aliases which stand
+	# for a CPU type and a company and sometimes even an OS.
+	386bsd)
+		basic_machine=i386-unknown
+		os=-bsd
+		;;
+	3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc)
+		basic_machine=m68000-att
+		;;
+	3b*)
+		basic_machine=we32k-att
+		;;
+	a29khif)
+		basic_machine=a29k-amd
+		os=-udi
+		;;
+	abacus)
+		basic_machine=abacus-unknown
+		;;
+	adobe68k)
+		basic_machine=m68010-adobe
+		os=-scout
+		;;
+	alliant | fx80)
+		basic_machine=fx80-alliant
+		;;
+	altos | altos3068)
+		basic_machine=m68k-altos
+		;;
+	am29k)
+		basic_machine=a29k-none
+		os=-bsd
+		;;
+	amd64)
+		basic_machine=x86_64-pc
+		;;
+	amd64-*)
+		basic_machine=x86_64-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	amdahl)
+		basic_machine=580-amdahl
+		os=-sysv
+		;;
+	amiga | amiga-*)
+		basic_machine=m68k-unknown
+		;;
+	amigaos | amigados)
+		basic_machine=m68k-unknown
+		os=-amigaos
+		;;
+	amigaunix | amix)
+		basic_machine=m68k-unknown
+		os=-sysv4
+		;;
+	apollo68)
+		basic_machine=m68k-apollo
+		os=-sysv
+		;;
+	apollo68bsd)
+		basic_machine=m68k-apollo
+		os=-bsd
+		;;
+	aros)
+		basic_machine=i386-pc
+		os=-aros
+		;;
+	aux)
+		basic_machine=m68k-apple
+		os=-aux
+		;;
+	balance)
+		basic_machine=ns32k-sequent
+		os=-dynix
+		;;
+	blackfin)
+		basic_machine=bfin-unknown
+		os=-linux
+		;;
+	blackfin-*)
+		basic_machine=bfin-`echo $basic_machine | sed 's/^[^-]*-//'`
+		os=-linux
+		;;
+	bluegene*)
+		basic_machine=powerpc-ibm
+		os=-cnk
+		;;
+	c54x-*)
+		basic_machine=tic54x-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	c55x-*)
+		basic_machine=tic55x-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	c6x-*)
+		basic_machine=tic6x-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	c90)
+		basic_machine=c90-cray
+		os=-unicos
+		;;
+	cegcc)
+		basic_machine=arm-unknown
+		os=-cegcc
+		;;
+	convex-c1)
+		basic_machine=c1-convex
+		os=-bsd
+		;;
+	convex-c2)
+		basic_machine=c2-convex
+		os=-bsd
+		;;
+	convex-c32)
+		basic_machine=c32-convex
+		os=-bsd
+		;;
+	convex-c34)
+		basic_machine=c34-convex
+		os=-bsd
+		;;
+	convex-c38)
+		basic_machine=c38-convex
+		os=-bsd
+		;;
+	cray | j90)
+		basic_machine=j90-cray
+		os=-unicos
+		;;
+	craynv)
+		basic_machine=craynv-cray
+		os=-unicosmp
+		;;
+	cr16 | cr16-*)
+		basic_machine=cr16-unknown
+		os=-elf
+		;;
+	crds | unos)
+		basic_machine=m68k-crds
+		;;
+	crisv32 | crisv32-* | etraxfs*)
+		basic_machine=crisv32-axis
+		;;
+	cris | cris-* | etrax*)
+		basic_machine=cris-axis
+		;;
+	crx)
+		basic_machine=crx-unknown
+		os=-elf
+		;;
+	da30 | da30-*)
+		basic_machine=m68k-da30
+		;;
+	decstation | decstation-3100 | pmax | pmax-* | pmin | dec3100 | decstatn)
+		basic_machine=mips-dec
+		;;
+	decsystem10* | dec10*)
+		basic_machine=pdp10-dec
+		os=-tops10
+		;;
+	decsystem20* | dec20*)
+		basic_machine=pdp10-dec
+		os=-tops20
+		;;
+	delta | 3300 | motorola-3300 | motorola-delta \
+	      | 3300-motorola | delta-motorola)
+		basic_machine=m68k-motorola
+		;;
+	delta88)
+		basic_machine=m88k-motorola
+		os=-sysv3
+		;;
+	dicos)
+		basic_machine=i686-pc
+		os=-dicos
+		;;
+	djgpp)
+		basic_machine=i586-pc
+		os=-msdosdjgpp
+		;;
+	dpx20 | dpx20-*)
+		basic_machine=rs6000-bull
+		os=-bosx
+		;;
+	dpx2* | dpx2*-bull)
+		basic_machine=m68k-bull
+		os=-sysv3
+		;;
+	ebmon29k)
+		basic_machine=a29k-amd
+		os=-ebmon
+		;;
+	elxsi)
+		basic_machine=elxsi-elxsi
+		os=-bsd
+		;;
+	encore | umax | mmax)
+		basic_machine=ns32k-encore
+		;;
+	es1800 | OSE68k | ose68k | ose | OSE)
+		basic_machine=m68k-ericsson
+		os=-ose
+		;;
+	fx2800)
+		basic_machine=i860-alliant
+		;;
+	genix)
+		basic_machine=ns32k-ns
+		;;
+	gmicro)
+		basic_machine=tron-gmicro
+		os=-sysv
+		;;
+	go32)
+		basic_machine=i386-pc
+		os=-go32
+		;;
+	h3050r* | hiux*)
+		basic_machine=hppa1.1-hitachi
+		os=-hiuxwe2
+		;;
+	h8300hms)
+		basic_machine=h8300-hitachi
+		os=-hms
+		;;
+	h8300xray)
+		basic_machine=h8300-hitachi
+		os=-xray
+		;;
+	h8500hms)
+		basic_machine=h8500-hitachi
+		os=-hms
+		;;
+	harris)
+		basic_machine=m88k-harris
+		os=-sysv3
+		;;
+	hp300-*)
+		basic_machine=m68k-hp
+		;;
+	hp300bsd)
+		basic_machine=m68k-hp
+		os=-bsd
+		;;
+	hp300hpux)
+		basic_machine=m68k-hp
+		os=-hpux
+		;;
+	hp3k9[0-9][0-9] | hp9[0-9][0-9])
+		basic_machine=hppa1.0-hp
+		;;
+	hp9k2[0-9][0-9] | hp9k31[0-9])
+		basic_machine=m68000-hp
+		;;
+	hp9k3[2-9][0-9])
+		basic_machine=m68k-hp
+		;;
+	hp9k6[0-9][0-9] | hp6[0-9][0-9])
+		basic_machine=hppa1.0-hp
+		;;
+	hp9k7[0-79][0-9] | hp7[0-79][0-9])
+		basic_machine=hppa1.1-hp
+		;;
+	hp9k78[0-9] | hp78[0-9])
+		# FIXME: really hppa2.0-hp
+		basic_machine=hppa1.1-hp
+		;;
+	hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893)
+		# FIXME: really hppa2.0-hp
+		basic_machine=hppa1.1-hp
+		;;
+	hp9k8[0-9][13679] | hp8[0-9][13679])
+		basic_machine=hppa1.1-hp
+		;;
+	hp9k8[0-9][0-9] | hp8[0-9][0-9])
+		basic_machine=hppa1.0-hp
+		;;
+	hppa-next)
+		os=-nextstep3
+		;;
+	hppaosf)
+		basic_machine=hppa1.1-hp
+		os=-osf
+		;;
+	hppro)
+		basic_machine=hppa1.1-hp
+		os=-proelf
+		;;
+	i370-ibm* | ibm*)
+		basic_machine=i370-ibm
+		;;
+	i*86v32)
+		basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+		os=-sysv32
+		;;
+	i*86v4*)
+		basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+		os=-sysv4
+		;;
+	i*86v)
+		basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+		os=-sysv
+		;;
+	i*86sol2)
+		basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'`
+		os=-solaris2
+		;;
+	i386mach)
+		basic_machine=i386-mach
+		os=-mach
+		;;
+	i386-vsta | vsta)
+		basic_machine=i386-unknown
+		os=-vsta
+		;;
+	iris | iris4d)
+		basic_machine=mips-sgi
+		case $os in
+		    -irix*)
+			;;
+		    *)
+			os=-irix4
+			;;
+		esac
+		;;
+	isi68 | isi)
+		basic_machine=m68k-isi
+		os=-sysv
+		;;
+	m68knommu)
+		basic_machine=m68k-unknown
+		os=-linux
+		;;
+	m68knommu-*)
+		basic_machine=m68k-`echo $basic_machine | sed 's/^[^-]*-//'`
+		os=-linux
+		;;
+	m88k-omron*)
+		basic_machine=m88k-omron
+		;;
+	magnum | m3230)
+		basic_machine=mips-mips
+		os=-sysv
+		;;
+	merlin)
+		basic_machine=ns32k-utek
+		os=-sysv
+		;;
+	microblaze)
+		basic_machine=microblaze-xilinx
+		;;
+	mingw32)
+		basic_machine=i386-pc
+		os=-mingw32
+		;;
+	mingw32ce)
+		basic_machine=arm-unknown
+		os=-mingw32ce
+		;;
+	miniframe)
+		basic_machine=m68000-convergent
+		;;
+	*mint | -mint[0-9]* | *MiNT | *MiNT[0-9]*)
+		basic_machine=m68k-atari
+		os=-mint
+		;;
+	mips3*-*)
+		basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`
+		;;
+	mips3*)
+		basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`-unknown
+		;;
+	monitor)
+		basic_machine=m68k-rom68k
+		os=-coff
+		;;
+	morphos)
+		basic_machine=powerpc-unknown
+		os=-morphos
+		;;
+	msdos)
+		basic_machine=i386-pc
+		os=-msdos
+		;;
+	ms1-*)
+		basic_machine=`echo $basic_machine | sed -e 's/ms1-/mt-/'`
+		;;
+	msys)
+		basic_machine=i386-pc
+		os=-msys
+		;;
+	mvs)
+		basic_machine=i370-ibm
+		os=-mvs
+		;;
+	nacl)
+		basic_machine=le32-unknown
+		os=-nacl
+		;;
+	ncr3000)
+		basic_machine=i486-ncr
+		os=-sysv4
+		;;
+	netbsd386)
+		basic_machine=i386-unknown
+		os=-netbsd
+		;;
+	netwinder)
+		basic_machine=armv4l-rebel
+		os=-linux
+		;;
+	news | news700 | news800 | news900)
+		basic_machine=m68k-sony
+		os=-newsos
+		;;
+	news1000)
+		basic_machine=m68030-sony
+		os=-newsos
+		;;
+	news-3600 | risc-news)
+		basic_machine=mips-sony
+		os=-newsos
+		;;
+	necv70)
+		basic_machine=v70-nec
+		os=-sysv
+		;;
+	next | m*-next )
+		basic_machine=m68k-next
+		case $os in
+		    -nextstep* )
+			;;
+		    -ns2*)
+		      os=-nextstep2
+			;;
+		    *)
+		      os=-nextstep3
+			;;
+		esac
+		;;
+	nh3000)
+		basic_machine=m68k-harris
+		os=-cxux
+		;;
+	nh[45]000)
+		basic_machine=m88k-harris
+		os=-cxux
+		;;
+	nindy960)
+		basic_machine=i960-intel
+		os=-nindy
+		;;
+	mon960)
+		basic_machine=i960-intel
+		os=-mon960
+		;;
+	nonstopux)
+		basic_machine=mips-compaq
+		os=-nonstopux
+		;;
+	np1)
+		basic_machine=np1-gould
+		;;
+	neo-tandem)
+		basic_machine=neo-tandem
+		;;
+	nse-tandem)
+		basic_machine=nse-tandem
+		;;
+	nsr-tandem)
+		basic_machine=nsr-tandem
+		;;
+	op50n-* | op60c-*)
+		basic_machine=hppa1.1-oki
+		os=-proelf
+		;;
+	openrisc | openrisc-*)
+		basic_machine=or32-unknown
+		;;
+	os400)
+		basic_machine=powerpc-ibm
+		os=-os400
+		;;
+	OSE68000 | ose68000)
+		basic_machine=m68000-ericsson
+		os=-ose
+		;;
+	os68k)
+		basic_machine=m68k-none
+		os=-os68k
+		;;
+	pa-hitachi)
+		basic_machine=hppa1.1-hitachi
+		os=-hiuxwe2
+		;;
+	paragon)
+		basic_machine=i860-intel
+		os=-osf
+		;;
+	parisc)
+		basic_machine=hppa-unknown
+		os=-linux
+		;;
+	parisc-*)
+		basic_machine=hppa-`echo $basic_machine | sed 's/^[^-]*-//'`
+		os=-linux
+		;;
+	pbd)
+		basic_machine=sparc-tti
+		;;
+	pbb)
+		basic_machine=m68k-tti
+		;;
+	pc532 | pc532-*)
+		basic_machine=ns32k-pc532
+		;;
+	pc98)
+		basic_machine=i386-pc
+		;;
+	pc98-*)
+		basic_machine=i386-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	pentium | p5 | k5 | k6 | nexgen | viac3)
+		basic_machine=i586-pc
+		;;
+	pentiumpro | p6 | 6x86 | athlon | athlon_*)
+		basic_machine=i686-pc
+		;;
+	pentiumii | pentium2 | pentiumiii | pentium3)
+		basic_machine=i686-pc
+		;;
+	pentium4)
+		basic_machine=i786-pc
+		;;
+	pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*)
+		basic_machine=i586-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	pentiumpro-* | p6-* | 6x86-* | athlon-*)
+		basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	pentiumii-* | pentium2-* | pentiumiii-* | pentium3-*)
+		basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	pentium4-*)
+		basic_machine=i786-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	pn)
+		basic_machine=pn-gould
+		;;
+	power)	basic_machine=power-ibm
+		;;
+	ppc | ppcbe)	basic_machine=powerpc-unknown
+		;;
+	ppc-* | ppcbe-*)
+		basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	ppcle | powerpclittle | ppc-le | powerpc-little)
+		basic_machine=powerpcle-unknown
+		;;
+	ppcle-* | powerpclittle-*)
+		basic_machine=powerpcle-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	ppc64)	basic_machine=powerpc64-unknown
+		;;
+	ppc64-*) basic_machine=powerpc64-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	ppc64le | powerpc64little | ppc64-le | powerpc64-little)
+		basic_machine=powerpc64le-unknown
+		;;
+	ppc64le-* | powerpc64little-*)
+		basic_machine=powerpc64le-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	ps2)
+		basic_machine=i386-ibm
+		;;
+	pw32)
+		basic_machine=i586-unknown
+		os=-pw32
+		;;
+	rdos)
+		basic_machine=i386-pc
+		os=-rdos
+		;;
+	rom68k)
+		basic_machine=m68k-rom68k
+		os=-coff
+		;;
+	rm[46]00)
+		basic_machine=mips-siemens
+		;;
+	rtpc | rtpc-*)
+		basic_machine=romp-ibm
+		;;
+	s390 | s390-*)
+		basic_machine=s390-ibm
+		;;
+	s390x | s390x-*)
+		basic_machine=s390x-ibm
+		;;
+	sa29200)
+		basic_machine=a29k-amd
+		os=-udi
+		;;
+	sb1)
+		basic_machine=mipsisa64sb1-unknown
+		;;
+	sb1el)
+		basic_machine=mipsisa64sb1el-unknown
+		;;
+	sde)
+		basic_machine=mipsisa32-sde
+		os=-elf
+		;;
+	sei)
+		basic_machine=mips-sei
+		os=-seiux
+		;;
+	sequent)
+		basic_machine=i386-sequent
+		;;
+	sh)
+		basic_machine=sh-hitachi
+		os=-hms
+		;;
+	sh5el)
+		basic_machine=sh5le-unknown
+		;;
+	sh64)
+		basic_machine=sh64-unknown
+		;;
+	sparclite-wrs | simso-wrs)
+		basic_machine=sparclite-wrs
+		os=-vxworks
+		;;
+	sps7)
+		basic_machine=m68k-bull
+		os=-sysv2
+		;;
+	spur)
+		basic_machine=spur-unknown
+		;;
+	st2000)
+		basic_machine=m68k-tandem
+		;;
+	stratus)
+		basic_machine=i860-stratus
+		os=-sysv4
+		;;
+	strongarm-* | thumb-*)
+		basic_machine=arm-`echo $basic_machine | sed 's/^[^-]*-//'`
+		;;
+	sun2)
+		basic_machine=m68000-sun
+		;;
+	sun2os3)
+		basic_machine=m68000-sun
+		os=-sunos3
+		;;
+	sun2os4)
+		basic_machine=m68000-sun
+		os=-sunos4
+		;;
+	sun3os3)
+		basic_machine=m68k-sun
+		os=-sunos3
+		;;
+	sun3os4)
+		basic_machine=m68k-sun
+		os=-sunos4
+		;;
+	sun4os3)
+		basic_machine=sparc-sun
+		os=-sunos3
+		;;
+	sun4os4)
+		basic_machine=sparc-sun
+		os=-sunos4
+		;;
+	sun4sol2)
+		basic_machine=sparc-sun
+		os=-solaris2
+		;;
+	sun3 | sun3-*)
+		basic_machine=m68k-sun
+		;;
+	sun4)
+		basic_machine=sparc-sun
+		;;
+	sun386 | sun386i | roadrunner)
+		basic_machine=i386-sun
+		;;
+	sv1)
+		basic_machine=sv1-cray
+		os=-unicos
+		;;
+	symmetry)
+		basic_machine=i386-sequent
+		os=-dynix
+		;;
+	t3e)
+		basic_machine=alphaev5-cray
+		os=-unicos
+		;;
+	t90)
+		basic_machine=t90-cray
+		os=-unicos
+		;;
+	tile*)
+		basic_machine=$basic_machine-unknown
+		os=-linux-gnu
+		;;
+	tx39)
+		basic_machine=mipstx39-unknown
+		;;
+	tx39el)
+		basic_machine=mipstx39el-unknown
+		;;
+	toad1)
+		basic_machine=pdp10-xkl
+		os=-tops20
+		;;
+	tower | tower-32)
+		basic_machine=m68k-ncr
+		;;
+	tpf)
+		basic_machine=s390x-ibm
+		os=-tpf
+		;;
+	udi29k)
+		basic_machine=a29k-amd
+		os=-udi
+		;;
+	ultra3)
+		basic_machine=a29k-nyu
+		os=-sym1
+		;;
+	v810 | necv810)
+		basic_machine=v810-nec
+		os=-none
+		;;
+	vaxv)
+		basic_machine=vax-dec
+		os=-sysv
+		;;
+	vms)
+		basic_machine=vax-dec
+		os=-vms
+		;;
+	vpp*|vx|vx-*)
+		basic_machine=f301-fujitsu
+		;;
+	vxworks960)
+		basic_machine=i960-wrs
+		os=-vxworks
+		;;
+	vxworks68)
+		basic_machine=m68k-wrs
+		os=-vxworks
+		;;
+	vxworks29k)
+		basic_machine=a29k-wrs
+		os=-vxworks
+		;;
+	w65*)
+		basic_machine=w65-wdc
+		os=-none
+		;;
+	w89k-*)
+		basic_machine=hppa1.1-winbond
+		os=-proelf
+		;;
+	xbox)
+		basic_machine=i686-pc
+		os=-mingw32
+		;;
+	xps | xps100)
+		basic_machine=xps100-honeywell
+		;;
+	xscale-* | xscalee[bl]-*)
+		basic_machine=`echo $basic_machine | sed 's/^xscale/arm/'`
+		;;
+	ymp)
+		basic_machine=ymp-cray
+		os=-unicos
+		;;
+	z8k-*-coff)
+		basic_machine=z8k-unknown
+		os=-sim
+		;;
+	z80-*-coff)
+		basic_machine=z80-unknown
+		os=-sim
+		;;
+	none)
+		basic_machine=none-none
+		os=-none
+		;;
+
+# Here we handle the default manufacturer of certain CPU types.  It is in
+# some cases the only manufacturer, in others, it is the most popular.
+	w89k)
+		basic_machine=hppa1.1-winbond
+		;;
+	op50n)
+		basic_machine=hppa1.1-oki
+		;;
+	op60c)
+		basic_machine=hppa1.1-oki
+		;;
+	romp)
+		basic_machine=romp-ibm
+		;;
+	mmix)
+		basic_machine=mmix-knuth
+		;;
+	rs6000)
+		basic_machine=rs6000-ibm
+		;;
+	vax)
+		basic_machine=vax-dec
+		;;
+	pdp10)
+		# there are many clones, so DEC is not a safe bet
+		basic_machine=pdp10-unknown
+		;;
+	pdp11)
+		basic_machine=pdp11-dec
+		;;
+	we32k)
+		basic_machine=we32k-att
+		;;
+	sh[1234] | sh[24]a | sh[24]aeb | sh[34]eb | sh[1234]le | sh[23]ele)
+		basic_machine=sh-unknown
+		;;
+	sparc | sparcv8 | sparcv9 | sparcv9b | sparcv9v)
+		basic_machine=sparc-sun
+		;;
+	cydra)
+		basic_machine=cydra-cydrome
+		;;
+	orion)
+		basic_machine=orion-highlevel
+		;;
+	orion105)
+		basic_machine=clipper-highlevel
+		;;
+	mac | mpw | mac-mpw)
+		basic_machine=m68k-apple
+		;;
+	pmac | pmac-mpw)
+		basic_machine=powerpc-apple
+		;;
+	*-unknown)
+		# Make sure to match an already-canonicalized machine name.
+		;;
+	*)
+		echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2
+		exit 1
+		;;
+esac
+
+# Here we canonicalize certain aliases for manufacturers.
+case $basic_machine in
+	*-digital*)
+		basic_machine=`echo $basic_machine | sed 's/digital.*/dec/'`
+		;;
+	*-commodore*)
+		basic_machine=`echo $basic_machine | sed 's/commodore.*/cbm/'`
+		;;
+	*)
+		;;
+esac
+
+# Decode manufacturer-specific aliases for certain operating systems.
+
+if [ x"$os" != x"" ]
+then
+case $os in
+	# First match some system type aliases
+	# that might get confused with valid system types.
+	# -solaris* is a basic system type, with this one exception.
+	-auroraux)
+		os=-auroraux
+		;;
+	-solaris1 | -solaris1.*)
+		os=`echo $os | sed -e 's|solaris1|sunos4|'`
+		;;
+	-solaris)
+		os=-solaris2
+		;;
+	-svr4*)
+		os=-sysv4
+		;;
+	-unixware*)
+		os=-sysv4.2uw
+		;;
+	-gnu/linux*)
+		os=`echo $os | sed -e 's|gnu/linux|linux-gnu|'`
+		;;
+	# First accept the basic system types.
+	# The portable systems comes first.
+	# Each alternative MUST END IN A *, to match a version number.
+	# -sysv* is not here because it comes later, after sysvr4.
+	-gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \
+	      | -*vms* | -sco* | -esix* | -isc* | -aix* | -cnk* | -sunos | -sunos[34]*\
+	      | -hpux* | -unos* | -osf* | -luna* | -dgux* | -auroraux* | -solaris* \
+	      | -sym* | -kopensolaris* \
+	      | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \
+	      | -aos* | -aros* \
+	      | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \
+	      | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \
+	      | -hiux* | -386bsd* | -knetbsd* | -mirbsd* | -netbsd* \
+	      | -openbsd* | -solidbsd* \
+	      | -ekkobsd* | -kfreebsd* | -freebsd* | -riscix* | -lynxos* \
+	      | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \
+	      | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \
+	      | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \
+	      | -chorusos* | -chorusrdb* | -cegcc* \
+	      | -cygwin* | -msys* | -pe* | -psos* | -moss* | -proelf* | -rtems* \
+	      | -mingw32* | -linux-gnu* | -linux-android* \
+	      | -linux-newlib* | -linux-uclibc* \
+	      | -uxpv* | -beos* | -mpeix* | -udk* \
+	      | -interix* | -uwin* | -mks* | -rhapsody* | -darwin* | -opened* \
+	      | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* \
+	      | -storm-chaos* | -tops10* | -tenex* | -tops20* | -its* \
+	      | -os2* | -vos* | -palmos* | -uclinux* | -nucleus* \
+	      | -morphos* | -superux* | -rtmk* | -rtmk-nova* | -windiss* \
+	      | -powermax* | -dnix* | -nx6 | -nx7 | -sei* | -dragonfly* \
+	      | -skyos* | -haiku* | -rdos* | -toppers* | -drops* | -es*)
+	# Remember, each alternative MUST END IN *, to match a version number.
+		;;
+	-qnx*)
+		case $basic_machine in
+		    x86-* | i*86-*)
+			;;
+		    *)
+			os=-nto$os
+			;;
+		esac
+		;;
+	-nto-qnx*)
+		;;
+	-nto*)
+		os=`echo $os | sed -e 's|nto|nto-qnx|'`
+		;;
+	-sim | -es1800* | -hms* | -xray | -os68k* | -none* | -v88r* \
+	      | -windows* | -osx | -abug | -netware* | -os9* | -beos* | -haiku* \
+	      | -macos* | -mpw* | -magic* | -mmixware* | -mon960* | -lnews*)
+		;;
+	-mac*)
+		os=`echo $os | sed -e 's|mac|macos|'`
+		;;
+	-linux-dietlibc)
+		os=-linux-dietlibc
+		;;
+	-linux*)
+		os=`echo $os | sed -e 's|linux|linux-gnu|'`
+		;;
+	-sunos5*)
+		os=`echo $os | sed -e 's|sunos5|solaris2|'`
+		;;
+	-sunos6*)
+		os=`echo $os | sed -e 's|sunos6|solaris3|'`
+		;;
+	-opened*)
+		os=-openedition
+		;;
+	-os400*)
+		os=-os400
+		;;
+	-wince*)
+		os=-wince
+		;;
+	-osfrose*)
+		os=-osfrose
+		;;
+	-osf*)
+		os=-osf
+		;;
+	-utek*)
+		os=-bsd
+		;;
+	-dynix*)
+		os=-bsd
+		;;
+	-acis*)
+		os=-aos
+		;;
+	-atheos*)
+		os=-atheos
+		;;
+	-syllable*)
+		os=-syllable
+		;;
+	-386bsd)
+		os=-bsd
+		;;
+	-ctix* | -uts*)
+		os=-sysv
+		;;
+	-nova*)
+		os=-rtmk-nova
+		;;
+	-ns2 )
+		os=-nextstep2
+		;;
+	-nsk*)
+		os=-nsk
+		;;
+	# Preserve the version number of sinix5.
+	-sinix5.*)
+		os=`echo $os | sed -e 's|sinix|sysv|'`
+		;;
+	-sinix*)
+		os=-sysv4
+		;;
+	-tpf*)
+		os=-tpf
+		;;
+	-triton*)
+		os=-sysv3
+		;;
+	-oss*)
+		os=-sysv3
+		;;
+	-svr4)
+		os=-sysv4
+		;;
+	-svr3)
+		os=-sysv3
+		;;
+	-sysvr4)
+		os=-sysv4
+		;;
+	# This must come after -sysvr4.
+	-sysv*)
+		;;
+	-ose*)
+		os=-ose
+		;;
+	-es1800*)
+		os=-ose
+		;;
+	-xenix)
+		os=-xenix
+		;;
+	-*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*)
+		os=-mint
+		;;
+	-aros*)
+		os=-aros
+		;;
+	-kaos*)
+		os=-kaos
+		;;
+	-zvmoe)
+		os=-zvmoe
+		;;
+	-dicos*)
+		os=-dicos
+		;;
+	-nacl*)
+		;;
+	-none)
+		;;
+	*)
+		# Get rid of the `-' at the beginning of $os.
+		os=`echo $os | sed 's/[^-]*-//'`
+		echo Invalid configuration \`$1\': system \`$os\' not recognized 1>&2
+		exit 1
+		;;
+esac
+else
+
+# Here we handle the default operating systems that come with various machines.
+# The value should be what the vendor currently ships out the door with their
+# machine or put another way, the most popular os provided with the machine.
+
+# Note that if you're going to try to match "-MANUFACTURER" here (say,
+# "-sun"), then you have to tell the case statement up towards the top
+# that MANUFACTURER isn't an operating system.  Otherwise, code above
+# will signal an error saying that MANUFACTURER isn't an operating
+# system, and we'll never get to this point.
+
+case $basic_machine in
+	score-*)
+		os=-elf
+		;;
+	spu-*)
+		os=-elf
+		;;
+	*-acorn)
+		os=-riscix1.2
+		;;
+	arm*-rebel)
+		os=-linux
+		;;
+	arm*-semi)
+		os=-aout
+		;;
+	c4x-* | tic4x-*)
+		os=-coff
+		;;
+	tic54x-*)
+		os=-coff
+		;;
+	tic55x-*)
+		os=-coff
+		;;
+	tic6x-*)
+		os=-coff
+		;;
+	# This must come before the *-dec entry.
+	pdp10-*)
+		os=-tops20
+		;;
+	pdp11-*)
+		os=-none
+		;;
+	*-dec | vax-*)
+		os=-ultrix4.2
+		;;
+	m68*-apollo)
+		os=-domain
+		;;
+	i386-sun)
+		os=-sunos4.0.2
+		;;
+	m68000-sun)
+		os=-sunos3
+		;;
+	m68*-cisco)
+		os=-aout
+		;;
+	mep-*)
+		os=-elf
+		;;
+	mips*-cisco)
+		os=-elf
+		;;
+	mips*-*)
+		os=-elf
+		;;
+	or32-*)
+		os=-coff
+		;;
+	*-tti)	# must be before sparc entry or we get the wrong os.
+		os=-sysv3
+		;;
+	sparc-* | *-sun)
+		os=-sunos4.1.1
+		;;
+	*-be)
+		os=-beos
+		;;
+	*-haiku)
+		os=-haiku
+		;;
+	*-ibm)
+		os=-aix
+		;;
+	*-knuth)
+		os=-mmixware
+		;;
+	*-wec)
+		os=-proelf
+		;;
+	*-winbond)
+		os=-proelf
+		;;
+	*-oki)
+		os=-proelf
+		;;
+	*-hp)
+		os=-hpux
+		;;
+	*-hitachi)
+		os=-hiux
+		;;
+	i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent)
+		os=-sysv
+		;;
+	*-cbm)
+		os=-amigaos
+		;;
+	*-dg)
+		os=-dgux
+		;;
+	*-dolphin)
+		os=-sysv3
+		;;
+	m68k-ccur)
+		os=-rtu
+		;;
+	m88k-omron*)
+		os=-luna
+		;;
+	*-next )
+		os=-nextstep
+		;;
+	*-sequent)
+		os=-ptx
+		;;
+	*-crds)
+		os=-unos
+		;;
+	*-ns)
+		os=-genix
+		;;
+	i370-*)
+		os=-mvs
+		;;
+	*-next)
+		os=-nextstep3
+		;;
+	*-gould)
+		os=-sysv
+		;;
+	*-highlevel)
+		os=-bsd
+		;;
+	*-encore)
+		os=-bsd
+		;;
+	*-sgi)
+		os=-irix
+		;;
+	*-siemens)
+		os=-sysv4
+		;;
+	*-masscomp)
+		os=-rtu
+		;;
+	f30[01]-fujitsu | f700-fujitsu)
+		os=-uxpv
+		;;
+	*-rom68k)
+		os=-coff
+		;;
+	*-*bug)
+		os=-coff
+		;;
+	*-apple)
+		os=-macos
+		;;
+	*-atari*)
+		os=-mint
+		;;
+	*)
+		os=-none
+		;;
+esac
+fi
+
+# Here we handle the case where we know the os, and the CPU type, but not the
+# manufacturer.  We pick the logical manufacturer.
+vendor=unknown
+case $basic_machine in
+	*-unknown)
+		case $os in
+			-riscix*)
+				vendor=acorn
+				;;
+			-sunos*)
+				vendor=sun
+				;;
+			-cnk*|-aix*)
+				vendor=ibm
+				;;
+			-beos*)
+				vendor=be
+				;;
+			-hpux*)
+				vendor=hp
+				;;
+			-mpeix*)
+				vendor=hp
+				;;
+			-hiux*)
+				vendor=hitachi
+				;;
+			-unos*)
+				vendor=crds
+				;;
+			-dgux*)
+				vendor=dg
+				;;
+			-luna*)
+				vendor=omron
+				;;
+			-genix*)
+				vendor=ns
+				;;
+			-mvs* | -opened*)
+				vendor=ibm
+				;;
+			-os400*)
+				vendor=ibm
+				;;
+			-ptx*)
+				vendor=sequent
+				;;
+			-tpf*)
+				vendor=ibm
+				;;
+			-vxsim* | -vxworks* | -windiss*)
+				vendor=wrs
+				;;
+			-aux*)
+				vendor=apple
+				;;
+			-hms*)
+				vendor=hitachi
+				;;
+			-mpw* | -macos*)
+				vendor=apple
+				;;
+			-*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*)
+				vendor=atari
+				;;
+			-vos*)
+				vendor=stratus
+				;;
+		esac
+		basic_machine=`echo $basic_machine | sed "s/unknown/$vendor/"`
+		;;
+esac
+
+echo $basic_machine$os
+exit
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/contrib/mod_sftp/configure b/contrib/mod_auth_otp/configure
similarity index 97%
copy from contrib/mod_sftp/configure
copy to contrib/mod_auth_otp/configure
index 89d830d..ed5d646 100755
--- a/contrib/mod_sftp/configure
+++ b/contrib/mod_auth_otp/configure
@@ -576,7 +576,7 @@ PACKAGE_VERSION=
 PACKAGE_STRING=
 PACKAGE_BUGREPORT=
 
-ac_unique_file="./mod_sftp.c"
+ac_unique_file="./mod_auth_otp.c"
 # Factoring default headers for most tests.
 ac_includes_default="\
 #include <stdio.h>
@@ -672,8 +672,10 @@ OBJEXT
 CPP
 GREP
 EGREP
+SET_MAKE
 INCLUDES
 LIBDIRS
+UTILS_LIBS
 LIBOBJS
 LTLIBOBJS'
 ac_subst_files=''
@@ -1256,6 +1258,12 @@ if test -n "$ac_init_help"; then
 
   cat <<\_ACEOF
 
+Optional Features:
+  --disable-FEATURE       do not include FEATURE (same as --enable-FEATURE=no)
+  --enable-FEATURE[=ARG]  include FEATURE [ARG=yes]
+  --enable-devel          enable developer-only code (default=no)
+
+
 Optional Packages:
   --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]
   --without-PACKAGE       do not use PACKAGE (same as --with-PACKAGE=no)
@@ -1267,6 +1275,14 @@ Optional Packages:
                           colon-separated list of include paths to add e.g.
                           --with-libraries=/some/mysql/libdir:/my/libs
 
+  --with-modules=LIST     add additional modules to proftpd. LIST is a
+                          colon-separated list of modules to add e.g.
+                          --with-modules=mod_readme:mod_ifsession
+
+  --with-shared=LIST      build DSO modules for proftpd. LIST is a
+                          colon-separated list of modules to build as DSOs
+                          e.g. --with-shared=mod_rewrite:mod_ifsession
+
 
 Some influential environment variables:
   CC          C compiler command
@@ -3200,7 +3216,7 @@ else
   { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 
 { echo "$as_me:$LINENO: checking for library containing strerror" >&5
@@ -3354,7 +3370,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3375,7 +3391,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3679,6 +3695,36 @@ _ACEOF
 
 fi
 
+{ echo "$as_me:$LINENO: checking whether ${MAKE-make} sets \$(MAKE)" >&5
+echo $ECHO_N "checking whether ${MAKE-make} sets \$(MAKE)... $ECHO_C" >&6; }
+set x ${MAKE-make}; ac_make=`echo "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'`
+if { as_var=ac_cv_prog_make_${ac_make}_set; eval "test \"\${$as_var+set}\" = set"; }; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  cat >conftest.make <<\_ACEOF
+SHELL = /bin/sh
+all:
+	@echo '@@@%%%=$(MAKE)=@@@%%%'
+_ACEOF
+# GNU make sometimes prints "make[1]: Entering...", which would confuse us.
+case `${MAKE-make} -f conftest.make 2>/dev/null` in
+  *@@@%%%=?*=@@@%%%*)
+    eval ac_cv_prog_make_${ac_make}_set=yes;;
+  *)
+    eval ac_cv_prog_make_${ac_make}_set=no;;
+esac
+rm -f conftest.make
+fi
+if eval test \$ac_cv_prog_make_${ac_make}_set = yes; then
+  { echo "$as_me:$LINENO: result: yes" >&5
+echo "${ECHO_T}yes" >&6; }
+  SET_MAKE=
+else
+  { echo "$as_me:$LINENO: result: no" >&5
+echo "${ECHO_T}no" >&6; }
+  SET_MAKE="MAKE=${MAKE-make}"
+fi
+
 
 { echo "$as_me:$LINENO: checking for ANSI C header files" >&5
 echo $ECHO_N "checking for ANSI C header files... $ECHO_C" >&6; }
@@ -3748,7 +3794,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3769,7 +3815,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3858,6 +3904,7 @@ fi
 
 
 
+
 for ac_header in stdlib.h unistd.h limits.h fcntl.h
 do
 as_ac_Header=`echo "ac_cv_header_$ac_header" | $as_tr_sh`
@@ -3998,6 +4045,20 @@ fi
 done
 
 
+UTILS_LIBS="-lsupp -lcrypto"
+
+# Check whether --enable-devel was given.
+if test "${enable_devel+set}" = set; then
+  enableval=$enable_devel;
+    if test x"$enableval" != xno ; then
+      if test `echo $enableval | grep -c coverage` = "1" ; then
+        UTILS_LIBS="--coverage $UTILS_LIBS"
+      fi
+    fi
+
+fi
+
+
 
 # Check whether --with-includes was given.
 if test "${with_includes+set}" = set; then
@@ -4031,6 +4092,63 @@ fi
 
 
 
+# Check whether --with-modules was given.
+if test "${with_modules+set}" = set; then
+  withval=$with_modules;
+    if test x"$withval" != x; then
+      if test x"$withval" = xyes; then
+        { { echo "$as_me:$LINENO: error: --with-modules parameter missing required colon-separated list of modules" >&5
+echo "$as_me: error: --with-modules parameter missing required colon-separated list of modules" >&2;}
+   { (exit 1); exit 1; }; }
+      fi
+
+      if test x"$withval" != xno; then
+        modules_list=`echo "$withval" | sed -e 's/:/ /g'`;
+
+        for amodule in $modules_list; do
+          if test x"$amodule" = xmod_sftp ; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SFTP 1
+_ACEOF
+
+          fi
+        done
+      fi
+    fi
+
+fi
+
+
+
+# Check whether --with-shared was given.
+if test "${with_shared+set}" = set; then
+  withval=$with_shared;
+    if test x"$withval" != x; then
+      if test x"$withval" = xyes; then
+        { { echo "$as_me:$LINENO: error: --with-shared parameter missing required colon-separated list of modules" >&5
+echo "$as_me: error: --with-shared parameter missing required colon-separated list of modules" >&2;}
+   { (exit 1); exit 1; }; }
+      fi
+
+      if test x"$withval" != xno; then
+        shared_modules_list=`echo "$withval" | sed -e 's/:/ /g'`;
+
+        for amodule in $shared_modules_list; do
+          if test x"$amodule" = xmod_sftp ; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_SFTP 1
+_ACEOF
+
+          fi
+        done
+      fi
+    fi
+
+fi
+
+
 saved_libs="$LIBS"
 LIBS="$LIBS -lcrypto"
 
@@ -4039,7 +4157,7 @@ echo $ECHO_N "checking whether linking with OpenSSL functions requires -ldl... $
 saved_libs="$LIBS"
 
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -ldl"
+LIBS="-lcrypto -ldl $LIBS"
 
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
@@ -4082,6 +4200,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
     { echo "$as_me:$LINENO: result: yes" >&5
 echo "${ECHO_T}yes" >&6; }
     LIBS="$saved_libs -ldl"
+    UTILS_LIBS="$UTILS_LIBS -ldl"
 
 else
   echo "$as_me: failed program was:" >&5
@@ -4103,7 +4222,7 @@ echo $ECHO_N "checking whether linking with OpenSSL functions requires -lz... $E
 saved_libs="$LIBS"
 
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -lz"
+LIBS="-lcrypto -lz $LIBS"
 
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
@@ -4150,6 +4269,7 @@ eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
     { echo "$as_me:$LINENO: result: yes" >&5
 echo "${ECHO_T}yes" >&6; }
     LIBS="$saved_libs -lz"
+    UTILS_LIBS="$UTILS_LIBS -lz"
 
 else
   echo "$as_me: failed program was:" >&5
@@ -4169,77 +4289,6 @@ rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
 LIBS="-lcrypto -lz $LIBS"
 
-{ echo "$as_me:$LINENO: checking whether OpenSSL has crippled AES support" >&5
-echo $ECHO_N "checking whether OpenSSL has crippled AES support... $ECHO_C" >&6; }
-cat >conftest.$ac_ext <<_ACEOF
-/* confdefs.h.  */
-_ACEOF
-cat confdefs.h >>conftest.$ac_ext
-cat >>conftest.$ac_ext <<_ACEOF
-/* end confdefs.h.  */
- #ifdef HAVE_STRING_H
-    # include <string.h>
-    #endif
-    #include <openssl/evp.h>
-
-int
-main ()
-{
-
-    EVP_CIPHER *c;
-    c = EVP_aes_192_cbc();
-    c = EVP_aes_256_cbc();
-
-  ;
-  return 0;
-}
-_ACEOF
-rm -f conftest.$ac_objext conftest$ac_exeext
-if { (ac_try="$ac_link"
-case "(($ac_try" in
-  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
-  *) ac_try_echo=$ac_try;;
-esac
-eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
-  (eval "$ac_link") 2>conftest.er1
-  ac_status=$?
-  grep -v '^ *+' conftest.er1 >conftest.err
-  rm -f conftest.er1
-  cat conftest.err >&5
-  echo "$as_me:$LINENO: \$? = $ac_status" >&5
-  (exit $ac_status); } && {
-	 test -z "$ac_c_werror_flag" ||
-	 test ! -s conftest.err
-       } && test -s conftest$ac_exeext &&
-       $as_test_x conftest$ac_exeext; then
-
-    { echo "$as_me:$LINENO: result: no" >&5
-echo "${ECHO_T}no" >&6; }
-    LIBS="$saved_libs"
-
-else
-  echo "$as_me: failed program was:" >&5
-sed 's/^/| /' conftest.$ac_ext >&5
-
-
-    { echo "$as_me:$LINENO: result: yes" >&5
-echo "${ECHO_T}yes" >&6; }
-
-cat >>confdefs.h <<\_ACEOF
-#define HAVE_AES_CRIPPLED_OPENSSL 1
-_ACEOF
-
-    LIBS="$saved_libs"
-
-
-fi
-
-rm -f core conftest.err conftest.$ac_objext conftest_ipa8_conftest.oo \
-      conftest$ac_exeext conftest.$ac_ext
-
-LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="-lcrypto -lz $LIBS"
-
 { echo "$as_me:$LINENO: checking whether OpenSSL supports SHA256" >&5
 echo $ECHO_N "checking whether OpenSSL supports SHA256... $ECHO_C" >&6; }
 cat >conftest.$ac_ext <<_ACEOF
@@ -4382,9 +4431,10 @@ LIBDIRS="$ac_build_addl_libdirs"
 
 
 
-ac_config_headers="$ac_config_headers mod_sftp.h"
 
-ac_config_files="$ac_config_files Makefile"
+ac_config_headers="$ac_config_headers mod_auth_otp.h"
+
+ac_config_files="$ac_config_files t/Makefile Makefile"
 
 cat >confcache <<\_ACEOF
 # This file is a shell script that caches the results of configure
@@ -4938,7 +4988,8 @@ cat >>$CONFIG_STATUS <<\_ACEOF
 for ac_config_target in $ac_config_targets
 do
   case $ac_config_target in
-    "mod_sftp.h") CONFIG_HEADERS="$CONFIG_HEADERS mod_sftp.h" ;;
+    "mod_auth_otp.h") CONFIG_HEADERS="$CONFIG_HEADERS mod_auth_otp.h" ;;
+    "t/Makefile") CONFIG_FILES="$CONFIG_FILES t/Makefile" ;;
     "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;;
 
   *) { { echo "$as_me:$LINENO: error: invalid argument: $ac_config_target" >&5
@@ -5060,13 +5111,15 @@ OBJEXT!$OBJEXT$ac_delim
 CPP!$CPP$ac_delim
 GREP!$GREP$ac_delim
 EGREP!$EGREP$ac_delim
+SET_MAKE!$SET_MAKE$ac_delim
 INCLUDES!$INCLUDES$ac_delim
 LIBDIRS!$LIBDIRS$ac_delim
+UTILS_LIBS!$UTILS_LIBS$ac_delim
 LIBOBJS!$LIBOBJS$ac_delim
 LTLIBOBJS!$LTLIBOBJS$ac_delim
 _ACEOF
 
-  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 63; then
+  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 65; then
     break
   elif $ac_last_try; then
     { { echo "$as_me:$LINENO: error: could not make $CONFIG_STATUS" >&5
@@ -5422,7 +5475,7 @@ do
     cat >>$CONFIG_STATUS <<_ACEOF
     # First, check the format of the line:
     cat >"\$tmp/defines.sed" <<\\CEOF
-/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*/b def
+/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*\$/b def
 /^[	 ]*#[	 ]*define[	 ][	 ]*$ac_word_re[(	 ]/b def
 b
 :def
diff --git a/contrib/mod_sftp/configure.in b/contrib/mod_auth_otp/configure.in
similarity index 54%
copy from contrib/mod_sftp/configure.in
copy to contrib/mod_auth_otp/configure.in
index 931b78f..e948de4 100644
--- a/contrib/mod_sftp/configure.in
+++ b/contrib/mod_auth_otp/configure.in
@@ -1,4 +1,23 @@
-AC_INIT(./mod_sftp.c)
+dnl ProFTPD - mod_auth_otp
+dnl Copyright (c) 2015-2016 TJ Saunders <tj at castaglia.org>
+dnl
+dnl This program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program; if not, write to the Free Software
+dnl Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+dnl
+dnl Process this file with autoconf to produce a configure script.
+
+AC_INIT(./mod_auth_otp.c)
 
 AC_CANONICAL_SYSTEM
 
@@ -9,10 +28,29 @@ AC_PROG_CPP
 AC_AIX
 AC_ISC_POSIX
 AC_MINIX
+AC_PROG_MAKE_SET
 
 AC_HEADER_STDC
+
 AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h)
 
+UTILS_LIBS="-lsupp -lcrypto"
+
+dnl Need to support/handle the --enable-devel option, to see if coverage
+dnl is being used
+AC_ARG_ENABLE(devel,
+  [AC_HELP_STRING(
+    [--enable-devel],
+    [enable developer-only code (default=no)])
+  ],
+  [
+    if test x"$enableval" != xno ; then
+      if test `echo $enableval | grep -c coverage` = "1" ; then
+        UTILS_LIBS="--coverage $UTILS_LIBS"
+      fi
+    fi
+  ])
+
 dnl Need to support/handle the --with-includes and --with-libraries options
 AC_ARG_WITH(includes,
   [AC_HELP_STRING(
@@ -46,18 +84,54 @@ AC_ARG_WITH(libraries,
     LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
   ])
 
+dnl Need to support/handle the --with-modules and --with-shared options, to
+dnl see if mod_sftp will be built
+AC_ARG_WITH(modules,
+  [AC_HELP_STRING(
+    [--with-modules=LIST],
+    [add additional modules to proftpd. LIST is a colon-separated list of modules to add e.g. --with-modules=mod_readme:mod_ifsession])
+  ],
+  [
+    if test x"$withval" != x; then
+      if test x"$withval" = xyes; then
+        AC_MSG_ERROR([--with-modules parameter missing required colon-separated list of modules])
+      fi
+
+      if test x"$withval" != xno; then
+        modules_list=`echo "$withval" | sed -e 's/:/ /g'`;
+
+        for amodule in $modules_list; do
+          if test x"$amodule" = xmod_sftp ; then
+            AC_DEFINE(HAVE_SFTP, 1, [mod_sftp support enabled])
+          fi
+        done
+      fi
+    fi
+  ])
+
+AC_ARG_WITH(shared,
+  [AC_HELP_STRING(
+    [--with-shared=LIST],
+    [build DSO modules for proftpd. LIST is a colon-separated list of modules to build as DSOs e.g. --with-shared=mod_rewrite:mod_ifsession])
+  ],
+  [
+    if test x"$withval" != x; then
+      if test x"$withval" = xyes; then
+        AC_MSG_ERROR([--with-shared parameter missing required colon-separated list of modules])
+      fi
+
+      if test x"$withval" != xno; then
+        shared_modules_list=`echo "$withval" | sed -e 's/:/ /g'`;
+
+        for amodule in $shared_modules_list; do
+          if test x"$amodule" = xmod_sftp ; then
+            AC_DEFINE(HAVE_SFTP, 1, [mod_sftp support enabled])
+          fi
+        done
+      fi
+    fi
+  ])
 
-dnl Check for a crippled OpenSSL library (e.g. Solaris 10).  More details
-dnl can be found in:
-dnl
-dnl   http://fixunix.com/ssh/73273-openssh-solaris-10-amd64.html
-dnl   http://marc.info/?l=openssh-unix-dev&m=113245772008292&w=2
-dnl   http://opensolaris.org/os/project/crypto/Documentation/sunwcry/
-dnl
-dnl So for those users stuck using the Solaris 10 whose OpenSSL does
-dnl not provide support for AES ciphers longer than 128 bits, we need to
-dnl check and disable those symbols.  Otherwise mod_sftp fails to build due
-dnl to linker errors.
 saved_libs="$LIBS"
 LIBS="$LIBS -lcrypto"
 
@@ -66,7 +140,7 @@ saved_libs="$LIBS"
 
 dnl Splice out -lsupp, since that library hasn't been built yet
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -ldl"
+LIBS="-lcrypto -ldl $LIBS"
 
 AC_TRY_LINK(
   [
@@ -76,6 +150,7 @@ AC_TRY_LINK(
   ], [
     AC_MSG_RESULT(yes)
     LIBS="$saved_libs -ldl"
+    UTILS_LIBS="$UTILS_LIBS -ldl"
   ], [
     AC_MSG_RESULT(no)
     LIBS="$saved_libs"
@@ -87,7 +162,7 @@ saved_libs="$LIBS"
 
 dnl Splice out -lsupp, since that library hasn't been built yet
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -lz"
+LIBS="-lcrypto -lz $LIBS"
 
 AC_TRY_LINK(
   [
@@ -101,6 +176,7 @@ AC_TRY_LINK(
   ], [
     AC_MSG_RESULT(yes)
     LIBS="$saved_libs -lz"
+    UTILS_LIBS="$UTILS_LIBS -lz"
   ], [
     AC_MSG_RESULT(no)
     LIBS="$saved_libs"
@@ -111,33 +187,6 @@ dnl Splice out -lsupp, since that library hasn't been built yet
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
 LIBS="-lcrypto -lz $LIBS"
 
-AC_MSG_CHECKING([whether OpenSSL has crippled AES support])
-AC_TRY_LINK(
-  [ #ifdef HAVE_STRING_H
-    # include <string.h>
-    #endif
-    #include <openssl/evp.h>
-  ],
-  [
-    EVP_CIPHER *c;
-    c = EVP_aes_192_cbc();
-    c = EVP_aes_256_cbc();
-  ],
-  [
-    AC_MSG_RESULT(no)
-    LIBS="$saved_libs"
-  ],
-  [
-    AC_MSG_RESULT(yes)
-    AC_DEFINE(HAVE_AES_CRIPPLED_OPENSSL, 1, [OpenSSL is missing AES192 and AES256 support])
-    LIBS="$saved_libs"
-  ]
-)
-
-dnl Splice out -lsupp, since that library hasn't been built yet
-LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="-lcrypto -lz $LIBS"
-
 AC_MSG_CHECKING([whether OpenSSL supports SHA256])
 AC_TRY_LINK(
   [
@@ -190,6 +239,10 @@ LIBDIRS="$ac_build_addl_libdirs"
 AC_SUBST(INCLUDES)
 AC_SUBST(LDFLAGS)
 AC_SUBST(LIBDIRS)
+AC_SUBST(UTILS_LIBS)
 
-AC_CONFIG_HEADER(mod_sftp.h)
-AC_OUTPUT(Makefile)
+AC_CONFIG_HEADER(mod_auth_otp.h)
+AC_OUTPUT(
+  t/Makefile
+  Makefile
+)
diff --git a/contrib/mod_auth_otp/crypto.c b/contrib/mod_auth_otp/crypto.c
new file mode 100644
index 0000000..c479d56
--- /dev/null
+++ b/contrib/mod_auth_otp/crypto.c
@@ -0,0 +1,128 @@
+/*
+ * ProFTPD - mod_auth_otp OpenSSL interface
+ * Copyright (c) 2015-2017 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.
+ */
+
+#include "mod_auth_otp.h"
+#include "crypto.h"
+
+int auth_otp_crypto_init(void) {
+  return 0;
+}
+
+void auth_otp_crypto_free(int flags) {
+  /* Only call EVP_cleanup() et al if other OpenSSL-using modules are not
+   * present.  If we called EVP_cleanup() here during a restart,
+   * and other modules want to use OpenSSL, we may be depriving those modules
+   * of OpenSSL functionality.
+   *
+   * At the moment, the modules known to use OpenSSL are mod_ldap,
+   * mod_sftp, mod_sql, and mod_sql_passwd, and mod_tls.
+   */
+  if (pr_module_get("mod_digest.c") == NULL &&
+      pr_module_get("mod_ldap.c") == NULL &&
+      pr_module_get("mod_proxy.c") == NULL &&
+      pr_module_get("mod_radius.c") == NULL &&
+      pr_module_get("mod_sftp.c") == NULL &&
+      pr_module_get("mod_sql.c") == NULL &&
+      pr_module_get("mod_sql_passwd.c") == NULL &&
+      pr_module_get("mod_tls.c") == NULL) {
+
+    ERR_free_strings();
+
+#if OPENSSL_VERSION_NUMBER >= 0x10000001L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
+    ERR_remove_thread_state();
+# else
+    /* The ERR_remove_state(0) usage is deprecated due to thread ID
+     * differences among platforms; see the OpenSSL-1.0.0c CHANGES file
+     * for details.  So for new enough OpenSSL installations, use the
+     * proper way to clear the error queue state.
+     */
+    ERR_remove_thread_state(NULL);
+# endif /* OpenSSL-1.1.x and later */
+#else
+    ERR_remove_state(0);
+#endif /* OpenSSL prior to 1.0.0-beta1 */
+
+    EVP_cleanup();
+    RAND_cleanup();
+  }
+}
+
+const char *auth_otp_crypto_get_errors(void) {
+  unsigned int count = 0;
+  unsigned long e = ERR_get_error();
+  BIO *bio = NULL;
+  char *data = NULL;
+  long datalen;
+  const char *str = "(unknown)";
+
+  /* Use ERR_print_errors() and a memory BIO to build up a string with
+   * all of the error messages from the error queue.
+   */
+
+  if (e) {
+    bio = BIO_new(BIO_s_mem());
+  }
+
+  while (e) {
+    pr_signals_handle();
+    BIO_printf(bio, "\n  (%u) %s", ++count, ERR_error_string(e, NULL));
+    e = ERR_get_error();
+  }
+
+  datalen = BIO_get_mem_data(bio, &data);
+  if (data) {
+    data[datalen] = '\0';
+    str = pstrndup(auth_otp_pool, data, datalen-1);
+  }
+
+  if (bio) {
+    BIO_free(bio);
+  }
+
+  return str;
+}
+
+int auth_otp_hmac(const EVP_MD *md, const unsigned char *key, size_t key_len,
+    const unsigned char *data, size_t data_len, unsigned char *mac,
+    size_t *mac_len) {
+
+  if ((key == NULL || key_len == 0) ||
+      (data == NULL || data_len == 0) ||
+      (mac == NULL || mac_len == NULL)) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (HMAC(md, key, key_len, data, data_len, mac,
+      (unsigned int *) mac_len) == NULL) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "HMAC error: %s", auth_otp_crypto_get_errors());
+    errno = EPERM;
+    return -1;
+  }
+ 
+  return 0;
+}
diff --git a/contrib/mod_sftp/service.h b/contrib/mod_auth_otp/crypto.h
similarity index 67%
copy from contrib/mod_sftp/service.h
copy to contrib/mod_auth_otp/crypto.h
index 0be2b36..37c29bb 100644
--- a/contrib/mod_sftp/service.h
+++ b/contrib/mod_auth_otp/crypto.h
@@ -1,6 +1,6 @@
 /*
- * ProFTPD - mod_sftp services (service)
- * Copyright (c) 2008-2011 TJ Saunders
+ * ProFTPD - mod_auth_otp misc crypto routines
+ * Copyright (c) 2015-2016 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
@@ -20,18 +20,20 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: service.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
+#ifndef MOD_AUTH_OTP_CRYPTO_H
+#define MOD_AUTH_OTP_CRYPTO_H
+
+#include "mod_auth_otp.h"
 
-#ifndef MOD_SFTP_SERVICE_H
-#define MOD_SFTP_SERVICE_H
+int auth_otp_crypto_init(void);
+void auth_otp_crypto_free(int);
 
-#include "packet.h"
+const char *auth_otp_crypto_get_errors(void);
 
-int sftp_service_handle(struct ssh2_packet *);
-int sftp_service_init(void);
+int auth_otp_hmac(const EVP_MD *md, const unsigned char *key, size_t key_len,
+  const unsigned char *data, size_t data_len, unsigned char *mac,
+  size_t *mac_len);
 
-#endif
+#endif /* MOD_AUTH_OTP_CRYPTO_H */
diff --git a/contrib/mod_auth_otp/db.c b/contrib/mod_auth_otp/db.c
new file mode 100644
index 0000000..4282646
--- /dev/null
+++ b/contrib/mod_auth_otp/db.c
@@ -0,0 +1,461 @@
+/*
+ * ProFTPD - mod_auth_otp database storage
+ * Copyright (c) 2015-2016 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.
+ */
+
+#include "mod_auth_otp.h"
+#include "mod_sql.h"
+#include "base32.h"
+#include "db.h"
+
+#define AUTH_OTP_SQL_VALUE_BUFSZ	32
+
+/* Max number of attempts for lock requests */
+#define AUTH_OTP_MAX_LOCK_ATTEMPTS	10
+
+static const char *trace_channel = "auth_otp";
+
+static char *db_get_name(pool *p, const char *name) {
+  cmdtable *cmdtab;
+  cmd_rec *cmd;
+  modret_t *res;
+
+  /* Find the cmdtable for the sql_escapestr command. */
+  cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_escapestr", NULL, NULL, NULL);
+  if (cmdtab == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: unable to find SQL hook symbol 'sql_escapestr'");
+    return pstrdup(p, name);
+  }
+
+  if (strlen(name) == 0) {
+    return pstrdup(p, "");
+  }
+
+  cmd = pr_cmd_alloc(p, 1, pr_str_strip(p, (char *) name));
+
+  /* Call the handler. */
+  res = pr_module_call(cmdtab->m, cmdtab->handler, cmd);
+
+  /* Check the results. */
+  if (MODRET_ISDECLINED(res) ||
+      MODRET_ISERROR(res)) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error executing 'sql_escapestring'");
+    return pstrdup(p, name);
+  }
+
+  return res->data;
+}
+
+int auth_otp_db_close(struct auth_otp_db *dbh) {
+  if (dbh->db_lockfd > 0) {
+    (void) close(dbh->db_lockfd);
+    dbh->db_lockfd = -1;
+  }
+
+  destroy_pool(dbh->pool);
+  return 0;
+}
+
+struct auth_otp_db *auth_otp_db_open(pool *p, const char *tabinfo) {
+  struct auth_otp_db *dbh = NULL;
+  pool *db_pool = NULL, *tmp_pool = NULL;
+  char *ptr, *ptr2, *named_query, *select_query = NULL, *update_query = NULL;
+  config_rec *c;
+
+  /* The tabinfo should look like:
+   *  "/<select-named-query>/<update-named-query>"
+   *
+   * Parse the named queries out of the string, and store them in the db
+   * handle.
+   */
+
+  ptr = strchr(tabinfo, '/');
+  if (ptr == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: badly formatted table info '%s'", tabinfo);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  ptr2 = strchr(ptr + 1, '/');
+  if (ptr2 == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: badly formatted table info '%s'", tabinfo);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  db_pool = make_sub_pool(p);
+  pr_pool_tag(db_pool, "Auth OTP Table Pool");
+  dbh = pcalloc(db_pool, sizeof(struct auth_otp_db));
+  dbh->pool = db_pool;
+
+  tmp_pool = make_sub_pool(p);
+
+  *ptr2 = '\0';
+  select_query = pstrdup(dbh->pool, ptr + 1);
+
+  /* Verify that the named query has indeed been defined. This is based on how
+   * mod_sql creates its config_rec names.
+   */
+  named_query = pstrcat(tmp_pool, "SQLNamedQuery_", select_query, NULL);
+  c = find_config(main_server->conf, CONF_PARAM, named_query, FALSE);
+  if (c == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: unable to resolve SQLNamedQuery name '%s'", select_query);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  *ptr = *ptr2 = '/';
+
+  ptr = ptr2;
+  ptr2 = strchr(ptr + 1, '/');
+  if (ptr2 != NULL) {
+    *ptr2 = '\0';
+  }
+
+  update_query = pstrdup(dbh->pool, ptr + 1);
+
+  if (ptr2 != NULL) {
+    *ptr2 = '/';
+  }
+
+  named_query = pstrcat(tmp_pool, "SQLNamedQuery_", update_query, NULL);
+  c = find_config(main_server->conf, CONF_PARAM, named_query, FALSE);
+  if (c == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: unable to resolve SQLNamedQuery name '%s'", update_query);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  destroy_pool(tmp_pool);
+
+  dbh->select_query = select_query;
+  dbh->update_query = update_query;
+
+  /* Prepare the lock structure. */
+  dbh->db_lock.l_whence = SEEK_CUR;
+  dbh->db_lock.l_start = 0;
+  dbh->db_lock.l_len = 0;
+
+  return dbh;
+}
+
+int auth_otp_db_get_user_info(pool *p, struct auth_otp_db *dbh,
+    const char *user, const unsigned char **secret, size_t *secret_len,
+    unsigned long *counter) {
+  int res;
+  pool *tmp_pool = NULL;
+  cmdtable *sql_cmdtab = NULL;
+  cmd_rec *sql_cmd = NULL;
+  modret_t *sql_res = NULL;
+  array_header *sql_data = NULL;
+  const char *select_query = NULL;
+  char *encoded, **values = NULL;
+  size_t encoded_len;
+  unsigned int nvalues = 0;
+
+  if (dbh == NULL ||
+      user == NULL ||
+      secret == NULL ||
+      secret_len == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Allocate a temporary pool for the duration of this lookup. */
+  tmp_pool = make_sub_pool(p);
+
+  /* Find the cmdtable for the sql_lookup command. */
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
+  if (sql_cmdtab == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: unable to find SQL hook symbol 'sql_lookup'");
+    destroy_pool(tmp_pool);
+    errno = EPERM;
+    return -1;
+  }
+
+  /* Prepare the SELECT query. */
+  select_query = dbh->select_query;
+  sql_cmd = pr_cmd_alloc(tmp_pool, 3, "sql_lookup", select_query,
+    db_get_name(tmp_pool, user));
+
+  /* Call the handler. */
+  sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
+
+  /* Check the results. */
+  if (sql_res == NULL ||
+      MODRET_ISERROR(sql_res)) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error processing SQLNamedQuery '%s'", select_query);
+    destroy_pool(tmp_pool);
+    errno = EPERM;
+    return -1;
+  }
+
+  sql_data = (array_header *) sql_res->data;
+
+  /* The expected number of items in the result set depends on whether we
+   * want/need the HOTP counter.  If not, then it's only 1 (for the secret),
+   * otherwise 2 (secret and current counter).
+   */
+  nvalues = (counter ? 2 : 1);
+
+  if (sql_data->nelts < nvalues) {
+    if (sql_data->nelts > 0) {
+      pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "error: SQLNamedQuery '%s' returned incorrect number of values (%d)",
+        select_query, sql_data->nelts);
+    }
+
+    destroy_pool(tmp_pool);
+
+    errno = (sql_data->nelts == 0) ? ENOENT : EINVAL;
+    return -1;
+  }
+
+  values = sql_data->elts;
+
+  /* Don't forget to base32-decode the value from the database. */
+  encoded = values[0];
+  encoded_len = strlen(encoded);
+
+  res = auth_otp_base32_decode(p, (const unsigned char *) encoded, encoded_len,
+    secret, secret_len);
+  if (res < 0) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error base32-decoding value from database: %s", strerror(errno));
+    errno = EPERM;
+    return -1;
+  }
+
+  pr_memscrub(values[0], *secret_len);
+
+  if (counter != NULL) {
+    *counter = (unsigned long) atol(values[1]);
+  }
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int auth_otp_db_have_user_info(pool *p, struct auth_otp_db *dbh,
+    const char *user) {
+  int res, xerrno = 0;
+  const unsigned char *secret = NULL;
+  size_t secret_len = 0;
+
+  res = auth_otp_db_get_user_info(p, dbh, user, &secret, &secret_len, NULL);
+  xerrno = errno;
+
+  if (res == 0) {
+    pr_memscrub((void *) secret, secret_len);
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+int auth_otp_db_update_counter(struct auth_otp_db *dbh, const char *user,
+    unsigned long counter) {
+  pool *tmp_pool = NULL;
+  cmdtable *sql_cmdtab = NULL;
+  cmd_rec *sql_cmd = NULL;
+  modret_t *sql_res = NULL;
+  const char *update_query = NULL;
+  char *counter_str = NULL;
+  size_t counter_len = 0;
+
+  if (dbh == NULL ||
+      user == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Allocate a temporary pool for the duration of this change. */
+  tmp_pool = make_sub_pool(dbh->pool);
+
+  /* Find the cmdtable for the sql_change command. */
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_change", NULL, NULL,
+    NULL);
+  if (sql_cmdtab == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error: unable to find SQL hook symbol 'sql_change'");
+    destroy_pool(tmp_pool);
+    return -1;
+  }
+
+  update_query = dbh->update_query;
+  counter_len = AUTH_OTP_SQL_VALUE_BUFSZ * sizeof(char);
+  counter_str = pcalloc(tmp_pool, counter_len);
+  snprintf(counter_str, counter_len-1, "%lu", counter);
+
+  sql_cmd = pr_cmd_alloc(tmp_pool, 4, "sql_change", update_query,
+    db_get_name(tmp_pool, user), counter_str);
+
+  /* Call the handler. */
+  sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
+
+  /* Check the results. */
+  if (sql_res == NULL ||
+      MODRET_ISERROR(sql_res)) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "error processing SQLNamedQuery '%s'", update_query);
+    destroy_pool(tmp_pool);
+    errno = EPERM;
+    return -1;
+  }
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+/* Locking routines */
+
+static const char *get_lock_type(struct flock *lock) {
+  const char *lock_type;
+
+  switch (lock->l_type) {
+    case F_RDLCK:
+      lock_type = "read-lock";
+      break;
+
+    case F_WRLCK:
+      lock_type = "write-lock";
+      break;
+
+    case F_UNLCK:
+      lock_type = "unlock";
+      break;
+
+    default:
+      lock_type = "[unknown]";
+  }
+
+  return lock_type;
+}
+
+static int do_lock(int fd, struct flock *lock) {
+  unsigned int nattempts = 1;
+  const char *lock_type;
+
+  lock_type = get_lock_type(lock);
+
+  pr_trace_msg(trace_channel, 9,
+    "attempt #%u to %s AuthOTPTableLock fd %d", nattempts, lock_type, fd);
+
+  while (fcntl(fd, F_SETLK, lock) < 0) {
+    int xerrno = errno;
+
+    if (xerrno == EINTR) {
+      pr_signals_handle();
+      continue;
+    }
+
+    pr_trace_msg(trace_channel, 3,
+      "%s (attempt #%u) of AuthOTPTableLock fd %d failed: %s", lock_type,
+      nattempts, fd, strerror(xerrno));
+    if (xerrno == EACCES) {
+      struct flock locker;
+
+      /* Get the PID of the process blocking this lock. */
+      if (fcntl(fd, F_GETLK, &locker) == 0) {
+        pr_trace_msg(trace_channel, 3, "process ID %lu has blocking %s lock on "
+          "AuthOTPTableLock fd %d", (unsigned long) locker.l_pid,
+          get_lock_type(&locker), fd);
+      }
+    }
+
+    if (xerrno == EAGAIN ||
+        xerrno == EACCES) {
+      /* Treat this as an interrupted call, call pr_signals_handle() (which
+       * will delay for a few msecs because of EINTR), and try again.
+       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
+       */
+
+      nattempts++;
+      if (nattempts <= AUTH_OTP_MAX_LOCK_ATTEMPTS) {
+        errno = EINTR;
+
+        pr_signals_handle();
+
+        errno = 0;
+        pr_trace_msg(trace_channel, 9,
+          "attempt #%u to %s AuthOTPTableLock fd %d", nattempts, lock_type, fd);
+        continue;
+      }
+
+      pr_trace_msg(trace_channel, 9, "unable to acquire %s on "
+        "AuthOTPTableLock fd %d after %u attempts: %s", lock_type, fd,
+        nattempts, strerror(xerrno));
+    }
+
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9,
+    "%s of AuthOTPTableLock fd %d successful after %u %s", lock_type, fd,
+    nattempts, nattempts != 1 ? "attempts" : "attempt");
+  return 0;
+}
+
+int auth_otp_db_rlock(struct auth_otp_db *dbh) {
+  int res = 0;
+
+  if (dbh->db_lockfd > 0) {
+    dbh->db_lock.l_type = F_RDLCK;
+    res = do_lock(dbh->db_lockfd, &dbh->db_lock);
+  }
+
+  return res;
+}
+
+int auth_otp_db_wlock(struct auth_otp_db *dbh) {
+  int res = 0;
+
+  if (dbh->db_lockfd > 0) {
+    dbh->db_lock.l_type = F_WRLCK;
+    res = do_lock(dbh->db_lockfd, &dbh->db_lock);
+  }
+
+  return res;
+}
+
+int auth_otp_db_unlock(struct auth_otp_db *dbh) {
+  int res = 0;
+
+  if (dbh->db_lockfd > 0) {
+    dbh->db_lock.l_type = F_UNLCK;
+    res = do_lock(dbh->db_lockfd, &dbh->db_lock);
+  }
+
+  return res;
+}
diff --git a/contrib/mod_auth_otp/db.h b/contrib/mod_auth_otp/db.h
new file mode 100644
index 0000000..8c0ab60
--- /dev/null
+++ b/contrib/mod_auth_otp/db.h
@@ -0,0 +1,60 @@
+/*
+ * ProFTPD - mod_auth_otp database routines
+ * Copyright (c) 2015-2016 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.
+ */
+
+#ifndef MOD_AUTH_OTP_DB_H
+#define MOD_AUTH_OTP_DB_H
+
+#include "mod_auth_otp.h"
+
+struct auth_otp_db {
+  pool *pool;
+
+  const char *select_query;
+  const char *update_query;
+
+  /* Database locking support. */
+  struct flock db_lock;
+  int db_lockfd;
+};
+
+int auth_otp_db_close(struct auth_otp_db *dbh);
+struct auth_otp_db *auth_otp_db_open(pool *p, const char *dbinfo);
+int auth_otp_db_rlock(struct auth_otp_db *dbh);
+int auth_otp_db_wlock(struct auth_otp_db *dbh);
+int auth_otp_db_unlock(struct auth_otp_db *dbh);
+
+/* Ask if the table has info (secrets, counters) for this user. */
+int auth_otp_db_have_user_info(pool *p, struct auth_otp_db *dbh,
+  const char *user);
+
+/* Retrieve the user's base32-encoded secret, and current counter (for HOTP). */
+int auth_otp_db_get_user_info(pool *p, struct auth_otp_db *dbh,
+  const char *user, const unsigned char **secret, size_t *secret_len,
+  unsigned long *counter);
+
+/* Update the user's current counter (for HOTP). */
+int auth_otp_db_update_counter(struct auth_otp_db *dbh, const char *user,
+  unsigned long counter);
+
+#endif /* MOD_AUTH_OTP_DB_H */
diff --git a/contrib/mod_auth_otp/install-sh b/contrib/mod_auth_otp/install-sh
new file mode 100755
index 0000000..ebc6691
--- /dev/null
+++ b/contrib/mod_auth_otp/install-sh
@@ -0,0 +1,250 @@
+#! /bin/sh
+#
+# install - install a program, script, or datafile
+# This comes from X11R5 (mit/util/scripts/install.sh).
+#
+# Copyright 1991 by the Massachusetts Institute of Technology
+#
+# Permission to use, copy, modify, distribute, and sell this software and its
+# documentation for any purpose is hereby granted without fee, provided that
+# the above copyright notice appear in all copies and that both that
+# copyright notice and this permission notice appear in supporting
+# documentation, and that the name of M.I.T. not be used in advertising or
+# publicity pertaining to distribution of the software without specific,
+# written prior permission.  M.I.T. makes no representations about the
+# suitability of this software for any purpose.  It is provided "as is"
+# without express or implied warranty.
+#
+# Calling this script install-sh is preferred over install.sh, to prevent
+# `make' implicit rules from creating a file called install from it
+# when there is no Makefile.
+#
+# This script is compatible with the BSD install script, but was written
+# from scratch.  It can only install one file at a time, a restriction
+# shared with many OS's install programs.
+
+
+# set DOITPROG to echo to test this script
+
+# Don't use :- since 4.3BSD and earlier shells don't like it.
+doit="${DOITPROG-}"
+
+
+# put in absolute paths if you don't have them in your path; or use env. vars.
+
+mvprog="${MVPROG-mv}"
+cpprog="${CPPROG-cp}"
+chmodprog="${CHMODPROG-chmod}"
+chownprog="${CHOWNPROG-chown}"
+chgrpprog="${CHGRPPROG-chgrp}"
+stripprog="${STRIPPROG-strip}"
+rmprog="${RMPROG-rm}"
+mkdirprog="${MKDIRPROG-mkdir}"
+
+transformbasename=""
+transform_arg=""
+instcmd="$mvprog"
+chmodcmd="$chmodprog 0755"
+chowncmd=""
+chgrpcmd=""
+stripcmd=""
+rmcmd="$rmprog -f"
+mvcmd="$mvprog"
+src=""
+dst=""
+dir_arg=""
+
+while [ x"$1" != x ]; do
+    case $1 in
+	-c) instcmd="$cpprog"
+	    shift
+	    continue;;
+
+	-d) dir_arg=true
+	    shift
+	    continue;;
+
+	-m) chmodcmd="$chmodprog $2"
+	    shift
+	    shift
+	    continue;;
+
+	-o) chowncmd="$chownprog $2"
+	    shift
+	    shift
+	    continue;;
+
+	-g) chgrpcmd="$chgrpprog $2"
+	    shift
+	    shift
+	    continue;;
+
+	-s) stripcmd="$stripprog"
+	    shift
+	    continue;;
+
+	-t=*) transformarg=`echo $1 | sed 's/-t=//'`
+	    shift
+	    continue;;
+
+	-b=*) transformbasename=`echo $1 | sed 's/-b=//'`
+	    shift
+	    continue;;
+
+	*)  if [ x"$src" = x ]
+	    then
+		src=$1
+	    else
+		# this colon is to work around a 386BSD /bin/sh bug
+		:
+		dst=$1
+	    fi
+	    shift
+	    continue;;
+    esac
+done
+
+if [ x"$src" = x ]
+then
+	echo "install:	no input file specified"
+	exit 1
+else
+	true
+fi
+
+if [ x"$dir_arg" != x ]; then
+	dst=$src
+	src=""
+	
+	if [ -d $dst ]; then
+		instcmd=:
+	else
+		instcmd=mkdir
+	fi
+else
+
+# Waiting for this to be detected by the "$instcmd $src $dsttmp" command
+# might cause directories to be created, which would be especially bad 
+# if $src (and thus $dsttmp) contains '*'.
+
+	if [ -f $src -o -d $src ]
+	then
+		true
+	else
+		echo "install:  $src does not exist"
+		exit 1
+	fi
+	
+	if [ x"$dst" = x ]
+	then
+		echo "install:	no destination specified"
+		exit 1
+	else
+		true
+	fi
+
+# If destination is a directory, append the input filename; if your system
+# does not like double slashes in filenames, you may need to add some logic
+
+	if [ -d $dst ]
+	then
+		dst="$dst"/`basename $src`
+	else
+		true
+	fi
+fi
+
+## this sed command emulates the dirname command
+dstdir=`echo $dst | sed -e 's,[^/]*$,,;s,/$,,;s,^$,.,'`
+
+# Make sure that the destination directory exists.
+#  this part is taken from Noah Friedman's mkinstalldirs script
+
+# Skip lots of stat calls in the usual case.
+if [ ! -d "$dstdir" ]; then
+defaultIFS='	
+'
+IFS="${IFS-${defaultIFS}}"
+
+oIFS="${IFS}"
+# Some sh's can't handle IFS=/ for some reason.
+IFS='%'
+set - `echo ${dstdir} | sed -e 's@/@%@g' -e 's@^%@/@'`
+IFS="${oIFS}"
+
+pathcomp=''
+
+while [ $# -ne 0 ] ; do
+	pathcomp="${pathcomp}${1}"
+	shift
+
+	if [ ! -d "${pathcomp}" ] ;
+        then
+		$mkdirprog "${pathcomp}"
+	else
+		true
+	fi
+
+	pathcomp="${pathcomp}/"
+done
+fi
+
+if [ x"$dir_arg" != x ]
+then
+	$doit $instcmd $dst &&
+
+	if [ x"$chowncmd" != x ]; then $doit $chowncmd $dst; else true ; fi &&
+	if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dst; else true ; fi &&
+	if [ x"$stripcmd" != x ]; then $doit $stripcmd $dst; else true ; fi &&
+	if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dst; else true ; fi
+else
+
+# If we're going to rename the final executable, determine the name now.
+
+	if [ x"$transformarg" = x ] 
+	then
+		dstfile=`basename $dst`
+	else
+		dstfile=`basename $dst $transformbasename | 
+			sed $transformarg`$transformbasename
+	fi
+
+# don't allow the sed command to completely eliminate the filename
+
+	if [ x"$dstfile" = x ] 
+	then
+		dstfile=`basename $dst`
+	else
+		true
+	fi
+
+# Make a temp file name in the proper directory.
+
+	dsttmp=$dstdir/#inst.$$#
+
+# Move or copy the file name to the temp name
+
+	$doit $instcmd $src $dsttmp &&
+
+	trap "rm -f ${dsttmp}" 0 &&
+
+# and set any options; do chmod last to preserve setuid bits
+
+# If any of these fail, we abort the whole thing.  If we want to
+# ignore errors from any of these, just make sure not to ignore
+# errors from the above "$doit $instcmd $src $dsttmp" command.
+
+	if [ x"$chowncmd" != x ]; then $doit $chowncmd $dsttmp; else true;fi &&
+	if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dsttmp; else true;fi &&
+	if [ x"$stripcmd" != x ]; then $doit $stripcmd $dsttmp; else true;fi &&
+	if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dsttmp; else true;fi &&
+
+# Now rename the file to the real destination.
+
+	$doit $rmcmd -f $dstdir/$dstfile &&
+	$doit $mvcmd $dsttmp $dstdir/$dstfile 
+
+fi &&
+
+
+exit 0
diff --git a/contrib/mod_auth_otp/mod_auth_otp.c b/contrib/mod_auth_otp/mod_auth_otp.c
new file mode 100644
index 0000000..cfc10bd
--- /dev/null
+++ b/contrib/mod_auth_otp/mod_auth_otp.c
@@ -0,0 +1,1069 @@
+/*
+ * ProFTPD: mod_auth_otp
+ * Copyright (c) 2015-2016 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.
+ *
+ * -----DO NOT EDIT BELOW THIS LINE-----
+ * $Archive: mod_auth_otp.a $
+ * $Libraries: -lcrypto$
+ */
+
+#include "mod_auth_otp.h"
+#if defined(HAVE_SFTP)
+# include "mod_sftp.h"
+#endif /* HAVE_SFTP */
+#include "db.h"
+#include "otp.h"
+
+/* mod_auth_otp option flags */
+#define AUTH_OTP_OPT_STANDARD_RESPONSE		0x001
+#define AUTH_OTP_OPT_REQUIRE_TABLE_ENTRY	0x002
+#define AUTH_OTP_OPT_DISPLAY_VERIFICATION_CODE	0x004
+
+#define AUTH_OTP_VERIFICATION_CODE_PROMPT	"Verification code: "
+
+/* From src/response.c */
+extern pr_response_t *resp_list;
+
+pool *auth_otp_pool = NULL;
+int auth_otp_logfd = -1;
+unsigned long auth_otp_opts = 0UL;
+module auth_otp_module;
+
+static authtable auth_otp_authtab[3];
+static int auth_otp_engine = FALSE;
+static unsigned int auth_otp_algo = AUTH_OTP_ALGO_TOTP_SHA1;
+static struct auth_otp_db *dbh = NULL;
+static config_rec *auth_otp_db_config = NULL;
+static int auth_otp_auth_code = PR_AUTH_BADPWD;
+
+/* Necessary prototypes */
+static int auth_otp_sess_init(void);
+static int handle_user_otp(pool *p, const char *user, const char *user_otp,
+  int authoritative);
+
+#if defined(HAVE_SFTP)
+/* mod_sftp support */
+static int auth_otp_using_sftp = FALSE;
+static sftp_kbdint_driver_t auth_otp_kbdint_driver;
+#endif /* HAVE_SFTP */
+
+static const char *trace_channel = "auth_otp";
+
+#if defined(HAVE_SFTP)
+static int auth_otp_kbdint_open(sftp_kbdint_driver_t *driver,
+    const char *user) {
+  const char *tabinfo;
+  int xerrno;
+
+  tabinfo = auth_otp_db_config->argv[0];
+
+  PRIVS_ROOT
+  dbh = auth_otp_db_open(driver->driver_pool, tabinfo);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (dbh == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "unable to open AuthOTPTable: %s", strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  driver->driver_pool = make_sub_pool(auth_otp_pool);
+  pr_pool_tag(driver->driver_pool, "AuthOTP keyboard-interactive driver pool");
+
+  return 0;
+}
+
+static int auth_otp_kbdint_authenticate(sftp_kbdint_driver_t *driver,
+    const char *user) {
+  int authoritative = FALSE, res, xerrno;
+  sftp_kbdint_challenge_t *challenge;
+  unsigned int recvd_count = 0;
+  const char **recvd_responses = NULL, *user_otp = NULL;
+
+  if (auth_otp_authtab[0].auth_flags & PR_AUTH_FL_REQUIRED) {
+    authoritative = TRUE;
+  }
+
+  /* Check first to see if we even have information for this user, for to
+   * use when verifying them.  If not, then don't prompt the user for info
+   * that we know, a priori, we cannot verify.
+   */
+  res = auth_otp_db_rlock(dbh);
+  if (res < 0) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "failed to read-lock AuthOTPTable: %s", strerror(errno));
+  }
+
+  res = auth_otp_db_have_user_info(driver->driver_pool, dbh, user);
+  xerrno = errno;
+
+  if (auth_otp_db_unlock(dbh) < 0) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "failed to unlock AuthOTPTable: %s", strerror(errno));
+  }
+
+  if (res < 0) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "no info for user '%s' found in AuthOTPTable, skipping "
+      "SSH2 keyboard-interactive challenge", user);
+    errno = xerrno;
+    return -1;
+  }
+
+  challenge = pcalloc(driver->driver_pool, sizeof(sftp_kbdint_challenge_t));
+  challenge->challenge = pstrdup(driver->driver_pool,
+    AUTH_OTP_VERIFICATION_CODE_PROMPT);
+  challenge->display_response = FALSE;
+
+  if (auth_otp_opts & AUTH_OTP_OPT_DISPLAY_VERIFICATION_CODE) {
+    challenge->display_response = TRUE;
+  }
+
+  if (sftp_kbdint_send_challenge(NULL, NULL, 1, challenge) < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error sending keyboard-interactive challenges: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (sftp_kbdint_recv_response(driver->driver_pool, 1,
+      &recvd_count, &recvd_responses) < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error receiving keyboard-interactive responses: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  user_otp = recvd_responses[0];
+  res = handle_user_otp(driver->driver_pool, user, user_otp, authoritative);
+  if (res == 1) {
+    return 0;
+  }
+
+  errno = EPERM;
+  return -1;
+}
+
+static int auth_otp_kbdint_close(sftp_kbdint_driver_t *driver) {
+  if (dbh != NULL) {
+    if (auth_otp_db_close(dbh) < 0) {
+      (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "error closing AuthOTPTable: %s", strerror(errno));
+    }
+
+    dbh = NULL;
+  }
+  
+  if (driver->driver_pool) {
+    destroy_pool(driver->driver_pool);
+    driver->driver_pool = NULL;
+  }
+
+  return 0;
+}
+#endif /* HAVE_SFTP */
+
+static int check_otp_code(pool *p, const char *user, const char *user_otp,
+    const unsigned char *secret, size_t secret_len, unsigned long counter) {
+  int res;
+  char code_str[9];
+  unsigned int code;
+
+  switch (auth_otp_algo) {
+    case AUTH_OTP_ALGO_TOTP_SHA1:
+    case AUTH_OTP_ALGO_TOTP_SHA256:
+    case AUTH_OTP_ALGO_TOTP_SHA512:
+      res = auth_otp_totp(p, secret, secret_len, counter, auth_otp_algo, &code);
+      if (res < 0) {
+        pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+          "error generating TOTP code for user '%s': %s", user,
+          strerror(errno));
+      }
+      break;
+
+    case AUTH_OTP_ALGO_HOTP:
+      res = auth_otp_hotp(p, secret, secret_len, counter, &code);
+      if (res < 0) {
+        pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+          "error generating HOTP code for user '%s': %s", user,
+          strerror(errno));
+      }
+      break;
+
+    default:
+      errno = EINVAL;
+      res = -1;
+      break;
+  }
+
+  if (res < 0) {
+    return -1;
+  }
+
+  memset(code_str, '\0', sizeof(code_str));
+
+  /* Note: If/when more than 6 digits are needed, the following format string
+   * would need to change to match.
+   */
+  snprintf(code_str, sizeof(code_str)-1, "%06u", code);
+
+  pr_trace_msg(trace_channel, 13,
+    "computed code '%s', client sent code '%s'", code_str, user_otp);
+
+  res = pr_auth_check(p, code_str, user, user_otp);
+  if (res == PR_AUTH_OK ||
+      res == PR_AUTH_RFC2228_OK) {
+    return 0;
+  }
+ 
+  return -1;
+}
+
+static int update_otp_counter(pool *p, const char *user,
+    unsigned long next_counter) {
+  int res = 0;
+
+  if (auth_otp_algo == AUTH_OTP_ALGO_HOTP) {
+    int lock;
+
+    lock = auth_otp_db_wlock(dbh);
+    if (lock < 0) {
+      (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "failed to write-lock AuthOTPTable: %s", strerror(errno));
+    }
+
+    res = auth_otp_db_update_counter(dbh, user, next_counter);
+    if (res < 0) {
+      (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "error updating AuthOTPTable for HOTP counter for user '%s': %s",
+        user, strerror(errno));
+    }
+
+    lock = auth_otp_db_unlock(dbh);
+    if (lock < 0) {
+      (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "failed to unlock AuthOTPTable: %s", strerror(errno));
+    }
+  }
+
+  return res;
+}
+
+/* Sets the auth_otp_auth_code variable upon failure. */
+static int handle_user_otp(pool *p, const char *user, const char *user_otp,
+    int authoritative) {
+  int res = 0, xerrno = 0;
+  const unsigned char *secret = NULL;
+  size_t secret_len = 0;
+  unsigned long counter = 0, *counter_ptr = NULL, next_counter = 0;
+
+  if (user_otp == NULL ||
+      (strlen(user_otp) == 0)) {
+    pr_trace_msg(trace_channel, 1,
+      "no OTP code provided by user, rejecting");
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "FAILED: user '%s' provided invalid OTP code", user);
+    auth_otp_auth_code = PR_AUTH_BADPWD;
+    return -1;
+  }
+
+  switch (auth_otp_algo) {
+    case AUTH_OTP_ALGO_TOTP_SHA1:
+    case AUTH_OTP_ALGO_TOTP_SHA256:
+    case AUTH_OTP_ALGO_TOTP_SHA512: {
+      counter = (unsigned long) time(NULL);
+      break;
+    }
+
+    case AUTH_OTP_ALGO_HOTP:
+      counter_ptr = &counter;
+      break;
+
+    default:
+      pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "unsupported AuthOTPAlgorithm configured");
+      return 0;
+  }
+
+  res = auth_otp_db_rlock(dbh);
+  if (res < 0) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "failed to read-lock AuthOTPTable: %s", strerror(errno));
+  }
+
+  res = auth_otp_db_get_user_info(p, dbh, user, &secret, &secret_len,
+    counter_ptr);
+  xerrno = errno;
+
+  if (auth_otp_db_unlock(dbh) < 0) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "failed to unlock AuthOTPTable: %s", strerror(errno));
+  }
+
+  if (res < 0) {
+    if (xerrno == ENOENT) {
+      pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "user '%s' has no OTP info in AuthOTPTable", user);
+
+    } else {
+      pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "unable to retrieve OTP info for user '%s': %s", user,
+        strerror(xerrno));
+    }
+
+    /* If there was no entry found in the table (errno = ENOENT), should we
+     * returned ERROR or DECLINED?
+     *
+     * If we are not authoritative, then we returned DECLINED, regardless of
+     * the errno value, in order to allow other modules a chance at handling
+     * the authentication.  This module can only be "authoritative" about
+     * OTP codes it can generate, and if there is no entry in the table,
+     * REQUIRE_TABLE_ENTRY option or not, we cannot generate a code for
+     * comparisons.
+     *
+     * If we ARE authoritative, and the REQUIRE_TABLE_ENTRY option is in
+     * effect, then we return ERROR -- this is how we require OTP codes for
+     * ALL users.  Otherwise we return DECLINED, despite being authoritative,
+     * because again, we don't have the necessary data for computing the code.
+     */
+
+    if (authoritative) {
+      if (auth_otp_opts & AUTH_OTP_OPT_REQUIRE_TABLE_ENTRY) {
+        (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+          "FAILED: user '%s' does not have entry in OTP tables", user);
+        auth_otp_auth_code = PR_AUTH_BADPWD;
+        return -1;
+      }
+    }
+
+    return 0;
+  }
+
+  res = check_otp_code(p, user, user_otp, secret, secret_len, counter);
+  if (res == 0) {
+    pr_memscrub((char *) secret, secret_len);
+    
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "SUCCESS: user '%s' provided valid OTP code", user);
+
+    /* XXX Update state with details about the expected OTP found,
+     * e.g. for clock drift.
+     */
+    update_otp_counter(p, user, counter + 1);
+    return 1;
+  }
+
+  /* We SHOULD be allowing for clock skew/counter drift here.  We
+   * currently check one window ahead AND behind; is this policy too lenient?
+   * RFC 6238, Section 5.2 recommends one window for network transmission
+   * delay (which assumes that the client's OTP is always "behind" the
+   * server's OTP).
+   *
+   * By checking one window ahead/behind, we allow for clock skew in either
+   * direction: server ahead of client (most likely), client ahead of server.
+   */
+  pr_trace_msg(trace_channel, 3,
+    "current counter check failed, checking one window behind");
+ 
+  switch (auth_otp_algo) {
+    case AUTH_OTP_ALGO_TOTP_SHA1:
+    case AUTH_OTP_ALGO_TOTP_SHA256:
+    case AUTH_OTP_ALGO_TOTP_SHA512:
+      next_counter = counter - AUTH_OTP_TOTP_TIMESTEP_SECS;
+      break;
+
+    case AUTH_OTP_ALGO_HOTP:
+      next_counter = counter - 1;
+      break;
+  }
+
+  res = check_otp_code(p, user, user_otp, secret, secret_len, next_counter);
+  if (res == 0) {
+    pr_memscrub((char *) secret, secret_len);
+
+    pr_trace_msg(trace_channel, 3,
+      "counter check SUCCEEDED for one counter window behind; client is "
+      "out-of-sync");
+ 
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "SUCCESS: user '%s' provided valid OTP code", user);
+
+    /* XXX Update state with details about the expected OTP found,
+     * e.g. for clock drift, event counter increment, etc.
+     *
+     * Note that here, the client is "behind".  Expected for TOTP, but not
+     * for HOTP.  Hmm.
+     */
+    update_otp_counter(p, user, counter + 1);
+    return 1;
+  }
+
+  pr_trace_msg(trace_channel, 3,
+    "counter one window ahead check failed, checking one window ahead");
+
+  switch (auth_otp_algo) {
+    case AUTH_OTP_ALGO_TOTP_SHA1:
+    case AUTH_OTP_ALGO_TOTP_SHA256:
+    case AUTH_OTP_ALGO_TOTP_SHA512:
+      next_counter = counter + AUTH_OTP_TOTP_TIMESTEP_SECS;
+      break;
+
+    case AUTH_OTP_ALGO_HOTP:
+      next_counter = counter + 1;
+      break;
+  }
+
+  res = check_otp_code(p, user, user_otp, secret, secret_len, next_counter);
+  if (res == 0) {
+    pr_memscrub((char *) secret, secret_len);
+
+    pr_trace_msg(trace_channel, 3,
+      "counter check SUCCEEDED for one counter window ahead; client is "
+      "out-of-sync");
+ 
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "SUCCESS: user '%s' provided valid OTP code", user);
+
+    /* XXX Update state with details about the expected OTP found,
+     * e.g. for clock drift, event counter increment, etc.
+     *
+     * Note that here, the client is "ahead".  NOT expected for TOTP, but is
+     * for HOTP.  Hmm.
+     */
+    update_otp_counter(p, user, counter + 1);
+    return 1;
+  }
+
+  pr_memscrub((char *) secret, secret_len);
+
+  if (authoritative) {
+    (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "FAILED: user '%s' provided invalid OTP code", user);
+    auth_otp_auth_code = PR_AUTH_BADPWD;
+    return -1;
+  }
+
+  return 0;
+}
+
+/* Authentication handlers
+ */
+
+MODRET auth_otp_auth(cmd_rec *cmd) {
+  int authoritative = FALSE, res = 0;
+  char *user = NULL, *user_otp = NULL;
+
+  if (auth_otp_engine == FALSE ||
+      dbh == NULL) {
+    return PR_DECLINED(cmd);
+  }
+
+  user = cmd->argv[0];
+  user_otp = cmd->argv[1];
+
+  /* Figure out our default return style: whether or not we should allow
+   * other auth modules a shot at this user or not is controlled by adding
+   * '*' to a module name in the AuthOrder directive.  By default, auth
+   * modules are not authoritative, and allow other auth modules a chance at
+   * authenticating the user.  This is not the most secure configuration, but
+   * it allows things like AuthUserFile to work "out of the box".
+   */
+  if (auth_otp_authtab[0].auth_flags & PR_AUTH_FL_REQUIRED) {
+    authoritative = TRUE;
+  }
+
+#if defined(HAVE_SFTP)
+  if (auth_otp_using_sftp) {
+    const char *proto = NULL;
+
+    proto = pr_session_get_protocol(0);
+    if (strcmp(proto, "ssh2") == 0) {
+      /* We should already have done the keyboard-interactive challenge by
+       * this point in the session.
+       */
+
+      if (auth_otp_auth_code != PR_AUTH_OK &&
+          auth_otp_auth_code != PR_AUTH_RFC2228_OK) {
+        if (authoritative) {
+          /* Indicate ERROR. */
+          res = -1;
+
+        } else {
+          /* Indicate DECLINED. */
+          res = 0;
+        }
+
+      } else {
+        /* Indicate HANDLED. */
+        res = 1;
+      }
+
+    } else {
+      res = handle_user_otp(cmd->tmp_pool, user, user_otp, authoritative);
+    }
+
+  } else {
+    res = handle_user_otp(cmd->tmp_pool, user, user_otp, authoritative);
+  }
+#else
+  res = handle_user_otp(cmd->tmp_pool, user, user_otp, authoritative);
+#endif /* HAVE_SFTP */
+
+  if (res == 1) {
+    session.auth_mech = "mod_auth_otp.c";
+    return PR_HANDLED(cmd);
+
+  } else if (res < 0) {
+    return PR_ERROR_INT(cmd, auth_otp_auth_code);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET auth_otp_chkpass(cmd_rec *cmd) {
+  const char *real_otp, *user, *user_otp;
+
+  if (auth_otp_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  real_otp = cmd->argv[0];
+  user = cmd->argv[1];
+  user_otp = cmd->argv[2];
+
+  if (strcmp(real_otp, user_otp) == 0) {
+    return PR_HANDLED(cmd);
+  }
+
+  switch (auth_otp_algo) {
+    case AUTH_OTP_ALGO_TOTP_SHA1:
+      pr_trace_msg(trace_channel, 9,
+        "expected TOTP-SHA1 '%s', got '%s' for user '%s'", real_otp, user_otp,
+        user);
+      break;
+
+    case AUTH_OTP_ALGO_TOTP_SHA256:
+      pr_trace_msg(trace_channel, 9,
+        "expected TOTP-SHA256 '%s', got '%s' for user '%s'", real_otp, user_otp,
+        user);
+      break;
+
+    case AUTH_OTP_ALGO_TOTP_SHA512:
+      pr_trace_msg(trace_channel, 9,
+        "expected TOTP-SHA512 '%s', got '%s' for user '%s'", real_otp, user_otp,
+        user);
+      break;
+
+    case AUTH_OTP_ALGO_HOTP:
+      pr_trace_msg(trace_channel, 9,
+        "expected HOTP '%s', got '%s' for user '%s'", real_otp, user_otp, user);
+      break;
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+/* Configuration handlers
+ */
+
+/* usage: AuthOTPAlgorithm algo */
+MODRET set_authotpalgo(cmd_rec *cmd) {
+  int algo = -1;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  if (strcasecmp(cmd->argv[1], "hotp") == 0) {
+    algo = AUTH_OTP_ALGO_HOTP;
+
+  } else if (strcasecmp(cmd->argv[1], "totp") == 0 ||
+             strcasecmp(cmd->argv[1], "totp-sha1") == 0) {
+    algo = AUTH_OTP_ALGO_TOTP_SHA1;
+
+#ifdef HAVE_SHA256_OPENSSL
+  } else if (strcasecmp(cmd->argv[1], "totp-sha256") == 0) {
+    algo = AUTH_OTP_ALGO_TOTP_SHA256;
+#endif /* SHA256 OpenSSL support */
+
+#ifdef HAVE_SHA512_OPENSSL
+  } else if (strcasecmp(cmd->argv[1], "totp-sha512") == 0) {
+    algo = AUTH_OTP_ALGO_TOTP_SHA512;
+#endif /* SHA512 OpenSSL support */
+
+  } else {
+    CONF_ERROR(cmd, "expected supported OTP algorithm");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = algo;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AuthOTPEngine on|off */
+MODRET set_authotpengine(cmd_rec *cmd) {
+  int engine = -1;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  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(int));
+  *((int *) c->argv[0]) = engine;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AuthOTPLog path|"none" */
+MODRET set_authotplog(cmd_rec *cmd) {
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AuthOTPOptions opt1 opt2 ... */
+MODRET set_authotpoptions(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "StandardResponse") == 0) {
+      opts |= AUTH_OTP_OPT_STANDARD_RESPONSE;
+
+    } else if (strcmp(cmd->argv[i], "RequireTableEntry") == 0) {
+      opts |= AUTH_OTP_OPT_REQUIRE_TABLE_ENTRY;
+
+    } else if (strcmp(cmd->argv[i], "DisplayVerificationCode") == 0) {
+      opts |= AUTH_OTP_OPT_DISPLAY_VERIFICATION_CODE;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown AuthOTPOption: '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AuthOTPTable sql:/... */
+MODRET set_authotptable(cmd_rec *cmd) {
+  char *ptr = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  /* Separate the parameter into the separate pieces.  The parameter is
+   * given as one string to enhance its similarity to URL syntax.
+   */
+  ptr = strchr(cmd->argv[1], ':');
+  if (ptr == NULL) {
+    CONF_ERROR(cmd, "badly formatted parameter");
+  }
+
+  if (strncasecmp(cmd->argv[1], "sql:/", 5) != 0) {
+    CONF_ERROR(cmd, "badly formatted parameter");
+  }
+
+  *ptr++ = '\0';
+
+  add_config_param_str(cmd->argv[0], 1, ptr);
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AuthOTPTableLock path */
+MODRET set_authotptablelock(cmd_rec *cmd) {
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  return PR_HANDLED(cmd);
+}
+
+/* Command handlers
+ */
+
+MODRET auth_otp_post_pass(cmd_rec *cmd) {
+  if (auth_otp_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (dbh != NULL) {
+    if (auth_otp_db_close(dbh) < 0) {
+      (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+        "error closing AuthOTPTable: %s", strerror(errno));
+    }
+
+    dbh = NULL;
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET auth_otp_pre_user(cmd_rec *cmd) {
+  const char *tabinfo;
+  int xerrno;
+
+  if (auth_otp_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  tabinfo = auth_otp_db_config->argv[0];
+
+  PRIVS_ROOT
+  dbh = auth_otp_db_open(auth_otp_pool, tabinfo);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (dbh == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "unable to open AuthOTPTable: %s", strerror(xerrno));
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET auth_otp_post_user(cmd_rec *cmd) {
+  const char *user;
+
+  if (auth_otp_engine == FALSE ||
+      dbh == NULL) {
+    return PR_DECLINED(cmd);
+  }
+
+  user = cmd->argv[1];
+
+  /* We want to respond using the normal 331 response code, BUT we'd like
+   * to change the response message.  Note that this might be considered
+   * an information leak, i.e. we're leaking the server's expectation of
+   * OTPs from the connecting client.
+   */
+
+  if (!(auth_otp_opts & AUTH_OTP_OPT_STANDARD_RESPONSE)) {
+    pr_response_clear(&resp_list);
+#if defined(HAVE_SFTP)
+    /* Note: for some reason, when building with mod_sftp, the '_' function
+     * used for localization is not resolvable by the linker, thus we
+     * work around the problem.  For now.
+     */
+    pr_response_add(R_331, "One-time password required for %s", user);
+#else
+    pr_response_add(R_331, _("One-time password required for %s"), user);
+#endif /* HAVE_SFTP */
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+/* Event listeners
+ */
+
+static void auth_otp_exit_ev(const void *event_data, void *user_data) {
+  if (dbh != NULL) {
+    (void) auth_otp_db_close(dbh);
+    dbh = NULL;
+  }
+}
+
+#if defined(PR_SHARED_MODULE)
+static void auth_otp_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_auth_otp.c", (const char *) event_data) == 0) {
+# if defined(HAVE_SFTP)
+    if (pr_module_exists("mod_sftp.c") == TRUE) {
+      sftp_kbdint_unregister_driver("auth_otp");
+    }
+# endif /* HAVE_SFTP */
+    pr_event_unregister(&auth_otp_module, NULL, NULL);
+  }
+}
+#endif /* PR_SHARED_MODULE */
+
+static void auth_otp_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&auth_otp_module, "core.exit", auth_otp_exit_ev);
+  pr_event_unregister(&auth_otp_module, "core.session-reinit",
+    auth_otp_sess_reinit_ev);
+
+  auth_otp_engine = FALSE;
+  auth_otp_opts = 0UL;
+  auth_otp_algo = AUTH_OTP_ALGO_TOTP_SHA1;
+  auth_otp_db_config = NULL;
+
+  if (auth_otp_logfd >= 0) {
+    (void) close(auth_otp_logfd);
+    auth_otp_logfd = -1;
+  }
+
+#if defined(HAVE_SFTP)
+  auth_otp_using_sftp = FALSE;
+  (void) sftp_kbdint_register_driver("auth_otp", &auth_otp_kbdint_driver);
+#endif /* HAVE_SFTP */
+
+  if (auth_otp_pool != NULL) {
+    destroy_pool(auth_otp_pool);
+  }
+
+  res = auth_otp_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&auth_otp_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
+/* Initialization routines
+ */
+
+static int auth_otp_init(void) {
+
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&auth_otp_module, "core.module-unload",
+    auth_otp_mod_unload_ev, NULL);
+#endif /* PR_SHARED_MODULE */
+
+  if (pr_module_exists("mod_sql.c") == FALSE) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_OTP_VERSION
+      ": Missing required 'mod_sql.c'; HOTP/TOTP logins will FAIL");
+  }
+
+#if defined(HAVE_SFTP)
+  auth_otp_using_sftp = pr_module_exists("mod_sftp.c");
+  if (auth_otp_using_sftp) {
+    /* Prepare our keyboard-interactive driver. */
+    memset(&auth_otp_kbdint_driver, 0, sizeof(auth_otp_kbdint_driver));
+    auth_otp_kbdint_driver.open = auth_otp_kbdint_open;
+    auth_otp_kbdint_driver.authenticate = auth_otp_kbdint_authenticate;
+    auth_otp_kbdint_driver.close = auth_otp_kbdint_close;
+
+    if (sftp_kbdint_register_driver("auth_otp", &auth_otp_kbdint_driver) < 0) {
+      int xerrno = errno;
+
+      pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_OTP_VERSION
+        ": notice: error registering 'keyboard-interactive' driver: %s",
+        strerror(xerrno));
+
+      errno = xerrno;
+      return -1;
+    }
+
+  } else {
+    pr_log_debug(DEBUG1, MOD_AUTH_OTP_VERSION
+      ": mod_sftp not loaded, skipping keyboard-interactive support");
+  }
+#endif /* HAVE_SFTP */
+
+  return 0;
+}
+
+static int auth_otp_sess_init(void) {
+  config_rec *c;
+
+  pr_event_register(&auth_otp_module, "core.session-reinit",
+    auth_otp_sess_reinit_ev, NULL);
+
+  if (pr_auth_add_auth_only_module("mod_auth_otp.c") < 0 &&
+      errno != EEXIST) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_OTP_VERSION
+      ": unable to add 'mod_auth_otp.c' as an auth-only module: %s",
+      strerror(errno));
+
+    errno = EPERM;
+    return -1;
+  }
+
+  /* XXX Can we handle both FTP and SSH2 connections in the same module? */
+
+  c = find_config(main_server->conf, CONF_PARAM, "AuthOTPEngine", FALSE);
+  if (c != NULL) {
+    auth_otp_engine = *((int *) c->argv[0]);
+  }
+
+  if (auth_otp_engine == FALSE) {
+#if defined(HAVE_SFTP)
+    if (auth_otp_using_sftp) {
+      sftp_kbdint_unregister_driver("auth_otp");
+    }
+#endif /* HAVE_SFTP */
+    return 0;
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "AuthOTPLog", FALSE);
+  if (c != NULL) {
+    char *path;
+
+    path = c->argv[0];
+    if (strncasecmp(path, "none", 5) != 0) {
+      int res, xerrno;
+
+      pr_signals_block();
+      PRIVS_ROOT
+      res = pr_log_openfile(path, &auth_otp_logfd, 0600); 
+      xerrno = errno;
+      PRIVS_RELINQUISH
+      pr_signals_unblock();
+
+      if (res < 0) {
+        if (res == -1) {
+          pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_OTP_VERSION
+            ": notice: unable to open AuthOTPLog '%s': %s", path,
+            strerror(xerrno));
+
+        } else if (res == PR_LOG_WRITABLE_DIR) {
+          pr_log_pri(PR_LOG_WARNING, MOD_AUTH_OTP_VERSION
+            ": notice: unable to open AuthOTPLog '%s': parent directory is "
+            "world-writable", path);
+
+        } else if (res == PR_LOG_SYMLINK) {
+          pr_log_pri(PR_LOG_WARNING, MOD_AUTH_OTP_VERSION
+            ": notice: unable to open AuthOTPLog '%s': cannot log to a symlink",
+            path);
+        }
+      }
+    }
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "AuthOTPTable", FALSE);
+  if (c == NULL) {
+    pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
+      "missing required AuthOTPTable directive, disabling module");
+    pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_OTP_VERSION
+      ": missing required AuthOTPTable directive, disabling module");
+    auth_otp_engine = FALSE;
+    (void) close(auth_otp_logfd);
+    auth_otp_logfd = -1;
+
+#if defined(HAVE_SFTP)
+    if (auth_otp_using_sftp) {
+      sftp_kbdint_unregister_driver("auth_otp");
+    }
+#endif /* HAVE_SFTP */
+
+    return 0;
+  }
+  auth_otp_db_config = c;
+
+  auth_otp_pool = make_sub_pool(session.pool);
+  pr_pool_tag(auth_otp_pool, MOD_AUTH_OTP_VERSION);
+
+  c = find_config(main_server->conf, CONF_PARAM, "AuthOTPAlgorithm", FALSE);
+  if (c != NULL) {
+    auth_otp_algo = *((int *) c->argv[0]);
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "AuthOTPOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    auth_otp_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "AuthOTPOptions", FALSE);
+  }
+
+  pr_event_register(&auth_otp_module, "core.exit", auth_otp_exit_ev, NULL);
+  return 0;
+}
+
+static cmdtable auth_otp_cmdtab[] = {
+  { PRE_CMD,		C_USER, G_NONE,	auth_otp_pre_user,	FALSE, FALSE },
+  { POST_CMD,		C_USER, G_NONE,	auth_otp_post_user,	FALSE, FALSE },
+  { POST_CMD,		C_PASS, G_NONE,	auth_otp_post_pass,	FALSE, FALSE },
+  { POST_CMD_ERR,	C_PASS, G_NONE,	auth_otp_post_pass,	FALSE, FALSE },
+  { 0, NULL },
+};
+
+static authtable auth_otp_authtab[] = {
+  { 0, "auth",	auth_otp_auth },
+  { 0, "check",	auth_otp_chkpass },
+  { 0, NULL, NULL }
+};
+
+static conftable auth_otp_conftab[] = {
+  { "AuthOTPAlgorithm",		set_authotpalgo,		NULL },
+  { "AuthOTPEngine",		set_authotpengine,		NULL },
+  { "AuthOTPLog",		set_authotplog,			NULL },
+  { "AuthOTPOptions",		set_authotpoptions,		NULL },
+  { "AuthOTPTable",		set_authotptable,		NULL },
+  { "AuthOTPTableLock",		set_authotptablelock,		NULL },
+  { NULL, NULL, NULL }
+};
+
+module auth_otp_module = {
+  NULL, NULL,
+
+  /* Module API version */
+  0x20,
+
+  /* Module name */
+  "auth_otp",
+
+  /* Module configuration handler table */
+  auth_otp_conftab,
+
+  /* Module command handler table */
+  auth_otp_cmdtab,
+
+  /* Module authentication handler table */
+  auth_otp_authtab,
+
+  /* Module initialization */
+  auth_otp_init,
+
+  /* Session initialization */
+  auth_otp_sess_init,
+
+  /* Module version */
+  MOD_AUTH_OTP_VERSION
+};
diff --git a/contrib/mod_snmp/mod_snmp.h.in b/contrib/mod_auth_otp/mod_auth_otp.h.in
similarity index 50%
copy from contrib/mod_snmp/mod_snmp.h.in
copy to contrib/mod_auth_otp/mod_auth_otp.h.in
index c9b76d1..453e783 100644
--- a/contrib/mod_snmp/mod_snmp.h.in
+++ b/contrib/mod_auth_otp/mod_auth_otp.h.in
@@ -1,6 +1,6 @@
 /*
- * ProFTPD - mod_snmp
- * Copyright (c) 2008-2012 TJ Saunders
+ * ProFTPD - mod_auth_otp
+ * Copyright (c) 2015-2017 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
@@ -20,60 +20,52 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_snmp.h.in,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
-#ifndef MOD_SNMP_H
-#define MOD_SNMP_H
+#ifndef MOD_AUTH_OTP_H
+#define MOD_AUTH_OTP_H
 
 #include "conf.h"
 #include "privs.h"
 
-/* Define if you have the <sys/sysctl.h> header.  */
-#undef HAVE_SYS_SYSCTL_H
-
-/* Define if you have the <sys/sysinfo.h> header.  */
-#undef HAVE_SYS_SYSINFO_H
-
-/* Define if you have the random(3) function.  */
-#undef HAVE_RANDOM
+/* Define if you have OpenSSL with SHA256 support. */
+#undef HAVE_SHA256_OPENSSL
 
-/* Define if you have the sysctl(3) function.  */
-#undef HAVE_SYSCTL
+/* Define if you have OpenSSL with SHA512 support. */
+#undef HAVE_SHA512_OPENSSL
 
-/* Define if you have the sysinfo(2) function.  */
-#undef HAVE_SYSINFO
+/* Define if you have mod_sftp support. */
+#undef HAVE_SFTP
 
-#include <signal.h>
-
-#if HAVE_SYS_MMAN_H
-# include <sys/mman.h>
-#endif
-
-#if HAVE_SYS_UIO_H
-# include <sys/uio.h>
-#endif
-
-#define MOD_SNMP_VERSION	"mod_snmp/0.2"
+#define MOD_AUTH_OTP_VERSION	"mod_auth_otp/0.2"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030403
-# error "ProFTPD 1.3.4rc3 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030402
+# error "ProFTPD 1.3.4rc2 or later required"
 #endif
 
-/* RFC1157, Section 4 defines SNMPv1 as having a version value of 0. */
-#define SNMP_PROTOCOL_VERSION_1		0
+#include <openssl/conf.h>
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+#include <openssl/err.h>
+#include <openssl/rand.h>
 
-/* RFC1901 defines SNMPv2 as having a version value of 1. */
-#define SNMP_PROTOCOL_VERSION_2		1
+/* Define if you have the LibreSSL library.  */
+#if defined(LIBRESSL_VERSION_NUMBER)
+# define HAVE_LIBRESSL	1
+#endif
 
-#define SNMP_PROTOCOL_VERSION_3		3
+/* mod_auth_otp option flags */
 
 /* Miscellaneous */
-extern int snmp_logfd;
-extern pool *snmp_pool;
-extern struct timeval snmp_start_tv;
-extern int snmp_proto_udp;
+extern int auth_otp_logfd;
+extern pool *auth_otp_pool;
+extern unsigned long auth_otp_opts;
+
+/* Supported OTP algorithms */
+#define AUTH_OTP_ALGO_HOTP		1
+#define AUTH_OTP_ALGO_TOTP_SHA1		2
+#define AUTH_OTP_ALGO_TOTP_SHA256	3
+#define AUTH_OTP_ALGO_TOTP_SHA512	4
 
 #endif
diff --git a/contrib/mod_auth_otp/otp.c b/contrib/mod_auth_otp/otp.c
new file mode 100644
index 0000000..dee2edc
--- /dev/null
+++ b/contrib/mod_auth_otp/otp.c
@@ -0,0 +1,134 @@
+/*
+ * ProFTPD - mod_auth_otp
+ * Copyright (c) 2015 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.
+ */
+
+#include "otp.h"
+#include "crypto.h"
+
+static const char *trace_channel = "auth_otp";
+
+static int otp(pool *p, const EVP_MD *md,
+    const unsigned char *key, size_t key_len,
+    unsigned long counter, unsigned int *code) {
+  register unsigned int i;
+  unsigned char hash[EVP_MAX_MD_SIZE], value[8];
+  size_t hash_len;
+  int offset = 0;
+  unsigned int truncated = 0;
+
+  /* RFC 4226 requires a big-endian ordering of the counter value.  While
+   * arranging that, encode the counter value into an unsigned char buffer
+   * for feeding into the HMAC function.
+   */
+  for (i = sizeof(value); i--; counter >>= 8) {
+    value[i] = counter;
+  }
+
+  hash_len = EVP_MAX_MD_SIZE;
+  if (auth_otp_hmac(md, key, key_len, value, sizeof(value), hash,
+      &hash_len) < 0) {
+    return -1;
+  }
+
+  pr_memscrub(value, sizeof(value));
+
+  offset = hash[hash_len-1] & 0x0f;
+
+  truncated = ((hash[offset+0] & 0x7f) << 24) |
+              ((hash[offset+1] & 0xff) << 16) |
+              ((hash[offset+2] & 0xff) << 8) |
+               (hash[offset+3] & 0xff);
+
+  pr_memscrub(hash, sizeof(hash));
+
+  truncated &= 0x7fffffff;
+
+  /* Note the 6 zeroes here; this determines the number of digits in the
+   * generated code. 
+   */
+  *code = truncated % 1000000;
+  return 0;
+}
+
+int auth_otp_hotp(pool *p, const unsigned char *key, size_t key_len,
+    unsigned long counter, unsigned int *code) {
+  const EVP_MD *md;
+  int res;
+
+  if (p == NULL ||
+      key == NULL ||
+      key_len == 0 ||
+      code == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* RFC 4226 (HOTP) uses HMAC-SHA1. */
+  md = EVP_sha1();
+
+  res = otp(p, md, key, key_len, counter, code);
+  return res;
+}
+
+int auth_otp_totp(pool *p, const unsigned char *key, size_t key_len,
+    unsigned long ts, unsigned int algo, unsigned int *code) {
+  const EVP_MD *md;
+  unsigned long counter;
+  int res;
+
+  if (p == NULL ||
+      key == NULL ||
+      key_len == 0 ||
+      code == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  switch (algo) {
+    case AUTH_OTP_ALGO_TOTP_SHA1:
+      md = EVP_sha1();
+      break;
+
+#ifdef HAVE_SHA256_OPENSSL
+    case AUTH_OTP_ALGO_TOTP_SHA256:
+      md = EVP_sha256();
+      break;
+#endif /* SHA256 OpenSSL support */
+
+#ifdef HAVE_SHA512_OPENSSL
+    case AUTH_OTP_ALGO_TOTP_SHA512:
+      md = EVP_sha512();
+      break;
+#endif /* SHA512 OpenSSL support */
+
+    default:
+      pr_trace_msg(trace_channel, 4,
+        "unsupported TOTP algorithm ID %u requested", algo);
+      errno = EINVAL;
+      return -1;
+  }
+
+  counter = ts / AUTH_OTP_TOTP_TIMESTEP_SECS;
+  res = otp(p, md, key, key_len, counter, code);
+  return res;
+}
diff --git a/contrib/mod_sftp/kbdint.h b/contrib/mod_auth_otp/otp.h
similarity index 56%
copy from contrib/mod_sftp/kbdint.h
copy to contrib/mod_auth_otp/otp.h
index 1d8a1b0..cd9578b 100644
--- a/contrib/mod_sftp/kbdint.h
+++ b/contrib/mod_auth_otp/otp.h
@@ -1,6 +1,6 @@
 /*
- * ProFTPD - mod_sftp keyboard-interactive API
- * Copyright (c) 2008-2011 TJ Saunders
+ * ProFTPD - mod_auth_otp
+ * Copyright (c) 2015 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
@@ -20,29 +20,26 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: kbdint.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
-#ifndef MOD_SFTP_KBDINT_H
-#define MOD_SFTP_KBDINT_H
+#ifndef MOD_AUTH_OTP_OTP_H
+#define MOD_AUTH_OTP_OTP_H
 
-/* Returns the registered driver by name, or NULL if no such driver has
- * been registered.
- */
-sftp_kbdint_driver_t *sftp_kbdint_get_driver(const char *);
+#include "mod_auth_otp.h"
 
-/* Returns the number of registered keyboard-interactive drivers. */
-unsigned int sftp_kbdint_have_drivers(void);
+/* Following the recommendation of RFC 6238, Section 5.2 */
+#define AUTH_OTP_TOTP_TIMESTEP_SECS		30
 
-/* Returns the first driver in the list. */
-sftp_kbdint_driver_t *sftp_kbdint_first_driver(void);
+/* Generate an OTP using the algorithm specified in RFC 4226 (HOTP). */
+int auth_otp_hotp(pool *p, const unsigned char *key, size_t key_len,
+  unsigned long counter, unsigned int *code);
 
-/* Returns the next driver in the list, or NULL if there are no remaining
- * drivers.
+/* Generate an OTP using the algorithm specified in RFC 6238 (TOTP).
+ *
+ * Note that RFC 6238 defines support using SHA1, SHA256, or SHA512;
+ * the algo argument here indicates which one to use.
  */
-sftp_kbdint_driver_t *sftp_kbdint_next_driver(void);
+int auth_otp_totp(pool *p, const unsigned char *key, size_t key_len,
+  unsigned long ts, unsigned int algo, unsigned int *code);
 
-#endif
+#endif /* MOD_AUTH_OTP_OTP_H */
diff --git a/contrib/mod_auth_otp/t/Makefile.in b/contrib/mod_auth_otp/t/Makefile.in
new file mode 100644
index 0000000..b30e25b
--- /dev/null
+++ b/contrib/mod_auth_otp/t/Makefile.in
@@ -0,0 +1,62 @@
+CC=@CC@
+ at SET_MAKE@
+
+top_builddir=../../..
+top_srcdir=../../..
+module_srcdir=..
+srcdir=@srcdir@
+VPATH=@srcdir@
+
+include $(top_srcdir)/Make.rules
+
+# Necessary redefinitions
+INCLUDES=-I. -I.. -I$(top_srcdir) -I$(top_srcdir)/include @INCLUDES@
+CPPFLAGS= $(ADDL_CPPFLAGS) -DHAVE_CONFIG_H $(DEFAULT_PATHS) $(PLATFORM) $(INCLUDES)
+LDFLAGS=-L$(top_srcdir)/lib @LIBDIRS@
+
+EXEEXT=@EXEEXT@
+
+TEST_API_DEPS=\
+  $(top_srcdir)/src/pool.o \
+  $(top_srcdir)/src/sets.o \
+  $(top_srcdir)/src/str.o \
+  $(top_srcdir)/src/support.o \
+  $(top_srcdir)/src/table.o \
+  $(top_srcdir)/src/netaddr.o \
+  $(top_srcdir)/src/event.o \
+  $(top_srcdir)/src/fsio.o \
+  $(top_srcdir)/src/log.o \
+  $(top_srcdir)/src/privs.o \
+  $(module_srcdir)/crypto.o \
+  $(module_srcdir)/base32.o \
+  $(module_srcdir)/otp.o
+
+TEST_API_LIBS=-lcheck
+
+TEST_API_OBJS=\
+  api/base32.o \
+  api/otp.o \
+  api/stubs.o \
+  api/tests.o
+
+
+all:
+	@echo "Running make from top level directory."
+	cd ../; $(MAKE) all
+
+dummy:
+
+Makefile: Makefile.in ../config.status
+	cd ../ && ./config.status
+
+api/.c.o:
+	$(CC) $(CPPFLAGS) $(CFLAGS) -c $<
+
+api-tests$(EXEEXT): $(TEST_API_OBJS) $(TEST_API_DEPS)
+	$(LIBTOOL) --mode=link --tag=CC $(CC) $(LDFLAGS) -o $@ $(TEST_API_DEPS) $(TEST_API_OBJS) $(LIBS) $(TEST_API_LIBS)
+	./$@
+
+check-api: dummy api-tests$(EXEEXT)
+
+clean:
+	$(LIBTOOL) --mode=clean $(RM) *.o api/*.o api-tests$(EXEEXT) api-tests.log
diff --git a/contrib/mod_auth_otp/t/api/base32.c b/contrib/mod_auth_otp/t/api/base32.c
new file mode 100644
index 0000000..dec5f6f
--- /dev/null
+++ b/contrib/mod_auth_otp/t/api/base32.c
@@ -0,0 +1,139 @@
+/*
+ * ProFTPD - mod_auth_otp testsuite
+ * Copyright (c) 2015-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Base32 API tests
+ */
+
+#include "tests.h"
+#include "base32.h"
+
+static pool *p = NULL;
+
+/* These values are taken from RFC 4458, Section 10.
+ *
+ * Note that this base32 implementation does NOT emit the padding characters,
+ * as an "optimization".
+ *
+ * The base32 encoded values are used for interoperability with e.g. Google
+ * Authenticator, for entering into the app via human interaction.  To
+ * reduce the friction, then, the padding characters are omitted.
+ */
+
+struct kat {
+  const char *raw;
+  const char *encoded;
+};
+
+static struct kat expected_codes[] = {
+  { "",       "" },
+  { "f",      "MY", },
+  { "fo",     "MZXQ" },
+  { "foo",    "MZXW6" },
+  { "foob",   "MZXW6YQ" },
+  { "foobar", "MZXW6YTBOI" }
+};
+static unsigned int expected_code_count = 6;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+}
+
+static void tear_down(void) {
+  if (p) {
+    destroy_pool(p);
+    p = NULL;
+    permanent_pool = NULL;
+  } 
+}
+
+START_TEST (base32_encode_test) {
+  register unsigned int i;
+  int res;
+
+  res = auth_otp_base32_encode(p, NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno %s (%d), got %s (%d)",
+    strerror(EINVAL), EINVAL, strerror(errno), errno);
+
+  for (i = 0; i < expected_code_count; i++) {
+    const unsigned char *raw, *encoded = NULL;
+    size_t raw_len, encoded_len = 0;
+
+    raw = (const unsigned char *) expected_codes[i].raw;
+    raw_len = strlen((char *) raw);
+
+    res = auth_otp_base32_encode(p, raw, raw_len, &encoded, &encoded_len);
+    fail_unless(res == 0, "Failed to base32 encode '%s': %s",
+      expected_codes[i].raw, strerror(errno));
+    fail_unless(strcmp((char *) encoded, expected_codes[i].encoded) == 0,
+      "Expected '%s' for value '%s', got '%s'", expected_codes[i].encoded,
+      expected_codes[i].raw, encoded);
+  }
+}
+END_TEST
+
+START_TEST (base32_decode_test) {
+  register unsigned int i;
+  int res;
+
+  res = auth_otp_base32_decode(p, NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno %s (%d), got %s (%d)",
+    strerror(EINVAL), EINVAL, strerror(errno), errno);
+
+  mark_point();
+
+  for (i = 0; i < expected_code_count; i++) {
+    const unsigned char *encoded, *raw = NULL;
+    size_t encoded_len, raw_len = 0;
+
+    encoded = (const unsigned char *) expected_codes[i].encoded;
+    encoded_len = strlen((char *) encoded);
+
+    res = auth_otp_base32_decode(p, encoded, encoded_len, &raw, &raw_len);
+    fail_unless(res == 0, "Failed to base32 decode '%s': %s",
+      expected_codes[i].encoded, strerror(errno));
+    fail_unless(strcmp((char *) raw, expected_codes[i].raw) == 0,
+      "Expected '%s' for value '%s', got '%s'", expected_codes[i].raw,
+      expected_codes[i].encoded, raw);
+  }
+}
+END_TEST
+
+Suite *tests_get_base32_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("base32");
+
+  testcase = tcase_create("base32");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+  tcase_add_test(testcase, base32_encode_test);
+  tcase_add_test(testcase, base32_decode_test);
+  suite_add_tcase(suite, testcase);
+
+  return suite;
+}
diff --git a/contrib/mod_auth_otp/t/api/otp.c b/contrib/mod_auth_otp/t/api/otp.c
new file mode 100644
index 0000000..7ac32bd
--- /dev/null
+++ b/contrib/mod_auth_otp/t/api/otp.c
@@ -0,0 +1,262 @@
+/*
+ * ProFTPD - mod_auth_otp testsuite
+ * Copyright (c) 2015 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* HOTP/TOTP API tests
+ */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+}
+
+static void tear_down(void) {
+  if (p) {
+    destroy_pool(p);
+    p = NULL;
+    permanent_pool = NULL;
+  } 
+}
+
+START_TEST (hotp_test) {
+  register unsigned int i;
+  int res;
+
+  /* These values are taken from RFC 4226, Appendix D. */  
+  const char *key = "12345678901234567890";
+  size_t key_len = strlen((char *) key);
+  struct kat {
+    unsigned long count;
+    unsigned int hotp;
+  };
+
+  struct kat expected_codes[] = {
+    { 0, 755224 },
+    { 1, 287082 },
+    { 2, 359152 },
+    { 3, 969429 },
+    { 4, 338314 },
+    { 5, 254676 },
+    { 6, 287922 },
+    { 7, 162583 },
+    { 8, 399871 },
+    { 9, 520489 }
+  };
+
+  res = auth_otp_hotp(p, (const unsigned char *) key, key_len, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno %s (%d), got %s (%d)",
+    strerror(EINVAL), EINVAL, strerror(errno), errno);
+
+  for (i = 0; i < 10; i++) {
+    unsigned int code;
+
+    res = auth_otp_hotp(p, (const unsigned char *) key, key_len,
+      expected_codes[i].count, &code);
+    fail_unless(res == 0, "Failed to generate HOTP for value %lu: %s",
+      expected_codes[i].count, strerror(errno));
+    fail_unless(code == expected_codes[i].hotp,
+      "Expected HOTP %u for value %lu, got %u", expected_codes[i].hotp,
+      expected_codes[i].count, code);
+  }
+}
+END_TEST
+
+START_TEST (totp_sha1_test) {
+  register unsigned int i;
+  int res;
+
+  /* These values are taken from RFC 6238, Appendix B. */  
+  const char *key = "12345678901234567890";
+  size_t key_len = strlen(key);
+  struct kat {
+    unsigned long count;
+    unsigned int totp;
+  };
+
+  /* Note: since we are generating 6 digit codes (for interoperability with
+   * e.g. Google Authenticator), not 8 as provided in the KAT in the RFC,
+   * these numbers are adjusted.
+   */
+  struct kat expected_codes[] = {
+    { 59,		  287082 },
+    { 1111111109,	   81804 },
+    { 1111111111,	   50471 },
+    { 1234567890,	    5924 },
+    { 2000000000,	  279037 },
+    { 20000000000,	  353130 }
+  };
+
+  res = auth_otp_totp(p, (const unsigned char *) key, key_len, 0, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno %s (%d), got %s (%d)",
+    strerror(EINVAL), EINVAL, strerror(errno), errno);
+
+  for (i = 0; i < 5; i++) {
+    unsigned int code;
+
+    res = auth_otp_totp(p, (const unsigned char *) key, key_len,
+      expected_codes[i].count, AUTH_OTP_ALGO_TOTP_SHA1, &code);
+    fail_unless(res == 0, "Failed to generate TOTP-SHA1 for value %lu: %s",
+      expected_codes[i].count, strerror(errno));
+    fail_unless(code == expected_codes[i].totp,
+      "Expected TOTP-SHA1 %u for value %lu, got %u", expected_codes[i].totp,
+      expected_codes[i].count, code);
+  }
+}
+END_TEST
+
+#ifdef HAVE_SHA256_OPENSSL
+START_TEST (totp_sha256_test) {
+  register unsigned int i;
+  int res;
+
+  /* These values are taken from RFC 6238, Appendix B.  Note that the key
+   * for SHA256 needs to be longer.
+   */  
+  const char *key = "12345678901234567890123456789012";
+  size_t key_len = strlen(key);
+  struct kat {
+    unsigned long count;
+    unsigned int totp;
+  };
+
+  /* Note: since we are generating 6 digit codes (for interoperability with
+   * e.g. Google Authenticator), not 8 as provided in the KAT in the RFC,
+   * these numbers are adjusted.
+   */
+  struct kat expected_codes[] = {
+    { 59,		  119246 },
+    { 1111111109,	   84774 },
+    { 1111111111,	   62674 },
+    { 1234567890,	  819424 },
+    { 2000000000,	  698825 },
+    { 20000000000,	  737706 }
+  };
+
+  res = auth_otp_totp(p, (const unsigned char *) key, key_len, 0, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno %s (%d), got %s (%d)",
+    strerror(EINVAL), EINVAL, strerror(errno), errno);
+
+  for (i = 0; i < 5; i++) {
+    unsigned int code;
+
+    res = auth_otp_totp(p, (const unsigned char *) key, key_len,
+      expected_codes[i].count, AUTH_OTP_ALGO_TOTP_SHA256, &code);
+    fail_unless(res == 0, "Failed to generate TOTP-SHA256 for value %lu: %s",
+      expected_codes[i].count, strerror(errno));
+    fail_unless(code == expected_codes[i].totp,
+      "Expected TOTP-SHA256 %u for value %lu, got %u", expected_codes[i].totp,
+      expected_codes[i].count, code);
+  }
+}
+END_TEST
+#endif /* SHA256 OpenSSL support */
+
+#ifdef HAVE_SHA512_OPENSSL
+START_TEST (totp_sha512_test) {
+  register unsigned int i;
+  int res;
+
+  /* These values are taken from RFC 6238, Appendix B.  Note that the key
+   * for SHA512 needs to be longer.
+   */  
+  const char *key = "1234567890123456789012345678901234567890123456789012345678901234";
+  size_t key_len = strlen(key);
+  struct kat {
+    unsigned long count;
+    unsigned int totp;
+  };
+
+  /* Note: since we are generating 6 digit codes (for interoperability with
+   * e.g. Google Authenticator), not 8 as provided in the KAT in the RFC,
+   * these numbers are adjusted.
+   */
+  struct kat expected_codes[] = {
+    { 59,		  693936 },
+    { 1111111109,	   91201 },
+    { 1111111111,	  943326 },
+    { 1234567890,	  441116 },
+    { 2000000000,	  618901 },
+    { 20000000000,	  863826 }
+  };
+
+  res = auth_otp_totp(p, (const unsigned char *) key, key_len, 0, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno %s (%d), got %s (%d)",
+    strerror(EINVAL), EINVAL, strerror(errno), errno);
+
+  for (i = 0; i < 5; i++) {
+    unsigned int code;
+
+    res = auth_otp_totp(p, (const unsigned char *) key, key_len,
+      expected_codes[i].count, AUTH_OTP_ALGO_TOTP_SHA512, &code);
+    fail_unless(res == 0, "Failed to generate TOTP-SHA512 for value %lu: %s",
+      expected_codes[i].count, strerror(errno));
+    fail_unless(code == expected_codes[i].totp,
+      "Expected TOTP-SHA512 %u for value %lu, got %u", expected_codes[i].totp,
+      expected_codes[i].count, code);
+  }
+}
+END_TEST
+#endif /* SHA512 OpenSSL support */
+
+Suite *tests_get_hotp_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("hotp");
+
+  testcase = tcase_create("hotp");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+  tcase_add_test(testcase, hotp_test);
+  suite_add_tcase(suite, testcase);
+
+  return suite;
+}
+
+Suite *tests_get_totp_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("totp");
+
+  testcase = tcase_create("totp");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+  tcase_add_test(testcase, totp_sha1_test);
+#ifdef HAVE_SHA256_OPENSSL
+  tcase_add_test(testcase, totp_sha256_test);
+#endif /* SHA256 OpenSSL support */
+#ifdef HAVE_SHA512_OPENSSL
+  tcase_add_test(testcase, totp_sha512_test);
+#endif /* SHA512 OpenSSL support */
+  suite_add_tcase(suite, testcase);
+
+  return suite;
+}
diff --git a/src/pidfile.c b/contrib/mod_auth_otp/t/api/stubs.c
similarity index 50%
copy from src/pidfile.c
copy to contrib/mod_auth_otp/t/api/stubs.c
index 9b8f33c..624547b 100644
--- a/src/pidfile.c
+++ b/contrib/mod_auth_otp/t/api/stubs.c
@@ -1,6 +1,6 @@
 /*
- * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2009 The ProFTPD Project team
+ * ProFTPD - mod_auth_otp API testsuite
+ * Copyright (c) 2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,43 +22,55 @@
  * OpenSSL in the source distribution.
  */
 
-/* Pidfile management
- * $Id: pidfile.c,v 1.5 2011-05-23 21:22:24 castaglia Exp $
- */
+#include "tests.h"
+
+/* Stubs */
+
+session_t session;
+server_rec *main_server = NULL;
+pool *auth_otp_pool = NULL;
+int auth_otp_logfd = -1;
+
+config_rec *find_config(xaset_t *set, int type, const char *name, int recurse) {
+  return NULL;
+}
+
+void *get_param_ptr(xaset_t *set, const char *name, int recurse) {
+  errno = ENOENT;
+  return NULL;
+}
 
-#include "conf.h"
-#include "privs.h"
+struct passwd *pr_auth_getpwnam(pool *p, const char *name) {
+  return getpwnam(name);
+}
 
-static const char *pidfile_path = PR_PID_FILE_PATH;
+void pr_alarms_block(void) {
+}
 
-void pr_pidfile_write(void) {
-  FILE *fh = NULL;
-  const char *path = NULL;
+void pr_alarms_unblock(void) {
+}
 
-  path = get_param_ptr(main_server->conf, "PidFile", FALSE);
-  if (path != NULL &&
-      *path) {
-    pidfile_path = pstrdup(permanent_pool, path);
+char *pr_env_get(pool *p, const char *key) {
+  errno = ENOSYS;
+  return NULL;
+}
 
-  } else {
-    path = pidfile_path;
-  }
+int pr_env_set(pool *p, const char *key, const char *value) {
+  return 0;
+}
 
-  PRIVS_ROOT
-  fh = fopen(path, "w");
-  PRIVS_RELINQUISH
+module *pr_module_get(const char *name) {
+  errno = ENOENT;
+  return NULL;
+}
 
-  if (fh == NULL) {
-    fprintf(stderr, "error opening PidFile '%s': %s\n", path, strerror(errno));
-    exit(1);
-  }
+void pr_signals_handle(void) {
+}
 
-  fprintf(fh, "%lu\n", (unsigned long) getpid());
-  if (fclose(fh) < 0) {
-    fprintf(stderr, "error writing PidFile '%s': %s\n", path, strerror(errno));
-  }
+int pr_trace_get_level(const char *channel) {
+  return 0;
 }
 
-int pr_pidfile_remove(void) {
-  return unlink(pidfile_path);
+int pr_trace_msg(const char *channel, int level, const char *fmt, ...) {
+  return 0;
 }
diff --git a/contrib/mod_auth_otp/t/api/tests.c b/contrib/mod_auth_otp/t/api/tests.c
new file mode 100644
index 0000000..0d4822f
--- /dev/null
+++ b/contrib/mod_auth_otp/t/api/tests.c
@@ -0,0 +1,121 @@
+/*
+ * ProFTPD - mod_auth_otp API testsuite
+ * Copyright (c) 2015 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+#include "tests.h"
+
+struct testsuite_info {
+  const char *name;
+  Suite *(*get_suite)(void);
+};
+
+static struct testsuite_info suites[] = {
+  { "base32", 		tests_get_base32_suite },
+  { "hotp", 		tests_get_hotp_suite },
+  { "totp", 		tests_get_totp_suite },
+
+  { NULL, NULL }
+};
+
+static Suite *tests_get_suite(const char *suite) { 
+  if (strcmp(suite, "base32") == 0) { 
+    return tests_get_base32_suite();
+
+  } else if (strcmp(suite, "hotp") == 0) { 
+    return tests_get_hotp_suite();
+
+  } else if (strcmp(suite, "totp") == 0) { 
+    return tests_get_totp_suite();
+  }
+
+  return NULL;
+}
+
+int main(int argc, char *argv[]) {
+  const char *log_file = "auth-otp-tests.log";
+  int nfailed = 0;
+  SRunner *runner = NULL;
+  char *requested = NULL;
+
+  runner = srunner_create(NULL);
+
+  /* XXX This log name should be set outside this code, e.g. via environment
+   * variable or command-line option.
+   */
+  srunner_set_log(runner, log_file);
+
+  requested = getenv("AUTH_OTP_TEST_SUITE");
+  if (requested) {
+    Suite *suite;
+
+    suite = tests_get_suite(requested);
+    if (suite) {
+      srunner_add_suite(runner, suite);
+
+    } else {
+      fprintf(stderr,
+        "No such test suite ('%s') requested via AUTH_OTP_TEST_SUITE\n",
+        requested);
+      return EXIT_FAILURE;
+    }
+
+  } else {
+    register unsigned int i;
+
+    for (i = 0; suites[i].name; i++) {
+      Suite *suite;
+
+      suite = (suites[i].get_suite)();
+      if (suite) {
+        srunner_add_suite(runner, suite);
+      }
+    }
+  }
+
+  requested = getenv("PR_TEST_NOFORK");
+  if (requested) {
+    srunner_set_fork_status(runner, CK_NOFORK);
+  }
+
+  srunner_run_all(runner, CK_NORMAL);
+
+  nfailed = srunner_ntests_failed(runner);
+
+  if (runner)
+    srunner_free(runner);
+
+  if (nfailed != 0) {
+    fprintf(stderr, "-------------------------------------------------\n");
+    fprintf(stderr, " FAILED %d %s\n\n", nfailed,
+      nfailed != 1 ? "tests" : "test");
+    fprintf(stderr, " Please send email to:\n\n");
+    fprintf(stderr, "   tj at castaglia.org\n\n");
+    fprintf(stderr, " containing the `%s' file (in the t/ directory)\n", log_file);
+    fprintf(stderr, " and the output from running `proftpd -V'\n");
+    fprintf(stderr, "-------------------------------------------------\n");
+
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff --git a/include/mkhome.h b/contrib/mod_auth_otp/t/api/tests.h
similarity index 58%
copy from include/mkhome.h
copy to contrib/mod_auth_otp/t/api/tests.h
index 145ce75..d67b8c1 100644
--- a/include/mkhome.h
+++ b/contrib/mod_auth_otp/t/api/tests.h
@@ -1,6 +1,6 @@
 /*
- * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * ProFTPD - mod_auth_otp API testsuite
+ * Copyright (c) 2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,20 +22,35 @@
  * OpenSSL in the source distribution.
  */
 
-/* Home-on-demand support
- * $Id: mkhome.h,v 1.3 2012-09-05 16:40:58 castaglia Exp $
+/* Testsuite management
  */
 
-#ifndef PR_MKHOME_H
-#define PR_MKHOME_H
+#ifndef MOD_AUTH_OTP_TESTS_H
+#define MOD_AUTH_OTP_TESTS_H
 
-int create_home(pool *, const char *, const char *, uid_t, gid_t);
+#include "mod_auth_otp.h"
+#include "otp.h"
 
-/* This flag indicates that root privs should NOT be used when creating
- * the parent directories for the home directory.  This flag is useful
- * mostly in cases where the home directory lies on a root-squashed
- * NFS share; using root privs will ultimately fail in such cases.
+#if 0
+#ifdef HAVE_CHECK_H
+# include <check.h>
+#else
+# error "Missing Check installation; necessary for mod_auth_otp testsuite"
+#endif
+#else
+# include <check.h>
+#endif
+
+Suite *tests_get_base32_suite(void);
+Suite *tests_get_hotp_suite(void);
+Suite *tests_get_totp_suite(void);
+
+/* Temporary hack/placement for this variable, until we get to testing
+ * the Signals API.
  */
-#define PR_MKHOME_FL_USE_USER_PRIVS	0x0001
+unsigned int recvd_signal_flags;
+
+extern pool *auth_otp_pool;
+extern int auth_otp_logfd;
 
-#endif /* PR_MKHOME_H */
+#endif /* MOD_AUTH_OTP_TESTS_H */
diff --git a/contrib/mod_ban.c b/contrib/mod_ban.c
index aab9278..c56e801 100644
--- a/contrib/mod_ban.c
+++ b/contrib/mod_ban.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_ban -- a module implementing ban lists using the Controls API
- *
- * Copyright (c) 2004-2014 TJ Saunders
+ * Copyright (c) 2004-2017 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
@@ -22,24 +21,24 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * This is mod_ban, contrib software for proftpd 1.2.x/1.3.x.
+ * This is mod_ban, contrib software for proftpd 1.3.x.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_ban.c,v 1.75 2014-04-30 17:33:33 castaglia Exp $
  */
 
 #include "conf.h"
 #include "privs.h"
 #include "mod_ctrls.h"
+#include "hanson-tpl.h"
+#include "json.h"
 
 #include <sys/ipc.h>
 #include <sys/shm.h>
 
-#define MOD_BAN_VERSION			"mod_ban/0.6.2"
+#define MOD_BAN_VERSION			"mod_ban/0.7"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030402
-# error "ProFTPD 1.3.4rc2 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 #ifndef PR_USE_CTRLS
@@ -136,6 +135,8 @@ struct ban_event_entry {
 #define BAN_EV_TYPE_TLS_HANDSHAKE		15
 #define BAN_EV_TYPE_ROOT_LOGIN			16
 #define BAN_EV_TYPE_USER_DEFINED		17
+#define BAN_EV_TYPE_BAD_PROTOCOL		18
+#define BAN_EV_TYPE_EMPTY_PASSWORD		19
 
 struct ban_event_list {
   struct ban_event_entry bel_entries[BAN_EVENT_LIST_MAXSZ + BAN_LIST_HEADROOMSZ];
@@ -148,6 +149,12 @@ struct ban_data {
   struct ban_event_list events;
 };
 
+/* Tracks whether we have already seen the client connect, so that we only
+ * generate the 'client-connect-rate' event once, even in the face of multiple
+ * HOST commands.
+ */
+static int ban_client_connected = FALSE;
+
 static struct ban_data *ban_lists = NULL;
 static int ban_engine = -1;
 
@@ -164,6 +171,8 @@ static char *ban_table = NULL;
 static pr_fh_t *ban_tabfh = NULL;
 static int ban_timerno = -1;
 
+static const char *trace_channel = "ban";
+
 /* Needed for implementing LoginRate rules; command handlers don't get an
  * arbitrary data pointer like event listeners do.
  */
@@ -172,7 +181,10 @@ static struct ban_event_entry *login_rate_tmpl = NULL;
 /* For communicating with memcached servers for shared/cached ban data. */
 static pr_memcache_t *mcache = NULL;
 
-struct ban_mcache_entry {
+/* For communicating with Redis servers for shared/cached ban data. */
+static pr_redis_t *redis = NULL;
+
+struct ban_cache_entry {
   int version;
 
   /* Timestamp indicating when this entry last changed.  Ideally it will
@@ -180,9 +192,9 @@ struct ban_mcache_entry {
    */
   uint32_t update_ts;
 
-  /* IP address/port of origin of this cache entry. */
+  /* IP address/port of origin/source server/vhost of this cache entry. */
   char *ip_addr;
-  int port;
+  unsigned int port;
 
   /* We could use a struct ban_entry here, except that it uses fixed-size
    * buffers for the strings, and for cache storage, dynamically allocated
@@ -199,20 +211,40 @@ struct ban_mcache_entry {
   int be_sid;
 };
 
-/* These are tpl format strings */
-#define BAN_MCACHE_KEY_FMT		"vs"
-#define BAN_MCACHE_VALUE_FMT		"S(ivsiisssvi)"
+#define BAN_CACHE_VALUE_VERSION	2
 
-#define BAN_MCACHE_VALUE_VERSION	1
+/* These are tpl format strings */
+#define BAN_CACHE_TPL_KEY_FMT		"vs"
+#define BAN_CACHE_TPL_VALUE_FMT		"S(iusiisssui)"
+
+/* These are the JSON format field names */
+#define BAN_CACHE_JSON_KEY_VERSION	"version"
+#define BAN_CACHE_JSON_KEY_UPDATE_TS	"update_ts"
+#define BAN_CACHE_JSON_KEY_IP_ADDR	"ip_addr"
+#define BAN_CACHE_JSON_KEY_PORT		"port"
+#define BAN_CACHE_JSON_KEY_TYPE		"ban_type"
+#define BAN_CACHE_JSON_KEY_NAME		"ban_name"
+#define BAN_CACHE_JSON_KEY_REASON	"ban_reason"
+#define BAN_CACHE_JSON_KEY_MESSAGE	"ban_message"
+#define BAN_CACHE_JSON_KEY_EXPIRES_TS	"expires_ts"
+#define BAN_CACHE_JSON_KEY_SERVER_ID	"server_id"
+
+#define BAN_CACHE_JSON_TYPE_USER_TEXT	"user ban"
+#define BAN_CACHE_JSON_TYPE_HOST_TEXT	"host ban"
+#define BAN_CACHE_JSON_TYPE_CLASS_TEXT	"class ban"
 
 /* BanCacheOptions flags */
 static unsigned long ban_cache_opts = 0UL;
 #define BAN_CACHE_OPT_MATCH_SERVER	0x001
+#define BAN_CACHE_OPT_USE_JSON		0x002
 
 static int ban_lock_shm(int);
+static int ban_sess_init(void);
 
 static void ban_anonrejectpasswords_ev(const void *, void *);
+static void ban_badprotocol_ev(const void *, void *);
 static void ban_clientconnectrate_ev(const void *, void *);
+static void ban_emptypassword_ev(const void *, void *);
 static void ban_maxclientsperclass_ev(const void *, void *);
 static void ban_maxclientsperhost_ev(const void *, void *);
 static void ban_maxclientsperuser_ev(const void *, void *);
@@ -231,21 +263,17 @@ static void ban_userdefined_ev(const void *, void *);
 static void ban_handle_event(unsigned int, int, const char *,
   struct ban_event_entry *);
 
-/* Functions for marshalling key/value data to/from shared cache,
- * e.g. memcached.
+/* Functions for marshalling key/value data to/from Redis/Memchache shared
+ * cache.
  */
-
-static int ban_mcache_key_get(pool *p, unsigned int type, const char *name,
+static int ban_cache_get_tpl_key(pool *p, unsigned int type, const char *name,
     void **key, size_t *keysz) {
+  int res;
   void *data = NULL;
   size_t datasz = 0;
-  int res;
 
-  res = tpl_jot(TPL_MEM, &data, &datasz, BAN_MCACHE_KEY_FMT, &type, &name);
+  res = tpl_jot(TPL_MEM, &data, &datasz, BAN_CACHE_TPL_KEY_FMT, &type, &name);
   if (res < 0) {
-    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "error constructing cache lookup key for type %u, name %s", type,
-      name);
     return -1;
   }
 
@@ -257,48 +285,95 @@ static int ban_mcache_key_get(pool *p, unsigned int type, const char *name,
   return 0;
 }
 
-static int ban_mcache_entry_get(pool *p, unsigned int type, const char *name,
-    struct ban_mcache_entry *bme) {
-  tpl_node *tn;
+static int ban_cache_get_json_key(pool *p, unsigned int type, const char *name,
+    void **key, size_t *keysz) {
+  pr_json_object_t *json;
+  char *json_text;
+
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_number(p, json, "ban_type_id", (double) type);
+  (void) pr_json_object_set_string(p, json, "ban_name", name);
+
+  json_text = pr_json_object_to_text(p, json, "");
+
+  /* Include the terminating NUL in the key. */
+  *keysz = strlen(json_text) + 1;
+  *key = pstrndup(p, json_text, *keysz - 1);
+  (void) pr_json_object_free(json);
+
+  return 0;
+}
+
+static int ban_cache_get_key(pool *p, unsigned int type, const char *name,
+    void **key, size_t *keysz) {
   int res;
-  void *key = NULL, *value = NULL;
-  char *ptr = NULL;
-  size_t keysz = 0, valuesz = 0;
-  uint32_t flags = 0;
+  const char *key_type = "unknown";
 
-  res = ban_mcache_key_get(p, type, name, &key, &keysz);
-  if (res < 0)
-    return -1;
+  if (ban_cache_opts & BAN_CACHE_OPT_USE_JSON) {
+    key_type = "JSON";
+    res = ban_cache_get_json_key(p, type, name, key, keysz);
 
-  value = pr_memcache_kget(mcache, &ban_module, (const char *) key, keysz,
-    &valuesz, &flags);
-  if (value == NULL) {
+  } else {
+    key_type = "TPL";
+    res = ban_cache_get_tpl_key(p, type, name, key, keysz);
+  }
+
+  if (res < 0) {
     (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "no matching memcache entry found for name %s, type %u", name, type);
+      "error constructing cache %s lookup key for type %u, name %s", key_type,
+      type, name);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ban_cache_entry_delete(pool *p, unsigned int type,
+    const char *name) {
+  int res;
+  void *key = NULL;
+  size_t keysz = 0;
+
+  res = ban_cache_get_key(p, type, name, &key, &keysz);
+  if (res < 0) {
     return -1;
   }
 
-  /* Unmarshal the ban entry. */
+  if (redis != NULL) {
+    res = pr_redis_kremove(redis, &ban_module, key, keysz);
 
-  tn = tpl_map(BAN_MCACHE_VALUE_FMT, bme);
+  } else {
+    res = pr_memcache_kremove(mcache, &ban_module, key, keysz, 0);
+  }
+
+  return res;
+}
+
+static int ban_cache_entry_decode_tpl(pool *p, void *value, size_t valuesz,
+    struct ban_cache_entry *bce) {
+  int res;
+  tpl_node *tn;
+  char *ptr = NULL;
+
+  tn = tpl_map(BAN_CACHE_TPL_VALUE_FMT, bce);
   if (tn == NULL) {
     (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "error allocating tpl_map for format '%s'", BAN_MCACHE_VALUE_FMT);
+      "error allocating tpl_map for format '%s'", BAN_CACHE_TPL_VALUE_FMT);
     return -1;
   }
 
   res = tpl_load(tn, TPL_MEM, value, valuesz);
   if (res < 0) {
-    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "%s", "error loading marshalled ban memcache data");
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION, "%s",
+      "error loading TPL ban cache data");
     tpl_free(tn);
     return -1;
   }
 
   res = tpl_unpack(tn, 0);
   if (res < 0) {
-    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "%s", "error unpacking marshalled ban memcache data");
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION, "%s",
+      "error unpacking TPL ban cache data");
     tpl_free(tn);
     return -1;
   }
@@ -306,86 +381,407 @@ static int ban_mcache_entry_get(pool *p, unsigned int type, const char *name,
   tpl_free(tn);
 
   /* Now that we've called tpl_free(), we need to free up the memory
-   * associated with the strings in the struct ban_mcache_entry, so we
+   * associated with the strings in the struct ban_cache_entry, so we
    * allocate them out of the given pool.
    */
 
-  ptr = bme->ip_addr;
+  ptr = bce->ip_addr;
   if (ptr != NULL) {
-    bme->ip_addr = pstrdup(p, ptr);
+    bce->ip_addr = pstrdup(p, ptr);
     free(ptr);
   }
 
-  ptr = bme->be_name;
+  ptr = bce->be_name;
   if (ptr != NULL) {
-    bme->be_name = pstrdup(p, ptr);
+    bce->be_name = pstrdup(p, ptr);
     free(ptr);
   }
 
-  ptr = bme->be_reason;
+  ptr = bce->be_reason;
   if (ptr != NULL) {
-    bme->be_reason = pstrdup(p, ptr);
+    bce->be_reason = pstrdup(p, ptr);
     free(ptr);
   }
 
-  ptr = bme->be_mesg;
+  ptr = bce->be_mesg;
   if (ptr != NULL) {
-    bme->be_mesg = pstrdup(p, ptr);
+    bce->be_mesg = pstrdup(p, ptr);
     free(ptr);
   }
 
   return 0;
 }
 
-static int ban_mcache_entry_set(pool *p, struct ban_mcache_entry *bme) {
-  tpl_node *tn;
+static int entry_get_json_number(pool *p, pr_json_object_t *json,
+    const char *key, double *val, const char *text) {
+  if (pr_json_object_get_number(p, json, key, val) < 0) {
+    if (errno == EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+       "ignoring non-number '%s' JSON field in '%s'", key, text);
+
+    } else {
+      (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+        "missing required '%s' JSON field in '%s'", key, text);
+    }
+
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int entry_get_json_string(pool *p, pr_json_object_t *json,
+    const char *key, char **val, const char *text) {
+  if (pr_json_object_get_string(p, json, key, val) < 0) {
+    if (errno == EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+       "ignoring non-string '%s' JSON field in '%s'", key, text);
+
+    } else {
+      (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+        "missing required '%s' JSON field in '%s'", key, text);
+    }
+
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ban_cache_entry_decode_json(pool *p, void *value, size_t valuesz,
+    struct ban_cache_entry *bce) {
+  int res;
+  pr_json_object_t *json;
+  const char *key;
+  char *entry, *text;
+  double number;
+
+  entry = value;
+  if (pr_json_text_validate(p, entry) == FALSE) {
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+      "unable to decode invalid JSON cache entry: '%s'", entry);
+    errno = EINVAL;
+    return -1;
+  }
+
+  json = pr_json_object_from_text(p, entry);
+
+  key = BAN_CACHE_JSON_KEY_VERSION;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->version = (int) number;
+
+  if (bce->version != BAN_CACHE_VALUE_VERSION) {
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+      "unsupported/unknown version value '%d' in cached JSON value, rejecting",
+      bce->version);
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  key = BAN_CACHE_JSON_KEY_UPDATE_TS;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->update_ts = (uint32_t) number;
+
+  key = BAN_CACHE_JSON_KEY_IP_ADDR;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->ip_addr = text;
+
+  key = BAN_CACHE_JSON_KEY_PORT;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->port = (unsigned int) number;
+
+  if (bce->port == 0 ||
+      bce->port > 65535) {
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+      "invalid port number %u in cached JSON value, rejecting", bce->port);
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  key = BAN_CACHE_JSON_KEY_TYPE;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res < 0) {
+    return -1;
+  }
+
+  if (strcmp(text, BAN_CACHE_JSON_TYPE_USER_TEXT) == 0) {
+    bce->be_type = BAN_TYPE_USER;
+
+  } else if (strcmp(text, BAN_CACHE_JSON_TYPE_HOST_TEXT) == 0) {
+    bce->be_type = BAN_TYPE_HOST;
+
+  } else if (strcmp(text, BAN_CACHE_JSON_TYPE_CLASS_TEXT) == 0) {
+    bce->be_type = BAN_TYPE_CLASS;
+
+  } else {
+    pr_trace_msg(trace_channel, 3,
+      "ignoring unknown/unsupported '%s' JSON field value: %s", key, text);
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  key = BAN_CACHE_JSON_KEY_NAME;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->be_name = text;
+
+  key = BAN_CACHE_JSON_KEY_REASON;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->be_reason = text;
+
+  key = BAN_CACHE_JSON_KEY_MESSAGE;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->be_mesg = text;
+
+  key = BAN_CACHE_JSON_KEY_EXPIRES_TS;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->be_expires = (uint32_t) number;
+
+  key = BAN_CACHE_JSON_KEY_SERVER_ID;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  bce->be_sid = (int) number;
+
+  (void) pr_json_object_free(json);
+
+  if (bce->be_sid <= 0) {
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+      "invalid server ID %d in cached JSON value, rejecting", bce->be_sid);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ban_cache_entry_get(pool *p, unsigned int type, const char *name,
+    struct ban_cache_entry *bce) {
   int res;
   void *key = NULL, *value = NULL;
   size_t keysz = 0, valuesz = 0;
-  uint32_t flags = 0;
+  const char *driver = NULL;
+
+  res = ban_cache_get_key(p, type, name, &key, &keysz);
+  if (res < 0) {
+    return -1;
+  }
 
-  /* Marshal the ban entry. */
+  if (redis != NULL) {
+    driver = "Redis";
 
-  tn = tpl_map(BAN_MCACHE_VALUE_FMT, bme);
+    value = pr_redis_kget(p, redis, &ban_module, (const char *) key, keysz,
+      &valuesz);
+
+  } else {
+    uint32_t flags = 0;
+
+    driver = "memcache";
+    value = pr_memcache_kget(mcache, &ban_module, (const char *) key, keysz,
+      &valuesz, &flags);
+  }
+
+  if (value == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 8,
+      "no matching %s entry found for name %s, type %u", driver, name, type);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  /* Decode the cached ban entry. */
+  if (ban_cache_opts & BAN_CACHE_OPT_USE_JSON) {
+    res = ban_cache_entry_decode_json(p, value, valuesz, bce);
+
+  } else {
+    res = ban_cache_entry_decode_tpl(p, value, valuesz, bce);
+  }
+
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 9, "retrieved ban entry in cache using %s",
+      ban_cache_opts & BAN_CACHE_OPT_USE_JSON ? "JSON" : "TPL");
+  }
+
+  return res;
+}
+
+static int ban_cache_entry_encode_tpl(pool *p, void **value, size_t *valuesz,
+    struct ban_cache_entry *bce) {
+  int res;
+  tpl_node *tn;
+  void *ptr = NULL;
+
+  tn = tpl_map(BAN_CACHE_TPL_VALUE_FMT, bce);
   if (tn == NULL) {
     (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "error allocating tpl_map for format '%s'", BAN_MCACHE_VALUE_FMT);
+      "error allocating tpl_map for format '%s'", BAN_CACHE_TPL_VALUE_FMT);
     return -1;
   }
 
   res = tpl_pack(tn, 0);
   if (res < 0) {
-    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "%s", "error marshalling ban memcache data");
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION, "%s",
+      "error encoding TPL ban cache data");
     return -1;
   }
 
-  res = tpl_dump(tn, TPL_MEM, &value, &valuesz);
+  res = tpl_dump(tn, TPL_MEM, &ptr, valuesz);
   if (res < 0) {
-    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "%s", "error dumping marshalled ban memcache data");
+    (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION, "%s",
+      "error dumping TPL ban cache data");
     return -1;
   }
 
+  /* Duplicate the value using the given pool, so that we can free up the
+   * memory allocated by tpl_dump().
+   */
+  *value = palloc(p, *valuesz);
+  memcpy(*value, ptr, *valuesz);
+
   tpl_free(tn);
+  free(ptr);
+
+  return 0;
+}
+
+static int ban_cache_entry_encode_json(pool *p, void **value, size_t *valuesz,
+    struct ban_cache_entry *bce) {
+  pr_json_object_t *json;
+  const char *ban_type = "unknown";
+  char *json_text;
+
+  json = pr_json_object_alloc(p);
+
+  (void) pr_json_object_set_number(p, json, BAN_CACHE_JSON_KEY_VERSION,
+    (double) bce->version);
+  (void) pr_json_object_set_number(p, json, BAN_CACHE_JSON_KEY_UPDATE_TS,
+    (double) bce->update_ts);
+  (void) pr_json_object_set_string(p, json, BAN_CACHE_JSON_KEY_IP_ADDR,
+    bce->ip_addr);
+  (void) pr_json_object_set_number(p, json, BAN_CACHE_JSON_KEY_PORT,
+    (double) bce->port);
+
+  /* Textify the ban type, for better inoperability. */
+  switch (bce->be_type) {
+    case BAN_TYPE_USER:
+      ban_type = BAN_CACHE_JSON_TYPE_USER_TEXT;
+      break;
+
+    case BAN_TYPE_HOST:
+      ban_type = BAN_CACHE_JSON_TYPE_HOST_TEXT;
+      break;
+
+    case BAN_TYPE_CLASS:
+      ban_type = BAN_CACHE_JSON_TYPE_CLASS_TEXT;
+      break;
+  }
+
+  (void) pr_json_object_set_string(p, json, BAN_CACHE_JSON_KEY_TYPE,
+    ban_type);
+  (void) pr_json_object_set_string(p, json, BAN_CACHE_JSON_KEY_NAME,
+    bce->be_name);
+  (void) pr_json_object_set_string(p, json, BAN_CACHE_JSON_KEY_REASON,
+    bce->be_reason);
+  (void) pr_json_object_set_string(p, json, BAN_CACHE_JSON_KEY_MESSAGE,
+    bce->be_mesg);
+  (void) pr_json_object_set_number(p, json, BAN_CACHE_JSON_KEY_EXPIRES_TS,
+    (double) bce->be_expires);
+  (void) pr_json_object_set_number(p, json, BAN_CACHE_JSON_KEY_SERVER_ID,
+    (double) bce->be_sid);
+
+  json_text = pr_json_object_to_text(p, json, "");
+
+  /* Include the terminating NUL in the value. */
+  *valuesz = strlen(json_text) + 1;
+  *value = pstrndup(p, json_text, *valuesz - 1);
 
-  res = ban_mcache_key_get(p, bme->be_type, bme->be_name, &key, &keysz);
+  (void) pr_json_object_free(json);
+  return 0;
+}
+
+static int ban_cache_entry_set(pool *p, struct ban_cache_entry *bce) {
+  int res;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+  const char *driver = NULL;
+
+  /* Encode the ban entry. */
+  if (ban_cache_opts & BAN_CACHE_OPT_USE_JSON) {
+    res = ban_cache_entry_encode_json(p, &value, &valuesz, bce);
+
+  } else {
+    res = ban_cache_entry_encode_tpl(p, &value, &valuesz, bce);
+  }
+
+  if (res < 0) {
+    return -1;
+  }
+
+  res = ban_cache_get_key(p, bce->be_type, bce->be_name, &key, &keysz);
   if (res < 0) {
-    free(value);
     return -1;
   }
 
-  res = pr_memcache_kset(mcache, &ban_module, (const char *) key, keysz,
-    value, valuesz, bme->be_expires, flags);
-  free(value);
+  if (redis != NULL) {
+    driver = "Redis";
+
+    res = pr_redis_kset(redis, &ban_module, (const char *) key, keysz,
+      value, valuesz, bce->be_expires);
+
+  } else {
+    uint32_t flags = 0;
+
+    driver = "memcache";
+    res = pr_memcache_kset(mcache, &ban_module, (const char *) key, keysz,
+      value, valuesz, bce->be_expires, flags);
+  }
 
   if (res < 0) {
+    int xerrno = errno;
+
     (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-      "unable to add memcache entry for name %s, type %u: %s", bme->be_name,
-      bme->be_type, strerror(errno));
+      "unable to add %s entry for name %s, type %u: %s", driver, bce->be_name,
+      bce->be_type, strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
+  pr_trace_msg(trace_channel, 9, "stored ban entry in cache using %s",
+    ban_cache_opts & BAN_CACHE_OPT_USE_JSON ? "JSON" : "TPL");
   return 0;
 }
 
@@ -758,7 +1154,7 @@ static time_t ban_parse_timestr(const char *str) {
  * or, if there isn't a rule-specific message, the BanMessage to the client.
  */
 static void ban_send_mesg(pool *p, const char *user, const char *rule_mesg) {
-  char *mesg = NULL;
+  const char *mesg = NULL;
 
   if (rule_mesg) {
     mesg = pstrdup(p, rule_mesg);
@@ -767,7 +1163,7 @@ static void ban_send_mesg(pool *p, const char *user, const char *rule_mesg) {
     mesg = pstrdup(p, ban_mesg);
   }
 
-  if (mesg) {
+  if (mesg != NULL) {
     mesg = pstrdup(p, mesg);
 
     if (strstr(mesg, "%c")) {
@@ -784,8 +1180,9 @@ static void ban_send_mesg(pool *p, const char *user, const char *rule_mesg) {
       mesg = sreplace(p, mesg, "%a", remote_ip, NULL);
     }
 
-    if (strstr(mesg, "%u"))
+    if (strstr(mesg, "%u")) {
       mesg = sreplace(p, mesg, "%u", user, NULL);
+    }
 
     pr_response_send_async(R_530, "%s", mesg);
   }
@@ -879,33 +1276,33 @@ static int ban_list_add(pool *p, unsigned int type, unsigned int sid,
     }
   }
 
-  /* Add the entry to memcached, if configured AND if the caller provided a pool
+  /* Add the entry to cache, if configured AND if the caller provided a pool
    * for such uses.
    */
-  if (mcache != NULL &&
+  if ((mcache != NULL || redis != NULL) &&
       p != NULL) {
-    struct ban_mcache_entry bme;
-    pr_netaddr_t *na;
+    struct ban_cache_entry bce;
+    const pr_netaddr_t *na;
 
-    memset(&bme, 0, sizeof(bme));
+    memset(&bce, 0, sizeof(bce));
 
-    bme.version = BAN_MCACHE_VALUE_VERSION;
-    bme.update_ts = (uint32_t) time(NULL);
+    bce.version = BAN_CACHE_VALUE_VERSION;
+    bce.update_ts = (uint32_t) time(NULL);
 
     na = pr_netaddr_get_sess_local_addr();
-    bme.ip_addr = (char *) pr_netaddr_get_ipstr(na);
-    bme.port = pr_netaddr_get_port(na);
+    bce.ip_addr = (char *) pr_netaddr_get_ipstr(na);
+    bce.port = pr_netaddr_get_port(na);
 
-    bme.be_type = type;
-    bme.be_name = (char *) name;
-    bme.be_reason = (char *) reason;
-    bme.be_mesg = (char *) (rule_mesg ? rule_mesg : "");
-    bme.be_expires = (uint32_t) (lasts ? time(NULL) + lasts : 0);
-    bme.be_sid = main_server->sid;
+    bce.be_type = type;
+    bce.be_name = (char *) name;
+    bce.be_reason = (char *) reason;
+    bce.be_mesg = (char *) (rule_mesg ? rule_mesg : "");
+    bce.be_expires = (uint32_t) (lasts ? time(NULL) + lasts : 0);
+    bce.be_sid = main_server->sid;
 
-    if (ban_mcache_entry_set(p, &bme) == 0) {
+    if (ban_cache_entry_set(p, &bce) == 0) {
       (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-        "memcache entry added for name %s, type %u", name, type);
+        "cache entry added for name %s, type %u", name, type);
     }
   }
 
@@ -947,72 +1344,85 @@ static int ban_list_exists(pool *p, unsigned int type, unsigned int sid,
     }
   }
 
-  /* Check with memcached, if configured AND if the caller provided a pool
-   * for such uses.
+  /* Check with cache, if configured AND if the caller provided a pool for
+   * such uses.
    */
-  if (mcache != NULL &&
+  if ((mcache != NULL || redis != NULL) &&
       p != NULL) {
     int res;
-    struct ban_mcache_entry bme;
+    struct ban_cache_entry bce;
 
-    memset(&bme, 0, sizeof(bme));
+    memset(&bce, 0, sizeof(bce));
 
-    res = ban_mcache_entry_get(p, type, name, &bme);
+    res = ban_cache_entry_get(p, type, name, &bce);
     if (res == 0) {
       int use_entry = TRUE;
+      time_t now;
 
-      /* XXX Check the expiration timestamp; if too old, delete it from the
+      /* Check the expiration timestamp; if too old, delete it from the
        * cache.
        */
+      time(&now);
+      if (bce.be_expires != 0 &&
+          bce.be_expires <= (uint32_t) now) {
+        pr_trace_msg(trace_channel, 3,
+          "purging expired entry from cache: %lu <= now %lu",
+          (unsigned long) bce.be_expires, (unsigned long) now);
+
+        (void) ban_cache_entry_delete(p, type, name);
+        errno = ENOENT;
+        return -1;
+      }
 
       /* XXX Check the entry version; if it doesn't match ours, then we
        * need to Do Something Intelligent(tm).
        */
 
       (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-        "found memcache entry for name %s, type %u: version %u, update_ts %s, "
+        "found cache entry for name %s, type %u: version %u, update_ts %s, "
         "ip_addr %s, port %u, be_type %u, be_name %s, be_reason %s, "
-        "be_mesg %s, be_expires %s, be_sid %u", name, type, bme.version,
-        pr_strtime(bme.update_ts), bme.ip_addr, bme.port, bme.be_type,
-        bme.be_name, bme.be_reason, bme.be_mesg ? bme.be_mesg : "<nil>",
-        pr_strtime(bme.be_expires), bme.be_sid);
+        "be_mesg %s, be_expires %s, be_sid %u", name, type, bce.version,
+        pr_strtime(bce.update_ts), bce.ip_addr, bce.port, bce.be_type,
+        bce.be_name, bce.be_reason, bce.be_mesg ? bce.be_mesg : "<nil>",
+        pr_strtime(bce.be_expires), bce.be_sid);
 
       /* Use BanCacheOptions to check the various struct fields for usability.
        */
 
       if (ban_cache_opts & BAN_CACHE_OPT_MATCH_SERVER) {
-        pr_netaddr_t *na;
+        const pr_netaddr_t *na;
 
-        /* Make sure that the IP address/port in the mcache entry matches
+        /* Make sure that the IP address/port in the cache entry matches
          * our address/port.
          */
-
         na = pr_netaddr_get_sess_local_addr();
         if (use_entry == TRUE &&
-            strcmp(bme.ip_addr, pr_netaddr_get_ipstr(na)) != 0) {
+            bce.ip_addr != NULL &&
+            strcmp(bce.ip_addr, pr_netaddr_get_ipstr(na)) != 0) {
           use_entry = FALSE;
 
           (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-            "BanCacheOption MatchServer: memcache entry IP address '%s' "
+            "BanCacheOption MatchServer: cache entry IP address '%s' "
             "does not match vhost IP address '%s', ignoring entry",
-            bme.ip_addr, pr_netaddr_get_ipstr(na));
+            bce.ip_addr, pr_netaddr_get_ipstr(na));
         }
 
         if (use_entry == TRUE &&
-            bme.port != pr_netaddr_get_port(na)) {
+            bce.port != pr_netaddr_get_port(na)) {
           use_entry = FALSE;
 
           (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
-            "BanCacheOption MatchServer: memcache entry port %u "
+            "BanCacheOption MatchServer: cache entry port %u "
             "does not match vhost port %d, ignoring entry",
-            bme.port, pr_netaddr_get_port(na));
+            bce.port, pr_netaddr_get_port(na));
         }
       }
 
       if (use_entry == TRUE) {
         if (mesg != NULL &&
-            strlen(bme.be_mesg) > 0) {
-          *mesg = bme.be_mesg;
+            bce.be_mesg != NULL &&
+            strlen(bce.be_mesg) > 0) {
+          *mesg = bce.be_mesg;
         }
 
         return 0;
@@ -1135,6 +1545,12 @@ static const char *ban_event_entry_typestr(unsigned int type) {
     case BAN_EV_TYPE_ANON_REJECT_PASSWORDS:
       return "AnonRejectPasswords";
 
+    case BAN_EV_TYPE_EMPTY_PASSWORD:
+      return "EmptyPassword";
+
+    case BAN_EV_TYPE_BAD_PROTOCOL:
+      return "BadProtocol";
+
     case BAN_EV_TYPE_MAX_CLIENTS_PER_CLASS:
       return "MaxClientsPerClass";
 
@@ -1343,7 +1759,7 @@ static void ban_event_list_expire(void) {
 /* Controls handlers
  */
 
-static server_rec *ban_get_server_by_id(int sid) {
+static server_rec *ban_get_server_by_id(unsigned int sid) {
   server_rec *s = NULL;
 
   for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
@@ -1359,11 +1775,13 @@ static server_rec *ban_get_server_by_id(int sid) {
   return s;
 }
 
-static int ban_get_sid_by_addr(pr_netaddr_t *server_addr,
+static int ban_get_sid_by_addr(const pr_netaddr_t *server_addr,
     unsigned int server_port) {
   server_rec *s = NULL;
 
   for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
+    pr_signals_handle();
+
     if (s->ServerPort == 0) {
       continue;
     }
@@ -1552,6 +1970,8 @@ static int ban_handle_info(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
 
         switch (type) {
           case BAN_EV_TYPE_ANON_REJECT_PASSWORDS:
+          case BAN_EV_TYPE_EMPTY_PASSWORD:
+          case BAN_EV_TYPE_BAD_PROTOCOL:
           case BAN_EV_TYPE_MAX_CLIENTS_PER_CLASS:
           case BAN_EV_TYPE_MAX_CLIENTS_PER_HOST:
           case BAN_EV_TYPE_MAX_CLIENTS_PER_USER:
@@ -1607,7 +2027,7 @@ static int ban_handle_info(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
 
 static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i = 0;
+  register int i = 0;
   unsigned int sid = 0;
 
   /* Check the ban ACL */
@@ -1619,8 +2039,8 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
   }
 
   /* Sanity check */
-  if (!reqargv) {
-    pr_ctrls_add_response(ctrl, "missing arguments");
+  if (reqargv == NULL) {
+    pr_ctrls_add_response(ctrl, "missing parameters");
     return -1;
   }
 
@@ -1658,7 +2078,7 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
 
     if (server_str != NULL) {
       char *ptr;
-      pr_netaddr_t *server_addr = NULL;
+      const pr_netaddr_t *server_addr = NULL;
       unsigned int server_port = 21;
       int res;
 
@@ -1694,7 +2114,7 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
   if (strcmp(reqargv[0], "user") == 0) {
 
     if (reqargc < 2) {
-      pr_ctrls_add_response(ctrl, "missing arguments");
+      pr_ctrls_add_response(ctrl, "missing parameters");
       return -1;
     }
 
@@ -1735,7 +2155,7 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
   } else if (strcmp(reqargv[0], "host") == 0) {
 
     if (reqargc < 2) {
-      pr_ctrls_add_response(ctrl, "missing arguments");
+      pr_ctrls_add_response(ctrl, "missing parameters");
       return -1;
     }
 
@@ -1746,12 +2166,11 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
 
     /* Add each site to the list */
     for (i = optind; i < reqargc; i++) {
+      const pr_netaddr_t *site;
 
       /* XXX handle multiple addresses */
-      pr_netaddr_t *site = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool,
-        reqargv[i], NULL);
-
-      if (!site) {
+      site = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, reqargv[i], NULL);
+      if (site == NULL) {
         pr_ctrls_add_response(ctrl, "ban: unknown host '%s'", reqargv[i]);
         continue;
       }
@@ -1785,7 +2204,7 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
   } else if (strcmp(reqargv[0], "class") == 0) {
 
     if (reqargc < 2) {
-      pr_ctrls_add_response(ctrl, "missing arguments");
+      pr_ctrls_add_response(ctrl, "missing parameters");
       return -1;
     }
 
@@ -1836,7 +2255,7 @@ static int ban_handle_ban(pr_ctrls_t *ctrl, int reqargc,
 
 static int ban_handle_permit(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i = 0;
+  register int i = 0;
   int optc;
   unsigned int sid = 0;
   const char *reqopts = "s:";
@@ -1851,8 +2270,9 @@ static int ban_handle_permit(pr_ctrls_t *ctrl, int reqargc,
   }
 
   /* Sanity check */
-  if (reqargc < 2 || reqargv == NULL) {
-    pr_ctrls_add_response(ctrl, "missing arguments");
+  if (reqargc < 2 ||
+      reqargv == NULL) {
+    pr_ctrls_add_response(ctrl, "missing parameters");
     return -1;
   }
 
@@ -1883,7 +2303,7 @@ static int ban_handle_permit(pr_ctrls_t *ctrl, int reqargc,
 
   if (server_str != NULL) {
     char *ptr;
-    pr_netaddr_t *server_addr = NULL;
+    const pr_netaddr_t *server_addr = NULL;
     unsigned int server_port = 21;
     int res;
 
@@ -1989,12 +2409,11 @@ static int ban_handle_permit(pr_ctrls_t *ctrl, int reqargc,
       }
 
       for (i = optind; i < reqargc; i++) {
+        const pr_netaddr_t *site;
 
         /* XXX handle multiple addresses */
-        pr_netaddr_t *site = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool,
-          reqargv[i], NULL);
-
-        if (site) {
+        site = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, reqargv[i], NULL);
+        if (site != NULL) {
           if (ban_list_remove(BAN_TYPE_HOST, sid,
                 pr_netaddr_get_ipstr(site)) == 0) {
             (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
@@ -2081,15 +2500,17 @@ static int ban_handle_permit(pr_ctrls_t *ctrl, int reqargc,
  */
 
 MODRET ban_pre_pass(cmd_rec *cmd) {
-  char *user, *rule_mesg = NULL;
+  const char *user;
+  char *rule_mesg = NULL;
 
-  if (ban_engine != TRUE)
+  if (ban_engine != TRUE) {
     return PR_DECLINED(cmd);
+  }
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-
-  if (!user)
+  if (user == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   /* Make sure the list is up-to-date. */
   ban_list_expire();
@@ -2133,7 +2554,7 @@ MODRET set_bancache(cmd_rec *cmd) {
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-#ifdef PR_USE_MEMCACHE
+#if defined(PR_USE_MEMCACHE)
   if (strcmp(cmd->argv[1], "memcache") == 0) {
     config_rec *c;
 
@@ -2142,13 +2563,24 @@ MODRET set_bancache(cmd_rec *cmd) {
 
     return PR_HANDLED(cmd);
   }
-#endif /* !PR_USE_MEMCACHE */
+#endif /* PR_USE_MEMCACHE */
+
+#if defined(PR_USE_REDIS)
+  if (strcmp(cmd->argv[1], "redis") == 0) {
+    config_rec *c;
+
+    c = add_config_param(cmd->argv[0], 1, NULL);
+    c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
+
+    return PR_HANDLED(cmd);
+  }
+#endif /* PR_USE_REDIS */
 
   CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unsupported BanCache driver '",
     cmd->argv[1], "'", NULL));
 }
 
-/* usage: BanCacheOptions MatchServer */
+/* usage: BanCacheOptions opt1 ... optN */
 MODRET set_bancacheoptions(cmd_rec *cmd) {
   register unsigned int i;
   config_rec *c;
@@ -2164,6 +2596,9 @@ MODRET set_bancacheoptions(cmd_rec *cmd) {
     if (strcmp(cmd->argv[i], "MatchServer") == 0) {
       opts |= BAN_CACHE_OPT_MATCH_SERVER;
 
+    } else if (strcmp(cmd->argv[i], "UseJSON") == 0) {
+      opts |= BAN_CACHE_OPT_USE_JSON;
+
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown BanCacheOption '",
         cmd->argv[i], "'", NULL));
@@ -2340,11 +2775,21 @@ MODRET set_banonevent(cmd_rec *cmd) {
     pr_event_register(&ban_module, "mod_auth.anon-reject-passwords",
       ban_anonrejectpasswords_ev, bee);
 
+  } else if (strcasecmp(cmd->argv[1], "BadProtocol") == 0) {
+    bee->bee_type = BAN_EV_TYPE_BAD_PROTOCOL;
+    pr_event_register(&ban_module, "core.bad-protocol", ban_badprotocol_ev,
+      bee);
+
   } else if (strcasecmp(cmd->argv[1], "ClientConnectRate") == 0) {
     bee->bee_type = BAN_EV_TYPE_CLIENT_CONNECT_RATE;
     pr_event_register(&ban_module, "mod_ban.client-connect-rate",
       ban_clientconnectrate_ev, bee);
 
+  } else if (strcasecmp(cmd->argv[1], "EmptyPassword") == 0) {
+    bee->bee_type = BAN_EV_TYPE_EMPTY_PASSWORD;
+    pr_event_register(&ban_module, "mod_auth.empty-password",
+      ban_emptypassword_ev, bee);
+
   } else if (strcasecmp(cmd->argv[1], "LoginRate") == 0) {
     /* We don't register an event listener here.  Instead we rely on
      * the POST_CMD handler for the PASS command; it's the "event"
@@ -2635,6 +3080,22 @@ static void ban_anonrejectpasswords_ev(const void *event_data,
     ipstr, tmpl);
 }
 
+static void ban_badprotocol_ev(const void *event_data, void *user_data) {
+
+  /* For this event, event_data is the client. */
+  conn_t *c = (conn_t *) event_data;
+  const char *ipstr;
+
+  /* user_data is a template of the ban event entry. */
+  struct ban_event_entry *tmpl = user_data;
+
+  if (ban_engine != TRUE)
+    return;
+
+  ipstr = pr_netaddr_get_ipstr(c->remote_addr);
+  ban_handle_event(BAN_EV_TYPE_BAD_PROTOCOL, BAN_TYPE_HOST, ipstr, tmpl);
+}
+
 static void ban_clientconnectrate_ev(const void *event_data, void *user_data) {
 
   /* For this event, event_data is the client. */
@@ -2651,6 +3112,19 @@ static void ban_clientconnectrate_ev(const void *event_data, void *user_data) {
   ban_handle_event(BAN_EV_TYPE_CLIENT_CONNECT_RATE, BAN_TYPE_HOST, ipstr, tmpl);
 }
 
+static void ban_emptypassword_ev(const void *event_data, void *user_data) {
+  const char *ipstr;
+
+  /* user_data is a template of the ban event entry. */
+  struct ban_event_entry *tmpl = user_data;
+
+  if (ban_engine != TRUE)
+    return;
+
+  ipstr = pr_netaddr_get_ipstr(session.c->remote_addr);
+  ban_handle_event(BAN_EV_TYPE_EMPTY_PASSWORD, BAN_TYPE_HOST, ipstr, tmpl);
+}
+
 static void ban_maxclientsperclass_ev(const void *event_data, void *user_data) {
 
   /* For this event, event_data is the class name. */
@@ -2932,6 +3406,7 @@ static void ban_restart_ev(const void *event_data, void *user_data) {
   pr_event_unregister(&ban_module, "core.timeout-login", NULL);
   pr_event_unregister(&ban_module, "core.timeout-no-transfer", NULL);
   pr_event_unregister(&ban_module, "mod_auth.anon-reject-passwords", NULL);
+  pr_event_unregister(&ban_module, "mod_auth.empty-password", NULL);
   pr_event_unregister(&ban_module, "mod_auth.max-clients-per-class", NULL);
   pr_event_unregister(&ban_module, "mod_auth.max-clients-per-host", NULL);
   pr_event_unregister(&ban_module, "mod_auth.max-clients-per-user", NULL);
@@ -2965,6 +3440,36 @@ static void ban_restart_ev(const void *event_data, void *user_data) {
   return;
 }
 
+static void ban_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  ban_cache_opts = 0UL;
+
+#if defined(PR_USE_MEMCACHE)
+  if (mcache != NULL) {
+    (void) pr_memcache_conn_set_namespace(mcache, &ban_module, NULL);
+    mcache = NULL;
+  }
+#endif /* PR_USE_MEMCACHE */
+
+#if defined(PR_USE_REDIS)
+  if (redis != NULL) {
+    (void) pr_redis_conn_set_namespace(redis, &ban_module, NULL, 0);
+    redis = NULL;
+  }
+#endif /* PR_USE_REDIS */
+
+  pr_event_unregister(&ban_module, "core.session-reinit", ban_sess_reinit_ev);
+
+  res = ban_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&ban_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED,
+      NULL);
+  }
+}
+
 static void ban_rootlogin_ev(const void *event_data, void *user_data) {
   const char *ipstr = pr_netaddr_get_ipstr(session.c->remote_addr);
 
@@ -3094,26 +3599,34 @@ static int ban_sess_init(void) {
   const char *remote_ip;
   char *rule_mesg = NULL;
 
-  if (ban_engine != TRUE)
+  pr_event_register(&ban_module, "core.session-reinit", ban_sess_reinit_ev,
+    NULL);
+
+  if (ban_engine != TRUE) {
     return 0;
+  }
 
   /* Check to see if the BanEngine directive is set to 'off'. */
   c = find_config(main_server->conf, CONF_PARAM, "BanEngine", FALSE);
   if (c) {
-    int use_bans = *((int *) c->argv[0]);
+    int use_bans;
 
-    if (!use_bans) {
+    use_bans = *((int *) c->argv[0]);
+    if (use_bans == FALSE) {
       ban_engine = FALSE;
       return 0;
     }
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "BanCache", FALSE);
-  if (c) {
+  if (c != NULL) {
+    int supported_driver = FALSE;
     char *driver;
 
     driver = c->argv[0];
-    if (strcmp(driver, "memcache") == 0) {
+
+#if defined(PR_USE_MEMCACHE)
+    if (strcasecmp(driver, "memcache") == 0) {
       mcache = pr_memcache_conn_get();
       if (mcache == NULL) {
         (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
@@ -3124,17 +3637,54 @@ static int ban_sess_init(void) {
        * driver is acceptable.
        */
       c = find_config(main_server->conf, CONF_PARAM, "BanCacheOptions", FALSE);
-      if (c) {
+      if (c != NULL) {
         ban_cache_opts = *((unsigned long *) c->argv[0]);
       }
 
       /* Configure a namespace prefix for our memcached keys. */
-      if (pr_memcache_conn_set_namespace(mcache, &ban_module, "mod_ban") < 0) {
+      if (pr_memcache_conn_set_namespace(mcache, &ban_module, "mod_ban.") < 0) {
         (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
           "error setting memcache namespace prefix: %s", strerror(errno));
       }
 
-    } else {
+      supported_driver = TRUE;
+    }
+#endif /* PR_USE_MEMCACHE */
+
+#if defined(PR_USE_REDIS)
+    if (strcasecmp(driver, "redis") == 0) {
+      redis = pr_redis_conn_get(session.pool);
+      if (redis == NULL) {
+        (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+          "error connecting to Redis: %s", strerror(errno));
+      }
+
+      /* We really only need to look up BanCacheOptions if the BanCache
+       * driver is acceptable.
+       */
+      c = find_config(main_server->conf, CONF_PARAM, "BanCacheOptions", FALSE);
+      if (c != NULL) {
+        ban_cache_opts = *((unsigned long *) c->argv[0]);
+      }
+
+      /* When using Redis, always use JSON. */
+      if (!(ban_cache_opts & BAN_CACHE_OPT_USE_JSON)) {
+        (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION, "%s",
+          "using JSON for Redis caching");
+        ban_cache_opts |= BAN_CACHE_OPT_USE_JSON;
+      }
+
+      /* Configure a namespace prefix for our Redis keys. */
+      if (pr_redis_conn_set_namespace(redis, &ban_module, "mod_ban.", 8) < 0) {
+        (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
+          "error setting Redis namespace prefix: %s", strerror(errno));
+      }
+
+      supported_driver = TRUE;
+    }
+#endif /* PR_USE_MEMCACHE */
+
+    if (supported_driver == FALSE) {
       (void) pr_log_writefile(ban_logfd, MOD_BAN_VERSION,
         "unsupported BanCache driver '%s' configured, ignoring", driver);
     }
@@ -3179,7 +3729,10 @@ static int ban_sess_init(void) {
     }
   }
 
-  pr_event_generate("mod_ban.client-connect-rate", session.c);
+  if (!ban_client_connected) {
+    pr_event_generate("mod_ban.client-connect-rate", session.c);
+    ban_client_connected = TRUE;
+  }
 
   pr_event_unregister(&ban_module, "core.restart", ban_restart_ev);
 
diff --git a/contrib/mod_copy.c b/contrib/mod_copy.c
index 1a48666..26b72a9 100644
--- a/contrib/mod_copy.c
+++ b/contrib/mod_copy.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_copy -- a module supporting copying of files on the server
  *                      without transferring the data to the client and back
- *
- * Copyright (c) 2009-2015 TJ Saunders
+ * Copyright (c) 2009-2016 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
@@ -29,7 +28,7 @@
 
 #include "conf.h"
 
-#define MOD_COPY_VERSION	"mod_copy/0.5"
+#define MOD_COPY_VERSION	"mod_copy/0.6"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030401
@@ -38,17 +37,22 @@
 
 extern pr_response_t *resp_list, *resp_err_list;
 
+module copy_module;
 static int copy_engine = TRUE;
+static unsigned long copy_opts = 0UL;
+#define COPY_OPT_NO_DELETE_ON_FAILURE	0x0001
 
 static const char *trace_channel = "copy";
 
+static int copy_sess_init(void);
+
 /* These are copied largely from src/mkhome.c */
 
 static int create_dir(const char *dir) {
   struct stat st;
   int res = -1;
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(dir);
   res = pr_fsio_stat(dir, &st);
 
   if (res < 0 &&
@@ -86,10 +90,10 @@ static int create_path(pool *p, const char *path) {
   struct stat st;
   char *curr_path, *dup_path; 
  
-  pr_fs_clear_cache();
- 
-  if (pr_fsio_stat(path, &st) == 0)
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) == 0) {
     return 0;
+  }
  
   dup_path = pstrdup(p, path);
 
@@ -153,7 +157,8 @@ static int create_path(pool *p, const char *path) {
   return 0;
 }
 
-static int copy_symlink(pool *p, const char *src_path, const char *dst_path) {
+static int copy_symlink(pool *p, const char *src_path, const char *dst_path,
+    int flags) {
   char *link_path = pcalloc(p, PR_TUNABLE_BUFFER_SIZE);
   int len;
 
@@ -183,7 +188,8 @@ static int copy_symlink(pool *p, const char *src_path, const char *dst_path) {
   return 0;
 }
 
-static int copy_dir(pool *p, const char *src_dir, const char *dst_dir) {
+static int copy_dir(pool *p, const char *src_dir, const char *dst_dir,
+    int flags) {
   DIR *dh = NULL;
   struct dirent *dent = NULL;
   int res = 0;
@@ -229,7 +235,7 @@ static int copy_dir(pool *p, const char *src_dir, const char *dst_dir) {
         break;
       }
 
-      if (copy_dir(iter_pool, src_path, dst_path) < 0) {
+      if (copy_dir(iter_pool, src_path, dst_path, flags) < 0) {
         res = -1;
         break;
       }
@@ -265,11 +271,18 @@ static int copy_dir(pool *p, const char *src_dir, const char *dst_dir) {
         break;
 
       } else {
-        if (pr_fs_copy_file(src_path, dst_path) < 0) {
+        if (pr_fs_copy_file2(src_path, dst_path, flags, NULL) < 0) {
+          int xerrno = errno;
+
+          pr_log_debug(DEBUG7, MOD_COPY_VERSION
+            ": error copying file '%s' to '%s': %s", src_path, dst_path,
+            strerror(xerrno));
+
           pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
           pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
           pr_response_clear(&resp_err_list);
 
+          errno = xerrno;
           res = -1;
           break;
 
@@ -282,7 +295,7 @@ static int copy_dir(pool *p, const char *src_dir, const char *dst_dir) {
 
           /* Write a TransferLog entry as well. */
 
-          pr_fs_clear_cache();
+          pr_fs_clear_cache2(dst_path);
           pr_fsio_stat(dst_path, &st);
 
           abs_path = dir_abs_path(p, dst_path, TRUE);
@@ -304,7 +317,7 @@ static int copy_dir(pool *p, const char *src_dir, const char *dst_dir) {
 
     /* Is this path a symlink? */
     } else if (S_ISLNK(st.st_mode)) {
-      if (copy_symlink(iter_pool, src_path, dst_path) < 0) {
+      if (copy_symlink(iter_pool, src_path, dst_path, flags) < 0) {
         res = -1;
         break;
       }
@@ -328,7 +341,7 @@ static int copy_dir(pool *p, const char *src_dir, const char *dst_dir) {
 
 static int copy_paths(pool *p, const char *from, const char *to) {
   struct stat st;
-  int res;
+  int res, flags = 0;
   xaset_t *set;
 
   set = get_dir_ctxt(p, (char *) to);
@@ -363,11 +376,15 @@ static int copy_paths(pool *p, const char *from, const char *to) {
     errno = xerrno;
     return -1;
   }
-   
+
+  if (copy_opts & COPY_OPT_NO_DELETE_ON_FAILURE) {
+    flags |= PR_FSIO_COPY_FILE_FL_NO_DELETE_ON_FAILURE;
+  }
+
   if (S_ISREG(st.st_mode)) { 
     char *abs_path;
 
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(to);
     res = pr_fsio_stat(to, &st);
     if (res == 0) {
       unsigned char *allow_overwrite;
@@ -382,7 +399,7 @@ static int copy_paths(pool *p, const char *from, const char *to) {
       }
     }
 
-    res = pr_fs_copy_file(from, to);
+    res = pr_fs_copy_file2(from, to, flags, NULL);
     if (res < 0) {
       int xerrno = errno;
 
@@ -393,8 +410,11 @@ static int copy_paths(pool *p, const char *from, const char *to) {
       return -1;
     }
 
-    pr_fs_clear_cache();
-    pr_fsio_stat(to, &st);
+    pr_fs_clear_cache2(to);
+    if (pr_fsio_stat(to, &st) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error stat'ing '%s': %s", to, strerror(errno));
+    }
 
     /* Write a TransferLog entry as well. */
     abs_path = dir_abs_path(p, to, TRUE);
@@ -422,7 +442,7 @@ static int copy_paths(pool *p, const char *from, const char *to) {
       return -1;
     }
 
-    res = copy_dir(p, from, to);
+    res = copy_dir(p, from, to, flags);
     if (res < 0) {
       int xerrno = errno;
 
@@ -435,7 +455,7 @@ static int copy_paths(pool *p, const char *from, const char *to) {
     }
 
   } else if (S_ISLNK(st.st_mode)) {
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(to);
     res = pr_fsio_stat(to, &st);
     if (res == 0) {
       unsigned char *allow_overwrite;
@@ -445,12 +465,13 @@ static int copy_paths(pool *p, const char *from, const char *to) {
           *allow_overwrite == FALSE) {
         pr_log_debug(DEBUG6, MOD_COPY_VERSION
           ": AllowOverwrite permission denied for '%s'", to);
+
         errno = EACCES;
         return -1;
       }
     }
 
-    res = copy_symlink(p, from, to);
+    res = copy_symlink(p, from, to, flags);
     if (res < 0) {
       int xerrno = errno;
 
@@ -464,6 +485,7 @@ static int copy_paths(pool *p, const char *from, const char *to) {
   } else {
     pr_log_debug(DEBUG7, MOD_COPY_VERSION
       ": unsupported file type for '%s'", from);
+
     errno = EINVAL;
     return -1;
   }
@@ -494,6 +516,36 @@ MODRET set_copyengine(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: CopyOptions opt1 ... */
+MODRET set_copyoptions(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "NoDeleteOnFailure") == 0) {
+      opts |= COPY_OPT_NO_DELETE_ON_FAILURE;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown CopyOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
 /* Command handlers
  */
 
@@ -507,7 +559,7 @@ MODRET copy_copy(cmd_rec *cmd) {
   }
 
   if (strncasecmp(cmd->argv[1], "COPY", 5) == 0) {
-    char *cmd_name, *from, *to;
+    char *cmd_name, *decoded_path, *from, *to;
     unsigned char *authenticated;
 
     if (cmd->argc != 4) {
@@ -518,15 +570,47 @@ MODRET copy_copy(cmd_rec *cmd) {
     if (authenticated == NULL ||
         *authenticated == FALSE) {
       pr_response_add_err(R_530, _("Please login with USER and PASS"));
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
     /* XXX What about paths which contain spaces? */
-    from = pr_fs_decode_path(cmd->tmp_pool, cmd->argv[2]);
-    from = dir_canonical_vpath(cmd->tmp_pool, from);
 
-    to = pr_fs_decode_path(cmd->tmp_pool, cmd->argv[3]);
-    to = dir_canonical_vpath(cmd->tmp_pool, to);
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[2],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[2], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), (char *) cmd->argv[2]);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    from = dir_canonical_vpath(cmd->tmp_pool, decoded_path);
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[3],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[3], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), (char *) cmd->argv[3]);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    to = dir_canonical_vpath(cmd->tmp_pool, decoded_path);
 
     cmd_name = cmd->argv[0];
     pr_cmd_set_name(cmd, "SITE_COPY");
@@ -534,8 +618,10 @@ MODRET copy_copy(cmd_rec *cmd) {
       int xerrno = EPERM;
 
       pr_cmd_set_name(cmd, cmd_name);
-      pr_response_add_err(R_550, "%s: %s", cmd->argv[3], strerror(xerrno));
+      pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[3],
+        strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -544,13 +630,19 @@ MODRET copy_copy(cmd_rec *cmd) {
     if (copy_paths(cmd->tmp_pool, from, to) < 0) {
       int xerrno = errno;
 
-      pr_response_add_err(R_550, "%s: %s", cmd->argv[1], strerror(xerrno));
+      pr_log_debug(DEBUG7, MOD_COPY_VERSION
+        ": error copying '%s' to '%s': %s", from, to, strerror(xerrno));
+
+      pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[1],
+        strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
 
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[1]);
+    pr_response_add(R_200, _("SITE %s command successful"),
+      (char *) cmd->argv[1]);
     return PR_HANDLED(cmd);
   }
 
@@ -582,6 +674,7 @@ MODRET copy_cpfr(cmd_rec *cmd) {
       *authenticated == FALSE) {
     pr_response_add_err(R_530, _("Please login with USER and PASS"));
   
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -592,8 +685,24 @@ MODRET copy_cpfr(cmd_rec *cmd) {
    * the "SITE CPFR", separating them with spaces.
    */
   for (i = 2; i <= cmd->argc-1; i++) {
-    path = pstrcat(cmd->tmp_pool, path, *path ? " " : "",
-      pr_fs_decode_path(cmd->tmp_pool, cmd->argv[i]), NULL);
+    char *decoded_path;
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[i],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[i], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", decoded_path, NULL);
   }
 
   res = pr_filter_allow_path(CURRENT_CONF, path);
@@ -605,12 +714,18 @@ MODRET copy_cpfr(cmd_rec *cmd) {
       pr_log_debug(DEBUG2, MOD_COPY_VERSION
         ": 'CPFR %s' denied by PathAllowFilter", path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), path);
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
 
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
       pr_log_debug(DEBUG2, MOD_COPY_VERSION
         ": 'CPFR %s' denied by PathDenyFilter", path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), path);
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
   }
 
@@ -619,22 +734,30 @@ MODRET copy_cpfr(cmd_rec *cmd) {
 
   if (!path ||
       !dir_check_canon(cmd->tmp_pool, cmd, cmd->group, path, NULL) ||
-      !exists(path)) {
-    pr_response_add_err(R_550, "%s: %s", path, strerror(errno));
+      !exists2(cmd->tmp_pool, path)) {
+    int xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  pr_table_add(session.notes, "mod_copy.cpfr-path",
-    pstrdup(session.pool, path), 0);
+  if (pr_table_add(session.notes, "mod_copy.cpfr-path",
+      pstrdup(session.pool, path), 0) < 0) {
+    pr_trace_msg(trace_channel, 4,
+      "error adding 'mod_copy.cpfr-path' note: %s", strerror(errno));
+  }
 
-  pr_response_add(R_350, _("File or directory exists, ready for destination "
-    "name"));
+  pr_response_add(R_350,
+    _("File or directory exists, ready for destination name"));
   return PR_HANDLED(cmd);
 }
 
 MODRET copy_cpto(cmd_rec *cmd) {
   register unsigned int i;
-  char *from, *to = "";
+  const char *from, *to = "";
   unsigned char *authenticated = NULL;
 
   if (copy_engine == FALSE) {
@@ -651,6 +774,7 @@ MODRET copy_cpto(cmd_rec *cmd) {
       *authenticated == FALSE) {
     pr_response_add_err(R_530, _("Please login with USER and PASS"));
 
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -660,6 +784,9 @@ MODRET copy_cpto(cmd_rec *cmd) {
   from = pr_table_get(session.notes, "mod_copy.cpfr-path", NULL);
   if (from == NULL) {
     pr_response_add_err(R_503, _("Bad sequence of commands"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -667,17 +794,62 @@ MODRET copy_cpto(cmd_rec *cmd) {
    * the "SITE CPTO", separating them with spaces.
    */
   for (i = 2; i <= cmd->argc-1; i++) {
-    to = pstrcat(cmd->tmp_pool, to, *to ? " " : "",
-      pr_fs_decode_path(cmd->tmp_pool, cmd->argv[i]), NULL);
+    char *decoded_path;
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[i],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[i], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    to = pstrcat(cmd->tmp_pool, to, *to ? " " : "", decoded_path, NULL);
   }
 
   to = dir_canonical_vpath(cmd->tmp_pool, to);
 
   if (copy_paths(cmd->tmp_pool, from, to) < 0) {
     int xerrno = errno;
+    const char *err_code = R_550;
 
-    pr_response_add_err(R_550, "%s: %s", cmd->argv[1], strerror(xerrno));
+    pr_log_debug(DEBUG7, MOD_COPY_VERSION
+      ": error copying '%s' to '%s': %s", from, to, strerror(xerrno));
+
+    /* Check errno for EDQOUT (or the most appropriate alternative).
+     * (I hate the fact that FTP has a special response code just for
+     * this, and that clients actually expect it.  Special cases are
+     * stupid.)
+     */
+    switch (xerrno) {
+#if defined(EDQUOT)
+      case EDQUOT:
+#endif /* EDQUOT */
+#if defined(EFBIG)
+      case EFBIG:
+#endif /* EFBIG */
+#if defined(ENOSPC)
+      case ENOSPC:
+#endif /* ENOSPC */
+        err_code = R_552;
+        break;
+
+      default:
+        err_code = R_550;
+        break;
+    }
 
+    pr_response_add_err(err_code, "%s: %s", (char *) cmd->argv[1],
+      strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -717,6 +889,22 @@ MODRET copy_post_pass(cmd_rec *cmd) {
     copy_engine = *((int *) c->argv[0]);
   }
 
+  if (copy_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "CopyOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    copy_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "CopyOptions", FALSE);
+  }
+
   return PR_DECLINED(cmd);
 }
 
@@ -745,6 +933,7 @@ static int copy_sess_init(void) {
 
 static conftable copy_conftab[] = {
   { "CopyEngine",	set_copyengine,		NULL },
+  { "CopyOptions",	set_copyoptions,	NULL },
 
   { NULL }
 };
diff --git a/contrib/mod_ctrls_admin.c b/contrib/mod_ctrls_admin.c
index c77e39e..b4bdc91 100644
--- a/contrib/mod_ctrls_admin.c
+++ b/contrib/mod_ctrls_admin.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_ctrls_admin -- a module implementing admin control handlers
- *
- * Copyright (c) 2000-2013 TJ Saunders
+ * Copyright (c) 2000-2017 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
@@ -22,21 +21,19 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * This is mod_controls, contrib software for proftpd 1.2 and above.
+ * This is mod_controls, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_ctrls_admin.c,v 1.47 2013-10-13 22:51:36 castaglia Exp $
  */
 
 #include "conf.h"
 #include "privs.h"
 #include "mod_ctrls.h"
 
-#define MOD_CTRLS_ADMIN_VERSION		"mod_ctrls_admin/0.9.7"
+#define MOD_CTRLS_ADMIN_VERSION		"mod_ctrls_admin/0.9.9"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030001
-# error "ProFTPD 1.3.0rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 #ifndef PR_USE_CTRLS
@@ -114,6 +111,281 @@ static void mem_printf(const char *fmt, ...) {
 /* Controls handlers
  */
 
+static server_rec *ctrls_config_find_server(pr_ctrls_t *ctrl,
+    const char *name) {
+  unsigned int port = 21;
+  const pr_netaddr_t *addr;
+  pr_ipbind_t *ipbind;
+  char *name_dup, *ptr;
+
+  name_dup = pstrdup(ctrl->ctrls_tmp_pool, name);
+  if (*name_dup == '[') {
+    size_t namelen;
+
+    /* Possible IPv6 address; make sure there's a terminating bracket. */
+    ptr = strchr(name_dup + 1, ']');
+    if (ptr == NULL) {
+      pr_ctrls_add_response(ctrl, "config: badly formatted IPv6 address: %s",
+        name);
+      errno = EINVAL;
+      return NULL;
+    }
+
+    namelen = ptr - (name_dup + 1);
+    name_dup = pstrndup(ctrl->ctrls_tmp_pool, name_dup + 1, namelen);
+
+    if (*(ptr+1) != '\0') {
+      port = atoi(ptr + 1);
+    }
+
+  } else {
+    ptr = strrchr(name_dup, ':');
+    if (ptr != NULL) {
+      port = atoi(ptr + 1);
+      *ptr = '\0';
+    }
+  }
+
+  addr = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, name_dup, NULL);
+  if (addr == NULL) {
+    pr_ctrls_add_response(ctrl, "config: no such server: %s", name_dup);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  ipbind = pr_ipbind_find(addr, port, TRUE);
+  if (ipbind != NULL) {
+    return ipbind->ib_server;
+  }
+
+  pr_ctrls_add_response(ctrl, "config: no such server: %s", name);
+  errno = ENOENT;
+  return NULL;
+}
+
+static int ctrls_config_dispatch_cmd(pr_ctrls_t *ctrl, cmd_rec *cmd) {
+  conftable *conftab;
+  char found = FALSE;
+
+  cmd->server = pr_parser_server_ctxt_get();
+  cmd->config = pr_parser_config_ctxt_get();
+
+  conftab = pr_stash_get_symbol2(PR_SYM_CONF, cmd->argv[0], NULL,
+    &cmd->stash_index, &cmd->stash_hash);
+  while (conftab != NULL) {
+    modret_t *mr;
+
+    pr_signals_handle();
+
+    cmd->argv[0] = conftab->directive;
+
+    mr = pr_module_call(conftab->m, conftab->handler, cmd);
+    if (mr != NULL) {
+      if (MODRET_ISERROR(mr)) {
+        pr_ctrls_add_response(ctrl, "config set: %s", MODRET_ERRMSG(mr));
+        errno = EPERM;
+        return -1;
+      }
+    }
+
+    if (!MODRET_ISDECLINED(mr)) {
+      found = TRUE;
+    }
+
+    conftab = pr_stash_get_symbol2(PR_SYM_CONF, cmd->argv[0], conftab,
+      &cmd->stash_index, &cmd->stash_hash);
+  }
+
+  if (cmd->tmp_pool) {
+    destroy_pool(cmd->tmp_pool);
+  }
+
+  if (found == FALSE) {
+    pr_ctrls_add_response(ctrl,
+      "config set: unknown configuration directive '%s'",
+      (char *) cmd->argv[0]);
+    errno = EPERM;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ctrls_handle_config_set(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+  register int i;
+  int res;
+  server_rec *s, *curr_main_server;
+  config_rec *c;
+  cmd_rec *cmd;
+  const char *name, *text;
+  size_t textlen;
+
+  /* At this point, reqargv should look something like:
+   *
+   *  0: "127.0.0.1:2121"
+   *  1: "TLSRequired"
+   *  ...
+   */
+
+  if (reqargc < 3 ||
+      reqargv == NULL) {
+    pr_ctrls_add_response(ctrl,
+      "config set: missing required parameters");
+    return -1;
+  }
+
+  name = reqargv[0];
+  s = ctrls_config_find_server(ctrl, name);
+  if (s == NULL) {
+    return -1;
+  }
+
+  res = pr_parser_prepare(ctrl->ctrls_tmp_pool, NULL);
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl, "config set: error preparing parser: %s",
+      strerror(errno));
+    return -1;
+  }
+
+  res = pr_parser_server_ctxt_push(s);
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl,
+      "config set: error adding server to parser stack: %s", strerror(errno));
+    (void) pr_parser_cleanup();
+    return -1;
+  }
+
+  text = "";
+  for (i = 1; i < reqargc; i++) {
+    text = pstrcat(ctrl->ctrls_tmp_pool, text, *text ? " " : "", reqargv[i],
+      NULL);
+  }
+
+  textlen = strlen(text);
+  cmd = pr_parser_parse_line(ctrl->ctrls_tmp_pool, text, textlen);
+  if (cmd == NULL) {
+    pr_ctrls_add_response(ctrl, "config set: error parsing config data: %s",
+      strerror(errno));
+    (void) pr_parser_cleanup();
+    return -1;
+  }
+
+  c = find_config(s->conf, CONF_PARAM, cmd->argv[0], FALSE);
+  if (c != NULL) {
+    /* Note that remove_config() relies on the Parser API. */
+    pr_config_remove(s->conf, cmd->argv[0], PR_CONFIG_FL_PRESERVE_ENTRY, FALSE);
+  }
+
+  curr_main_server = main_server;
+  res = ctrls_config_dispatch_cmd(ctrl, cmd);
+  main_server = curr_main_server;
+
+  if (res < 0) {
+    if (c != NULL) {
+      xaset_t *set;
+
+      /* The config_rec "remembers" its parent set; we just need to add
+       * the record back into that set.
+       */
+      set = c->set;
+      xaset_insert_end(set, (xasetmember_t *) c);
+    }
+
+  } else {
+    pr_ctrls_add_response(ctrl, "config set: %s configured",
+      (char *) cmd->argv[0]);
+    pr_config_merge_down(s->conf, TRUE);
+  }
+
+  (void) pr_parser_cleanup();
+  return 0;
+}
+
+static int ctrls_handle_config_remove(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+  int res;
+  server_rec *s;
+  const char *name, *directive;
+
+  /* At this point, reqargv should look something like:
+   *
+   *  0: "127.0.0.1:2121"
+   *  1: "TLSRequired"
+   */
+
+  if (reqargc < 2 ||
+      reqargv == NULL) {
+    pr_ctrls_add_response(ctrl,
+      "config remove: missing required parameters");
+    return -1;
+  }
+
+  if (reqargc != 2) {
+    pr_ctrls_add_response(ctrl,
+      "config remove: wrong number of parameters");
+    return -1;
+  }
+
+  name = reqargv[0];
+  s = ctrls_config_find_server(ctrl, name);
+  if (s == NULL) {
+    return -1;
+  }
+
+  res = pr_parser_prepare(ctrl->ctrls_tmp_pool, NULL);
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl, "config remove: error preparing parser: %s",
+      strerror(errno));
+    return -1;
+  }
+
+  res = pr_parser_server_ctxt_push(s);
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl,
+      "config remove: error adding server to parser stack: %s",
+      strerror(errno));
+    (void) pr_parser_cleanup();
+    return -1;
+  }
+
+  directive = reqargv[1];
+  res = remove_config(s->conf, directive, FALSE);
+  if (res == TRUE) {
+    pr_ctrls_add_response(ctrl, "config remove: %s removed", directive);
+    pr_config_merge_down(s->conf, TRUE);
+
+  } else {
+    pr_ctrls_add_response(ctrl, "config remove: %s not found in configuration",
+      directive);
+  }
+
+  (void) pr_parser_cleanup();
+  return 0;
+}
+
+static int ctrls_handle_config(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+
+  /* Sanity check */
+  if (reqargc == 0 ||
+      reqargv == NULL) {
+    pr_ctrls_add_response(ctrl, "config: missing required parameters");
+    return -1;
+  }
+
+  if (strncmp(reqargv[0], "set", 4) == 0) {
+    return ctrls_handle_config_set(ctrl, --reqargc, ++reqargv);
+
+  } else if (strncmp(reqargv[0], "remove", 7) == 0) {
+    return ctrls_handle_config_remove(ctrl, --reqargc, ++reqargv);
+  }
+
+  pr_ctrls_add_response(ctrl, "config: unknown config action: '%s'",
+    reqargv[0]);
+  return -1;
+}
+
 static int ctrls_handle_debug(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
 
@@ -241,30 +513,33 @@ static int ctrls_handle_dns(pr_ctrls_t *ctrl, int reqargc,
   return 0;
 }
 
-static int admin_addr_down(pr_ctrls_t *ctrl, pr_netaddr_t *addr,
+static int admin_addr_down(pr_ctrls_t *ctrl, const pr_netaddr_t *addr,
     unsigned int port) {
 
   pr_ctrls_log(MOD_CTRLS_ADMIN_VERSION, "down: disabling %s#%u",
     pr_netaddr_get_ipstr(addr), port);
 
   if (pr_ipbind_close(addr, port, FALSE) < 0) {
-    if (errno == ENOENT)
+    if (errno == ENOENT) {
       pr_ctrls_add_response(ctrl, "down: no such server: %s#%u",
         pr_netaddr_get_ipstr(addr), port);
-    else
+
+    } else {
       pr_ctrls_add_response(ctrl, "down: %s#%u already disabled",
         pr_netaddr_get_ipstr(addr), port);
+    }
 
-  } else
+  } else {
     pr_ctrls_add_response(ctrl, "down: %s#%u disabled",
       pr_netaddr_get_ipstr(addr), port);
+  }
 
   return 0;
 }
 
 static int ctrls_handle_down(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i = 0;
+  register int i = 0;
 
   /* Handle scheduled downs of virtual servers in the future, and
    * cancellations of scheduled downs.
@@ -287,7 +562,7 @@ static int ctrls_handle_down(pr_ctrls_t *ctrl, int reqargc,
   for (i = 0; i < reqargc; i++) {
     unsigned int server_port = 21;
     char *server_str = reqargv[i], *tmp = NULL;
-    pr_netaddr_t *server_addr = NULL;
+    const pr_netaddr_t *server_addr = NULL;
     array_header *addrs = NULL;
 
     /* Check for an argument of "all" */
@@ -304,7 +579,7 @@ static int ctrls_handle_down(pr_ctrls_t *ctrl, int reqargc,
     }
 
     server_addr = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, server_str, &addrs);
-    if (!server_addr) {
+    if (server_addr == NULL) {
       pr_ctrls_add_response(ctrl, "down: no such server: %s#%u",
         server_str, server_port);
       continue;
@@ -312,12 +587,13 @@ static int ctrls_handle_down(pr_ctrls_t *ctrl, int reqargc,
 
     admin_addr_down(ctrl, server_addr, server_port);
 
-    if (addrs) {
+    if (addrs != NULL) {
       register unsigned int j;
       pr_netaddr_t **elts = addrs->elts;
 
-      for (j = 0; j < addrs->nelts; j++)
+      for (j = 0; j < addrs->nelts; j++) {
         admin_addr_down(ctrl, elts[j], server_port);
+      }
     }
   }
 
@@ -376,11 +652,12 @@ static int ctrls_handle_get(pr_ctrls_t *ctrl, int reqargc,
     if (reqargc == 1) {
       conftable *conftab;
       int stash_idx = -1;
+      unsigned int stash_hash = 0;
 
       /* Create a list of all known configuration directives. */
 
-      conftab = pr_stash_get_symbol(PR_SYM_CONF, NULL, NULL, &stash_idx);
-
+      conftab = pr_stash_get_symbol2(PR_SYM_CONF, NULL, NULL, &stash_idx,
+        &stash_hash);
       while (stash_idx != -1) {
         pr_signals_handle();
 
@@ -388,10 +665,12 @@ static int ctrls_handle_get(pr_ctrls_t *ctrl, int reqargc,
           pr_ctrls_add_response(ctrl, "%s (mod_%s.c)", conftab->directive,
             conftab->m->name);
 
-        } else
+        } else {
           stash_idx++;
+        }
 
-        conftab = pr_stash_get_symbol(PR_SYM_CONF, NULL, conftab, &stash_idx);
+        conftab = pr_stash_get_symbol2(PR_SYM_CONF, NULL, conftab, &stash_idx,
+          &stash_hash);
       }
 
       /* Be nice, and sort the directives lexicographically */
@@ -432,7 +711,7 @@ static int ctrls_handle_kick(pr_ctrls_t *ctrl, int reqargc,
 
   /* Handle 'kick user' requests. */
   if (strcmp(reqargv[0], "user") == 0) {
-    register unsigned int i = 0;
+    register int i = 0;
     int optc, kicked_count = 0, kicked_max = -1;
     const char *reqopts = "n:";
     pr_scoreboard_entry_t *score = NULL;
@@ -532,7 +811,7 @@ static int ctrls_handle_kick(pr_ctrls_t *ctrl, int reqargc,
 
   /* Handle 'kick host' requests. */
   } else if (strcmp(reqargv[0], "host") == 0) {
-    register unsigned int i = 0;
+    register int i = 0;
     int optc, kicked_count = 0, kicked_max = -1;
     const char *reqopts = "n:";
     pr_scoreboard_entry_t *score = NULL;
@@ -569,7 +848,7 @@ static int ctrls_handle_kick(pr_ctrls_t *ctrl, int reqargc,
     for (i = optind; i < reqargc; i++) {
       unsigned char kicked_host = FALSE;
       const char *addr;
-      pr_netaddr_t *na;
+      const pr_netaddr_t *na;
 
       na = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, reqargv[i], NULL);
       if (na == NULL) {
@@ -630,7 +909,7 @@ static int ctrls_handle_kick(pr_ctrls_t *ctrl, int reqargc,
 
   /* Handle 'kick class' requests. */
   } else if (strcmp(reqargv[0], "class") == 0) {
-    register unsigned int i = 0;
+    register int i = 0;
     int optc, kicked_count = 0, kicked_max = -1;
     const char *reqopts = "n:";
     pr_scoreboard_entry_t *score = NULL;
@@ -751,7 +1030,7 @@ static int ctrls_handle_restart(pr_ctrls_t *ctrl, int reqargc,
 
   /* Be pedantic */
   if (reqargc > 1) {
-    pr_ctrls_add_response(ctrl, "bad number of arguments");
+    pr_ctrls_add_response(ctrl, "wrong number of parameters");
     return -1;
   }
 
@@ -767,11 +1046,17 @@ static int ctrls_handle_restart(pr_ctrls_t *ctrl, int reqargc,
       struct tm *tm;
 
       tm = pr_gmtime(ctrl->ctrls_tmp_pool, &ctrls_admin_start);
-      pr_ctrls_add_response(ctrl,
-        "server restarted %u %s since %04d-%02d-%02d %02d:%02d:%02d GMT",
-        ctrls_admin_nrestarts, ctrls_admin_nrestarts != 1 ? "times" : "time",
-        tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
-        tm->tm_sec);
+      if (tm != NULL) {
+        pr_ctrls_add_response(ctrl,
+          "server restarted %u %s since %04d-%02d-%02d %02d:%02d:%02d GMT",
+          ctrls_admin_nrestarts, ctrls_admin_nrestarts != 1 ? "times" : "time",
+          tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
+          tm->tm_sec);
+      } else {
+        pr_ctrls_add_response(ctrl, "error obtaining GMT timestamp: %s",
+          strerror(errno));
+        return -1;
+      }
 
     } else {
       pr_ctrls_add_response(ctrl, "unsupported parameter '%s'", reqargv[0]);
@@ -794,7 +1079,7 @@ static int ctrls_handle_scoreboard(pr_ctrls_t *ctrl, int reqargc,
   }
 
   if (reqargc != 1) {
-    pr_ctrls_add_response(ctrl, "bad number of arguments");
+    pr_ctrls_add_response(ctrl, "wrong number of parameters");
     return -1;
   }
 
@@ -812,7 +1097,7 @@ static int ctrls_handle_scoreboard(pr_ctrls_t *ctrl, int reqargc,
 
 static int ctrls_handle_shutdown(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i = 0;
+  register int i = 0;
   int respargc = 0;
   char **respargv = NULL;
 
@@ -844,8 +1129,9 @@ static int ctrls_handle_shutdown(pr_ctrls_t *ctrl, int reqargc,
       /* If the timeout is less than the waiting period, reduce the
        * waiting period by half.
        */
-      if (timeout < waiting)
+      if (timeout < waiting) {
         waiting /= 2;
+      }
     }
 
     /* Now, simply wait for all sessions to be done.  For bonus points,
@@ -907,10 +1193,11 @@ static int ctrls_handle_shutdown(pr_ctrls_t *ctrl, int reqargc,
     "shutdown: flushed to %s/%s client: return value: 0",
     ctrl->ctrls_cl->cl_user, ctrl->ctrls_cl->cl_group);
 
-  for (i = 0; i < respargc; i++)
+  for (i = 0; i < respargc; i++) {
     pr_ctrls_log(MOD_CTRLS_ADMIN_VERSION,
       "shutdown: flushed to %s/%s client: '%s'",
       ctrl->ctrls_cl->cl_user, ctrl->ctrls_cl->cl_group, respargv[i]);
+  }
 
   /* Shutdown by raising SIGTERM.  Easy. */
   raise(SIGTERM);
@@ -918,7 +1205,7 @@ static int ctrls_handle_shutdown(pr_ctrls_t *ctrl, int reqargc,
   return 0;
 }
 
-static int admin_addr_status(pr_ctrls_t *ctrl, pr_netaddr_t *addr,
+static int admin_addr_status(pr_ctrls_t *ctrl, const pr_netaddr_t *addr,
     unsigned int port) {
   pr_ipbind_t *ipbind = NULL;
 
@@ -936,13 +1223,12 @@ static int admin_addr_status(pr_ctrls_t *ctrl, pr_netaddr_t *addr,
 
   pr_ctrls_add_response(ctrl, "status: %s#%u %s", pr_netaddr_get_ipstr(addr),
     port, ipbind->ib_isactive ? "UP" : "DOWN");
-
   return 0;
 }
 
 static int ctrls_handle_status(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i = 0;
+  register int i = 0;
 
   /* Check the status ACL. */
   if (!pr_ctrls_check_acl(ctrl, ctrls_admin_acttab, "status")) {
@@ -961,7 +1247,7 @@ static int ctrls_handle_status(pr_ctrls_t *ctrl, int reqargc,
   for (i = 0; i < reqargc; i++) {
     unsigned int server_port = 21;
     char *server_str = reqargv[i], *tmp = NULL;
-    pr_netaddr_t *server_addr = NULL;
+    const pr_netaddr_t *server_addr = NULL;
     array_header *addrs = NULL;
 
     /* Check for an argument of "all" */
@@ -987,22 +1273,23 @@ static int ctrls_handle_status(pr_ctrls_t *ctrl, int reqargc,
     }
 
     server_addr = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, server_str, &addrs);
-
-    if (!server_addr) {
+    if (server_addr == NULL) {
       pr_ctrls_add_response(ctrl, "status: no such server: %s#%u",
         server_str, server_port);
       continue;
     }
 
-    if (admin_addr_status(ctrl, server_addr, server_port) < 0)
+    if (admin_addr_status(ctrl, server_addr, server_port) < 0) {
       continue;
+    }
 
-    if (addrs) {
+    if (addrs != NULL) {
       register unsigned int j;
       pr_netaddr_t **elts = addrs->elts;
 
-      for (j = 0; j < addrs->nelts; j++)
+      for (j = 0; j < addrs->nelts; j++) {
         admin_addr_status(ctrl, elts[j], server_port);
+      }
     }
   }
 
@@ -1028,7 +1315,7 @@ static int ctrls_handle_trace(pr_ctrls_t *ctrl, int reqargc,
   }
 
   if (strcmp(reqargv[0], "info") != 0) {
-    register unsigned int i;
+    register int i;
 
     for (i = 0; i < reqargc; i++) {
       char *channel, *tmp;
@@ -1066,17 +1353,18 @@ static int ctrls_handle_trace(pr_ctrls_t *ctrl, int reqargc,
     }
  
   } else {
-    pr_table_t *trace_tab = pr_trace_get_table();
+    pr_table_t *trace_tab;
 
-    if (trace_tab) {
-      void *key = NULL, *value = NULL;
+    trace_tab = pr_trace_get_table();
+    if (trace_tab != NULL) {
+      const void *key = NULL, *value = NULL;
 
       pr_ctrls_add_response(ctrl, "%-10s %-6s", "Channel", "Level");
       pr_ctrls_add_response(ctrl, "---------- ------");
 
       pr_table_rewind(trace_tab);
       key = pr_table_next(trace_tab);
-      while (key) {
+      while (key != NULL) {
         pr_signals_handle();
 
         value = pr_table_get(trace_tab, (const char *) key, NULL);
@@ -1100,7 +1388,7 @@ static int ctrls_handle_trace(pr_ctrls_t *ctrl, int reqargc,
 #endif /* PR_USE_TRACE */
 }
 
-static int admin_addr_up(pr_ctrls_t *ctrl, pr_netaddr_t *addr,
+static int admin_addr_up(pr_ctrls_t *ctrl, const pr_netaddr_t *addr,
     unsigned int port) {
   pr_ipbind_t *ipbind = NULL;
   int res = 0;
@@ -1111,6 +1399,7 @@ static int admin_addr_up(pr_ctrls_t *ctrl, pr_netaddr_t *addr,
     pr_ctrls_add_response(ctrl,
       "up: no server associated with %s#%u", pr_netaddr_get_ipstr(addr),
       port);
+    errno = ENOENT;
     return -1;
   }
 
@@ -1154,7 +1443,7 @@ static int admin_addr_up(pr_ctrls_t *ctrl, pr_netaddr_t *addr,
 
 static int ctrls_handle_up(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i = 0;
+  register int i = 0;
 
   /* Handle scheduled ups of virtual servers in the future, and
    * cancellations of scheduled ups.
@@ -1177,7 +1466,7 @@ static int ctrls_handle_up(pr_ctrls_t *ctrl, int reqargc,
   for (i = 0; i < reqargc; i++) {
     unsigned int server_port = 21;
     char *server_str = reqargv[i], *tmp = NULL;
-    pr_netaddr_t *server_addr = NULL;
+    const pr_netaddr_t *server_addr = NULL;
     array_header *addrs = NULL;
 
     tmp = strchr(server_str, '#');
@@ -1187,22 +1476,25 @@ static int ctrls_handle_up(pr_ctrls_t *ctrl, int reqargc,
     }
 
     server_addr = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, server_str, &addrs);
-    if (!server_addr) {
+    if (server_addr == NULL) {
       pr_ctrls_add_response(ctrl, "up: unable to resolve address for '%s'",
         server_str);
       return -1;
     }
 
-    if (admin_addr_up(ctrl, server_addr, server_port) < 0)
+    if (admin_addr_up(ctrl, server_addr, server_port) < 0) {
       return -1;
+    }
 
-    if (addrs) {
+    if (addrs != NULL) {
       register unsigned int j;
       pr_netaddr_t **elts = addrs->elts;
 
-      for (j = 0; j < addrs->nelts; j++)
-        if (admin_addr_up(ctrl, elts[j], server_port) < 0)
+      for (j = 0; j < addrs->nelts; j++) {
+        if (admin_addr_up(ctrl, elts[j], server_port) < 0) {
           return -1;
+        }
+      }
     }
   }
 
@@ -1404,6 +1696,8 @@ static int ctrls_admin_init(void) {
 }
 
 static ctrls_acttab_t ctrls_admin_acttab[] = {
+  { "config",	"set config directives",	NULL,
+    ctrls_handle_config },
   { "debug",    "set debugging level",		NULL,
     ctrls_handle_debug },
   { "dns",	"set UseReverseDNS configuration",	NULL,
diff --git a/contrib/mod_deflate.c b/contrib/mod_deflate.c
index f1e098d..479f048 100644
--- a/contrib/mod_deflate.c
+++ b/contrib/mod_deflate.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_deflate -- a module for supporting on-the-fly compression
- *
- * Copyright (c) 2004-2014 TJ Saunders
+ * Copyright (c) 2004-2017 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
@@ -25,8 +24,7 @@
  * This is mod_deflate, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
  *
- * $Id: mod_deflate.c,v 1.14 2014-01-03 07:19:01 castaglia Exp $
- * $Libraries: -lz $
+ * $Libraries: -lz$
  */
 
 #include <zlib.h>
@@ -43,6 +41,8 @@
 
 module deflate_module;
 
+static int deflate_sess_init(void);
+
 static int deflate_enabled = FALSE;
 static int deflate_engine = FALSE;
 static int deflate_logfd = -1;
@@ -139,7 +139,7 @@ static int deflate_netio_close_cb(pr_netio_stream_t *nstrm) {
   if (nstrm->strm_type == PR_NETIO_STRM_DATA) {
     z_stream *zstrm;
 
-    zstrm = pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
+    zstrm = (z_stream *) pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
     if (zstrm == NULL) {
       return 0;
     }
@@ -307,7 +307,7 @@ static int deflate_netio_read_cb(pr_netio_stream_t *nstrm, char *buf,
     size_t copylen = 0;
     z_stream *zstrm;
 
-    zstrm = pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
+    zstrm = (z_stream *) pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
     if (zstrm == NULL) {
       pr_trace_msg(trace_channel, 2,
         "no zstream found in stream data for reading");
@@ -523,7 +523,7 @@ static int deflate_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
     int res = 0;
     z_stream *zstrm;
 
-    zstrm = pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
+    zstrm = (z_stream *) pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
     if (zstrm == NULL) {
       return 0;
     }
@@ -584,7 +584,7 @@ static int deflate_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
           session.total_raw_out += res;
 
           /* Watch out for short writes. */
-          if (res == datalen) {
+          if ((size_t) res == datalen) {
             break;
           }
 
@@ -612,7 +612,7 @@ static int deflate_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
     size_t datalen, offset = 0;
     z_stream *zstrm;
 
-    zstrm = pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
+    zstrm = (z_stream *) pr_table_get(nstrm->notes, DEFLATE_NETIO_NOTE, NULL);
     if (zstrm == NULL) {
       pr_trace_msg(trace_channel, 2,
         "no zstream found in stream data for writing");
@@ -684,7 +684,7 @@ static int deflate_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
         (unsigned long) datalen, nstrm->strm_fd);
 
       /* Watch out for short writes */
-      if (res == datalen) {
+      if ((size_t) res == datalen) {
         zstrm->next_out = deflate_zbuf;
         zstrm->avail_out = deflate_zbufsz;
         break;
@@ -728,8 +728,9 @@ MODRET set_deflateengine(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   bool = get_boolean(cmd, 1);
-  if (bool == -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));
@@ -740,14 +741,18 @@ MODRET set_deflateengine(cmd_rec *cmd) {
 
 /* usage: DeflateLog path|"none" */
 MODRET set_deflatelog(cmd_rec *cmd) {
+  char *path;
+
   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));
+  path = cmd->argv[1];
+  if (pr_fs_valid_path(path) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": ", path, " is not a valid path",
+      NULL));
+  }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
@@ -775,7 +780,7 @@ MODRET deflate_opts(cmd_rec *cmd) {
       deflate_strategy = MOD_DEFLATE_DEFAULT_STRATEGY;
       deflate_window_bits = MOD_DEFLATE_DEFAULT_WINDOW_BITS;
 
-      pr_response_add(R_200, _("%s OK"), cmd->argv[0]);
+      pr_response_add(R_200, _("%s OK"), (char *) cmd->argv[0]);
       return PR_HANDLED(cmd);
 
     } else {
@@ -783,23 +788,38 @@ MODRET deflate_opts(cmd_rec *cmd) {
 
       if (cmd->argc % 2 != 0) {
         pr_response_add_err(R_501, _("Bad number of parameters"));
+
+        pr_cmd_set_errno(cmd, EINVAL);
+        errno = EINVAL;
         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) {
+        char *key, *val;
+
+        key = cmd->argv[i];
+        val = cmd->argv[i+1];
+
+        if (strcasecmp(key, "blocksize") == 0 ||
+            strcasecmp(key, "engine") == 0) {
           pr_response_add_err(R_501, _("%s: unsupported MODE Z option: %s"),
-            cmd->argv[0], cmd->argv[i]);
+            (char *) cmd->argv[0], key);
+
+          pr_cmd_set_errno(cmd, ENOSYS);
+          errno = ENOSYS;
           return PR_ERROR(cmd);
 
-        } else if (strcasecmp(cmd->argv[i], "level") == 0) {
-          int level = atoi(cmd->argv[i+1]);
+        } else if (strcasecmp(key, "level") == 0) {
+          int level;
 
+          level = atoi(val);
           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]);
+              (char *) cmd->argv[0], key, val);
+
+            pr_cmd_set_errno(cmd, EINVAL);
+            errno = EINVAL;
             return PR_ERROR(cmd);
           }
 
@@ -807,7 +827,10 @@ MODRET deflate_opts(cmd_rec *cmd) {
 
         } else {
           pr_response_add_err(R_501, _("%s: unknown MODE Z option: %s"),
-            cmd->argv[0], cmd->argv[i]);
+            (char *) cmd->argv[0], key);
+
+          pr_cmd_set_errno(cmd, EINVAL);
+          errno = EINVAL;
           return PR_ERROR(cmd);
         }
       }
@@ -821,19 +844,22 @@ MODRET deflate_opts(cmd_rec *cmd) {
 }
 
 MODRET deflate_mode(cmd_rec *cmd) {
+  char *mode;
+
   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);
+      "declining MODE Z (wrong number of parameters: %d)", cmd->argc);
     return PR_DECLINED(cmd);
   }
 
-  cmd->argv[1][0] = toupper(cmd->argv[1][0]);
+  mode = cmd->argv[1];
+  mode[0] = toupper(mode[0]);
 
-  if (cmd->argv[1][0] == 'Z') {
+  if (mode[0] == 'Z') {
     if (session.rfc2228_mech) {
       (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
         "declining MODE Z (RFC2228 mechanism '%s' in effect)",
@@ -843,6 +869,9 @@ MODRET deflate_mode(cmd_rec *cmd) {
         session.rfc2228_mech);
 
       pr_response_add_err(R_504, _("Unable to handle MODE Z at this time"));
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
@@ -855,7 +884,7 @@ MODRET deflate_mode(cmd_rec *cmd) {
      * compression.
      */
 
-    deflate_netio = pr_alloc_netio2(session.pool, &deflate_module);
+    deflate_netio = pr_alloc_netio2(session.pool, &deflate_module, NULL);
     deflate_netio->close = deflate_netio_close_cb;
     deflate_netio->open = deflate_netio_open_cb;
     deflate_netio->read = deflate_netio_read_cb;
@@ -890,7 +919,8 @@ MODRET deflate_mode(cmd_rec *cmd) {
 
       } else {
         (void) pr_log_writefile(deflate_logfd, MOD_DEFLATE_VERSION,
-          "%s %s: unregistered netio", cmd->argv[0], cmd->argv[1]);
+          "%s %s: unregistered netio", (char *) cmd->argv[0],
+          (char *) cmd->argv[1]);
       }
 
       if (deflate_netio) {
@@ -905,12 +935,43 @@ MODRET deflate_mode(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void deflate_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&deflate_module, "core.session-reinit",
+    deflate_sess_reinit_ev);
+
+  deflate_engine = FALSE;
+  pr_feat_remove("MODE Z");
+  (void) close(deflate_logfd);
+  deflate_logfd = -1;
+
+  res = deflate_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&deflate_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization functions
  */
 
+static int deflate_init(void) {
+  pr_log_debug(DEBUG5, MOD_DEFLATE_VERSION ": using zlib " ZLIB_VERSION);
+  return 0;
+}
+
 static int deflate_sess_init(void) {
   config_rec *c;
 
+  pr_event_register(&deflate_module, "core.session-reinit",
+    deflate_sess_reinit_ev, NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "DeflateEngine", FALSE);
   if (c &&
       *((unsigned int *) c->argv[0]) == TRUE) {
@@ -963,19 +1024,18 @@ static int deflate_sess_init(void) {
    * 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;
+  if (deflate_zbuf == NULL) {
+    deflate_zbufsz = pr_config_get_xfer_bufsz() * 8;
+    deflate_zbuf_ptr = deflate_zbuf = pcalloc(session.pool, deflate_zbufsz);
+    deflate_zbuflen = 0;
+  }
 
-  return 0;
-}
+  if (deflate_rbuf == NULL) {
+    deflate_rbufsz = pr_config_get_xfer_bufsz();
+    deflate_rbuf = palloc(session.pool, deflate_rbufsz);
+    deflate_rbuflen = 0;
+  }
 
-static int deflate_init(void) {
-  pr_log_debug(DEBUG5, MOD_DEFLATE_VERSION ": using zlib " ZLIB_VERSION);
   return 0;
 }
 
diff --git a/contrib/mod_digest.c b/contrib/mod_digest.c
new file mode 100644
index 0000000..4835b87
--- /dev/null
+++ b/contrib/mod_digest.c
@@ -0,0 +1,2920 @@
+/*
+ * ProFTPD: mod_digest - File hashing/checksumming module
+ * Copyright (c) Mathias Berchtold <mb at smartftp.com>
+ * Copyright (c) 2016-2017 TJ Saunders <tj at castaglia.org>
+ * 
+ * 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.
+ *
+ * -----DO NOT EDIT BELOW THIS LINE-----
+ * $Libraries: -lcrypto$
+ */
+
+#include "conf.h"
+
+#define MOD_DIGEST_VERSION      "mod_digest/2.0.0"
+
+/* Define the custom commands/responses used. */
+#ifndef C_HASH
+# define C_HASH		"HASH"
+#endif
+#ifndef C_MD5
+# define C_MD5		"MD5"
+#endif
+#ifndef C_XCRC
+# define C_XCRC		"XCRC"
+#endif
+#ifndef C_XMD5
+# define C_XMD5		"XMD5"
+#endif
+#ifndef C_XSHA
+# define C_XSHA		"XSHA"
+#endif
+#ifndef C_XSHA1
+# define C_XSHA1	"XSHA1"
+#endif
+#ifndef C_XSHA256
+# define C_XSHA256	"XSHA256"
+#endif
+#ifndef C_XSHA512
+# define C_XSHA512	"XSHA512"
+#endif
+
+/* Non-standard FTP response/status codes */
+#ifndef R_251
+# define R_251		"251"
+#endif
+#ifndef R_556
+# define R_556		"556"
+#endif
+
+/* Make sure the version of proftpd is as necessary. */
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
+#endif
+
+#if !defined(HAVE_OPENSSL) && !defined(PR_USE_OPENSSL)
+# error "OpenSSL support required (--enable-openssl)"
+#else
+# include <openssl/bio.h>
+# include <openssl/evp.h>
+# include <openssl/err.h>
+#endif
+
+module digest_module;
+
+static int digest_caching = TRUE;
+
+#ifndef DIGEST_CACHE_DEFAULT_SIZE
+# define DIGEST_CACHE_DEFAULT_SIZE	10000
+#endif
+#ifndef DIGEST_CACHE_DEFAULT_MAX_AGE
+# define DIGEST_CACHE_DEFAULT_MAX_AGE	30
+#endif
+static unsigned int digest_cache_max_size = DIGEST_CACHE_DEFAULT_SIZE;
+static unsigned int digest_cache_max_age = DIGEST_CACHE_DEFAULT_MAX_AGE;
+
+static EVP_MD_CTX *digest_cache_xfer_ctx = NULL;
+
+static int digest_engine = TRUE;
+static pool *digest_pool = NULL;
+
+#define DIGEST_OPT_NO_TRANSFER_CACHE		0x0001
+
+/* Note that the internal APIs for opportunistic caching only appeared,
+ * in working order, in 1.3.6rc2.  So disable it by default for earlier
+ * versions of proftpd.
+ */
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# define DIGEST_DEFAULT_OPTS			DIGEST_OPT_NO_TRANSFER
+#else
+# define DIGEST_DEFAULT_OPTS			0UL
+#endif
+
+static unsigned long digest_opts = DIGEST_DEFAULT_OPTS;
+
+/* Tables used as in-memory caches. */
+static pr_table_t *digest_crc32_tab = NULL;
+static pr_table_t *digest_md5_tab = NULL;
+static pr_table_t *digest_sha1_tab = NULL;
+static pr_table_t *digest_sha256_tab = NULL;
+static pr_table_t *digest_sha512_tab = NULL;
+
+/* Used for tracking the cache keys for expiring. */
+struct digest_cache_key {
+  struct digest_cache_key *next, *prev;
+  pool *pool;
+  unsigned long algo;
+  const char *path;
+  time_t mtime;
+  off_t start;
+  off_t len;
+  const char *key;
+  const char *hex_digest;
+};
+
+static xaset_t *digest_cache_keys = NULL;
+
+/* How often do we check for expired cache entries (in secs)? */
+#define DIGEST_CACHE_EXPIRY_INTVL		5
+
+/* Digest algorithms supported by mod_digest. */
+#define DIGEST_ALGO_CRC32		0x0001
+#ifndef OPENSSL_NO_MD5
+# define DIGEST_ALGO_MD5		0x0002
+#else
+# define DIGEST_ALGO_MD5		0x0000
+#endif /* OPENSSL_NO_MD5 */
+#ifndef OPENSSL_NO_SHA
+# define DIGEST_ALGO_SHA1		0x0004
+#else
+# define DIGEST_ALGO_SHA1		0x0000
+#endif /* OPENSSL_NO_SHA */
+#ifndef OPENSSL_NO_SHA256
+# define DIGEST_ALGO_SHA256		0x0008
+#else
+# define DIGEST_ALGO_SHA256		0x0000
+#endif /* OPENSSL_NO_SHA256 */
+#ifndef OPENSSL_NO_SHA512
+# define DIGEST_ALGO_SHA512		0x0010
+#else
+# define DIGEST_ALGO_SHA512		0x0000
+#endif /* OPENSSL_NO_SHA512 */
+
+#define DIGEST_DEFAULT_ALGOS \
+  (DIGEST_ALGO_CRC32|DIGEST_ALGO_MD5|DIGEST_ALGO_SHA1|DIGEST_ALGO_SHA256|DIGEST_ALGO_SHA512)
+
+static unsigned long digest_algos = DIGEST_DEFAULT_ALGOS;
+
+static const EVP_MD *digest_hash_md = NULL;
+static unsigned long digest_hash_algo = DIGEST_ALGO_SHA1;
+
+/* Flags for determining the style of hash function names. */
+#define DIGEST_ALGO_FL_IANA_STYLE	0x0001
+
+/* We will invoke the progress callback every Nth iteration of the read(2)
+ * loop when digesting a file.
+ */
+#ifndef DIGEST_PROGRESS_NTH_ITER
+# define DIGEST_PROGRESS_NTH_ITER	40000
+#endif
+
+static const char *trace_channel = "digest";
+
+/* Necessary prototypes. */
+static void digest_data_xfer_ev(const void *event_data, void *user_data);
+static int digest_sess_init(void);
+static const char *get_algo_name(unsigned long algo, int flags);
+
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# define PR_STR_FL_HEX_USE_UC			0x0001
+# define PR_STR_FL_HEX_USE_LC			0x0002
+# define pr_str_bin2hex         		digest_bin2hex
+
+static char *digest_bin2hex(pool *p, const unsigned char *buf, size_len,
+    int flags) {
+  static const char *hex_lc = "0123456789abcdef", *hex_uc = "0123456789ABCDEF";
+  register unsigned int i;
+  const char *hex_vals;
+  char *hex, *ptr;
+  size_t hex_len;
+
+  if (p == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (len == 0) {
+    return pstrdup(p, "");
+  }
+
+  /* By default, we use lowercase hex values. */
+  hex_vals = hex_lc;
+  if (flags & PR_STR_FL_HEX_USE_UC) {
+    hex_vals = hex_uc;
+  }
+
+
+  hex_len = (len * 2) + 1;
+  hex = palloc(p, hex_len);
+
+  ptr = hex;
+  for (i = 0; i < len; i++) {
+    *ptr++ = hex_vals[buf[i] >> 4];
+    *ptr++ = hex_vals[buf[i] % 16];
+  }
+  *ptr = '\0';
+
+  return hex;
+}
+#endif
+
+/* CRC32 implementation, as OpenSSL EVP_MD.  The following OpenSSL files
+ * used as templates:
+ *
+ *  crypto/evp/m_md2.c
+ *  crypto/md2/md2.c
+ */
+
+#define CRC32_BLOCK		4
+#define CRC32_DIGEST_LENGTH	4
+#define CRC32_TABLE_SIZE	256
+
+typedef struct crc32_ctx_st {
+  uint32_t *crc32_table;
+  uint32_t data;
+} CRC32_CTX;
+
+static int CRC32_Init(CRC32_CTX *ctx) {
+  register unsigned int i;
+
+  /* Initialize the lookup table.   The magic number in the loop is the official
+   * polynomial used by CRC32 in PKZip.
+   */
+
+  ctx->crc32_table = malloc(sizeof(uint32_t) * CRC32_TABLE_SIZE);
+  if (ctx->crc32_table == NULL) {
+    errno = ENOMEM;
+    return 0;
+  }
+
+  for (i = 0; i < CRC32_TABLE_SIZE; i++) {
+    register unsigned int j;
+    uint32_t crc;
+
+    crc = i;
+    for (j = 8; j > 0; j--) {
+      if (crc & 1) {
+        crc = (crc >> 1) ^ 0xEDB88320;
+      } else {
+        crc >>= 1;
+      }
+    }
+
+    ctx->crc32_table[i] = crc;
+  }
+
+  ctx->data = 0xffffffff;
+  return 1;
+}
+
+#define CRC32(ctx, c, b) (ctx->crc32_table[((int)(c) ^ (b)) & 0xff] ^ ((c) >> 8))
+#define DOCRC(ctx, c, d)  c = CRC32(ctx, c, *d++)
+
+static int CRC32_Update(CRC32_CTX *ctx, const unsigned char *data,
+    size_t datasz) {
+
+  if (datasz == 0) {
+    return 1;
+  }
+
+  while (datasz > 0) {
+    DOCRC(ctx, ctx->data, data);
+    datasz--;
+  }
+
+  return 1;
+}
+
+static int CRC32_Final(unsigned char *md, CRC32_CTX *ctx) {
+  uint32_t crc;
+
+  crc = ctx->data;
+  crc ^= 0xffffffff;
+  crc = htonl(crc);
+
+  memcpy(md, &crc, sizeof(crc));
+  return 1;
+}
+
+static int CRC32_Free(CRC32_CTX *ctx) {
+  if (ctx->crc32_table != NULL) {
+    free(ctx->crc32_table);
+    ctx->crc32_table = NULL;
+  }
+
+  return 1;
+}
+
+static int crc32_init(EVP_MD_CTX *ctx) {
+  return CRC32_Init(ctx->md_data);
+}
+
+static int crc32_update(EVP_MD_CTX *ctx, const void *data, size_t datasz) {
+  return CRC32_Update(ctx->md_data, data, datasz);
+}
+
+static int crc32_final(EVP_MD_CTX *ctx, unsigned char *md) {
+  return CRC32_Final(md, ctx->md_data);
+}
+
+static int crc32_free(EVP_MD_CTX *ctx) {
+  return CRC32_Free(ctx->md_data);
+}
+
+static const EVP_MD crc32_md = {
+  NID_undef,
+  NID_undef,
+  CRC32_DIGEST_LENGTH,
+  0,
+  crc32_init,
+  crc32_update,
+  crc32_final,
+  NULL,
+  crc32_free,
+  EVP_PKEY_NULL_method,
+  CRC32_BLOCK,
+  sizeof(EVP_MD *) + sizeof(CRC32_CTX)
+};
+
+static const EVP_MD *EVP_crc32(void) {
+  return &crc32_md;
+}
+
+static const char *get_errors(void) {
+  unsigned int count = 0;
+  unsigned long error_code;
+  BIO *bio = NULL;
+  char *data = NULL;
+  long datalen;
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
+
+  /* Use ERR_print_errors() and a memory BIO to build up a string with
+   * all of the error messages from the error queue.
+   */
+
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
+    bio = BIO_new(BIO_s_mem());
+  }
+
+  while (error_code) {
+    pr_signals_handle();
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  }
+
+  datalen = BIO_get_mem_data(bio, &data);
+  if (data) {
+    data[datalen] = '\0';
+    str = pstrdup(session.pool, data);
+  }
+  if (bio != NULL) {
+    BIO_free(bio);
+  }
+
+  return str;
+}
+
+static void digest_hash_feat_add(pool *p) {
+  char *feat_str = "";
+  int flags;
+
+  /* Per Draft, the hash function names should be those used in:
+   *  https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.txt
+   */
+  flags = DIGEST_ALGO_FL_IANA_STYLE;
+
+  if (digest_algos & DIGEST_ALGO_CRC32) {
+    int current_hash;
+
+    current_hash = (digest_hash_algo == DIGEST_ALGO_CRC32);
+    feat_str = pstrcat(p, *feat_str ? feat_str : "",
+      get_algo_name(DIGEST_ALGO_CRC32, flags), current_hash ? "*" : "", ";",
+      NULL);
+  }
+
+  if (digest_algos & DIGEST_ALGO_MD5) {
+    int current_hash;
+
+    current_hash = (digest_hash_algo == DIGEST_ALGO_MD5);
+    feat_str = pstrcat(p, *feat_str ? feat_str : "",
+      get_algo_name(DIGEST_ALGO_MD5, flags), current_hash ? "*" : "", ";",
+      NULL);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA1) {
+    int current_hash;
+
+    current_hash = (digest_hash_algo == DIGEST_ALGO_SHA1);
+    feat_str = pstrcat(p, *feat_str ? feat_str : "",
+      get_algo_name(DIGEST_ALGO_SHA1, flags), current_hash ? "*" : "", ";",
+      NULL);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA256) {
+    int current_hash;
+
+    current_hash = (digest_hash_algo == DIGEST_ALGO_SHA256);
+    feat_str = pstrcat(p, *feat_str ? feat_str : "",
+      get_algo_name(DIGEST_ALGO_SHA256, flags), current_hash ? "*" : "", ";",
+      NULL);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA512) {
+    int current_hash;
+
+    current_hash = (digest_hash_algo == DIGEST_ALGO_SHA512);
+    feat_str = pstrcat(p, *feat_str ? feat_str : "",
+      get_algo_name(DIGEST_ALGO_SHA512, flags), current_hash ? "*" : "", ";",
+      NULL);
+  }
+
+  feat_str = pstrcat(p, "HASH ", feat_str, NULL);
+  pr_feat_add(feat_str);
+}
+
+static void digest_hash_feat_remove(void) {
+  const char *feat, *hash_feat = NULL;
+
+  feat = pr_feat_get();
+  while (feat != NULL) {
+    pr_signals_handle();
+
+    if (strncmp(feat, C_HASH, 4) == 0) {
+      hash_feat = feat;
+      break;
+    }
+
+    feat = pr_feat_get_next();
+  }
+
+  if (hash_feat != NULL) {
+    pr_feat_remove(hash_feat);
+  }
+}
+
+static void digest_x_feat_add(pool *p) {
+  if (digest_algos & DIGEST_ALGO_CRC32) {
+    pr_feat_add(C_XCRC);
+  }
+
+  if (digest_algos & DIGEST_ALGO_MD5) {
+    pr_feat_add(C_MD5);
+    pr_feat_add(C_XMD5);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA1) {
+    pr_feat_add(C_XSHA);
+    pr_feat_add(C_XSHA1);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA256) {
+    pr_feat_add(C_XSHA256);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA512) {
+    pr_feat_add(C_XSHA512);
+  }
+}
+
+static void digest_x_help_add(pool *p) {
+  if (digest_algos & DIGEST_ALGO_CRC32) {
+    pr_help_add(C_XCRC, _("<sp> pathname [<sp> start <sp> end]"), TRUE);
+  }
+
+  if (digest_algos & DIGEST_ALGO_MD5) {
+    pr_help_add(C_MD5, _("<sp> pathname"), TRUE);
+    pr_help_add(C_XMD5, _("<sp> pathname [<sp> start <sp> end]"), TRUE);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA1) {
+    pr_help_add(C_XSHA, _("<sp> pathname [<sp> start <sp> end]"), TRUE);
+    pr_help_add(C_XSHA1, _("<sp> pathname [<sp> start <sp> end]"), TRUE);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA256) {
+    pr_help_add(C_XSHA256, _("<sp> pathname [<sp> start <sp> end]"), TRUE);
+  }
+
+  if (digest_algos & DIGEST_ALGO_SHA512) {
+    pr_help_add(C_XSHA512, _("<sp> pathname [<sp> start <sp> end]"), TRUE);
+  }
+}
+
+/* Configuration handlers
+ */
+
+/* Usage: DigestAlgorithms algo1 ... */
+MODRET set_digestalgorithms(cmd_rec *cmd) {
+  config_rec *c;
+  unsigned long algos = 0UL;
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL|CONF_ANON);
+
+  /* We need at least ONE algorithm. */
+  if (cmd->argc < 2) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  if (strcasecmp(cmd->argv[1], "all") == 0) {
+    algos = DIGEST_DEFAULT_ALGOS;
+
+  } else {
+    register unsigned int i;
+
+    for (i = 1; i < cmd->argc; i++) {
+      if (strcasecmp(cmd->argv[i], "crc32") == 0) {
+        algos |= DIGEST_ALGO_CRC32;
+
+      } else if (strcasecmp(cmd->argv[i], "md5") == 0) {
+#ifndef OPENSSL_NO_MD5
+        algos |= DIGEST_ALGO_MD5;
+#else
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_MD5 */
+
+      } else if (strcasecmp(cmd->argv[i], "sha1") == 0) {
+#ifndef OPENSSL_NO_SHA
+        algos |= DIGEST_ALGO_SHA1;
+#else
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_SHA */
+
+      } else if (strcasecmp(cmd->argv[i], "sha256") == 0) {
+#ifndef OPENSSL_NO_SHA256
+        algos |= DIGEST_ALGO_SHA256;
+#else
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_SHA256 */
+
+      } else if (strcasecmp(cmd->argv[i], "sha512") == 0) {
+#ifndef OPENSSL_NO_SHA512
+        algos |= DIGEST_ALGO_SHA512;
+#else
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_SHA512 */
+
+      } else {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+          "unknown/unsupported DigestAlgorithm: ", cmd->argv[i], NULL));
+      }
+    }
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = algos;
+  c->flags |= CF_MERGEDOWN;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DigestCache on|off|"size" count ["maxAge" age] */
+MODRET set_digestcache(cmd_rec *cmd) {
+  register unsigned int i;
+  config_rec *c;
+
+  if (cmd->argc < 2 ||
+      cmd->argc > 5) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL|CONF_ANON);
+
+  if (cmd->argc == 2) {
+    int caching = -1;
+
+    caching = get_boolean(cmd, 1);
+    if (caching == -1) {
+      CONF_ERROR(cmd, "expected Boolean parameter");
+    }
+
+    c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+    c->argv[0] = palloc(c->pool, sizeof(int));
+    *((int *) c->argv[0]) = caching;
+    c->argv[1] = palloc(c->pool, sizeof(unsigned int));
+    *((unsigned int *) c->argv[1]) = DIGEST_CACHE_DEFAULT_SIZE;
+    c->argv[2] = palloc(c->pool, sizeof(unsigned int));
+    *((unsigned int *) c->argv[2]) = DIGEST_CACHE_DEFAULT_MAX_AGE;
+    c->flags |= CF_MERGEDOWN;
+
+    return PR_HANDLED(cmd);
+  }
+
+  c = add_config_param(cmd->argv[0], 3, NULL, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = TRUE;
+  c->argv[1] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[1]) = DIGEST_CACHE_DEFAULT_SIZE;
+  c->argv[2] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[2]) = DIGEST_CACHE_DEFAULT_MAX_AGE;
+  c->flags |= CF_MERGEDOWN;
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strncasecmp(cmd->argv[i], "size", 5) == 0) {
+      long size;
+      char *ptr = NULL;
+
+      if (i+1 == cmd->argc) {
+        CONF_ERROR(cmd, "wrong number of parameters");
+      }
+
+      size = strtol(cmd->argv[i+1], &ptr, 10);
+      if (ptr && *ptr) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid cache size: ",
+          cmd->argv[i+1], NULL));
+      }
+
+      if (size < 1) {
+        CONF_ERROR(cmd, "cache size must be greater than 0");
+      }
+
+      *((unsigned int *) c->argv[1]) = (unsigned int) size;
+      i++;
+
+    } else if (strncasecmp(cmd->argv[i], "maxAge", 7) == 0) {
+      int max_age;
+
+      if (i+1 == cmd->argc) {
+        CONF_ERROR(cmd, "wrong number of parameters");
+      }
+
+      if (pr_str_get_duration(cmd->argv[i+1], &max_age) < 0) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid max age: ",
+          cmd->argv[i+1], NULL));
+      }
+
+      if (max_age < 1) {
+        CONF_ERROR(cmd, "maxAge parameter must be greater than 1");
+      }
+
+      *((unsigned int *) c->argv[2]) = max_age;
+      i++;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown DigestCache parameter: ",
+        cmd->argv[i], NULL));
+    }
+  }
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DigestDefaultAlgorithm algo */
+MODRET set_digestdefaultalgo(cmd_rec *cmd) {
+  config_rec *c;
+  const char *algo_name;
+  unsigned long algo = 0UL;
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
+  CHECK_ARGS(cmd, 1);
+
+  algo_name = cmd->argv[1];
+
+  if (strcasecmp(algo_name, "crc32") == 0) {
+    algo = DIGEST_ALGO_CRC32;
+
+  } else if (strcasecmp(algo_name, "md5") == 0) {
+#ifndef OPENSSL_NO_MD5
+    algo = DIGEST_ALGO_MD5;
+#else
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", algo_name, "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_MD5 */
+
+  } else if (strcasecmp(algo_name, "sha1") == 0) {
+#ifndef OPENSSL_NO_SHA
+    algo = DIGEST_ALGO_SHA1;
+#else
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", algo_name, "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_SHA */
+
+  } else if (strcasecmp(algo_name, "sha256") == 0) {
+#ifndef OPENSSL_NO_SHA256
+    algo = DIGEST_ALGO_SHA256;
+#else
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", algo_name, "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_SHA256 */
+
+  } else if (strcasecmp(algo_name, "sha512") == 0) {
+#ifndef OPENSSL_NO_SHA512
+    algo = DIGEST_ALGO_SHA512;
+#else
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", algo_name, "' DigestAlgorithm", NULL));
+#endif /* OPENSSL_NO_SHA512 */
+
+  } else {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+      "unknown/unsupported DigestAlgorithm: ", algo_name, NULL));
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = algo;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DigestEnable on|off */
+MODRET set_digestenable(cmd_rec *cmd) {
+  int enable = -1;
+  config_rec *c;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_DIR|CONF_DYNDIR);
+
+  enable = get_boolean(cmd, 1);
+  if (enable == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = enable;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DigestEngine on|off */
+MODRET set_digestengine(cmd_rec *cmd) {
+  int engine = -1;
+  config_rec *c;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
+
+  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] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = engine;
+
+  c->flags |= CF_MERGEDOWN;
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DigestMaxSize len */
+MODRET set_digestmaxsize(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  const char *num, *units = "";
+  off_t max_size;
+
+  if (cmd->argc < 2 ||
+      cmd->argc > 3) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL|CONF_ANON);
+
+  /* Handle "DigestMaxSize none|off" by using a value of zero. */
+  if (cmd->argc == 2 &&
+      get_boolean(cmd, 1) == FALSE) {
+    c = add_config_param(cmd->argv[0], 1, NULL);
+    c->argv[0] = pcalloc(c->pool, sizeof(off_t));
+    c->flags |= CF_MERGEDOWN;
+    return PR_HANDLED(cmd);
+  }
+
+  num = cmd->argv[1];
+  if (cmd->argc == 3) {
+    units = cmd->argv[2];
+  }
+
+  if (pr_str_get_nbytes(num, units, &max_size) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "badly formatted size value: ",
+      num, units, NULL));
+  }
+
+  if (max_size == 0) {
+    CONF_ERROR(cmd, "requires a value greater than zero");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(off_t));
+  *((off_t *) c->argv[0]) = max_size;
+  c->flags |= CF_MERGEDOWN;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DigestOptions opt1 ... */
+MODRET set_digestoptions(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "NoTransferCache") == 0) {
+      opts |= DIGEST_OPT_NO_TRANSFER_CACHE;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown DigestOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
+static int check_digest_max_size(off_t len) {
+  config_rec *c;
+  off_t max_size;
+
+  c = find_config(CURRENT_CONF, CONF_PARAM, "DigestMaxSize", FALSE);
+  if (c == NULL) {
+    return 0;
+  }
+
+  max_size = *((off_t *) c->argv[0]);
+  if (max_size == 0) {
+    /* Special sentinel value to "disable" any inherited configs. */
+    return 0;
+  }
+
+  if (len > max_size) {
+    pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+      ": %s requested len (%" PR_LU ") exceeds DigestMaxSize %" PR_LU
+      ", rejecting", session.curr_cmd, (pr_off_t) len, (pr_off_t) max_size);
+    errno = EPERM;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int can_digest_file(pool *p, const char *path, off_t start, size_t len,
+    struct stat *st) {
+  config_rec *d;
+  char *dir_path, *ptr;
+
+  if (!S_ISREG(st->st_mode)) {
+    pr_trace_msg(trace_channel, 2, "path '%s' is not a regular file", path);
+    errno = EISDIR;
+    return -1;
+  }
+
+  if (start > 0) {
+    if (start > st->st_size) {
+      pr_log_debug(DEBUG3, MOD_DIGEST_VERSION
+        ": requested offset (%" PR_LU " bytes) for path '%s' exceeds file size "
+        "(%lu bytes)", (pr_off_t) start, path, (unsigned long) st->st_size);
+      errno = EINVAL;
+      return -1;
+    }
+  }
+
+  if (len > 0) {
+    if (((off_t) (start + len)) > st->st_size) {
+      pr_log_debug(DEBUG3, MOD_DIGEST_VERSION
+        ": requested offset/length (offset %" PR_LU " bytes, length %lu bytes) "
+        "for path '%s' exceeds file size (%lu bytes)", (pr_off_t) start,
+        (unsigned long) len, path, (unsigned long) st->st_size);
+      errno = EINVAL;
+      return -1;
+    }
+  }
+
+  /* Check for the "DigestEnable off" for the directory containing this file.
+   * Make sure we check any possible .ftpaccess files in the directory which
+   * might themselves contain a DigestEnable configuration.
+   */
+  ptr = strrchr(path, '/');
+  if (ptr == NULL ||
+      ptr == path) {
+    /* Note that this check for the last '/' character should NEVER fail; we
+     * should always be given the full path here.
+     *
+     * Also, if not NULL, the last '/' should NEVER be the first character in
+     * the given path, as it means the path is a directory, and that case
+     * should be already handled/avoided above.
+     */
+    return 0;
+  }
+
+  dir_path = pstrndup(p, path, (ptr - path));
+
+  pr_trace_msg(trace_channel, 1, "checking for DigestEnable in '%s'", dir_path);
+  d = dir_match_path(p, dir_path);
+  if (d != NULL) {
+    config_rec *c;
+
+    c = find_config(d->subset, CONF_PARAM, "DigestEnable", FALSE);
+    if (c != NULL) {
+      int digest_enable;
+
+      digest_enable = *((int *) c->argv[0]);
+      if (digest_enable == FALSE) {
+        pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+          ": digest of '%s' denied by DigestEnable configuration", path);
+        errno = EPERM;
+        return -1;
+      }
+    }
+  }
+
+  return 0;
+}
+
+/* Note that this is implemented in a case-INSENSITIVE manner, in order to
+ * protect any unfortunate case-insensitive filesystems (such as HFS on
+ * Mac, even though it is case-preserving).
+ */
+static int blacklisted_file(const char *path) {
+  int res = FALSE;
+
+  if (strncasecmp("/dev/full", path, 10) == 0 ||
+      strncasecmp("/dev/null", path, 10) == 0 ||
+      strncasecmp("/dev/random", path, 12) == 0 ||
+      strncasecmp("/dev/urandom", path, 13) == 0 ||
+      strncasecmp("/dev/zero", path, 10) == 0) {
+    res = TRUE;
+  }
+
+  return res;
+}
+
+static int compute_digest(pool *p, const char *path, off_t start, off_t len,
+    const EVP_MD *md, unsigned char *digest, unsigned int *digest_len,
+    time_t *mtime, void (*hash_progress_cb)(const char *, off_t)) {
+  int res, xerrno = 0;
+  pr_fh_t *fh;
+  struct stat st;
+  unsigned char *buf;
+  size_t bufsz, readsz, iter_count;
+  EVP_MD_CTX md_ctx;
+
+  fh = pr_fsio_open(path, O_RDONLY);
+  if (fh == NULL) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1, "unable to read '%s': %s", path,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = pr_fsio_fstat(fh, &st);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1, "unable to stat '%s': %s", path,
+      strerror(xerrno));
+    (void) pr_fsio_close(fh);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = can_digest_file(p, path, start, len, &st);
+  if (res < 0) {
+    xerrno = errno;
+    (void) pr_fsio_close(fh);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (mtime != NULL) {
+    /* Inform the caller of the last-mod-time for this file, for use in
+     * e.g caching.
+     */
+    *mtime = st.st_mtime;
+  }
+
+  /* Determine the optimal block size for reading. */
+  fh->fh_iosz = bufsz = st.st_blksize;
+
+  if (pr_fsio_lseek(fh, start, SEEK_SET) == (off_t) -1) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1, "error seeking to offset %" PR_LU
+      " in '%s': %s", (pr_off_t) start, path, strerror(xerrno));
+
+    (void) pr_fsio_close(fh);
+    errno = xerrno;
+    return -1;
+  }
+
+  EVP_MD_CTX_init(&md_ctx);
+  if (EVP_DigestInit_ex(&md_ctx, md, NULL) != 1) {
+    pr_log_debug(DEBUG1, MOD_DIGEST_VERSION
+      ": error preparing digest context: %s", get_errors());
+    (void) pr_fsio_close(fh);
+    EVP_MD_CTX_cleanup(&md_ctx);
+    errno = EPERM;
+    return -1;
+  }
+
+  buf = palloc(p, bufsz);
+
+  readsz = bufsz;
+  if ((off_t) readsz > len) {
+    readsz = len;
+  }
+
+  iter_count = 0;
+  res = pr_fsio_read(fh, (char *) buf, readsz);
+  xerrno = errno;
+
+  while (len > 0) {
+    iter_count++;
+
+    if (res < 0 &&
+        errno == EAGAIN) {
+      /* Add a small delay by treating this as EINTR. */
+      errno = xerrno = EINTR;
+    }
+
+    pr_signals_handle();
+
+    if (res < 0 &&
+        xerrno == EINTR) {
+      /* If we were interrupted, try again. */
+      res = pr_fsio_read(fh, (char *) buf, readsz);
+      continue;
+    }
+
+    if (EVP_DigestUpdate(&md_ctx, buf, res) != 1) {
+      pr_log_debug(DEBUG1, MOD_DIGEST_VERSION
+        ": error updating digest: %s", get_errors());
+    }
+
+    len -= res;
+
+    /* Every Nth iteration, invoke the progress callback. */
+    if ((iter_count % DIGEST_PROGRESS_NTH_ITER) == 0) {
+      (hash_progress_cb)(path, len);
+    }
+
+    readsz = bufsz;
+    if ((off_t) readsz > len) {
+      readsz = len;
+    }
+
+    res = pr_fsio_read(fh, (char *) buf, readsz);
+    xerrno = errno;
+  }
+
+  (void) pr_fsio_close(fh);
+
+  if (len != 0) {
+    EVP_MD_CTX_cleanup(&md_ctx);
+    pr_log_debug(DEBUG3, MOD_DIGEST_VERSION
+      ": failed to read all %" PR_LU " bytes of '%s' (premature EOF?)",
+      (pr_off_t) len, path);
+    errno = EIO;
+    return -1;
+  }
+
+  if (EVP_DigestFinal_ex(&md_ctx, digest, digest_len) != 1) {
+    pr_log_debug(DEBUG1, MOD_DIGEST_VERSION
+      ": error finishing digest: %s", get_errors());
+    EVP_MD_CTX_cleanup(&md_ctx);
+    errno = EPERM;
+    return -1;
+  }
+
+  EVP_MD_CTX_cleanup(&md_ctx);
+  return 0;
+}
+
+static const EVP_MD *get_algo_md(unsigned long algo) {
+  const EVP_MD *md = NULL;
+
+  switch (algo) {
+    case DIGEST_ALGO_CRC32:
+      md = EVP_crc32();
+      break;
+
+#ifndef OPENSSL_NO_MD5
+    case DIGEST_ALGO_MD5:
+      md = EVP_md5();
+      break;
+#endif /* OPENSSL_NO_MD5 */
+
+#ifndef OPENSSL_NO_SHA1
+    case DIGEST_ALGO_SHA1:
+      md = EVP_sha1();
+      break;
+#endif /* OPENSSL_NO_SHA1 */
+
+#ifndef OPENSSL_NO_SHA256
+    case DIGEST_ALGO_SHA256:
+      md = EVP_sha256();
+      break;
+#endif /* OPENSSL_NO_SHA256 */
+
+#ifndef OPENSSL_NO_SHA512
+    case DIGEST_ALGO_SHA512:
+      md = EVP_sha512();
+      break;
+#endif /* OPENSSL_NO_SHA512 */
+
+    default:
+      errno = ENOENT;
+      break;
+  }
+
+  return md;
+}
+
+static const char *get_algo_name(unsigned long algo, int flags) {
+  const char *algo_name = "(unknown)";
+
+  switch (algo) {
+    case DIGEST_ALGO_CRC32:
+      algo_name = "CRC32";
+      break;
+
+    case DIGEST_ALGO_MD5:
+      algo_name = "MD5";
+      break;
+
+    case DIGEST_ALGO_SHA1:
+      if (flags & DIGEST_ALGO_FL_IANA_STYLE) {
+        algo_name = "SHA-1";
+
+      } else {
+        algo_name = "SHA1";
+      }
+      break;
+
+    case DIGEST_ALGO_SHA256:
+      if (flags & DIGEST_ALGO_FL_IANA_STYLE) {
+        algo_name = "SHA-256";
+
+      } else {
+        algo_name = "SHA256";
+      }
+      break;
+
+    case DIGEST_ALGO_SHA512:
+      if (flags & DIGEST_ALGO_FL_IANA_STYLE) {
+        algo_name = "SHA-512";
+
+      } else {
+        algo_name = "SHA512";
+      }
+      break;
+
+    default:
+      errno = ENOENT;
+      break;
+  }
+
+  return algo_name;
+}
+
+static pr_table_t *get_cache(unsigned long algo) {
+  pr_table_t *cache = NULL;
+
+  switch (algo) {
+    case DIGEST_ALGO_CRC32:
+      cache = digest_crc32_tab;
+      break;
+
+    case DIGEST_ALGO_MD5:
+      cache = digest_md5_tab;
+      break;
+
+    case DIGEST_ALGO_SHA1:
+      cache = digest_sha1_tab;
+      break;
+
+    case DIGEST_ALGO_SHA256:
+      cache = digest_sha256_tab;
+      break;
+
+    case DIGEST_ALGO_SHA512:
+      cache = digest_sha512_tab;
+      break;
+
+    default:
+      pr_trace_msg(trace_channel, 4,
+        "unable to determine cache for %s digest", get_algo_name(algo, 0));
+      errno = EINVAL;
+      return NULL;
+  }
+
+  if (cache == NULL) {
+    errno = ENOENT;
+  }
+
+  return cache;
+}
+
+static unsigned int get_cache_size(void) {
+  int res;
+  unsigned int cache_size = 0;
+
+  if (digest_caching == FALSE) {
+    return 0;
+  }
+
+  res = pr_table_count(digest_crc32_tab);
+  if (res >= 0) {
+    cache_size += res;
+  }
+
+  res = pr_table_count(digest_md5_tab);
+  if (res >= 0) {
+    cache_size += res;
+  }
+
+  res = pr_table_count(digest_sha1_tab);
+  if (res >= 0) {
+    cache_size += res;
+  }
+
+  res = pr_table_count(digest_sha256_tab);
+  if (res >= 0) {
+    cache_size += res;
+  }
+
+  res = pr_table_count(digest_sha512_tab);
+  if (res >= 0) {
+    cache_size += res;
+  }
+
+  return cache_size;
+}
+
+/* Format the keys for the in-memory caches as:
+ *  "<path>@<mtime>,<start>+<len>"
+ */
+static const char *get_key_for_cache(pool *p, const char *path, time_t mtime,
+    off_t start, size_t len) {
+  const char *key;
+  char mtime_str[256], start_str[256], len_str[256];
+
+  memset(mtime_str, '\0', sizeof(mtime_str));
+  snprintf(mtime_str, sizeof(mtime_str)-1, "%llu", (unsigned long long) mtime);
+
+  memset(start_str, '\0', sizeof(start_str));
+  snprintf(start_str, sizeof(start_str)-1, "%" PR_LU, (pr_off_t) start);
+
+  memset(len_str, '\0', sizeof(len_str));
+  snprintf(len_str, sizeof(len_str)-1, "%llu", (unsigned long long) len);
+
+  key = pstrcat(p, path, "@", mtime_str, ",", start_str, "+", len_str, NULL);
+  return key;
+}
+
+/* Order items in the cache keys list by mtime, for easier/faster searching
+ * for the keys to expire.
+ */
+static int cache_key_cmp(xasetmember_t *a, xasetmember_t *b) {
+  struct digest_cache_key *k1, *k2;
+
+  k1 = (struct digest_cache_key *) a;
+  k2 = (struct digest_cache_key *) b;
+
+  if (k1->mtime < k2->mtime) {
+    return -1;
+  }
+
+  if (k1->mtime > k2->mtime) {
+    return 1;
+  }
+
+  return 0;
+}
+
+static struct digest_cache_key *create_cache_key(pool *p, unsigned long algo,
+    const char *path, time_t mtime, off_t start, size_t len,
+    const char *hex_digest) {
+  int res;
+  struct digest_cache_key *cache_key;
+  pool *sub_pool;
+
+  sub_pool = make_sub_pool(digest_pool);
+  pr_pool_tag(sub_pool, "DigestCache entry");
+
+  cache_key = pcalloc(sub_pool, sizeof(struct digest_cache_key));
+  cache_key->pool = sub_pool;
+  cache_key->path = pstrdup(cache_key->pool, path);
+  cache_key->mtime = mtime;
+  cache_key->start = start;
+  cache_key->len = len;
+  cache_key->algo = algo;
+  cache_key->key = get_key_for_cache(cache_key->pool, path, mtime, start, len);
+  cache_key->hex_digest = pstrdup(cache_key->pool, hex_digest);
+
+  if (digest_cache_keys == NULL) {
+    digest_cache_keys = xaset_create(digest_pool, cache_key_cmp);
+  }
+
+  res = xaset_insert_sort(digest_cache_keys, (xasetmember_t *) cache_key, TRUE);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 12,
+      "error adding cache key '%s' to set: %s", cache_key->key,
+      strerror(errno));
+  }
+
+  return cache_key;
+}
+
+static struct digest_cache_key *find_cache_key(pool *p, unsigned long algo,
+    const char *path, time_t mtime, off_t start, off_t len) {
+  struct digest_cache_key *cache_key = NULL;
+
+  for (cache_key = (struct digest_cache_key *) digest_cache_keys->xas_list;
+       cache_key != NULL;
+       cache_key = cache_key->next) {
+    if (cache_key->algo != algo) {
+      continue;
+    }
+
+    if (cache_key->mtime != mtime) {
+      continue;
+    }
+
+    if (cache_key->start != start) {
+      continue;
+    }
+
+    if (cache_key->len != len) {
+      continue;
+    }
+
+    if (strcmp(cache_key->path, path) == 0) {
+      return cache_key;
+    }
+  }
+
+  errno = ENOENT;
+  return NULL;
+}
+
+static int destroy_cache_key(pool *p, unsigned long algo, const char *path,
+    time_t mtime, off_t start, off_t len) {
+  struct digest_cache_key *cache_key;
+  int res;
+
+  cache_key = find_cache_key(p, algo, path, mtime, start, len);
+  if (cache_key == NULL) {
+    return -1;
+  }
+
+  res = xaset_remove(digest_cache_keys, (xasetmember_t *) cache_key);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 12,
+      "error removing cache key '%s' from set: %s", cache_key->key,
+      strerror(errno));
+  }
+
+  destroy_pool(cache_key->pool);
+  return 0;
+}
+
+static int remove_cached_digest(pool *p, unsigned long algo, const char *path,
+    time_t mtime, off_t start, size_t len) {
+  const char *key;
+  pr_table_t *cache;
+
+  cache = get_cache(algo);
+  if (cache == NULL) {
+    return -1;
+  }
+
+  key = get_key_for_cache(p, path, mtime, start, len);
+  if (key == NULL) {
+    return -1;
+  }
+
+  if (pr_table_remove(cache, key, NULL) == NULL) {
+    return -1;
+  }
+
+  destroy_cache_key(p, algo, path, mtime, start, len);
+  return 0;
+}
+
+static int add_cached_digest(pool *p, cmd_rec *cmd, unsigned long algo,
+    const char *path, time_t mtime, off_t start, size_t len,
+    const char *hex_digest) {
+  int res;
+  struct digest_cache_key *cache_key;
+  pr_table_t *cache;
+  const char *algo_name;
+
+  if (digest_caching == FALSE) {
+    return 0;
+  }
+
+  cache = get_cache(algo);
+  if (cache == NULL) {
+    return -1;
+  }
+
+  cache_key = create_cache_key(p, algo, path, mtime, start, len, hex_digest);
+
+  /* Stash the algorith name, and digest, as notes. */
+  algo_name = get_algo_name(algo, 0);
+  if (pr_table_add(cmd->notes, "mod_digest.algo",
+      pstrdup(cmd->pool, algo_name), 0) <  0) {
+    pr_trace_msg(trace_channel, 3,
+      "error adding 'mod_digest.algo' note: %s", strerror(errno));
+  }
+
+  if (pr_table_add(cmd->notes, "mod_digest.digest",
+      pstrdup(cmd->pool, hex_digest), 0) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error adding 'mod_digest.digest' note: %s", strerror(errno));
+  }
+
+  res = pr_table_add(cache, cache_key->key, (void *) cache_key->hex_digest, 0);
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 12,
+      "cached digest '%s' for %s digest, key '%s'", hex_digest,
+      get_algo_name(algo, 0), cache_key->key);
+  }
+
+  return res;
+}
+
+static char *get_cached_digest(pool *p, unsigned long algo, const char *path,
+    time_t mtime, off_t start, size_t len) {
+  const char *algo_name, *key;
+  pr_table_t *cache;
+  const void *val;
+
+  if (digest_caching == FALSE) {
+    errno = ENOENT;
+    return NULL;
+  }
+
+  cache = get_cache(algo);
+  if (cache == NULL) {
+    return NULL;
+  }
+
+  key = get_key_for_cache(p, path, mtime, start, len);
+  if (key == NULL) {
+    return NULL;
+  }
+
+  algo_name = get_algo_name(algo, 0);
+
+  pr_trace_msg(trace_channel, 19,
+    "checking for cached %s digest using key '%s'", algo_name, key);
+
+  val = pr_table_get(cache, key, NULL);
+  if (val != NULL) {
+    char *hex_digest;
+    time_t now;
+
+    /* We know that there's a key there; check to see if it should be
+     * expired.
+     */
+    time(&now);
+
+    if (now > (mtime + digest_cache_max_age)) {
+      pr_trace_msg(trace_channel, 12,
+        "cached digest for %s digest using key '%s' has expired, evicting",
+        algo_name, key);
+
+      if (remove_cached_digest(p, algo, path, mtime, start, len) < 0) {
+        pr_trace_msg(trace_channel, 15,
+          "error removing key '%s' from %s cache: %s", key, algo_name,
+          strerror(errno));
+      }
+
+      errno = ENOENT;
+      return NULL;
+    }
+
+    hex_digest = pstrdup(p, val);
+    pr_trace_msg(trace_channel, 12,
+      "using cached digest '%s' for %s digest, key '%s'", hex_digest,
+      algo_name, key);
+    return hex_digest;
+  }
+
+  errno = ENOENT;
+  return NULL;
+}
+
+static int digest_cache_expiry_cb(CALLBACK_FRAME) {
+  struct digest_cache_key *cache_key;
+  time_t now;
+
+  if (digest_cache_keys == NULL ||
+      digest_cache_keys->xas_list == NULL) {
+    /* Empty list; nothing to do. */
+    return 1;
+  }
+
+  time(&now);
+
+  /* We've ordered the keys in the list by mtime.  This means that once
+   * we see keys whose mtime has not exceed the max age, we can stop iterating.
+   */
+
+  for (cache_key = (struct digest_cache_key *) digest_cache_keys->xas_list;
+       cache_key != NULL;
+       cache_key = cache_key->next) {
+    if (now > (cache_key->mtime + digest_cache_max_age)) {
+      if (remove_cached_digest(digest_pool, cache_key->algo, cache_key->path,
+          cache_key->mtime, cache_key->start, cache_key->len) < 0) {
+        pr_trace_msg(trace_channel, 12,
+          "error removing cache key '%s' from set: %s", cache_key->key,
+         strerror(errno));
+
+      } else {
+        pr_trace_msg(trace_channel, 15,
+          "removed expired cache key '%s' from set", cache_key->key);
+      }
+
+    } else {
+      break;
+    }
+  }
+
+  /* Always restart the timer. */
+  return 1;
+}
+
+static int check_cache_size(cmd_rec *cmd) {
+  unsigned int cache_size;
+
+  /* Note: if caching is disabled, this condition will never be true. */
+  cache_size = get_cache_size();
+  if (cache_size >= digest_cache_max_size) {
+    int xerrno = EAGAIN;
+
+#ifdef EBUSY
+    /* This errno value may not be available on all platforms, but it is
+     * the most appropriate.
+     */
+    xerrno = EBUSY;
+#endif /* EBUSY */
+
+    pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+      ": cache size (%u) meets/exceeds max cache size (%u), "
+      "refusing %s command", cache_size, digest_cache_max_size,
+      (char *) cmd->argv[0]);
+
+    /* Generate an event, for benefit of any possible listeners
+     * (e.g. mod_ban).
+     */
+    pr_event_generate("mod_digest.max-cache-size", NULL);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+static char *get_digest(cmd_rec *cmd, unsigned long algo, const char *path,
+    time_t mtime, off_t start, size_t len, int flags,
+    void (*hash_progress_cb)(const char *, off_t)) {
+  int res;
+  const EVP_MD *md;
+  unsigned char *digest = NULL;
+  unsigned int digest_len;
+  char *hex_digest;
+  const char *algo_name;
+
+  hex_digest = get_cached_digest(cmd->tmp_pool, algo, path, mtime, start, len);
+
+  /* We check the cache size AFTER looking for a cached value, as part of
+   * looking for a cached value involves expiring the cached values at
+   * lookup time.
+   */
+  if (check_cache_size(cmd) < 0) {
+    return NULL;
+  }
+
+  if (hex_digest != NULL) {
+    /* Stash the algorith name, and digest, as notes. */
+
+    algo_name = get_algo_name(algo, 0);
+    if (pr_table_add(cmd->notes, "mod_digest.algo",
+        pstrdup(cmd->pool, algo_name), 0) <  0) {
+      pr_trace_msg(trace_channel, 3,
+        "error adding 'mod_digest.algo' note: %s", strerror(errno));
+    }
+
+    if (pr_table_add(cmd->notes, "mod_digest.digest",
+        pstrdup(cmd->pool, hex_digest), 0) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error adding 'mod_digest.digest' note: %s", strerror(errno));
+    }
+
+    if (flags & PR_STR_FL_HEX_USE_UC) {
+      register unsigned int i;
+
+      for (i = 0; hex_digest[i]; i++) {
+        hex_digest[i] = toupper((int) hex_digest[i]);
+      }
+    }
+
+    return hex_digest;
+  }
+
+  md = get_algo_md(algo);
+  digest_len = EVP_MD_size(md);
+  digest = palloc(cmd->tmp_pool, digest_len);
+
+  res = compute_digest(cmd->tmp_pool, path, start, len, md, digest,
+    &digest_len, &mtime, hash_progress_cb);
+  if (res < 0) {
+    return NULL;
+  }
+
+  hex_digest = pr_str_bin2hex(cmd->tmp_pool, digest, digest_len,
+    PR_STR_FL_HEX_USE_LC);
+
+  if (add_cached_digest(cmd->pool, cmd, algo, path, mtime, start, len,
+      hex_digest) < 0) {
+    pr_trace_msg(trace_channel, 8,
+      "error caching %s digest for path '%s': %s", get_algo_name(algo, 0),
+      path, strerror(errno));
+  }
+
+  /* Stash the algorith name, and digest, as notes. */
+
+  algo_name = get_algo_name(algo, 0);
+  if (pr_table_add(cmd->notes, "mod_digest.algo",
+      pstrdup(cmd->pool, algo_name), 0) <  0) {
+    pr_trace_msg(trace_channel, 3,
+      "error adding 'mod_digest.algo' note: %s", strerror(errno));
+  }
+
+  if (pr_table_add(cmd->notes, "mod_digest.digest",
+      pstrdup(cmd->pool, hex_digest), 0) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error adding 'mod_digest.digest' note: %s", strerror(errno));
+  }
+
+  if (flags & PR_STR_FL_HEX_USE_UC) {
+    register unsigned int i;
+
+    for (i = 0; hex_digest[i]; i++) {
+      hex_digest[i] = toupper((int) hex_digest[i]);
+    }
+  }
+
+  return hex_digest;
+}
+
+static void digest_progress_cb(const char *path, off_t remaining) {
+  int res;
+
+  pr_trace_msg(trace_channel, 19,
+    "%" PR_LU " bytes remaining for digesting of '%s'", (pr_off_t) remaining,
+    path);
+
+  /* Make sure to reset the idle timer, to prevent ProFTPD from timing out
+   * the session.
+   */
+  res = pr_timer_reset(PR_TIMER_IDLE, ANY_MODULE);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 15,
+      "error resetting TimeoutIdle timer: %s", strerror(errno));
+  }
+
+  /* AND write something on the control connection, to prevent any middleboxes
+   * from timing out the session.
+   */
+  pr_response_add(R_DUP, _("Computing..."));
+}
+
+static modret_t *digest_xcmd(cmd_rec *cmd, unsigned long algo) {
+  char *orig_path, *path;
+  struct stat st;
+
+  CHECK_CMD_MIN_ARGS(cmd, 2);
+
+  /* Note: no support for "XCMD path end" because it's implemented differently
+   * by other FTP servers, and is ambiguous (is the 'end' number the end, or
+   * the start, or...?).
+   */
+  if (cmd->argc == 3) {
+    pr_response_add_err(R_501, _("Invalid number of parameters"));
+    return PR_ERROR((cmd));
+  }
+
+  /* XXX Watch out for paths with spaces in them! */
+  path = orig_path = cmd->argv[1];
+
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int link_len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      link_len = dir_readlink(cmd->tmp_pool, path, link_path,
+        sizeof(link_path)-1, PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (link_len > 0) {
+        link_path[link_len] = '\0';
+        path = pstrdup(cmd->tmp_pool, link_path);
+      }
+    }
+  }
+
+  path = dir_realpath(cmd->tmp_pool, path);
+  if (path == NULL) {
+    int xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (blacklisted_file(path) == TRUE) {
+    pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+      ": rejecting request to checksum blacklisted special file '%s'", path);
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd->arg, strerror(EPERM));
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  if (!dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL)) {
+    int xerrno = EPERM;
+
+    pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+      ": %s denied by <Limit> configuration", (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
+    int xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (!S_ISREG(st.st_mode)) {
+    pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+      ": unable to handle %s for non-file path '%s'", (char *) cmd->argv[0],
+      path);
+    pr_response_add_err(R_550, _("%s: Not a regular file"), orig_path);
+    return PR_ERROR(cmd);
+
+  } else {
+    off_t len, start_pos, end_pos;
+
+    if (cmd->argc > 3) {
+      char *ptr = NULL;
+
+#ifdef HAVE_STRTOULL
+      start_pos = strtoull(cmd->argv[2], &ptr, 10);
+#else
+      start_pos = strtoul(cmd->argv[2], &ptr, 10);
+#endif /* HAVE_STRTOULL */
+
+      if (ptr && *ptr) {
+        pr_response_add_err(R_501,
+          _("%s requires a start greater than or equal to 0"),
+          (char *) cmd->argv[0]);
+        return PR_ERROR(cmd);
+      }
+
+      ptr = NULL;
+#ifdef HAVE_STRTOULL
+      end_pos = strtoull(cmd->argv[3], &ptr, 10);
+#else
+      end_pos = strtoul(cmd->argv[3], &ptr, 10);
+#endif /* HAVE_STRTOULL */
+
+      if (ptr && *ptr) {
+        pr_response_add_err(R_501,
+          _("%s requires an end greater than 0"), (char *) cmd->argv[0]);
+        return PR_ERROR(cmd);
+      }
+
+    } else {
+      start_pos = 0;
+      end_pos = st.st_size;
+    }
+
+    if (end_pos > st.st_size) {
+      pr_response_add_err(R_501,
+        _("%s: end exceeds file size"), (char *) cmd->argv[0]);
+      return PR_ERROR(cmd);
+    }
+
+    len = end_pos - start_pos;
+
+    if (start_pos >= end_pos) {
+      pr_response_add_err(R_501,
+        _("%s requires end (%" PR_LU ") greater than start (%" PR_LU ")"),
+        (char *) cmd->argv[0], (pr_off_t) end_pos, (pr_off_t) start_pos);
+      return PR_ERROR(cmd);
+    }
+
+    if (check_digest_max_size(len) < 0) {
+      pr_response_add_err(R_550, "%s: %s", orig_path, strerror(EPERM));
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
+      return PR_ERROR(cmd);
+    }
+
+    if (get_algo_md(algo) != NULL) {
+      char *hex_digest;
+
+      pr_response_add(R_250, _("Computing %s digest"), get_algo_name(algo, 0));
+      hex_digest = get_digest(cmd, algo, path, st.st_mtime, start_pos, len,
+        PR_STR_FL_HEX_USE_UC, digest_progress_cb);
+      if (hex_digest != NULL) {
+        pr_response_add(R_DUP, "%s", hex_digest);
+        return PR_HANDLED(cmd);
+      }
+
+      /* TODO: More detailed error message? */
+      pr_response_add_err(R_550, "%s: %s", orig_path, strerror(errno));
+
+    } else {
+      pr_response_add_err(R_550, _("%s: Hash algorithm not available"),
+        (char *) cmd->argv[0]);
+    }
+  }
+
+  return PR_ERROR(cmd);
+}
+
+/* Command handlers
+ */
+
+MODRET digest_hash(cmd_rec *cmd) {
+  int xerrno = 0;
+  char *error_code = NULL, *orig_path = NULL, *path = NULL, *hex_digest = NULL;
+  struct stat st;
+  off_t len, start_pos, end_pos;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  CHECK_CMD_MIN_ARGS(cmd, 2);
+
+  path = orig_path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int link_len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      link_len = dir_readlink(cmd->tmp_pool, path, link_path,
+        sizeof(link_path)-1, PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (link_len > 0) {
+        link_path[link_len] = '\0';
+        path = pstrdup(cmd->tmp_pool, link_path);
+      }
+    }
+  }
+
+  path = dir_realpath(cmd->tmp_pool, path);
+  if (path == NULL) {
+    xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (blacklisted_file(path) == TRUE) {
+    pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+      ": rejecting request to checksum blacklisted special file '%s'", path);
+    pr_response_add_err(R_556, "%s: %s", (char *) cmd->arg, strerror(EPERM));
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  if (!dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL)) {
+    xerrno = EPERM;
+
+    pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+      ": %s denied by <Limit> configuration", (char *) cmd->argv[0]);
+    pr_response_add_err(R_552, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
+    xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (!S_ISREG(st.st_mode)) {
+    pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+      ": unable to handle %s for non-file path '%s'", (char *) cmd->argv[0],
+      path);
+    pr_response_add_err(R_553, _("%s: Not a regular file"), orig_path);
+    return PR_ERROR(cmd);
+  }
+
+  start_pos = 0;
+  end_pos = st.st_size;
+  len = end_pos - start_pos;
+
+  if (check_digest_max_size(len) < 0) {
+    pr_response_add_err(R_556, "%s: %s", orig_path, strerror(EPERM));
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  pr_trace_msg(trace_channel, 14, "%s: using %s algorithm on path '%s'",
+    (char *) cmd->argv[0], get_algo_name(digest_hash_algo, 0), path);
+
+  pr_response_add(R_213, _("Computing %s digest"),
+    get_algo_name(digest_hash_algo, DIGEST_ALGO_FL_IANA_STYLE));
+  hex_digest = get_digest(cmd, digest_hash_algo, path, st.st_mtime, start_pos,
+    len, PR_STR_FL_HEX_USE_LC, digest_progress_cb);
+  xerrno = errno;
+
+  if (hex_digest != NULL) {
+    pr_response_add(R_DUP, "%s %" PR_LU "-%" PR_LU " %s %s",
+      get_algo_name(digest_hash_algo, DIGEST_ALGO_FL_IANA_STYLE),
+      (pr_off_t) start_pos, (pr_off_t) end_pos, hex_digest, orig_path);
+    return PR_HANDLED(cmd);
+  }
+
+  switch (xerrno) {
+#ifdef EBUSY
+    case EBUSY:
+#endif
+    case EAGAIN:
+      /* The HASH draft recommends using 450 for these cases. */
+      error_code = R_450;
+      break;
+
+    case EISDIR:
+      /* The HASH draft recommends using 553 for these cases. */
+      error_code = R_553;
+      break;
+
+    case EPERM:
+      /* This can happen if the directory is blocked via "DigestEnable off". */
+      error_code = R_552;
+      break;
+
+    default:
+      error_code = R_550;
+      break;
+  }
+
+  /* TODO: More detailed error message? */
+  pr_response_add_err(error_code, "%s: %s", orig_path, strerror(xerrno));
+
+  pr_cmd_set_errno(cmd, xerrno);
+  errno = xerrno;
+  return PR_ERROR(cmd);
+}
+
+MODRET digest_opts_hash(cmd_rec *cmd) {
+  char *algo_name;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (cmd->argc > 2) {
+    pr_response_add_err(R_501, _("OPTS HASH: Wrong number of parameters"));
+    return PR_ERROR(cmd);
+  }
+
+  if (cmd->argc == 1) {
+    int flags = DIGEST_ALGO_FL_IANA_STYLE;
+
+    /* Client is querying the current hash algorithm */
+    pr_response_add(R_200, "%s", get_algo_name(digest_hash_algo, flags));
+    return PR_HANDLED(cmd);
+  }
+
+  /* Client is setting/changing the current hash algorithm. */
+
+  algo_name = cmd->argv[1];
+
+  if (strcasecmp(algo_name, "CRC32") == 0) {
+    if (digest_algos & DIGEST_ALGO_CRC32) {
+      digest_hash_algo = DIGEST_ALGO_CRC32;
+      digest_hash_md = get_algo_md(digest_hash_algo);
+
+    } else {
+      pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
+      return PR_ERROR(cmd);
+    }
+
+#ifndef OPENSSL_NO_MD5
+  } else if (strcasecmp(algo_name, "MD5") == 0) {
+    if (digest_algos & DIGEST_ALGO_MD5) {
+      digest_hash_algo = DIGEST_ALGO_MD5;
+      digest_hash_md = get_algo_md(digest_hash_algo);
+
+    } else {
+      pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
+      return PR_ERROR(cmd);
+    }
+#endif /* OPENSSL_NO_MD5 */
+
+#ifndef OPENSSL_NO_SHA1
+  } else if (strcasecmp(algo_name, "SHA-1") == 0) {
+    if (digest_algos & DIGEST_ALGO_SHA1) {
+      digest_hash_algo = DIGEST_ALGO_SHA1;
+      digest_hash_md = get_algo_md(digest_hash_algo);
+
+    } else {
+      pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
+      return PR_ERROR(cmd);
+    }
+#endif /* OPENSSL_NO_SHA1 */
+
+#ifndef OPENSSL_NO_SHA256
+  } else if (strcasecmp(algo_name, "SHA-256") == 0) {
+    if (digest_algos & DIGEST_ALGO_SHA256) {
+      digest_hash_algo = DIGEST_ALGO_SHA256;
+      digest_hash_md = get_algo_md(digest_hash_algo);
+
+    } else {
+      pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
+      return PR_ERROR(cmd);
+    }
+#endif /* OPENSSL_NO_SHA256 */
+
+#ifndef OPENSSL_NO_SHA512
+  } else if (strcasecmp(algo_name, "SHA-512") == 0) {
+    if (digest_algos & DIGEST_ALGO_SHA512) {
+      digest_hash_algo = DIGEST_ALGO_SHA512;
+      digest_hash_md = get_algo_md(digest_hash_algo);
+
+    } else {
+      pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
+      return PR_ERROR(cmd);
+    }
+#endif /* OPENSSL_NO_SHA512 */
+
+  } else {
+    pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
+    return PR_ERROR(cmd);
+  }
+
+  digest_hash_feat_remove();
+  digest_hash_feat_add(cmd->tmp_pool);
+
+  pr_response_add(R_200, "%s", algo_name);
+  return PR_HANDLED(cmd);
+}
+
+MODRET digest_pre_retr(cmd_rec *cmd) {
+  config_rec *c;
+  const char *proto;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_caching == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (session.sf_flags & SF_ASCII) {
+    pr_trace_msg(trace_channel, 19,
+      "%s: ASCII mode transfer (TYPE A) in effect, not computing/caching "
+      "opportunistic digest for download", (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_opts & DIGEST_OPT_NO_TRANSFER_CACHE) {
+    pr_trace_msg(trace_channel, 19,
+      "%s: NoTransferCache DigestOption in effect, not computing/caching "
+      "opportunistic digest for download", (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  if (session.restart_pos > 0) {
+    pr_trace_msg(trace_channel, 12,
+      "REST %" PR_LU " sent before %s, declining to compute transfer digest",
+      (pr_off_t) session.restart_pos, (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  proto = pr_session_get_protocol(0);
+  if (strcasecmp(proto, "ftp") == 0 ||
+      strcasecmp(proto, "ftps") == 0) {
+    unsigned char use_sendfile = TRUE;
+
+    /* If UseSendfile is in effect, then we cannot watch the outbound traffic.
+     * Note that UseSendfile is enabled by default.
+     */
+    c = find_config(CURRENT_CONF, CONF_PARAM, "UseSendfile", FALSE);
+    if (c != NULL) {
+      use_sendfile = *((unsigned char *) c->argv[0]);
+    }
+
+    if (use_sendfile) {
+      pr_trace_msg(trace_channel, 12,
+        "UseSendfile in effect, declining to compute digest for %s transfer",
+        (char *) cmd->argv[0]);
+      return PR_DECLINED(cmd);
+    }
+  }
+
+  digest_cache_xfer_ctx = EVP_MD_CTX_create();
+  if (EVP_DigestInit_ex(digest_cache_xfer_ctx, digest_hash_md, NULL) != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "error preparing %s digest: %s", get_algo_name(digest_hash_algo, 0),
+      get_errors());
+    EVP_MD_CTX_destroy(digest_cache_xfer_ctx);
+    digest_cache_xfer_ctx = NULL;
+
+  } else {
+    pr_event_register(&digest_module, "core.data-write", digest_data_xfer_ev,
+      digest_cache_xfer_ctx);
+    pr_event_register(&digest_module, "mod_sftp.sftp.data-write",
+      digest_data_xfer_ev, digest_cache_xfer_ctx);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET digest_log(cmd_rec *cmd) {
+  const char *algo_name;
+  unsigned char *digest;
+  unsigned int digest_len;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0) {
+    pr_event_unregister(&digest_module, "core.data-write", NULL);
+    pr_event_unregister(&digest_module, "mod_sftp.sftp.data-write", NULL);
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0) {
+    pr_event_unregister(&digest_module, "core.data-read", NULL);
+    pr_event_unregister(&digest_module, "mod_sftp.sftp.data-read", NULL);
+
+  } else {
+    /* Not interested in this command. */
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_caching == FALSE ||
+      (digest_opts & DIGEST_OPT_NO_TRANSFER_CACHE)) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_cache_xfer_ctx == NULL) {
+    return PR_DECLINED(cmd);
+  }
+
+  algo_name = get_algo_name(digest_hash_algo, 0);
+  digest_len = EVP_MD_size(digest_hash_md);
+  digest = palloc(cmd->tmp_pool, digest_len);
+
+  if (EVP_DigestFinal_ex(digest_cache_xfer_ctx, digest, &digest_len) != 1) {
+    pr_trace_msg(trace_channel, 1,
+      "error finishing %s digest for %s: %s", algo_name,
+      (char *) cmd->argv[0], get_errors());
+
+  } else {
+    int res;
+    struct stat st;
+    const char *path;
+
+    path = session.xfer.path;
+    pr_fs_clear_cache2(path);
+    res = pr_fsio_stat(path, &st);
+    if (res == 0) {
+      char *hex_digest;
+      off_t start, len;
+      time_t mtime;
+
+      hex_digest = pr_str_bin2hex(cmd->tmp_pool, digest, digest_len,
+        PR_STR_FL_HEX_USE_LC);
+
+      mtime = st.st_mtime;
+      start = 0;
+      len = st.st_size;
+
+      if (add_cached_digest(cmd->pool, cmd, digest_hash_algo, path, mtime,
+          start, len, hex_digest) < 0) {
+        pr_trace_msg(trace_channel, 8,
+          "error caching %s digest for path '%s': %s", algo_name, path,
+          strerror(errno));
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 7,
+        "error checking '%s' post-%s: %s", path, (char *) cmd->argv[0],
+        strerror(errno));
+    }
+  }
+
+  EVP_MD_CTX_destroy(digest_cache_xfer_ctx);
+  digest_cache_xfer_ctx = NULL;
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET digest_log_err(cmd_rec *cmd) {
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0) {
+    pr_event_unregister(&digest_module, "core.data-write", NULL);
+    pr_event_unregister(&digest_module, "mod_sftp.sftp.data-write", NULL);
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0) {
+    pr_event_unregister(&digest_module, "core.data-read", NULL);
+    pr_event_unregister(&digest_module, "mod_sftp.sftp.data-read", NULL);
+
+  } else {
+    /* Not interested in this command. */
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_caching == FALSE ||
+      (digest_opts & DIGEST_OPT_NO_TRANSFER_CACHE)) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_cache_xfer_ctx != NULL) {
+    EVP_MD_CTX_destroy(digest_cache_xfer_ctx);
+    digest_cache_xfer_ctx = NULL;
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET digest_pre_appe(cmd_rec *cmd) {
+  int res;
+  struct stat st;
+  char *path;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_caching == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  /* If we are appending to an existing file, then do NOT compute the digest;
+   * we only do the opportunistic digest computation for complete files.  If
+   * file exists, but is zero length, then do proceed with the computation.
+   */
+
+  path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  if (path == NULL) {
+    return PR_DECLINED(cmd);
+  }
+
+  pr_fs_clear_cache2(path);
+  res = pr_fsio_stat(path, &st);
+  if (res == 0) {
+    if (!S_ISREG(st.st_mode)) {
+      /* Not a regular file. */
+      return PR_DECLINED(cmd);
+    }
+
+    if (st.st_size > 0) {
+      /* Not a zero length file. */
+      return PR_DECLINED(cmd);
+    }
+  }
+
+  if (session.sf_flags & SF_ASCII) {
+    pr_trace_msg(trace_channel, 19,
+      "%s: ASCII mode transfer (TYPE A) in effect, not computing/caching "
+      "opportunistic digest for upload", (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_opts & DIGEST_OPT_NO_TRANSFER_CACHE) {
+    pr_trace_msg(trace_channel, 19,
+      "%s: NoTransferCache DigestOption in effect, not computing/caching "
+      "opportunistic digest for upload", (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  /* Does REST + APPE even make any sense? */
+  if (session.restart_pos > 0) {
+    pr_trace_msg(trace_channel, 12,
+      "REST %" PR_LU " sent before %s, declining to compute transfer digest",
+      (pr_off_t) session.restart_pos, (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  digest_cache_xfer_ctx = EVP_MD_CTX_create();
+  if (EVP_DigestInit_ex(digest_cache_xfer_ctx, digest_hash_md, NULL) != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "error preparing %s digest: %s", get_algo_name(digest_hash_algo, 0),
+      get_errors());
+    EVP_MD_CTX_destroy(digest_cache_xfer_ctx);
+    digest_cache_xfer_ctx = NULL;
+
+  } else {
+    pr_event_register(&digest_module, "core.data-read", digest_data_xfer_ev,
+      digest_cache_xfer_ctx);
+    pr_event_register(&digest_module, "mod_sftp.sftp.data-read",
+      digest_data_xfer_ev, digest_cache_xfer_ctx);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET digest_pre_stor(cmd_rec *cmd) {
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_caching == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (session.sf_flags & SF_ASCII) {
+    pr_trace_msg(trace_channel, 19,
+      "%s: ASCII mode transfer (TYPE A) in effect, not computing/caching "
+      "opportunistic digest for upload", (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  if (digest_opts & DIGEST_OPT_NO_TRANSFER_CACHE) {
+    pr_trace_msg(trace_channel, 19,
+      "%s: NoTransferCache DigestOption in effect, not computing/caching "
+      "opportunistic digest for upload", (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  if (session.restart_pos > 0) {
+    pr_trace_msg(trace_channel, 12,
+      "REST %" PR_LU " sent before %s, declining to compute transfer digest",
+      (pr_off_t) session.restart_pos, (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  digest_cache_xfer_ctx = EVP_MD_CTX_create();
+  if (EVP_DigestInit_ex(digest_cache_xfer_ctx, digest_hash_md, NULL) != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "error preparing %s digest: %s", get_algo_name(digest_hash_algo, 0),
+      get_errors());
+    EVP_MD_CTX_destroy(digest_cache_xfer_ctx);
+    digest_cache_xfer_ctx = NULL;
+
+  } else {
+    pr_event_register(&digest_module, "core.data-read", digest_data_xfer_ev,
+      digest_cache_xfer_ctx);
+    pr_event_register(&digest_module, "mod_sftp.sftp.data-read",
+      digest_data_xfer_ev, digest_cache_xfer_ctx);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET digest_post_pass(cmd_rec *cmd) {
+  config_rec *c;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  c = find_config(CURRENT_CONF, CONF_PARAM, "DigestEngine", FALSE);
+  if (c != NULL) {
+    digest_engine = *((int *) c->argv[0]);
+  }
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+  
+  c = find_config(CURRENT_CONF, CONF_PARAM, "DigestAlgorithms", FALSE);
+  if (c != NULL) {
+    digest_algos = *((unsigned long *) c->argv[0]);
+  }
+
+  c = find_config(CURRENT_CONF, CONF_PARAM, "DigestCache", FALSE);
+  if (c != NULL) {
+    digest_caching = *((int *) c->argv[0]);
+    if (digest_caching == TRUE) {
+      digest_cache_max_size = *((unsigned int *) c->argv[1]);
+      digest_cache_max_age = *((unsigned int *) c->argv[2]);
+    }
+  }
+
+  if (digest_caching == TRUE) {
+    int timerno;
+
+    /* Register a timer for periodically scanning for expired cache entries
+     * to evict.
+     */
+    timerno = pr_timer_add(DIGEST_CACHE_EXPIRY_INTVL, -1, &digest_module,
+      digest_cache_expiry_cb, "DigestCache expiry");
+    if (timerno < 0) {
+      pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+        ": error adding timer for DigestCache expiration: %s", strerror(errno));
+    }
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET digest_md5(cmd_rec *cmd) {
+  int xerrno = 0;
+  char *error_code = NULL, *orig_path = NULL, *path = NULL, *hex_digest = NULL;
+  struct stat st;
+  off_t len, start_pos, end_pos;
+  unsigned long algo = DIGEST_ALGO_MD5;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!(digest_algos & algo)) {
+    pr_log_debug(DEBUG9, MOD_DIGEST_VERSION
+      ": unable to handle %s command: MD5 disabled by DigestAlgorithms",
+      (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  CHECK_CMD_MIN_ARGS(cmd, 2);
+
+  orig_path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  path = dir_realpath(cmd->tmp_pool, orig_path);
+  if (path == NULL) {
+    xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (blacklisted_file(path) == TRUE) {
+    pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+      ": rejecting request to checksum blacklisted special file '%s'", path);
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd->arg, strerror(EPERM));
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  if (!dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL)) {
+    xerrno = EPERM;
+
+    pr_log_debug(DEBUG8, MOD_DIGEST_VERSION
+      ": %s denied by <Limit> configuration", (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
+    xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (!S_ISREG(st.st_mode)) {
+    pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+      ": unable to handle %s for non-file path '%s'", (char *) cmd->argv[0],
+      path);
+    pr_response_add_err(R_504, _("%s: Not a regular file"), orig_path);
+    return PR_ERROR(cmd);
+  }
+
+  start_pos = 0;
+  end_pos = st.st_size;
+  len = end_pos - start_pos;
+
+  if (check_digest_max_size(len) < 0) {
+    pr_response_add_err(R_550, "%s: %s", orig_path, strerror(EPERM));
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  pr_trace_msg(trace_channel, 14, "%s: using %s algorithm on path '%s'",
+    (char *) cmd->argv[0], get_algo_name(algo, 0), path);
+
+  pr_response_add(R_251, _("Computing %s digest"), get_algo_name(algo, 0));
+  hex_digest = get_digest(cmd, algo, path, st.st_mtime, start_pos,
+    len, PR_STR_FL_HEX_USE_UC, digest_progress_cb);
+  xerrno = errno;
+
+  if (hex_digest != NULL) {
+    pr_response_add(R_DUP, "%s %s", orig_path, hex_digest);
+    return PR_HANDLED(cmd);
+  }
+
+  switch (xerrno) {
+    case EISDIR:
+      /* The MD5 draft recommends using 504 for these cases. */
+      error_code = R_504;
+      break;
+
+    default:
+      error_code = R_550;
+      break;
+  }
+
+  /* TODO: More detailed error message? */
+  pr_response_add_err(error_code, "%s: %s", orig_path, strerror(xerrno));
+
+  pr_cmd_set_errno(cmd, xerrno);
+  errno = xerrno;
+  return PR_ERROR(cmd);
+}
+
+MODRET digest_xcrc(cmd_rec *cmd) {
+  unsigned long algo = DIGEST_ALGO_CRC32;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!(digest_algos & algo)) {
+    pr_log_debug(DEBUG9, MOD_DIGEST_VERSION
+      ": unable to handle %s command: CRC32 disabled by DigestAlgorithms",
+      (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  return digest_xcmd(cmd, algo);
+}
+
+MODRET digest_xmd5(cmd_rec *cmd) {
+  unsigned long algo = DIGEST_ALGO_MD5;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!(digest_algos & algo)) {
+    pr_log_debug(DEBUG9, MOD_DIGEST_VERSION
+      ": unable to handle %s command: MD5 disabled by DigestAlgorithms",
+      (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  return digest_xcmd(cmd, algo);
+}
+
+MODRET digest_xsha1(cmd_rec *cmd) {
+  unsigned long algo = DIGEST_ALGO_SHA1;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!(digest_algos & algo)) {
+    pr_log_debug(DEBUG9, MOD_DIGEST_VERSION
+      ": unable to handle %s command: SHA1 disabled by DigestAlgorithms",
+      (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  return digest_xcmd(cmd, algo);
+}
+
+MODRET digest_xsha256(cmd_rec *cmd) {
+  unsigned long algo = DIGEST_ALGO_SHA256;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!(digest_algos & algo)) {
+    pr_log_debug(DEBUG9, MOD_DIGEST_VERSION
+      ": unable to handle %s command: SHA256 disabled by DigestAlgorithms",
+      (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  return digest_xcmd(cmd, algo);
+}
+
+MODRET digest_xsha512(cmd_rec *cmd) {
+  unsigned long algo = DIGEST_ALGO_SHA512;
+
+  if (digest_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!(digest_algos & algo)) {
+    pr_log_debug(DEBUG9, MOD_DIGEST_VERSION
+      ": unable to handle %s command: SHA512 disabled by DigestAlgorithms",
+      (char *) cmd->argv[0]);
+    return PR_DECLINED(cmd);
+  }
+
+  return digest_xcmd(cmd, algo);
+}
+
+/* Event listeners
+ */
+
+static void digest_data_xfer_ev(const void *event_data, void *user_data) {
+  const pr_buffer_t *pbuf;
+  EVP_MD_CTX *md_ctx;
+
+  md_ctx = user_data;
+  pbuf = event_data;
+
+  if (EVP_DigestUpdate(md_ctx, pbuf->buf, pbuf->buflen) != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "error updating %s digest: %s", get_algo_name(digest_hash_algo, 0),
+      get_errors());
+
+  } else {
+    pr_trace_msg(trace_channel, 19,
+      "updated %s digest with %lu bytes", get_algo_name(digest_hash_algo, 0),
+      (unsigned long) pbuf->buflen);
+  }
+}
+
+#if defined(PR_SHARED_MODULE)
+static void digest_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp((char *) event_data, "mod_digest.c") == 0) {
+    pr_event_unregister(&digest_module, NULL, NULL);
+  }
+}
+#endif /* PR_SHARED_MODULE */
+
+static void digest_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&digest_module, "core.session-reinit",
+    digest_sess_reinit_ev);
+
+  digest_engine = TRUE;
+  digest_caching = TRUE;
+  digest_cache_max_size = DIGEST_CACHE_DEFAULT_SIZE;
+  digest_cache_max_age = DIGEST_CACHE_DEFAULT_MAX_AGE;
+  digest_opts = DIGEST_DEFAULT_OPTS;
+  digest_algos = DIGEST_DEFAULT_ALGOS;
+  digest_hash_algo = DIGEST_ALGO_SHA1;
+  digest_hash_md = NULL;
+
+  res = digest_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&digest_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
+/* Initialization routines
+ */
+
+static int digest_init(void) {
+  digest_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(digest_pool, MOD_DIGEST_VERSION);
+
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&digest_module, "core.module-unload", digest_mod_unload_ev,
+    NULL);
+#endif /* PR_SHARED_MODULE */
+
+  return 0;
+}
+
+static int digest_sess_init(void) {
+  config_rec *c;
+
+  pr_event_register(&digest_module, "core.session-reinit",
+    digest_sess_reinit_ev, NULL);
+
+  c = find_config(main_server->conf, CONF_PARAM, "DigestEngine", FALSE);
+  if (c != NULL) {
+    digest_engine = *((int *) c->argv[0]);
+  }
+
+  if (digest_engine == FALSE) {
+    return 0;
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "DigestAlgorithms", FALSE);
+  if (c != NULL) {
+    digest_algos = *((unsigned long *) c->argv[0]);
+  }
+
+  /* Use the configured algorithms to determine our default HASH; it may be
+   * that SHA1 is disabled or not available via OpenSSL.
+   *
+   * Per the HASH Draft, the default HASH SHOULD be SHA1; if not, it should
+   * be a "stronger" HASH function.  Thus this ordering may not be what you
+   * would expect.
+   */
+
+  if (digest_algos & DIGEST_ALGO_SHA1) {
+    digest_hash_algo = DIGEST_ALGO_SHA1;
+
+  } else if (digest_algos & DIGEST_ALGO_SHA256) {
+    digest_hash_algo = DIGEST_ALGO_SHA256;
+
+  } else if (digest_algos & DIGEST_ALGO_SHA512) {
+    digest_hash_algo = DIGEST_ALGO_SHA512;
+
+  } else if (digest_algos & DIGEST_ALGO_MD5) {
+    digest_hash_algo = DIGEST_ALGO_MD5;
+
+  } else {
+    /* We are GUARANTEED to always be able to do CRC32. */
+    digest_hash_algo = DIGEST_ALGO_CRC32;
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "DigestDefaultAlgorithm",
+    FALSE);
+  if (c != NULL) {
+    unsigned long algo;
+
+    algo = *((unsigned long *) c->argv[0]);
+
+    /* It is possible that the the configured default algorithm does NOT
+     * appear in the algorithms list.  Assume that the algorithms list takes
+     * precedence, and ignore the default algo if it is not in the list.
+     */
+
+    if (digest_algos & algo) {
+      digest_hash_algo = algo;
+
+    } else {
+      pr_log_debug(DEBUG5, MOD_DIGEST_VERSION
+        ": DigestDefaultAlgorithm %s not allowed by DigestAlgorithms, ignoring",
+        get_algo_name(algo, 0));
+    }
+  }
+
+  digest_hash_md = get_algo_md(digest_hash_algo);
+
+  c = find_config(main_server->conf, CONF_PARAM, "DigestCache", FALSE);
+  if (c != NULL) {
+    digest_caching = *((int *) c->argv[0]);
+    if (digest_caching == TRUE) {
+      digest_cache_max_size = *((unsigned int *) c->argv[1]);
+      digest_cache_max_age = *((unsigned int *) c->argv[2]);
+    }
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "DigestOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    digest_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "DigestOptions", FALSE);
+  }
+
+  if (digest_caching == TRUE) {
+    digest_crc32_tab = pr_table_alloc(digest_pool, 0);
+    digest_md5_tab = pr_table_alloc(digest_pool, 0);
+    digest_sha1_tab = pr_table_alloc(digest_pool, 0);
+    digest_sha256_tab = pr_table_alloc(digest_pool, 0);
+    digest_sha512_tab = pr_table_alloc(digest_pool, 0);
+  }
+
+  digest_hash_feat_add(session.pool);
+  pr_help_add(C_HASH, _("<sp> pathname"), TRUE);
+
+  digest_x_feat_add(session.pool);
+  digest_x_help_add(session.pool);
+
+  return 0;
+}
+
+/* Module API tables
+ */
+
+static cmdtable digest_cmdtab[] = {
+  { CMD, C_HASH,	G_READ, digest_hash,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_OPTS"_HASH",	G_NONE,	digest_opts_hash,FALSE,FALSE },
+
+  { CMD, C_MD5,		G_READ, digest_md5,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_XCRC,	G_READ, digest_xcrc,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_XMD5,	G_READ, digest_xmd5,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_XSHA,	G_READ, digest_xsha1,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_XSHA1,	G_READ, digest_xsha1,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_XSHA256,	G_READ, digest_xsha256,	TRUE, FALSE, CL_READ|CL_INFO },
+  { CMD, C_XSHA512,	G_READ, digest_xsha512,	TRUE, FALSE, CL_READ|CL_INFO },
+
+  { POST_CMD,	C_PASS, G_NONE,	digest_post_pass, TRUE, FALSE },
+
+  /* Command handlers for opportunistic digest computation/caching.
+   * Note that we use C_ANY for better interoperability with e.g.
+   * mod_log/mod_sql, due to command dispatching precedence rules.
+   */
+  { PRE_CMD,	C_APPE, G_NONE, digest_pre_appe,	TRUE,	FALSE },
+  { PRE_CMD,	C_RETR, G_NONE, digest_pre_retr,	TRUE,	FALSE },
+  { PRE_CMD,	C_STOR,	G_NONE, digest_pre_stor,	TRUE,	FALSE },
+  { LOG_CMD,	C_ANY, 	G_NONE, digest_log,		FALSE,	FALSE },
+  { LOG_CMD_ERR,C_ANY,	G_NONE, digest_log_err,		FALSE,	FALSE },
+
+  { 0, NULL }
+};
+
+static conftable digest_conftab[] = {
+  { "DigestAlgorithms",		set_digestalgorithms,	NULL },
+  { "DigestCache",		set_digestcache,	NULL },
+  { "DigestDefaultAlgorithm",	set_digestdefaultalgo,	NULL },
+  { "DigestEnable",		set_digestenable,	NULL },
+  { "DigestEngine",		set_digestengine,	NULL },
+  { "DigestMaxSize",		set_digestmaxsize,	NULL },
+  { "DigestOptions",		set_digestoptions,	NULL },
+
+  { NULL }
+};
+
+module digest_module = {
+  NULL, NULL,
+
+  /* Module API version */
+  0x20,
+
+  /* Module name */
+  "digest",
+
+  /* Module configuration table */
+  digest_conftab,
+
+  /* Module command handler table */
+  digest_cmdtab,
+
+  /* Module auth handler table */
+  NULL,
+
+  /* Module initialization function */
+  digest_init,
+
+  /* Session initialization function */
+  digest_sess_init,
+
+  /* Module version */
+  MOD_DIGEST_VERSION
+};
diff --git a/contrib/mod_dnsbl/Makefile.in b/contrib/mod_dnsbl/Makefile.in
index 32f5e67..d381c2f 100644
--- a/contrib/mod_dnsbl/Makefile.in
+++ b/contrib/mod_dnsbl/Makefile.in
@@ -44,5 +44,5 @@ clean:
 	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).a $(MODULE_NAME).la *.o *.lo .libs/*.o
 
 dist: clean
-	$(RM) Makefile mod_dnsbl.h config.status config.cache config.log
+	$(RM) Makefile mod_dnsbl.h config.status config.cache config.log *.gcda *.gcno
 	-$(RM) -r .libs/ .git/ CVS/ RCS/
diff --git a/contrib/mod_dnsbl/configure b/contrib/mod_dnsbl/configure
index f6e5568..eeb0455 100755
--- a/contrib/mod_dnsbl/configure
+++ b/contrib/mod_dnsbl/configure
@@ -3200,7 +3200,7 @@ else
   { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 
 { echo "$as_me:$LINENO: checking for library containing strerror" >&5
@@ -3354,7 +3354,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3375,7 +3375,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3680,6 +3680,38 @@ _ACEOF
 fi
 
 
+
+# Check whether --with-includes was given.
+if test "${with_includes+set}" = set; then
+  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for ainclude in $ac_addl_includes; do
+      if test x"$ac_build_addl_includes" = x ; then
+        ac_build_addl_includes="-I$ainclude"
+      else
+        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
+      fi
+    done
+    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+
+fi
+
+
+
+# Check whether --with-libraries was given.
+if test "${with_libraries+set}" = set; then
+  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for alibdir in $ac_addl_libdirs; do
+      if test x"$ac_build_addl_libdirs" = x ; then
+        ac_build_addl_libdirs="-L$alibdir"
+      else
+        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
+      fi
+    done
+    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+fi
+
+
 { echo "$as_me:$LINENO: checking for ANSI C header files" >&5
 echo $ECHO_N "checking for ANSI C header files... $ECHO_C" >&6; }
 if test "${ac_cv_header_stdc+set}" = set; then
@@ -3748,7 +3780,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3769,7 +3801,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3999,38 +4031,6 @@ done
 
 
 
-# Check whether --with-includes was given.
-if test "${with_includes+set}" = set; then
-  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
-    for ainclude in $ac_addl_includes; do
-      if test x"$ac_build_addl_includes" = x ; then
-        ac_build_addl_includes="-I$ainclude"
-      else
-        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
-      fi
-    done
-    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
-
-fi
-
-
-
-# Check whether --with-libraries was given.
-if test "${with_libraries+set}" = set; then
-  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
-    for alibdir in $ac_addl_libdirs; do
-      if test x"$ac_build_addl_libdirs" = x ; then
-        ac_build_addl_libdirs="-L$alibdir"
-      else
-        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
-      fi
-    done
-    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
-
-fi
-
-
-
 ac_dnsbl_libs=""
 { echo "$as_me:$LINENO: checking for resolver symbols in libc" >&5
 echo $ECHO_N "checking for resolver symbols in libc... $ECHO_C" >&6; }
@@ -5345,7 +5345,7 @@ do
     cat >>$CONFIG_STATUS <<_ACEOF
     # First, check the format of the line:
     cat >"\$tmp/defines.sed" <<\\CEOF
-/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*/b def
+/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*\$/b def
 /^[	 ]*#[	 ]*define[	 ][	 ]*$ac_word_re[(	 ]/b def
 b
 :def
diff --git a/contrib/mod_dnsbl/configure.in b/contrib/mod_dnsbl/configure.in
index 5f9d16e..c70ef0b 100644
--- a/contrib/mod_dnsbl/configure.in
+++ b/contrib/mod_dnsbl/configure.in
@@ -1,3 +1,22 @@
+dnl ProFTPD - mod_dnsbl
+dnl Copyright (c) 2012-2015 TJ Saunders <tj at castaglia.org>
+dnl
+dnl This program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program; if not, write to the Free Software
+dnl Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+dnl
+dnl Process this file with autoconf to produce a configure script.
+
 AC_INIT(./mod_dnsbl.c)
 
 AC_CANONICAL_SYSTEM
@@ -10,9 +29,6 @@ AC_AIX
 AC_ISC_POSIX
 AC_MINIX
 
-AC_HEADER_STDC
-AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h)
-
 dnl Need to support/handle the --with-includes and --with-libraries options
 AC_ARG_WITH(includes,
   [AC_HELP_STRING(
@@ -46,6 +62,9 @@ AC_ARG_WITH(libraries,
     LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
   ])
 
+AC_HEADER_STDC
+AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h)
+
 dnl Check whether libc provides the DNS resolver symbols (e.g. *BSD/Mac OSX)
 dnl or not.  And if not, check whether we need to link directly with
 dnl /usr/lib/libresolv.a (32-bit) or /usr/lib64/libresolv.a (64-bit).
diff --git a/contrib/mod_dnsbl/mod_dnsbl.c b/contrib/mod_dnsbl/mod_dnsbl.c
index e92d8de..9859a5f 100644
--- a/contrib/mod_dnsbl/mod_dnsbl.c
+++ b/contrib/mod_dnsbl/mod_dnsbl.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_dnsbl -- a module for checking DNSBL (DNS Black Lists)
  *                       servers before allowing a connection
- *
- * Copyright (c) 2007-2013 TJ Saunders
+ * Copyright (c) 2007-2016 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
@@ -25,8 +24,6 @@
  *
  * This is mod_dnsbl, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_dnsbl.c,v 1.2 2013-10-13 16:48:08 castaglia Exp $
  */
 
 #include "mod_dnsbl.h"
@@ -42,11 +39,16 @@
 
 #define DNSBL_REASON_MAX_LEN		256
 
+module dnsbl_module;
+
 static int dnsbl_engine = FALSE;
 static int dnsbl_logfd = -1;
 
 static const char *trace_channel = "dnsbl";
 
+/* Necessary prototypes. */
+static int dnsbl_sess_init(void);
+
 typedef enum {
   DNSBL_POLICY_ALLOW_DENY,
   DNSBL_POLICY_DENY_ALLOW
@@ -159,7 +161,7 @@ static void lookup_reason(pool *p, const char *name) {
 }
 
 static int lookup_addr(pool *p, const char *addr, const char *domain) {
-  pr_netaddr_t *reject_addr = NULL;
+  const pr_netaddr_t *reject_addr = NULL;
   const char *name = pstrcat(p, addr, ".", domain, NULL);
 
   (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
@@ -184,6 +186,105 @@ static int lookup_addr(pool *p, const char *addr, const char *domain) {
   return 0;
 }
 
+static int dnsbl_reject_conn(void) {
+  config_rec *c;
+  pool *tmp_pool = NULL;
+  const char *rev_ip_addr = NULL;
+  int reject_conn = FALSE;
+  dnsbl_policy_e policy = DNSBL_POLICY_DENY_ALLOW;
+
+  c = find_config(main_server->conf, CONF_PARAM, "DNSBLPolicy", FALSE);
+  if (c) {
+    policy = *((dnsbl_policy_e *) c->argv[0]);
+  }
+
+  switch (policy) {
+    case DNSBL_POLICY_ALLOW_DENY:
+      pr_trace_msg(trace_channel, 8,
+        "using policy of allowing connections unless listed by DNSBLDomains");
+      reject_conn = FALSE;
+      break;
+
+    case DNSBL_POLICY_DENY_ALLOW:
+      pr_trace_msg(trace_channel, 8,
+        "using policy of rejecting connections unless listed by DNSBLDomains");
+      reject_conn = TRUE;
+      break;
+  }
+
+  tmp_pool = make_sub_pool(permanent_pool);
+  rev_ip_addr = get_reversed_addr(tmp_pool);
+  if (rev_ip_addr == NULL) {
+    (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
+      "client address '%s' is an IPv6 address, skipping",
+      pr_netaddr_get_ipstr(session.c->remote_addr));
+    destroy_pool(tmp_pool);
+    return -1;
+  }
+
+  switch (policy) {
+    /* For this policy, the connection will be allowed unless the connecting
+     * client is listed by any of the DNSBLDomain sites.
+     */
+    case DNSBL_POLICY_ALLOW_DENY: {
+      c = find_config(main_server->conf, CONF_PARAM, "DNSBLDomain", FALSE);
+      while (c) {
+        const char *domain;
+    
+        pr_signals_handle();
+
+        domain = c->argv[0];
+
+        if (lookup_addr(tmp_pool, rev_ip_addr, domain) < 0) {
+          (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
+            "client address '%s' is listed by DNSBLDomain '%s', rejecting "
+            "connection", pr_netaddr_get_ipstr(session.c->remote_addr), domain);
+          reject_conn = TRUE;
+          break;
+        }
+
+        c = find_config_next(c, c->next, CONF_PARAM, "DNSBLDomain", FALSE);
+      }
+
+      break;
+    }
+
+    /* For this policy, the connection will be NOT allowed unless the
+     * connecting client is listed by any of the DNSBLDomain sites.
+     */
+    case DNSBL_POLICY_DENY_ALLOW: {
+      c = find_config(main_server->conf, CONF_PARAM, "DNSBLDomain", FALSE);
+      while (c) {
+        const char *domain;
+
+        pr_signals_handle();
+
+        domain = c->argv[0];
+
+        if (lookup_addr(tmp_pool, rev_ip_addr, domain) < 0) {
+          (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
+            "client address '%s' is listed by DNSBLDomain '%s', allowing "
+            "connection", pr_netaddr_get_ipstr(session.c->remote_addr), domain);
+          reject_conn = FALSE;
+          break;
+        }
+    
+        c = find_config_next(c, c->next, CONF_PARAM, "DNSBLDomain", FALSE);
+      } 
+
+      break; 
+    }
+  }
+
+  destroy_pool(tmp_pool);
+
+  if (reject_conn) {
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
 /* Configuration handlers
  */
 
@@ -264,15 +365,35 @@ MODRET set_dnsblpolicy(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* Event listeners
+ */
+
 /* Initialization functions
  */
 
+static void dnsbl_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&dnsbl_module, "core.session-reinit",
+    dnsbl_sess_reinit_ev);
+
+  (void) close(dnsbl_logfd);
+  dnsbl_logfd = -1;
+
+  res = dnsbl_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&dnsbl_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 static int dnsbl_sess_init(void) {
   config_rec *c;
-  pool *tmp_pool = NULL;
-  const char *rev_ip_addr = NULL;
-  int reject_conn = FALSE;
-  dnsbl_policy_e policy = DNSBL_POLICY_DENY_ALLOW;
+
+  pr_event_register(&dnsbl_module, "core.session-reinit", dnsbl_sess_reinit_ev,
+    NULL);
 
   c = find_config(main_server->conf, CONF_PARAM, "DNSBLEngine", FALSE);
   if (c &&
@@ -314,92 +435,7 @@ static int dnsbl_sess_init(void) {
     }
   }
 
-  c = find_config(main_server->conf, CONF_PARAM, "DNSBLPolicy", FALSE);
-  if (c) {
-    policy = *((dnsbl_policy_e *) c->argv[0]);
-  }
-
-  switch (policy) {
-    case DNSBL_POLICY_ALLOW_DENY:
-      pr_trace_msg(trace_channel, 8,
-        "using policy of allowing connections unless listed by DNSBLDomains");
-      reject_conn = FALSE;
-      break;
-
-    case DNSBL_POLICY_DENY_ALLOW:
-      pr_trace_msg(trace_channel, 8,
-        "using policy of rejecting connections unless listed by DNSBLDomains");
-      reject_conn = TRUE;
-      break;
-  }
-
-  tmp_pool = make_sub_pool(permanent_pool);
-  rev_ip_addr = get_reversed_addr(tmp_pool);
-  if (!rev_ip_addr) {
-    (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
-      "client address '%s' is an IPv6 address, skipping",
-      pr_netaddr_get_ipstr(session.c->remote_addr));
-    destroy_pool(tmp_pool);
-    return 0;
-  }
-
-  switch (policy) {
-    /* For this policy, the connection will be allowed unless the connecting
-     * client is listed by any of the DNSBLDomain sites.
-     */
-    case DNSBL_POLICY_ALLOW_DENY: {
-      c = find_config(main_server->conf, CONF_PARAM, "DNSBLDomain", FALSE);
-      while (c) {
-        const char *domain;
-    
-        pr_signals_handle();
-
-        domain = c->argv[0];
-
-        if (lookup_addr(tmp_pool, rev_ip_addr, domain) < 0) {
-          (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
-            "client address '%s' is listed by DNSBLDomain '%s', rejecting "
-            "connection", pr_netaddr_get_ipstr(session.c->remote_addr), domain);
-          reject_conn = TRUE;
-          break;
-        }
-
-        c = find_config_next(c, c->next, CONF_PARAM, "DNSBLDomain", FALSE);
-      }
-
-      break;
-    }
-
-    /* For this policy, the connection will be NOT allowed unless the
-     * connecting client is listed by any of the DNSBLDomain sites.
-     */
-    case DNSBL_POLICY_DENY_ALLOW: {
-      c = find_config(main_server->conf, CONF_PARAM, "DNSBLDomain", FALSE);
-      while (c) {
-        const char *domain;
-
-        pr_signals_handle();
-
-        domain = c->argv[0];
-
-        if (lookup_addr(tmp_pool, rev_ip_addr, domain) < 0) {
-          (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
-            "client address '%s' is listed by DNSBLDomain '%s', allowing "
-            "connection", pr_netaddr_get_ipstr(session.c->remote_addr), domain);
-          reject_conn = FALSE;
-          break;
-        }
-    
-        c = find_config_next(c, c->next, CONF_PARAM, "DNSBLDomain", FALSE);
-      } 
-
-      break; 
-    }
-  }
-
-  destroy_pool(tmp_pool);
-
-  if (reject_conn) {
+  if (dnsbl_reject_conn() == TRUE) {
     (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
       "client not allowed by DNSBLPolicy, rejecting connection");
     errno = EACCES;
diff --git a/contrib/mod_dnsbl/mod_dnsbl.h.in b/contrib/mod_dnsbl/mod_dnsbl.h.in
index f1e41c3..303190d 100644
--- a/contrib/mod_dnsbl/mod_dnsbl.h.in
+++ b/contrib/mod_dnsbl/mod_dnsbl.h.in
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_dnsbl -- a module for checking DNSBL (DNS Black Lists)
  *                       servers before allowing a connection
- *
- * Copyright (c) 2007-2013 TJ Saunders
+ * Copyright (c) 2007-2016 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
@@ -22,8 +21,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_dnsbl.h.in,v 1.2 2013-09-18 21:47:19 castaglia Exp $
  */
 
 #ifndef MOD_DNSBL_H
diff --git a/contrib/mod_dynmasq.c b/contrib/mod_dynmasq.c
index dc1cdad..9408aaa 100644
--- a/contrib/mod_dynmasq.c
+++ b/contrib/mod_dynmasq.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_dynmasq -- a module for dynamically updating MasqueradeAddress
  *                         configurations, as when DynDNS names are used
- *
- * Copyright (c) 2004-2013 TJ Saunders
+ * Copyright (c) 2004-2016 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
@@ -23,10 +22,8 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * This is mod_dynmasq, contrib software for proftpd 1.2.x and above.
+ * This is mod_dynmasq, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_dynmasq.c,v 1.11 2013-10-13 22:51:36 castaglia Exp $
  */
 
 #include "conf.h"
@@ -35,11 +32,11 @@
 # include "mod_ctrls.h"
 #endif
 
-#define MOD_DYNMASQ_VERSION		"mod_dynmasq/0.4"
+#define MOD_DYNMASQ_VERSION		"mod_dynmasq/0.5"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030201
-# error "ProFTPD 1.3.2rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 extern xaset_t *server_list;
@@ -55,51 +52,49 @@ static ctrls_acttab_t dynmasq_acttab[];
 static void dynmasq_refresh(void) {
   server_rec *s;
 
-  /* Clear the netaddr cache.  Sadly, this is required in order for any
-   * updates to be discovered this way.
-   */
-  pr_netaddr_clear_cache();
-
   pr_log_debug(DEBUG2, MOD_DYNMASQ_VERSION
     ": resolving all MasqueradeAddress directives (could take a little while)");
 
   for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
-    config_rec *c = find_config(s->conf, CONF_PARAM, "MasqueradeAddress",
-      FALSE);
+    config_rec *c;
+
+    c = find_config(s->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
+    if (c != NULL) {
+      const char *masq_addr;
+      const pr_netaddr_t *na;
 
-    if (c) {
-      pr_netaddr_t *na = pr_netaddr_get_addr(s->pool, c->argv[1], NULL);
+      masq_addr = c->argv[1];
 
-      if (na) {
+      pr_netaddr_clear_ipcache(masq_addr);
+      na = pr_netaddr_get_addr(s->pool, masq_addr, NULL);
+      if (na != NULL) {
         /* Compare the obtained netaddr with the one already present.
          * Only update the "live" netaddr if they differ.
          */
         pr_log_debug(DEBUG2, MOD_DYNMASQ_VERSION
-          ": resolved MasqueradeAddress '%s' to IP address %s",
-          (const char *) c->argv[1], pr_netaddr_get_ipstr(na));
+          ": resolved MasqueradeAddress '%s' to IP address %s", masq_addr,
+          pr_netaddr_get_ipstr(na));
 
         if (pr_netaddr_cmp(c->argv[0], na) != 0) {
           pr_log_pri(PR_LOG_DEBUG, MOD_DYNMASQ_VERSION
             ": MasqueradeAddress '%s' updated for new address %s (was %s)",
-            (const char *) c->argv[1], pr_netaddr_get_ipstr(na),
+            masq_addr, pr_netaddr_get_ipstr(na),
             pr_netaddr_get_ipstr(c->argv[0]));
 
           /* Overwrite the old netaddr pointer.  Note that this constitutes
            * a minor memory leak, as there currently isn't a way to free
            * the memory used by a netaddr object.  Hrm.
            */
-          c->argv[0] = na;
+          c->argv[0] = (void *) na;
 
         } else {
           pr_log_debug(DEBUG2, MOD_DYNMASQ_VERSION
-            ": MasqueradeAddress '%s' has not changed addresses",
-            (const char *) c->argv[1]);
+            ": MasqueradeAddress '%s' has not changed addresses", masq_addr);
         }
  
       } else {
-        pr_log_pri(PR_LOG_NOTICE, MOD_DYNMASQ_VERSION
-          ": unable to resolve '%s', keeping previous address",
-          (const char *) c->argv[1]);
+        pr_log_pri(PR_LOG_INFO, MOD_DYNMASQ_VERSION
+          ": unable to resolve '%s', keeping previous address", masq_addr);
       }
     }
   }
diff --git a/contrib/mod_exec.c b/contrib/mod_exec.c
index 936655a..85a9fc0 100644
--- a/contrib/mod_exec.c
+++ b/contrib/mod_exec.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_exec -- a module for executing external scripts
- *
- * Copyright (c) 2002-2014 TJ Saunders
+ * Copyright (c) 2002-2016 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
@@ -23,8 +22,6 @@
  *
  * This is mod_exec, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_exec.c,v 1.40 2014-05-02 21:13:33 castaglia Exp $
  */
 
 #include "conf.h"
@@ -34,7 +31,7 @@
 # include <sys/resource.h>
 #endif
 
-#define MOD_EXEC_VERSION	"mod_exec/0.9.12"
+#define MOD_EXEC_VERSION	"mod_exec/0.9.14"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030402
@@ -43,6 +40,8 @@
 
 module exec_module;
 
+#define EXEC_MAX_FD_COUNT		1024
+
 static pool *exec_pool = NULL;
 static int exec_engine = FALSE;
 static unsigned int exec_nexecs = 0;
@@ -84,13 +83,14 @@ struct exec_event_data {
 
 /* Prototypes */
 static void exec_any_ev(const void *, void *);
-static char *exec_subst_var(pool *, char *, cmd_rec *);
+static const char *exec_subst_var(pool *, const char *, cmd_rec *);
 static int exec_log(const char *, ...)
 #ifdef __GNUC__
       __attribute__ ((format (printf, 1, 2)));
 #else
       ;
 #endif
+static int exec_sess_init(void);
 
 /* Support routines
  */
@@ -106,6 +106,18 @@ static int exec_closelog(void) {
   return 0;
 }
 
+static int exec_enabled(void) {
+  config_rec *c;
+  int enabled = TRUE;
+
+  c = find_config(CURRENT_CONF, CONF_PARAM, "ExecEnable", FALSE);
+  if (c) {
+    enabled = *((int *) c->argv[0]);
+  }
+
+  return enabled;
+}
+
 static char *exec_get_cmd(char **list) {
   char *res = NULL, *dst = NULL;
   unsigned char quote_mode = FALSE;
@@ -312,17 +324,21 @@ static void exec_prepare_fds(int stdin_fd, int stdout_fd, int stderr_fd) {
 # elif defined(RLIMIT_OFILE)
   if (getrlimit(RLIMIT_OFILE, &rlim) < 0) {
 # endif
-    exec_log("getrlimit() error: %s", strerror(errno));
+    /* Ignore ENOSYS (and EPERM, since some libc's use this as ENOSYS). */
+    if (errno != ENOSYS &&
+        errno != EPERM) {
+      exec_log("getrlimit() error: %s", strerror(errno));
+    }
 
     /* Pick some arbitrary high number. */
-    nfiles = 1024;
+    nfiles = EXEC_MAX_FD_COUNT;
 
   } else {
     nfiles = rlim.rlim_max;
   }
 
 #else /* no RLIMIT_NOFILE or RLIMIT_OFILE */
-   nfiles = 1024;
+   nfiles = EXEC_MAX_FD_COUNT;
 #endif
 
   /* Yes, using a long for the nfiles variable is not quite kosher; it should
@@ -336,8 +352,9 @@ static void exec_prepare_fds(int stdin_fd, int stdout_fd, int stderr_fd) {
    * mod_exec's forked processes never return/exit.)
    */
 
-  if (nfiles < 0) {
-    nfiles = 1024;
+  if (nfiles < 0 ||
+      nfiles > EXEC_MAX_FD_COUNT) {
+    nfiles = EXEC_MAX_FD_COUNT;
   }
 
   /* Close the "non-standard" file descriptors. */
@@ -458,30 +475,34 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
     /* Child process */
     char **env = NULL, *path = NULL, *ptr = NULL;
     register unsigned int i = 0;
+
+    /* Note: there is no need to clean up this temporary pool, as we've
+     * forked.  If the exec call succeeds, this child process will exit
+     * normally, and its process space recovered by the OS.  If the exec
+     * call fails, we still exit, and the process space is recovered by
+     * the OS.  Either way, the memory will be cleaned up without need for
+     * us to do it explicitly (unless one wanted to be pedantic about it,
+     * of course).
+     */
+    pool *tmp_pool = cmd ? cmd->tmp_pool : make_sub_pool(session.pool);
  
     /* Don't forget to update the PID. */
     session.pid = getpid();
 
     if (!(exec_opts & EXEC_OPT_USE_STDIN)) {
 
-      /* Note: there is no need to clean up this temporary pool, as we've
-       * forked.  If the exec call succeeds, this child process will exit
-       * normally, and its process space recovered by the OS.  If the exec
-       * call fails, we still exit, and the process space is recovered by
-       * the OS.  Either way, the memory will be cleaned up without need for
-       * us to do it explicitly (unless one wanted to be pedantic about it,
-       * of course).
-       */
-      pool *tmp_pool = cmd ? cmd->tmp_pool : make_sub_pool(session.pool);
-
       /* Prepare the environment. */
       env = exec_prepare_environ(tmp_pool, cmd);
  
       /* Perform any required substitution on the command arguments. */
       for (i = 3; i < c->argc; i++) {
         pr_signals_handle();
-        c->argv[i] = exec_subst_var(tmp_pool, c->argv[i], cmd);
+        c->argv[i] = (void *) exec_subst_var(tmp_pool, c->argv[i], cmd);
       }
+
+    } else {
+      /* Make sure that env is at least a NULL-terminated array. */
+      env = pcalloc(tmp_pool, sizeof(char **));
     }
 
     /* Restore previous signal actions. */
@@ -525,10 +546,10 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
       PRIVS_REVOKE
     }
 
-    exec_log("preparing to execute '%s' with uid %lu (euid %lu), "
-      "gid %lu (egid %lu)", (const char *) c->argv[2],
-      (unsigned long) getuid(), (unsigned long) geteuid(),
-      (unsigned long) getgid(), (unsigned long) getegid());
+    exec_log("preparing to execute '%s' with uid %s (euid %s), "
+      "gid %s (egid %s)", (const char *) c->argv[2],
+      pr_uid2str(tmp_pool, getuid()), pr_uid2str(tmp_pool, geteuid()),
+      pr_gid2str(tmp_pool, getgid()), pr_gid2str(tmp_pool, getegid()));
 
     path = c->argv[2];
 
@@ -616,7 +637,7 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
       for (i = 3; i < c->argc && c->argv[i] != NULL; i++) {
         pr_signals_handle();
 
-        c->argv[i] = exec_subst_var(tmp_pool, c->argv[i], cmd);
+        c->argv[i] = (void *) exec_subst_var(tmp_pool, c->argv[i], cmd);
 
         /* Write the argument to stdin, terminated by a newline. */
         if (write(exec_stdin_pipe[1], c->argv[i], strlen(c->argv[i])) < 0) {
@@ -665,6 +686,7 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
       fd_set readfds;
       struct timeval tv;
       time_t start_time = time(NULL);
+      pool *tmp_pool = cmd ? cmd->tmp_pool : make_sub_pool(session.pool);
 
       /* We set the result value to zero initially, so that at least one
        * pass through the stdout/stderr reading code happens.
@@ -741,15 +763,17 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
         }
 
         if (fds >= 0) {
-          int buflen;
-          char buf[PIPE_BUF];
+          long buflen, bufsz;
+          char *buf;
+
+          buf = pr_fsio_getpipebuf(tmp_pool, exec_stdout_pipe[0], &bufsz);
 
           /* The child sent us something.  How thoughtful. */
 
           if (FD_ISSET(exec_stdout_pipe[0], &readfds)) {
-            memset(buf, '\0', sizeof(buf));
+            memset(buf, '\0', bufsz);
 
-            buflen = read(exec_stdout_pipe[0], buf, sizeof(buf)-1);
+            buflen = read(exec_stdout_pipe[0], buf, bufsz-1);
             if (buflen > 0) {
               if (exec_opts & EXEC_OPT_SEND_STDOUT) {
 
@@ -796,9 +820,9 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
           }
 
           if (FD_ISSET(exec_stderr_pipe[0], &readfds)) {
-            memset(buf, '\0', sizeof(buf));
+            memset(buf, '\0', bufsz);
 
-            buflen = read(exec_stderr_pipe[0], buf, sizeof(buf)-1);
+            buflen = read(exec_stderr_pipe[0], buf, bufsz-1);
             if (buflen > 0) {
 
               /* Trim trailing CRs and LFs. */
@@ -833,6 +857,10 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
         res = waitpid(pid, &status, WNOHANG);
       }
 
+      if (cmd == NULL) {
+        destroy_pool(tmp_pool);
+      }
+
     } else {
       res = waitpid(pid, &status, 0);
       while (res <= 0) {
@@ -897,23 +925,26 @@ static int exec_ssystem(cmd_rec *cmd, config_rec *c, int flags) {
 }
 
 /* Perform any substitution of "magic cookie" values. */
-static char *exec_subst_var(pool *tmp_pool, char *varstr, cmd_rec *cmd) {
+static const char *exec_subst_var(pool *tmp_pool, const char *varstr,
+    cmd_rec *cmd) {
   char *ptr = NULL;
 
-  if (!varstr)
+  if (varstr == NULL) {
     return NULL;
+  }
 
   ptr = strstr(varstr, "%a");
   if (ptr != NULL) {
-    pr_netaddr_t *remote_addr = pr_netaddr_get_sess_remote_addr();
+    const pr_netaddr_t *remote_addr;
 
-    varstr = sreplace(tmp_pool, varstr, "%a", remote_addr ?
-        pr_netaddr_get_ipstr(remote_addr) : "", NULL);
+    remote_addr = pr_netaddr_get_sess_remote_addr();
+    varstr = sreplace(tmp_pool, varstr, "%a",
+      remote_addr ? pr_netaddr_get_ipstr(remote_addr) : "", NULL);
   }
 
   ptr = strstr(varstr, "%A");
   if (ptr != NULL) {
-    char *anon_pass;
+    const char *anon_pass;
 
     anon_pass = pr_table_get(session.notes, "mod_auth.anon-passwd", NULL);
     if (anon_pass == NULL) {
@@ -936,7 +967,8 @@ static char *exec_subst_var(pool *tmp_pool, char *varstr, cmd_rec *cmd) {
   }
 
   ptr = strstr(varstr, "%F");
-  if (ptr != NULL) {
+  if (ptr != NULL &&
+      cmd != NULL) {
     if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
       char *path;
 
@@ -977,7 +1009,8 @@ static char *exec_subst_var(pool *tmp_pool, char *varstr, cmd_rec *cmd) {
   }
 
   ptr = strstr(varstr, "%f");
-  if (ptr != NULL) {
+  if (ptr != NULL &&
+      cmd != NULL) {
 
     if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
       char *path;
@@ -1034,49 +1067,49 @@ static char *exec_subst_var(pool *tmp_pool, char *varstr, cmd_rec *cmd) {
 
   ptr = strstr(varstr, "%h");
   if (ptr != NULL) {
-    const char *remote_name = pr_netaddr_get_sess_remote_name();
+    const char *remote_name;
 
-    varstr = sreplace(tmp_pool, varstr, "%h", 
+    remote_name = pr_netaddr_get_sess_remote_name();
+    varstr = sreplace(tmp_pool, varstr, "%h",
       remote_name ? remote_name : "", NULL);
   }
 
   ptr = strstr(varstr, "%l");
   if (ptr != NULL) {
-    char *rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident",
-      NULL);
+    const char *rfc1413_ident;
 
-    if (rfc1413_ident == NULL)
+    rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident",
+      NULL);
+    if (rfc1413_ident == NULL) {
       rfc1413_ident = "UNKNOWN";
+    }
 
     varstr = sreplace(tmp_pool, varstr, "%l", rfc1413_ident, NULL);
   }
 
   ptr = strstr(varstr, "%m");
   if (ptr != NULL) {
-    varstr = sreplace(tmp_pool, varstr, "%m", cmd ? cmd->argv[0] : "",
-      NULL);
+    varstr = sreplace(tmp_pool, varstr, "%m", cmd ? cmd->argv[0] : "", NULL);
   }
 
   ptr = strstr(varstr, "%r");
-  if (ptr != NULL) {
-    if (cmd) {
-      if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
-          session.hide_password) {
-        varstr = sreplace(tmp_pool, varstr, "%r", "PASS (hidden)", NULL);
+  if (ptr != NULL &&
+      cmd != NULL) {
+    if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
+        session.hide_password) {
+      varstr = sreplace(tmp_pool, varstr, "%r", "PASS (hidden)", NULL);
 
-      } else {
-        varstr = sreplace(tmp_pool, varstr, "%r",
-          pr_cmd_get_displayable_str(cmd, NULL), NULL);
-      }
+    } else {
+      varstr = sreplace(tmp_pool, varstr, "%r",
+        pr_cmd_get_displayable_str(cmd, NULL), NULL);
     }
   }
 
   ptr = strstr(varstr, "%U");
   if (ptr != NULL) {
-    char *user;
+    const char *user;
 
     user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-
     varstr = sreplace(tmp_pool, varstr, "%U",
       user ? user : "", NULL);
   }
@@ -1100,13 +1133,15 @@ static char *exec_subst_var(pool *tmp_pool, char *varstr, cmd_rec *cmd) {
   }
 
   ptr = strstr(varstr, "%w");
-  if (ptr != NULL) {
-    char *rnfr_path = "-";
+  if (ptr != NULL &&
+      cmd != NULL) {
+    const char *rnfr_path = "-";
 
     if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
       rnfr_path = pr_table_get(session.notes, "mod_core.rnfr-path", NULL);
-      if (rnfr_path == NULL)
+      if (rnfr_path == NULL) {
         rnfr_path = "-";
+      }
     }
 
     varstr = sreplace(tmp_pool, varstr, "%w", rnfr_path, NULL);
@@ -1141,15 +1176,17 @@ static char *exec_subst_var(pool *tmp_pool, char *varstr, cmd_rec *cmd) {
     if (strncmp(key, "%{time:", 7) == 0) {
       char time_str[128], *fmt;
       time_t now;
-      struct tm *time_info;
+      struct tm *tm;
 
       fmt = pstrndup(tmp_pool, key + 7, strlen(key) - 8);
 
       now = time(NULL);
-      time_info = pr_localtime(NULL, &now);
-
       memset(time_str, 0, sizeof(time_str));
-      strftime(time_str, sizeof(time_str), fmt, time_info);
+
+      tm = pr_localtime(NULL, &now);
+      if (tm != NULL) {
+        strftime(time_str, sizeof(time_str), fmt, tm);
+      }
 
       val = pstrdup(tmp_pool, time_str);
 
@@ -1189,8 +1226,13 @@ MODRET exec_pre_cmd(cmd_rec *cmd) {
   config_rec *c = NULL;
   array_header *seen_execs = NULL;
 
-  if (!exec_engine)
+  if (!exec_engine) {
     return PR_DECLINED(cmd);
+  }
+
+  if (!exec_enabled()) {
+    return PR_DECLINED(cmd);
+  }
 
   /* Create an array that will contain the IDs of the Execs we've
    * already processed.
@@ -1229,11 +1271,11 @@ MODRET exec_pre_cmd(cmd_rec *cmd) {
     if (exec_match_cmd(cmd, c->argv[1])) {
       int res = exec_ssystem(cmd, c, EXEC_FL_NO_SEND);
       if (res != 0) {
-        exec_log("%s ExecBeforeCommand '%s' failed: %s", cmd->argv[0],
+        exec_log("%s ExecBeforeCommand '%s' failed: %s", (char *) cmd->argv[0],
           (const char *) c->argv[2], strerror(res));
 
       } else {
-        exec_log("%s ExecBeforeCommand '%s' succeeded", cmd->argv[0],
+        exec_log("%s ExecBeforeCommand '%s' succeeded", (char *) cmd->argv[0],
           (const char *) c->argv[2]);
       }
     }
@@ -1248,8 +1290,13 @@ MODRET exec_post_cmd(cmd_rec *cmd) {
   config_rec *c = NULL;
   array_header *seen_execs = NULL;
 
-  if (!exec_engine)
+  if (!exec_engine) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (!exec_enabled()) {
     return PR_DECLINED(cmd);
+  }
 
   /* Create an array that will contain the IDs of the Execs we've
    * already processed.
@@ -1287,11 +1334,11 @@ MODRET exec_post_cmd(cmd_rec *cmd) {
     if (exec_match_cmd(cmd, c->argv[1])) {
       int res = exec_ssystem(cmd, c, 0);
       if (res != 0) {
-        exec_log("%s ExecOnCommand '%s' failed: %s", cmd->argv[0],
+        exec_log("%s ExecOnCommand '%s' failed: %s", (char *) cmd->argv[0],
           (const char *) c->argv[2], strerror(res));
 
       } else {
-        exec_log("%s ExecOnCommand '%s' succeeded", cmd->argv[0],
+        exec_log("%s ExecOnCommand '%s' succeeded", (char *) cmd->argv[0],
           (const char *) c->argv[2]);
       }
     }
@@ -1306,8 +1353,13 @@ MODRET exec_post_cmd_err(cmd_rec *cmd) {
   config_rec *c = NULL;
   array_header *seen_execs = NULL;
 
-  if (!exec_engine)
+  if (!exec_engine) {
     return PR_DECLINED(cmd);
+  }
+
+  if (!exec_enabled()) {
+    return PR_DECLINED(cmd);
+  }
 
   /* Create an array that will contain the IDs of the Execs we've
    * already processed.
@@ -1347,11 +1399,11 @@ MODRET exec_post_cmd_err(cmd_rec *cmd) {
 
       res = exec_ssystem(cmd, c, 0);
       if (res != 0) {
-        exec_log("%s ExecOnError '%s' failed: %s", cmd->argv[0],
+        exec_log("%s ExecOnError '%s' failed: %s", (char *) cmd->argv[0],
           (const char *) c->argv[2], strerror(res));
 
       } else {
-        exec_log("%s ExecOnError '%s' succeeded", cmd->argv[0],
+        exec_log("%s ExecOnError '%s' succeeded", (char *) cmd->argv[0],
           (const char *) c->argv[2]);
       }
     }
@@ -1369,33 +1421,58 @@ MODRET exec_post_cmd_err(cmd_rec *cmd) {
 MODRET set_execbeforecommand(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *path;
 
-  if (cmd->argc-1 < 2)
+  if (cmd->argc-1 < 2) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|
     CONF_DIR);
 
-  if (*cmd->argv[2] != '/')
+  path = cmd->argv[2];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
+  }
 
   c = add_config_param(cmd->argv[0], 0);
   c->argc = cmd->argc + 1;
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1));
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1));
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
   *((unsigned int *) c->argv[0]) = exec_nexecs++;
 
   exec_parse_cmds(c, cmd->argv[1]);
 
-  for (i = 2; i < cmd->argc; i++)
+  for (i = 2; i < cmd->argc; i++) {
     c->argv[i] = pstrdup(c->pool, cmd->argv[i]);
+  }
 
   c->flags |= CF_MERGEDOWN_MULTI;
 
   return PR_HANDLED(cmd);
 }
 
+/* usage: ExecEnable on|off */
+MODRET set_execenable(cmd_rec *cmd) {
+  int enable = -1;
+  config_rec *c;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ANON|CONF_DIR|CONF_DYNDIR);
+
+  enable = get_boolean(cmd, 1);
+  if (enable == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = enable;
+
+  return PR_HANDLED(cmd);
+}
+
 /* usage: ExecEngine on|off */
 MODRET set_execengine(cmd_rec *cmd) {
   int engine = -1;
@@ -1423,17 +1500,24 @@ MODRET set_execengine(cmd_rec *cmd) {
 MODRET set_execenviron(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *key;
 
   CHECK_ARGS(cmd, 2);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   c = add_config_param_str(cmd->argv[0], 2, NULL, cmd->argv[2]);
 
-  /* Make sure the given environment variable name is uppercased. */
-  for (i = 0; i < strlen(cmd->argv[1]); i++)
-    (cmd->argv[1])[i] = toupper((cmd->argv[1])[i]);
-  c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
+  /* Make sure the given environment variable name is uppercased.
+   * NOTE: Are there cases where this SHOULD NOT happen?  Why should
+   * environment variable names always be uppercased?
+   */
+  key = cmd->argv[1];
 
+  for (i = 0; i < strlen(key); i++) {
+    key[i] = toupper(key[i]);
+  }
+
+  c->argv[0] = pstrdup(c->pool, key);
   return PR_HANDLED(cmd);
 }
 
@@ -1450,30 +1534,34 @@ MODRET set_execlog(cmd_rec *cmd) {
 MODRET set_execoncommand(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *path;
 
-  if (cmd->argc-1 < 2)
+  if (cmd->argc-1 < 2) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|
     CONF_DIR);
 
-  if (*cmd->argv[2] != '/')
+  path = cmd->argv[2];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
+  }
 
   c = add_config_param(cmd->argv[0], 0);
   c->argc = cmd->argc + 1;
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1));
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1));
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
   *((unsigned int *) c->argv[0]) = exec_nexecs++;
 
   exec_parse_cmds(c, cmd->argv[1]);
 
-  for (i = 2; i < cmd->argc; i++)
+  for (i = 2; i < cmd->argc; i++) {
     c->argv[i] = pstrdup(c->pool, cmd->argv[i]);
+  }
 
   c->flags |= CF_MERGEDOWN_MULTI;
-
   return PR_HANDLED(cmd);
 }
 
@@ -1481,20 +1569,24 @@ MODRET set_execoncommand(cmd_rec *cmd) {
 MODRET set_execonconnect(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *path;
 
-  if (cmd->argc-1 < 1)
+  if (cmd->argc-1 < 1) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (*cmd->argv[1] != '/')
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
+  }
 
   c = add_config_param(cmd->argv[0], 0);
   c->argc = cmd->argc + 1;
 
   /* Add one for the terminating NULL. */
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1)); 
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1)); 
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
   *((unsigned int *) c->argv[0]) = exec_nexecs++;
@@ -1510,30 +1602,34 @@ MODRET set_execonconnect(cmd_rec *cmd) {
 MODRET set_execonerror(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *path;
 
-  if (cmd->argc-1 < 2)
+  if (cmd->argc-1 < 2) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|
     CONF_DIR); 
 
-  if (*cmd->argv[2] != '/')
+  path = cmd->argv[2];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
+  }
 
   c = add_config_param(cmd->argv[0], 0);
   c->argc = cmd->argc + 1;
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1));
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1));
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
   *((unsigned int *) c->argv[0]) = exec_nexecs++;
 
   exec_parse_cmds(c, cmd->argv[1]);
   
-  for (i = 2; i < cmd->argc; i++) 
+  for (i = 2; i < cmd->argc; i++) {
     c->argv[i] = pstrdup(c->pool, cmd->argv[i]);
+  }
 
   c->flags |= CF_MERGEDOWN_MULTI;
-
   return PR_HANDLED(cmd);
 }
 
@@ -1541,25 +1637,33 @@ MODRET set_execonerror(cmd_rec *cmd) {
 MODRET set_execonevent(cmd_rec *cmd) {
   register unsigned int i;
   unsigned int flags = EXEC_FL_CLEAR_GROUPS|EXEC_FL_NO_SEND;
+  char *event_name, *path;
+  size_t event_namelen;
   config_rec *c;
   struct exec_event_data *eed;
 
-  if (cmd->argc-1 < 2)
+  if (cmd->argc-1 < 2) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (cmd->argv[1][strlen(cmd->argv[1])-1] == '*') {
+  event_name = cmd->argv[1];
+  event_namelen = strlen(event_name);
+
+  if (event_name[event_namelen-1] == '*') {
     flags |= EXEC_FL_RUN_AS_ROOT;
-    cmd->argv[1][strlen(cmd->argv[1])-1] = '\0';
-  }
+    event_name[event_namelen-1] = '\0';
+    event_namelen--;
 
-  if (cmd->argv[1][strlen(cmd->argv[1])-1] == '~') {
+  } else if (event_name[event_namelen-1] == '~') {
     flags |= EXEC_FL_RUN_AS_USER;
-    cmd->argv[1][strlen(cmd->argv[1])-1] = '\0';
+    event_name[event_namelen-1] = '\0';
+    event_namelen--;
   }
 
-  if (*cmd->argv[2] != '/') {
+  path = cmd->argv[2];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
   }
 
@@ -1567,7 +1671,7 @@ MODRET set_execonevent(cmd_rec *cmd) {
   c->pool = make_sub_pool(cmd->server->pool);
   pr_pool_tag(c->pool, cmd->argv[0]);
   c->argc = cmd->argc + 1;
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1));
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1));
 
   /* Unused for event config_recs. */
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
@@ -1579,7 +1683,7 @@ MODRET set_execonevent(cmd_rec *cmd) {
 
   eed = pcalloc(c->pool, sizeof(struct exec_event_data));
   eed->flags = flags;
-  eed->event = pstrdup(c->pool, cmd->argv[1]);
+  eed->event = pstrdup(c->pool, event_name);
   eed->c = c;
 
   if (strncasecmp(eed->event, "MaxConnectionRate", 18) == 0) {
@@ -1601,26 +1705,31 @@ MODRET set_execonevent(cmd_rec *cmd) {
 MODRET set_execonexit(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *path;
 
-  if (cmd->argc-1 < 1)
+  if (cmd->argc-1 < 1) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (*cmd->argv[1] != '/')
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
+  }
 
   c = add_config_param(cmd->argv[0], 0);
   c->argc = cmd->argc + 1;
 
   /* Add one for the terminating NULL. */
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1));
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1));
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
   *((unsigned int *) c->argv[0]) = exec_nexecs++;
 
-  for (i = 1; i < cmd->argc; i++)
+  for (i = 1; i < cmd->argc; i++) {
     c->argv[i+1] = pstrdup(c->pool, cmd->argv[i]);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -1629,26 +1738,31 @@ MODRET set_execonexit(cmd_rec *cmd) {
 MODRET set_execonrestart(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
+  char *path;
 
-  if (cmd->argc-1 < 1)
+  if (cmd->argc-1 < 1) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (*cmd->argv[1] != '/')
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, "path to program must be a full path");
+  }
 
   c = add_config_param(cmd->argv[0], 0);
   c->argc = cmd->argc + 1;
 
   /* Add one for the terminating NULL. */
-  c->argv = pcalloc(c->pool, sizeof(char *) * (c->argc + 1));
+  c->argv = pcalloc(c->pool, sizeof(void *) * (c->argc + 1));
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
   *((unsigned int *) c->argv[0]) = exec_nexecs++;
 
-  for (i = 1; i < cmd->argc; i++) 
+  for (i = 1; i < cmd->argc; i++) {
     c->argv[i+1] = pstrdup(c->pool, cmd->argv[i]);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -1659,8 +1773,9 @@ MODRET set_execoptions(cmd_rec *cmd) {
   register unsigned int i;
   unsigned int opts = 0U;
 
-  if (cmd->argc-1 == 0)
+  if (cmd->argc-1 == 0) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -1681,7 +1796,7 @@ MODRET set_execoptions(cmd_rec *cmd) {
 
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown ExecOption: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
     }
   }
 
@@ -1739,7 +1854,6 @@ static void exec_exit_ev(const void *event_data, void *user_data) {
     return;
 
   c = find_config(main_server->conf, CONF_PARAM, "ExecOnExit", FALSE);
-
   while (c) {
     int res;
 
@@ -1834,6 +1948,29 @@ static void exec_restart_ev(const void *event_data, void *user_data) {
   return;
 }
 
+static void exec_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&exec_module, "core.exit", exec_exit_ev);
+  pr_event_unregister(&exec_module, "core.session-reinit", exec_sess_reinit_ev);
+
+  exec_engine = FALSE;
+  exec_opts = 0U;
+  exec_timeout = 0;
+
+  (void) close(exec_logfd);
+  exec_logfd = -1;
+  exec_logname = NULL;
+
+  res = exec_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&exec_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -1842,6 +1979,9 @@ static int exec_sess_init(void) {
   config_rec *c = NULL;
   const char *proto;
 
+  pr_event_register(&exec_module, "core.session-reinit", exec_sess_reinit_ev,
+    NULL);
+
   use_exec = get_param_ptr(main_server->conf, "ExecEngine", FALSE);
   if (use_exec != NULL &&
       *use_exec == TRUE) {
@@ -1852,7 +1992,6 @@ static int exec_sess_init(void) {
     return 0;
   }
 
-  /* Register a "core.exit" event handler. */
   pr_event_register(&exec_module, "core.exit", exec_exit_ev, NULL);
 
   c = find_config(main_server->conf, CONF_PARAM, "ExecOptions", FALSE);
@@ -1898,7 +2037,6 @@ static int exec_sess_init(void) {
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "ExecOnConnect", FALSE);
-
   while (c) {
     int res;
 
@@ -1939,6 +2077,7 @@ static int exec_init(void) {
 
 static conftable exec_conftab[] = {
   { "ExecBeforeCommand",set_execbeforecommand,	NULL },
+  { "ExecEnable",	set_execenable,		NULL },
   { "ExecEngine",	set_execengine,		NULL },
   { "ExecEnviron",	set_execenviron,	NULL },
   { "ExecLog",		set_execlog,		NULL },
diff --git a/contrib/mod_geoip.c b/contrib/mod_geoip.c
index a0ec648..a559ea7 100644
--- a/contrib/mod_geoip.c
+++ b/contrib/mod_geoip.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_geoip -- a module for looking up country/city/etc for clients
- *
- * Copyright (c) 2010-2015 TJ Saunders
+ * Copyright (c) 2010-2016 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
@@ -36,7 +35,7 @@
  * module for Apache.
  */
 
-#define MOD_GEOIP_VERSION		"mod_geoip/0.7"
+#define MOD_GEOIP_VERSION		"mod_geoip/0.9"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030402
@@ -119,6 +118,15 @@ static struct geoip_filter_key geoip_filter_keys[] = {
   { NULL, -1 }
 };
 
+#if PR_USE_REGEX
+/* GeoIP filter */
+struct geoip_filter {
+  int filter_id;
+  const char *filter_pattern;
+  pr_regex_t *filter_re;
+};
+#endif /* PR_USE_REGEX */
+
 /* GeoIP policies */
 typedef enum {
   GEOIP_POLICY_ALLOW_DENY,
@@ -133,6 +141,180 @@ static const char *trace_channel = "geoip";
 static const char *get_geoip_filter_name(int);
 static const char *get_geoip_filter_value(int);
 
+static int get_filter_id(const char *filter_name) {
+  register unsigned int i;
+  int filter_id = -1;
+
+  for (i = 0; geoip_filter_keys[i].filter_name != NULL; i++) {
+    if (strcasecmp(filter_name, geoip_filter_keys[i].filter_name) == 0) {
+      filter_id = geoip_filter_keys[i].filter_id;
+      break;
+    }
+  }
+
+  return filter_id;
+}
+
+#if PR_USE_REGEX
+static int get_filter(pool *p, const char *pattern, pr_regex_t **pre) {
+  int res;
+
+  *pre = pr_regexp_alloc(&geoip_module);
+
+  res = pr_regexp_compile(*pre, pattern, REG_EXTENDED|REG_NOSUB|REG_ICASE);
+  if (res != 0) {
+    char errstr[256];
+
+    memset(errstr, '\0', sizeof(errstr));
+    pr_regexp_error(res, *pre, errstr, sizeof(errstr)-1);
+    pr_regexp_free(&geoip_module, *pre);
+    *pre = NULL;
+
+    pr_log_pri(PR_LOG_DEBUG, MOD_GEOIP_VERSION
+      ": pattern '%s' failed regex compilation: %s", pattern, errstr);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return res;
+}
+
+static struct geoip_filter *make_filter(pool *p, const char *filter_name,
+    const char *pattern) {
+  struct geoip_filter *filter;
+  int filter_id;
+  pr_regex_t *pre = NULL;
+
+  filter_id = get_filter_id(filter_name);
+  if (filter_id < 0) {
+    pr_log_debug(DEBUG0, MOD_GEOIP_VERSION ": unknown GeoIP filter name '%s'",
+      filter_name);
+    return NULL;
+  }
+
+  if (get_filter(p, pattern, &pre) < 0) {
+    return NULL;
+  }
+
+  filter = pcalloc(p, sizeof(struct geoip_filter));
+  filter->filter_id = filter_id;
+  filter->filter_pattern = pstrdup(p, pattern);
+  filter->filter_re = pre;
+
+  return filter;
+}
+
+static array_header *get_sql_filters(pool *p, const char *query_name) {
+  register unsigned int i;
+  cmdtable *sql_cmdtab = NULL;
+  cmd_rec *sql_cmd = NULL;
+  modret_t *sql_res = NULL;
+  array_header *sql_data = NULL;
+  const char **values = NULL;
+  array_header *sql_filters = NULL;
+
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
+  if (sql_cmdtab == NULL) {
+    (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+      "unable to execute SQLNamedQuery '%s': mod_sql not loaded", query_name);
+    errno = EPERM;
+    return NULL;
+  }
+
+  sql_cmd = pr_cmd_alloc(p, 2, "sql_lookup", query_name);
+
+  sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
+  if (sql_res == NULL ||
+      MODRET_ISERROR(sql_res)) {
+    (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+      "error processing SQLNamedQuery '%s'; check mod_sql logs for details",
+      query_name);
+    errno = EPERM;
+    return NULL;
+  }
+
+  sql_data = sql_res->data;
+  pr_trace_msg(trace_channel, 9, "SQLNamedQuery '%s' returned item count %d",
+    query_name, sql_data->nelts);
+
+  if (sql_data->nelts == 0) {
+    (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+      "SQLNamedQuery '%s' returned no values", query_name);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  if (sql_data->nelts % 2 == 1) {
+    (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+      "SQLNamedQuery '%s' returned odd number of values (%d), "
+      "expected even number", query_name, sql_data->nelts);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  values = sql_data->elts;
+  sql_filters = make_array(p, 0, sizeof(struct geoip_filter));
+
+  for (i = 0; i < sql_data->nelts; i += 2) {
+    const char *filter_name, *pattern = NULL;
+    struct geoip_filter *filter;
+
+    filter_name = values[i];
+    pattern = values[i+1];
+
+    filter = make_filter(p, filter_name, pattern);
+    if (filter == NULL) {
+      pr_trace_msg(trace_channel, 3, "unable to use '%s %s' as filter: %s",
+        filter_name, pattern, strerror(errno));
+      continue;
+    }
+
+    *((struct geoip_filter **) push_array(sql_filters)) = filter;
+  }
+
+  return sql_filters;
+}
+#endif /* PR_USE_REGEX */
+
+static void resolve_deferred_patterns(pool *p, const char *directive) {
+#if PR_USE_REGEX
+  config_rec *c;
+
+  c = find_config(main_server->conf, CONF_PARAM, directive, FALSE);
+  while (c != NULL) {
+    register unsigned int i;
+    array_header *deferred_filters, *filters;
+
+    pr_signals_handle();
+
+    filters = c->argv[0];
+    deferred_filters = c->argv[1];
+
+    for (i = 0; i < deferred_filters->nelts; i++) {
+      const char *query_name;
+      array_header *sql_filters;
+
+      query_name = ((const char **) deferred_filters->elts)[i];
+
+      sql_filters = get_sql_filters(p, query_name);
+      if (sql_filters == NULL) {
+        continue;
+      }
+
+      array_cat(filters, sql_filters);
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, directive, FALSE);
+  }
+#endif /* PR_USE_REGEX */
+}
+
+static void resolve_deferred_filters(pool *p) {
+  resolve_deferred_patterns(p, "GeoIPAllowFilter");
+  resolve_deferred_patterns(p, "GeoIPDenyFilter");
+}
+
 static int check_geoip_filters(geoip_policy_e policy) {
   int allow_conn = 0, matched_allow_filter = -1, matched_deny_filter = -1;
 #if PR_USE_REGEX
@@ -140,9 +322,9 @@ static int check_geoip_filters(geoip_policy_e policy) {
 
   c = find_config(main_server->conf, CONF_PARAM, "GeoIPAllowFilter", FALSE);
   while (c != NULL) {
-    int filter_id, res;
-    pr_regex_t *filter_re;
-    const char *filter_name, *filter_pattern, *filter_value;
+    register unsigned int i;
+    int matched = TRUE;
+    array_header *filters;
 
     pr_signals_handle();
 
@@ -150,44 +332,59 @@ static int check_geoip_filters(geoip_policy_e policy) {
       matched_allow_filter = FALSE;
     }
 
-    filter_id = *((int *) c->argv[0]);
-    filter_pattern = c->argv[1];
-    filter_re = c->argv[2];
+    filters = c->argv[0];
 
-    filter_value = get_geoip_filter_value(filter_id);
-    if (filter_value == NULL) {
-      c = find_config_next(c, c->next, CONF_PARAM, "GeoIPAllowFilter", FALSE);
-      continue;
-    }
+    for (i = 0; i < filters->nelts; i++) {
+      int filter_id, res;
+      struct geoip_filter *filter;
+      pr_regex_t *filter_re;
+      const char *filter_name, *filter_pattern, *filter_value;
+
+      filter = ((struct geoip_filter **) filters->elts)[i]; 
+      filter_id = filter->filter_id;
+      filter_pattern = filter->filter_pattern;
+      filter_re = filter->filter_re;
+
+      filter_value = get_geoip_filter_value(filter_id);
+      if (filter_value == NULL) {
+        matched = FALSE;
+        break;
+      }
 
-    filter_name = get_geoip_filter_name(filter_id);
+      filter_name = get_geoip_filter_name(filter_id);
 
-    res = pr_regexp_exec(filter_re, filter_value, 0, NULL, 0, 0, 0);
-    pr_trace_msg(trace_channel, 12,
-      "%s filter value %s %s GeoIPAllowFilter pattern '%s'",
-      filter_name, filter_value, res == 0 ? "matched" : "did not match",
-      filter_pattern);
+      res = pr_regexp_exec(filter_re, filter_value, 0, NULL, 0, 0, 0);
+      pr_trace_msg(trace_channel, 12,
+        "%s filter value %s %s GeoIPAllowFilter pattern '%s'",
+        filter_name, filter_value, res == 0 ? "matched" : "did not match",
+        filter_pattern);
+      if (res == 0) {
+        (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+          "%s filter value '%s' matched GeoIPAllowFilter pattern '%s'",
+          filter_name, filter_value, filter_pattern);
 
-    if (res == 0) {
-      (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
-        "%s filter value '%s' matched GeoIPAllowFilter pattern '%s'",
-        filter_name, filter_value, filter_pattern);
+      } else {
+        (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+          "%s filter value '%s' did not match GeoIPAllowFilter pattern '%s'",
+          filter_name, filter_value, filter_pattern);
+          matched = FALSE;
+          break;
+      }
+    }
+
+    if (matched == TRUE) {
       matched_allow_filter = TRUE;
       break;
     }
 
-    (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
-      "%s filter value '%s' did not match GeoIPAllowFilter pattern '%s'",
-      filter_name, filter_value, filter_pattern);
-
     c = find_config_next(c, c->next, CONF_PARAM, "GeoIPAllowFilter", FALSE);
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "GeoIPDenyFilter", FALSE);
   while (c != NULL) {
-    int filter_id, res;
-    pr_regex_t *filter_re;
-    const char *filter_name, *filter_pattern, *filter_value;
+    register unsigned int i;
+    int matched = TRUE;
+    array_header *filters;
 
     pr_signals_handle();
 
@@ -195,36 +392,50 @@ static int check_geoip_filters(geoip_policy_e policy) {
       matched_deny_filter = FALSE;
     }
 
-    filter_id = *((int *) c->argv[0]);
-    filter_pattern = c->argv[1];
-    filter_re = c->argv[2];
+    filters = c->argv[0];
 
-    filter_value = get_geoip_filter_value(filter_id);
-    if (filter_value == NULL) {
-      c = find_config_next(c, c->next, CONF_PARAM, "GeoIPDenyFilter", FALSE);
-      continue;
-    }
+    for (i = 0; i < filters->nelts; i++) {
+      int filter_id, res;
+      struct geoip_filter *filter;
+      pr_regex_t *filter_re;
+      const char *filter_name, *filter_pattern, *filter_value;
 
-    filter_name = get_geoip_filter_name(filter_id);
+      filter = ((struct geoip_filter **) filters->elts)[i];
+      filter_id = filter->filter_id;
+      filter_pattern = filter->filter_pattern;
+      filter_re = filter->filter_re;
 
-    res = pr_regexp_exec(filter_re, filter_value, 0, NULL, 0, 0, 0);
-    pr_trace_msg(trace_channel, 12,
-      "%s filter value %s %s GeoIPDenyFilter pattern '%s'",
-      filter_name, filter_value, res == 0 ? "matched" : "did not match",
-      filter_pattern);
+      filter_value = get_geoip_filter_value(filter_id);
+      if (filter_value == NULL) {
+        matched = FALSE;
+        break;
+      }
+
+      filter_name = get_geoip_filter_name(filter_id);
+
+      res = pr_regexp_exec(filter_re, filter_value, 0, NULL, 0, 0, 0);
+      pr_trace_msg(trace_channel, 12,
+        "%s filter value %s %s GeoIPDenyFilter pattern '%s'",
+        filter_name, filter_value, res == 0 ? "matched" : "did not match",
+        filter_pattern);
+      if (res == 0) {
+        (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+          "%s filter value '%s' matched GeoIPDenyFilter pattern '%s'",
+          filter_name, filter_value, filter_pattern);
+      } else {
+        (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
+          "%s filter value '%s' did not match GeoIPDenyFilter pattern '%s'",
+          filter_name, filter_value, filter_pattern);
+        matched = FALSE;
+        break;
+      }
+    }
 
-    if (res == 0) {
-      (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
-        "%s filter value '%s' matched GeoIPDenyFilter pattern '%s'",
-        filter_name, filter_value, filter_pattern);
+    if (matched == TRUE) {
       matched_deny_filter = TRUE;
       break;
     }
 
-    (void) pr_log_writefile(geoip_logfd, MOD_GEOIP_VERSION,
-      "%s filter value '%s' did not match GeoIPDenyFilter pattern '%s'",
-      filter_name, filter_value, filter_pattern);
-
     c = find_config_next(c, c->next, CONF_PARAM, "GeoIPDenyFilter", FALSE);
   }
 #endif /* !HAVE_REGEX_H or !HAVE_REGCOMP */
@@ -984,52 +1195,71 @@ static void set_geoip_values(void) {
  */
 
 /* usage:
- *  GeoIPAllowFilter key regex
- *  GeoIPDenyFilter key regex
+ *  GeoIPAllowFilter key1 regex1 [key2 regex2 ...]
+ *                   sql:/...
+ *  GeoIPDenyFilter key1 regex1 [key2 regex2 ...]
+ *                  sql:/...
  */
 MODRET set_geoipfilter(cmd_rec *cmd) {
 #if PR_USE_REGEX
-  register unsigned int i;
   config_rec *c;
-  pr_regex_t *pre;
-  int filter_id = -1, res;
+  array_header *deferred_patterns, *filters;
 
-  CHECK_ARGS(cmd, 2);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  /* Make sure a supported filter key was configured. */
-  for (i = 0; geoip_filter_keys[i].filter_name != NULL; i++) {
-    if (strcasecmp(cmd->argv[1], geoip_filter_keys[i].filter_name) == 0) {
-      filter_id = geoip_filter_keys[i].filter_id;
-      break;
-    }
+  if (cmd->argc == 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
   }
 
-  if (filter_id == -1) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown GeoIP filter name '",
-      cmd->argv[1], "'", NULL));
+  /* IFF the first parameter starts with "sql:/", then we expect ONLY one
+   * parameter.  If not, then we expect an even number of parameters.
+   */
+
+  if (strncmp(cmd->argv[1], "sql:/", 5) == 0) {
+    if (cmd->argc > 2) {
+      CONF_ERROR(cmd, "wrong number of parameters");
+    }
+
+  } else {
+    if ((cmd->argc-1) % 2 != 0) {
+      CONF_ERROR(cmd, "wrong number of parameters");
+    }
   }
 
-  pre = pr_regexp_alloc(&geoip_module);
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
+  filters = make_array(c->pool, 0, sizeof(struct geoip_filter *));
+  deferred_patterns = make_array(c->pool, 0, sizeof(char *));
 
-  res = pr_regexp_compile(pre, cmd->argv[2], REG_EXTENDED|REG_NOSUB|REG_ICASE);
-  if (res != 0) {
-    char errstr[256];
+  if (cmd->argc == 2) {
+    const char *pattern;
 
-    memset(errstr, '\0', sizeof(errstr));
-    pr_regexp_error(res, pre, errstr, sizeof(errstr)-1);
-    pr_regexp_free(&geoip_module, pre);
+    pattern = cmd->argv[1];
 
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "pattern '", cmd->argv[2],
-      "' failed regex compilation: ", errstr, NULL));
-  }
+    /* Advance past the "sql:/" prefix. */
+    *((char **) push_array(deferred_patterns)) = pstrdup(c->pool, pattern + 5);
 
-  c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
-  c->argv[0] = palloc(c->pool, sizeof(int));
-  *((int *) c->argv[0]) = filter_id;
-  c->argv[1] = pstrdup(c->pool, cmd->argv[2]);
-  c->argv[2] = pre;
+  } else {
+    register unsigned int i;
+
+    for (i = 1; i < cmd->argc; i += 2) {
+      const char *filter_name, *pattern = NULL;
+      struct geoip_filter *filter;
 
+      filter_name = cmd->argv[i];
+      pattern = cmd->argv[i+1];
+
+      filter = make_filter(c->pool, filter_name, pattern);
+      if (filter == NULL) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '",
+          filter_name, " ", pattern, "' as filter: ", strerror(errno), NULL));
+      }
+
+      *((struct geoip_filter **) push_array(filters)) = filter;
+    }
+  }
+
+  c->argv[0] = filters;
+  c->argv[1] = deferred_patterns;
   return PR_HANDLED(cmd);
 
 #else /* no regular expression support at the moment */
@@ -1156,6 +1386,9 @@ MODRET geoip_post_pass(cmd_rec *cmd) {
     return PR_DECLINED(cmd);
   }
 
+  /* Scan for any deferred GeoIP filters and resolve them. */
+  resolve_deferred_filters(cmd->tmp_pool);
+
   /* Modules such as mod_ifsession may have added new filters; check the
    * filters again.
    */
@@ -1168,6 +1401,7 @@ MODRET geoip_post_pass(cmd_rec *cmd) {
       ": Connection denied to %s due to GeoIP filter/policy",
       pr_netaddr_get_ipstr(session.c->remote_addr));
 
+    pr_event_generate("mod_geoip.connection-denied", NULL);
     pr_session_disconnect(&geoip_module, PR_SESS_DISCONNECT_CONFIG_ACL,
       "GeoIP Filters");
   }
@@ -1300,7 +1534,7 @@ static int geoip_sess_init(void) {
   get_geoip_info(sess_geoips);
 
   c = find_config(main_server->conf, CONF_PARAM, "GeoIPPolicy", FALSE);
-  if (c) {
+  if (c != NULL) {
     geoip_policy = *((geoip_policy_e *) c->argv[0]);
   }
 
@@ -1327,6 +1561,8 @@ static int geoip_sess_init(void) {
       ": Connection denied to %s due to GeoIP filter/policy",
       pr_netaddr_get_ipstr(session.c->remote_addr));
 
+    pr_event_generate("mod_geoip.connection-denied", NULL);
+
     /* XXX send_geoip_mesg(tmp_pool, mesg) */
     destroy_pool(tmp_pool);
 
diff --git a/contrib/mod_ifsession.c b/contrib/mod_ifsession.c
index 06ef869..3466335 100644
--- a/contrib/mod_ifsession.c
+++ b/contrib/mod_ifsession.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_ifsession -- a module supporting conditional
  *                            per-user/group/class configuration contexts.
- *
- * Copyright (c) 2002-2014 TJ Saunders
+ * Copyright (c) 2002-2016 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
@@ -23,10 +22,8 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * This is mod_ifsession, contrib software for proftpd 1.2 and above.
+ * This is mod_ifsession, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_ifsession.c,v 1.56 2014-02-15 18:54:00 castaglia Exp $
  */
 
 #include "conf.h"
@@ -58,6 +55,8 @@ static const char *ifsess_home_dir = NULL;
 /* For supporting DisplayLogin files in <IfUser>/<IfGroup> sections. */
 static pr_fh_t *displaylogin_fh = NULL;
 
+static int ifsess_sess_init(void);
+
 static const char *trace_channel = "ifsession";
 
 /* Necessary prototypes */
@@ -214,7 +213,8 @@ static char *ifsess_dir_interpolate(pool *p, const char *path) {
   }
 
   if (*ret == '~') {
-    char *interp_dir = NULL, *user, *ptr;
+    const char *user;
+    char *interp_dir = NULL, *ptr;
 
     user = pstrdup(p, ret+1);
     ptr = strchr(user, '/');
@@ -291,7 +291,7 @@ static void ifsess_resolve_dir(config_rec *c) {
   }
 
   /* Check for any expandable variables. */
-  c->name = path_subst_uservar(c->pool, &c->name);
+  c->name = (char *) path_subst_uservar(c->pool, (const char **) &c->name);
 
   /* Handle any '~' interpolation. */
   interp_dir = ifsess_dir_interpolate(c->pool, c->name);
@@ -346,15 +346,95 @@ void ifsess_resolve_server_dirs(server_rec *s) {
   }
 }
 
+static int ifsess_sess_merge_class(void) {
+  register unsigned int i = 0;
+  config_rec *c = NULL;
+  pool *tmp_pool = make_sub_pool(session.pool);
+  array_header *class_remove_list = make_array(tmp_pool, 1,
+    sizeof(config_rec *));
+
+  c = find_config(main_server->conf, -1, IFSESS_CLASS_TEXT, FALSE);
+  while (c != NULL) {
+    config_rec *list = NULL;
+
+    pr_signals_handle();
+
+    list = find_config(c->subset, IFSESS_CLASS_NUMBER, NULL, FALSE);
+    if (list != NULL) {
+      unsigned char mergein = FALSE;
+
+#ifdef PR_USE_REGEX
+      if (*((unsigned char *) list->argv[1]) == PR_EXPR_EVAL_REGEX) {
+        pr_regex_t *pre = list->argv[2];
+
+        if (session.conn_class != NULL) {
+          pr_log_debug(DEBUG8, MOD_IFSESSION_VERSION
+            ": evaluating regexp pattern '%s' against subject '%s'",
+            pr_regexp_get_pattern(pre), session.conn_class->cls_name);
+
+          if (pr_regexp_exec(pre, session.conn_class->cls_name, 0, NULL, 0, 0,
+              0) == 0) {
+            mergein = TRUE;
+          }
+        }
+
+      } else
+#endif /* regex support */
+
+      if (*((unsigned char *) list->argv[1]) == PR_EXPR_EVAL_OR &&
+          pr_expr_eval_class_or((char **) &list->argv[2]) == TRUE) {
+        mergein = TRUE;
+
+      } else if (*((unsigned char *) list->argv[1]) == PR_EXPR_EVAL_AND &&
+          pr_expr_eval_class_and((char **) &list->argv[2]) == TRUE) {
+        mergein = TRUE;
+      }
+
+      if (mergein) {
+        pr_log_debug(DEBUG2, MOD_IFSESSION_VERSION
+          ": merging <IfClass %s> directives in", (char *) list->argv[0]);
+        ifsess_dup_set(session.pool, main_server->conf, c->subset);
+
+        /* Add this config_rec pointer to the list of pointers to be
+         * removed later.
+         */
+        *((config_rec **) push_array(class_remove_list)) = c;
+
+        /* Do NOT call fixup_dirs() here; we need to wait until after
+         * authentication to do so (in which case, mod_auth will handle the
+         * call to fixup_dirs() for us).
+         */
+
+        ifsess_merged = TRUE;
+
+      } else {
+        pr_log_debug(DEBUG9, MOD_IFSESSION_VERSION
+          ": <IfClass %s> not matched, skipping", (char *) list->argv[0]);
+      }
+    }
+
+    c = find_config_next(c, c->next, -1, IFSESS_CLASS_TEXT, FALSE);
+  }
+
+  /* Now, remove any <IfClass> config_recs that have been merged in. */
+  for (i = 0; i < class_remove_list->nelts; i++) {
+    c = ((config_rec **) class_remove_list->elts)[i];
+    xaset_remove(main_server->conf, (xasetmember_t *) c);
+  }
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
 /* Configuration handlers
  */
 
 MODRET start_ifctxt(cmd_rec *cmd) {
   config_rec *c = NULL;
   int config_type = 0, eval_type = 0;
-  int argc = 0;
+  unsigned int argc = 0;
   char *name = NULL;
-  char **argv = NULL;
+  void **argv = NULL;
   array_header *acl = NULL;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
@@ -465,18 +545,22 @@ MODRET start_ifctxt(cmd_rec *cmd) {
     argv = cmd->argv;
   }
 
-  acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
+  acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
+  if (acl == NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error creating regex expression: ",
+      strerror(errno), NULL));
+  }
 
   c = add_config_param(name, 0);
 
   c->config_type = config_type;
   c->argc = acl->nelts + 2;
-  c->argv = pcalloc(c->pool, (c->argc + 2) * sizeof(char *));
+  c->argv = pcalloc(c->pool, (c->argc + 2) * sizeof(void *));
   c->argv[0] = pstrdup(c->pool, cmd->arg);
   c->argv[1] = pcalloc(c->pool, sizeof(unsigned char));
   *((unsigned char *) c->argv[1]) = eval_type;
 
-  argv = (char **) c->argv + 2;
+  argv = c->argv + 2;
 
   if (acl) {
     while (acl->nelts--) {
@@ -526,7 +610,8 @@ MODRET end_ifctxt(cmd_rec *cmd) {
 
 MODRET ifsess_pre_pass(cmd_rec *cmd) {
   config_rec *c;
-  char *displaylogin = NULL, *sess_user, *sess_group, *user, *group = NULL;
+  const char *user = NULL, *group = NULL, *sess_user, *sess_group;
+  char *displaylogin = NULL;
   array_header *gids = NULL, *groups = NULL, *sess_groups = NULL;
   struct passwd *pw = NULL;
   struct group *gr = NULL;
@@ -1062,83 +1147,10 @@ static int ifsess_init(void) {
 }
 
 static int ifsess_sess_init(void) {
-  register unsigned int i = 0;
-  config_rec *c = NULL;
-  pool *tmp_pool = make_sub_pool(session.pool);
-  array_header *class_remove_list = make_array(tmp_pool, 1,
-    sizeof(config_rec *));
-
-  c = find_config(main_server->conf, -1, IFSESS_CLASS_TEXT, FALSE);
-
-  while (c) {
-    config_rec *list = NULL;
-
-    pr_signals_handle();
-
-    list = find_config(c->subset, IFSESS_CLASS_NUMBER, NULL, FALSE);
-    if (list != NULL) {
-      unsigned char mergein = FALSE;
-
-#ifdef PR_USE_REGEX
-      if (*((unsigned char *) list->argv[1]) == PR_EXPR_EVAL_REGEX) {
-        pr_regex_t *pre = list->argv[2];
-
-        if (session.conn_class != NULL) {
-          pr_log_debug(DEBUG8, MOD_IFSESSION_VERSION
-            ": evaluating regexp pattern '%s' against subject '%s'",
-            pr_regexp_get_pattern(pre), session.conn_class->cls_name);
-
-          if (pr_regexp_exec(pre, session.conn_class->cls_name, 0, NULL, 0, 0,
-              0) == 0) {
-            mergein = TRUE;
-          }
-        }
-
-      } else
-#endif /* regex support */
-
-      if (*((unsigned char *) list->argv[1]) == PR_EXPR_EVAL_OR &&
-          pr_expr_eval_class_or((char **) &list->argv[2]) == TRUE) {
-        mergein = TRUE;
-
-      } else if (*((unsigned char *) list->argv[1]) == PR_EXPR_EVAL_AND &&
-          pr_expr_eval_class_and((char **) &list->argv[2]) == TRUE) {
-        mergein = TRUE;
-      }
-
-      if (mergein) {
-        pr_log_debug(DEBUG2, MOD_IFSESSION_VERSION
-          ": merging <IfClass %s> directives in", (char *) list->argv[0]);
-        ifsess_dup_set(session.pool, main_server->conf, c->subset);
-
-        /* Add this config_rec pointer to the list of pointers to be
-         * removed later.
-         */
-        *((config_rec **) push_array(class_remove_list)) = c;
-
-        /* Do NOT call fixup_dirs() here; we need to wait until after
-         * authentication to do so (in which case, mod_auth will handle the
-         * call to fixup_dirs() for us).
-         */
-
-        ifsess_merged = TRUE;
-
-      } else {
-        pr_log_debug(DEBUG9, MOD_IFSESSION_VERSION
-          ": <IfClass %s> not matched, skipping", (char *) list->argv[0]);
-      }
-    }
-
-    c = find_config_next(c, c->next, -1, IFSESS_CLASS_TEXT, FALSE);
+  if (ifsess_sess_merge_class() < 0) {
+    return -1;
   }
 
-  /* Now, remove any <IfClass> config_recs that have been merged in. */
-  for (i = 0; i < class_remove_list->nelts; i++) {
-    c = ((config_rec **) class_remove_list->elts)[i];
-    xaset_remove(main_server->conf, (xasetmember_t *) c);
-  }
-
-  destroy_pool(tmp_pool);
   return 0;
 }
 
diff --git a/contrib/mod_ifversion.c b/contrib/mod_ifversion.c
index b73f51c..c1333f5 100644
--- a/contrib/mod_ifversion.c
+++ b/contrib/mod_ifversion.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_ifversion -- a module supporting conditional configuration
  *                           depending on the proftpd server version
  *
- * Copyright (c) 2009-2013 TJ Saunders
+ * Copyright (c) 2009-2015 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
@@ -25,8 +25,6 @@
  *
  * This is mod_ifversion, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_ifversion.c,v 1.5 2013-02-15 22:46:42 castaglia Exp $
  */
 
 #include "conf.h"
@@ -34,8 +32,8 @@
 #define MOD_IFVERSION_VERSION	"mod_ifversion/0.1"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030101
-# error "ProFTPD 1.3.1rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 /* Support routines
@@ -443,12 +441,13 @@ MODRET start_ifversion(cmd_rec *cmd) {
   if ((matched && !negated) ||
       (!matched && negated)) {
     pr_log_debug(DEBUG3, "%s: using '%s %s' section at line %u",
-      cmd->argv[0], cmd->argv[1], cmd->argv[2], pr_parser_get_lineno());
-      return PR_HANDLED(cmd);
+      (char *) cmd->argv[0], (char *) cmd->argv[1], (char *) cmd->argv[2],
+      pr_parser_get_lineno()); return PR_HANDLED(cmd);
   }
 
   pr_log_debug(DEBUG3, "%s: skipping '%s %s' section at line %u",
-    cmd->argv[0], cmd->argv[1], cmd->argv[2], pr_parser_get_lineno());
+    (char *) cmd->argv[0], (char *) cmd->argv[1], (char *) cmd->argv[2],
+    pr_parser_get_lineno());
 
   while (ifversion_ctx_count > 0 &&
          (config_line = pr_parser_read_line(buf, sizeof(buf))) != NULL) {
diff --git a/contrib/mod_ldap.c b/contrib/mod_ldap.c
index f4eb85a..30fa369 100644
--- a/contrib/mod_ldap.c
+++ b/contrib/mod_ldap.c
@@ -1,7 +1,7 @@
 /*
  * mod_ldap - LDAP password lookup module for ProFTPD
  * Copyright (c) 1999-2013, John Morrissey <jwm at horde.net>
- * Copyright (c) 2013-2016 The ProFTPD Project
+ * Copyright (c) 2013-2017 The ProFTPD Project
  *
  * 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
@@ -22,7 +22,6 @@
  * source code for OpenSSL in the source distribution.
  *
  * -----DO NOT EDIT BELOW THIS LINE-----
- * $Id: mod_ldap.c,v 1.107 2013-11-24 00:45:28 castaglia Exp $
  * $Libraries: -lldap -llber$
  */
 
@@ -42,7 +41,18 @@
 #include <lber.h>
 #include <ldap.h>
 
+module ldap_module;
+
 static int ldap_logfd = -1;
+static pool *ldap_pool = NULL;
+
+static const char *trace_channel = "ldap";
+#if defined(LBER_OPT_LOG_PRINT_FN)
+static const char *libtrace_channel = "ldap.library";
+#endif
+
+/* Necessary prototypes */
+static int ldap_sess_init(void);
 
 #if LDAP_API_VERSION >= 2000
 # define HAS_LDAP_SASL_BIND_S
@@ -100,14 +110,14 @@ static void pr_ldap_set_sizelimit(LDAP *limit_ld, int limit) {
       limit, ldap_err2string(res));
 
   } else {
-    (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+    pr_trace_msg(trace_channel, 5,
       "set search query size limit to %d entries", limit);
   }
 
 #else
   limit_ld->ld_sizelimit = limit;
 
-  (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+  pr_trace_msg(trace_channel, 5,
     "set search query size limit to %d entries", limit);
 #endif
 }
@@ -169,8 +179,8 @@ static struct timeval ldap_querytimeout_tv;
 static uid_t ldap_defaultuid = -1;
 static gid_t ldap_defaultgid = -1;
 
-#ifdef LDAP_OPT_X_TLS
-static int ldap_use_tls = 0;
+#if defined(LDAP_OPT_X_TLS)
+static int ldap_use_tls = FALSE;
 #endif
 
 static LDAP *ld = NULL;
@@ -181,8 +191,8 @@ static void pr_ldap_unbind(void) {
   int res;
 
   if (ld == NULL) {
-    (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-     "not unbinding to an already unbound connection");
+    pr_trace_msg(trace_channel, 13,
+      "not unbinding to an already unbound connection");
     return;
   }
 
@@ -192,7 +202,7 @@ static void pr_ldap_unbind(void) {
       "error unbinding connection: %s", ldap_err2string(res));
 
   } else {
-    (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+    pr_trace_msg(trace_channel, 8,
       "connection successfully unbound");
   }
 
@@ -263,12 +273,25 @@ static int do_ldap_connect(LDAP **conn_ld, int do_bind) {
     "connected to %s:%d", ldap_server ? ldap_server : "(null)", ldap_port);
 #endif /* HAS_LDAP_INITIALIZE */
 
-#ifdef LDAP_OPT_X_TLS
-  if (ldap_use_tls == 1) {
+#if defined(LDAP_OPT_X_TLS)
+  if (ldap_use_tls == TRUE) {
     res = ldap_start_tls_s(*conn_ld, NULL, NULL);
     if (res != LDAP_SUCCESS) {
-      (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-       "failed to start TLS: %s", ldap_err2string(res));
+      char *diag_msg = NULL;
+
+      ldap_get_option(*conn_ld, LDAP_OPT_DIAGNOSTIC_MESSAGE,
+        (void *) &diag_msg);
+
+      if (diag_msg != NULL) {
+        (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+         "failed to start TLS: %s: %s", ldap_err2string(res), diag_msg);
+        ldap_memfree(diag_msg);
+
+      } else {
+        (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+         "failed to start TLS: %s", ldap_err2string(res));
+      }
+
       pr_ldap_unbind();
       return -1;
     }
@@ -329,8 +352,14 @@ static int do_ldap_connect(LDAP **conn_ld, int do_bind) {
   return 1;
 }
 
+#if defined(LBER_OPT_LOG_PRINT_FN)
+static void ldap_tracelog_cb(const char *msg) {
+  (void) pr_trace_msg(libtrace_channel, 1, "%s", msg);
+}
+#endif /* no LBER_OPT_LOG_PRINT_FN */
+
 static int pr_ldap_connect(LDAP **conn_ld, int do_bind) {
-  int start_server_index;
+  unsigned int start_server_index;
   char *item;
   LDAPURLDesc *url;
 
@@ -350,8 +379,10 @@ static int pr_ldap_connect(LDAP **conn_ld, int do_bind) {
     /* item might be NULL if no LDAPServer directive was specified
      * and we're using the SDK default.
      */
-    if (item) {
+    if (item != NULL) {
       if (ldap_is_ldap_url(item)) {
+        char *url_desc;
+
         if (ldap_url_parse(item, &url) != LDAP_URL_SUCCESS) {
           (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
             "URL %s was valid during server startup, but is no longer valid?!",
@@ -364,6 +395,13 @@ static int pr_ldap_connect(LDAP **conn_ld, int do_bind) {
           continue;
         }
 
+        url_desc = ldap_url_desc2str(url);
+        if (url_desc != NULL) {
+          (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+            "parsed '%s' as '%s'", item, url_desc);
+          ldap_memfree(url_desc);
+        }
+
 #ifdef HAS_LDAP_INITIALIZE
         ldap_server_url = item;
 #else /* HAS_LDAP_INITIALIZE */
@@ -380,9 +418,8 @@ static int pr_ldap_connect(LDAP **conn_ld, int do_bind) {
         if (url->lud_scope != LDAP_SCOPE_DEFAULT) {
           ldap_search_scope = url->lud_scope;
           if (ldap_search_scope == LDAP_SCOPE_BASE) {
-            pr_log_debug(DEBUG3, MOD_LDAP_VERSION
-              ": WARNING: LDAP URL search scopes default to 'base' (not 'sub') "
-              "and may not be what you want");
+            pr_log_debug(DEBUG0, MOD_LDAP_VERSION
+              ": WARNING: LDAP URL search scopes default to 'base', not 'subtree', and may not be what you want (see LDAPSearchScope)");
           }
         }
 
@@ -400,6 +437,17 @@ static int pr_ldap_connect(LDAP **conn_ld, int do_bind) {
     }
 
     if (do_ldap_connect(conn_ld, do_bind) == 1) {
+      /* This debug level value should be LDAP_DEBUG_ANY, but that macro is, I
+       * think, OpenLDAP-specific.
+       */
+      int debug_level = -1, res;
+
+      res = ldap_set_option(*conn_ld, LDAP_OPT_DEBUG_LEVEL, &debug_level);
+      if (res != LDAP_OPT_SUCCESS) {
+        (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+          "error setting DEBUG_ANY debug level: %s", ldap_err2string(res));
+      }
+
       return 1;
     }
 
@@ -413,9 +461,9 @@ static int pr_ldap_connect(LDAP **conn_ld, int do_bind) {
   return -1;
 }
 
-static char *pr_ldap_interpolate_filter(pool *p, char *template,
+static const char *pr_ldap_interpolate_filter(pool *p, char *template,
     const char *value) {
-  char *escaped_value, *filter;
+  const char *escaped_value, *filter;
 
   escaped_value = sreplace(p, (char *) value,
     "\\", "\\\\",
@@ -445,8 +493,8 @@ static char *pr_ldap_interpolate_filter(pool *p, char *template,
   return filter;
 }
 
-static LDAPMessage *pr_ldap_search(char *basedn, char *filter, char *attrs[],
-    int sizelimit, int retry) {
+static LDAPMessage *pr_ldap_search(const char *basedn, const char *filter,
+    char *attrs[], int sizelimit, int retry) {
   int res;
   LDAPMessage *result;
 
@@ -497,8 +545,9 @@ static LDAPMessage *pr_ldap_search(char *basedn, char *filter, char *attrs[],
 }
 
 static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
-    const char *replace, char *basedn, char *attrs[], char **user_dn) {
-  char *filter, *dn;
+    const char *replace, const char *basedn, char *attrs[], char **user_dn) {
+  const char *filter;
+  char *dn;
   int i = 0;
   struct passwd *pw;
   LDAPMessage *result, *e;
@@ -532,7 +581,7 @@ static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
     return NULL;
   }
 
-  pw = pcalloc(session.pool, sizeof(struct passwd));
+  pw = pcalloc(ldap_pool, sizeof(struct passwd));
   while (attrs[i] != NULL) {
     (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
       "fetching values for attribute %s", attrs[i]);
@@ -562,7 +611,7 @@ static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
         ++i;
 
         (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-          "using LDAPDefaultUID %lu", (unsigned long) pw->pw_uid);
+          "using LDAPDefaultUID %s", pr_uid2str(NULL, pw->pw_uid));
         continue;
       }
 
@@ -581,19 +630,27 @@ static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
         ++i;
 
         (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-          "using LDAPDefaultGID %lu", (unsigned long) pw->pw_gid);
+          "using LDAPDefaultGID %s", pr_gid2str(NULL, pw->pw_gid));
         continue;
       }
 
       if (strcasecmp(attrs[i], ldap_attr_homedirectory) == 0) {
         if (ldap_genhdir == FALSE ||
-            ldap_genhdir_prefix == FALSE ||
             ldap_genhdir_prefix == NULL) {
           dn = ldap_get_dn(ld, e);
 
-          (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-            "no %s attribute for DN %s, LDAPGenerateHomedirPrefix not "
-            "configured", ldap_attr_homedirectory, dn);
+          if (ldap_genhdir == FALSE) {
+            (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+              "no %s attribute for DN %s, LDAPGenerateHomedir not enabled",
+              ldap_attr_homedirectory, dn);
+
+          } else {
+            (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+              "no %s attribute for DN %s, LDAPGenerateHomedir enabled but "
+              "LDAPGenerateHomedirPrefix not configured",
+              ldap_attr_homedirectory, dn);
+          }
+
           free(dn);
           return NULL;
         }
@@ -683,16 +740,29 @@ static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
     } else if (strcasecmp(attrs[i], ldap_attr_homedirectory) == 0) {
       if (ldap_forcegenhdir == TRUE) {
         if (ldap_genhdir == FALSE ||
-            ldap_genhdir_prefix == FALSE ||
             ldap_genhdir_prefix == NULL) {
-          (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-            "LDAPForceGeneratedHomedir enabled, but LDAPGenerateHomedir "
-            "is not");
+
+          if (ldap_genhdir == FALSE) {
+            (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+              "LDAPForceGeneratedHomedir enabled but LDAPGenerateHomedir is "
+              "not enabled");
+
+          } else {
+            (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+              "LDAPForceGeneratedHomedir and LDAPGenerateHomedir enabled, but "
+              "missing required LDAPGenerateHomedirPrefix");
+          }
+
           return NULL;
         }
 
+        if (pw->pw_dir != NULL) {
+          pr_trace_msg(trace_channel, 8, "LDAPForceGeneratedHomedir in effect, "
+            "overriding current LDAP home directory '%s'", pw->pw_dir);
+        }
+
         if (ldap_genhdir_prefix_nouname == TRUE) {
-          pw->pw_dir = pstrcat(session.pool, ldap_genhdir_prefix, NULL);
+          pw->pw_dir = pstrdup(session.pool, ldap_genhdir_prefix);
 
         } else {
           LDAP_VALUE_T **canon_username;
@@ -716,6 +786,9 @@ static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
         pw->pw_dir = pstrdup(session.pool, LDAP_VALUE(values, 0));
       }
 
+      pr_trace_msg(trace_channel, 8, "using LDAP home directory '%s'",
+        pw->pw_dir);
+
     } else if (strcasecmp(attrs[i], ldap_attr_loginshell) == 0) {
       pw->pw_shell = pstrdup(session.pool, LDAP_VALUE(values, 0));
 
@@ -739,15 +812,16 @@ static struct passwd *pr_ldap_user_lookup(pool *p, char *filter_template,
   ldap_msgfree(result);
 
   (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-    "found user %s, UID %lu, GID %lu, homedir %s, shell %s",
-    pw->pw_name, (unsigned long) pw->pw_uid, (unsigned long) pw->pw_gid,
+    "found user %s, UID %s, GID %s, homedir %s, shell %s",
+    pw->pw_name, pr_uid2str(p, pw->pw_uid), pr_gid2str(p, pw->pw_gid),
     pw->pw_dir, pw->pw_shell);
   return pw;
 }
 
 static struct group *pr_ldap_group_lookup(pool *p, char *filter_template,
     const char *replace, char *attrs[]) {
-  char *filter, *dn;
+  const char *filter;
+  char *dn;
   int i = 0, value_count = 0, value_offset;
   struct group *gr;
   LDAPMessage *result, *e;
@@ -835,7 +909,7 @@ static struct group *pr_ldap_group_lookup(pool *p, char *filter_template,
   ldap_msgfree(result);
 
   (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-    "found group %s, GID %lu", gr->gr_name, (unsigned long) gr->gr_gid);
+    "found group %s, GID %s", gr->gr_name, pr_gid2str(NULL, gr->gr_gid));
   for (i = 0; i < value_count; ++i) {
     (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
       "+ member: %s", gr->gr_mem[i]);
@@ -865,9 +939,9 @@ static void parse_quota(pool *p, const char *replace, char *str) {
 }
 
 static unsigned char pr_ldap_quota_lookup(pool *p, char *filter_template,
-    const char *replace, char *basedn) {
-  char *filter = NULL,
-       *attrs[] = {
+    const char *replace, const char *basedn) {
+  const char *filter = NULL;
+  char *attrs[] = {
          ldap_attr_ftpquota, ldap_attr_ftpquota_profiledn, NULL,
        };
   int orig_scope, res;
@@ -880,7 +954,7 @@ static unsigned char pr_ldap_quota_lookup(pool *p, char *filter_template,
     return FALSE;
   }
 
-  if (filter_template) {
+  if (filter_template != NULL) {
     filter = pr_ldap_interpolate_filter(p, filter_template, replace);
     if (filter == NULL) {
       return FALSE;
@@ -933,8 +1007,8 @@ static unsigned char pr_ldap_quota_lookup(pool *p, char *filter_template,
 
     } else {
       (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-        "no entries for filter %s, using default quota %s",
-        filter ? filter : "(null)", ldap_default_quota);
+        "no entries for filter %s, using default quota %s", filter,
+        ldap_default_quota);
     }
 
     parse_quota(p, replace, pstrdup(p, ldap_default_quota));
@@ -992,7 +1066,8 @@ static unsigned char pr_ldap_quota_lookup(pool *p, char *filter_template,
 
 static unsigned char pr_ldap_ssh_pubkey_lookup(pool *p, char *filter_template,
     const char *replace, char *basedn) {
-  char *filter, *attrs[] = {
+  const char *filter;
+  char *attrs[] = {
     ldap_attr_ssh_pubkey, NULL,
   };
   int num_keys, i;
@@ -1060,20 +1135,18 @@ static struct group *pr_ldap_getgrnam(pool *p, const char *group_name) {
 }
 
 static struct group *pr_ldap_getgrgid(pool *p, gid_t gid) {
-  char gidstr[PR_TUNABLE_BUFFER_SIZE] = {'\0'},
-       *group_attrs[] = {
-         ldap_attr_cn, ldap_attr_gidnumber, ldap_attr_memberuid, NULL,
-       };
-
-  snprintf(gidstr, sizeof(gidstr), "%lu", (unsigned long) gid);
+  const char *gidstr;
+  char *group_attrs[] = {
+    ldap_attr_cn, ldap_attr_gidnumber, ldap_attr_memberuid, NULL,
+  };
 
-  return pr_ldap_group_lookup(p, ldap_group_gid_filter, (const char *) gidstr,
-    group_attrs);
+  gidstr = pr_gid2str(p, gid);
+  return pr_ldap_group_lookup(p, ldap_group_gid_filter, gidstr, group_attrs);
 }
 
 static struct passwd *pr_ldap_getpwnam(pool *p, const char *username) {
-  char *filter,
-       *name_attrs[] = {
+  const char *filter;
+  char *name_attrs[] = {
          ldap_attr_userpassword, ldap_attr_uid, ldap_attr_uidnumber,
          ldap_attr_gidnumber, ldap_attr_homedirectory,
          ldap_attr_loginshell, NULL,
@@ -1110,20 +1183,19 @@ static struct passwd *pr_ldap_getpwnam(pool *p, const char *username) {
 }
 
 static struct passwd *pr_ldap_getpwuid(pool *p, uid_t uid) {
-  char uidstr[PR_TUNABLE_BUFFER_SIZE] = {'\0'},
-       *uid_attrs[] = {
-         ldap_attr_uid, ldap_attr_uidnumber, ldap_attr_gidnumber,
-         ldap_attr_homedirectory, ldap_attr_loginshell, NULL,
-       };
-
-  snprintf(uidstr, sizeof(uidstr), "%lu", (unsigned long) uid);
+  const char *uidstr;
+  char *uid_attrs[] = {
+    ldap_attr_uid, ldap_attr_uidnumber, ldap_attr_gidnumber,
+    ldap_attr_homedirectory, ldap_attr_loginshell, NULL,
+  };
 
-  return pr_ldap_user_lookup(p, ldap_user_uid_filter, (const char *) uidstr,
+  uidstr = pr_uid2str(p, uid);
+  return pr_ldap_user_lookup(p, ldap_user_uid_filter, uidstr,
     ldap_user_basedn, uid_attrs, ldap_authbinds ? &ldap_authbind_dn : NULL);
 }
 
 MODRET handle_ldap_quota_lookup(cmd_rec *cmd) {
-  char *basedn;
+  const char *basedn;
 
   basedn = pr_ldap_interpolate_filter(cmd->tmp_pool,
     ldap_user_basedn, cmd->argv[0]);
@@ -1141,7 +1213,7 @@ MODRET handle_ldap_quota_lookup(cmd_rec *cmd) {
 
   } else {
     (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-      "returning cached quota for user %s", cmd->argv[0]);
+      "returning cached quota for user %s", (char *) cmd->argv[0]);
   }
 
   return mod_create_data(cmd, cached_quota);
@@ -1257,7 +1329,8 @@ MODRET ldap_auth_getgrgid(cmd_rec *cmd) {
 }
 
 MODRET ldap_auth_getgroups(cmd_rec *cmd) {
-  char *filter, *w[] = {
+  const char *filter;
+  char *w[] = {
     ldap_attr_gidnumber, ldap_attr_cn, NULL,
   };
   struct passwd *pw;
@@ -1281,15 +1354,15 @@ MODRET ldap_auth_getgroups(cmd_rec *cmd) {
     gr = pr_ldap_getgrgid(cmd->tmp_pool, pw->pw_gid);
     if (gr != NULL) {
       (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-        "adding user %s primary group %s/%lu", pw->pw_name, gr->gr_name,
-        (unsigned long) pw->pw_gid);
+        "adding user %s primary group %s/%s", pw->pw_name, gr->gr_name,
+        pr_gid2str(NULL, pw->pw_gid));
       *((gid_t *) push_array(gids)) = pw->pw_gid;
       *((char **) push_array(groups)) = pstrdup(session.pool, gr->gr_name);
 
     } else {
       (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-        "unable to determine group name for user %s primary GID %lu, skipping",
-        pw->pw_name, (unsigned long) pw->pw_gid);
+        "unable to determine group name for user %s primary GID %s, skipping",
+        pw->pw_name, pr_gid2str(NULL, pw->pw_gid));
     }
   }
 
@@ -1340,7 +1413,7 @@ MODRET ldap_auth_getgroups(cmd_rec *cmd) {
 
       (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
         "added user %s secondary group %s/%s",
-        (pw && pw->pw_name) ? pw->pw_name : cmd->argv[0],
+        (pw && pw->pw_name) ? pw->pw_name : (char *) cmd->argv[0],
         LDAP_VALUE(cn, 0), LDAP_VALUE(gidNumber, 0));
     }
 
@@ -1367,19 +1440,21 @@ return_groups:
  * cmd->argv[1] : cleartext password
  */
 MODRET ldap_auth_auth(cmd_rec *cmd) {
-  const char *username = cmd->argv[0];
-  char *filter = NULL,
-       *pass_attrs[] = {
+  const char *filter = NULL, *username;
+  char *pass_attrs[] = {
          ldap_attr_userpassword, ldap_attr_uid, ldap_attr_uidnumber,
          ldap_attr_gidnumber, ldap_attr_homedirectory,
          ldap_attr_loginshell, NULL,
        };
   struct passwd *pw = NULL;
+  int res;
 
   if (ldap_do_users == FALSE) {
     return PR_DECLINED(cmd);
   }
 
+  username = cmd->argv[0];
+
   filter = pr_ldap_interpolate_filter(cmd->tmp_pool, ldap_user_basedn,
     username);
   if (filter == NULL) {
@@ -1410,10 +1485,18 @@ MODRET ldap_auth_auth(cmd_rec *cmd) {
     return PR_ERROR_INT(cmd, PR_AUTH_NOPWD);
   }
 
-  if (pr_auth_check(cmd->tmp_pool, ldap_authbinds ? NULL : pw->pw_passwd,
-      username, cmd->argv[1])) {
-    (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
-      "bad password for user %s", pw->pw_name);
+  res = pr_auth_check(cmd->tmp_pool, ldap_authbinds ? NULL : pw->pw_passwd,
+    username, cmd->argv[1]);
+  if (res != 0) {
+    if (res == -1) {
+      (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+        "bad password for user %s: %s", pw->pw_name, strerror(errno));
+
+    } else {
+      (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+        "bad password for user %s", pw->pw_name);
+    }
+
     return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
   }
 
@@ -1503,7 +1586,7 @@ MODRET ldap_auth_check(cmd_rec *cmd) {
 
   /* Check to see how the password is encrypted, and check accordingly. */
 
-  if (encname_len == strlen(cryptpass + 1)) {
+  if ((size_t) encname_len == strlen(cryptpass + 1)) {
     /* No leading {scheme}. */
     hash_method = ldap_defaultauthscheme;
     encname_len = 0;
@@ -1615,12 +1698,14 @@ MODRET set_ldaplog(cmd_rec *cmd) {
 MODRET set_ldapprotoversion(cmd_rec *cmd) {
   int i = 0;
   config_rec *c;
+  char *version;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  while (cmd->argv[1][i]) {
-    if (!PR_ISDIGIT((int) cmd->argv[1][i])) {
+  version = cmd->argv[1];
+  while (version[i]) {
+    if (!PR_ISDIGIT((int) version[i])) {
       CONF_ERROR(cmd, "LDAPProtocolVersion: argument must be numeric!");
     }
 
@@ -1629,14 +1714,14 @@ MODRET set_ldapprotoversion(cmd_rec *cmd) {
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(int));
-  *((int *) c->argv[0]) = atoi(cmd->argv[1]);
+  *((int *) c->argv[0]) = atoi(version);
 
   return PR_HANDLED(cmd);
 }
 
 MODRET set_ldapserver(cmd_rec *cmd) {
-  int i, len;
-  char *item;
+  register unsigned int i;
+  int len;
   LDAPURLDesc *url;
   array_header *urls = NULL;
   config_rec *c;
@@ -1649,44 +1734,58 @@ MODRET set_ldapserver(cmd_rec *cmd) {
   c->argv[0] = urls;
 
   for (i = 1; i < cmd->argc; ++i) {
-    if (ldap_is_ldap_url(cmd->argv[i])) {
-      if (ldap_url_parse(cmd->argv[i], &url) != LDAP_URL_SUCCESS) {
-        CONF_ERROR(cmd, "LDAPServer: must be supplied with a valid LDAP URL");
+    char *item;
+
+    item = cmd->argv[i];
+
+    if (ldap_is_ldap_url(item)) {
+      char *url_desc;
+
+      if (ldap_url_parse(item, &url) != LDAP_URL_SUCCESS) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+          "must be supplied with a valid LDAP URL: ", item, NULL));
       }
 
-      if (find_config(main_server->conf, CONF_PARAM, "LDAPSearchScope", FALSE)) {
+      url_desc = ldap_url_desc2str(url);
+      if (url_desc != NULL) {
+        pr_log_debug(DEBUG0, "%s: parsed URL '%s' as '%s'",
+          (char *) cmd->argv[0], item, url_desc);
+        ldap_memfree(url_desc);
+      }
+
+      if (find_config(cmd->server->conf, CONF_PARAM, "LDAPSearchScope", FALSE)) {
         CONF_ERROR(cmd, "LDAPSearchScope cannot be used when LDAPServer specifies a URL; specify a search scope in the LDAPServer URL instead");
       }
 
 #ifdef HAS_LDAP_INITIALIZE
-      if (strncasecmp(cmd->argv[i], "ldap:", strlen("ldap:")) != 0 &&
-          strncasecmp(cmd->argv[i], "ldaps:", strlen("ldaps:")) != 0) {
-
+      if (strncasecmp(item, "ldap:", 5) != 0 &&
+          strncasecmp(item, "ldaps:", 6) != 0) {
         CONF_ERROR(cmd, "Invalid scheme specified by LDAPServer URL: valid schemes are 'ldap' or 'ldaps'");
       }
 
 #else /* HAS_LDAP_INITIALIZE */
-      if (strncasecmp(cmd->argv[i], "ldap:", strlen("ldap:")) != 0) {
+      if (strncasecmp(item, "ldap:", 5) != 0) {
         CONF_ERROR(cmd, "Invalid scheme specified by LDAPServer URL: valid schemes are 'ldap'");
       }
 #endif /* HAS_LDAP_INITIALIZE */
 
-      if (url->lud_dn && strcmp(url->lud_dn, "") != 0) {
+      if (url->lud_dn != NULL &&
+          strcmp(url->lud_dn, "") != 0) {
         CONF_ERROR(cmd, "A base DN may not be specified by an LDAPServer URL, only by LDAPUsers or LDAPGroups");
       }
 
-      if (url->lud_filter && strcmp(url->lud_filter, "") != 0) {
+      if (url->lud_filter != NULL &&
+         strcmp(url->lud_filter, "") != 0) {
         CONF_ERROR(cmd, "A search filter may not be specified by an LDAPServer URL, only by LDAPUsers or LDAPGroups");
       }
 
       ldap_free_urldesc(url);
-      *((char **) push_array(urls)) = pstrdup(c->pool, cmd->argv[i]);
+      *((char **) push_array(urls)) = pstrdup(c->pool, item);
 
     } else {
-      /* Split non-URL arguments on whitespace and insert them as
-       * separate servers.
+      /* Split non-URL arguments on whitespace and insert them as separate
+       * servers.
        */
-      item = cmd->argv[i];
       while (*item) {
         len = strcspn(item, " \f\n\r\t\v");
         *((char **) push_array(urls)) = pstrndup(c->pool, item, len);
@@ -1702,6 +1801,7 @@ MODRET set_ldapserver(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: LDAPUseTLS on|off */
 MODRET set_ldapusetls(cmd_rec *cmd) {
 #ifndef LDAP_OPT_X_TLS
   CONF_ERROR(cmd, "LDAPUseTLS: Your LDAP libraries do not appear to support TLS");
@@ -1726,7 +1826,7 @@ MODRET set_ldapusetls(cmd_rec *cmd) {
 #endif /* LDAP_OPT_X_TLS */
 }
 
-MODRET set_ldapdninfo(cmd_rec *cmd) {
+MODRET set_ldapbinddn(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -1736,23 +1836,48 @@ MODRET set_ldapdninfo(cmd_rec *cmd) {
 
 MODRET set_ldapsearchscope(cmd_rec *cmd) {
   config_rec *c;
+  const char *scope_name;
+  int search_scope;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   c = find_config(main_server->conf, CONF_PARAM, "LDAPServer", FALSE);
-  if (c != NULL &&
-      ldap_is_ldap_url(c->argv[0])) {
-    CONF_ERROR(cmd, "LDAPSearchScope cannot be used when LDAPServer specifies a URL; specify a search scope in the LDAPServer URL instead");
-  }
+  if (c != NULL) {
+    register unsigned int i;
+    array_header *ldap_servers = NULL;
 
-    if (strcasecmp(cmd->argv[1], "base") != 0 &&
-        strcasecmp(cmd->argv[1], "onelevel") != 0 &&
-        strcasecmp(cmd->argv[1], "subtree") != 0) {
-      CONF_ERROR(cmd, "LDAPSearchScope: invalid search scope")
+    ldap_servers = c->argv[0];
+    for (i = 0; i < ldap_servers->nelts; i++) {
+      char *elt;
+
+      elt = ((char **) ldap_servers->elts)[i];
+      if (ldap_is_ldap_url(elt)) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "cannot be used when LDAPServer specifies a URL (see '", elt, "'); specify a search scope in the LDAPServer URL instead", NULL));
+      }
     }
+  }
+
+  scope_name = cmd->argv[1];
+
+  if (strcasecmp(scope_name, "base") == 0) {
+    search_scope = LDAP_SCOPE_BASE;
+
+  } else if (strcasecmp(scope_name, "one") == 0 ||
+             strcasecmp(scope_name, "onelevel") == 0) {
+    search_scope = LDAP_SCOPE_ONELEVEL;
+
+  } else if (strcasecmp(scope_name, "subtree") == 0) {
+    search_scope = LDAP_SCOPE_SUBTREE;
+
+  } else {
+    CONF_ERROR(cmd, "search scope must be one of: base, onelevel, subtree");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = search_scope;
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
   return PR_HANDLED(cmd);
 }
 
@@ -1879,36 +2004,38 @@ MODRET set_ldapusers(cmd_rec *cmd) {
 }
 
 MODRET set_ldapdefaultuid(cmd_rec *cmd) {
-  char *endptr;
   config_rec *c;
+  uid_t uid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(uid_t));
-  *((uid_t *) c->argv[0]) = strtoul(cmd->argv[1], &endptr, 10);
-  if (*endptr != '\0') {
+
+  if (pr_str2uid(cmd->argv[1], &uid) < 0) {
     CONF_ERROR(cmd, "LDAPDefaultUID: UID argument must be numeric");
   }
 
+  *((uid_t *) c->argv[0]) = uid;
   return PR_HANDLED(cmd);
 }
 
 MODRET set_ldapdefaultgid(cmd_rec *cmd) {
-  char *endptr;
   config_rec *c;
+  gid_t gid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(gid_t));
-  *((gid_t *) c->argv[0]) = strtoul(cmd->argv[1], &endptr, 10);
-  if (*endptr != '\0') {
+
+  if (pr_str2gid(cmd->argv[1], &gid) < 0) {
     CONF_ERROR(cmd, "LDAPDefaultGID: GID argument must be numeric");
   }
 
+  *((gid_t *) c->argv[0]) = gid;
   return PR_HANDLED(cmd);
 }
 
@@ -1968,10 +2095,17 @@ MODRET set_ldapgenhdir(cmd_rec *cmd) {
 }
 
 MODRET set_ldapgenhdirprefix(cmd_rec *cmd) {
+  char *prefix;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  prefix = cmd->argv[1];
+  if (strlen(prefix) == 0) {
+    CONF_ERROR(cmd, "must not be an empty string");
+  }
+
+  add_config_param_str(cmd->argv[0], 1, prefix);
   return PR_HANDLED(cmd);
 }
 
@@ -2047,6 +2181,69 @@ MODRET set_ldapdefaultquota(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void ldap_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&ldap_module, "core.session-reinit", ldap_sess_reinit_ev);
+
+  /* Restore defaults. */
+  (void) close(ldap_logfd);
+  ldap_logfd = -1;
+  ldap_protocol_version = 3;
+  ldap_servers = NULL;
+#if defined(LDAP_OPT_X_TLS)
+  ldap_use_tls = FALSE;
+#endif /* LDAP_OPT_X_TLS */
+  ldap_dn = NULL;
+  ldap_dnpass = NULL;
+  ldap_search_scope = LDAP_SCOPE_SUBTREE;
+  ldap_querytimeout = 0;
+  ldap_dereference = LDAP_DEREF_NEVER;
+  ldap_authbinds = TRUE;
+  ldap_defaultauthscheme = "crypt";
+  ldap_attr_uid = "uid";
+  ldap_attr_uidnumber = "uidNumber";
+  ldap_attr_gidnumber = "gidNumber";
+  ldap_attr_homedirectory = "homeDirectory";
+  ldap_attr_userpassword = "userPassword";
+  ldap_attr_loginshell = "loginShell";
+  ldap_attr_cn = "cn";
+  ldap_attr_memberuid = "memberUid";
+  ldap_attr_ftpquota = "ftpQuota";
+  ldap_attr_ftpquota_profiledn = "ftpQuotaProfileDN";
+  ldap_do_users = FALSE;
+  ldap_user_basedn = NULL;
+  ldap_user_name_filter = NULL;
+  ldap_user_uid_filter = NULL;
+  ldap_do_groups = FALSE;
+  ldap_group_name_filter = NULL;
+  ldap_group_gid_filter = NULL;
+  ldap_group_member_filter = NULL;
+  ldap_default_quota = NULL;
+  ldap_defaultuid = (uid_t) -1;
+  ldap_defaultgid = (gid_t) -1;
+  ldap_forcedefaultuid = FALSE;
+  ldap_forcedefaultgid = FALSE;
+  ldap_forcegenhdir = FALSE;
+  ldap_genhdir = FALSE;
+  ldap_genhdir_prefix = NULL;
+  ldap_genhdir_prefix_nouname = FALSE;
+
+  destroy_pool(ldap_pool);
+  ldap_pool = NULL;
+
+  res = ldap_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&ldap_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED,
+      NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -2059,10 +2256,15 @@ static int ldap_mod_init(void) {
 }
 
 static int ldap_sess_init(void) {
-  char *scope;
   config_rec *c;
   void *ptr;
 
+  pr_event_register(&ldap_module, "core.session-reinit", ldap_sess_reinit_ev,
+    NULL);
+
+  ldap_pool = make_sub_pool(session.pool);
+  pr_pool_tag(ldap_pool, MOD_LDAP_VERSION);
+
   c = find_config(main_server->conf, CONF_PARAM, "LDAPLog", FALSE);
   if (c != NULL) {
     char *path;
@@ -2070,7 +2272,7 @@ static int ldap_sess_init(void) {
     path = c->argv[0];
 
     if (strncasecmp(path, "none", 5) != 0) {
-      int res, xerrno;
+      int res, xerrno = 0;
 
       pr_signals_block();
       PRIVS_ROOT
@@ -2113,53 +2315,45 @@ static int ldap_sess_init(void) {
      * ldap_init()/ldap_initialize() will connect to the LDAP SDK's
      * default.
      */
-    ldap_servers = make_array(session.pool, 1, sizeof(char *));
+    ldap_servers = make_array(ldap_pool, 1, sizeof(char *));
     *((char **) push_array(ldap_servers)) = NULL;
   }
 
-#ifdef LDAP_OPT_X_TLS
+#if defined(LDAP_OPT_X_TLS)
   ptr = get_param_ptr(main_server->conf, "LDAPUseTLS", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_use_tls = *((int *) ptr);
   }
 #endif /* LDAP_OPT_X_TLS */
 
   c = find_config(main_server->conf, CONF_PARAM, "LDAPBindDN", FALSE);
   if (c != NULL) {
-    ldap_dn = pstrdup(session.pool, c->argv[0]);
-    ldap_dnpass = pstrdup(session.pool, c->argv[1]);
+    ldap_dn = pstrdup(ldap_pool, c->argv[0]);
+    ldap_dnpass = pstrdup(ldap_pool, c->argv[1]);
   }
 
-  scope = get_param_ptr(main_server->conf, "LDAPSearchScope", FALSE);
-  if (scope) {
-    if (strcasecmp(scope, "base") == 0) {
-      ldap_search_scope = LDAP_SCOPE_BASE;
-
-    } else if (strcasecmp(scope, "onelevel") == 0) {
-      ldap_search_scope = LDAP_SCOPE_ONELEVEL;
-
-    } else if (strcasecmp(scope, "subtree") == 0) {
-      ldap_search_scope = LDAP_SCOPE_SUBTREE;
-    }
+  c = find_config(main_server->conf, CONF_PARAM, "LDAPSearchScope", FALSE);
+  if (c != NULL) {
+    ldap_search_scope = *((int *) c->argv[0]);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPQueryTimeout", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_querytimeout = *((int *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPAliasDereference", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_dereference = *((int *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPAuthBinds", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_authbinds = *((int *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPDefaultAuthScheme", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_defaultauthscheme = (char *) ptr;
   }
 
@@ -2170,133 +2364,148 @@ static int ldap_sess_init(void) {
   if (c != NULL) {
     do {
       if (strcasecmp(c->argv[0], "uid") == 0) {
-        ldap_attr_uid = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_uid = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "uidNumber") == 0) {
-        ldap_attr_uidnumber = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_uidnumber = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "gidNumber") == 0) {
-        ldap_attr_gidnumber = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_gidnumber = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "homeDirectory") == 0) {
-        ldap_attr_homedirectory = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_homedirectory = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "userPassword") == 0) {
-        ldap_attr_userpassword = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_userpassword = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "loginShell") == 0) {
-        ldap_attr_loginshell = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_loginshell = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "cn") == 0) {
-        ldap_attr_cn = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_cn = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "memberUid") == 0) {
-        ldap_attr_memberuid = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_memberuid = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "ftpQuota") == 0) {
-        ldap_attr_ftpquota = pstrdup(session.pool, c->argv[1]);
+        ldap_attr_ftpquota = pstrdup(ldap_pool, c->argv[1]);
 
       } else if (strcasecmp(c->argv[0], "ftpQuotaProfileDN") == 0) {
-        ldap_attr_ftpquota_profiledn = pstrdup(session.pool, c->argv[1]);
-
+        ldap_attr_ftpquota_profiledn = pstrdup(ldap_pool, c->argv[1]);
       }
+
     } while ((c = find_config_next(c, c->next, CONF_PARAM, "LDAPAttr", FALSE)));
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "LDAPUsers", FALSE);
   if (c != NULL) {
     ldap_do_users = TRUE;
-    ldap_user_basedn = pstrdup(session.pool, c->argv[0]);
+    ldap_user_basedn = pstrdup(ldap_pool, c->argv[0]);
 
     if (c->argc > 1) {
-      ldap_user_name_filter = pstrdup(session.pool, c->argv[1]);
+      ldap_user_name_filter = pstrdup(ldap_pool, c->argv[1]);
 
     } else {
-      ldap_user_name_filter = pstrcat(session.pool,
+      ldap_user_name_filter = pstrcat(ldap_pool,
         "(&(", ldap_attr_uid, "=%v)(objectclass=posixAccount))", NULL);
     }
 
     if (c->argc > 2) {
-      ldap_user_uid_filter = pstrdup(session.pool, c->argv[2]);
+      ldap_user_uid_filter = pstrdup(ldap_pool, c->argv[2]);
 
     } else {
-      ldap_user_uid_filter = pstrcat(session.pool,
+      ldap_user_uid_filter = pstrcat(ldap_pool,
         "(&(", ldap_attr_uidnumber, "=%v)(objectclass=posixAccount))", NULL);
     }
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPDefaultUID", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_defaultuid = *((uid_t *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPDefaultGID", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_defaultgid = *((gid_t *) ptr);
   }
 
+  ldap_default_quota = get_param_ptr(main_server->conf, "LDAPDefaultQuota",
+    FALSE);
+
   ptr = get_param_ptr(main_server->conf, "LDAPForceDefaultUID", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_forcedefaultuid = *((int *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPForceDefaultGID", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_forcedefaultgid = *((int *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPForceGeneratedHomedir", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_forcegenhdir = *((int *) ptr);
   }
 
   ptr = get_param_ptr(main_server->conf, "LDAPGenerateHomedir", FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_genhdir = *((int *) ptr);
   }
 
   ldap_genhdir_prefix = get_param_ptr(main_server->conf,
     "LDAPGenerateHomedirPrefix", FALSE);
 
-  ldap_default_quota = get_param_ptr(main_server->conf, "LDAPDefaultQuota",
-    FALSE);
-
   ptr = get_param_ptr(main_server->conf, "LDAPGenerateHomedirPrefixNoUsername",
     FALSE);
-  if (ptr) {
+  if (ptr != NULL) {
     ldap_genhdir_prefix_nouname = *((int *) ptr);
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "LDAPGroups", FALSE);
   if (c != NULL) {
     ldap_do_groups = TRUE;
-    ldap_gid_basedn = pstrdup(session.pool, c->argv[0]);
+    ldap_gid_basedn = pstrdup(ldap_pool, c->argv[0]);
 
     if (c->argc > 1) {
-      ldap_group_name_filter = pstrdup(session.pool, c->argv[1]);
+      ldap_group_name_filter = pstrdup(ldap_pool, c->argv[1]);
 
     } else {
-      ldap_group_name_filter = pstrcat(session.pool,
+      ldap_group_name_filter = pstrcat(ldap_pool,
         "(&(", ldap_attr_cn, "=%v)(objectclass=posixGroup))", NULL);
     }
 
     if (c->argc > 2) {
-      ldap_group_gid_filter = pstrdup(session.pool, c->argv[2]);
+      ldap_group_gid_filter = pstrdup(ldap_pool, c->argv[2]);
 
     } else {
-      ldap_group_gid_filter = pstrcat(session.pool,
+      ldap_group_gid_filter = pstrcat(ldap_pool,
         "(&(", ldap_attr_gidnumber, "=%v)(objectclass=posixGroup))", NULL);
     }
 
     if (c->argc > 3) {
-      ldap_group_member_filter = pstrdup(session.pool, c->argv[3]);
+      ldap_group_member_filter = pstrdup(ldap_pool, c->argv[3]);
 
     } else {
-      ldap_group_member_filter = pstrcat(session.pool,
+      ldap_group_member_filter = pstrcat(ldap_pool,
         "(&(", ldap_attr_memberuid, "=%v)(objectclass=posixGroup))", NULL);
     }
   }
 
+#if defined(LBER_OPT_LOG_PRINT_FN)
+  /* If trace logging is enabled for the 'ldap.library' channel, direct
+   * libldap (via liblber) to log to our trace logging.
+   */
+  if (pr_trace_get_level(libtrace_channel) >= 1) {
+    int res;
+
+    res = ber_set_option(NULL, LBER_OPT_LOG_PRINT_FN, ldap_tracelog_cb);
+    if (res != LBER_OPT_SUCCESS) {
+      (void) pr_log_writefile(ldap_logfd, MOD_LDAP_VERSION,
+        "error setting trace logging function: %s", strerror(EINVAL));
+    }
+  }
+#endif /* LBER_OPT_LOG_PRINT_FN */
+
   return 0;
 }
 
@@ -2307,7 +2516,7 @@ static conftable ldap_conftab[] = {
   { "LDAPAliasDereference",	set_ldapaliasdereference,	NULL },
   { "LDAPAttr",			set_ldapattr,			NULL },
   { "LDAPAuthBinds",		set_ldapauthbinds,		NULL },
-  { "LDAPBindDN",		set_ldapdninfo,			NULL },
+  { "LDAPBindDN",		set_ldapbinddn,			NULL },
   { "LDAPDefaultAuthScheme",	set_ldapdefaultauthscheme,	NULL },
   { "LDAPDefaultGID",		set_ldapdefaultgid,		NULL },
   { "LDAPDefaultQuota",		set_ldapdefaultquota,		NULL },
diff --git a/contrib/mod_load/Makefile.in b/contrib/mod_load/Makefile.in
index a4a14c8..a5b0e81 100644
--- a/contrib/mod_load/Makefile.in
+++ b/contrib/mod_load/Makefile.in
@@ -39,5 +39,5 @@ clean:
 	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).la *.o *.lo .libs/*.o
 
 dist: clean
-	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log
+	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log *.gcda *.gcno
 	-$(RM) -r CVS/ RCS/
diff --git a/contrib/mod_load/configure b/contrib/mod_load/configure
index bf7ab2f..a2e4762 100755
--- a/contrib/mod_load/configure
+++ b/contrib/mod_load/configure
@@ -1258,6 +1258,18 @@ if test -n "$ac_init_help"; then
 
   cat <<\_ACEOF
 
+Optional Packages:
+  --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]
+  --without-PACKAGE       do not use PACKAGE (same as --with-PACKAGE=no)
+  --with-includes=LIST    add additional include paths to proftpd. LIST is a
+                          colon-separated list of include paths to add e.g.
+                          --with-includes=/some/mysql/include:/my/include
+
+  --with-libraries=LIST   add additional library paths to proftpd. LIST is a
+                          colon-separated list of include paths to add e.g.
+                          --with-libraries=/some/mysql/libdir:/my/libs
+
+
 Some influential environment variables:
   CC          C compiler command
   CFLAGS      C compiler flags
@@ -3190,7 +3202,7 @@ else
   { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 
 { echo "$as_me:$LINENO: checking for library containing strerror" >&5
@@ -3344,7 +3356,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3365,7 +3377,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3670,6 +3682,38 @@ _ACEOF
 fi
 
 
+
+# Check whether --with-includes was given.
+if test "${with_includes+set}" = set; then
+  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for ainclude in $ac_addl_includes; do
+      if test x"$ac_build_addl_includes" = x ; then
+        ac_build_addl_includes="-I$ainclude"
+      else
+        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
+      fi
+    done
+    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+
+fi
+
+
+
+# Check whether --with-libraries was given.
+if test "${with_libraries+set}" = set; then
+  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for alibdir in $ac_addl_libdirs; do
+      if test x"$ac_build_addl_libdirs" = x ; then
+        ac_build_addl_libdirs="-L$alibdir"
+      else
+        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
+      fi
+    done
+    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+fi
+
+
 { echo "$as_me:$LINENO: checking for ANSI C header files" >&5
 echo $ECHO_N "checking for ANSI C header files... $ECHO_C" >&6; }
 if test "${ac_cv_header_stdc+set}" = set; then
@@ -3738,7 +3782,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3759,7 +3803,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -5955,7 +5999,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_func_getloadavg_setgid=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 { echo "$as_me:$LINENO: result: $ac_cv_func_getloadavg_setgid" >&5
@@ -7042,7 +7086,7 @@ do
     cat >>$CONFIG_STATUS <<_ACEOF
     # First, check the format of the line:
     cat >"\$tmp/defines.sed" <<\\CEOF
-/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*/b def
+/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*\$/b def
 /^[	 ]*#[	 ]*define[	 ][	 ]*$ac_word_re[(	 ]/b def
 b
 :def
diff --git a/contrib/mod_load/configure.in b/contrib/mod_load/configure.in
index eb0c0c0..10c1d6d 100644
--- a/contrib/mod_load/configure.in
+++ b/contrib/mod_load/configure.in
@@ -1,3 +1,22 @@
+dnl ProFTPD - mod_load
+dnl Copyright (c) 2012-2015 TJ Saunders <tj at castaglia.org>
+dnl
+dnl This program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program; if not, write to the Free Software
+dnl Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+dnl
+dnl Process this file with autoconf to produce a configure script.
+
 AC_INIT(./mod_load.c)
 
 AC_CANONICAL_SYSTEM
@@ -10,6 +29,39 @@ AC_AIX
 AC_ISC_POSIX
 AC_MINIX
 
+dnl Need to support/handle the --with-includes and --with-libraries options
+AC_ARG_WITH(includes,
+  [AC_HELP_STRING(
+    [--with-includes=LIST],
+    [add additional include paths to proftpd. LIST is a colon-separated list of include paths to add e.g. --with-includes=/some/mysql/include:/my/include])
+  ],
+  [ ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for ainclude in $ac_addl_includes; do
+      if test x"$ac_build_addl_includes" = x ; then
+        ac_build_addl_includes="-I$ainclude"
+      else
+        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
+      fi
+    done
+    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+  ])
+
+AC_ARG_WITH(libraries,
+  [AC_HELP_STRING(
+    [--with-libraries=LIST],
+    [add additional library paths to proftpd. LIST is a colon-separated list of include paths to add e.g. --with-libraries=/some/mysql/libdir:/my/libs])
+  ],
+  [ ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for alibdir in $ac_addl_libdirs; do
+      if test x"$ac_build_addl_libdirs" = x ; then
+        ac_build_addl_libdirs="-L$alibdir"
+      else
+        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
+      fi
+    done
+    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+  ])
+
 AC_HEADER_STDC
 AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h)
 
diff --git a/contrib/mod_load/mod_load.c b/contrib/mod_load/mod_load.c
index 4c206b2..483b06c 100644
--- a/contrib/mod_load/mod_load.c
+++ b/contrib/mod_load/mod_load.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_load -- a module for refusing connections based on system load
- *
- * Copyright (c) 2001-2013 TJ Saunders
+ * Copyright (c) 2001-2016 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
@@ -30,8 +29,6 @@
  *
  *   Copyright (C) 1985, 86, 87, 88, 89, 91, 92, 93, 1994, 1995, 1997
  *     Free Software Foundation, Inc.
- * 
- * $Id: mod_load.c,v 1.8 2013-10-13 22:51:36 castaglia Exp $
  */
 
 #include "conf.h"
diff --git a/contrib/mod_log_forensic.c b/contrib/mod_log_forensic.c
index 4eacae3..7f8efec 100644
--- a/contrib/mod_log_forensic.c
+++ b/contrib/mod_log_forensic.c
@@ -1,8 +1,7 @@
 /*
  * mod_log_forensic - a buffering log module for aiding in server behavior
  *                    forensic analysis
- *
- * Copyright (c) 2011-2013 TJ Saunders
+ * Copyright (c) 2011-2016 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
@@ -41,7 +40,6 @@ module log_forensic_module;
 static pool *forensic_pool = NULL;
 static int forensic_engine = FALSE;
 static int forensic_logfd = -1;
-static struct timeval forensic_tv;
 
 /* Criteria for flushing out the "forensic" logs. */
 #define FORENSIC_CRIT_FAILED_LOGIN		0x00001
@@ -109,6 +107,9 @@ static const char *forensic_log_levels[] = {
   "40", "41", "42", "43", "44", "45", "46", "47", "48", "49"
 };
 
+/* Necessary prototypes */
+static int forensic_sess_init(void);
+
 static void forensic_add_msg(unsigned int log_type, int log_level,
     const char *log_msg, size_t log_msglen) {
   struct forensic_msg *fm;
@@ -244,8 +245,8 @@ static void forensic_write_metadata(void) {
    */
   struct iovec iov[64];
   int niov = 0, res;
-  struct timeval now;
-  unsigned long elapsed;
+  uint64_t now;
+  unsigned long elapsed_ms;
 
   /* Write session metadata in key/value message headers:
    *
@@ -303,12 +304,11 @@ static void forensic_write_metadata(void) {
   iov[niov].iov_len = 9;
   niov++;
 
-  gettimeofday(&now, NULL);
-  elapsed = (((now.tv_sec - forensic_tv.tv_sec) * 1000L) +
-    ((now.tv_usec - forensic_tv.tv_usec) / 1000L));
+  pr_gettimeofday_millis(&now);
+  elapsed_ms = (unsigned long) (now - session.connect_time_ms);
 
   memset(elapsed_str, '\0', sizeof(elapsed_str));
-  res = snprintf(elapsed_str, sizeof(elapsed_str)-1, "%lu\n", elapsed);
+  res = snprintf(elapsed_str, sizeof(elapsed_str)-1, "%lu\n", elapsed_ms);
   iov[niov].iov_base = (void *) elapsed_str;
   iov[niov].iov_len = res;
   niov++;
@@ -333,7 +333,7 @@ static void forensic_write_metadata(void) {
     iov[niov].iov_len = 6;
     niov++;
 
-    iov[niov].iov_base = session.user;
+    iov[niov].iov_base = (void *) session.user;
     iov[niov].iov_len = strlen(session.user);
     niov++;
 
@@ -850,6 +850,46 @@ static void forensic_mod_unload_ev(const void *event_data, void *user_data) {
 }
 #endif /* PR_SHARED_MODULE */
 
+static void forensic_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&log_forensic_module, "core.exit", forensic_exit_ev);
+  pr_event_unregister(&log_forensic_module, "core.log.unspec", forensic_log_ev);
+  pr_event_unregister(&log_forensic_module, "core.log.xferlog",
+    forensic_log_ev);
+  pr_event_unregister(&log_forensic_module, "core.log.syslog", forensic_log_ev);
+  pr_event_unregister(&log_forensic_module, "core.log.systemlog",
+    forensic_log_ev);
+  pr_event_unregister(&log_forensic_module, "core.log.extlog", forensic_log_ev);
+  pr_event_unregister(&log_forensic_module, "core.log.tracelog",
+    forensic_log_ev);
+  pr_event_unregister(&log_forensic_module, "core.session-reinit",
+    forensic_sess_reinit_ev);
+
+  forensic_engine = FALSE;
+  (void) close(forensic_logfd);
+  forensic_logfd = -1;
+  forensic_criteria = FORENSIC_CRIT_DEFAULT;
+  forensic_msgs = NULL;
+  forensic_nmsgs = FORENSIC_DEFAULT_NMSGS;
+  forensic_msg_idx = 0;
+
+  if (forensic_subpool != NULL) {
+    destroy_pool(forensic_subpool);
+    forensic_subpool = NULL;
+  }
+
+  forensic_subpool_msgno = 1;
+
+  res = forensic_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&log_forensic_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Module Initialization
  */
 
@@ -872,6 +912,9 @@ static int forensic_sess_init(void) {
   int tracelog_listen = TRUE;
   int res, xerrno;
 
+  pr_event_register(&log_forensic_module, "core.session-reinit",
+    forensic_sess_reinit_ev, NULL);
+
   /* Is this module enabled? */
   c = find_config(main_server->conf, CONF_PARAM, "ForensicLogEngine", FALSE);
   if (c != NULL) {
@@ -925,8 +968,6 @@ static int forensic_sess_init(void) {
     return 0;
   }
 
-  gettimeofday(&forensic_tv, NULL);
-
   /* Are there any log types for which we shouldn't be listening? */
   c = find_config(main_server->conf, CONF_PARAM, "ForensicLogCapture", FALSE);
   if (c) {
@@ -944,8 +985,10 @@ static int forensic_sess_init(void) {
     forensic_criteria = *((unsigned long *) c->argv[0]);
   }
 
-  forensic_pool = make_sub_pool(session.pool);
-  pr_pool_tag(forensic_pool, MOD_LOG_FORENSIC_VERSION);
+  if (forensic_pool == NULL) {
+    forensic_pool = make_sub_pool(session.pool);
+    pr_pool_tag(forensic_pool, MOD_LOG_FORENSIC_VERSION);
+  }
 
   c = find_config(main_server->conf, CONF_PARAM, "ForensicLogBufferSize",
     FALSE);
diff --git a/contrib/mod_qos.c b/contrib/mod_qos.c
index 02a664e..18d753a 100644
--- a/contrib/mod_qos.c
+++ b/contrib/mod_qos.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_qos -- a module for managing QoS socket options
  *
  * Copyright (c) 2010 Philip Prindeville
- * Copyright (c) 2010-2011 The ProFTPD Project
+ * Copyright (c) 2010-2014 The ProFTPD Project
  *
  * 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,8 +24,6 @@
  * source distribution.
  *
  * This is mod_qos, contrib software for proftpd 1.3.x and above.
- *
- * $Id: mod_qos.c,v 1.4 2011-05-23 20:56:40 castaglia Exp $
  */
 
 #include "conf.h"
@@ -238,6 +236,9 @@ static struct qos_rec qos_vals[] = {
   { NULL,	-1 }
 };
 
+/* Prototypes. */
+static int qos_sess_init(void);
+
 static int qos_get_int(const char *str) {
   register unsigned int i;
 
@@ -393,6 +394,22 @@ static void qos_mod_unload_ev(const void *event_data, void *user_data) {
 }
 #endif /* PR_SHARED_MODULE */
 
+static void qos_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&qos_module, "core.data-connect", qos_data_connect_ev);
+  pr_event_unregister(&qos_module, "core.data-listen", qos_data_listen_ev);
+  pr_event_unregister(&qos_module, "core.session-reinit", qos_sess_reinit_ev);
+
+  res = qos_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&qos_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -425,6 +442,9 @@ static int qos_sess_init(void) {
   }
 #endif
 
+  pr_event_register(&qos_module, "core.session-reinit", qos_sess_reinit_ev,
+    NULL);
+
   return 0;
 }
 
diff --git a/contrib/mod_quotatab.c b/contrib/mod_quotatab.c
index f2cad8a..59b6207 100644
--- a/contrib/mod_quotatab.c
+++ b/contrib/mod_quotatab.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_quotatab -- a module for managing FTP byte/file quotas via
  *                          centralized tables
- *
- * Copyright (c) 2001-2014 TJ Saunders
+ * Copyright (c) 2001-2016 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
@@ -27,8 +26,6 @@
  * the ideas in Eric Estabrook's mod_quota, available from
  * ftp://pooh.urbanrage.com/pub/c/.  This module, however, has been written
  * from scratch to implement quotas in a different way.
- *
- * $Id: mod_quotatab.c,v 1.87 2013-12-09 19:16:13 castaglia Exp $
  */
 
 #include "mod_quotatab.h"
@@ -171,6 +168,7 @@ static unsigned long quotatab_opts = 0UL;
 MODRET quotatab_pre_stor(cmd_rec *);
 MODRET quotatab_post_stor(cmd_rec *);
 MODRET quotatab_post_stor_err(cmd_rec *);
+static int quotatab_sess_init(void);
 
 static int quotatab_rlock(quota_table_t *);
 static int quotatab_runlock(quota_table_t *);
@@ -386,12 +384,13 @@ static char *quota_display_site_files(pool *p, unsigned int files_used,
 /* Quota logging routines */
 static int quotatab_closelog(void) {
   /* sanity check */
-  if (quota_logfd != -1) {
-    close(quota_logfd);
-    quota_logfd = -1;
-    quota_logname = NULL;
+  if (quota_logfd >= 0) {
+    (void) close(quota_logfd);
   }
 
+  quota_logfd = -1;
+  quota_logname = NULL;
+
   return 0;
 }
 
@@ -411,15 +410,12 @@ int quotatab_log(const char *fmt, ...) {
 }
 
 int quotatab_openlog(void) {
-  int res = 0;
-
-  /* Sanity checks. */
-  if (quota_logname)
-    return 0;
+  int res = 0, xerrno;
 
   quota_logname = get_param_ptr(main_server->conf, "QuotaLog", FALSE);
-  if (quota_logname == NULL)
+  if (quota_logname == NULL) {
     return 0;
+  }
 
   /* check for "none" */
   if (strcasecmp(quota_logname, "none") == 0) {
@@ -430,13 +426,14 @@ int quotatab_openlog(void) {
   pr_signals_block();
   PRIVS_ROOT
   res = pr_log_openfile(quota_logname, &quota_logfd, PR_LOG_SYSTEM_MODE);
+  xerrno = errno;
   PRIVS_RELINQUISH
   pr_signals_unblock();
 
   switch (res) {
     case -1:
       pr_log_pri(LOG_NOTICE, MOD_QUOTATAB_VERSION
-        ": unable to open QuotaLog '%s': %s", quota_logname, strerror(errno));
+        ": unable to open QuotaLog '%s': %s", quota_logname, strerror(xerrno));
       break;
 
     case PR_LOG_WRITABLE_DIR:
@@ -885,6 +882,93 @@ int quotatab_unregister_backend(const char *backend, unsigned int srcs) {
   return 0;
 }
 
+unsigned char quotatab_lookup_default(quota_tabtype_t tab_type, void *ptr,
+    const char *name, quota_type_t quota_type) {
+  config_rec *c;
+
+  c = find_config(main_server->conf, CONF_PARAM, "QuotaDefault", FALSE);
+  while (c != NULL) {
+    char *type_str;
+    quota_limit_t *limit;
+
+    pr_signals_handle();
+
+    type_str = c->argv[0];
+
+    /* What quota type is being looked up, and what kind does this QuotaDefault
+     * provide?
+     */
+    switch (quota_type) {
+      case USER_QUOTA:
+        if (strncasecmp(type_str, "user", 5) != 0) {
+          c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault", FALSE);
+          continue;
+        }
+        break;
+
+      case GROUP_QUOTA:
+        if (strncasecmp(type_str, "group", 6) != 0) {
+          c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault", FALSE);
+          continue;
+        }
+        break;
+
+      case CLASS_QUOTA:
+        if (strncasecmp(type_str, "class", 6) != 0) {
+          c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault", FALSE);
+          continue;
+        }
+        break;
+
+      case ALL_QUOTA:
+        if (strncasecmp(type_str, "all", 4) != 0) {
+          c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault", FALSE);
+          continue;
+        }
+        break;
+
+       default:
+         c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault", FALSE);
+        continue;
+    }
+
+    limit = ptr;
+
+    /* Retrieve the limit record (8 values):
+     *
+     *  per_session
+     *  limit_type
+     *  bytes_{in,out,xfer}_avail
+     *  files_{in,out,xfer}_avail
+     */
+
+    memmove(limit->name, name, strlen(name) + 1);
+    limit->quota_type = quota_type;
+
+    limit->quota_per_session = pr_str_is_boolean(c->argv[1]);
+
+    if (strncasecmp(c->argv[2], "soft", 5) == 0) {
+      limit->quota_limit_type = SOFT_LIMIT;
+
+    } else if (strncasecmp(c->argv[2], "hard", 5) == 0) {
+      limit->quota_limit_type = HARD_LIMIT;
+    }
+
+    limit->bytes_in_avail = atof(c->argv[3]);
+    limit->bytes_out_avail = atof(c->argv[4]);
+    limit->bytes_xfer_avail = atof(c->argv[5]);
+    limit->files_in_avail = atoi(c->argv[6]);
+    limit->files_out_avail = atoi(c->argv[7]);
+    limit->files_xfer_avail = atoi(c->argv[8]);
+
+    quotatab_log("using default %s limit from QuotaDefault directive",
+      type_str);
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
 /* Note: this function will only find the first occurrence of the given
  *  name and type in the table.  This means that if there is a malformed
  *  quota table, with duplicate name/type pairs, the duplicates will be
@@ -896,8 +980,8 @@ unsigned char quotatab_lookup(quota_tabtype_t tab_type, void *ptr,
   if (tab_type == TYPE_TALLY) {
 
     /* Make sure the requested table can do lookups. */
-    if (!tally_tab ||
-        !tally_tab->tab_lookup) {
+    if (tally_tab == NULL ||
+        tally_tab->tab_lookup == NULL) {
       errno = EPERM;
       return FALSE;
     }
@@ -917,98 +1001,6 @@ unsigned char quotatab_lookup(quota_tabtype_t tab_type, void *ptr,
       res = limit_tab->tab_lookup(limit_tab, ptr, name, quota_type);
     }
 
-    /* If no limit has been found at this point, AND if a QuotaDefault
-     * directive has been configured, use that configured default.
-     */
-    if (res == FALSE) {
-      config_rec *c;
-
-      c = find_config(main_server->conf, CONF_PARAM, "QuotaDefault", FALSE);
-      while (c != NULL) {
-        char *type_str;
-        quota_limit_t *limit;
-
-        pr_signals_handle();
-
-        type_str = c->argv[0];
-
-        /* What quota type is being looked up, and what kind does this
-         * QuotaDefault provide?
-         */
-        switch (quota_type) {
-          case USER_QUOTA:
-            if (strncasecmp(type_str, "user", 5) != 0) {
-              c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault",
-                FALSE);
-              continue;
-            }
-            break;
-
-          case GROUP_QUOTA:
-            if (strncasecmp(type_str, "group", 6) != 0) {
-              c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault",
-                FALSE);
-              continue;
-            }
-            break;
-
-          case CLASS_QUOTA:
-            if (strncasecmp(type_str, "class", 6) != 0) {
-              c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault",
-                FALSE);
-              continue;
-            }
-            break;
-
-          case ALL_QUOTA:
-            if (strncasecmp(type_str, "all", 4) != 0) {
-              c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault",
-                FALSE);
-              continue;
-            }
-            break;
-
-           default:
-             c = find_config_next(c, c->next, CONF_PARAM, "QuotaDefault",
-              FALSE);
-            continue;
-        }
- 
-        limit = ptr;
-
-        /* Retrieve the limit record (8 values):
-         *
-         *  per_session
-         *  limit_type
-         *  bytes_{in,out,xfer}_avail
-         *  files_{in,out,xfer}_avail
-         */
-
-        memmove(limit->name, name, strlen(name) + 1);
-        limit->quota_type = quota_type;
-
-        limit->quota_per_session = pr_str_is_boolean(c->argv[1]);
-      
-        if (strncasecmp(c->argv[2], "soft", 5) == 0) {
-          limit->quota_limit_type = SOFT_LIMIT;
-          
-        } else if (strncasecmp(c->argv[2], "hard", 5) == 0) {
-          limit->quota_limit_type = HARD_LIMIT;
-        }
-
-        limit->bytes_in_avail = atof(c->argv[3]);
-        limit->bytes_out_avail = atof(c->argv[4]);
-        limit->bytes_xfer_avail = atof(c->argv[5]);
-        limit->files_in_avail = atoi(c->argv[6]);
-        limit->files_out_avail = atoi(c->argv[7]);
-        limit->files_xfer_avail = atoi(c->argv[8]);
-
-        quotatab_log("using default limit from QuotaDefault directive");
-        res = TRUE;
-        break;
-      }
-    }
-
     return res;
   }
 
@@ -1399,13 +1391,17 @@ int quotatab_write(quota_tally_t *tally,
 /* FSIO handlers
  */
 
+static off_t copied_bytes = 0;
+
 static int quotatab_fsio_write(pr_fh_t *fh, int fd, const char *buf,
     size_t bufsz) {
   int res;
+  off_t total_bytes;
 
   res = write(fd, buf, bufsz);
-  if (res < 0)
+  if (res < 0) {
     return res;
+  }
 
   if (have_quota_update == 0) {
     return res;
@@ -1422,8 +1418,22 @@ static int quotatab_fsio_write(pr_fh_t *fh, int fd, const char *buf,
    * simultaneous connections.
    */
 
+  /* If the client is copying a file (versus uploading a file), then we need
+   * to track the "total bytes" differently.
+   */
+  if (session.curr_cmd_id == PR_CMD_SITE_ID &&
+      (session.curr_cmd_rec->argc >= 2 &&
+       (strncasecmp(session.curr_cmd_rec->argv[1], "CPTO", 5) == 0 ||
+        strncasecmp(session.curr_cmd_rec->argv[1], "COPY", 5) == 0))) {
+    copied_bytes += res;
+    total_bytes = copied_bytes;
+
+  } else {
+    total_bytes = session.xfer.total_bytes;
+  }
+
   if (sess_limit.bytes_in_avail > 0.0 &&
-      sess_tally.bytes_in_used + session.xfer.total_bytes > sess_limit.bytes_in_avail) {
+      sess_tally.bytes_in_used + total_bytes > sess_limit.bytes_in_avail) {
     int xerrno;
     char *errstr = NULL;
 
@@ -1435,7 +1445,7 @@ static int quotatab_fsio_write(pr_fh_t *fh, int fd, const char *buf,
   }
 
   if (sess_limit.bytes_xfer_avail > 0.0 &&
-      sess_tally.bytes_xfer_used + session.xfer.total_bytes > sess_limit.bytes_xfer_avail) {
+      sess_tally.bytes_xfer_used + total_bytes > sess_limit.bytes_xfer_avail) {
     int xerrno;
     char *errstr = NULL;
 
@@ -1474,7 +1484,7 @@ MODRET set_quotadefault(cmd_rec *cmd) {
       strncasecmp(cmd->argv[1], "class", 6) != 0 &&
       strncasecmp(cmd->argv[1], "all", 4) != 0) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown quota type '",
-      cmd->argv[1], "' configured", NULL));
+      (char *) cmd->argv[1], "' configured", NULL));
   } 
 
   c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
@@ -1482,7 +1492,7 @@ MODRET set_quotadefault(cmd_rec *cmd) {
   /* per-session */
   if (pr_str_is_boolean(cmd->argv[2]) < 0) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-      "expected Boolean per-session parameter: ", cmd->argv[2], NULL));
+      "expected Boolean per-session parameter: ", (char *) cmd->argv[2], NULL));
   }
 
   c->argv[1] = pstrdup(c->pool, cmd->argv[2]);
@@ -1550,9 +1560,10 @@ MODRET set_quotadisplayunits(cmd_rec *cmd) {
   } else if (strcasecmp(cmd->argv[1], "Gb") == 0) {
     units = GIGA;
 
-  } else
+  } else {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown display units: ",
-      cmd->argv[1], NULL));
+      (char *) cmd->argv[1], NULL));
+  }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = palloc(c->pool, sizeof(quota_units_t));
@@ -1604,8 +1615,8 @@ MODRET set_quotaexcludefilter(cmd_rec *cmd) {
     pr_regexp_error(res, pre, errstr, sizeof(errstr));
     pr_regexp_free(NULL, pre);
 
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1], "' failed regex "
-      "compilation: ", errstr, NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", (char *) cmd->argv[1],
+      "' failed regex compilation: ", errstr, NULL));
   }
 
   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
@@ -1621,15 +1632,19 @@ MODRET set_quotaexcludefilter(cmd_rec *cmd) {
 
 /* usage: QuotaLock file */
 MODRET set_quotalock(cmd_rec *cmd) {
+  char *path;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   /* Check for non-absolute paths */
-  if (*cmd->argv[1] != '/')
+  if (*path != '/') {
     CONF_ERROR(cmd, "absolute path required");
+  }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
-
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
@@ -1848,7 +1863,7 @@ MODRET quotatab_pre_appe(cmd_rec *cmd) {
     have_quota_update = 0;
 
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -1862,10 +1877,10 @@ MODRET quotatab_pre_appe(cmd_rec *cmd) {
       sess_tally.bytes_in_used >= sess_limit.bytes_in_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_IN(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -1877,10 +1892,10 @@ MODRET quotatab_pre_appe(cmd_rec *cmd) {
       sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_XFER(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -1892,7 +1907,7 @@ MODRET quotatab_pre_appe(cmd_rec *cmd) {
   /* Briefly cache the size (in bytes) of the file being appended to, so that
    * if successful, the byte counts can be adjusted correctly.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) < 0) {
     quotatab_disk_nbytes = 0;
 
@@ -1916,7 +1931,7 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     have_quota_update = 0;
     return PR_DECLINED(cmd);
   }
@@ -1925,7 +1940,7 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
    * in file size as the increment.  Make sure that no caching effects 
    * mess with the stat.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) >= 0) {
     append_bytes = st.st_size - quotatab_disk_nbytes;
 
@@ -1934,8 +1949,8 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
       append_bytes = 0;
 
     } else {
-      quotatab_log("%s: error checking '%s': %s", cmd->argv[0], cmd->arg,
-        strerror(errno));
+      quotatab_log("%s: error checking '%s': %s", (char *) cmd->argv[0],
+        cmd->arg, strerror(errno));
     }
   }
 
@@ -1949,10 +1964,10 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
       sess_tally.bytes_in_used >= sess_limit.bytes_in_avail) {
 
     if (!have_err_response) {
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_IN(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     }
 
     if (sess_tally.bytes_in_used > sess_limit.bytes_in_avail &&
@@ -1975,9 +1990,10 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
           -1, 0, -1);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0], cmd->arg);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          cmd->arg);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->arg);
+          (char *) cmd->argv[0], cmd->arg);
       }
     }
 
@@ -1985,10 +2001,10 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
       sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     if (!have_err_response) {
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_XFER(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     }
 
     if (sess_tally.bytes_xfer_used > sess_limit.bytes_xfer_avail &&
@@ -2002,9 +2018,9 @@ MODRET quotatab_post_appe(cmd_rec *cmd) {
           -1, 0, -1);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0], cmd->arg);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],            cmd->arg);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->arg);
+          (char *) cmd->argv[0], cmd->arg);
       }
     }
   }
@@ -2025,7 +2041,7 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     have_quota_update = 0;
     return PR_DECLINED(cmd);
   }
@@ -2034,7 +2050,7 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
    * in file size as the increment.  Make sure that no caching effects 
    * mess with the stat.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) >= 0) {
     append_bytes = st.st_size - quotatab_disk_nbytes;
 
@@ -2043,8 +2059,8 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
       append_bytes = 0;
 
     } else {
-      quotatab_log("%s: error checking '%s': %s", cmd->argv[0], cmd->arg,
-        strerror(errno));
+      quotatab_log("%s: error checking '%s': %s", (char *) cmd->argv[0],
+        cmd->arg, strerror(errno));
     }
   }
 
@@ -2058,10 +2074,10 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
       sess_tally.bytes_in_used >= sess_limit.bytes_in_avail) {
 
     if (!have_err_response) {
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_IN(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     }
 
     if (sess_tally.bytes_in_used > sess_limit.bytes_in_avail &&
@@ -2084,9 +2100,10 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
           -1, 0, -1);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0], cmd->arg);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          cmd->arg);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->arg);
+          (char *) cmd->argv[0], cmd->arg);
       }
     }
 
@@ -2094,10 +2111,10 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
       sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     if (!have_err_response) {
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_XFER(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     }
 
     if (sess_tally.bytes_xfer_used > sess_limit.bytes_xfer_avail &&
@@ -2120,9 +2137,10 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
           -1, 0, -1);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0], cmd->arg);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          cmd->arg);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->arg);
+          (char *) cmd->argv[0], cmd->arg);
       }
     }
   }
@@ -2134,6 +2152,7 @@ MODRET quotatab_post_appe_err(cmd_rec *cmd) {
 MODRET quotatab_pre_copy(cmd_rec *cmd) {
   struct stat st;
 
+  copied_bytes = 0;
   have_aborted_transfer = FALSE;
   have_err_response = FALSE;
 
@@ -2146,7 +2165,7 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
     have_quota_update = 0;
 
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->argv[1], quota_exclude_filter);
+      (char *) cmd->argv[0], (char *) cmd->argv[1], quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -2160,10 +2179,10 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
       sess_tally.bytes_in_used >= sess_limit.bytes_in_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_IN(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -2175,10 +2194,10 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
              sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_XFER(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -2190,7 +2209,7 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
   /* Briefly cache the size (in bytes) of the file being overwritten, so that
    * if successful, the byte counts can be adjusted correctly.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->argv[2]);
   if (pr_fsio_stat(cmd->argv[2], &st) < 0) {
     quotatab_disk_nbytes = 0;
 
@@ -2219,10 +2238,10 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
         sess_tally.files_in_used >= sess_limit.files_in_avail) {
 
       /* Report the exceeding of the threshold. */
-      quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+      quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
         DISPLAY_FILES_IN(cmd));
       pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-        cmd->argv[0], DISPLAY_FILES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_FILES_IN(cmd));
       have_err_response = TRUE;
 
       /* Set an appropriate errno value. */
@@ -2234,10 +2253,10 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
                sess_tally.files_xfer_used >= sess_limit.files_xfer_avail) {
 
       /* Report the exceeding of the threshold. */
-      quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+      quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
         DISPLAY_FILES_XFER(cmd));
       pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-        cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
       have_err_response = TRUE;
 
       /* Set an appropriate errno value. */
@@ -2247,6 +2266,7 @@ MODRET quotatab_pre_copy(cmd_rec *cmd) {
     }
   }
 
+  have_quota_update = QUOTA_HAVE_WRITE_UPDATE;
   return PR_DECLINED(cmd);
 }
 
@@ -2255,17 +2275,22 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
   off_t copy_bytes = 0;
   int dst_truncated = FALSE;
 
+  copied_bytes = 0;
+
   /* Sanity check */
-  if (!use_quotas)
+  if (!use_quotas) {
+    have_quota_update = 0;
     return PR_DECLINED(cmd);
+  }
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->argv[2])) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->argv[2], quota_exclude_filter);
+      (char *) cmd->argv[0], (char *) cmd->argv[2], quota_exclude_filter);
+    have_quota_update = 0;
     return PR_DECLINED(cmd);
   }
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->argv[2]);
   if (pr_fsio_stat(cmd->argv[2], &st) == 0) {
     if (quotatab_disk_nfiles == 0) {
 
@@ -2316,10 +2341,10 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
 
     if (!have_err_response) {
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_IN(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     }
 
     if (sess_tally.bytes_in_used > sess_limit.bytes_in_avail &&
@@ -2334,7 +2359,7 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
       }
 
       if (res < 0) {
-        quotatab_log("notice: unable to unlink '%s': %s", cmd->argv[2],
+        quotatab_log("notice: unable to unlink '%s': %s", (char *) cmd->argv[2],
           strerror(errno));
 
       } else {
@@ -2342,10 +2367,10 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
           -quotatab_disk_nfiles, 0, -quotatab_disk_nfiles);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0],
-          cmd->argv[2]);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          (char *) cmd->argv[2]);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->argv[2]);
+          (char *) cmd->argv[0], (char *) cmd->argv[2]);
       }
     }
 
@@ -2354,10 +2379,10 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
 
     if (!have_err_response) {
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_XFER(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     }
 
     if (sess_tally.bytes_xfer_used > sess_limit.bytes_xfer_avail &&
@@ -2372,7 +2397,7 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
       }
 
       if (res < 0) {
-        quotatab_log("notice: unable to unlink '%s': %s", cmd->argv[2],
+        quotatab_log("notice: unable to unlink '%s': %s", (char *) cmd->argv[2],
           strerror(errno));
 
       } else {
@@ -2380,10 +2405,10 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
           -quotatab_disk_nfiles, 0, -quotatab_disk_nfiles);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0],
-          cmd->argv[2]);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          (char *) cmd->argv[2]);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->argv[2]);
+          (char *) cmd->argv[0], (char *) cmd->argv[2]);
       }
     }
   }
@@ -2398,10 +2423,10 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
 
       if (!have_err_response) {
         /* Report the reaching of the threshold. */
-        quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+        quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
           DISPLAY_FILES_IN(cmd));
         pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-          cmd->argv[0], DISPLAY_FILES_IN(cmd));
+          (char *) cmd->argv[0], DISPLAY_FILES_IN(cmd));
       }
 
     } else if (sess_limit.files_xfer_avail != 0 &&
@@ -2409,10 +2434,10 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
 
       if (!have_err_response) {
         /* Report the reaching of the threshold. */
-        quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+        quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
           DISPLAY_FILES_XFER(cmd));
         pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-          cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+          (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
       }
     }
   }
@@ -2420,19 +2445,25 @@ MODRET quotatab_post_copy(cmd_rec *cmd) {
   /* Clear the cached bytes/files. */
   quotatab_disk_nbytes = 0;
   quotatab_disk_nfiles = 0;
-  
+
+  have_quota_update = 0;
   return PR_DECLINED(cmd);
 }
 
 MODRET quotatab_post_copy_err(cmd_rec *cmd) {
+  copied_bytes = 0;
+
   /* Sanity check */
-  if (!use_quotas)
+  if (!use_quotas) {
+    have_quota_update = 0;
     return PR_DECLINED(cmd);
+  }
 
   /* Clear the cached bytes/files. */
   quotatab_disk_nbytes = 0;
   quotatab_disk_nfiles = 0;
-  
+
+  have_quota_update = 0;
   return PR_DECLINED(cmd);
 }
 
@@ -2455,14 +2486,14 @@ MODRET quotatab_pre_dele(cmd_rec *cmd) {
   if (path) {
     if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
       quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-        cmd->argv[0], cmd->arg, quota_exclude_filter);
+        (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
       return PR_DECLINED(cmd);
     }
 
     /* Briefly cache the size (in bytes) of the file to be deleted, so that
      * if successful, the byte counts can be adjusted correctly.
      */
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(path);
     if (pr_fsio_lstat(path, &quotatab_dele_st) < 0) {
       quotatab_disk_nbytes = 0;
 
@@ -2487,7 +2518,7 @@ MODRET quotatab_post_dele(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -2522,11 +2553,11 @@ MODRET quotatab_post_dele(cmd_rec *cmd) {
       user_owner = pr_auth_uid2name(cmd->tmp_pool, quotatab_dele_st.st_uid);
       group_owner = pr_auth_gid2name(cmd->tmp_pool, quotatab_dele_st.st_gid);
 
-      quotatab_log("deleted file '%s' belongs to user '%s' (UID %lu), "
-        "not the current user '%s' (UID %lu); attempting to credit user '%s' "
+      quotatab_log("deleted file '%s' belongs to user '%s' (UID %s), "
+        "not the current user '%s' (UID %s); attempting to credit user '%s' "
         "for the deleted bytes", path, user_owner,
-        (unsigned long) quotatab_dele_st.st_uid, session.user,
-        (unsigned long) session.uid, user_owner);
+        pr_uid2str(cmd->tmp_pool, quotatab_dele_st.st_uid), session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid), user_owner);
 
       quotatab_mutex_lock(F_WRLCK);
 
@@ -2731,7 +2762,7 @@ MODRET quotatab_post_pass(cmd_rec *cmd) {
 
   /* Check for a limit and a tally entry for these groups. */
   if (!have_limit_entry) {
-    char *group_name = session.group;
+    const char *group_name = session.group;
     gid_t group_id = session.gid;
 
     if (quotatab_lookup(TYPE_LIMIT, &sess_limit, group_name, GROUP_QUOTA)) {
@@ -2740,7 +2771,7 @@ MODRET quotatab_post_pass(cmd_rec *cmd) {
 
     } else {
       if (session.groups) {
-        register int i = 0;
+        register unsigned int i = 0;
 
         char **group_names = session.groups->elts;
         gid_t *group_ids = session.gids->elts;
@@ -2811,6 +2842,145 @@ MODRET quotatab_post_pass(cmd_rec *cmd) {
     }
   }
 
+  if (!have_limit_entry) {
+    if (quotatab_lookup_default(TYPE_LIMIT, &sess_limit, session.user,
+        USER_QUOTA)) {
+      quotatab_log("found limit entry for user '%s'", session.user);
+      have_limit_entry = TRUE;
+
+      if (quotatab_lookup(TYPE_TALLY, &sess_tally, session.user, USER_QUOTA)) {
+        quotatab_log("found tally entry for user '%s'", session.user);
+        have_quota_entry = TRUE;
+
+      } else {
+        if (quotatab_create_tally()) {
+          quotatab_log("created tally entry for user '%s'", session.user);
+          have_quota_entry = TRUE;
+        }
+      }
+
+      quotatab_mutex_lock(F_UNLCK);
+
+      if (have_quota_entry) {
+        if ((quotatab_opts & QUOTA_OPT_SCAN_ON_LOGIN) &&
+            (sess_limit.bytes_in_avail > 0 ||
+             sess_limit.files_in_avail > 0)) {
+          double byte_count = 0;
+          unsigned int file_count = 0;
+          time_t then;
+
+          quotatab_log("ScanOnLogin enabled, scanning current directory '%s' "
+            "for files owned by user '%s'", pr_fs_getcwd(), session.user);
+
+          time(&then);
+          if (quotatab_scan_dir(cmd->tmp_pool, pr_fs_getcwd(), session.uid, -1,
+              0, &byte_count, &file_count) < 0) {
+            quotatab_log("unable to scan '%s': %s", pr_fs_getcwd(),
+              strerror(errno));
+
+          } else {
+            double bytes_diff = (double)
+              (byte_count - sess_tally.bytes_in_used);
+            int files_diff = file_count - sess_tally.files_in_used;
+
+            quotatab_log("found %0.2lf bytes in %u %s for user '%s' "
+              "in %lu secs", byte_count, file_count,
+              file_count != 1 ? "files" : "file", session.user,
+              (unsigned long) time(NULL) - then);
+
+            quotatab_log("updating tally (%0.2lf bytes, %d %s difference)",
+              bytes_diff, files_diff, files_diff != 1 ? "files" : "file");
+
+            /* Write out an updated quota entry */
+            QUOTATAB_TALLY_WRITE(bytes_diff, 0, 0, files_diff, 0, 0);
+          }
+        }
+      }
+    }
+  }
+
+  if (!have_limit_entry) {
+    const char *group_name = session.group;
+    gid_t group_id = session.gid;
+
+    if (quotatab_lookup_default(TYPE_LIMIT, &sess_limit, group_name,
+        GROUP_QUOTA)) {
+      quotatab_log("found limit entry for group '%s'", group_name);
+      have_limit_entry = TRUE;
+
+    } else {
+      if (session.groups) {
+        register unsigned int i = 0;
+
+        char **group_names = session.groups->elts;
+        gid_t *group_ids = session.gids->elts;
+
+        /* Scan the list of supplemental group memberships for this user. */
+        for (i = 0; i < session.groups->nelts; i++) {
+          group_name = group_names[i];
+          group_id = group_ids[i];
+
+          if (quotatab_lookup_default(TYPE_LIMIT, &sess_limit, group_name,
+              GROUP_QUOTA)) {
+            quotatab_log("found limit entry for group '%s'", group_name);
+            have_limit_entry = TRUE;
+            break;
+          }
+        }
+      }
+    }
+
+    if (have_limit_entry) {
+      if (quotatab_lookup(TYPE_TALLY, &sess_tally, group_name, GROUP_QUOTA)) {
+        quotatab_log("found tally entry for group '%s'", group_name);
+        have_quota_entry = TRUE;
+
+      } else {
+        if (quotatab_create_tally()) {
+          quotatab_log("created tally entry for group '%s'", group_name);
+          have_quota_entry = TRUE;
+        }
+      }
+
+      quotatab_mutex_lock(F_UNLCK);
+
+      if (have_quota_entry) {
+        if ((quotatab_opts & QUOTA_OPT_SCAN_ON_LOGIN) &&
+            (sess_limit.bytes_in_avail > 0 ||
+             sess_limit.files_in_avail > 0)) {
+          double byte_count = 0;
+          unsigned int file_count = 0;
+          time_t then;
+
+          quotatab_log("ScanOnLogin enabled, scanning current directory '%s' "
+            "for files owned by group '%s'", pr_fs_getcwd(), group_name);
+
+          time(&then);
+          if (quotatab_scan_dir(cmd->tmp_pool, pr_fs_getcwd(), -1, group_id,
+              0, &byte_count, &file_count) < 0) {
+            quotatab_log("unable to scan '%s': %s", pr_fs_getcwd(),
+              strerror(errno));
+
+          } else {
+            double bytes_diff = byte_count - sess_tally.bytes_in_used;
+            int files_diff = file_count - sess_tally.files_in_used;
+
+            quotatab_log("found %0.2lf bytes in %u %s for group '%s' "
+              "in %lu secs", byte_count, file_count,
+              file_count != 1 ? "files" : "file", group_name,
+              (unsigned long) time(NULL) - then);
+
+            quotatab_log("updating tally (%0.2lf bytes, %d %s difference)",
+              bytes_diff, files_diff, files_diff != 1 ? "files" : "file");
+
+            /* Write out an updated quota entry */
+            QUOTATAB_TALLY_WRITE(bytes_diff, 0, 0, files_diff, 0, 0);
+          }
+        }
+      }
+    }
+  }
+
   /* Check for a limit and a tally entry for this class. */
   if (!have_limit_entry &&
       session.conn_class != NULL) {
@@ -3082,7 +3252,7 @@ MODRET quotatab_pre_retr(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -3096,10 +3266,10 @@ MODRET quotatab_pre_retr(cmd_rec *cmd) {
       sess_tally.bytes_out_used >= sess_limit.bytes_out_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_OUT(cmd));
     pr_response_add_err(R_451, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_OUT(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_OUT(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3111,10 +3281,10 @@ MODRET quotatab_pre_retr(cmd_rec *cmd) {
       sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_XFER(cmd));
     pr_response_add_err(R_451, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3130,10 +3300,10 @@ MODRET quotatab_pre_retr(cmd_rec *cmd) {
       sess_tally.files_out_used >= sess_limit.files_out_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_OUT(cmd));
     pr_response_add_err(R_451, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_FILES_OUT(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_OUT(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3145,10 +3315,10 @@ MODRET quotatab_pre_retr(cmd_rec *cmd) {
       sess_tally.files_xfer_used >= sess_limit.files_xfer_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s: denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s: denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_XFER(cmd));
     pr_response_add(R_451, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3169,7 +3339,7 @@ MODRET quotatab_post_retr(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -3184,19 +3354,19 @@ MODRET quotatab_post_retr(cmd_rec *cmd) {
       sess_tally.bytes_out_used >= sess_limit.bytes_out_avail) {
 
     /* Report the reaching of the threshold. */
-    quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+    quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_OUT(cmd));
     pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_OUT(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_OUT(cmd));
 
   } else if (sess_limit.bytes_xfer_avail > 0.0 &&
       sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     /* Report the reaching of the threshold. */
-    quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+    quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_XFER(cmd));
     pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
   }
 
   /* Check quotas to see if files download or total quota has been reached.
@@ -3206,19 +3376,19 @@ MODRET quotatab_post_retr(cmd_rec *cmd) {
       sess_tally.files_out_used >= sess_limit.files_out_avail) {
 
     /* Report the reaching of the threshold. */
-    quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+    quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_OUT(cmd));
     pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-      cmd->argv[0], DISPLAY_FILES_OUT(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_OUT(cmd));
 
   } else if (sess_limit.files_xfer_avail != 0 &&
     sess_tally.files_xfer_used >= sess_limit.files_xfer_avail) {
 
     /* Report the reaching of the threshold. */
-    quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+    quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_XFER(cmd));
     pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-      cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
   }
 
   return PR_DECLINED(cmd);
@@ -3232,7 +3402,7 @@ MODRET quotatab_post_retr_err(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -3249,10 +3419,10 @@ MODRET quotatab_post_retr_err(cmd_rec *cmd) {
     if (!have_err_response) {
 
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_OUT(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_OUT(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_OUT(cmd));
     }
 
   } else if (sess_limit.bytes_xfer_avail > 0.0 &&
@@ -3261,10 +3431,10 @@ MODRET quotatab_post_retr_err(cmd_rec *cmd) {
     if (!have_err_response) {
 
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_XFER(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     }
   }
 
@@ -3277,10 +3447,10 @@ MODRET quotatab_post_retr_err(cmd_rec *cmd) {
     if (!have_err_response) {
 
       /* Report the reaching of the treshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_FILES_OUT(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_FILES_OUT(cmd));
+        (char *) cmd->argv[0], DISPLAY_FILES_OUT(cmd));
     }
 
   } else if (sess_limit.files_xfer_avail != 0 &&
@@ -3289,10 +3459,10 @@ MODRET quotatab_post_retr_err(cmd_rec *cmd) {
     if (!have_err_response) {
 
       /* Report the reaching of the treshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_FILES_XFER(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
     }
   }
 
@@ -3310,7 +3480,7 @@ MODRET quotatab_pre_rmd(cmd_rec *cmd) {
   /* Briefly cache the size (in bytes) of the directory to be deleted, so that
    * if successful, the byte counts can be adjusted correctly.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) < 0) {
     quotatab_disk_nbytes = 0;
 
@@ -3329,7 +3499,7 @@ MODRET quotatab_post_rmd(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -3351,14 +3521,14 @@ MODRET quotatab_pre_rnto(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
   /* Briefly cache the size (in bytes) of the file being overwritten, so that
    * if successful, the byte counts can be adjusted correctly.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) < 0) {
     quotatab_disk_nbytes = 0;
     quotatab_disk_nfiles = 0;
@@ -3379,7 +3549,7 @@ MODRET quotatab_post_rnto(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -3409,7 +3579,7 @@ MODRET quotatab_pre_stor(cmd_rec *cmd) {
     have_quota_update = 0;
 
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     return PR_DECLINED(cmd);
   }
 
@@ -3423,10 +3593,10 @@ MODRET quotatab_pre_stor(cmd_rec *cmd) {
       sess_tally.bytes_in_used >= sess_limit.bytes_in_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_IN(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3438,10 +3608,10 @@ MODRET quotatab_pre_stor(cmd_rec *cmd) {
       sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_BYTES_XFER(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3457,10 +3627,10 @@ MODRET quotatab_pre_stor(cmd_rec *cmd) {
       sess_tally.files_in_used >= sess_limit.files_in_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_IN(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_FILES_IN(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_IN(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3472,10 +3642,10 @@ MODRET quotatab_pre_stor(cmd_rec *cmd) {
       sess_tally.files_xfer_used >= sess_limit.files_xfer_avail) {
 
     /* Report the exceeding of the threshold. */
-    quotatab_log("%s denied: quota exceeded: used %s", cmd->argv[0],
+    quotatab_log("%s denied: quota exceeded: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_XFER(cmd));
     pr_response_add_err(R_552, _("%s denied: quota exceeded: used %s"),
-      cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
     have_err_response = TRUE;
 
     /* Set an appropriate errno value. */
@@ -3489,7 +3659,7 @@ MODRET quotatab_pre_stor(cmd_rec *cmd) {
    * stat fails, it means that a new file is being uploaded, so set the
    * disk_nbytes to be zero. 
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) < 0) {
     quotatab_disk_nbytes = 0;
 
@@ -3513,7 +3683,7 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     have_quota_update = 0;
     return PR_DECLINED(cmd);
   }
@@ -3529,7 +3699,7 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
     if (delete_stores != NULL &&
         *delete_stores == TRUE) {
       quotatab_log("%s: upload aborted and DeleteAbortedStores on, "
-        "skipping tally update", cmd->argv[0]);
+        "skipping tally update", (char *) cmd->argv[0]);
       have_quota_update = 0;
       return PR_DECLINED(cmd);
     }
@@ -3539,7 +3709,7 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
    * in file size as the increment.  Make sure that no caching effects
    * mess with the stat.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(cmd->arg);
   if (pr_fsio_lstat(cmd->arg, &st) >= 0) {
     store_bytes = st.st_size - quotatab_disk_nbytes;
 
@@ -3548,8 +3718,8 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
       store_bytes = 0;
 
     } else {
-      quotatab_log("%s: error checking '%s': %s", cmd->argv[0], cmd->arg,
-        strerror(errno));
+      quotatab_log("%s: error checking '%s': %s", (char *) cmd->argv[0],
+        cmd->arg, strerror(errno));
     }
   }
 
@@ -3570,10 +3740,10 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
 
     if (!have_err_response) {
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_IN(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     }
 
     if (sess_tally.bytes_in_used > sess_limit.bytes_in_avail &&
@@ -3596,21 +3766,22 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
           -1, 0, -1);
         
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0], cmd->arg);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          cmd->arg);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->arg);
+          (char *) cmd->argv[0], cmd->arg);
       }
     }
 
   } else if (sess_limit.bytes_xfer_avail > 0.0 &&
-      sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
+             sess_tally.bytes_xfer_used >= sess_limit.bytes_xfer_avail) {
 
     if (!have_err_response) {
       /* Report the reaching of the threshold. */ 
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_XFER(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     }
 
     if (sess_tally.bytes_xfer_used > sess_limit.bytes_xfer_avail &&
@@ -3633,9 +3804,10 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
           -1, 0, -1);
 
         /* Report the removal of the file. */
-        quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0], cmd->arg);
+        quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
+          cmd->arg);
         pr_response_add(R_DUP, _("%s: notice: quota reached: '%s' removed"),
-          cmd->argv[0], cmd->arg);
+          (char *) cmd->argv[0], cmd->arg);
       }
     }
   }
@@ -3648,10 +3820,10 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
 
     if (!have_err_response) {
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_FILES_IN(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_FILES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_FILES_IN(cmd));
     }
 
   } else if (sess_limit.files_xfer_avail != 0 &&
@@ -3659,10 +3831,10 @@ MODRET quotatab_post_stor(cmd_rec *cmd) {
 
     if (!have_err_response) {
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_FILES_XFER(cmd));
       pr_response_add(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
     }
   }
 
@@ -3682,7 +3854,7 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
 
   if (quotatab_ignore_path(cmd->tmp_pool, cmd->arg)) {
     quotatab_log("%s: path '%s' matched QuotaExcludeFilter '%s', ignoring",
-      cmd->argv[0], cmd->arg, quota_exclude_filter);
+      (char *) cmd->argv[0], cmd->arg, quota_exclude_filter);
     have_quota_update = 0;
     return PR_DECLINED(cmd);
   }
@@ -3698,7 +3870,7 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
     if (delete_stores != NULL &&
         *delete_stores == TRUE) {
       quotatab_log("%s: upload aborted and DeleteAbortedStores on, "
-        "skipping tally update", cmd->argv[0]);
+        "skipping tally update", (char *) cmd->argv[0]);
       have_quota_update = 0;
       return PR_DECLINED(cmd);
     } 
@@ -3709,7 +3881,7 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
      * in file size as the increment.  Make sure that no caching effects 
      * mess with the stat.
      */
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(cmd->arg);
     if (pr_fsio_lstat(cmd->arg, &st) >= 0) {
       store_bytes = st.st_size - quotatab_disk_nbytes;
 
@@ -3718,8 +3890,8 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
         store_bytes = 0;
 
       } else {
-        quotatab_log("%s: error checking '%s': %s", cmd->argv[0], cmd->arg,
-          strerror(errno));
+        quotatab_log("%s: error checking '%s': %s", (char *) cmd->argv[0],
+          cmd->arg, strerror(errno));
       }
     }
   }
@@ -3743,10 +3915,10 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
     if (!have_err_response) {
 
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_IN(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_IN(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_IN(cmd));
     }
 
     if (sess_tally.bytes_in_used > sess_limit.bytes_in_avail) {
@@ -3767,10 +3939,10 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
         } else {
 
           /* Report the removal of the file. */
-          quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0],
+          quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
             cmd->arg);
           pr_response_add_err(R_DUP,
-            _("%s: notice: quota reached: '%s' removed"), cmd->argv[0],
+            _("%s: notice: quota reached: '%s' removed"), (char *) cmd->argv[0],
             cmd->arg);
         }
       }
@@ -3784,10 +3956,10 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
     if (!have_err_response) {
 
       /* Report the reaching of the threshold. */
-      quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+      quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
         DISPLAY_BYTES_XFER(cmd));
       pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-        cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
+        (char *) cmd->argv[0], DISPLAY_BYTES_XFER(cmd));
     }
 
     if (sess_tally.bytes_xfer_used > sess_limit.bytes_xfer_avail) {
@@ -3808,10 +3980,10 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
         } else {
 
           /* Report the removal of the file. */
-          quotatab_log("%s: quota reached: '%s' removed", cmd->argv[0],
+          quotatab_log("%s: quota reached: '%s' removed", (char *) cmd->argv[0],
             cmd->arg);
           pr_response_add_err(R_DUP,
-            _("%s: notice: quota reached: '%s' removed"), cmd->argv[0],
+            _("%s: notice: quota reached: '%s' removed"), (char *) cmd->argv[0],
             cmd->arg);
         }
       }
@@ -3827,19 +3999,19 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
       sess_tally.files_in_used >= sess_limit.files_in_avail) {
 
     /* Report the reaching of the threshold. */
-    quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+    quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_IN(cmd));
     pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-      cmd->argv[0], DISPLAY_FILES_IN(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_IN(cmd));
 
   } else if (sess_limit.files_xfer_avail != 0 &&
       sess_tally.files_xfer_used >= sess_limit.files_xfer_avail) {
 
     /* Report the reaching of the threshold. */
-    quotatab_log("%s: quota reached: used %s", cmd->argv[0],
+    quotatab_log("%s: quota reached: used %s", (char *) cmd->argv[0],
       DISPLAY_FILES_XFER(cmd));
     pr_response_add_err(R_DUP, _("%s: notice: quota reached: used %s"),
-      cmd->argv[0], DISPLAY_FILES_XFER(cmd));
+      (char *) cmd->argv[0], DISPLAY_FILES_XFER(cmd));
   }
 
   have_quota_update = 0;
@@ -3849,8 +4021,9 @@ MODRET quotatab_post_stor_err(cmd_rec *cmd) {
 MODRET quotatab_pre_site(cmd_rec *cmd) {
 
   /* Make sure it's a valid SITE command */
-  if (cmd->argc < 2)
+  if (cmd->argc < 2) {
     return PR_DECLINED(cmd);
+  }
 
   if (strncasecmp(cmd->argv[1], "COPY", 5) == 0) {
     cmd_rec *copy_cmd;
@@ -3862,10 +4035,11 @@ MODRET quotatab_pre_site(cmd_rec *cmd) {
   } else if (strncasecmp(cmd->argv[1], "CPTO", 5) == 0) {
     register unsigned int i;
     cmd_rec *copy_cmd;
-    char *from, *to = "";
+    const char *from, *to = "";
 
-    if (cmd->argc < 3)
+    if (cmd->argc < 3) {
       return PR_DECLINED(cmd);
+    }
 
     from = pr_table_get(session.notes, "mod_copy.cpfr-path", NULL);
     if (from == NULL) {
@@ -3891,8 +4065,9 @@ MODRET quotatab_pre_site(cmd_rec *cmd) {
 MODRET quotatab_site(cmd_rec *cmd) {
 
   /* Make sure it's a valid SITE QUOTA command */
-  if (cmd->argc < 2)
+  if (cmd->argc < 2) {
     return PR_DECLINED(cmd);
+  }
 
   if (strncasecmp(cmd->argv[1], "QUOTA", 6) == 0) {
     char *cmd_name;
@@ -4008,10 +4183,11 @@ MODRET quotatab_post_site(cmd_rec *cmd) {
   } else if (strncasecmp(cmd->argv[1], "CPTO", 5) == 0) {
     register unsigned int i;
     cmd_rec *copy_cmd;
-    char *from, *to = "";
+    const char *from, *to = "";
 
-    if (cmd->argc < 3)
+    if (cmd->argc < 3) {
       return PR_DECLINED(cmd);
+    }
 
     from = pr_table_get(session.notes, "mod_copy.cpfr-path", NULL);
     if (from == NULL) {
@@ -4050,7 +4226,7 @@ MODRET quotatab_post_site_err(cmd_rec *cmd) {
   } else if (strncasecmp(cmd->argv[1], "CPTO", 5) == 0) {
     register unsigned int i;
     cmd_rec *copy_cmd;
-    char *from, *to = "";
+    const char *from, *to = "";
 
     from = pr_table_get(session.notes, "mod_copy.cpfr-path", NULL);
     if (from == NULL) {
@@ -4127,9 +4303,7 @@ static void quotatab_mod_unload_ev(const void *event_data, void *user_data) {
       quotatab_pool = NULL;
     }
 
-    close(quota_logfd);
-    quota_logfd = -1;
-    quota_logname = NULL;
+    quotatab_closelog();
   }
 }
 #endif
@@ -4144,13 +4318,49 @@ static void quotatab_restart_ev(const void *event_data, void *user_data) {
   return;
 }
 
+static void quotatab_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&quotatab_module, "core.exit", quotatab_exit_ev);
+  pr_event_unregister(&quotatab_module, "core.session-reinit",
+    quotatab_sess_reinit_ev);
+
+  /* Reset defaults. */
+  use_quotas = FALSE;
+  (void) close(quota_logfd);
+  quota_logfd = -1;
+  quota_logname = NULL;
+  quotatab_opts = 0UL;
+  allow_site_quota = TRUE;
+  use_dirs = FALSE;
+  use_quotas = FALSE;
+  have_quota_entry = FALSE;
+  have_quota_limit_table = FALSE;
+  have_quota_tally_table = FALSE;
+  byte_units = BYTE;
+
+  (void) close(quota_lockfd);
+  quota_lockfd = -1;
+
+  (void) quotatab_close(TYPE_LIMIT);
+  (void) quotatab_close(TYPE_TALLY);
+
+  res = quotatab_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&quotatab_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
 static int quotatab_init(void) {
 
   /* Initialize the module's memory pool. */
-  if (!quotatab_pool) {
+  if (quotatab_pool == NULL) {
     quotatab_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(quotatab_pool, MOD_QUOTATAB_VERSION);
   }
@@ -4171,6 +4381,9 @@ static int quotatab_sess_init(void) {
     *quotatab_usedirs = NULL;
   quota_units_t *units = NULL;
 
+  pr_event_register(&quotatab_module, "core.session-reinit",
+    quotatab_sess_reinit_ev, NULL);
+
   /* Check to see if quotas are enabled for this server. */
   quotatab_engine = get_param_ptr(main_server->conf, "QuotaEngine", FALSE);
   if (quotatab_engine != NULL &&
diff --git a/contrib/mod_quotatab.h b/contrib/mod_quotatab.h
index d196930..ed30333 100644
--- a/contrib/mod_quotatab.h
+++ b/contrib/mod_quotatab.h
@@ -2,7 +2,7 @@
  * ProFTPD: mod_quotatab -- a module for managing FTP byte/file quotas via
  *                          centralized tables
  *
- * Copyright (c) 2001-2012 TJ Saunders
+ * Copyright (c) 2001-2016 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
@@ -28,8 +28,6 @@
  * the ideas in Eric Estabrook's mod_quota, available from
  * ftp://pooh.urbanrage.com/pub/c/.  This module, however, has been written
  * from scratch to implement quotas in a different way.
- *
- * $Id: mod_quotatab.h,v 1.13 2012-03-31 00:35:43 castaglia Exp $
  */
 
 #ifndef MOD_QUOTATAB_H
@@ -216,6 +214,9 @@ int quotatab_unregister_backend(const char *, unsigned int);
 unsigned char quotatab_lookup(quota_tabtype_t, void *, const char *,
   quota_type_t);
 
+unsigned char quotatab_lookup_default(quota_tabtype_t, void *, const char *,
+  quota_type_t);
+
 /* Reads via this function are only ever done on tally tables.  Limit tables
  * are read via the quotatab_lookup function.  Returns 0 on success,
  * -1 on failure (with errno set appropriately).
diff --git a/contrib/mod_quotatab_file.c b/contrib/mod_quotatab_file.c
index 76e213f..37fab3d 100644
--- a/contrib/mod_quotatab_file.c
+++ b/contrib/mod_quotatab_file.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_quotatab_file -- a mod_quotatab sub-module for managing quota
  *                               data via file-based tables
- *
- * Copyright (c) 2002-2011 TJ Saunders
+ * Copyright (c) 2002-2016 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
@@ -21,8 +20,6 @@
  * As a special exemption, TJ Saunders gives permission to link this program
  * with OpenSSL, and distribute the resulting executable, without including
  * the source code for OpenSSL in the source distribution.
- *
- * $Id: mod_quotatab_file.c,v 1.8 2011-05-23 20:56:40 castaglia Exp $
  */
 
 #include "mod_quotatab.h"
@@ -87,6 +84,9 @@ static int filetab_create(quota_table_t *filetab, void *ptr) {
 
   /* Seek to the end of the table */
   current_pos = lseek(filetab->tab_handle, (off_t) 0, SEEK_END);
+  if (current_pos < 0) {
+    return -1;
+  }
 
   while ((res = writev(filetab->tab_handle, quotav, 8)) < 0) {
     if (errno == EINTR) {
@@ -196,6 +196,10 @@ static int filetab_read(quota_table_t *filetab, void *ptr) {
   /* Mark the current file position. */
   off_t current_pos = lseek(filetab->tab_handle, (off_t) 0, SEEK_CUR);
 
+  if (current_pos < 0) {
+    return - 1;
+  }
+
   /* Use readv() to make this more efficient.  It is done piecewise, rather
    * than doing a normal read(2) directly into the struct pointer, to avoid
    * alignment/padding issues.
@@ -350,6 +354,10 @@ static int filetab_write(quota_table_t *filetab, void *ptr) {
   /* Mark the current file position. */
   off_t current_pos = lseek(filetab->tab_handle, (off_t) 0, SEEK_CUR);
 
+  if (current_pos < 0) {
+    return -1;
+  }
+
   /* Use writev() to make this more efficient.  It is done piecewise, rather
    * than doing a normal write(2) directly from the struct pointer, to avoid
    * alignment/padding issues.
diff --git a/contrib/mod_quotatab_ldap.c b/contrib/mod_quotatab_ldap.c
index 011ad65..045180c 100644
--- a/contrib/mod_quotatab_ldap.c
+++ b/contrib/mod_quotatab_ldap.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_quotatab_ldap -- a mod_quotatab sub-module for obtaining
  *                               quota information from an LDAP directory.
  *
- * Copyright (c) 2002-2009 TJ Saunders
+ * Copyright (c) 2002-2014 TJ Saunders
  * Copyright (c) 2002-3 John Morrissey
  *
  * This program is free software; you can redistribute it and/or modify
@@ -51,7 +51,9 @@ static unsigned char ldaptab_lookup(quota_table_t *ldaptab, void *ptr,
   }
 
   /* Find the cmdtable for the ldap_quota_lookup command. */
-  if ((ldap_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "ldap_quota_lookup", NULL, NULL)) == NULL) {
+  ldap_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "ldap_quota_lookup", NULL,
+    NULL, NULL);
+  if (ldap_cmdtab == NULL) {
     quotatab_log("error: unable to find LDAP hook symbol 'ldap_quota_lookup'");
     return FALSE;
   }
diff --git a/contrib/mod_quotatab_radius.c b/contrib/mod_quotatab_radius.c
index e226c3c..5380650 100644
--- a/contrib/mod_quotatab_radius.c
+++ b/contrib/mod_quotatab_radius.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_quotatab_radius -- a mod_quotatab sub-module for obtaining
  *                                 quota information from RADIUS servers.
  *
- * Copyright (c) 2005-2011 TJ Saunders
+ * Copyright (c) 2005-2014 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
@@ -55,8 +55,8 @@ static unsigned char radiustab_lookup(quota_table_t *radiustab, void *ptr,
   }
 
   /* Find the cmdtable for the radius_quota_lookup command. */
-  cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "radius_quota_lookup", NULL,
-    NULL);
+  cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "radius_quota_lookup", NULL,
+    NULL, NULL);
   if (cmdtab == NULL) {
     quotatab_log("error: unable to find RADIUS hook symbol "
       "'radius_quota_lookup'");
diff --git a/contrib/mod_quotatab_sql.c b/contrib/mod_quotatab_sql.c
index 54cbeb2..e1a21c0 100644
--- a/contrib/mod_quotatab_sql.c
+++ b/contrib/mod_quotatab_sql.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_quotatab_sql -- a mod_quotatab sub-module for managing quota
  *                              data via SQL-based tables
  *
- * Copyright (c) 2002-2013 TJ Saunders
+ * Copyright (c) 2002-2015 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
@@ -21,8 +21,6 @@
  * As a special exemption, TJ Saunders gives permission to link this program
  * with OpenSSL, and distribute the resulting executable, without including
  * the source code for OpenSSL in the source distribution.
- *
- * $Id: mod_quotatab_sql.c,v 1.17 2013-01-21 19:59:59 castaglia Exp $
  */
 
 #include "mod_quotatab.h"
@@ -32,10 +30,10 @@
 
 module quotatab_sql_module;
 
-static cmd_rec *sqltab_cmd_create(pool *parent_pool, int argc, ...) {
+static cmd_rec *sqltab_cmd_create(pool *parent_pool, unsigned int argc, ...) {
+  register unsigned int i = 0;
   pool *cmd_pool = NULL;
   cmd_rec *cmd = NULL;
-  register unsigned int i = 0;
   va_list argp;
 
   cmd_pool = make_sub_pool(parent_pool);
@@ -43,14 +41,15 @@ static cmd_rec *sqltab_cmd_create(pool *parent_pool, int argc, ...) {
   cmd->pool = cmd_pool;
 
   cmd->argc = argc;
-  cmd->argv = (char **) pcalloc(cmd->pool, argc * sizeof(char *));
+  cmd->argv = pcalloc(cmd->pool, argc * sizeof(void *));
 
   /* Hmmm... */
   cmd->tmp_pool = cmd->pool;
 
   va_start(argp, argc);
-  for (i = 0; i < argc; i++)
+  for (i = 0; i < argc; i++) {
     cmd->argv[i] = va_arg(argp, char *);
+  }
   va_end(argp);
 
   return cmd;
@@ -62,14 +61,15 @@ static char *sqltab_get_name(pool *p, char *name) {
   modret_t *res;
 
   /* Find the cmdtable for the sql_escapestr command. */
-  cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_escapestr", NULL, NULL);
+  cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_escapestr", NULL, NULL, NULL);
   if (cmdtab == NULL) {
     quotatab_log("error: unable to find SQL hook symbol 'sql_escapestr'");
     return name;
   }
 
-  if (strlen(name) == 0)
+  if (strlen(name) == 0) {
     return name;
+  }
 
   cmd = sqltab_cmd_create(p, 1, pr_str_strip(p, name));
 
@@ -176,7 +176,8 @@ static int sqltab_create(quota_table_t *sqltab, void *ptr) {
     tally_files_in, tally_files_out, tally_files_xfer);
 
   /* Find the cmdtable for the sql_change command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_change", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_change", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     quotatab_log("error: unable to find SQL hook symbol 'sql_change'");
     destroy_pool(tmp_pool);
@@ -219,7 +220,8 @@ static unsigned char sqltab_lookup(quota_table_t *sqltab, void *ptr,
   }
 
   /* Find the cmdtable for the sql_lookup command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     quotatab_log("error: unable to find SQL hook symbol 'sql_lookup'");
     destroy_pool(tmp_pool);
@@ -526,7 +528,8 @@ static int sqltab_write(quota_table_t *sqltab, void *ptr) {
     sqltab_get_name(tmp_pool, tally->name), tally_quota_type);
 
   /* Find the cmdtable for the sql_change command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_change", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_change", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     quotatab_log("error: unable to find SQL hook symbol 'sql_change'");
     destroy_pool(tmp_pool);
diff --git a/contrib/mod_radius.c b/contrib/mod_radius.c
index a714327..b56cdfe 100644
--- a/contrib/mod_radius.c
+++ b/contrib/mod_radius.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_radius -- a module for RADIUS authentication and accounting
- *
- * Copyright (c) 2001-2014 TJ Saunders
+ * Copyright (c) 2001-2016 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
@@ -26,11 +25,9 @@
  *
  * This module is based in part on code in Alan DeKok's (aland at freeradius.org)
  * mod_auth_radius for Apache, in part on the FreeRADIUS project's code.
- *
- * $Id: mod_radius.c,v 1.72 2014-01-06 18:37:44 castaglia Exp $
  */
 
-#define MOD_RADIUS_VERSION "mod_radius/0.9.2"
+#define MOD_RADIUS_VERSION	"mod_radius/0.9.3"
 
 #include "conf.h"
 #include "privs.h"
@@ -47,16 +44,14 @@
 /* From RFC2138 */
 #define RADIUS_STRING_LEN	254
 
-/* RADIUS attribute structures
- */
+/* RADIUS attribute structures */
 typedef struct {
   unsigned char type;
   unsigned char length;
   unsigned char data[1];
 } radius_attrib_t;
 
-/* RADIUS packet header
- */
+/* RADIUS packet header */
 typedef struct {
   unsigned char code;
   unsigned char id;
@@ -69,8 +64,7 @@ typedef struct {
 
 #define RADIUS_HEADER_LEN	20
 
-/* RADIUS ID Definitions (see RFC2865)
- */
+/* RADIUS ID Definitions (see RFC 2865, 2866) */
 #define RADIUS_AUTH_REQUEST		1
 #define RADIUS_AUTH_ACCEPT		2
 #define RADIUS_AUTH_REJECT		3
@@ -79,8 +73,7 @@ typedef struct {
 #define RADIUS_ACCT_STATUS		6
 #define RADIUS_AUTH_CHALLENGE		11
 
-/* RADIUS Attribute Definitions (see RFC2865)
- */
+/* RADIUS Attribute Definitions (see RFC 2865, 2866) */
 #define RADIUS_USER_NAME		1
 #define RADIUS_PASSWORD			2
 #define RADIUS_NAS_IP_ADDRESS		4
@@ -89,6 +82,7 @@ typedef struct {
 #define RADIUS_OLD_PASSWORD		17
 #define RADIUS_REPLY_MESSAGE		18
 #define RADIUS_STATE			24
+#define RADIUS_CLASS			25
 #define RADIUS_VENDOR_SPECIFIC		26
 #define RADIUS_SESSION_TIMEOUT		27
 #define RADIUS_IDLE_TIMEOUT		28
@@ -101,39 +95,48 @@ typedef struct {
 #define RADIUS_ACCT_AUTHENTIC		45
 #define RADIUS_ACCT_SESSION_TIME	46
 #define RADIUS_ACCT_TERMINATE_CAUSE	49
+#define RADIUS_ACCT_EVENT_TS		55
 #define RADIUS_NAS_PORT_TYPE		61
+#define RADIUS_MESSAGE_AUTHENTICATOR	80
 #define RADIUS_NAS_IPV6_ADDRESS		95
 
-/* RADIUS service types
- */
+/* RADIUS service types */
 #define RADIUS_SVC_LOGIN		1
 #define RADIUS_SVC_AUTHENTICATE_ONLY	8
 
-/* RADIUS status types
- */
+/* RADIUS status types */
 #define RADIUS_ACCT_STATUS_START	1
 #define RADIUS_ACCT_STATUS_STOP		2
 #define RADIUS_ACCT_STATUS_ALIVE	3
 
-/* RADIUS NAS port types
- */
+/* RADIUS NAS port types */
 #define RADIUS_NAS_PORT_TYPE_VIRTUAL	5
 
-/* RADIUS authentication types
- */
+/* RADIUS authentication types */
 #define RADIUS_AUTH_NONE		0
 #define RADIUS_AUTH_RADIUS		1
 #define RADIUS_AUTH_LOCAL		2
 
+/* RADIUS Acct-Terminate-Cause types */
+#define RADIUS_ACCT_TERMINATE_CAUSE_USER_REQUEST	1
+#define RADIUS_ACCT_TERMINATE_CAUSE_LOST_SERVICE	3
+#define RADIUS_ACCT_TERMINATE_CAUSE_IDLE_TIMEOUT	4
+#define RADIUS_ACCT_TERMINATE_CAUSE_SESSION_TIMEOUT	5
+#define RADIUS_ACCT_TERMINATE_CAUSE_ADMIN_RESET		6
+#define RADIUS_ACCT_TERMINATE_CAUSE_ADMIN_REBOOT	7
+#define RADIUS_ACCT_TERMINATE_CAUSE_SERVICE_UNAVAIL	15
+#define RADIUS_ACCT_TERMINATE_CAUSE_USER_ERROR		16
+
 /* The RFC says 4096 octets max, and most packets are less than 256.
  * However, this number is just larger than the maximum MTU of just
  * most types of networks, except maybe for gigabit ethernet.
  */
 #define RADIUS_PACKET_LEN		1600
 
-/* Miscellaneous default values
- */
-#define DEFAULT_RADIUS_TIMEOUT	30
+/* Miscellaneous default values */
+#define DEFAULT_RADIUS_TIMEOUT		10
+
+#define RADIUS_ATTRIB_LEN(attr)		((attr)->length)
 
 /* Adjust the VSA length (I'm not sure why this is necessary, but a reading
  * of the FreeRADIUS sources show it to be.  Weird.)
@@ -149,13 +152,14 @@ typedef struct radius_server_obj {
   pool *pool;
 
   /* RADIUS server IP address */
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
 
   /* RADIUS server port */
   unsigned short port;
 
   /* RADIUS server shared secret */
   unsigned char *secret;
+  size_t secret_len;
 
   /* How long to wait for RADIUS responses */
   unsigned int timeout;
@@ -165,12 +169,19 @@ typedef struct radius_server_obj {
 module radius_module;
 
 static pool *radius_pool = NULL;
-static unsigned char radius_engine = FALSE;
+static int radius_engine = FALSE;
 static radius_server_t *radius_acct_server = NULL;
 static radius_server_t *radius_auth_server = NULL;
-
 static int radius_logfd = -1;
-static char *radius_logname = NULL;
+
+/* mod_radius option flags */
+#define RADIUS_OPT_IGNORE_REPLY_MESSAGE_ATTR		0x0001
+#define RADIUS_OPT_IGNORE_CLASS_ATTR			0x0002
+#define RADIUS_OPT_IGNORE_SESSION_TIMEOUT_ATTR		0x0004
+#define RADIUS_OPT_IGNORE_IDLE_TIMEOUT_ATTR		0x0008
+#define RADIUS_OPT_REQUIRE_MAC				0x0010
+
+static unsigned long radius_opts = 0UL;
 
 static struct sockaddr radius_local_sock, radius_remote_sock;
 
@@ -182,6 +193,12 @@ static int radius_session_authtype = RADIUS_AUTH_LOCAL;
 static unsigned char radius_auth_ok = FALSE;
 static unsigned char radius_auth_reject = FALSE;
 
+/* For tracking the Class attribute, for sending in accounting requests. */
+static char *radius_acct_class = NULL;
+static size_t radius_acct_classlen = 0;
+static char *radius_acct_user = NULL;
+static size_t radius_acct_userlen = 0;
+
 /* "Fake" user/group information for RADIUS users. */
 static unsigned char radius_have_user_info = FALSE;
 static struct passwd radius_passwd;
@@ -244,33 +261,30 @@ static int radius_quota_files_xfer_attr_id = 0;
  */
 static unsigned char radius_last_acct_pkt_id = 0;
 
+static const char *trace_channel = "radius";
+
 /* Convenience macros. */
 #define RADIUS_IS_VAR(str) \
-  str[0] == '$' && str[1] == '(' && str[strlen(str)-1] == ')'
+  ((str[0] == '$') && (str[1] == '(') && (str[strlen(str)-1] == ')'))
 
 /* Function prototypes. */
-static void radius_add_attrib(radius_packet_t *, unsigned char,
+static radius_attrib_t *radius_add_attrib(radius_packet_t *, unsigned char,
   const unsigned char *, size_t);
 static void radius_add_passwd(radius_packet_t *, unsigned char,
-  const unsigned char *, unsigned char *);
+  const unsigned char *, unsigned char *, size_t);
 static void radius_build_packet(radius_packet_t *, const unsigned char *,
-  const unsigned char *, unsigned char *);
+  const unsigned char *, unsigned char *, size_t);
 static unsigned char radius_have_var(char *);
-static int radius_closelog(void);
-static int radius_close_socket(int);
-static void radius_get_acct_digest(radius_packet_t *, unsigned char *);
 static radius_attrib_t *radius_get_attrib(radius_packet_t *, unsigned char);
+static radius_attrib_t *radius_get_next_attrib(radius_packet_t *,
+  unsigned char, unsigned int *, radius_attrib_t *);
 static void radius_get_rnd_digest(radius_packet_t *);
 static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *,
   unsigned char);
-
-static int radius_log(const char *, ...)
-#ifdef __GNUC__
-       __attribute__ ((format (printf, 1, 2)));
-#else
-       ;
-#endif
-
+static void radius_set_acct_digest(radius_packet_t *, const unsigned char *,
+  size_t);
+static void radius_set_auth_mac(radius_packet_t *, const unsigned char *,
+  size_t);
 static radius_server_t *radius_make_server(pool *);
 static int radius_openlog(void);
 static int radius_open_socket(void);
@@ -278,17 +292,23 @@ static unsigned char radius_parse_gids_str(pool *, char *, gid_t **,
   unsigned int *);
 static unsigned char radius_parse_groups_str(pool *, char *, char ***,
   unsigned int *);
-static void radius_parse_var(char *, int *, char **);
-static void radius_process_accpt_packet(radius_packet_t *);
+static int radius_parse_var(char *, int *, char **);
+static int radius_process_accept_packet(radius_packet_t *,
+  const unsigned char *, size_t);
+static int radius_process_reject_packet(radius_packet_t *,
+  const unsigned char *, size_t);
 static void radius_process_group_info(config_rec *);
 static void radius_process_quota_info(config_rec *);
 static void radius_process_user_info(config_rec *);
 static radius_packet_t *radius_recv_packet(int, unsigned int);
 static int radius_send_packet(int, radius_packet_t *, radius_server_t *);
-static unsigned char radius_start_accting(void);
-static unsigned char radius_stop_accting(void);
+static int radius_start_accting(void);
+static int radius_stop_accting(void);
+static int radius_verify_auth_mac(radius_packet_t *, const char *,
+  const unsigned char *, size_t);
 static int radius_verify_packet(radius_packet_t *, radius_packet_t *,
-  unsigned char *);
+  const unsigned char *, size_t);
+static int radius_sess_init(void);
 
 /* Support functions
  */
@@ -338,30 +358,40 @@ static char *radius_argsep(char **arg) {
 /* Check a "$(attribute-id:default)" string for validity. */
 static unsigned char radius_have_var(char *var) {
   int id = 0;
-  char *tmp = NULL;
+  char *ptr = NULL;
+  size_t varlen;
+
+  varlen = strlen(var);
 
   /* Must be at least six characters. */
-  if (strlen(var) < 7)
+  if (varlen < 7) {
     return FALSE;
+  }
 
   /* Must start with '$(', and end with ')'. */
-  if (!RADIUS_IS_VAR(var))
+  if (RADIUS_IS_VAR(var) == FALSE) {
     return FALSE;
+  }
 
   /* Must have a ':'. */
-  if ((tmp = strchr(var, ':')) == NULL)
+  ptr = strchr(var, ':');
+  if (ptr == NULL) {
     return FALSE;
+  }
 
   /* ':' must be between '(' and ')'. */
-  if (tmp < (var + 3) || tmp > &var[strlen(var)-2])
+  if (ptr < (var + 3) ||
+      ptr > &var[varlen-2]) {
     return FALSE;
+  }
 
   /* Parse out the component int/string. */
   radius_parse_var(var, &id, NULL);
 
   /* Int must be greater than zero. */
-  if (id < 1)
+  if (id < 1) {
     return FALSE;
+  }
 
   return TRUE;
 }
@@ -369,45 +399,59 @@ static unsigned char radius_have_var(char *var) {
 /* Separate the given "$(attribute-id:default)" string into its constituent
  * custom attribute ID (int) and default (string) components.
  */
-static void radius_parse_var(char *var, int *attr_id, char **attr_default) {
-  pool *tmp_pool = make_sub_pool(radius_pool);
-  char *var_cpy = pstrdup(tmp_pool, var), *tmp = NULL;
-  size_t var_cpylen;
+static int radius_parse_var(char *var, int *attr_id, char **attr_default) {
+  pool *tmp_pool;
+  char *var_cpy, *ptr = NULL;
+  size_t var_len, var_cpylen;
+
+  if (var == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  var_cpylen = strlen(var_cpy);
-  if (var_cpylen == 0) {
+  var_len = var_cpylen = strlen(var);
+  if (var_len == 0) {
     /* Empty string; nothing to do. */
-    destroy_pool(tmp_pool);
-    return;
+    return 0;
   }
+  
+  tmp_pool = make_sub_pool(radius_pool);
+  var_cpy = pstrdup(tmp_pool, var);
 
   /* First, strip off the "$()" variable characters. */
   var_cpy[var_cpylen-1] = '\0';
   var_cpy += 2;
 
   /* Find the delimiting ':' */
-  tmp = strchr(var_cpy, ':');
-
-  *tmp++ = '\0';
+  ptr = strchr(var_cpy, ':');
+  if (ptr != NULL) {
+    *ptr++ = '\0';
+  }
 
-  if (attr_id)
+  if (attr_id) {
     *attr_id = atoi(var_cpy);
+  }
 
   if (attr_default) {
-    tmp = strchr(var, ':');
+    ptr = strchr(var, ':');
 
     /* Note: this works because the calling of this function by
      * radius_have_var(), which occurs during the parsing process, uses
      * a NULL for this portion, so that the string stored in the config_rec
      * is not actually manipulated, as is done here.
      */
-    var[strlen(var)-1] = '\0';
+    if (var_len > 0) {
+      var[var_len-1] = '\0';
+    }
 
-    *attr_default = ++tmp;
+    if (ptr != NULL) {
+      *attr_default = ++ptr;
+    }
   }
 
   /* Clean up. */
   destroy_pool(tmp_pool);
+  return 0;
 }
 
 static unsigned char radius_parse_gids_str(pool *p, char *gids_str, 
@@ -463,28 +507,213 @@ static unsigned char radius_parse_groups_str(pool *p, char *groups_str,
   return TRUE;
 }
 
-static void radius_process_accpt_packet(radius_packet_t *packet) {
+static int radius_process_standard_attribs(radius_packet_t *pkt,
+    const unsigned char *secret, size_t secret_len) {
+  int attrib_count = 0;
+  radius_attrib_t *attrib = NULL;
+  unsigned char attrib_len;
 
-  /* First, parse the packet for any non-RadiusUserInfo attributes,
-   * such as timeouts.  None are currently implemented, but...this would
-   * be the place to do it.
-   */
-  /* radius_log("parsing packet for custom attribute IDs"); */
+  pr_trace_msg(trace_channel, 2, "parsing packet for standard attribute IDs");
 
-  /* Now, parse the packet for any server-supplied RadiusUserInfo attributes,
-   * if RadiusUserInfo is indeed in effect.
+  if (radius_verify_auth_mac(pkt, "Access-Accept", secret, secret_len) < 0) {
+    return -1;
+  }
+
+  /* TODO: Should we handle the Service-Type attribute here, make sure that it
+   * is a) a service type we implement, and b) the service type that we
+   * requested?
    */
 
-  if (radius_have_user_info == FALSE &&
-      radius_have_group_info == FALSE &&
-      radius_have_quota_info == FALSE) {
-    /* Return now if there's no reason for doing extra work. */
-    return;
+  /* Handle any CLASS attribute. */
+  if (!(radius_opts & RADIUS_OPT_IGNORE_CLASS_ATTR)) {
+    attrib = radius_get_attrib(pkt, RADIUS_CLASS);
+    if (attrib != NULL) {
+      attrib_len = RADIUS_ATTRIB_LEN(attrib);
+      if (attrib_len > 0) {
+        char *class = NULL;
+
+        class = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
+        pr_trace_msg(trace_channel, 7,
+          "found Class attribute in Access-Accept message: %s", class);
+        radius_acct_class = class;
+        radius_acct_classlen = attrib_len;
+      }
+
+      attrib_count++;
+
+    } else {
+      pr_trace_msg(trace_channel, 6,
+        "Access-Accept packet lacks Class attribute (%d)", RADIUS_CLASS);
+    }
+  }
+
+  /* Handle any User-Name attribute, per RFC 2865, Section 5.1. */
+  attrib = radius_get_attrib(pkt, RADIUS_USER_NAME);
+  if (attrib != NULL) {
+    attrib_len = RADIUS_ATTRIB_LEN(attrib);
+    if (attrib_len > 0) {
+      char *user_name = NULL;
+
+      user_name = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
+      pr_trace_msg(trace_channel, 7,
+        "found User-Name attribute in Access-Accept message: %s", user_name);
+      radius_acct_user = user_name;
+      radius_acct_userlen = attrib_len;
+    }
+
+    attrib_count++;
+
+  } else {
+    pr_trace_msg(trace_channel, 6,
+      "Access-Accept packet lacks User-Name attribute (%d)", RADIUS_USER_NAME);
+  }
+
+  /* Handle any REPLY_MESSAGE attributes. */
+  if (!(radius_opts & RADIUS_OPT_IGNORE_REPLY_MESSAGE_ATTR)) {
+    unsigned int pkt_len = 0;
+
+    attrib = radius_get_next_attrib(pkt, RADIUS_REPLY_MESSAGE, &pkt_len, NULL);
+    while (attrib != NULL) {
+      pr_signals_handle();
+
+      attrib_len = RADIUS_ATTRIB_LEN(attrib);
+      if (attrib_len > 0) {
+        char *reply_msg = NULL;
+
+        reply_msg = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
+        pr_trace_msg(trace_channel, 7,
+          "found REPLY_MESSAGE attribute in Access-Accept message: '%s'",
+          reply_msg);
+        pr_response_add(R_DUP, "%s", reply_msg);
+      }
+
+      attrib_count++;
+
+      if (pkt_len == 0) {
+        break;
+      }
+
+      attrib = radius_get_next_attrib(pkt, RADIUS_REPLY_MESSAGE, &pkt_len,
+        attrib);
+    }
+
+    if (attrib_count == 0) {
+      pr_trace_msg(trace_channel, 6,
+        "Access-Accept packet lacks Reply-Message attribute (%d)",
+        RADIUS_REPLY_MESSAGE);
+    }
+  }
+
+  /* Handle any IDLE_TIMEOUT attribute. */
+  if (!(radius_opts & RADIUS_OPT_IGNORE_IDLE_TIMEOUT_ATTR)) {
+    attrib = radius_get_attrib(pkt, RADIUS_IDLE_TIMEOUT);
+    if (attrib != NULL) {
+      attrib_len = RADIUS_ATTRIB_LEN(attrib);
+      if (attrib_len > 0) {
+        int timeout = -1;
+
+        if (attrib_len > sizeof(timeout)) {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "invalid attribute length (%u) for Idle-Timeout, truncating",
+            attrib_len);
+          attrib_len = sizeof(timeout);
+        }
+
+        memcpy(&timeout, attrib->data, attrib_len);
+        timeout = ntohl(timeout);
+
+        if (timeout < 0) {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes Idle-Timeout attribute %d for illegal timeout: %d",
+            RADIUS_IDLE_TIMEOUT, timeout);
+
+        } else {
+          config_rec *c;
+
+          pr_trace_msg(trace_channel, 2,
+            "packet includes Idle-Timeout attribute %d for timeout: %d",
+            RADIUS_IDLE_TIMEOUT, timeout);
+          remove_config(main_server->conf, "TimeoutIdle", TRUE);
+
+          c = pr_config_add_set(&main_server->conf, "TimeoutIdle",
+            PR_CONFIG_FL_INSERT_HEAD);
+          c->config_type = CONF_PARAM;
+          c->argc = 1;
+          c->argv[0] = palloc(c->pool, sizeof(int));
+          *((int *) c->argv[0]) = timeout;
+
+          attrib_count++;
+        }
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 6,
+        "Access-Accept packet lacks Idle-Timeout attribute (%d)",
+        RADIUS_IDLE_TIMEOUT);
+    }
+  }
+
+  /* Handle any SESSION_TIMEOUT attribute. */
+  if (!(radius_opts & RADIUS_OPT_IGNORE_SESSION_TIMEOUT_ATTR)) {
+    attrib = radius_get_attrib(pkt, RADIUS_SESSION_TIMEOUT);
+    if (attrib != NULL) {
+      attrib_len = RADIUS_ATTRIB_LEN(attrib);
+      if (attrib_len > 0) {
+        int timeout = -1;
+
+        if (attrib_len > sizeof(timeout)) {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "invalid attribute length (%u) for Session-Timeout, truncating",
+            attrib_len);
+          attrib_len = sizeof(timeout);
+        }
+
+        memcpy(&timeout, attrib->data, attrib_len);
+        timeout = ntohl(timeout);
+
+        if (timeout < 0) {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes Session-Timeout attribute %d for illegal "
+            "timeout: %d", RADIUS_SESSION_TIMEOUT, timeout);
+
+        } else {
+          config_rec *c;
+
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes Session-Timeout attribute %d for timeout: %d",
+            RADIUS_SESSION_TIMEOUT, timeout);
+          remove_config(main_server->conf, "TimeoutSession", TRUE);
+
+          c = pr_config_add_set(&main_server->conf, "TimeoutSession",
+            PR_CONFIG_FL_INSERT_HEAD);
+          c->config_type = CONF_PARAM;
+          c->argc = 2;
+          c->argv[0] = palloc(c->pool, sizeof(int));
+          *((int *) c->argv[0]) = timeout;
+          c->argv[1] = palloc(c->pool, sizeof(unsigned int));
+          *((unsigned int *) c->argv[1]) = 0;
+
+          attrib_count++;
+        }
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 6,
+        "Access-Accept packet lacks Session-Timeout attribute (%d)",
+        RADIUS_SESSION_TIMEOUT);
+    }
   }
 
+  return attrib_count;
+}
+
+static int radius_process_user_info_attribs(radius_packet_t *pkt) {
+  int attrib_count = 0;
+
   if (radius_uid_attr_id || radius_gid_attr_id ||
       radius_home_attr_id || radius_shell_attr_id) {
-    radius_log("parsing packet for RadiusUserInfo attributes");
+    pr_trace_msg(trace_channel, 2,
+      "parsing packet for RadiusUserInfo attributes");
 
     /* These custom values will been supplied in the configuration file, and
      * set when the RadiusUserInfo config_rec is retrieved, during
@@ -492,9 +721,9 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
      */
 
     if (radius_uid_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_uid_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_uid_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         int uid = -1;
@@ -507,7 +736,8 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
          */
 
         if (attrib_len > sizeof(uid)) {
-          radius_log("invalid attribute length (%u) for user ID, truncating",
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "invalid attribute length (%u) for user ID, truncating",
             attrib_len);
           attrib_len = sizeof(uid);
         }
@@ -516,29 +746,32 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
         uid = ntohl(uid);
 
         if (uid < 0) {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "illegal user ID: '%u'", radius_vendor_name, radius_uid_attr_id,
-            uid);
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes '%s' Vendor-Specific Attribute %d for illegal "
+            "user ID: %d", radius_vendor_name, radius_uid_attr_id, uid);
 
         } else {
           radius_passwd.pw_uid = uid;
 
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "user ID: '%u'", radius_vendor_name, radius_uid_attr_id,
+          pr_trace_msg(trace_channel, 3,
+            "packet includes '%s' Vendor-Specific Attribute %d for user ID: %d",
+            radius_vendor_name, radius_uid_attr_id,
             radius_passwd.pw_uid);
+          attrib_count++;
         }
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "user ID: defaulting to '%u'", radius_vendor_name, radius_uid_attr_id,
-          radius_passwd.pw_uid);
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d "
+          "for user ID; defaulting to '%u'", radius_vendor_name,
+          radius_uid_attr_id, radius_passwd.pw_uid);
       }
     }
 
     if (radius_gid_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_gid_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_gid_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         int gid = -1;
@@ -551,7 +784,8 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
          */
 
         if (attrib_len > sizeof(gid)) {
-          radius_log("invalid attribute length (%u) for group ID, truncating",
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "invalid attribute length (%u) for group ID, truncating",
             attrib_len);
           attrib_len = sizeof(gid);
         }
@@ -560,106 +794,103 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
         gid = ntohl(gid);
 
         if (gid < 0) {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "illegal group ID: '%u'", radius_vendor_name, radius_gid_attr_id,
-            gid);
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes '%s' Vendor-Specific Attribute %d for illegal "
+            "group ID: %d", radius_vendor_name, radius_gid_attr_id, gid);
 
         } else {
           radius_passwd.pw_gid = gid;
 
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "group ID: '%u'", radius_vendor_name, radius_gid_attr_id,
+          pr_trace_msg(trace_channel, 3,
+            "packet includes '%s' Vendor-Specific Attribute %d for group "
+            "ID: %d", radius_vendor_name, radius_gid_attr_id,
             radius_passwd.pw_gid);
+          attrib_count++;
         }
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "group ID: defaulting to '%u'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d "
+          "for group ID; defaulting to '%u'", radius_vendor_name,
           radius_gid_attr_id, radius_passwd.pw_gid);
       }
     }
 
     if (radius_home_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_home_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_home_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *home;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
 
-        /* RADIUS strings are not NUL-terminated. */
-        home = pcalloc(radius_pool, attrib_len + 1);
-
-        /* Parse the attribute value into a string of the necessary length,
-         * then replace radius_passwd.pw_dir with it.  Make sure it's a sane
-         * home directory (ie starts with a '/').
-         */
-
-        memcpy(home, attrib->data, attrib_len);
-
+        home = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         if (*home != '/') {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "illegal home: '%s'", radius_vendor_name, radius_home_attr_id,
-            home);
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes '%s' Vendor-Specific Attribute %d for illegal "
+            "home: '%s'", radius_vendor_name, radius_home_attr_id, home);
 
         } else {
           radius_passwd.pw_dir = home;
 
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "home directory: '%s'", radius_vendor_name, radius_home_attr_id,
+          pr_trace_msg(trace_channel, 3,
+            "packet includes '%s' Vendor-Specific Attribute %d for home "
+            "directory: '%s'", radius_vendor_name, radius_home_attr_id,
             radius_passwd.pw_dir);
+          attrib_count++;
         }
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "home directory: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "home directory; defaulting to '%s'", radius_vendor_name,
           radius_home_attr_id, radius_passwd.pw_dir);
       }
     }
 
     if (radius_shell_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_shell_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_shell_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *shell;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
 
-        /* RADIUS strings are not NUL-terminated. */
-        shell = pcalloc(radius_pool, attrib_len + 1);
-
-        /* Parse the attribute value into a string of the necessary length,
-         * then replace radius_passwd.pw_shell with it.  Make sure it's a sane
-         * shell (ie starts with a '/').
-         */
-
-        memcpy(shell, attrib->data, attrib_len);
-
+        shell = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         if (*shell != '/') {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "illegal shell: '%s'", radius_vendor_name, radius_shell_attr_id,
-            shell);
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes '%s' Vendor-Specific Attribute %d for illegal "
+            "shell: '%s'", radius_vendor_name, radius_shell_attr_id, shell);
 
         } else {
           radius_passwd.pw_shell = shell;
 
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
+          pr_trace_msg(trace_channel, 3,
+            "packet includes '%s' Vendor-Specific Attribute %d for "
             "shell: '%s'", radius_vendor_name, radius_shell_attr_id,
             radius_passwd.pw_shell);
+          attrib_count++;
         }
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "shell: defaulting to '%s'", radius_vendor_name, radius_shell_attr_id,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "shell; defaulting to '%s'", radius_vendor_name, radius_shell_attr_id,
           radius_passwd.pw_shell);
       }
     }
   }
 
+  return attrib_count;
+}
+
+static int radius_process_group_info_attribs(radius_packet_t *pkt) {
+  int attrib_count = 0;
+
   if (radius_prime_group_name_attr_id ||
       radius_addl_group_names_attr_id ||
       radius_addl_group_ids_attr_id) {
@@ -667,48 +898,46 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
     char **groups = NULL;
     gid_t *gids = NULL;
 
-    radius_log("parsing packet for RadiusGroupInfo attributes");
+    pr_trace_msg(trace_channel, 2,
+      "parsing packet for RadiusGroupInfo attributes");
 
     if (radius_prime_group_name_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_prime_group_name_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_prime_group_name_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *group_name;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
 
-        /* RADIUS strings are not NUL-terminated. */
-        group_name = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(group_name, attrib->data, attrib_len);
-
+        group_name = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_prime_group_name = pstrdup(radius_pool, group_name);
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "primary group name: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for primary "
+          "group name: '%s'", radius_vendor_name,
           radius_prime_group_name_attr_id, radius_prime_group_name);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "prime group name: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "prime group name; defaulting to '%s'", radius_vendor_name,
           radius_prime_group_name_attr_id, radius_prime_group_name);
       }
     }
 
     if (radius_addl_group_names_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_addl_group_names_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_addl_group_names_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *group_names, *group_names_str;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        group_names = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(group_names, attrib->data, attrib_len);
+        group_names = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
 
         /* Make a copy of the string, for parsing purposes.  The parsing
          * of this string will consume it.
@@ -717,36 +946,38 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
 
         if (!radius_parse_groups_str(radius_pool, group_names_str, &groups,
             &ngroups)) {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "illegal additional group names: '%s'", radius_vendor_name,
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes '%s' Vendor-Specific Attribute %d for illegal "
+            "additional group names: '%s'", radius_vendor_name,
             radius_addl_group_names_attr_id, group_names);
 
         } else {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
+          pr_trace_msg(trace_channel, 3,
+            "packet includes '%s' Vendor-Specific Attribute %d for "
             "additional group names: '%s'", radius_vendor_name,
             radius_addl_group_names_attr_id, group_names);
         }
 
+        attrib_count++;
+
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "additional group names: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "additional group names; defaulting to '%s'", radius_vendor_name,
           radius_addl_group_names_attr_id, radius_addl_group_names_str);
       }
     }
 
     if (radius_addl_group_ids_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_addl_group_ids_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_addl_group_ids_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *group_ids, *group_ids_str;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        group_ids = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(group_ids, attrib->data, attrib_len);
+        group_ids = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
 
         /* Make a copy of the string, for parsing purposes.  The parsing
          * of this string will consume it.
@@ -754,19 +985,24 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
         group_ids_str = pstrdup(radius_pool, group_ids);
 
         if (!radius_parse_gids_str(radius_pool, group_ids_str, &gids, &ngids)) {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "illegal additional group IDs: '%s'", radius_vendor_name,
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "packet includes '%s' Vendor-Specific Attribute %d for illegal "
+            "additional group IDs: '%s'", radius_vendor_name,
             radius_addl_group_ids_attr_id, group_ids);
 
         } else {
-          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-            "additional group IDs: '%s'", radius_vendor_name,
+          pr_trace_msg(trace_channel, 3,
+            "packet includes '%s' Vendor-Specific Attribute %d for additional "
+            "group IDs: '%s'", radius_vendor_name,
             radius_addl_group_ids_attr_id, group_ids);
         }
 
+        attrib_count++;
+
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "additional group IDs: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "additional group IDs; defaulting to '%s'", radius_vendor_name,
           radius_addl_group_ids_attr_id, radius_addl_group_ids_str);
       }
     }
@@ -781,11 +1017,18 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
       radius_addl_group_ids = gids;
 
     } else {
-      radius_log("server provided mismatched number of group names (%u) "
-        "and group IDs (%u), ignoring them", ngroups, ngids);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "server provided mismatched number of group names (%u) and group "
+        "IDs (%u), ignoring them", ngroups, ngids);
     }
   }
 
+  return attrib_count;
+}
+
+static int radius_process_quota_info_attribs(radius_packet_t *pkt) {
+  int attrib_count = 0;
+
   if (radius_quota_per_sess_attr_id ||
       radius_quota_limit_type_attr_id ||
       radius_quota_bytes_in_attr_id ||
@@ -795,224 +1038,295 @@ static void radius_process_accpt_packet(radius_packet_t *packet) {
       radius_quota_files_out_attr_id ||
       radius_quota_files_xfer_attr_id) {
 
-    radius_log("parsing packet for RadiusQuotaInfo attributes");
+    pr_trace_msg(trace_channel, 2,
+      "parsing packet for RadiusQuotaInfo attributes");
 
     if (radius_quota_per_sess_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_per_sess_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_per_sess_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *per_sess;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        per_sess = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(per_sess, attrib->data, attrib_len);
+        per_sess = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
 
         radius_quota_per_sess = per_sess;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota per-session: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 2,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota "
+          "per-session: '%s'", radius_vendor_name,
           radius_quota_per_sess_attr_id, radius_quota_per_sess);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota per-session: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota per-session; defaulting to '%s'", radius_vendor_name,
           radius_quota_per_sess_attr_id, radius_quota_per_sess);
       }
     }
 
     if (radius_quota_limit_type_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_limit_type_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_limit_type_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *limit_type;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        limit_type = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(limit_type, attrib->data, attrib_len);
-
+        limit_type = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_limit_type = limit_type;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota limit type: '%s'", radius_vendor_name,
-          radius_quota_limit_type_attr_id, radius_quota_limit_type);
+        pr_trace_msg(trace_channel, 2,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota limit "
+          "type: '%s'", radius_vendor_name, radius_quota_limit_type_attr_id,
+          radius_quota_limit_type);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota limit type: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota limit type; defaulting to '%s'", radius_vendor_name,
           radius_quota_limit_type_attr_id, radius_quota_limit_type);
       }
     }
 
     if (radius_quota_bytes_in_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_bytes_in_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_bytes_in_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *bytes_in;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        bytes_in = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(bytes_in, attrib->data, attrib_len);
-
+        bytes_in = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_bytes_in = bytes_in;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota bytes in available: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota bytes "
+          "in available: '%s'", radius_vendor_name,
           radius_quota_bytes_in_attr_id, radius_quota_bytes_in);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota bytes in available: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota bytes in available; defaulting to '%s'", radius_vendor_name,
           radius_quota_bytes_in_attr_id, radius_quota_bytes_in);
       }
     }
 
     if (radius_quota_bytes_out_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_bytes_out_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_bytes_out_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *bytes_out;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        bytes_out = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(bytes_out, attrib->data, attrib_len);
-
+        bytes_out = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_bytes_out = bytes_out;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota bytes out available: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota bytes "
+          "out available: '%s'", radius_vendor_name,
           radius_quota_bytes_out_attr_id, radius_quota_bytes_out);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota bytes out available: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota bytes out available; defaulting to '%s'", radius_vendor_name,
           radius_quota_bytes_out_attr_id, radius_quota_bytes_out);
       }
     }
 
     if (radius_quota_bytes_xfer_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_bytes_xfer_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_bytes_xfer_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *bytes_xfer;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        bytes_xfer = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(bytes_xfer, attrib->data, attrib_len);
-
+        bytes_xfer = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_bytes_xfer = bytes_xfer;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota bytes xfer available: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota bytes "
+          "xfer available: '%s'", radius_vendor_name,
           radius_quota_bytes_xfer_attr_id, radius_quota_bytes_xfer);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota bytes xfer available: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota bytes xfer available; defaulting to '%s'", radius_vendor_name,
           radius_quota_bytes_xfer_attr_id, radius_quota_bytes_xfer);
       }
     }
 
     if (radius_quota_files_in_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_files_in_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_files_in_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *files_in;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        files_in = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(files_in, attrib->data, attrib_len);
-
+        files_in = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_files_in = files_in;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota files in available: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota files "
+          "in available: '%s'", radius_vendor_name,
           radius_quota_files_in_attr_id, radius_quota_files_in);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota files in available: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota files in available; defaulting to '%s'", radius_vendor_name,
           radius_quota_files_in_attr_id, radius_quota_files_in);
       }
     }
 
     if (radius_quota_files_out_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_files_out_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_files_out_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *files_out;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        files_out = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(files_out, attrib->data, attrib_len);
-
+        files_out = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_files_out = files_out;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota files out available: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota files "
+          "out available: '%s'", radius_vendor_name,
           radius_quota_files_out_attr_id, radius_quota_files_out);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota files out available: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota files out available; defaulting to '%s'", radius_vendor_name,
           radius_quota_files_out_attr_id, radius_quota_files_out);
       }
     }
 
     if (radius_quota_files_xfer_attr_id) {
-      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
-        radius_quota_files_xfer_attr_id);
+      radius_attrib_t *attrib;
 
+      attrib = radius_get_vendor_attrib(pkt, radius_quota_files_xfer_attr_id);
       if (attrib) {
         unsigned char attrib_len;
         char *files_xfer;
 
         attrib_len = RADIUS_VSA_ATTRIB_LEN(attrib);
-
-        /* RADIUS strings are not NUL-terminated. */
-        files_xfer = pcalloc(radius_pool, attrib_len + 1);
-        memcpy(files_xfer, attrib->data, attrib_len);
-
+        files_xfer = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
         radius_quota_files_xfer = files_xfer;
 
-        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
-          "quota files xfer available: '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 3,
+          "packet includes '%s' Vendor-Specific Attribute %d for quota files "
+          "xfer available: '%s'", radius_vendor_name,
           radius_quota_files_xfer_attr_id, radius_quota_files_xfer);
+        attrib_count++;
 
       } else {
-        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
-          "quota files xfer available: defaulting to '%s'", radius_vendor_name,
+        pr_trace_msg(trace_channel, 6,
+          "Access-Accept packet lacks '%s' Vendor-Specific Attribute %d for "
+          "quota files xfer available; defaulting to '%s'", radius_vendor_name,
           radius_quota_files_xfer_attr_id, radius_quota_files_xfer);
       }
     }
   }
+
+  return attrib_count;
+}
+
+static int radius_process_accept_packet(radius_packet_t *pkt,
+    const unsigned char *secret, size_t secret_len) {
+  int attrib_count = 0, res;;
+
+  res = radius_process_standard_attribs(pkt, secret, secret_len);
+  if (res < 0) {
+    return -1;
+  }
+
+  attrib_count += res;
+
+  /* Now, parse the packet for any server-supplied RadiusUserInfo attributes,
+   * if RadiusUserInfo is indeed in effect.
+   */
+
+  if (radius_have_user_info == FALSE &&
+      radius_have_group_info == FALSE &&
+      radius_have_quota_info == FALSE) {
+    /* Return now if there's no reason for doing extra work. */
+    return attrib_count;
+  }
+
+  attrib_count += radius_process_user_info_attribs(pkt);
+  attrib_count += radius_process_group_info_attribs(pkt);
+  attrib_count += radius_process_quota_info_attribs(pkt);
+
+  return attrib_count;
+}
+
+static int radius_process_reject_packet(radius_packet_t *pkt,
+    const unsigned char *secret, size_t secret_len) {
+  int attrib_count = 0;
+
+  if (radius_verify_auth_mac(pkt, "Access-Reject", secret, secret_len) < 0) {
+    return -1;
+  }
+
+  /* Handle any REPLY_MESSAGE attributes. */
+  if (!(radius_opts & RADIUS_OPT_IGNORE_REPLY_MESSAGE_ATTR)) {
+    radius_attrib_t *attrib = NULL;
+    unsigned int pkt_len = 0;
+
+    attrib = radius_get_next_attrib(pkt, RADIUS_REPLY_MESSAGE, &pkt_len,
+      NULL);
+    while (attrib != NULL) {
+      unsigned char attrib_len;
+
+      pr_signals_handle();
+
+      attrib_len = RADIUS_ATTRIB_LEN(attrib);
+      if (attrib_len > 0) {
+        char *reply_msg = NULL;
+
+        reply_msg = pstrndup(radius_pool, (char *) attrib->data, attrib_len);
+
+        pr_trace_msg(trace_channel, 7,
+          "found REPLY_MESSAGE attribute in Access-Reject message: '%s'",
+          reply_msg);
+        pr_response_add_err(R_DUP, "%s", reply_msg);
+      }
+
+      attrib_count++;
+
+      if (pkt_len == 0) {
+        break;
+      }
+
+      attrib = radius_get_next_attrib(pkt, RADIUS_REPLY_MESSAGE, &pkt_len,
+        attrib);
+    }
+  }
+
+  return attrib_count;
 }
 
 static void radius_process_group_info(config_rec *c) {
@@ -1028,7 +1342,7 @@ static void radius_process_group_info(config_rec *c) {
    */
 
   param = (char *) c->argv[0];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_prime_group_name_attr_id,
       &radius_prime_group_name);
 
@@ -1048,8 +1362,8 @@ static void radius_process_group_info(config_rec *c) {
     /* Now, parse the default value provided. */
     if (!radius_parse_groups_str(c->pool, radius_addl_group_names_str,
         &groups, &ngroups)) {
-      radius_log("badly formatted RadiusGroupInfo default additional "
-        "group names");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "badly formatted RadiusGroupInfo default additional group names");
       have_illegal_value = TRUE;
     }
 
@@ -1067,8 +1381,8 @@ static void radius_process_group_info(config_rec *c) {
     /* Similarly, parse the default value provided. */
     if (!radius_parse_gids_str(c->pool, radius_addl_group_ids_str,
         &gids, &ngids)) {
-      radius_log("badly formatted RadiusGroupInfo default additional "
-        "group IDs");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "badly formatted RadiusGroupInfo default additional group IDs");
       have_illegal_value = TRUE;
     }
 
@@ -1079,8 +1393,9 @@ static void radius_process_group_info(config_rec *c) {
 
   if (!have_illegal_value &&
       ngroups != ngids) {
-    radius_log("mismatched number of RadiusGroupInfo default additional "
-      "group names (%u) and IDs (%u)", ngroups, ngids);
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "mismatched number of RadiusGroupInfo default additional group "
+      "names (%u) and IDs (%u)", ngroups, ngids);
     have_illegal_value = TRUE;
   }
 
@@ -1092,7 +1407,8 @@ static void radius_process_group_info(config_rec *c) {
 
   } else {
     radius_have_group_info = FALSE;
-    radius_log("error with RadiusGroupInfo parameters, ignoring them");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error with RadiusGroupInfo parameters, ignoring them");
   }
 }
 
@@ -1106,7 +1422,7 @@ static void radius_process_quota_info(config_rec *c) {
    */
 
   param = (char *) c->argv[0];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_per_sess_attr_id,
       &radius_quota_per_sess);
 
@@ -1115,13 +1431,18 @@ static void radius_process_quota_info(config_rec *c) {
 
     if (strcasecmp(param, "false") != 0 &&
         strcasecmp(param, "true") != 0) {
-      radius_log("illegal RadiusQuotaInfo per-session value: '%s'", param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo per-session value: '%s'", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo per-session value: %s", param);
     }
   }
 
   param = (char *) c->argv[1];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_limit_type_attr_id,
       &radius_quota_limit_type);
 
@@ -1130,13 +1451,18 @@ static void radius_process_quota_info(config_rec *c) {
 
     if (strcasecmp(param, "hard") != 0 &&
         strcasecmp(param, "soft") != 0) {
-      radius_log("illegal RadiusQuotaInfo limit type value: '%s'", param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo limit type value: '%s'", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo limit type value: %s", param);
     }
   }
 
   param = (char *) c->argv[2];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_bytes_in_attr_id,
       &radius_quota_bytes_in);
 
@@ -1144,21 +1470,26 @@ static void radius_process_quota_info(config_rec *c) {
     char *endp = NULL;
 
     if (strtod(param, &endp) < 0) {
-      radius_log("illegal RadiusQuotaInfo bytes in value: negative number");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo bytes in value: negative number");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusQuotaInfo bytes in value: '%s' not a number",
-        param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo bytes in value: '%s' not a number", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo bytes in value: %s", param);
     }
 
     radius_quota_bytes_in = param;
   }
 
   param = (char *) c->argv[3];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_bytes_out_attr_id,
       &radius_quota_bytes_out);
 
@@ -1166,21 +1497,26 @@ static void radius_process_quota_info(config_rec *c) {
     char *endp = NULL;
 
     if (strtod(param, &endp) < 0) {
-      radius_log("illegal RadiusQuotaInfo bytes out value: negative number");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo bytes out value: negative number");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusQuotaInfo bytes out value: '%s' not a number",
-        param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo bytes out value: '%s' not a number", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo bytes out value: %s", param);
     }
 
     radius_quota_bytes_out = param;
   }
 
   param = (char *) c->argv[4];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_bytes_xfer_attr_id,
       &radius_quota_bytes_xfer);
 
@@ -1188,21 +1524,26 @@ static void radius_process_quota_info(config_rec *c) {
     char *endp = NULL;
 
     if (strtod(param, &endp) < 0) {
-      radius_log("illegal RadiusQuotaInfo bytes xfer value: negative number");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo bytes xfer value: negative number");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusQuotaInfo bytes xfer value: '%s' not a number",
-        param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo bytes xfer value: '%s' not a number", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo bytes xfer value: %s", param);
     }
 
     radius_quota_bytes_xfer = param;
   }
 
   param = (char *) c->argv[5];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_files_in_attr_id,
       &radius_quota_files_in);
 
@@ -1211,22 +1552,22 @@ static void radius_process_quota_info(config_rec *c) {
     unsigned long res;
 
     res = strtoul(param, &endp, 10);
-    if (res < 0) {
-      radius_log("illegal RadiusQuotaInfo files in value: negative number");
-      have_illegal_value = TRUE;
-    }
-
     if (endp && *endp) {
-      radius_log("illegal RadiusQuotaInfo files in value: '%s' not a number",
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo files in value: '%s' not a number",
         param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo files in value: %lu", res);
     }
 
     radius_quota_files_in = param;
   }
 
   param = (char *) c->argv[6];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_files_out_attr_id,
       &radius_quota_files_out);
 
@@ -1235,22 +1576,21 @@ static void radius_process_quota_info(config_rec *c) {
     unsigned long res;
 
     res = strtoul(param, &endp, 10);
-    if (res < 0) {
-      radius_log("illegal RadiusQuotaInfo files out value: negative number");
-      have_illegal_value = TRUE;
-    }
-    
     if (endp && *endp) {
-      radius_log("illegal RadiusQuotaInfo files out value: '%s' not a number",
-        param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo files out value: '%s' not a number", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo files out value: %lu", res);
     }
 
     radius_quota_files_out = param;
   }
 
   param = (char *) c->argv[7];
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_quota_files_xfer_attr_id,
       &radius_quota_files_xfer);
 
@@ -1259,15 +1599,14 @@ static void radius_process_quota_info(config_rec *c) {
     unsigned long res;
 
     res = strtoul(param, &endp, 10);
-    if (res < 0) {
-      radius_log("illegal RadiusQuotaInfo files xfer value: negative number");
-      have_illegal_value = TRUE;
-    }
-    
     if (endp && *endp) {
-      radius_log("illegal RadiusQuotaInfo files xfer value: '%s' not a number",
-        param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusQuotaInfo files xfer value: '%s' not a number", param);
       have_illegal_value = TRUE;
+
+    } else {
+      pr_trace_msg(trace_channel, 17,
+        "found RadiusQuotaInfo files xfer value: %lu", res);
     }
 
     radius_quota_files_xfer = param;
@@ -1277,7 +1616,8 @@ static void radius_process_quota_info(config_rec *c) {
     radius_have_quota_info = TRUE;
 
   } else {
-   radius_log("error with RadiusQuotaInfo parameters, ignoring them");
+   (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+     "error with RadiusQuotaInfo parameters, ignoring them");
   }
 }
 
@@ -1301,20 +1641,21 @@ static void radius_process_user_info(config_rec *c) {
   /* Process the UID string. */
   param = (char *) c->argv[0];
 
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     char *endp = NULL, *value = NULL;
 
     radius_parse_var(param, &radius_uid_attr_id, &value);
     radius_passwd.pw_uid = (uid_t) strtoul(value, &endp, 10);
 
     if (radius_passwd.pw_uid == (uid_t) -1) {
-      radius_log("illegal RadiusUserInfo default UID value: -1 not allowed");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo default UID value: -1 not allowed");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusUserInfo default UID value: '%s' not a number",
-        value);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo default UID value: '%s' not a number", value);
       have_illegal_value = TRUE;
     }
 
@@ -1324,12 +1665,14 @@ static void radius_process_user_info(config_rec *c) {
     radius_passwd.pw_uid = (uid_t) strtoul(param, &endp, 10);
 
     if (radius_passwd.pw_uid == (uid_t) -1) {
-      radius_log("illegal RadiusUserInfo UID value: -1 not allowed");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo UID value: -1 not allowed");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusUserInfo UID value: '%s' not a number", param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo UID value: '%s' not a number", param);
       have_illegal_value = TRUE;
     }
   }
@@ -1337,20 +1680,21 @@ static void radius_process_user_info(config_rec *c) {
   /* Process the GID string. */
   param = (char *) c->argv[1];
 
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     char *endp = NULL, *value = NULL;
 
     radius_parse_var(param, &radius_gid_attr_id, &value);
     radius_passwd.pw_gid = (gid_t) strtoul(value, &endp, 10);
 
     if (radius_passwd.pw_gid == (gid_t) -1) {
-      radius_log("illegal RadiusUserInfo default GID value: -1 not allowed");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo default GID value: -1 not allowed");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusUserInfo default GID value: '%s' not a number",
-        value);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo default GID value: '%s' not a number", value);
       have_illegal_value = TRUE;
     }
 
@@ -1360,12 +1704,14 @@ static void radius_process_user_info(config_rec *c) {
     radius_passwd.pw_gid = (gid_t) strtoul(param, &endp, 10);
 
     if (radius_passwd.pw_gid == (gid_t) -1) {
-      radius_log("illegal RadiusUserInfo GID value: -1 not allowed");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo GID value: -1 not allowed");
       have_illegal_value = TRUE;
     }
 
     if (endp && *endp) {
-      radius_log("illegal RadiusUserInfo GID value: '%s' not a number", param);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo GID value: '%s' not a number", param);
       have_illegal_value = TRUE;
     }
   }
@@ -1373,12 +1719,13 @@ static void radius_process_user_info(config_rec *c) {
   /* Parse the home directory string. */
   param = (char *) c->argv[2];
 
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_home_attr_id, &radius_passwd.pw_dir);
 
     if (*radius_passwd.pw_dir != '/') {
-      radius_log("illegal RadiusUserInfo default home value: '%s' "
-        "not an absolute path", radius_passwd.pw_dir);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo default home value: '%s' not an absolute path",
+        radius_passwd.pw_dir);
       have_illegal_value = TRUE;
     }
 
@@ -1391,12 +1738,13 @@ static void radius_process_user_info(config_rec *c) {
   /* Process the shell string. */
   param = (char *) c->argv[3];
   
-  if (RADIUS_IS_VAR(param)) {
+  if (RADIUS_IS_VAR(param) == TRUE) {
     radius_parse_var(param, &radius_shell_attr_id, &radius_passwd.pw_shell);
 
     if (*radius_passwd.pw_shell != '/') {
-      radius_log("illegal RadiusUserInfo default shell value: '%s' "
-        "not an absolute path", radius_passwd.pw_shell);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "illegal RadiusUserInfo default shell value: '%s' not an absolute path",
+        radius_passwd.pw_shell);
       have_illegal_value = TRUE;
     }
 
@@ -1410,7 +1758,8 @@ static void radius_process_user_info(config_rec *c) {
     radius_have_user_info = TRUE;
 
   } else {
-    radius_log("error with RadiusUserInfo parameters, ignoring them");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error with RadiusUserInfo parameters, ignoring them");
   }
 }
 
@@ -1454,17 +1803,21 @@ static void radius_reset(void) {
 
 static unsigned char *radius_xor(unsigned char *p, unsigned char *q,
     size_t len) {
-  register int i = 0;
+  register size_t i = 0;
   unsigned char *tmp = p;
 
-  for (i = 0; i < len; i++)
+  for (i = 0; i < len; i++) {
     *(p++) ^= *(q++);
+  }
 
   return tmp;
 }
 
 #if defined(PR_USE_OPENSSL)
+# include <openssl/err.h>
 # include <openssl/md5.h>
+# include <openssl/hmac.h>
+
 #else
 /* Built-in MD5 */
 
@@ -1779,73 +2132,45 @@ static void Decode(uint32_t *output, unsigned char *input, unsigned int len) {
 }
 
 #ifndef HAVE_MEMCPY
-/* Note: Replace "for loop" with standard memcpy if possible.
- */
+/* Note: Replace "for loop" with standard memcpy if possible. */
 static void MD5_memcpy(unsigned char *output, unsigned char *input,
     unsigned int len) {
   unsigned int i;
 
-  for (i = 0; i < len; i++)
+  for (i = 0; i < len; i++) {
     output[i] = input[i];
+  }
 }
 
-/* Note: Replace "for loop" with standard memset if possible.
- */
+/* Note: Replace "for loop" with standard memset if possible. */
 static void MD5_memset(unsigned char *output, int value, unsigned int len) {
   unsigned int i;
 
-  for (i = 0; i < len; i++)
-    ((char *)output)[i] = (char)value;
+  for (i = 0; i < len; i++) {
+    ((char *) output)[i] = (char) value;
+  }
 }
 #endif
 #endif /* !PR_USE_OPENSSL */
 
-/* Logging */
-
-static int radius_closelog(void) {
-
-  /* sanity check */
-  if (radius_logfd != -1) {
-    close(radius_logfd);
-    radius_logfd = -1;
-    radius_logname = NULL;
-  }
-
-  return 0;
-}
-
-static int radius_log(const char *fmt, ...) {
-  va_list msg;
-  int res;
-
-  /* sanity check */
-  if (!radius_logname)
-    return 0;
-
-  va_start(msg, fmt);
-  res = pr_log_vwritefile(radius_logfd, MOD_RADIUS_VERSION, fmt, msg);
-  va_end(msg);
-
-  return res;
-}
-
 static int radius_openlog(void) {
   int res = 0, xerrno = 0;
+  config_rec *c;
+  const char *path;
 
-  /* Sanity checks */
-  radius_logname = (char *) get_param_ptr(main_server->conf, "RadiusLog",
-    FALSE);
-  if (radius_logname == NULL)
+  c = find_config(main_server->conf, CONF_PARAM, "RadiusLog", FALSE);
+  if (c == NULL) {
     return 0;
+  }
 
-  if (strcasecmp(radius_logname, "none") == 0) {
-    radius_logname = NULL;
+  path = c->argv[0];
+  if (strcasecmp(path, "none") == 0) {
     return 0;
   }
 
   pr_signals_block();
   PRIVS_ROOT
-  res = pr_log_openfile(radius_logname, &radius_logfd, PR_LOG_SYSTEM_MODE);
+  res = pr_log_openfile(path, &radius_logfd, PR_LOG_SYSTEM_MODE);
   xerrno = errno;
   PRIVS_RELINQUISH
   pr_signals_unblock();
@@ -1856,9 +2181,9 @@ static int radius_openlog(void) {
 
 /* RADIUS routines */
 
-/* Add an attribute to a RADIUS packet. */
-static void radius_add_attrib(radius_packet_t *packet, unsigned char type,
-    const unsigned char *data, size_t datalen) {
+/* Add an attribute to a RADIUS packet.  Returns the added attribute. */
+static radius_attrib_t *radius_add_attrib(radius_packet_t *packet,
+    unsigned char type, const unsigned char *data, size_t datalen) {
   radius_attrib_t *attrib = NULL;
 
   attrib = (radius_attrib_t *) ((unsigned char *) packet +
@@ -1874,19 +2199,123 @@ static void radius_add_attrib(radius_packet_t *packet, unsigned char type,
   packet->length = htons(ntohs(packet->length) + attrib->length);
 
   memcpy(attrib->data, data, datalen);
-}
 
-/* Add a RADIUS password attribute to the packet. */
-static void radius_add_passwd(radius_packet_t *packet, unsigned char type,
-    const unsigned char *passwd, unsigned char *secret) {
+  return attrib;
+}
 
-  MD5_CTX ctx, secret_ctx;
+/* Add a RADIUS message authenticator attribute to the packet. */
+static void radius_set_auth_mac(radius_packet_t *pkt,
+   const unsigned char *secret, size_t secret_len) {
+#ifdef PR_USE_OPENSSL
+  const EVP_MD *md;
+  unsigned char digest[EVP_MAX_MD_SIZE];
+  unsigned int digest_len = 0, mac_len = 16;
+  radius_attrib_t *attrib = NULL;
+
+  /* First, add the Message-Authenticator attribute, with a value of all zeroes,
+   * per RFC 3579, Section 3.2.
+   */
+  memset(digest, '\0', sizeof(digest));
+  attrib = radius_add_attrib(pkt, RADIUS_MESSAGE_AUTHENTICATOR,
+    (const unsigned char *) digest, mac_len);
+
+  /* Now, calculate the HMAC-MD5 of the packet. */
+
+  md = EVP_md5();
+  if (HMAC(md, secret, secret_len, (unsigned char *) pkt, ntohs(pkt->length),
+      digest, &digest_len) == NULL) {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error generating Message-Authenticator: %s",
+      ERR_error_string(ERR_get_error(), NULL));
+
+  } else {
+    /* Finally, overwrite the all-zeroes Message-Authenticator value with our
+     * calculated value.
+     */
+    memcpy(attrib->data, digest, mac_len);
+  }
+#endif /* PR_USE_OPENSSL */
+}
+
+static int radius_verify_auth_mac(radius_packet_t *pkt, const char *pkt_type,
+    const unsigned char *secret, size_t secret_len) {
+  int res = 0;
+  radius_attrib_t *attrib = NULL;
+
+  /* Handle any Message-Authenticator attribute, per RFC 2869, Section 5.14. */
+  attrib = radius_get_attrib(pkt, RADIUS_MESSAGE_AUTHENTICATOR);
+  if (attrib != NULL) {
+    unsigned char attrib_len;
+    unsigned int expected_len = 16;
+
+    attrib_len = RADIUS_ATTRIB_LEN(attrib);
+    if (attrib_len != expected_len) {
+#ifdef PR_USE_OPENSSL
+      const EVP_MD *md;
+      unsigned char digest[EVP_MAX_MD_SIZE], replied[EVP_MAX_MD_SIZE];
+      unsigned int digest_len = 0;
+
+      /* First, make a copy of the packet's Message-Authenticator value, for
+       * comparison with what we will calculate.
+       */
+      memset(replied, '\0', sizeof(replied));
+      memcpy(replied, attrib->data, attrib_len);
+
+      /* Next, zero out the value so that we can calculate it ourselves. */
+      memset(attrib->data, '\0', attrib_len);
+
+      memset(digest, '\0', sizeof(digest));
+      md = EVP_md5();
+      if (HMAC(md, secret, secret_len, (unsigned char *) pkt,
+          ntohs(pkt->length), digest, &digest_len) == NULL) {
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "error generating Message-Authenticator: %s",
+          ERR_error_string(ERR_get_error(), NULL));
+        return 0;
+      }
+
+      if (memcmp(replied, digest, expected_len) != 0) {
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "packet Message-Authenticator verification failed: mismatched MACs");
+        errno = EINVAL;
+        return -1;
+      }
+
+      res = 0;
+
+#endif /* PR_USE_OPENSSL */
+    } else {
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "%s packet has incorrect Message-Authenticator attribute length "
+        "(%u != %u), rejecting", pkt_type, attrib_len, expected_len);
+      errno = EINVAL;
+      return -1;
+    }
+
+  } else {
+    pr_trace_msg(trace_channel, 6,
+      "%s packet lacks Message-Authenticator attribute (%d)", pkt_type,
+      RADIUS_MESSAGE_AUTHENTICATOR);
+
+    if (radius_opts & RADIUS_OPT_REQUIRE_MAC) {
+      errno = EPERM;
+      return -1;
+    }
+  }
+
+  return res;
+}
+
+/* Add a RADIUS password attribute to the packet. */
+static void radius_add_passwd(radius_packet_t *packet, unsigned char type,
+    const unsigned char *passwd, unsigned char *secret, size_t secret_len) {
+  MD5_CTX ctx, secret_ctx;
   radius_attrib_t *attrib = NULL;
   unsigned char calculated[RADIUS_VECTOR_LEN];
   unsigned char pwhash[PR_TUNABLE_BUFFER_SIZE];
   unsigned char *digest = NULL;
   register unsigned int i = 0;
-  size_t pwlen, secretlen;
+  size_t pwlen;
 
   pwlen = strlen((const char *) passwd);
 
@@ -1917,9 +2346,8 @@ static void radius_add_passwd(radius_packet_t *packet, unsigned char type,
   }
 
   /* Encrypt the password.  Password: c[0] = p[0] ^ MD5(secret + digest) */
-  secretlen = strlen((const char *) secret);
   MD5_Init(&secret_ctx);
-  MD5_Update(&secret_ctx, secret, secretlen);
+  MD5_Update(&secret_ctx, secret, secret_len);
 
   /* Save this hash for later. */
   ctx = secret_ctx;
@@ -1947,20 +2375,23 @@ static void radius_add_passwd(radius_packet_t *packet, unsigned char type,
     radius_xor(&pwhash[i * RADIUS_PASSWD_LEN], calculated, RADIUS_PASSWD_LEN);
   }
 
-  if (type == RADIUS_OLD_PASSWORD)
+  if (type == RADIUS_OLD_PASSWORD) {
     attrib = radius_get_attrib(packet, RADIUS_OLD_PASSWORD);
-  
-  if (!attrib)
+  }
+ 
+  if (attrib == NULL) {
     radius_add_attrib(packet, type, pwhash, pwlen);
 
-  else
-
+  } else {
     /* Overwrite the packet data. */
     memcpy(attrib->data, pwhash, pwlen);
+  }
+
+  pr_memscrub(pwhash, sizeof(pwhash));
 }
 
-static void radius_get_acct_digest(radius_packet_t *packet,
-    unsigned char *secret) {
+static void radius_set_acct_digest(radius_packet_t *packet,
+    const unsigned char *secret, size_t secret_len) {
   MD5_CTX ctx;
 
   /* Clear the current digest (not needed yet for accounting packets) */
@@ -1972,7 +2403,7 @@ static void radius_get_acct_digest(radius_packet_t *packet,
   MD5_Update(&ctx, (unsigned char *) packet, ntohs(packet->length));
 
   /* Add the secret to the mix. */
-  MD5_Update(&ctx, secret, strlen((const char *) secret));
+  MD5_Update(&ctx, secret, secret_len);
 
   /* Set the calculated digest in place in the packet. */
   MD5_Final(packet->digest, &ctx);
@@ -2006,19 +2437,38 @@ static void radius_get_rnd_digest(radius_packet_t *packet) {
 /* RADIUS packet manipulation functions.
  */
 
-/* Find an attribute in a RADIUS packet.  Note that the packet length
- * is always kept in network byte order.
+/* For iterating through all of the attributes in a packet, callers can
+ * provide a pointer to the previous attribute returned, or NULL.
  */
-static radius_attrib_t *radius_get_attrib(radius_packet_t *packet,
-    unsigned char type) {
-  radius_attrib_t *attrib = (radius_attrib_t *) &packet->data;
-  int len = ntohs(packet->length) - RADIUS_HEADER_LEN;
+static radius_attrib_t *radius_get_next_attrib(radius_packet_t *packet,
+    unsigned char attrib_type, unsigned int *packet_len,
+    radius_attrib_t *prev_attrib) {
+  radius_attrib_t *attrib = NULL;
+  unsigned int len;
 
-  while (attrib->type != type) {
+  if (packet_len == NULL) {
+    len = ntohs(packet->length) - RADIUS_HEADER_LEN;
+
+  } else {
+    len = *packet_len;
+  }
+
+  if (prev_attrib == NULL) {
+    attrib = (radius_attrib_t *) &packet->data;
+
+  } else {
+    attrib = prev_attrib;
+  }
+
+  while (attrib->type != attrib_type) {
     if (attrib->length == 0 ||
         (len -= attrib->length) <= 0) {
 
       /* Requested attribute not found. */
+      if (packet_len != NULL) {
+        *packet_len = 0;
+      }
+
       return NULL;
     }
 
@@ -2026,9 +2476,18 @@ static radius_attrib_t *radius_get_attrib(radius_packet_t *packet,
     attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
   }
 
+  if (packet_len != NULL) {
+    *packet_len = len;
+  }
+
   return attrib;
 }
 
+static radius_attrib_t *radius_get_attrib(radius_packet_t *packet,
+    unsigned char attrib_type) {
+  return radius_get_next_attrib(packet, attrib_type, NULL, NULL);
+}
+
 /* Find a Vendor-Specific Attribute (VSA) in a RADIUS packet.  Note that
  * the packet length is always kept in network byte order.
  */
@@ -2039,13 +2498,13 @@ static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *packet,
 
   while (attrib) {
     unsigned int vendor_id = 0;
-    radius_attrib_t *vsa = NULL;
 
     pr_signals_handle();
 
     if (attrib->length == 0) {
-      radius_log("packet includes invalid length (%u) for attribute type %u, "
-        " rejecting", attrib->length, attrib->type);
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet includes invalid length (%u) for attribute type %u, rejecting",
+        attrib->length, attrib->type);
       return NULL;
     }
 
@@ -2056,8 +2515,10 @@ static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *packet,
     }
 
     /* The first four octets (bytes) of data will contain the Vendor-Id. */
-    memcpy(&vendor_id, attrib->data, sizeof(unsigned int));
-    vendor_id = ntohl(vendor_id);
+    if (attrib->length >= 4) {
+      memcpy(&vendor_id, attrib->data, 4);
+      vendor_id = ntohl(vendor_id);
+    }
 
     if (vendor_id != radius_vendor_id) {
       len -= attrib->length;
@@ -2066,16 +2527,20 @@ static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *packet,
     }
 
     /* Parse the data value for this attribute into a VSA structure. */
-    vsa = (radius_attrib_t *) ((char *) attrib->data + sizeof(int));
+    if (attrib->length > 4) {
+      radius_attrib_t *vsa = NULL;
 
-    /* Does this VSA have the type requested? */
-    if (vsa->type != type) {
-      len -= attrib->length;
-      attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
-      continue;
-    }
+      vsa = (radius_attrib_t *) ((char *) attrib->data + sizeof(int));
+
+      /* Does this VSA have the type requested? */
+      if (vsa->type != type) {
+        len -= attrib->length;
+        attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
+        continue;
+      }
 
-    return vsa;
+      return vsa;
+    }
   }
 
   return NULL;
@@ -2086,7 +2551,7 @@ static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *packet,
  */
 static void radius_build_packet(radius_packet_t *packet,
     const unsigned char *user, const unsigned char *passwd,
-    unsigned char *secret) {
+    unsigned char *secret, size_t secret_len) {
   unsigned int nas_port_type = htonl(RADIUS_NAS_PORT_TYPE_VIRTUAL);
   int nas_port = htonl(main_server->ServerPort);
   char *caller_id = NULL;
@@ -2108,13 +2573,12 @@ static void radius_build_packet(radius_packet_t *packet,
 
   /* Add the password attribute, if given. */
   if (passwd) {
-    radius_add_passwd(packet, RADIUS_PASSWORD, passwd, secret);
+    radius_add_passwd(packet, RADIUS_PASSWORD, passwd, secret, secret_len);
 
   } else if (packet->code != RADIUS_ACCT_REQUEST) {
-
     /* Add a NULL password if necessary. */
     radius_add_passwd(packet, RADIUS_PASSWORD, (const unsigned char *) "",
-      secret);
+      secret, 1);
   }
 
   /* Add a NAS identifier attribute of the service name, e.g. 'ftp'. */
@@ -2130,22 +2594,69 @@ static void radius_build_packet(radius_packet_t *packet,
 
 #ifdef PR_USE_IPV6
   if (pr_netaddr_use_ipv6()) {
-    struct in6_addr *inaddr;
+    const pr_netaddr_t *local_addr;
+    int family;
 
-    inaddr = pr_netaddr_get_inaddr(pr_netaddr_get_sess_local_addr());
+    local_addr = pr_netaddr_get_sess_local_addr();
+    family = pr_netaddr_get_family(local_addr);
 
-    /* Ideally we would use the inaddr->s6_addr32 to get to the 128-bit
-     * IPv6 address.  But `s6_addr32' turns out to be a macro that is not
-     * available on all systems (FreeBSD, for example, does not provide this
-     * macro unless you're building its kernel).
-     *
-     * As a workaround, try using the (hopefully) more portable s6_addr
-     * macro.
-     */
+    switch (family) {
+      case AF_INET: {
+        struct in_addr *inaddr;
+
+        inaddr = pr_netaddr_get_inaddr(local_addr);
+
+        /* Add a NAS-IP-Address attribute. */
+        radius_add_attrib(packet, RADIUS_NAS_IP_ADDRESS,
+          (unsigned char *) &(inaddr->s_addr), sizeof(inaddr->s_addr));
+        break;
+      }
+
+      case AF_INET6: {
+        if (pr_netaddr_is_v4mappedv6(local_addr)) {
+          pr_netaddr_t *v4_addr;
+
+          /* Note: in the future, switch to using a per-packet pool. */
+          v4_addr = pr_netaddr_v6tov4(radius_pool, local_addr);
+          if (v4_addr != NULL) {
+            struct in_addr *inaddr;
+
+            inaddr = pr_netaddr_get_inaddr(v4_addr);
 
-    /* Add a NAS-IPv6-Address attribute. */
-    radius_add_attrib(packet, RADIUS_NAS_IPV6_ADDRESS,
-      (unsigned char *) inaddr->s6_addr, sizeof(inaddr->s6_addr));
+            /* Add a NAS-IP-Address attribute. */
+            radius_add_attrib(packet, RADIUS_NAS_IP_ADDRESS,
+              (unsigned char *) &(inaddr->s_addr), sizeof(inaddr->s_addr));
+
+          } else {
+            (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+              "error converting '%s' to IPv4 address: %s",
+              pr_netaddr_get_ipstr(local_addr), strerror(errno));
+          }
+
+        } else {
+          struct in6_addr *inaddr;
+          uint32_t ipv6_addr[4];
+
+          inaddr = pr_netaddr_get_inaddr(pr_netaddr_get_sess_local_addr());
+
+          /* Ideally we would use the inaddr->s6_addr32 to get to the 128-bit
+           * IPv6 address.  But `s6_addr32' turns out to be a macro that is not
+           * available on all systems (FreeBSD, for example, does not provide
+           * this macro unless you're building its kernel).
+           *
+           * As a workaround, try using the (hopefully) more portable s6_addr
+           * macro.
+           */
+          memcpy(ipv6_addr, inaddr->s6_addr, sizeof(ipv6_addr));
+
+          /* Add a NAS-IPv6-Address attribute. */
+          radius_add_attrib(packet, RADIUS_NAS_IPV6_ADDRESS,
+            (unsigned char *) ipv6_addr, sizeof(ipv6_addr));
+        }
+
+        break;
+      }
+    }
 
   } else {
 #else
@@ -2197,6 +2708,7 @@ static radius_server_t *radius_make_server(pool *parent_pool) {
   server->addr = NULL;
   server->port = RADIUS_AUTH_PORT;
   server->secret = NULL;
+  server->secret_len = 0;
   server->timeout = DEFAULT_RADIUS_TIMEOUT;
   server->next = NULL;
 
@@ -2209,9 +2721,15 @@ static int radius_open_socket(void) {
   unsigned short local_port = 0;
 
   /* Obtain a socket descriptor. */
-  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
-    radius_log("notice: unable to open socket for communication: %s",
-      strerror(errno));
+  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
+  if (sockfd < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "notice: unable to open socket for communication: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
@@ -2234,8 +2752,10 @@ static int radius_open_socket(void) {
     (local_port < USHRT_MAX));
 
   if (local_port >= USHRT_MAX) {
-    close(sockfd);
-    radius_log("notice: unable to bind to socket: no open local ports");
+    (void) close(sockfd);
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "notice: unable to bind to socket: no open local ports");
+    errno = EPERM;
     return -1;
   }
 
@@ -2243,10 +2763,6 @@ static int radius_open_socket(void) {
   return sockfd;
 }
 
-static int radius_close_socket(int sockfd) {
-  return close(sockfd);
-}
-
 static radius_packet_t *radius_recv_packet(int sockfd, unsigned int timeout) {
   static unsigned char recvbuf[RADIUS_PACKET_LEN];
   radius_packet_t *packet = NULL;
@@ -2265,20 +2781,30 @@ static radius_packet_t *radius_recv_packet(int sockfd, unsigned int timeout) {
   FD_SET(sockfd, &rset);
 
   res = select(sockfd + 1, &rset, NULL, NULL, &tv);
-
   if (res == 0) {
-    radius_log("server failed to respond in %u seconds", timeout);
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "server failed to respond in %u seconds", timeout);
     return NULL;
 
   } else if (res < 0) {
+    int xerrno = errno;
 
-    radius_log("error: unable to receive response: %s", strerror(errno));
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: unable to receive response: %s", strerror(xerrno));
+
+    errno = xerrno;
     return NULL;
   }
 
-  if ((recvlen = recvfrom(sockfd, (char *) recvbuf, RADIUS_PACKET_LEN,
-      0, &radius_remote_sock, &sockaddrlen)) < 0) {
-    radius_log("error reading packet: %s", strerror(errno));
+  recvlen = recvfrom(sockfd, (char *) recvbuf, RADIUS_PACKET_LEN, 0,
+    &radius_remote_sock, &sockaddrlen);
+  if (recvlen < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error reading packet: %s", strerror(xerrno));
+
+    errno = xerrno;
     return NULL;
   }
 
@@ -2287,7 +2813,8 @@ static radius_packet_t *radius_recv_packet(int sockfd, unsigned int timeout) {
   /* Make sure the packet is of valid length. */
   if (ntohs(packet->length) != recvlen ||
       ntohs(packet->length) > RADIUS_PACKET_LEN) {
-    radius_log("received corrupted packet");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "received corrupted packet");
     return NULL;
   }
 
@@ -2296,6 +2823,7 @@ static radius_packet_t *radius_recv_packet(int sockfd, unsigned int timeout) {
 
 static int radius_send_packet(int sockfd, radius_packet_t *packet,
     radius_server_t *server) {
+  int res;
   struct sockaddr_in *radius_sockaddr_in =
     (struct sockaddr_in *) &radius_remote_sock;
 
@@ -2305,53 +2833,72 @@ static int radius_send_packet(int sockfd, radius_packet_t *packet,
   radius_sockaddr_in->sin_addr.s_addr = pr_netaddr_get_addrno(server->addr);
   radius_sockaddr_in->sin_port = htons(server->port);
 
-  if (sendto(sockfd, (char *) packet, ntohs(packet->length), 0, 
-      &radius_remote_sock, sizeof(struct sockaddr_in)) < 0) {
-    radius_log("error: unable to send packet: %s", strerror(errno));
+  res = sendto(sockfd, (char *) packet, ntohs(packet->length), 0,
+    &radius_remote_sock, sizeof(struct sockaddr_in));
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: unable to send packet: %s", strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
-  radius_log("sending packet to %s:%u",
-    inet_ntoa(radius_sockaddr_in->sin_addr),
+  (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+    "sending packet to %s:%u", inet_ntoa(radius_sockaddr_in->sin_addr),
     ntohs(radius_sockaddr_in->sin_port));
 
   return 0;
 }
 
-
-static unsigned char radius_start_accting(void) {
-  int sockfd = -1, acct_status = 0, acct_authentic = 0;
+static int radius_start_accting(void) {
+  int sockfd = -1, acct_status = 0, acct_authentic = 0, now = 0, pid_len = 0;
   radius_packet_t *request = NULL, *response = NULL;
   radius_server_t *acct_server = NULL;
   unsigned char recvd_response = FALSE, *authenticated = NULL;
+  char pid_str[16];
 
   /* Check to see if RADIUS accounting should be done. */
-  if (!radius_engine || !radius_acct_server)
-    return TRUE;
+  if (radius_engine == FALSE ||
+      radius_acct_server == NULL) {
+    return 0;
+  }
 
   /* Only do accounting for authenticated users. */
   authenticated = get_param_ptr(main_server->conf, "authenticated", FALSE);
-  if (!authenticated || *authenticated == FALSE)
-    return TRUE;
-
-  /* Allocate a packet. */
-  request = (radius_packet_t *) pcalloc(radius_pool, sizeof(radius_packet_t));
+  if (authenticated == NULL ||
+      *authenticated == FALSE) {
+    return 0;
+  }
 
   /* Open a RADIUS socket */
   sockfd = radius_open_socket();
   if (sockfd < 0) {
-    radius_log("socket open failed");
-    return FALSE;
+    int xerrno = errno;
+
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "socket open failed: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
   }
 
+  /* Allocate a packet. */
+  request = (radius_packet_t *) pcalloc(radius_pool, sizeof(radius_packet_t));
+
+  now = htonl(time(NULL));
+
+  memset(pid_str, '\0', sizeof(pid_str));
+  pid_len = snprintf(pid_str, sizeof(pid_str), "%08u",
+    (unsigned int) session.pid);
+
   /* Loop through the list of servers, trying each one until the packet is
    * successfully sent.
    */
   acct_server = radius_acct_server;
 
   while (acct_server) {
-    char pid[10] = {'\0'};
-
     pr_signals_handle();
 
     /* Clear the packet. */
@@ -2363,7 +2910,8 @@ static unsigned char radius_start_accting(void) {
       radius_realm ?
         (const unsigned char *) pstrcat(radius_pool, session.user,
           radius_realm, NULL) :
-        (const unsigned char *) session.user, NULL, acct_server->secret);
+        (const unsigned char *) session.user, NULL, acct_server->secret,
+        acct_server->secret_len);
 
     radius_last_acct_pkt_id = request->id;
 
@@ -2372,104 +2920,192 @@ static unsigned char radius_start_accting(void) {
     radius_add_attrib(request, RADIUS_ACCT_STATUS_TYPE,
       (unsigned char *) &acct_status, sizeof(int));
 
-    snprintf(pid, sizeof(pid), "%08d", (int) getpid());
     radius_add_attrib(request, RADIUS_ACCT_SESSION_ID,
-      (const unsigned char *) pid, strlen(pid));
+      (const unsigned char *) pid_str, pid_len);
 
     acct_authentic = htonl(RADIUS_AUTH_LOCAL);
     radius_add_attrib(request, RADIUS_ACCT_AUTHENTIC,
       (unsigned char *) &acct_authentic, sizeof(int));
 
+    radius_add_attrib(request, RADIUS_ACCT_EVENT_TS, (unsigned char *) &now,
+      sizeof(int));
+
+    if (radius_acct_user != NULL) {
+      /* See RFC 2865, Section 5.1. */
+      radius_add_attrib(request, RADIUS_USER_NAME,
+        (const unsigned char *) radius_acct_user, radius_acct_userlen);
+    }
+
+    if (radius_acct_class != NULL) {
+      radius_add_attrib(request, RADIUS_CLASS,
+        (const unsigned char *) radius_acct_class, radius_acct_classlen);
+    }
+
     /* Calculate the signature. */
-    radius_get_acct_digest(request, acct_server->secret);
+    radius_set_acct_digest(request, acct_server->secret,
+      acct_server->secret_len);
 
     /* Send the request. */
-    radius_log("sending start acct request packet");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "sending start acct request packet");
     if (radius_send_packet(sockfd, request, acct_server) < 0) {
-      radius_log("packet send failed");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet send failed");
       acct_server = acct_server->next;
       continue;
     }
 
     /* Receive the response. */
-    radius_log("receiving acct response packet");
-    if ((response = radius_recv_packet(sockfd, acct_server->timeout)) == NULL) {
-      radius_log("packet receive failed");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "receiving acct response packet");
+    response = radius_recv_packet(sockfd, acct_server->timeout);
+    if (response == NULL) {
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet receive failed");
       acct_server = acct_server->next;
       continue;
     }
 
-    radius_log("packet receive succeeded");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "packet receive succeeded");
     recvd_response = TRUE;
     break;
   }
 
   /* Close the socket. */
-  if (radius_close_socket(sockfd) < 0) {
-    radius_log("socket close failed");
-  }
+  (void) close(sockfd);
 
   if (recvd_response) {
 
     /* Verify the response. */
-    radius_log("verifying packet");
-    if (radius_verify_packet(request, response, acct_server->secret) < 0)
-      return FALSE;
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "verifying packet");
+    if (radius_verify_packet(request, response, acct_server->secret,
+        acct_server->secret_len) < 0) {
+      return -1;
+    }
 
     /* Handle the response. */
     switch (response->code) {
       case RADIUS_ACCT_RESPONSE:
-        radius_log("accounting started for user '%s'", session.user);
-        return TRUE;
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "accounting started for user '%s'", session.user);
+        return 0;
 
       default:
-        radius_log("notice: server returned unknown response code: %02x",
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "notice: server returned unknown response code: %02x",
           response->code);
-        return FALSE;
+        return -1;
     }
 
-  } else
-    radius_log("error: no acct servers responded");
+  } else {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: no acct servers responded");
+  }
 
   /* Default return value. */
-  return FALSE;
+  return -1;
 }
 
-static unsigned char radius_stop_accting(void) {
-  int sockfd = -1, acct_status = 0, acct_authentic = 0, now = 0;
+/* Maps the ProFTPD disconnect reason code to the RADIUS Acct-Terminate-Cause
+ * attribute values.
+ */
+static unsigned int radius_get_terminate_cause(void) {
+  unsigned int cause = RADIUS_ACCT_TERMINATE_CAUSE_SERVICE_UNAVAIL;
+
+  switch (session.disconnect_reason) {
+    case PR_SESS_DISCONNECT_CLIENT_QUIT:
+      cause = RADIUS_ACCT_TERMINATE_CAUSE_USER_REQUEST;
+      break;
+
+    case PR_SESS_DISCONNECT_CLIENT_EOF:
+      cause = RADIUS_ACCT_TERMINATE_CAUSE_LOST_SERVICE;
+      break;
+
+    case PR_SESS_DISCONNECT_SIGNAL:
+      cause = RADIUS_ACCT_TERMINATE_CAUSE_ADMIN_RESET;
+      break;
+
+    case PR_SESS_DISCONNECT_SERVER_SHUTDOWN:
+      cause = RADIUS_ACCT_TERMINATE_CAUSE_ADMIN_REBOOT;
+      break;
+
+    case PR_SESS_DISCONNECT_TIMEOUT: {
+      const char *details = NULL;
+
+      pr_session_get_disconnect_reason(&details);
+      if (details != NULL) {
+        if (strcasecmp(details, "TimeoutIdle") == 0) {
+          cause = RADIUS_ACCT_TERMINATE_CAUSE_IDLE_TIMEOUT;
+
+        } else if (strcasecmp(details, "TimeoutSession") == 0) {
+          cause = RADIUS_ACCT_TERMINATE_CAUSE_SESSION_TIMEOUT;
+        }
+      }
+
+      break;
+    }
+  }
+
+  return cause;
+}
+
+static int radius_stop_accting(void) {
+  int sockfd = -1, acct_status = 0, acct_authentic = 0, event_ts = 0,
+    now = 0, pid_len = 0, session_duration = 0;
+  unsigned int terminate_cause = 0;
   radius_packet_t *request = NULL, *response = NULL;
   radius_server_t *acct_server = NULL;
   unsigned char recvd_response = FALSE, *authenticated = NULL;
   off_t radius_session_bytes_in = 0;
   off_t radius_session_bytes_out = 0;
+  char pid_str[16];
 
   /* Check to see if RADIUS accounting should be done. */
-  if (!radius_engine || !radius_acct_server)
-    return TRUE;
+  if (radius_engine == FALSE ||
+      radius_acct_server == NULL) {
+    return 0;
+  }
 
   /* Only do accounting for authenticated users. */
   authenticated = get_param_ptr(main_server->conf, "authenticated", FALSE);
-
-  if (!authenticated || *authenticated == FALSE)
-    return TRUE;
-
-  /* Allocate a packet. */
-  request = (radius_packet_t *) pcalloc(radius_pool, sizeof(radius_packet_t));
+  if (authenticated == NULL ||
+      *authenticated == FALSE) {
+    return 0;
+  }
 
   /* Open a RADIUS socket */
   sockfd = radius_open_socket();
   if (sockfd < 0) {
-    radius_log("socket open failed");
-    return FALSE;
+    int xerrno = errno;
+
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "socket open failed: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
   }
 
+  /* Allocate a packet. */
+  request = (radius_packet_t *) pcalloc(radius_pool, sizeof(radius_packet_t));
+
+  now = time(NULL);
+  event_ts = htonl(now);
+  session_duration = htonl(now - radius_session_start);
+  terminate_cause = htonl(radius_get_terminate_cause());
+
+  memset(pid_str, '\0', sizeof(pid_str));
+  pid_len = snprintf(pid_str, sizeof(pid_str)-1, "%08u",
+    (unsigned int) session.pid);
+
   /* Loop through the list of servers, trying each one until the packet is
    * successfully sent.
    */
   acct_server = radius_acct_server;
 
   while (acct_server) {
-    char pid[10] = {'\0'};
+    const char *ip_str;
 
     pr_signals_handle();
 
@@ -2482,30 +3118,31 @@ static unsigned char radius_stop_accting(void) {
       radius_realm ?
         (const unsigned char *) pstrcat(radius_pool, session.user,
           radius_realm, NULL) :
-        (const unsigned char *) session.user, NULL, acct_server->secret);
+        (const unsigned char *) session.user, NULL, acct_server->secret,
+        acct_server->secret_len);
 
     /* Use the ID of the last accounting packet sent, plus one.  Be sure
      * to handle the datatype overflow case.
      */
-    if ((request->id = radius_last_acct_pkt_id + 1) == 0)
+    request->id = radius_last_acct_pkt_id + 1;
+    if (request->id == 0) {
       request->id = 1;
+    }
 
     /* Add accounting attributes. */
     acct_status = htonl(RADIUS_ACCT_STATUS_STOP);
     radius_add_attrib(request, RADIUS_ACCT_STATUS_TYPE,
       (unsigned char *) &acct_status, sizeof(int));
-
-    snprintf(pid, sizeof(pid), "%08d", (int) getpid());
+ 
     radius_add_attrib(request, RADIUS_ACCT_SESSION_ID,
-      (const unsigned char *) pid, strlen(pid));
+      (const unsigned char *) pid_str, pid_len);
 
     acct_authentic = htonl(RADIUS_AUTH_LOCAL);
     radius_add_attrib(request, RADIUS_ACCT_AUTHENTIC,
       (unsigned char *) &acct_authentic, sizeof(int));
 
-    now = htonl(time(NULL) - radius_session_start);
     radius_add_attrib(request, RADIUS_ACCT_SESSION_TIME,
-      (unsigned char *) &now, sizeof(int));
+      (unsigned char *) &session_duration, sizeof(int));
 
     radius_session_bytes_in = htonl(session.total_bytes_in);
     radius_add_attrib(request, RADIUS_ACCT_INPUT_OCTETS,
@@ -2515,69 +3152,103 @@ static unsigned char radius_stop_accting(void) {
     radius_add_attrib(request, RADIUS_ACCT_OUTPUT_OCTETS,
       (unsigned char *) &radius_session_bytes_out, sizeof(int));
 
+    radius_add_attrib(request, RADIUS_ACCT_TERMINATE_CAUSE,
+      (unsigned char *) &terminate_cause, sizeof(int));
+
+    radius_add_attrib(request, RADIUS_ACCT_EVENT_TS,
+      (unsigned char *) &event_ts, sizeof(int));
+
+    if (radius_acct_user != NULL) {
+      /* See RFC 2865, Section 5.1. */
+      radius_add_attrib(request, RADIUS_USER_NAME,
+        (const unsigned char *) radius_acct_user, radius_acct_userlen);
+    }
+
+    if (radius_acct_class != NULL) {
+      radius_add_attrib(request, RADIUS_CLASS,
+        (const unsigned char *) radius_acct_class, radius_acct_classlen);
+    }
+
     /* Calculate the signature. */
-    radius_get_acct_digest(request, acct_server->secret);
+    radius_set_acct_digest(request, acct_server->secret,
+      acct_server->secret_len);
 
     /* Send the request. */
-    radius_log("sending stop acct request packet");
+    ip_str = pr_netaddr_get_ipstr(acct_server->addr);
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "sending stop acct request packet to %s#%u", ip_str, acct_server->port);
     if (radius_send_packet(sockfd, request, acct_server) < 0) {
-      radius_log("packet send failed");
-      return FALSE;
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet send failed to %s#%u", ip_str, acct_server->port);
+      acct_server = acct_server->next;
+      continue;
     }
 
     /* Receive the response. */
-    radius_log("receiving acct response packet");
-    if ((response = radius_recv_packet(sockfd, acct_server->timeout)) == NULL) {
-      radius_log("packet receive failed");
-      return FALSE;
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "receiving acct response packet");
+    response = radius_recv_packet(sockfd, acct_server->timeout);
+    if (response == NULL) {
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet receive failed from %s#%u", ip_str, acct_server->port);
+      acct_server = acct_server->next;
+      continue;
     }
 
-    radius_log("packet receive succeeded");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "packet receive succeeded succeeded from %s#%u", ip_str,
+      acct_server->port);
     recvd_response = TRUE;
     break;
   }
 
   /* Close the socket. */
-  if (radius_close_socket(sockfd) < 0) {
-    radius_log("socket close failed");
-  }
+  (void) close(sockfd);
 
   if (recvd_response) {
 
     /* Verify the response. */
-    radius_log("verifying packet");
-    if (radius_verify_packet(request, response, acct_server->secret) < 0)
-      return FALSE;
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "verifying packet");
+    if (radius_verify_packet(request, response, acct_server->secret,
+        acct_server->secret_len) < 0) {
+      return -1;
+    }
 
     /* Handle the response. */
     switch (response->code) {
       case RADIUS_ACCT_RESPONSE:
-        radius_log("accounting ended for user '%s'", session.user);
-        return TRUE;
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "accounting ended for user '%s'", session.user);
+        return 0;
 
       default:
-        radius_log("notice: server returned unknown response code: %02x",
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "notice: server returned unknown response code: %02x",
           response->code);
-        return FALSE;
+        return -1;
     }
 
   } else {
-    radius_log("error: no acct servers responded");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: no accounting servers responded");
   }
 
   /* Default return value. */
-  return FALSE;
+  return -1;
 }
 
 /* Verify the response packet from the server. */
 static int radius_verify_packet(radius_packet_t *req_packet, 
-    radius_packet_t *resp_packet, unsigned char *secret) {
+    radius_packet_t *resp_packet, const unsigned char *secret,
+    size_t secret_len) {
   MD5_CTX ctx;
-  unsigned char calculated[RADIUS_VECTOR_LEN] = {'\0'};
-  unsigned char replied[RADIUS_VECTOR_LEN] = {'\0'};
+  unsigned char calculated[RADIUS_VECTOR_LEN], replied[RADIUS_VECTOR_LEN];
 
   /* sanity check */
-  if (!req_packet || !resp_packet || !secret) {
+  if (req_packet == NULL ||
+      resp_packet == NULL ||
+      secret == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -2588,7 +3259,8 @@ static int radius_verify_packet(radius_packet_t *req_packet,
 
   /* Check that the packet IDs match. */
   if (resp_packet->id != req_packet->id) {
-    radius_log("packet verification failed: response packet ID %d does not "
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "packet verification failed: response packet ID %d does not "
       "match the request packet ID %d", resp_packet->id, req_packet->id);
     return -1;
   }
@@ -2610,15 +3282,18 @@ static int radius_verify_packet(radius_packet_t *req_packet,
   MD5_Init(&ctx);
   MD5_Update(&ctx, (unsigned char *) resp_packet, ntohs(resp_packet->length));
 
-  if (*secret)
-    MD5_Update(&ctx, secret, strlen((const char *) secret));
+  if (*secret) {
+    MD5_Update(&ctx, secret, secret_len);
+  }
 
   /* Set the calculated digest. */
   MD5_Final(calculated, &ctx);
 
   /* Do the digests match properly? */
   if (memcmp(calculated, replied, RADIUS_VECTOR_LEN) != 0) {
-    radius_log("packet verification failed: mismatched digests");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "packet verification failed: mismatched digests");
+    errno = EINVAL;
     return -1;
   }
 
@@ -2636,10 +3311,10 @@ MODRET radius_auth(cmd_rec *cmd) {
   if (radius_auth_ok) {
     session.auth_mech = "mod_radius.c";
     return PR_HANDLED(cmd);
-  }
 
-  else if (radius_auth_reject)
+  } else if (radius_auth_reject) {
     return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+  }
 
   /* Default return value. */
   return PR_DECLINED(cmd);
@@ -2699,7 +3374,7 @@ MODRET radius_getgroups(cmd_rec *cmd) {
       gids = (array_header *) cmd->argv[1];
 
       if (radius_have_user_info) {
-         *((gid_t *) push_array(gids)) = radius_passwd.pw_gid;
+        *((gid_t *) push_array(gids)) = radius_passwd.pw_gid;
       }
 
       for (i = 0; i < radius_addl_group_count; i++) {
@@ -2824,39 +3499,49 @@ MODRET radius_quota_lookup(cmd_rec *cmd) {
  * username as supplied by the client.
  */
 MODRET radius_pre_pass(cmd_rec *cmd) {
-  int sockfd = -1;
+  int pid_len = 0, sockfd = -1;
   radius_packet_t *request = NULL, *response = NULL;
   radius_server_t *auth_server = NULL;
   unsigned char recvd_response = FALSE;
   unsigned int service;
-  char *user;
+  const char *user;
+  char pid_str[16];
 
   /* Check to see whether RADIUS authentication should even be done. */
-  if (!radius_engine ||
-      !radius_auth_server)
+  if (radius_engine == FALSE ||
+      radius_auth_server == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-  if (!user) {
-    radius_log("missing prerequisite USER command, declining to handle PASS");
+  if (user == NULL) {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "missing prerequisite USER command, declining to handle PASS");
     pr_response_add_err(R_503, _("Login with USER first"));
     return PR_ERROR(cmd);
   }
 
-  /* Allocate a packet. */
-  request = (radius_packet_t *) pcalloc(cmd->tmp_pool,
-    sizeof(radius_packet_t));
-
   /* Open a RADIUS socket */
   sockfd = radius_open_socket();
   if (sockfd < 0) {
-    radius_log("socket open failed");
+    int xerrno = errno;
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "socket open failed: %s", strerror(xerrno));
+    errno = xerrno;
     return PR_DECLINED(cmd);
   }
 
+  /* Allocate a packet. */
+  request = (radius_packet_t *) pcalloc(cmd->tmp_pool,
+    sizeof(radius_packet_t));
+
   /* Clear the OK flag. */
   radius_auth_ok = FALSE;
 
+  memset(pid_str, '\0', sizeof(pid_str));
+  pid_len = snprintf(pid_str, sizeof(pid_str)-1, "%08u",
+    (unsigned int) session.pid);
+
   /* If mod_radius expects to find VSAs in the returned packet, it needs
    * to send a service type of Login, otherwise, use the Authenticate-Only
    * service type.
@@ -2875,8 +3560,9 @@ MODRET radius_pre_pass(cmd_rec *cmd) {
    * successfully sent.
    */
   auth_server = radius_auth_server;
+  while (auth_server != NULL) {
+    const char *ip_str;
 
-  while (auth_server) {
     pr_signals_handle();
 
     /* Clear the packet. */
@@ -2887,79 +3573,131 @@ MODRET radius_pre_pass(cmd_rec *cmd) {
     radius_build_packet(request, radius_realm ?
       (const unsigned char *) pstrcat(radius_pool, user, radius_realm, NULL) :
       (const unsigned char *) user, (const unsigned char *) cmd->arg,
-      auth_server->secret);
+      auth_server->secret, auth_server->secret_len);
 
     radius_add_attrib(request, RADIUS_SERVICE_TYPE, (unsigned char *) &service,
       sizeof(service));
 
+    radius_add_attrib(request, RADIUS_ACCT_SESSION_ID,
+      (const unsigned char *) pid_str, pid_len);
+
+    /* Calculate the signature. */
+    radius_set_auth_mac(request, auth_server->secret, auth_server->secret_len);
+
     /* Send the request. */
-    radius_log("sending auth request packet");
+    ip_str = pr_netaddr_get_ipstr(auth_server->addr);
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "sending auth request packet to %s#%d", ip_str, auth_server->port);
     if (radius_send_packet(sockfd, request, auth_server) < 0) {
-      radius_log("packet send failed");
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet send failed to %s#%d", ip_str, auth_server->port);
       auth_server = auth_server->next;
       continue;
     }
 
     /* Receive the response. */
-    radius_log("receiving auth response packet");
-    if ((response = radius_recv_packet(sockfd, auth_server->timeout)) == NULL) {
-      radius_log("packet receive failed");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "receiving auth response packet from %s#%d", ip_str, auth_server->port);
+    response = radius_recv_packet(sockfd, auth_server->timeout);
+    if (response == NULL) {
+      (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+        "packet receive failed from %s#%d", ip_str, auth_server->port);
       auth_server = auth_server->next;
       continue;
     }
 
-    radius_log("packet receive succeeded");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "packet receive succeeded from %s#%d", ip_str, auth_server->port);
     recvd_response = TRUE;
     break;
   }
 
   /* Close the socket. */
-  if (radius_close_socket(sockfd) < 0) {
-    radius_log("socket close failed");
-  }
+  (void) close(sockfd);
 
   if (recvd_response) {
+    int res;
 
     /* Verify the response. */
-    radius_log("verifying packet");
-    if (radius_verify_packet(request, response, auth_server->secret) < 0)
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "verifying packet");
+    res = radius_verify_packet(request, response, auth_server->secret,
+        auth_server->secret_len);
+    if (res < 0) {
       return PR_DECLINED(cmd);
+    }
 
     /* Handle the response */
     switch (response->code) {
       case RADIUS_AUTH_ACCEPT:
-        radius_log("authentication successful for user '%s'", user);
-
-        radius_session_authtype = htonl(RADIUS_AUTH_RADIUS);
-
         /* Process the packet for custom attributes */
-        radius_process_accpt_packet(response);
+        res = radius_process_accept_packet(response, auth_server->secret,
+          auth_server->secret_len);
+        if (res < 0) {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "DISCARDING Access-Accept packet for user '%s' due to failed "
+            "Message-Authenticator check; is the shared secret correct?",
+            user);
+          pr_log_pri(PR_LOG_NOTICE, MOD_RADIUS_VERSION
+            ": DISCARDING Access-Accept packet for user '%s' due to failed "
+            "Message-Authenticator check; is the shared secret correct?", user);
 
-        radius_auth_ok = TRUE;
+        } else {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "authentication successful for user '%s'", user);
+          pr_trace_msg(trace_channel, 9,
+            "processed %d %s in Access-Accept packet", res,
+            res != 1 ? "attributes" : "attribute");
+
+          radius_auth_ok = TRUE;
+          radius_session_authtype = htonl(RADIUS_AUTH_RADIUS);
+        }
         break;
 
       case RADIUS_AUTH_REJECT:
-        radius_log("authentication failed for user '%s'", user);
-        radius_auth_ok = FALSE;
-        radius_auth_reject = TRUE;
-        radius_reset();
+        /* Process the packet for custom attributes */
+        res = radius_process_reject_packet(response, auth_server->secret,
+          auth_server->secret_len);
+        if (res < 0) {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "DISCARDING Access-Reject packet for user '%s' due to failed "
+            "Message-Authenticator check; is the shared secret correct?",
+            user);
+          pr_log_pri(PR_LOG_NOTICE, MOD_RADIUS_VERSION
+            ": DISCARDING Access-Reject packet for user '%s' due to failed "
+            "Message-Authenticator check; is the shared secret correct?", user);
+
+        } else {
+          (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+            "authentication failed for user '%s'", user);
+          pr_trace_msg(trace_channel, 9,
+            "processed %d %s in Access-Reject packet", res,
+            res != 1 ? "attributes" : "attribute");
+
+          radius_auth_ok = FALSE;
+          radius_auth_reject = TRUE;
+          radius_reset();
+        }
         break;
 
       case RADIUS_AUTH_CHALLENGE:
         /* Just log this case for now. */
-        radius_log("authentication challenged for user '%s'", user);
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "authentication challenged for user '%s'", user);
         radius_reset();
         break;
 
       default:
-        radius_log("notice: server returned unknown response code: %02x",
+        (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+          "notice: server returned unknown response code: %02x",
           response->code);
         radius_reset();
         break;
     }
 
   } else {
-    radius_log("error: no auth servers responded");
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: no auth servers responded");
   }
 
   return PR_DECLINED(cmd);
@@ -2968,16 +3706,18 @@ MODRET radius_pre_pass(cmd_rec *cmd) {
 MODRET radius_post_pass(cmd_rec *cmd) {
 
   /* Check to see if RADIUS accounting should be done. */
-  if (!radius_engine || !radius_acct_server)
+  if (!radius_engine || !radius_acct_server) {
     return PR_DECLINED(cmd);
+  }
 
   /* Fill in the username in the faked user info, if need be. */
   if (radius_have_user_info) {
-    radius_passwd.pw_name = session.user;
+    radius_passwd.pw_name = (char *) session.user;
   }
 
-  if (!radius_start_accting()) {
-    radius_log("error: unable to start accounting");
+  if (radius_start_accting() < 0) {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: unable to start accounting: %s", strerror(errno));
   }
 
   return PR_DECLINED(cmd);
@@ -2998,8 +3738,10 @@ MODRET set_radiusacctserver(cmd_rec *cmd) {
   unsigned short server_port = 0;
   char *port = NULL;
 
-  if (cmd->argc-1 < 2 || cmd->argc-1 > 3)
+  if (cmd->argc-1 < 2 ||
+      cmd->argc-1 > 3) {
     CONF_ERROR(cmd, "missing parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -3030,11 +3772,17 @@ MODRET set_radiusacctserver(cmd_rec *cmd) {
   radius_server->port = (server_port ? server_port : RADIUS_ACCT_PORT);
   radius_server->secret = (unsigned char *) pstrdup(radius_server->pool,
     cmd->argv[2]);
+  radius_server->secret_len = strlen((char *) radius_server->secret);
 
   if (cmd->argc-1 == 3) {
-    if ((radius_server->timeout = atoi(cmd->argv[3])) < 0) {
-      CONF_ERROR(cmd, "timeout must be greater than or equal to zero");
+    int timeout = -1;
+
+    if (pr_str_get_duration(cmd->argv[3], &timeout) < 0) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing timeout value '",
+        cmd->argv[1], "': ", strerror(errno), NULL));
     }
+
+    radius_server->timeout = timeout;
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
@@ -3051,8 +3799,10 @@ MODRET set_radiusauthserver(cmd_rec *cmd) {
   unsigned short server_port = 0;
   char *port = NULL;
 
-  if (cmd->argc-1 < 2 || cmd->argc-1 > 3)
+  if (cmd->argc-1 < 2 ||
+      cmd->argc-1 > 3) {
     CONF_ERROR(cmd, "missing parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -3062,9 +3812,11 @@ MODRET set_radiusauthserver(cmd_rec *cmd) {
     /* Separate the server name from the port */
     *(port++) = '\0';
 
-    if ((server_port = (unsigned short) atoi(port)) < 1024)
+    server_port = (unsigned short) atoi(port);
+    if (server_port < 1024) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "port number must be greater "
         "than 1023", NULL));
+    }
   }
 
   if (pr_netaddr_get_addr(cmd->tmp_pool, cmd->argv[1], NULL) == NULL) {
@@ -3080,11 +3832,17 @@ MODRET set_radiusauthserver(cmd_rec *cmd) {
   radius_server->port = (server_port ? server_port : RADIUS_AUTH_PORT);
   radius_server->secret = (unsigned char *) pstrdup(radius_server->pool,
     cmd->argv[2]);
+  radius_server->secret_len = strlen((char *) radius_server->secret);
 
   if (cmd->argc-1 == 3) {
-    if ((radius_server->timeout = atoi(cmd->argv[3])) < 0) {
-      CONF_ERROR(cmd, "timeout must be greater than or equal to zero");
-    }
+    int timeout = -1;
+
+    if (pr_str_get_duration(cmd->argv[3], &timeout) < 0) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing timeout value '",
+        cmd->argv[1], "': ", strerror(errno), NULL));
+    } 
+    
+    radius_server->timeout = timeout;
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
@@ -3096,19 +3854,20 @@ MODRET set_radiusauthserver(cmd_rec *cmd) {
 
 /* usage: RadiusEngine on|off */
 MODRET set_radiusengine(cmd_rec *cmd) {
-  int bool = -1;
+  int engine = -1;
   config_rec *c = NULL;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  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(int));
-  *((int *) c->argv[0]) = bool;
+  *((int *) c->argv[0]) = engine;
 
   return PR_HANDLED(cmd);
 }
@@ -3209,6 +3968,47 @@ MODRET set_radiusnasidentifier(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: RadiusOptions opt1 ... */
+MODRET set_radiusoptions(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0)
+    CONF_ERROR(cmd, "wrong number of parameters");
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "IgnoreReplyMessage") == 0) {
+      opts |= RADIUS_OPT_IGNORE_REPLY_MESSAGE_ATTR;
+
+    } else if (strcmp(cmd->argv[i], "IgnoreClass") == 0) {
+      opts |= RADIUS_OPT_IGNORE_CLASS_ATTR;
+
+    } else if (strcmp(cmd->argv[i], "IgnoreIdleTimeout") == 0) {
+      opts |= RADIUS_OPT_IGNORE_IDLE_TIMEOUT_ATTR;
+
+    } else if (strcmp(cmd->argv[i], "IgnoreSessionTimeout") == 0) {
+      opts |= RADIUS_OPT_IGNORE_SESSION_TIMEOUT_ATTR;
+
+    } else if (strcmp(cmd->argv[i], "RequireMAC") == 0) {
+      opts |= RADIUS_OPT_REQUIRE_MAC;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown TLSOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
 /* usage: RadiusQuotaInfo per-sess limit-type bytes-in bytes-out bytes-xfer
  *          files-in files-out files-xfer
  */
@@ -3224,80 +4024,78 @@ MODRET set_radiusquotainfo(cmd_rec *cmd) {
 
   if (!radius_have_var(cmd->argv[2])) {
     if (strcasecmp(cmd->argv[2], "hard") != 0 &&
-        strcasecmp(cmd->argv[2], "soft") != 0)
+        strcasecmp(cmd->argv[2], "soft") != 0) {
       CONF_ERROR(cmd, "invalid limit type value");
+    }
   }
 
   if (!radius_have_var(cmd->argv[3])) {
     char *endp = NULL;
 
     /* Make sure it's a number, at least. */
-    if (strtod(cmd->argv[3], &endp) < 0)
+    if (strtod(cmd->argv[3], &endp) < 0) {
       CONF_ERROR(cmd, "negative bytes value not allowed");
+    }
 
-    if (endp && *endp)
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid bytes parameter: not a number");
+    }
   }
 
   if (!radius_have_var(cmd->argv[4])) {
     char *endp = NULL;
 
     /* Make sure it's a number, at least. */
-    if (strtod(cmd->argv[4], &endp) < 0)
+    if (strtod(cmd->argv[4], &endp) < 0) {
       CONF_ERROR(cmd, "negative bytes value not allowed");
+    }
 
-    if (endp && *endp)
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid bytes parameter: not a number");
+    }
   }
 
   if (!radius_have_var(cmd->argv[5])) {
     char *endp = NULL;
 
     /* Make sure it's a number, at least. */
-    if (strtod(cmd->argv[5], &endp) < 0)
+    if (strtod(cmd->argv[5], &endp) < 0) {
       CONF_ERROR(cmd, "negative bytes value not allowed");
+    }
 
-    if (endp && *endp)
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid bytes parameter: not a number");
+    }
   }
 
   if (!radius_have_var(cmd->argv[6])) {
     char *endp = NULL;
-    unsigned long res;
 
     /* Make sure it's a number, at least. */
-    res = strtoul(cmd->argv[6], &endp, 10);
-    if (res < 0)
-      CONF_ERROR(cmd, "negative files value not allowed");
-
-    if (endp && *endp)
+    (void) strtoul(cmd->argv[6], &endp, 10);
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid files parameter: not a number");
+    }
   }
 
   if (!radius_have_var(cmd->argv[7])) {
     char *endp = NULL;
-    unsigned long res;
 
     /* Make sure it's a number, at least. */
-    res = strtoul(cmd->argv[7], &endp, 10);
-    if (res < 0)
-      CONF_ERROR(cmd, "negative files value not allowed");
-
-    if (endp && *endp)
+    (void) strtoul(cmd->argv[7], &endp, 10);
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid files parameter: not a number");
+    }
   }
 
   if (!radius_have_var(cmd->argv[8])) {
     char *endp = NULL;
-    unsigned long res;
 
     /* Make sure it's a number, at least. */
-    res = strtoul(cmd->argv[8], &endp, 10);
-    if (res < 0)
-      CONF_ERROR(cmd, "negative files value not allowed");
-
-    if (endp && *endp)
+    (void) strtoul(cmd->argv[8], &endp, 10);
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid files parameter: not a number");
+    }
   }
 
   add_config_param_str(cmd->argv[0], 8, cmd->argv[1], cmd->argv[2],
@@ -3323,47 +4121,45 @@ MODRET set_radiususerinfo(cmd_rec *cmd) {
 
   if (!radius_have_var(cmd->argv[1])) {
     char *endp = NULL;
-    unsigned long res;
 
     /* Make sure it's a number, at least. */
-    res = strtoul(cmd->argv[1], &endp, 10);
-    if (res < 0)
-      CONF_ERROR(cmd, "negative UID not allowed");
-
-    if (endp && *endp)
+    (void) strtoul(cmd->argv[1], &endp, 10);
+    if (endp && *endp) {
       CONF_ERROR(cmd, "invalid UID parameter: not a number");
+    }
   }
 
   if (!radius_have_var(cmd->argv[2])) {
     char *endp = NULL;
-    unsigned long res;
 
     /* Make sure it's a number, at least. */
-    res = strtoul(cmd->argv[2], &endp, 10);
-    if (res < 0)
-      CONF_ERROR(cmd, "negative GID not allowed");
-
+    (void) strtoul(cmd->argv[2], &endp, 10);
     if (endp && *endp)
       CONF_ERROR(cmd, "invalid GID parameter: not a number");
   } 
 
   if (!radius_have_var(cmd->argv[3])) {
+    char *path;
 
+    path = cmd->argv[3];
     /* Make sure the path is absolute, at least. */
-    if (*(cmd->argv[3]) != '/')
+    if (*path != '/') {
       CONF_ERROR(cmd, "home relative path not allowed");
+    }
   }
 
   if (!radius_have_var(cmd->argv[4])) {
+    char *path;
 
+    path = cmd->argv[4];
     /* Make sure the path is absolute, at least. */
-    if (*(cmd->argv[4]) != '/')
+    if (*path != '/') {
       CONF_ERROR(cmd, "shell relative path not allowed");
+    }
   }
 
   add_config_param_str(cmd->argv[0], 4, cmd->argv[1], cmd->argv[2],
     cmd->argv[3], cmd->argv[4]);
-
   return PR_HANDLED(cmd);
 }
 
@@ -3379,12 +4175,14 @@ MODRET set_radiusvendor(cmd_rec *cmd) {
   /* Make sure that the given vendor ID number is valid. */
   id = strtol(cmd->argv[2], &tmp, 10);
 
-  if (tmp && *tmp)
+  if (tmp && *tmp) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": vendor id '", cmd->argv[2],
       "' is not a valid number", NULL));
+  }
 
-  if (id < 0)
+  if (id < 0) {
     CONF_ERROR(cmd, "vendor id must be a positive number");
+  }
 
   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
   c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
@@ -3398,12 +4196,13 @@ MODRET set_radiusvendor(cmd_rec *cmd) {
  */
 
 static void radius_exit_ev(const void *event_data, void *user_data) {
+  if (radius_stop_accting() < 0) {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "error: unable to stop accounting: %s", strerror(errno));
+  }
 
-  if (!radius_stop_accting())
-    radius_log("error: unable to stop accounting");
-
-  radius_closelog();
-  return;
+  (void) close(radius_logfd);
+  radius_logfd = -1;
 }
 
 #if defined(PR_SHARED_MODULE)
@@ -3418,7 +4217,6 @@ static void radius_mod_unload_ev(const void *event_data, void *user_data) {
 
     close(radius_logfd);
     radius_logfd = -1;
-    radius_logname = NULL;
   }
 }
 #endif /* PR_SHARED_MODULE */
@@ -3426,8 +4224,9 @@ static void radius_mod_unload_ev(const void *event_data, void *user_data) {
 static void radius_restart_ev(const void *event_data, void *user_data) {
 
   /* Re-allocate the pool used by this module. */
-  if (radius_pool)
+  if (radius_pool) {
     destroy_pool(radius_pool);
+  }
 
   radius_pool = make_sub_pool(permanent_pool);
   pr_pool_tag(radius_pool, MOD_RADIUS_VERSION);
@@ -3435,6 +4234,75 @@ static void radius_restart_ev(const void *event_data, void *user_data) {
   return;
 }
 
+static void radius_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&radius_module, "core.exit", radius_exit_ev);
+  pr_event_unregister(&radius_module, "core.session-reinit",
+    radius_sess_reinit_ev);
+
+  /* Reset defaults. */
+  radius_engine = FALSE;
+  radius_acct_server = NULL;
+  radius_auth_server = NULL;
+  (void) close(radius_logfd);
+  radius_logfd = -1;
+  radius_opts = 0UL;
+  radius_nas_identifier_config = NULL;
+  radius_vendor_name = "Unix";
+  radius_vendor_id = 4;
+  radius_realm = NULL;
+
+  radius_have_user_info = FALSE;
+  radius_uid_attr_id = 0;
+  radius_gid_attr_id = 0;
+  radius_home_attr_id = 0;
+  radius_shell_attr_id = 0;
+
+  radius_have_group_info = FALSE;
+  radius_prime_group_name_attr_id = 0;
+  radius_addl_group_names_attr_id = 0;
+  radius_addl_group_ids_attr_id = 0;
+  radius_prime_group_name = NULL;
+  radius_addl_group_count = 0;
+  radius_addl_group_names = 0;
+  radius_addl_group_names_str = NULL;
+  radius_addl_group_ids = NULL;
+  radius_addl_group_ids_str = NULL;
+
+  radius_have_quota_info = FALSE;
+  radius_quota_per_sess_attr_id = 0;
+  radius_quota_limit_type_attr_id = 0;
+  radius_quota_bytes_in_attr_id = 0;
+  radius_quota_bytes_out_attr_id = 0;
+  radius_quota_bytes_xfer_attr_id = 0;
+  radius_quota_files_in_attr_id = 0;
+  radius_quota_files_out_attr_id = 0;
+  radius_quota_files_xfer_attr_id = 0;
+  radius_quota_per_sess = NULL;
+  radius_quota_limit_type = NULL;
+  radius_quota_bytes_in = NULL;
+  radius_quota_bytes_out = NULL;
+  radius_quota_bytes_xfer = NULL;
+  radius_quota_files_in = NULL;
+  radius_quota_files_out = NULL;
+  radius_quota_files_xfer = NULL;
+
+  radius_have_other_info = FALSE;
+
+  /* Note that we deliberately leave the radius_session_start time_t alone;
+   * it is initialized at the start of the session, regardless of vhost.
+   */
+
+  res = radius_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&radius_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -3443,6 +4311,19 @@ static int radius_sess_init(void) {
   config_rec *c = NULL;
   radius_server_t **current_server = NULL;
 
+  pr_event_register(&radius_module, "core.session-reinit",
+    radius_sess_reinit_ev, NULL);
+
+  /* Is RadiusEngine on? */
+  c = find_config(main_server->conf, CONF_PARAM, "RadiusEngine", FALSE);
+  if (c != NULL) {
+    radius_engine = *((int *) c->argv[0]);
+  }
+
+  if (radius_engine == FALSE) {
+    return 0;
+  }
+
   res = radius_openlog();
   if (res < 0) {
     if (res == -1) {
@@ -3460,39 +4341,37 @@ static int radius_sess_init(void) {
     }
   }
 
-  /* Is RadiusEngine on? */
-  radius_engine = FALSE;
-  c = find_config(main_server->conf, CONF_PARAM, "RadiusEngine", FALSE);
-  if (c) {
-    if (*((int *) c->argv[0]) == TRUE) {
-      radius_engine = TRUE;
-    }
-  }
-
-  if (!radius_engine) {
-    radius_log("RadiusEngine not enabled");
-    radius_closelog();
-    return 0;
-  }
-
   /* Initialize session variables */
   time(&radius_session_start);
 
+  c = find_config(main_server->conf, CONF_PARAM, "RadiusOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    radius_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "RadiusOptions", FALSE);
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "RadiusNASIdentifier", FALSE);
-  if (c) {
+  if (c != NULL) {
     radius_nas_identifier_config = c->argv[0];
 
-    radius_log("RadiusNASIdentifier '%s' configured",
-      radius_nas_identifier_config);
+    pr_trace_msg(trace_channel, 3,
+      "RadiusNASIdentifier '%s' configured", radius_nas_identifier_config);
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "RadiusVendor", FALSE);
-  if (c) {
+  if (c != NULL) {
     radius_vendor_name = c->argv[0];
     radius_vendor_id = *((unsigned int *) c->argv[1]);
 
-    radius_log("RadiusVendor '%s' (Vendor-Id %u) configured",
-      radius_vendor_name, radius_vendor_id);
+    pr_trace_msg(trace_channel, 3,
+      "RadiusVendor '%s' (Vendor-Id %u) configured", radius_vendor_name,
+      radius_vendor_id);
   }
 
   /* Find any configured RADIUS servers for this session */
@@ -3501,34 +4380,42 @@ static int radius_sess_init(void) {
   /* Point to the start of the accounting server list. */
   current_server = &radius_acct_server;
 
-  while (c) {
+  while (c != NULL) {
+    pr_signals_handle();
+
     *current_server = *((radius_server_t **) c->argv[0]);
     current_server = &(*current_server)->next;
 
     c = find_config_next(c, c->next, CONF_PARAM, "RadiusAcctServer", FALSE);
   }
 
-  if (!radius_acct_server)
-    radius_log("notice: no configured RadiusAcctServers, no accounting");
+  if (radius_acct_server == NULL) {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "notice: no configured RadiusAcctServers, no accounting");
+  }
 
   c = find_config(main_server->conf, CONF_PARAM, "RadiusAuthServer", FALSE);
 
   /* Point to the start of the authentication server list. */
   current_server = &radius_auth_server;
 
-  while (c) {
+  while (c != NULL) {
+    pr_signals_handle();
+
     *current_server = *((radius_server_t **) c->argv[0]);
     current_server = &(*current_server)->next;
 
     c = find_config_next(c, c->next, CONF_PARAM, "RadiusAuthServer", FALSE);
   }
 
-  if (!radius_auth_server)
-    radius_log("notice: no configured RadiusAuthServers, no authentication");
+  if (radius_auth_server == NULL) {
+    (void) pr_log_writefile(radius_logfd, MOD_RADIUS_VERSION,
+      "notice: no configured RadiusAuthServers, no authentication");
+  }
 
   /* Prepare any configured fake user information. */
   c = find_config(main_server->conf, CONF_PARAM, "RadiusUserInfo", FALSE);
-  if (c) {
+  if (c != NULL) {
 
     /* Process the parameter string stored in the found config_rec. */
     radius_process_user_info(c);
@@ -3561,7 +4448,7 @@ static int radius_sess_init(void) {
 
   /* Prepare any configured fake group information. */
   c = find_config(main_server->conf, CONF_PARAM, "RadiusGroupInfo", FALSE);
-  if (c) {
+  if (c != NULL) {
 
     /* Process the parameter string stored in the found config_rec. */
     radius_process_group_info(c);
@@ -3571,25 +4458,29 @@ static int radius_sess_init(void) {
      * TRUE by radius_process_group_info(), unless there was some
      * illegal value.
      */
-    if (!radius_auth_server)
+    if (radius_auth_server == NULL) {
       radius_have_group_info = FALSE;
+    }
   }
 
   /* Prepare any configure quota information. */
   c = find_config(main_server->conf, CONF_PARAM, "RadiusQuotaInfo", FALSE);
-  if (c) {
+  if (c != NULL) {
     radius_process_quota_info(c);
 
-    if (!radius_auth_server)
+    if (radius_auth_server == NULL) {
       radius_have_quota_info = FALSE;
+    }
   }
 
   /* Check for a configured RadiusRealm.  If present, use username + realm
    * in RADIUS packets as the user name, else just use the username.
    */
   radius_realm = get_param_ptr(main_server->conf, "RadiusRealm", FALSE);
-  if (radius_realm)
-    radius_log("using RadiusRealm '%s'", radius_realm);
+  if (radius_realm) {
+    pr_trace_msg(trace_channel, 3,
+      "using RadiusRealm '%s'", radius_realm);
+  }
 
   pr_event_register(&radius_module, "core.exit", radius_exit_ev, NULL);
   return 0;
@@ -3622,6 +4513,7 @@ static conftable radius_conftab[] = {
   { "RadiusGroupInfo",		set_radiusgroupinfo,	NULL },
   { "RadiusLog",		set_radiuslog,		NULL },
   { "RadiusNASIdentifier",	set_radiusnasidentifier,NULL },
+  { "RadiusOptions",		set_radiusoptions,	NULL },
   { "RadiusQuotaInfo",		set_radiusquotainfo,	NULL },
   { "RadiusRealm",		set_radiusrealm,	NULL },
   { "RadiusUserInfo",		set_radiususerinfo,	NULL },
diff --git a/contrib/mod_ratio.c b/contrib/mod_ratio.c
index fd53d4d..0386649 100644
--- a/contrib/mod_ratio.c
+++ b/contrib/mod_ratio.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_ratio -- Support upload/download ratios.
  * Portions Copyright (c) 1998-1999 Johnie Ingram.
  * Copyright (c) 2002 James Dogopoulos.
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2014 The ProFTPD Project team
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -144,13 +144,15 @@ _dispatch_ratio (cmd_rec * cmd, char *match)
   authtable *m;
   modret_t *mr = NULL;
 
-  m = pr_stash_get_symbol (PR_SYM_AUTH, match, NULL, &cmd->stash_index);
+  m = pr_stash_get_symbol2 (PR_SYM_AUTH, match, NULL, &cmd->stash_index,
+    &cmd->stash_hash);
   while (m)
     {
       mr = pr_module_call (m->m, m->handler, cmd);
       if (MODRET_ISHANDLED (mr) || MODRET_ISERROR (mr))
 	break;
-      m = pr_stash_get_symbol (PR_SYM_AUTH, match, m, &cmd->stash_index);
+      m = pr_stash_get_symbol2 (PR_SYM_AUTH, match, m, &cmd->stash_index,
+        &cmd->stash_hash);
     }
 
   if (MODRET_ISERROR(mr))
diff --git a/contrib/mod_readme.c b/contrib/mod_readme.c
index 18b7078..2710950 100644
--- a/contrib/mod_readme.c
+++ b/contrib/mod_readme.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -43,26 +43,40 @@ static void readme_add_path(pool *p, const char *path) {
   struct stat st;
   
   if (pr_fsio_stat(path, &st) == 0) {
-    int days;
+    int days = 0;
     time_t now;
-    struct tm *tm = NULL;
-    char *ptr = NULL;
+    struct tm *now_tm = NULL;
     char time_str[32] = {'\0'};
 
     (void) time(&now);
 
-    tm = pr_gmtime(p, &now);
-    days = (int) (365.25 * tm->tm_year) + tm->tm_yday;
+    now_tm = pr_gmtime(p, &now);
+    if (now_tm != NULL) {
+      struct tm *mtime_tm = NULL;
+      char *ptr = NULL;
 
-    tm = pr_gmtime(p, &st.st_mtime);
-    days -= (int) (365.25 * tm->tm_year) + tm->tm_yday;
+      days = (int) (365.25 * now_tm->tm_year) + now_tm->tm_yday;
 
-    memset(time_str, '\0', sizeof(time_str));
-    snprintf(time_str, sizeof(time_str)-1, "%.26s", ctime(&st.st_mtime));
+      mtime_tm = pr_gmtime(p, &st.st_mtime);
+      if (mtime_tm != NULL) {
+        days -= (int) (365.25 * mtime_tm->tm_year) + mtime_tm->tm_yday;
+
+      } else {
+        pr_log_debug(DEBUG3, MOD_README_VERSION
+          ": error obtaining GMT timestamp: %s", strerror(errno));
+      }
+
+      memset(time_str, '\0', sizeof(time_str));
+      snprintf(time_str, sizeof(time_str)-1, "%.26s", ctime(&st.st_mtime));
     
-    ptr = strchr(time_str, '\n');
-    if (ptr != NULL) {
-      *ptr = '\0';
+      ptr = strchr(time_str, '\n');
+      if (ptr != NULL) {
+        *ptr = '\0';
+      }
+
+    } else {
+      pr_log_debug(DEBUG3, MOD_README_VERSION
+        ": error obtaining GMT timestamp: %s", strerror(errno));
     }
 
     /* As a format nicety, if we're handling the PASS command, automatically
@@ -75,8 +89,10 @@ static void readme_add_path(pool *p, const char *path) {
     }
 
     pr_response_add(R_DUP, _("Please read the file %s"), path);
-    pr_response_add(R_DUP, _("   it was last modified on %.26s - %i %s ago"),
-      time_str, days, days == 1 ? _("day") : _("days"));
+    if (now_tm != NULL) {
+      pr_response_add(R_DUP, _("   it was last modified on %.26s - %i %s ago"),
+        time_str, days, days == 1 ? _("day") : _("days"));
+    }
   }
 }
 
@@ -147,7 +163,8 @@ MODRET set_displayreadme(cmd_rec *cmd) {
   c = add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
   c->flags |= CF_MERGEDOWN;
   
-  pr_log_debug(DEBUG5, "Added pattern %s to readme list", cmd->argv[1]);
+  pr_log_debug(DEBUG5, "Added pattern %s to readme list",
+    (char *) cmd->argv[1]);
   return PR_HANDLED(cmd);
 }
 
diff --git a/contrib/mod_rewrite.c b/contrib/mod_rewrite.c
index 99e7cb5..6804bc0 100644
--- a/contrib/mod_rewrite.c
+++ b/contrib/mod_rewrite.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_rewrite -- a module for rewriting FTP commands
- *
- * Copyright (c) 2001-2013 TJ Saunders
+ * Copyright (c) 2001-2016 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
@@ -23,8 +22,6 @@
  *
  * This is mod_rewrite, contrib software for proftpd 1.2 and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_rewrite.c,v 1.75 2013-10-13 23:43:44 castaglia Exp $
  */
 
 #include "conf.h"
@@ -136,8 +133,8 @@ static char rewrite_vars[REWRITE_MAX_VARS][13] = {
 /* Necessary prototypes */
 static char *rewrite_argsep(char **);
 static void rewrite_closelog(void);
-static char *rewrite_expand_var(cmd_rec *, const char *, const char *);
-static char *rewrite_get_cmd_name(cmd_rec *);
+static const char *rewrite_expand_var(cmd_rec *, const char *, const char *);
+static const char *rewrite_get_cmd_name(cmd_rec *);
 static void rewrite_log(char *format, ...);
 static unsigned char rewrite_match_cond(cmd_rec *, config_rec *);
 static void rewrite_openlog(void);
@@ -150,14 +147,19 @@ static int rewrite_read_fifo(int, char *, size_t);
 static unsigned char rewrite_regexec(const char *, pr_regex_t *, unsigned char,
     rewrite_match_t *);
 static void rewrite_replace_cmd_arg(cmd_rec *, char *);
-static char *rewrite_subst(cmd_rec *c, char *);
-static char *rewrite_subst_backrefs(cmd_rec *, char *, rewrite_match_t *);
-static char *rewrite_subst_env(cmd_rec *, char *);
-static char *rewrite_subst_maps(cmd_rec *, char *);
-static char *rewrite_subst_maps_fifo(cmd_rec *, config_rec *, rewrite_map_t *);
-static char *rewrite_subst_maps_int(cmd_rec *, config_rec *, rewrite_map_t *);
-static char *rewrite_subst_maps_txt(cmd_rec *, config_rec *, rewrite_map_t *);
-static char *rewrite_subst_vars(cmd_rec *, char *);
+static int rewrite_sess_init(void);
+static const char *rewrite_subst(cmd_rec *c, const char *);
+static const char *rewrite_subst_backrefs(cmd_rec *, const char *,
+  rewrite_match_t *);
+static const char *rewrite_subst_env(cmd_rec *, const char *);
+static const char *rewrite_subst_maps(cmd_rec *, const char *);
+static const char *rewrite_subst_maps_fifo(cmd_rec *, config_rec *,
+  rewrite_map_t *);
+static const char *rewrite_subst_maps_int(cmd_rec *, config_rec *,
+  rewrite_map_t *);
+static const char *rewrite_subst_maps_txt(cmd_rec *, config_rec *,
+  rewrite_map_t *);
+static const char *rewrite_subst_vars(cmd_rec *, const char *);
 static void rewrite_wait_fifo(int);
 static int rewrite_write_fifo(int, char *, size_t);
 
@@ -167,7 +169,7 @@ static int rewrite_write_fifo(int, char *, size_t);
 #define REWRITE_CHECK_VAR(p, m) \
     if (p == NULL) rewrite_log("rewrite_expand_var(): %" m " expands to NULL")
 
-static char *rewrite_expand_var(cmd_rec *cmd, const char *subst_pattern,
+static const char *rewrite_expand_var(cmd_rec *cmd, const char *subst_pattern,
     const char *var) {
   size_t varlen;
 
@@ -178,7 +180,7 @@ static char *rewrite_expand_var(cmd_rec *cmd, const char *subst_pattern,
     return (session.conn_class ? session.conn_class->cls_name : NULL);
 
   } else if (strncmp(var, "%F", 3) == 0) {
-    char *cmd_name;
+    const char *cmd_name;
 
     cmd_name = rewrite_get_cmd_name(cmd);
 
@@ -248,21 +250,24 @@ static char *rewrite_expand_var(cmd_rec *cmd, const char *subst_pattern,
     return session.user;
 
   } else if (strncmp(var, "%a", 3) == 0) {
-    return (char *) pr_netaddr_get_ipstr(session.c->remote_addr);
+    return pr_netaddr_get_ipstr(session.c->remote_addr);
 
   } else if (strncmp(var, "%h", 3) == 0) {
-    return (char *) session.c->remote_name;
+    return session.c->remote_name;
 
   } else if (strncmp(var, "%v", 3) == 0) {
-    return (char *) main_server->ServerName;
+    return main_server->ServerName;
 
   } else if (strncmp(var, "%G", 3) == 0) {
 
     if (session.groups != NULL) {
       register unsigned int i = 0;
-      char *suppl_groups = pstrcat(cmd->tmp_pool, "", NULL);
-      char **groups = (char **) session.groups->elts;
+      const char *suppl_groups;
+      char **groups;
+
+      suppl_groups = pstrcat(cmd->tmp_pool, "", NULL);
 
+      groups = (char **) session.groups->elts;
       for (i = 0; i < session.groups->nelts; i++) {
         suppl_groups = pstrcat(cmd->tmp_pool, suppl_groups,
           i != 0 ? "," : "", groups[i], NULL);
@@ -281,7 +286,7 @@ static char *rewrite_expand_var(cmd_rec *cmd, const char *subst_pattern,
       return pr_table_get(session.notes, "mod_core.rnfr-path", NULL);
 
     } else {
-      char *cmd_name;
+      const char *cmd_name;
 
       cmd_name = rewrite_get_cmd_name(cmd);
       rewrite_log("rewrite_expand_var(): %%w not valid for this command ('%s')",
@@ -315,61 +320,66 @@ static char *rewrite_expand_var(cmd_rec *cmd, const char *subst_pattern,
 
     /* Always use localtime(3) here. */
     time(&now);
-    tm = pr_localtime(cmd->tmp_pool, &now);
-
     memset(time_str, '\0', sizeof(time_str));
 
-    if (varlen == 7) {
-      /* %{TIME} */
-      snprintf(time_str, sizeof(time_str)-1, "%04d%02d%02d%02d%02d%02d",
-        tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
-        tm->tm_min, tm->tm_sec);
+    tm = pr_localtime(cmd->tmp_pool, &now);
+    if (tm != NULL) {
+      if (varlen == 7) {
+        /* %{TIME} */
+        snprintf(time_str, sizeof(time_str)-1, "%04d%02d%02d%02d%02d%02d",
+          tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
+          tm->tm_min, tm->tm_sec);
 
-    } else {
-      switch (var[7]) {
-        case 'D':
-          /* %{TIME_DAY} */ 
-          snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_mday);
-          break;
+      } else {
+        switch (var[7]) {
+          case 'D':
+            /* %{TIME_DAY} */
+            snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_mday);
+            break;
 
-        case 'H':
-          /* %{TIME_HOUR} */ 
-          snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_hour);
-          break;
+          case 'H':
+            /* %{TIME_HOUR} */
+            snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_hour);
+            break;
 
-        case 'M':
-          if (var[8] == 'I') {
-            /* %{TIME_MIN} */ 
-            snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_min);
+          case 'M':
+            if (var[8] == 'I') {
+              /* %{TIME_MIN} */
+              snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_min);
 
-          } else if (var[8] == 'O') {
-            /* %{TIME_MON} */ 
-            snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_mon + 1);
-          }
-          break;
+            } else if (var[8] == 'O') {
+              /* %{TIME_MON} */
+              snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_mon + 1);
+            }
+            break;
 
-        case 'S':
-          /* %{TIME_SEC} */ 
-          snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_sec);
-          break;
+          case 'S':
+            /* %{TIME_SEC} */
+            snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_sec);
+            break;
 
-        case 'W':
-          /* %{TIME_WDAY} */ 
-          snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_wday);
-          break;
+          case 'W':
+            /* %{TIME_WDAY} */
+            snprintf(time_str, sizeof(time_str)-1, "%02d", tm->tm_wday);
+            break;
 
-        case 'Y':
-          /* %{TIME_YEAR} */ 
-          snprintf(time_str, sizeof(time_str)-1, "%04d", tm->tm_year + 1900);
-          break;
+          case 'Y':
+            /* %{TIME_YEAR} */
+            snprintf(time_str, sizeof(time_str)-1, "%04d", tm->tm_year + 1900);
+            break;
 
-        default:
-          rewrite_log("unknown variable: '%s'", var); 
-          return NULL;
+          default:
+            rewrite_log("unknown variable: '%s'", var);
+            return NULL;
+        }
       }
     }
 
     return pstrdup(cmd->tmp_pool, time_str);
+
+  } else {
+    pr_trace_msg(trace_channel, 1, "error obtaining local timestamp: %s",
+      strerror(errno));
   }
 
   rewrite_log("unknown variable: '%s'", var); 
@@ -418,19 +428,17 @@ static char *rewrite_argsep(char **arg) {
   return res;
 }
 
-static char *rewrite_get_cmd_name(cmd_rec *cmd) {
+static const char *rewrite_get_cmd_name(cmd_rec *cmd) {
   if (pr_cmd_cmp(cmd, PR_CMD_SITE_ID) != 0) {
     return cmd->argv[0];
+  }
 
-  } else {
-    if (strcasecmp(cmd->argv[1], "CHGRP") == 0 ||
-        strcasecmp(cmd->argv[1], "CHMOD") == 0) {
-      return pstrcat(cmd->pool, cmd->argv[0], " ", cmd->argv[1], NULL);
-
-    } else {
-      return cmd->argv[0];
-    }
+  if (strcasecmp(cmd->argv[1], "CHGRP") == 0 ||
+      strcasecmp(cmd->argv[1], "CHMOD") == 0) {
+    return pstrcat(cmd->pool, cmd->argv[0], " ", cmd->argv[1], NULL);
   }
+
+  return cmd->argv[0];
 }
 
 static unsigned int rewrite_parse_cond_flags(pool *p, const char *flags_str) {
@@ -504,7 +512,7 @@ static unsigned int rewrite_parse_rule_flags(pool *p, const char *flags_str) {
 }
 
 static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
-  char *cond_str = cond->argv[0];
+  const char *cond_str = cond->argv[0];
   unsigned char negated = *((unsigned char *) cond->argv[2]);
   rewrite_cond_op_t cond_op = *((rewrite_cond_op_t *) cond->argv[3]);
 
@@ -565,7 +573,7 @@ static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
         cond_str);
 
       memset(&rewrite_cond_matches, '\0', sizeof(rewrite_cond_matches));
-      rewrite_cond_matches.match_string = cond_str;
+      rewrite_cond_matches.match_string = (char *) cond_str;
       return rewrite_regexec(cond_str, cond->argv[1], negated,
         &rewrite_cond_matches);
     }
@@ -576,10 +584,11 @@ static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
       rewrite_log("rewrite_match_cond(): checking dir test cond against "
         "path '%s'", cond_str);
 
-      pr_fs_clear_cache();
+      pr_fs_clear_cache2(cond_str);
       if (pr_fsio_lstat(cond_str, &st) >= 0 &&
-          S_ISDIR(st.st_mode))
+          S_ISDIR(st.st_mode)) {
         res = TRUE;
+      }
 
       if (!negated)
         return res;
@@ -593,10 +602,11 @@ static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
       rewrite_log("rewrite_match_cond(): checking file test cond against "
         "path '%s'", cond_str);
 
-      pr_fs_clear_cache();
+      pr_fs_clear_cache2(cond_str);
       if (pr_fsio_lstat(cond_str, &st) >= 0 &&
-          S_ISREG(st.st_mode))
+          S_ISREG(st.st_mode)) {
         res = TRUE;
+      }
 
       if (!negated)
         return res;
@@ -610,10 +620,11 @@ static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
       rewrite_log("rewrite_match_cond(): checking symlink test cond against "
         "path '%s'", cond_str);
 
-      pr_fs_clear_cache();
+      pr_fs_clear_cache2(cond_str);
       if (pr_fsio_lstat(cond_str, &st) >= 0 &&
-          S_ISLNK(st.st_mode))
+          S_ISLNK(st.st_mode)) {
         res = TRUE;
+      }
 
       if (!negated)
         return res;
@@ -627,11 +638,12 @@ static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
       rewrite_log("rewrite_match_cond(): checking size test cond against "
         "path '%s'", cond_str);
 
-      pr_fs_clear_cache();
+      pr_fs_clear_cache2(cond_str);
       if (pr_fsio_lstat(cond_str, &st) >= 0 &&
           S_ISREG(st.st_mode) &&
-          st.st_size > 0)
+          st.st_size > 0) {
         res = TRUE;
+      }
 
       if (!negated)
         return res;
@@ -649,22 +661,24 @@ static unsigned char rewrite_match_cond(cmd_rec *cmd, config_rec *cond) {
 
 static unsigned char rewrite_parse_map_str(char *str, rewrite_map_t *map) {
   static char *substr = NULL;
-  char *tmp = NULL;
+  char *ptr = NULL;
 
   /* A NULL string is used to set/reset this function. */
-  if (!str) {
+  if (str == NULL) {
     substr = NULL;
     return FALSE;
   }
 
-  if (!substr)
+  if (substr == NULL) {
     substr = str;
+  }
 
   /* Format: ${map-name:lookup-key[|default-value]} */
   rewrite_log("rewrite_parse_map_str(): parsing '%s'", substr);
-  if (substr && (tmp = strstr(substr, "${")) != NULL) {
+  if (substr != NULL &&
+      (ptr = strstr(substr, "${")) != NULL) {
     char twiddle;
-    char *map_start = tmp + 2;
+    char *map_start = ptr + 2;
     char *map_end = strchr(map_start, '}');
 
     if (!map_end) {
@@ -675,33 +689,33 @@ static unsigned char rewrite_parse_map_str(char *str, rewrite_map_t *map) {
     /* This fiddling is needed to preserve a copy of the complete map string. */
     twiddle = map_end[1];
     map_end[1] = '\0';
-    map->map_string = pstrdup(map->map_pool, tmp);
+    map->map_string = pstrdup(map->map_pool, ptr);
     map_end[1] = twiddle;
 
     /* OK, now back to our regular schedule parsing... */
     *map_end = '\0';
 
-    tmp = strchr(map_start, ':');
-    if (tmp == NULL) {
+    ptr = strchr(map_start, ':');
+    if (ptr == NULL) {
       rewrite_log("rewrite_parse_mapstr(): notice: badly formatted map string");
       return FALSE;
     }
-    *tmp = '\0';
+    *ptr = '\0';
 
     /* We've teased out the map name. */
     map->map_name = map_start;
 
     /* Advance the pointer so that the rest of the components can be parsed. */
-    map_start = ++tmp;
+    map_start = ++ptr;
 
     map->map_lookup_key = map_start;
 
-    tmp = strchr(map_start, '|');
-    if (tmp != NULL) {
-      *tmp = '\0';
+    ptr = strchr(map_start, '|');
+    if (ptr != NULL) {
+      *ptr = '\0';
 
       /* We've got the default value. */
-      map->map_default_value = ++tmp;
+      map->map_default_value = ++ptr;
 
     } else {
       map->map_default_value = "";
@@ -879,9 +893,9 @@ static void rewrite_replace_cmd_arg(cmd_rec *cmd, char *new_arg) {
   }
 }
 
-static char *rewrite_subst(cmd_rec *cmd, char *pattern) {
+static const char *rewrite_subst(cmd_rec *cmd, const char *pattern) {
   int have_cond_backrefs = FALSE;
-  char *new_pattern = NULL;
+  const char *new_pattern = NULL;
 
   rewrite_log("rewrite_subst(): original pattern: '%s'", pattern);
 
@@ -940,10 +954,10 @@ static char *rewrite_subst(cmd_rec *cmd, char *pattern) {
   return new_pattern;
 }
 
-static char *rewrite_subst_backrefs(cmd_rec *cmd, char *pattern,
+static const char *rewrite_subst_backrefs(cmd_rec *cmd, const char *pattern,
     rewrite_match_t *matches) {
   register unsigned int i = 0;
-  char *replacement_pattern = NULL;
+  const char *replacement_pattern = NULL;
   int use_notes = TRUE;
 
   /* We do NOT stash the backrefs in the cmd->notes table for sensitive
@@ -1018,7 +1032,8 @@ static char *rewrite_subst_backrefs(cmd_rec *cmd, char *pattern,
          * string with the literal string.
          */
         if (*(ptr - 1) == '$') {
-          char *res, *var;
+          const char *res;
+          char *var;
           size_t var_len = sizeof(buf) + 1;
 
           var = pcalloc(cmd->tmp_pool, var_len);
@@ -1046,7 +1061,8 @@ static char *rewrite_subst_backrefs(cmd_rec *cmd, char *pattern,
          * string with the literal string.
          */
         if (*(ptr - 1) == '%') {
-          char *res, *var;
+          const char *res;
+          char *var;
           size_t var_len = sizeof(buf) + 1;
 
           var = pcalloc(cmd->tmp_pool, var_len);
@@ -1070,7 +1086,8 @@ static char *rewrite_subst_backrefs(cmd_rec *cmd, char *pattern,
     }
 
     if (matches->match_groups[i].rm_so != -1) {
-      char *value, *res, tmp;
+      const char *res;
+      char *value, tmp;
 
       /* There's a match for the backref in the string, substitute in
        * the backreferenced value.
@@ -1121,7 +1138,7 @@ static char *rewrite_subst_backrefs(cmd_rec *cmd, char *pattern,
       (matches->match_string)[matches->match_groups[i].rm_eo] = tmp;
 
     } else {
-      char *res;
+      const char *res;
 
       /* There's backreference in the string, but there no matching
        * group (i.e. backreferenced value).  Substitute in an empty string
@@ -1169,12 +1186,19 @@ static char *rewrite_subst_backrefs(cmd_rec *cmd, char *pattern,
   return (replacement_pattern ? replacement_pattern : pattern);
 }
 
-static char *rewrite_subst_env(cmd_rec *cmd, char *pattern) {
-  char *new_pattern = NULL, *ptr;
+static const char *rewrite_subst_env(cmd_rec *cmd, const char *pattern) {
+  const char *new_pattern = NULL;
+  char *pat, *ptr;
+
+  /* We need to make a duplicate of the given pattern, since we twiddle some
+   * of its bytes.
+   */
+  pat = pstrdup(cmd->tmp_pool, pattern);
 
-  ptr = strstr(pattern, "%{ENV:");
-  while (ptr) {
-    char ch, *ptr2, *key, *res, *val;
+  ptr = strstr(pat, "%{ENV:");
+  while (ptr != NULL) {
+    const char *val, *res;
+    char ch, *ptr2, *key;
 
     pr_signals_handle();
 
@@ -1189,13 +1213,13 @@ static char *rewrite_subst_env(cmd_rec *cmd, char *pattern) {
     key = pstrdup(cmd->tmp_pool, ptr);
     *(ptr2 + 1) = ch;
 
-    val = rewrite_expand_var(cmd, pattern, key);
+    val = rewrite_expand_var(cmd, pat, key);
     if (val != NULL) {
       rewrite_log("rewrite_subst_env(): replacing variable '%s' with '%s'",
         key, val);
 
       if (new_pattern == NULL) {
-        new_pattern = pstrdup(cmd->pool, pattern);
+        new_pattern = pstrdup(cmd->pool, pat);
       }
 
       res = pr_str_replace(cmd->pool, rewrite_max_replace, new_pattern, key,
@@ -1217,13 +1241,14 @@ static char *rewrite_subst_env(cmd_rec *cmd, char *pattern) {
   return (new_pattern ? new_pattern : pattern);
 }
 
-static char *rewrite_subst_maps(cmd_rec *cmd, char *pattern) {
+static const char *rewrite_subst_maps(cmd_rec *cmd, const char *pattern) {
   rewrite_map_t map;
-  char *tmp_pattern = pstrdup(cmd->pool, pattern), *new_pattern = NULL;
+  const char *tmp_pattern, *new_pattern = NULL;
 
+  tmp_pattern = pstrdup(cmd->pool, pattern);
   map.map_pool = cmd->tmp_pool;
 
-  while (rewrite_parse_map_str(tmp_pattern, &map)) {
+  while (rewrite_parse_map_str((char *) tmp_pattern, &map)) {
     config_rec *c = NULL;
     unsigned char have_map = FALSE;
 
@@ -1238,12 +1263,11 @@ static char *rewrite_subst_maps(cmd_rec *cmd, char *pattern) {
      * name is actually valid.
      */
     c = find_config(main_server->conf, CONF_PARAM, "RewriteMap", FALSE);
-
-    while (c) {
+    while (c != NULL) {
       pr_signals_handle();
 
       if (strcmp(c->argv[0], map.map_name) == 0) { 
-        char *lookup_value = NULL, *res;
+        const char *lookup_value = NULL, *res;
         have_map = TRUE;
 
         rewrite_log("rewrite_subst_maps(): mapping '%s' using '%s'",
@@ -1305,11 +1329,12 @@ static char *rewrite_subst_maps(cmd_rec *cmd, char *pattern) {
   return (new_pattern ? new_pattern : pattern);
 }
 
-static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
+static const char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
     rewrite_map_t *map) {
   int fifo_fd = -1, fifo_lockfd = -1, res;
   char *value = NULL, *fifo_lockname = NULL;
   const char *fifo = (char *) c->argv[2];
+  size_t map_lookup_keylen;
 
 #ifndef HAVE_FLOCK
   struct flock lock;
@@ -1317,7 +1342,6 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
 
   /* The FIFO file descriptor should already be open. */
   fifo_fd = *((int *) c->argv[3]);
-
   if (fifo_fd == -1) {
     rewrite_log("rewrite_subst_maps_fifo(): missing necessary FIFO file "
       "descriptor");
@@ -1328,8 +1352,7 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
   pr_signals_block();
 
   /* See if a RewriteLock has been configured. */
-  fifo_lockname = (char *) get_param_ptr(main_server->conf, "RewriteLock",
-    FALSE);
+  fifo_lockname = get_param_ptr(main_server->conf, "RewriteLock", FALSE);
   if (fifo_lockname != NULL) {
     /* Make sure the file exists. */
     fifo_lockfd = open(fifo_lockname, O_RDWR|O_CREAT, 0666);
@@ -1342,9 +1365,10 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
   /* Obtain a write lock on the lock file, if configured */
   if (fifo_lockfd != -1) {
 #ifdef HAVE_FLOCK
-    if (flock(fifo_lockfd, LOCK_EX) < 0)
+    if (flock(fifo_lockfd, LOCK_EX) < 0) {
       rewrite_log("rewrite_subst_maps_fifo(): error obtaining lock: %s",
         strerror(errno));
+    }
 #else
     lock.l_type = F_WRLCK;
     lock.l_whence = 0;
@@ -1379,18 +1403,20 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
    */
 
   pr_signals_unblock();
-  if (rewrite_write_fifo(fifo_fd,
-      pstrcat(cmd->tmp_pool, map->map_lookup_key, "\n", NULL),
-      strlen(map->map_lookup_key) + 1) != strlen(map->map_lookup_key) + 1) {
-
+  map_lookup_keylen = strlen(map->map_lookup_key);
+  res = rewrite_write_fifo(fifo_fd,
+    pstrcat(cmd->tmp_pool, map->map_lookup_key, "\n", NULL),
+    map_lookup_keylen + 1);
+  if ((size_t) res != (map_lookup_keylen + 1)) {
     rewrite_log("rewrite_subst_maps_fifo(): error writing lookup key '%s' to "
       "FIFO '%s': %s", map->map_lookup_key, fifo, strerror(errno));
 
     if (fifo_lockfd != -1) {
 #ifdef HAVE_FLOCK
-      if (flock(fifo_lockfd, LOCK_UN) < 0)
+      if (flock(fifo_lockfd, LOCK_UN) < 0) {
         rewrite_log("rewrite_subst_maps_fifo(): error releasing lock: %s",
           strerror(errno));
+      }
 #else
       lock.l_type = F_UNLCK;
       lock.l_whence = 0;
@@ -1408,7 +1434,7 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
       }
 #endif /* HAVE_FLOCK */
 
-      close(fifo_lockfd);
+      (void) close(fifo_lockfd);
     }
 
     /* Return the default value */
@@ -1434,9 +1460,10 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
   pr_signals_unblock();
   res = rewrite_read_fifo(fifo_fd, value, REWRITE_FIFO_MAXLEN);
   if (res <= 0) {
-    if (res < 0)
+    if (res < 0) {
       rewrite_log("rewrite_subst_maps_fifo(): error reading value from FIFO "
         "'%s': %s", fifo, strerror(errno));
+    }
 
     /* Use the default value */
     value = map->map_default_value;
@@ -1468,9 +1495,10 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
 
   if (fifo_lockfd != -1) {
 #ifdef HAVE_FLOCK
-    if (flock(fifo_lockfd, LOCK_UN) < 0)
+    if (flock(fifo_lockfd, LOCK_UN) < 0) {
       rewrite_log("rewrite_subst_maps_fifo(): error releasing lock: %s",
         strerror(errno));
+    }
 #else
     lock.l_type = F_UNLCK;
     lock.l_whence = 0;
@@ -1488,35 +1516,37 @@ static char *rewrite_subst_maps_fifo(cmd_rec *cmd, config_rec *c,
     }
 #endif /* HAVE_FLOCK */
 
-    close(fifo_lockfd);
+    (void) close(fifo_lockfd);
   }
 
   pr_signals_unblock();
-
   return value;
 }
 
-static char *rewrite_subst_maps_int(cmd_rec *cmd, config_rec *c,
+static const char *rewrite_subst_maps_int(cmd_rec *cmd, config_rec *c,
     rewrite_map_t *map) {
-  char *value = NULL;
+  const char *value = NULL;
   char *(*map_func)(pool *, char *) = (char *(*)(pool *, char *)) c->argv[2];
    
   value = map_func(cmd->tmp_pool, map->map_lookup_key);
-  if (value == NULL)
+  if (value == NULL) {
     value = map->map_default_value;
+  }
 
   return value;
 }
 
-static char *rewrite_subst_maps_txt(cmd_rec *cmd, config_rec *c,
+static const char *rewrite_subst_maps_txt(cmd_rec *cmd, config_rec *c,
     rewrite_map_t *map) {
   rewrite_map_txt_t *txtmap = c->argv[2];
-  char **txt_keys = NULL, **txt_vals = NULL, *value = NULL;
+  const char *value = NULL;
+  char **txt_keys = NULL, **txt_vals = NULL;
   register unsigned int i = 0;
 
   /* Make sure this map is up-to-date. */
-  if (!rewrite_parse_map_txt(txtmap))
+  if (!rewrite_parse_map_txt(txtmap)) {
     rewrite_log("rewrite_subst_maps_txt(): error parsing txt file");
+  }
 
   txt_keys = (char **) txtmap->txt_keys;
   txt_vals = (char **) txtmap->txt_values;
@@ -1534,16 +1564,19 @@ static char *rewrite_subst_maps_txt(cmd_rec *cmd, config_rec *c,
   return value;
 }
 
-static char *rewrite_subst_vars(cmd_rec *cmd, char *pattern) {
+static const char *rewrite_subst_vars(cmd_rec *cmd, const char *pattern) {
   register unsigned int i = 0;
-  char *new_pattern = NULL;
+  const char *new_pattern = NULL;
 
   for (i = 0; i < REWRITE_MAX_VARS; i++) {
-    char *res, *val = NULL;
+    const char *val = NULL, *res;
+
+    pr_signals_handle();
 
     /* Does this variable occur in the substitution pattern? */
-    if (strstr(pattern, rewrite_vars[i]) == NULL)
+    if (strstr(pattern, rewrite_vars[i]) == NULL) {
       continue;
+    }
 
     val = rewrite_expand_var(cmd, pattern, rewrite_vars[i]);
     if (val != NULL) {
@@ -1703,38 +1736,39 @@ static int rewrite_utf8_to_ucs4(unsigned long *ucs4_buf,
  * probably will) modify their key arguments.
  */
 
-static char *rewrite_map_int_replaceall(pool *map_pool, char *key) {
+static const char *rewrite_map_int_replaceall(pool *map_pool, char *key) {
   char sep = *key;
   char *value = NULL, *src = NULL, *dst = NULL;
-  char *tmp = NULL, *res = NULL;
+  const char *res = NULL;
+  char *ptr = NULL, *str;
 
   /* Due to the way in which this internal function works, the first
    * character of the given key is used as a delimiter separating
    * the given key, and the sequences to replace for this function.
    */
-  char *str = pstrdup(map_pool, key + 1);
+  str = pstrdup(map_pool, key + 1);
 
-  tmp = strchr(str, sep);
-  if (tmp == NULL) {
+  ptr = strchr(str, sep);
+  if (ptr == NULL) {
     rewrite_log("rewrite_map_int_replaceall(): badly formatted input key");
     return NULL;
   }
 
-  *tmp = '\0';
+  *ptr = '\0';
   value = str;
   rewrite_log("rewrite_map_int_replaceall(): actual key: '%s'", value); 
  
-  str = tmp + 1;
+  str = ptr + 1;
 
-  tmp = strchr(str, sep);
-  if (tmp == NULL) {
+  ptr = strchr(str, sep);
+  if (ptr == NULL) {
     rewrite_log("rewrite_map_int_replaceall(): badly formatted input key");
     return NULL;
   }
 
-  *tmp = '\0';
+  *ptr = '\0';
   src = str;
-  dst = tmp + 1;
+  dst = ptr + 1;
   
   rewrite_log("rewrite_map_int_replaceall(): replacing '%s' with '%s'", src,
     dst);
@@ -1762,24 +1796,32 @@ static char *rewrite_map_int_replaceall(pool *map_pool, char *key) {
   return res;
 }
 
-static char *rewrite_map_int_tolower(pool *map_pool, char *key) {
+static const char *rewrite_map_int_tolower(pool *map_pool, char *key) {
   register unsigned int i = 0;
-  char *value = pstrdup(map_pool, key);
-  size_t valuelen = strlen(value);
+  char *value;
+  size_t valuelen;
+
+  value = pstrdup(map_pool, key);
+  valuelen = strlen(value);
 
-  for (i = 0; i < valuelen; i++)
+  for (i = 0; i < valuelen; i++) {
     value[i] = tolower(value[i]);
+  }
 
   return value;
 }
 
-static char *rewrite_map_int_toupper(pool *map_pool, char *key) {
+static const char *rewrite_map_int_toupper(pool *map_pool, char *key) {
   register unsigned int i = 0;
-  char *value = pstrdup(map_pool, key);
-  size_t valuelen = strlen(value);
+  char *value;
+  size_t valuelen;
 
-  for (i = 0; i < valuelen; i++)
+  value = pstrdup(map_pool, key);
+  valuelen = strlen(value);
+
+  for (i = 0; i < valuelen; i++) {
     value[i] = toupper(value[i]);
+  }
 
   return value;
 }
@@ -1788,23 +1830,23 @@ static char *rewrite_map_int_toupper(pool *map_pool, char *key) {
  * path).  Returns the escaped string on success, NULL on error; failures can
  * be caused by: bad % escape sequences, decoding %00, or a special character.
  */
-static char *rewrite_map_int_unescape(pool *map_pool, char *key) {
+static const char *rewrite_map_int_unescape(pool *map_pool, char *key) {
   register int i, j;
-  char *value = pcalloc(map_pool, sizeof(char) * strlen(key));
+  char *value;
 
+  value = pcalloc(map_pool, sizeof(char) * strlen(key));
   for (i = 0, j = 0; key[j]; ++i, ++j) {
     if (key[j] != '%') {
       value[i] = key[j];
 
     } else {
-
-      if (!PR_ISXDIGIT(key[j+1]) || !PR_ISXDIGIT(key[j+2])) {
+      if (!PR_ISXDIGIT(key[j+1]) ||
+          !PR_ISXDIGIT(key[j+2])) {
         rewrite_log("rewrite_map_int_unescape(): bad escape sequence '%c%c%c'",
           key[j], key[j+1], key[j+2]);
         return NULL;
 
       } else {
-
         value[i] = rewrite_hex_to_char(&key[j+1]);
         j += 2;
         if (key[i] == '/' || key[i] == '\0') {
@@ -1951,8 +1993,10 @@ static char *rewrite_map_int_utf8trans(pool *map_pool, char *key) {
   static unsigned long ucs4_longs[PR_TUNABLE_BUFFER_SIZE] = {0L};
 
   /* If the key is NULL or empty, do nothing. */
-  if (!key || !strlen(key))
+  if (key == NULL ||
+      !strlen(key)) {
     return NULL;
+  }
 
   /* Always make sure the buffers are clear for this run. */
   memset(utf8_val, '\0', PR_TUNABLE_BUFFER_SIZE);
@@ -1968,14 +2012,15 @@ static char *rewrite_map_int_utf8trans(pool *map_pool, char *key) {
     return NULL;
 
   } else if (ucs4strlen > 1) {
-    register unsigned int i = 0;
+    register int i = 0;
 
     /* Cast the UTF-8 longs to unsigned chars.  NOTE: this is an assumption
      * about casts; it just so happens, quite nicely, that UCS4 maps one-to-one
      * to ISO-8859-1 (Latin-1).
      */
-    for (i = 0; i < ucs4strlen; i++)
+    for (i = 0; i < ucs4strlen; i++) {
       utf8_val[i] = (unsigned char) ucs4_longs[i];
+    }
 
     return pstrdup(map_pool, (const char *) utf8_val);
   }
@@ -2075,84 +2120,99 @@ MODRET set_rewritecondition(cmd_rec *cmd) {
   unsigned char negated = FALSE;
   rewrite_cond_op_t cond_op = 0;
   int regex_flags = REG_EXTENDED, res = -1;
+  char *pattern;
 
-  if (cmd->argc-1 < 2 || cmd->argc-1 > 3)
+  if (cmd->argc-1 < 2 ||
+      cmd->argc-1 > 3) {
     CONF_ERROR(cmd, "bad number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR);
 
   /* The following variables are not allowed in RewriteConditions:
    *  %P (PID), and %t (Unix epoch).  Check for them.
    */
-  if (strstr(cmd->argv[2], "%P") || strstr(cmd->argv[2], "%t"))
+  if (strstr(cmd->argv[2], "%P") != NULL ||
+      strstr(cmd->argv[2], "%t") != NULL) {
     CONF_ERROR(cmd, "illegal RewriteCondition variable used");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR);
 
   /* Make sure that, if present, the flags parameter is correctly formatted. */
   if (cmd->argc-1 == 3) {
-    if (cmd->argv[3][0] != '[' || cmd->argv[3][strlen(cmd->argv[3])-1] != ']')
+    char *flags_str;
+
+    flags_str = cmd->argv[3];
+
+    if (flags_str[0] != '[' ||
+        flags_str[strlen(flags_str)-1] != ']') {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-        ": badly formatted flags parameter: '", cmd->argv[3], "'", NULL));
+        ": badly formatted flags parameter: '", flags_str, "'", NULL));
+    }
 
     /* We need to parse the flags parameter here, to see if any flags which
      * affect the compilation of the regex (e.g. NC) are present.
      */
-    cond_flags = rewrite_parse_cond_flags(cmd->tmp_pool, cmd->argv[3]);
+    cond_flags = rewrite_parse_cond_flags(cmd->tmp_pool, flags_str);
     if (cond_flags == 0) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-        ": unknown RewriteCondition flags '", cmd->argv[3], "'", NULL));
+        ": unknown RewriteCondition flags '", flags_str, "'", NULL));
     }
 
-    if (cond_flags & REWRITE_COND_FLAG_NOCASE)
+    if (cond_flags & REWRITE_COND_FLAG_NOCASE) {
       regex_flags |= REG_ICASE;
+    }
   }
 
-  if (!rewrite_conds) {
-    if (rewrite_cond_pool)
+  if (rewrite_conds == NULL) {
+    if (rewrite_cond_pool != NULL) {
       destroy_pool(rewrite_cond_pool);
+    }
 
     rewrite_cond_pool = make_sub_pool(rewrite_pool);
     rewrite_conds = make_array(rewrite_cond_pool, 0, sizeof(config_rec *));
   }
 
   /* Check for a leading '!' negation prefix to the regex pattern */
-  if (*cmd->argv[2] == '!') {
-    cmd->argv[2]++;
+  pattern = cmd->argv[2];
+  if (pattern[0] == '!') {
+    pattern++;
     negated = TRUE;
   }
 
   /* Check the next character in the given pattern.  It may be a lexical
    * or a file test pattern...
    */
-  if (*cmd->argv[2] == '>') {
+  if (*pattern == '>') {
     cond_op = REWRITE_COND_OP_LEX_LT;
-    cond_data = pstrdup(rewrite_pool, ++cmd->argv[2]);
+    cond_data = pstrdup(rewrite_pool, ++pattern);
 
-  } else if (*cmd->argv[2] == '<') {
+  } else if (*pattern == '<') {
     cond_op = REWRITE_COND_OP_LEX_GT;
-    cond_data = pstrdup(rewrite_pool, ++cmd->argv[2]);
+    cond_data = pstrdup(rewrite_pool, ++pattern);
 
-  } else if (*cmd->argv[2] == '=') {
+  } else if (*pattern == '=') {
     cond_op = REWRITE_COND_OP_LEX_EQ;
-    cond_data = pstrdup(rewrite_pool, ++cmd->argv[2]);
+    cond_data = pstrdup(rewrite_pool, ++pattern);
 
-  } else if (strncmp(cmd->argv[2], "-d", 3) == 0) {
+  } else if (strncmp(pattern, "-d", 3) == 0) {
     cond_op = REWRITE_COND_OP_TEST_DIR;
 
-  } else if (strncmp(cmd->argv[2], "-f", 3) == 0) {
+  } else if (strncmp(pattern, "-f", 3) == 0) {
     cond_op = REWRITE_COND_OP_TEST_FILE;
 
-  } else if (strncmp(cmd->argv[2], "-l", 3) == 0) {
+  } else if (strncmp(pattern, "-l", 3) == 0) {
     cond_op = REWRITE_COND_OP_TEST_SYMLINK;
 
-  } else if (strncmp(cmd->argv[2], "-s", 3) == 0) {
+  } else if (strncmp(pattern, "-s", 3) == 0) {
     cond_op = REWRITE_COND_OP_TEST_SIZE;
 
   } else {
     cond_op = REWRITE_COND_OP_REGEX;
     cond_data = pr_regexp_alloc(&rewrite_module);
 
-    res = pr_regexp_compile(cond_data, cmd->argv[2], regex_flags);
+    res = pr_regexp_compile(cond_data, pattern, regex_flags);
     if (res != 0) {
       char errstr[200] = {'\0'};
 
@@ -2160,7 +2220,7 @@ MODRET set_rewritecondition(cmd_rec *cmd) {
       regfree(cond_data);
 
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to compile '",
-        cmd->argv[2], "' regex: ", errstr, NULL));
+        pattern, "' regex: ", errstr, NULL));
     }
   }
 
@@ -2254,15 +2314,18 @@ MODRET set_rewriteengine(cmd_rec *cmd) {
 
 /* usage: RewriteLock file */
 MODRET set_rewritelock(cmd_rec *cmd) {
+  char *path;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   /* Check for non-absolute paths */
-  if (*cmd->argv[1] != '/')
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, "absolute path required");
+  }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
-
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
@@ -2288,16 +2351,19 @@ MODRET set_rewritemaxreplace(cmd_rec *cmd) {
 
 /* usage: RewriteLog file|"none" */
 MODRET set_rewritelog(cmd_rec *cmd) {
+  char *path;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   /* Check for non-absolute paths */
-  if (strcasecmp(cmd->argv[1], "none") != 0 && *(cmd->argv[1]) != '/')
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, cmd->argv[0], ": absolute path "
-      "required", NULL));
-
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  path = cmd->argv[1];
+  if (strcasecmp(path, "none") != 0 &&
+      *path != '/') {
+    CONF_ERROR(cmd, "absolute path required");
+  }
 
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
@@ -2376,17 +2442,18 @@ MODRET set_rewritemap(cmd_rec *cmd) {
     txtmap = pcalloc(txt_pool, sizeof(rewrite_map_txt_t));
 
     /* Make sure the given path is absolute. */
-    if (*mapsrc != '/')
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, cmd->argv[0],
+    if (*mapsrc != '/') {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, (char *) cmd->argv[0],
         ": txt: absolute path required", NULL));
+    }
 
     txtmap->txt_pool = txt_pool;
     txtmap->txt_path = pstrdup(txt_pool, mapsrc);    
 
     if (!rewrite_parse_map_txt(txtmap)) {
-      pr_log_debug(DEBUG3, "%s: error parsing map file", cmd->argv[0]);
+      pr_log_debug(DEBUG3, "%s: error parsing map file", (char *) cmd->argv[0]);
       pr_log_debug(DEBUG3, "%s: check the RewriteLog for details",
-        cmd->argv[0]);
+        (char *) cmd->argv[0]);
     }
 
     map = (void *) txtmap;
@@ -2414,36 +2481,46 @@ MODRET set_rewriterule(cmd_rec *cmd) {
   unsigned char negated = FALSE;
   int regex_flags = REG_EXTENDED, res = -1;
   register unsigned int i = 0;
+  char *pattern;
 
-  if (cmd->argc-1 < 2 || cmd->argc-1 > 3)
+  if (cmd->argc-1 < 2 ||
+      cmd->argc-1 > 3) {
     CONF_ERROR(cmd, "bad number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR);
 
   /* Make sure that, if present, the flags parameter is correctly formatted. */
   if (cmd->argc-1 == 3) {
-    if (cmd->argv[3][0] != '[' || cmd->argv[3][strlen(cmd->argv[3])-1] != ']')
+    char *flags_str;
+
+    flags_str = cmd->argv[3];
+    if (flags_str[0] != '[' ||
+        flags_str[strlen(flags_str)-1] != ']') {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-        ": badly formatted flags parameter: '", cmd->argv[3], "'", NULL));
+        ": badly formatted flags parameter: '", flags_str, "'", NULL));
+    }
 
     /* We need to parse the flags parameter here, to see if any flags which
      * affect the compilation of the regex (e.g. NC) are present.
      */
-    rule_flags = rewrite_parse_rule_flags(cmd->tmp_pool, cmd->argv[3]);
+    rule_flags = rewrite_parse_rule_flags(cmd->tmp_pool, flags_str);
 
-    if (rule_flags & REWRITE_RULE_FLAG_NOCASE)
+    if (rule_flags & REWRITE_RULE_FLAG_NOCASE) {
       regex_flags |= REG_ICASE;
+    }
   }
 
   pre = pr_regexp_alloc(&rewrite_module);
 
   /* Check for a leading '!' prefix, signifying regex negation */
-  if (*cmd->argv[1] == '!') {
+  pattern = cmd->argv[1];
+  if (*pattern == '!') {
     negated = TRUE;
-    cmd->argv[1]++;
+    pattern++;
   }
 
-  res = pr_regexp_compile_posix(pre, cmd->argv[1], regex_flags);
+  res = pr_regexp_compile_posix(pre, pattern, regex_flags);
   if (res != 0) {
     char errstr[200] = {'\0'};
 
@@ -2451,7 +2528,7 @@ MODRET set_rewriterule(cmd_rec *cmd) {
     pr_regexp_free(NULL, pre);
 
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to compile '",
-      cmd->argv[1], "' regex: ", errstr, NULL));
+      pattern, "' regex: ", errstr, NULL));
   }
 
   c = add_config_param(cmd->argv[0], 6, pre, NULL, NULL, NULL, NULL, NULL);
@@ -2477,8 +2554,9 @@ MODRET set_rewriterule(cmd_rec *cmd) {
     arg_conds = (config_rec **) c->argv[3];
     conf_conds = (config_rec **) rewrite_conds->elts;
 
-    for (i = 0; i <= rewrite_conds->nelts; i++)
+    for (i = 0; i <= rewrite_conds->nelts; i++) {
       arg_conds[i] = conf_conds[i];
+    }
 
     arg_conds[rewrite_conds->nelts] = NULL;
 
@@ -2660,7 +2738,7 @@ MODRET rewrite_fixup(cmd_rec *cmd) {
     } 
 
     if (exec_rule) {
-      char *new_arg = NULL;
+      const char *new_arg = NULL;
       unsigned int rule_flags = *((unsigned int *) c->argv[4]);
 
       rewrite_log("rewrite_fixup(): executing RewriteRule");
@@ -2668,10 +2746,10 @@ MODRET rewrite_fixup(cmd_rec *cmd) {
 
       if (strlen(new_arg) > 0) {
         int flags = PR_STR_FL_PRESERVE_COMMENTS;
-        char *param, *dup;
+        char *param, *dup_arg;
         array_header *list;
 
-        rewrite_replace_cmd_arg(cmd, new_arg);
+        rewrite_replace_cmd_arg(cmd, (char *) new_arg);
         rewrite_log("rewrite_fixup(): %s arg now '%s'", cmd_name, new_arg);
 
         /* Be sure to overwrite the entire cmd->argv array, not just
@@ -2697,8 +2775,8 @@ MODRET rewrite_fixup(cmd_rec *cmd) {
           }
         }
 
-        dup = pstrdup(cmd->tmp_pool, new_arg);
-        while ((param = pr_str_get_word(&dup, flags)) != NULL) {
+        dup_arg = pstrdup(cmd->tmp_pool, new_arg);
+        while ((param = pr_str_get_word(&dup_arg, flags)) != NULL) {
           pr_signals_handle();
 
           *((char **) push_array(list)) = pstrdup(cmd->pool, param);
@@ -2708,8 +2786,7 @@ MODRET rewrite_fixup(cmd_rec *cmd) {
         /* NULL-terminate the list. */
         *((char **) push_array(list)) = NULL;
 
-        cmd->argv = (char **) list->elts;
-
+        cmd->argv = list->elts;
         pr_cmd_clear_cache(cmd);
 
       } else {
@@ -2771,7 +2848,7 @@ static void rewrite_restart_ev(const void *event_data, void *user_data) {
 }
 
 static void rewrite_rewrite_home_ev(const void *event_data, void *user_data) {
-  char *pw_dir;
+  const char *pw_dir;
   pool *tmp_pool;
   cmd_rec *cmd;
   modret_t *mr; 
@@ -2788,7 +2865,7 @@ static void rewrite_rewrite_home_ev(const void *event_data, void *user_data) {
   pr_pool_tag(tmp_pool, "rewrite home pool");
 
   cmd = pr_cmd_alloc(tmp_pool, 2, pstrdup(tmp_pool, "REWRITE_HOME"), pw_dir);
-  cmd->arg = pw_dir;
+  cmd->arg = pstrdup(tmp_pool, pw_dir);
   cmd->tmp_pool = tmp_pool;
 
   /* Call rewrite_fixup() directly, rather than going through the entire
@@ -2823,16 +2900,83 @@ static void rewrite_rewrite_home_ev(const void *event_data, void *user_data) {
   destroy_pool(tmp_pool);
 }
 
+static void rewrite_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+  config_rec *c;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&rewrite_module, "core.exit", rewrite_exit_ev);
+  pr_event_unregister(&rewrite_module, "core.session-reinit",
+    rewrite_sess_reinit_ev);
+  pr_event_unregister(&rewrite_module, "mod_auth.rewrite-home",
+    rewrite_rewrite_home_ev);
+
+  /* Reset defaults. */
+  rewrite_engine = FALSE;
+  (void) close(rewrite_logfd);
+  rewrite_logfd = -1;
+  rewrite_logfile = NULL;
+  rewrite_max_replace = PR_STR_MAX_REPLACEMENTS;
+
+  /* Close any opened FIFO RewriteMaps. */
+  c = find_config(session.prev_server->conf, CONF_PARAM, "RewriteMap", FALSE);
+  while (c != NULL) {
+    pr_signals_handle();
+
+    if (strcmp(c->argv[1], "fifo") == 0) {
+      int fd;
+
+      fd = *((int *) c->argv[3]);
+      (void) close(fd);
+      *((int *) c->argv[3]) = -1;
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "RewriteMap", FALSE);
+  }
+
+  res = rewrite_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&rewrite_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
+
 /* Initialization functions
  */
 
+static int rewrite_init(void) {
+
+  /* Allocate a pool for this module's use. */
+  if (rewrite_pool == NULL) {
+    rewrite_pool = make_sub_pool(permanent_pool);
+    pr_pool_tag(rewrite_pool, MOD_REWRITE_VERSION);
+  }
+
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&rewrite_module, "core.module-unload",
+    rewrite_mod_unload_ev, NULL);
+#endif /* PR_SHARED_MODULE */
+
+  /* Add a restart handler. */
+  pr_event_register(&rewrite_module, "core.restart", rewrite_restart_ev,
+    NULL);
+
+  return 0;
+}
+
 static int rewrite_sess_init(void) {
   config_rec *c = NULL;
-  unsigned char *engine = get_param_ptr(main_server->conf, "RewriteEngine",
-    FALSE);
+  unsigned char *engine = NULL;
+
+  pr_event_register(&rewrite_module, "core.session-reinit",
+    rewrite_sess_reinit_ev, NULL);
 
   /* Is RewriteEngine on? */
-  if (!engine || *engine == FALSE) {
+  engine = get_param_ptr(main_server->conf, "RewriteEngine", FALSE);
+  if (engine == NULL ||
+      *engine == FALSE) {
     rewrite_engine = FALSE;
     return 0;
   }
@@ -2882,26 +3026,6 @@ static int rewrite_sess_init(void) {
   return 0;
 }
 
-static int rewrite_init(void) {
-
-  /* Allocate a pool for this module's use. */
-  if (!rewrite_pool) {
-    rewrite_pool = make_sub_pool(permanent_pool);
-    pr_pool_tag(rewrite_pool, MOD_REWRITE_VERSION);
-  }
-
-#if defined(PR_SHARED_MODULE)
-  pr_event_register(&rewrite_module, "core.module-unload",
-    rewrite_mod_unload_ev, NULL);
-#endif /* PR_SHARED_MODULE */
-
-  /* Add a restart handler. */
-  pr_event_register(&rewrite_module, "core.restart", rewrite_restart_ev,
-    NULL);
-
-  return 0;
-}
-
 /* Module API Tables
  */
 
diff --git a/contrib/mod_sftp/Makefile.in b/contrib/mod_sftp/Makefile.in
index 38ca1ea..e1a5ac4 100644
--- a/contrib/mod_sftp/Makefile.in
+++ b/contrib/mod_sftp/Makefile.in
@@ -10,33 +10,47 @@ SHARED_CFLAGS=-DPR_SHARED_MODULE
 SHARED_LDFLAGS=-avoid-version -export-dynamic -module
 VPATH=@srcdir@
 
+MODULE_LIBS=@MODULE_LIBS@
+
 MODULE_NAME=mod_sftp
-MODULE_OBJS=mod_sftp.o msg.o packet.o cipher.o mac.o umac.o \
+MODULE_OBJS=mod_sftp.o msg.o packet.o cipher.o mac.o umac.o umac128.o \
   compress.o kex.o keys.o crypto.o utf8.o session.o service.o kbdint.o \
   auth-hostbased.o auth-kbdint.o auth-password.o auth-publickey.o auth.o \
   disconnect.o rfc4716.o keystore.o channel.o blacklist.o agent.o \
   interop.o tap.o fxp.o scp.o display.o misc.o date.o
 SHARED_MODULE_OBJS=mod_sftp.lo msg.lo packet.lo cipher.lo mac.lo umac.lo \
-  compress.lo kex.lo keys.lo crypto.lo utf8.lo session.lo service.lo kbdint.lo \
-  auth-hostbased.lo auth-kbdint.lo auth-password.lo auth-publickey.lo auth.lo \
-  disconnect.lo rfc4716.lo keystore.lo channel.lo blacklist.lo agent.lo \
-  interop.lo tap.lo fxp.lo scp.lo display.lo misc.lo date.lo
+  umac128.lo compress.lo kex.lo keys.lo crypto.lo utf8.lo session.lo \
+  service.lo kbdint.lo auth-hostbased.lo auth-kbdint.lo auth-password.lo \
+  auth-publickey.lo auth.lo disconnect.lo rfc4716.lo keystore.lo channel.lo \
+  blacklist.lo agent.lo interop.lo tap.lo fxp.lo scp.lo display.lo misc.lo \
+  date.lo
 
 # Necessary redefinitions
 INCLUDES=-I. -I../.. -I../../include @INCLUDES@
 CPPFLAGS= $(ADDL_CPPFLAGS) -DHAVE_CONFIG_H $(DEFAULT_PATHS) $(PLATFORM) $(INCLUDES)
 LDFLAGS=-L../../lib @LDFLAGS@
 
-.c.o:
+# We special-case the building of umac128.o in order to use preprocessor
+# tricks to get the implementation, rather than making it be all in runtime.
+UMAC128_CPPFLAGS=-DUMAC_OUTPUT_LEN=16 -Dumac_alloc=umac128_alloc -Dumac_init=umac128_init -Dumac_new=umac128_new -Dumac_update=umac128_update -Dumac_final=umac128_final -Dumac_delete=umac128_delete -Dumac_reset=umac128_reset
+
+umac128.o: umac.c
+	$(CC) $(CPPFLAGS) $(CFLAGS) $(UMAC128_CPPFLAGS) -o umac128.o -c umac.c
+
+%.o: %.c
 	$(CC) $(CPPFLAGS) $(CFLAGS) -c $<
 
-.c.lo:
+umac128.lo: umac.c
+	$(LIBTOOL) --mode=compile --tag=CC $(CC) $(CPPFLAGS) $(CFLAGS) $(SHARED_CFLAGS) $(UMAC128_CPPFLAGS) -o umac128.lo -c umac.c
+
+%.lo: %.c
 	$(LIBTOOL) --mode=compile --tag=CC $(CC) $(CPPFLAGS) $(CFLAGS) $(SHARED_CFLAGS) -c $<
 
 shared: $(SHARED_MODULE_OBJS)
-	$(LIBTOOL) --mode=link --tag=CC $(CC) -o $(MODULE_NAME).la $(SHARED_MODULE_OBJS) -rpath $(LIBEXECDIR) $(LDFLAGS) $(SHARED_LDFLAGS) $(SHARED_MODULE_LIBS) `cat $(MODULE_NAME).c | grep '$$Libraries:' | sed -e 's/^.*\$$Libraries: \(.*\)\\$$/\1/'`
+	$(LIBTOOL) --mode=link --tag=CC $(CC) -o $(MODULE_NAME).la $(SHARED_MODULE_OBJS) -rpath $(LIBEXECDIR) $(LDFLAGS) $(SHARED_LDFLAGS) $(MODULE_LIBS) $(SHARED_MODULE_LIBS) `cat $(MODULE_NAME).c | grep '$$Libraries:' | sed -e 's/^.*\$$Libraries: \(.*\)\\$$/\1/'`
 
 static: $(MODULE_OBJS)
+	test -z "$(MODULE_LIBS)" || echo "$(MODULE_LIBS)" >> $(MODULE_LIBS_FILE)
 	$(AR) rc $(MODULE_NAME).a $(MODULE_OBJS)
 	$(RANLIB) $(MODULE_NAME).a
 
@@ -53,5 +67,5 @@ clean:
 	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).a $(MODULE_NAME).la *.o *.lo .libs/*.o
 
 dist: clean
-	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log
+	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log *.gcda *.gcno
 	-$(RM) -r CVS/ RCS/
diff --git a/contrib/mod_sftp/agent.c b/contrib/mod_sftp/agent.c
index 4f94c6d..87aa29e 100644
--- a/contrib/mod_sftp/agent.c
+++ b/contrib/mod_sftp/agent.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp agent
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: agent.c,v 1.5 2012-04-06 16:53:52 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -85,6 +83,7 @@ static unsigned char *agent_request(pool *p, int fd, const char *path,
     unsigned char *req, uint32_t reqlen, uint32_t *resplen) {
   unsigned char msg[AGENT_REQUEST_MSGSZ], *buf, *ptr;
   uint32_t bufsz, buflen;
+  size_t write_len;
   int res;
 
   bufsz = buflen = sizeof(msg);
@@ -94,7 +93,8 @@ static unsigned char *agent_request(pool *p, int fd, const char *path,
 
   /* Send the message length to the agent. */
 
-  res = write(fd, ptr, (bufsz - buflen));
+  write_len = bufsz - buflen;
+  res = write(fd, ptr, write_len);
   if (res < 0) {
     int xerrno = errno;
 
@@ -107,10 +107,10 @@ static unsigned char *agent_request(pool *p, int fd, const char *path,
   }
 
   /* Handle short writes. */
-  if (res != (bufsz - buflen)) {
+  if ((size_t) res != write_len) {
     pr_trace_msg(trace_channel, 3,
       "short write (%d of %lu bytes sent) when talking to SSH agent at '%s'",
-      res, (unsigned long) (bufsz - buflen), path);
+      res, (unsigned long) (write_len), path);
     errno = EIO;
     return NULL;
   }
@@ -130,7 +130,7 @@ static unsigned char *agent_request(pool *p, int fd, const char *path,
   }
 
   /* Handle short writes. */
-  if (res != reqlen) {
+  if ((uint32_t) res != reqlen) {
     pr_trace_msg(trace_channel, 3,
       "short write (%d of %lu bytes sent) when talking to SSH agent at '%s'",
       res, (unsigned long) reqlen, path);
@@ -219,7 +219,11 @@ static int agent_connect(const char *path) {
     return -1;
   }
 
-  fcntl(fd, F_SETFD, FD_CLOEXEC);
+  if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error setting CLOEXEC on fd %d for talking to SSH agent: %s",
+      fd, strerror(errno));
+  }
 
   PRIVS_ROOT
   res = connect(fd, (struct sockaddr *) &sock, len);
diff --git a/contrib/mod_sftp/agent.h b/contrib/mod_sftp/agent.h
index 4540dd4..45fe7b3 100644
--- a/contrib/mod_sftp/agent.h
+++ b/contrib/mod_sftp/agent.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp SSH agent interaction
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: agent.h,v 1.2 2012-03-06 07:01:32 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_AGENT_H
 #define MOD_SFTP_AGENT_H
 
+#include "mod_sftp.h"
+
 struct agent_key {
   unsigned char *key_data;
   uint32_t key_datalen;
@@ -39,4 +37,4 @@ int sftp_agent_get_keys(pool *p, const char *, array_header *);
 const unsigned char *sftp_agent_sign_data(pool *, const char *,
   const unsigned char *, uint32_t, const unsigned char *, uint32_t, uint32_t *);
 
-#endif
+#endif /* MOD_SFTP_AGENT_H */
diff --git a/contrib/mod_sftp/auth-hostbased.c b/contrib/mod_sftp/auth-hostbased.c
index c2cbaa7..c06e399 100644
--- a/contrib/mod_sftp/auth-hostbased.c
+++ b/contrib/mod_sftp/auth-hostbased.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp 'hostbased' user authentication
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: auth-hostbased.c,v 1.10 2012-03-13 18:58:48 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -43,17 +41,22 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
     unsigned char **buf, uint32_t *buflen, int *send_userauth_fail) {
   struct passwd *pw;
   char *hostkey_algo, *host_fqdn, *host_user, *host_user_utf8;
-  const char *fp = NULL;
+  const char *fp = NULL, *fp_algo = NULL;
   unsigned char *hostkey_data, *signature_data;
   unsigned char *buf2, *ptr2;
   const unsigned char *id;
   uint32_t buflen2, bufsz2, hostkey_datalen, id_len, signature_len;
   enum sftp_key_type_e pubkey_type;
+  int fp_algo_id;
 
   if (pr_cmd_dispatch_phase(pass_cmd, PRE_CMD, 0) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "authentication request for user '%s' blocked by '%s' handler",
-      orig_user, pass_cmd->argv[0]);
+      orig_user, (char *) pass_cmd->argv[0]);
+
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): blocked by '%s' handler", orig_user,
+      (char *) pass_cmd->argv[0]);
 
     pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
     pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -64,6 +67,14 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
   }
 
   hostkey_algo = sftp_msg_read_string(pkt->pool, buf, buflen);
+  if (hostkey_algo == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "missing required host key algorithm, rejecting request");
+
+    *send_userauth_fail = TRUE;
+    errno = EINVAL;
+    return 0;
+  }
 
   hostkey_datalen = sftp_msg_read_int(pkt->pool, buf, buflen);
   hostkey_data = sftp_msg_read_data(pkt->pool, buf, buflen, hostkey_datalen);
@@ -104,6 +115,10 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
       "unsupported host key algorithm '%s' requested, rejecting request",
       hostkey_algo);
 
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): unsupported host key algorithm '%s' requested",
+      user, hostkey_algo);
+
     *send_userauth_fail = TRUE;
     errno = EINVAL;
     return 0;
@@ -122,28 +137,46 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
 #ifdef OPENSSL_FIPS
   if (FIPS_mode()) {
+# if defined(HAVE_SHA256_OPENSSL)
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_SHA256;
+    fp_algo = "SHA256";
+# else
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_SHA1;
+    fp_algo = "SHA1";
+# endif /* HAVE_SHA256_OPENSSL */
+
     fp = sftp_keys_get_fingerprint(pkt->pool, hostkey_data, hostkey_datalen,
-      SFTP_KEYS_FP_DIGEST_SHA1);
+      fp_algo_id);
     if (fp != NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "public key SHA1 fingerprint: %s", fp);
+        "public key %s fingerprint: %s", fp_algo, fp);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error obtaining public key SHA1 fingerprint: %s", strerror(errno));
+        "error obtaining public key %s fingerprint: %s", fp_algo,
+        strerror(errno));
     }
 
   } else {
 #endif /* OPENSSL_FIPS */
+#if defined(HAVE_SHA256_OPENSSL)
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_SHA256;
+    fp_algo = "SHA256";
+#else
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_MD5;
+    fp_algo = "MD5";
+#endif /* HAVE_SHA256_OPENSSL */
+
     fp = sftp_keys_get_fingerprint(pkt->pool, hostkey_data, hostkey_datalen,
-      SFTP_KEYS_FP_DIGEST_MD5);
+      fp_algo_id);
     if (fp != NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "public key MD5 fingerprint: %s", fp);
+        "public key %s fingerprint: %s", fp_algo, fp);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error obtaining public key MD5 fingerprint: %s", strerror(errno));
+        "error obtaining public key %s fingerprint: %s", fp_algo,
+        strerror(errno));
     }
 #ifdef OPENSSL_FIPS
   }
@@ -170,6 +203,9 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
    */
 
   if (sftp_blacklist_reject_key(pkt->pool, hostkey_data, hostkey_datalen)) {
+    pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): requested host "
+      "key is blacklisted", user);
+
     *send_userauth_fail = TRUE;
     errno = EACCES;
     return 0;
@@ -182,6 +218,9 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
   if (sftp_keystore_verify_host_key(pkt->pool, user, host_fqdn, host_user,
       hostkey_data, hostkey_datalen) < 0) {
+    pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): authentication "
+      "via '%s' host key failed", user, hostkey_algo);
+
     *send_userauth_fail = TRUE;
     errno = EACCES;
     return 0;
@@ -218,7 +257,12 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "failed to verify '%s' signature on hostbased auth request for "
       "user '%s', host %s", hostkey_algo, orig_user, host_fqdn);
+
+    pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): signature "
+      "verification of '%s' host key failed", user, hostkey_algo);
+
     *send_userauth_fail = TRUE;
+    errno = EACCES;
     return 0;
   }
 
@@ -239,3 +283,7 @@ int sftp_auth_hostbased(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
   return 1;
 }
+
+int sftp_auth_hostbased_init(pool *p) {
+  return 0;
+}
diff --git a/contrib/mod_sftp/auth-kbdint.c b/contrib/mod_sftp/auth-kbdint.c
index de0f0e1..c004e8b 100644
--- a/contrib/mod_sftp/auth-kbdint.c
+++ b/contrib/mod_sftp/auth-kbdint.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp 'keyboard-interactive' user authentication
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2015 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: auth-kbdint.c,v 1.10 2014-03-02 22:05:43 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -34,6 +32,14 @@
 #include "utf8.h"
 #include "kbdint.h"
 
+/* This array tracks the names of kbdint drivers that have successfully
+ * authenticated the user.  In any given session, the same kbdint driver CANNOT
+ * be used twice for authentication; this supports authentication chains
+ * like "keyboard-interactive+keyboard-interactive", requiring clients to
+ * authenticate using multiple different kbdint means.
+ */
+static array_header *kbdint_drivers = NULL;
+
 static const char *trace_channel = "ssh2";
 
 int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
@@ -50,6 +56,10 @@ int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
       "no 'keyboard-interactive' drivers currently registered, unable to "
       "authenticate user '%s' via 'keyboard-interactive' method", user);
 
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): keyboard-interactive authentication disabled",
+      user);
+
     *send_userauth_fail = TRUE;
     errno = EPERM;
     return 0;
@@ -58,7 +68,11 @@ int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
   if (pr_cmd_dispatch_phase(pass_cmd, PRE_CMD, 0) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "authentication request for user '%s' blocked by '%s' handler",
-      orig_user, pass_cmd->argv[0]);
+      orig_user, (char *) pass_cmd->argv[0]);
+
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): blocked by '%s' handler", orig_user,
+      (char *) pass_cmd->argv[0]);
 
     pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
     pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -103,6 +117,12 @@ int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
         "cipher algorithm '%s' or MAC algorithm '%s' unacceptable for "
         "keyboard-interactive authentication, denying authentication request",
         cipher_algo, mac_algo);
+
+      pr_log_auth(PR_LOG_NOTICE,
+        "USER %s (Login failed): cipher algorithm '%s' or MAC algorithm '%s' "
+        "unsupported for keyboard-interactive authentication", user,
+        cipher_algo, mac_algo);
+
       *send_userauth_fail = TRUE;
       errno = EPERM;
       return 0;
@@ -125,9 +145,31 @@ int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
    */
 
   driver = sftp_kbdint_first_driver();
-  while (driver) {
+  while (driver != NULL) {
+    register unsigned int i;
+    int skip_driver = FALSE;
+
     pr_signals_handle();
 
+    /* If this driver has already successfully handled this user, skip it. */
+    for (i = 0; i < kbdint_drivers->nelts; i++) {
+      char *dri;
+
+      dri = ((char **) kbdint_drivers->elts)[i];
+      if (strcmp(driver->driver_name, dri) == 0) {
+        skip_driver = TRUE;
+        break;
+      }
+    }
+
+    if (skip_driver) {
+      pr_trace_msg(trace_channel, 9,
+        "skipping already-used kbdint driver '%s' for user '%s'",
+        driver->driver_name, user);
+      driver = sftp_kbdint_next_driver();
+      continue;
+    }
+
     pr_trace_msg(trace_channel, 3, "trying kbdint driver '%s' for user '%s'",
       driver->driver_name, user);
 
@@ -140,8 +182,12 @@ int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
     res = driver->authenticate(driver, user);
     driver->close(driver);
 
-    if (res == 0)
+    if (res == 0) {
+      /* Store the driver name for future checking. */
+      *((char **) push_array(kbdint_drivers)) = pstrdup(sftp_pool,
+        driver->driver_name);
       break; 
+    }
 
     driver = sftp_kbdint_next_driver();
   }
@@ -160,3 +206,9 @@ int sftp_auth_kbdint(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
   return 1;
 }
+
+int sftp_auth_kbdint_init(pool *p) {
+  kbdint_drivers = make_array(p, 0, sizeof(char *));
+
+  return 0;
+}
diff --git a/contrib/mod_sftp/auth-password.c b/contrib/mod_sftp/auth-password.c
index a92df6f..2605af7 100644
--- a/contrib/mod_sftp/auth-password.c
+++ b/contrib/mod_sftp/auth-password.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp 'password' user authentication
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2015 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: auth-password.c,v 1.10 2014-03-02 22:05:43 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -57,6 +55,12 @@ int sftp_auth_password(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
         "cipher algorithm '%s' or MAC algorithm '%s' unacceptable for "
         "password authentication, denying password authentication request",
         cipher_algo, mac_algo);
+
+      pr_log_auth(PR_LOG_NOTICE,
+        "USER %s (Login failed): cipher algorithm '%s' or MAC algorithm '%s' "
+        "unsupported for password authentication", user,
+        cipher_algo, mac_algo);
+
       *send_userauth_fail = TRUE;
       errno = EPERM;
       return 0;
@@ -79,7 +83,11 @@ int sftp_auth_password(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
   if (pr_cmd_dispatch_phase(pass_cmd, PRE_CMD, 0) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "authentication request for user '%s' blocked by '%s' handler",
-      orig_user, pass_cmd->argv[0]);
+      orig_user, (char *) pass_cmd->argv[0]);
+
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): blocked by '%s' handler", orig_user,
+      (char *) pass_cmd->argv[0]);
 
     pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
     pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -164,3 +172,7 @@ int sftp_auth_password(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
   return 1;
 }
+
+int sftp_auth_password_init(pool *p) {
+  return 0;
+}
diff --git a/contrib/mod_sftp/auth-publickey.c b/contrib/mod_sftp/auth-publickey.c
index 1caec4d..94f504a 100644
--- a/contrib/mod_sftp/auth-publickey.c
+++ b/contrib/mod_sftp/auth-publickey.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp 'publickey' user authentication
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: auth-publickey.c,v 1.13 2012-07-10 00:52:20 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -35,6 +33,14 @@
 #include "interop.h"
 #include "blacklist.h"
 
+/* This array tracks the fingerprints of publickeys that have been
+ * successfully verified.  In any given session, the same publickey CANNOT
+ * be used twice for authentication; this supports authentication chains
+ * like "publickey+publickey", requiring clients to authenticate using multiple
+ * different publickeys.
+ */
+static array_header *publickey_fps = NULL;
+
 static const char *trace_channel = "ssh2";
 
 static int send_pubkey_ok(const char *algo, const unsigned char *pubkey_data,
@@ -76,18 +82,23 @@ static int send_pubkey_ok(const char *algo, const unsigned char *pubkey_data,
 int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
     const char *orig_user, const char *user, const char *service,
     unsigned char **buf, uint32_t *buflen, int *send_userauth_fail) {
-  int have_signature, res;
+  register unsigned int i;
+  int fp_algo_id = 0, have_signature, res;
   enum sftp_key_type_e pubkey_type;
   unsigned char *pubkey_data;
   char *pubkey_algo = NULL;
-  const char *fp = NULL;
+  const char *fp = NULL, *fp_algo = NULL;
   uint32_t pubkey_len;
   struct passwd *pw;
 
   if (pr_cmd_dispatch_phase(pass_cmd, PRE_CMD, 0) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "authentication request for user '%s' blocked by '%s' handler",
-      orig_user, pass_cmd->argv[0]);
+      orig_user, (char *) pass_cmd->argv[0]);
+
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): blocked by '%s' handler", orig_user,
+      (char *) pass_cmd->argv[0]);
 
     pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
     pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -145,6 +156,10 @@ int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
       "unsupported public key algorithm '%s' requested, rejecting request",
       pubkey_algo);
 
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): unsupported public key algorithm '%s' requested",
+      user, pubkey_algo);
+
     *send_userauth_fail = TRUE;
     errno = EINVAL;
     return 0;
@@ -172,33 +187,76 @@ int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
 #ifdef OPENSSL_FIPS
   if (FIPS_mode()) {
+# if defined(HAVE_SHA256_OPENSSL)
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_SHA256;
+    fp_algo = "SHA256";
+# else
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_SHA1;
+    fp_algo = "SHA1";
+# endif /* HAVE_SHA256_OPENSSL */
+
     fp = sftp_keys_get_fingerprint(pkt->pool, pubkey_data, pubkey_len,
-      SFTP_KEYS_FP_DIGEST_SHA1);
+      fp_algo_id);
     if (fp != NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "public key SHA1 fingerprint: %s", fp);
+        "public key %s fingerprint: %s", fp_algo, fp);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error obtaining public key SHA1 fingerprint: %s", strerror(errno));
+        "error obtaining public key %s fingerprint: %s", fp_algo,
+        strerror(errno));
+      fp_algo = NULL;
     }
 
   } else {
 #endif /* OPENSSL_FIPS */
+#if defined(HAVE_SHA256_OPENSSL)
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_SHA256;
+    fp_algo = "SHA256";
+#else
+    fp_algo_id = SFTP_KEYS_FP_DIGEST_MD5;
+    fp_algo = "MD5";
+#endif /* HAVE_SHA256_OPENSSL */
+
     fp = sftp_keys_get_fingerprint(pkt->pool, pubkey_data, pubkey_len,
-      SFTP_KEYS_FP_DIGEST_MD5);
+      fp_algo_id);
     if (fp != NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "public key MD5 fingerprint: %s", fp);
+        "public key %s fingerprint: %s", fp_algo, fp);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error obtaining public key MD5 fingerprint: %s", strerror(errno));
+        "error obtaining public key %s fingerprint: %s", fp_algo,
+        strerror(errno));
+      fp_algo = NULL;
     }
 #ifdef OPENSSL_FIPS
   }
 #endif /* OPENSSL_FIPS */
 
+  if (fp != NULL) {
+    const char *k, *v;
+
+    /* Log the fingerprint (and fingerprinting algorithm used), for
+     * debugging/auditing; make it available via environment variable as well.
+     */
+
+    k = pstrdup(session.pool, "SFTP_USER_PUBLICKEY_ALGO");
+    v = pstrdup(session.pool, pubkey_algo);
+    pr_env_unset(session.pool, k);
+    pr_env_set(session.pool, k, v);
+
+    k = pstrdup(session.pool, "SFTP_USER_PUBLICKEY_FINGERPRINT");
+    v = pstrdup(session.pool, fp);
+    pr_env_unset(session.pool, k);
+    pr_env_set(session.pool, k, v);
+
+    k = pstrdup(session.pool, "SFTP_USER_PUBLICKEY_FINGERPRINT_ALGO");
+    v = pstrdup(session.pool, fp_algo);
+    pr_env_unset(session.pool, k);
+    pr_env_set(session.pool, k, v);
+  }
+
   pw = pr_auth_getpwnam(pkt->pool, user);
   if (pw == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -236,6 +294,9 @@ int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
      */
 
     if (sftp_blacklist_reject_key(pkt->pool, pubkey_data, pubkey_len)) {
+      pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): requested public "
+        "key is blacklisted", user);
+
       *send_userauth_fail = TRUE;
       errno = EPERM;
       return 0;
@@ -261,6 +322,9 @@ int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
 
     if (sftp_keystore_verify_user_key(pkt->pool, user, pubkey_data,
         pubkey_len) < 0) {
+      pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): authentication "
+        "via '%s' public key failed", user, pubkey_algo);
+
       *send_userauth_fail = TRUE;
       errno = EACCES;
       return 0;
@@ -304,6 +368,10 @@ int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "failed to verify '%s' signature on public key auth request for "
         "user '%s'", pubkey_algo, orig_user);
+
+      pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): signature "
+        "verification of '%s' public key failed", user, pubkey_algo);
+
       *send_userauth_fail = TRUE;
       errno = EACCES;
       return 0;
@@ -325,5 +393,35 @@ int sftp_auth_publickey(struct ssh2_packet *pkt, cmd_rec *pass_cmd,
     return 0;
   }
 
+  /* Check the key fingerprint against any previously used keys, to see if the
+   * same key is being reused.
+   */
+  for (i = 0; i < publickey_fps->nelts; i++) {
+    char *fpi;
+
+    fpi = ((char **) publickey_fps->elts)[i];
+    if (strcmp(fp, fpi) == 0) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "publickey request reused previously verified publickey "
+        "(fingerprint %s), rejecting", fp);
+
+      pr_log_auth(PR_LOG_NOTICE, "USER %s (Login failed): public key request "
+       "reused previously verified public key (fingerprint %s)", user, fp);
+
+      *send_userauth_fail = TRUE;
+      errno = EACCES;
+      return 0;
+    }
+  }
+
+  /* Store the fingerprint for future checking. */
+  *((char **) push_array(publickey_fps)) = pstrdup(sftp_pool, fp);
+
   return 1;
 }
+
+int sftp_auth_publickey_init(pool *p) {
+  publickey_fps = make_array(p, 0, sizeof(char *));
+
+  return 0;
+}
diff --git a/contrib/mod_sftp/auth.c b/contrib/mod_sftp/auth.c
index cb70698..d0e389f 100644
--- a/contrib/mod_sftp/auth.c
+++ b/contrib/mod_sftp/auth.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp user authentication
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: auth.c,v 1.53 2014-03-04 07:54:12 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -51,9 +49,15 @@ static unsigned int auth_attempts = 0;
 
 static pool *auth_pool = NULL;
 static char *auth_default_dir = NULL;
+
 static const char *auth_avail_meths = NULL;
-static const char *auth_remaining_meths = NULL;
-static unsigned int auth_meths_enabled = 0;
+
+/* This is a bitset of flags, used to check on the authentication method
+ * used by the client, to see if the requested method is even enabled.
+ */
+static unsigned int auth_meths_enabled_flags = 0;
+
+static array_header *auth_chains = NULL;
 
 static int auth_sent_userauth_banner_file = FALSE;
 static int auth_sent_userauth_success = FALSE;
@@ -85,12 +89,12 @@ static void ensure_open_passwd(pool *p) {
   pr_auth_getgrent(p);
 }
 
-static char *get_default_chdir(pool *p) {
+static const char *get_default_chdir(pool *p) {
   config_rec *c;
-  char *path = NULL;
+  const char *path = NULL;
 
   c = find_config(main_server->conf, CONF_PARAM, "DefaultChdir", FALSE);
-  while (c) {
+  while (c != NULL) {
     int res;
 
     pr_signals_handle();
@@ -115,16 +119,16 @@ static char *get_default_chdir(pool *p) {
     path = pdircat(p, session.cwd, path, NULL);
   }
 
-  if (path) {
+  if (path != NULL) {
     path = path_subst_uservar(p, &path);
   }
 
   return path;
 }
 
-static char *get_default_root(pool *p) {
+static const char *get_default_root(pool *p) {
   config_rec *c;
-  char *path = NULL;
+  const char *path = NULL;
 
   c = find_config(main_server->conf, CONF_PARAM, "DefaultRoot", FALSE);
   while (c) {
@@ -185,26 +189,34 @@ static char *get_default_root(pool *p) {
 
 static void set_userauth_methods(void) {
   config_rec *c;
+  register unsigned int i;
 
-  if (auth_meths_enabled > 0) {
-    /* No need to do the lookup if we've already done it. */
+  if (auth_chains != NULL) {
     return;
   }
 
-  auth_avail_meths = auth_remaining_meths = NULL;
-  auth_meths_enabled = 0;
+  auth_avail_meths = NULL;
+  auth_meths_enabled_flags = 0;
 
   c = find_config(main_server->conf, CONF_PARAM, "SFTPAuthMethods", FALSE);
-  if (c) {
-    auth_avail_meths = auth_remaining_meths = c->argv[0];
-    auth_meths_enabled = *((unsigned int *) c->argv[1]);
+  if (c != NULL) {
+    /* Sanity checking of the configured methods is done in the postparse
+     * event listener; we can use this as-is without fear.
+     */
+    auth_chains = c->argv[0];
 
   } else {
+    struct sftp_auth_chain *auth_chain;
+
+    auth_chains = make_array(auth_pool, 0, sizeof(struct sftp_auth_chain *));
+
     c = find_config(main_server->conf, CONF_PARAM, "SFTPAuthorizedUserKeys",
       FALSE);
-    if (c) {
-      auth_avail_meths = "publickey";
-      auth_meths_enabled |= SFTP_AUTH_FL_METH_PUBLICKEY;
+    if (c != NULL) {
+      auth_chain = sftp_auth_chain_alloc(auth_pool);
+      sftp_auth_chain_add_method(auth_chain, SFTP_AUTH_FL_METH_PUBLICKEY,
+        "publickey", NULL);
+      *((struct sftp_auth_chain **) push_array(auth_chains)) = auth_chain;
 
     } else {
       pr_trace_msg(trace_channel, 9, "no SFTPAuthorizedUserKeys configured, "
@@ -213,16 +225,11 @@ static void set_userauth_methods(void) {
 
     c = find_config(main_server->conf, CONF_PARAM, "SFTPAuthorizedHostKeys",
       FALSE);
-    if (c) {
-      if (auth_avail_meths) {
-        auth_avail_meths = pstrcat(auth_pool, auth_avail_meths, ",hostbased",
-          NULL);
-
-      } else {
-        auth_avail_meths = "hostbased";
-      }
-
-      auth_meths_enabled |= SFTP_AUTH_FL_METH_HOSTBASED;
+    if (c != NULL) {
+      auth_chain = sftp_auth_chain_alloc(auth_pool);
+      sftp_auth_chain_add_method(auth_chain, SFTP_AUTH_FL_METH_HOSTBASED,
+        "hostbased", NULL);
+      *((struct sftp_auth_chain **) push_array(auth_chains)) = auth_chain;
 
     } else {
       pr_trace_msg(trace_channel, 9, "no SFTPAuthorizedHostKeys configured, "
@@ -230,15 +237,10 @@ static void set_userauth_methods(void) {
     }
 
     if (sftp_kbdint_have_drivers() > 0) {
-      if (auth_avail_meths) {
-        auth_avail_meths = pstrcat(auth_pool, auth_avail_meths,
-          ",keyboard-interactive", NULL);
-
-      } else {
-        auth_avail_meths = "keyboard-interactive";
-      }
-
-      auth_meths_enabled |= SFTP_AUTH_FL_METH_KBDINT;
+      auth_chain = sftp_auth_chain_alloc(auth_pool);
+      sftp_auth_chain_add_method(auth_chain, SFTP_AUTH_FL_METH_KBDINT,
+        "keyboard-interactive", NULL);
+      *((struct sftp_auth_chain **) push_array(auth_chains)) = auth_chain;
 
     } else {
       pr_trace_msg(trace_channel, 9, "no kbdint drivers present, not "
@@ -246,35 +248,80 @@ static void set_userauth_methods(void) {
     }
 
     /* The 'password' method is always available. */
-    if (auth_avail_meths) {
-      auth_avail_meths = pstrcat(auth_pool, auth_avail_meths, ",password",
-        NULL);
+    auth_chain = sftp_auth_chain_alloc(auth_pool);
+    sftp_auth_chain_add_method(auth_chain, SFTP_AUTH_FL_METH_PASSWORD,
+      "password", NULL);
+    *((struct sftp_auth_chain **) push_array(auth_chains)) = auth_chain;
+  }
 
-    } else {
-      auth_avail_meths = "password";
-    }
+  for (i = 0 ; i < auth_chains->nelts; i++) {
+    struct sftp_auth_chain *auth_chain;
+    struct sftp_auth_method *meth;
+
+    auth_chain = ((struct sftp_auth_chain **) auth_chains->elts)[i];
+    meth = ((struct sftp_auth_method **) auth_chain->methods->elts)[0];
 
-    auth_meths_enabled |= SFTP_AUTH_FL_METH_PASSWORD;
+    if (!(auth_meths_enabled_flags & meth->method_id)) {
+      auth_meths_enabled_flags |= meth->method_id;
 
-    auth_remaining_meths = pstrdup(auth_pool, auth_avail_meths);
+      if (auth_avail_meths != NULL) {
+        auth_avail_meths = pstrcat(auth_pool, auth_avail_meths, ",",
+          meth->method_name, NULL);
+      } else {
+        auth_avail_meths = meth->method_name;
+      }
+    }
   }
 
   pr_trace_msg(trace_channel, 9, "offering authentication methods: %s",
     auth_avail_meths);
+
+  /* Prepare the method-specific APIs, too. */
+  if (sftp_auth_hostbased_init(auth_pool) < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error preparing for 'hostbased' authentication: %s", strerror(errno));
+  }
+
+  if (sftp_auth_kbdint_init(auth_pool) < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error preparing for 'keyboard-interactive' authentication: %s",
+      strerror(errno));
+  }
+
+  if (sftp_auth_password_init(auth_pool) < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error preparing for 'password' authentication: %s", strerror(errno));
+  }
+
+  if (sftp_auth_publickey_init(auth_pool) < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error preparing for 'publickey' authentication: %s", strerror(errno));
+  }
 }
 
 static int setup_env(pool *p, char *user) {
   struct passwd *pw;
   config_rec *c;
-  int login_acl, i, res, show_symlinks = FALSE, xerrno;
+  int login_acl, i, res, root_revoke = TRUE, show_symlinks = FALSE, xerrno;
   struct stat st;
-  char *default_chdir, *default_root, *home_dir;
+  const char *default_chdir, *default_root, *home_dir;
   const char *sess_ttyname = NULL, *xferlog = NULL;
   cmd_rec *cmd;
 
   session.hide_password = TRUE;
 
   pw = pr_auth_getpwnam(p, user);
+  if (pw == NULL) {
+    xerrno = errno;
+
+    /* This is highly unlikely to happen...*/
+    pr_log_auth(PR_LOG_NOTICE,
+      "USER %s (Login failed): Unable to retrieve user information: %s", user,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
 
   pw = dup_passwd(p, pw);
 
@@ -329,7 +376,7 @@ static int setup_env(pool *p, char *user) {
    * incorrect value (Bug#3421).
    */
 
-  pw->pw_dir = path_subst_uservar(p, &pw->pw_dir);
+  pw->pw_dir = (char *) path_subst_uservar(p, (const char **) &pw->pw_dir);
 
   if (session.gids == NULL &&
       session.groups == NULL) {
@@ -351,7 +398,7 @@ static int setup_env(pool *p, char *user) {
   home_dir = dir_realpath(p, pw->pw_dir);
   PRIVS_RELINQUISH
 
-  if (home_dir) {
+  if (home_dir != NULL) {
     sstrncpy(session.cwd, home_dir, sizeof(session.cwd));
 
   } else {
@@ -359,7 +406,7 @@ static int setup_env(pool *p, char *user) {
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "CreateHome", FALSE);
-  if (c) {
+  if (c != NULL) {
     if (*((unsigned char *) c->argv[0]) == TRUE) {
       if (create_home(p, session.cwd, user, pw->pw_uid, pw->pw_gid) < 0) {
         return -1;
@@ -368,7 +415,7 @@ static int setup_env(pool *p, char *user) {
   }
 
   default_chdir = get_default_chdir(p);
-  if (default_chdir) {
+  if (default_chdir != NULL) {
     default_chdir = dir_abs_path(p, default_chdir, TRUE);
     sstrncpy(session.cwd, default_chdir, sizeof(session.cwd));
   }
@@ -458,8 +505,10 @@ static int setup_env(pool *p, char *user) {
   PRIVS_RELINQUISH
 
   if (res < 0) {
-    pr_log_pri(PR_LOG_WARNING, "unable to set process groups: %s",
-      strerror(xerrno));
+    if (xerrno != ENOSYS) {
+      pr_log_pri(PR_LOG_WARNING, "unable to set process groups: %s",
+        strerror(xerrno));
+    }
   }
 
   default_root = get_default_root(session.pool);
@@ -493,12 +542,27 @@ static int setup_env(pool *p, char *user) {
 
   /* Should we give up root privs completely here? */
   c = find_config(main_server->conf, CONF_PARAM, "RootRevoke", FALSE);
-  if (c != NULL &&
-      *((int *) c->argv[0]) == FALSE) {
-    pr_log_debug(DEBUG8, MOD_SFTP_VERSION
-      ": retaining root privileges per RootRevoke setting");
+  if (c != NULL) {
+    root_revoke = *((int *) c->argv[0]);
+
+    if (root_revoke == FALSE) {
+      pr_log_debug(DEBUG8, MOD_SFTP_VERSION
+        ": retaining root privileges per RootRevoke setting");
+    }
 
   } else {
+    /* Do a recursive look for any UserOwner directives; honoring that
+     * configuration also requires root privs.
+     */
+    c = find_config(main_server->conf, CONF_PARAM, "UserOwner", TRUE);
+    if (c != NULL) {
+      pr_log_debug(DEBUG9, MOD_SFTP_VERSION
+        ": retaining root privileges per UserOwner setting");
+      root_revoke = FALSE;
+    }
+  }
+
+  if (root_revoke) {
     PRIVS_ROOT
     PRIVS_REVOKE
     session.disable_id_switching = TRUE;
@@ -575,7 +639,7 @@ static int setup_env(pool *p, char *user) {
 
   /* Make sure directory config pointers are set correctly */
   cmd = pr_cmd_alloc(p, 1, C_PASS);
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
   cmd->arg = "";
   dir_check_full(p, cmd, G_NONE, session.cwd, NULL);
 
@@ -592,7 +656,7 @@ static int setup_env(pool *p, char *user) {
     build_dyn_config(p, session.cwd, &st, TRUE);
   }
 
-  pr_scoreboard_update_entry(session.pid,
+  pr_scoreboard_entry_update(session.pid,
     PR_SCORE_USER, session.user,
     PR_SCORE_CWD, session.cwd,
     NULL);
@@ -721,35 +785,54 @@ static int send_userauth_failure(char *failed_meth) {
   pkt = sftp_ssh2_packet_create(auth_pool);
 
   if (failed_meth) {
-    meths = pstrdup(pkt->pool, auth_remaining_meths);
-    meths = sreplace(pkt->pool, meths, failed_meth, "", NULL);
+    register unsigned int i;
 
-    if (*meths == ',') {
-      meths++;
-    }
+    auth_avail_meths = NULL;
+    auth_meths_enabled_flags = 0;
 
-    if (meths[strlen(meths)-1] == ',') {
-      meths[strlen(meths)-1] = '\0';
-    }
+    for (i = 0 ; i < auth_chains->nelts; i++) {
+      register unsigned int j;
+      struct sftp_auth_chain *auth_chain;
+      struct sftp_auth_method *meth = NULL;
 
-    if (strstr(meths, ",,") != NULL) {
-      meths = sreplace(pkt->pool, meths, ",,", ",", NULL);
-    }
+      pr_signals_handle();
+      auth_chain = ((struct sftp_auth_chain **) auth_chains->elts)[i];
 
-    if (strncmp(failed_meth, "publickey", 10) == 0) {
-      auth_meths_enabled &= ~SFTP_AUTH_FL_METH_PUBLICKEY;
+      for (j = 0; j < auth_chain->methods->nelts; j++) {
+        struct sftp_auth_method *m;
 
-    } else if (strncmp(failed_meth, "hostbased", 10) == 0) {
-      auth_meths_enabled &= ~SFTP_AUTH_FL_METH_HOSTBASED;
+        m = ((struct sftp_auth_method **) auth_chain->methods->elts)[j];
+        if (m->succeeded != TRUE &&
+            m->failed != TRUE) {
+          meth = m;
+          break;
+        }
+      }
+
+      if (meth == NULL) {
+        /* All of the methods in this list have failed; check the next
+         * list.
+         */
+        continue;
+      }
 
-    } else if (strncmp(failed_meth, "password", 9) == 0) {
-      auth_meths_enabled &= ~SFTP_AUTH_FL_METH_PASSWORD;
+      if (strcmp(meth->method_name, failed_meth) != 0) {
+        if (!(auth_meths_enabled_flags & meth->method_id)) {
+          auth_meths_enabled_flags |= meth->method_id;
 
-    } else if (strncmp(failed_meth, "keyboard-interactive", 21) == 0) {
-      auth_meths_enabled &= ~SFTP_AUTH_FL_METH_KBDINT;
+          if (auth_avail_meths != NULL) {
+            auth_avail_meths = pstrcat(auth_pool, auth_avail_meths, ",",
+              meth->method_name, NULL);
+          } else {
+            auth_avail_meths = meth->method_name;
+          }
+        }
+      } else {
+        meth->failed = TRUE;
+      }
     }
 
-    if (strlen(meths) == 0) {
+    if (auth_avail_meths == NULL) {
       /* If there are no more auth methods available, we have to disconnect. */
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "no more auth methods available, disconnecting");
@@ -757,12 +840,20 @@ static int send_userauth_failure(char *failed_meth) {
         NULL);
     }
 
-    auth_remaining_meths = pstrdup(auth_pool, meths);
-
   } else {
-    meths = pstrdup(pkt->pool, auth_avail_meths);
+    if (auth_avail_meths == NULL) {
+      /* This situation can happen when the authentication method succeeded,
+       * BUT the subsequent login actions failed (such as failing to chroot,
+       * no such home directory, etc etc).  But we still need a list
+       * of authentication methods to send back to the client in this message;
+       * we thus use the empty string.
+       */
+      auth_avail_meths = "";
+    }
   }
 
+  meths = pstrdup(pkt->pool, auth_avail_meths);
+
   buflen = bufsz;
   ptr = buf = palloc(pkt->pool, bufsz);
 
@@ -825,7 +916,99 @@ static int send_userauth_success(void) {
   return 0;
 }
 
-static int send_userauth_methods(void) {
+static int set_userauth_success(const char *succeeded_meth) {
+  register unsigned int i;
+  int completed = FALSE;
+
+  auth_avail_meths = NULL;
+  auth_meths_enabled_flags = 0;
+
+  for (i = 0 ; i < auth_chains->nelts; i++) {
+    register unsigned int j;
+    struct sftp_auth_chain *auth_chain;
+
+    pr_signals_handle();
+    auth_chain = ((struct sftp_auth_chain **) auth_chains->elts)[i];
+
+    for (j = 0; j < auth_chain->methods->nelts; j++) {
+      struct sftp_auth_method *meth = NULL;
+
+      meth = ((struct sftp_auth_method **) auth_chain->methods->elts)[j];
+      if (meth->succeeded != TRUE &&
+          meth->failed != TRUE) {
+
+        if (strcmp(meth->method_name, succeeded_meth) == 0) {
+          /* TODO: What about submethods, for kbdint drivers? */
+          meth->succeeded = TRUE;
+        }
+
+        /* Add the next method in the list (if any) to the available methods. */
+        j++;
+        if (j < auth_chain->methods->nelts) {
+          meth = ((struct sftp_auth_method **) auth_chain->methods->elts)[j];
+
+          if (!(auth_meths_enabled_flags & meth->method_id)) {
+            auth_meths_enabled_flags |= meth->method_id;
+
+            if (auth_avail_meths != NULL) {
+              auth_avail_meths = pstrcat(auth_pool, auth_avail_meths, ",",
+                meth->method_name, NULL);
+            } else {
+              auth_avail_meths = meth->method_name;
+            }
+          }
+        }
+
+        break;
+      }
+    }
+  }
+
+  /* Now, having marked the list items that have succeeded, check each list
+   * to see if any has been completed.
+   */
+
+  for (i = 0 ; i < auth_chains->nelts; i++) {
+    register unsigned int j;
+    struct sftp_auth_chain *auth_chain;
+    int ok = TRUE;
+
+    pr_signals_handle();
+    auth_chain = ((struct sftp_auth_chain **) auth_chains->elts)[i];
+
+    for (j = 0; j < auth_chain->methods->nelts; j++) {
+      struct sftp_auth_method *meth = NULL;
+
+      meth = ((struct sftp_auth_method **) auth_chain->methods->elts)[j];
+      if (meth->succeeded != TRUE) {
+        ok = FALSE;
+        break;
+      }
+    }
+
+    if (ok == TRUE) {
+      /* We have a successfully completed list! */
+      auth_chain->completed = completed = TRUE;
+      break;
+    }
+  }
+
+  if (completed == TRUE) {
+    return 1;
+  }
+
+  if (auth_avail_meths == NULL) {
+    /* If there are no more auth methods available, we have to disconnect. */
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "no more auth methods available, disconnecting");
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
+      NULL);
+  }
+
+  return 0;
+}
+
+static int send_userauth_methods(char partial_success) {
   struct ssh2_packet *pkt;
   unsigned char *buf, *ptr;
   uint32_t buflen, bufsz = 1024;
@@ -836,17 +1019,12 @@ static int send_userauth_methods(void) {
   buflen = bufsz;
   ptr = buf = palloc(pkt->pool, bufsz);
 
-  /* We send the remaining auth methods, not the avail auth methods, since
-   * the list of remaining auth methods may have changed (i.e. because of
-   * of failed auth attempts).
-   */
-
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-    "sending acceptable userauth methods: %s", auth_remaining_meths);
+    "sending acceptable userauth methods: %s", auth_avail_meths);
   
   sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_USER_AUTH_FAILURE);
-  sftp_msg_write_string(&buf, &buflen, auth_remaining_meths);
-  sftp_msg_write_bool(&buf, &buflen, FALSE);
+  sftp_msg_write_string(&buf, &buflen, auth_avail_meths);
+  sftp_msg_write_bool(&buf, &buflen, partial_success);
 
   pkt->payload = ptr;
   pkt->payload_len = (bufsz - buflen);
@@ -893,20 +1071,20 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
   orig_user = sftp_msg_read_string(pkt->pool, &buf, &buflen);
 
   user_cmd = pr_cmd_alloc(pkt->pool, 2, pstrdup(pkt->pool, C_USER), orig_user);
-  user_cmd->cmd_class = CL_AUTH;
+  user_cmd->cmd_class = CL_AUTH|CL_SSH;
   user_cmd->arg = orig_user;
 
   pass_cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, C_PASS));
-  user_cmd->cmd_class = CL_AUTH;
+  pass_cmd->cmd_class = CL_AUTH|CL_SSH;
   pass_cmd->arg = pstrdup(pkt->pool, "(hidden)");
 
-  /* Dispatch these as a PRE_CMDs, so that mod_delay's tactics can be used
+  /* Dispatch these as PRE_CMDs, so that mod_delay's tactics can be used
    * to ameliorate any timing-based attacks.
    */
   if (pr_cmd_dispatch_phase(user_cmd, PRE_CMD, 0) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "authentication request for user '%s' blocked by '%s' handler",
-      orig_user, user_cmd->argv[0]);
+      orig_user, (char *) user_cmd->argv[0]);
 
     pr_response_add_err(R_530, "Login incorrect.");
     pr_cmd_dispatch_phase(user_cmd, POST_CMD_ERR, 0);
@@ -1011,9 +1189,10 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
       "session.notes: %s", strerror(errno));
   }
 
-  cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "USERAUTH_REQUEST"));
+  cmd = pr_cmd_alloc(pkt->pool, 3, pstrdup(pkt->pool, "USERAUTH_REQUEST"),
+    pstrdup(pkt->pool, user), pstrdup(pkt->pool, method));
   cmd->arg = pstrcat(pkt->pool, user, " ", method, NULL);
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   if (auth_attempts_max > 0 &&
       auth_attempts > auth_attempts_max) {
@@ -1044,7 +1223,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
      * the list of authentication methods supported by the server is being
      * queried.
      */
-    if (send_userauth_methods() < 0) {
+    if (send_userauth_methods(FALSE) < 0) {
       pr_response_add_err(R_530, "Login incorrect.");
       pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
       pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -1063,7 +1242,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     return 0;
 
   } else if (strncmp(method, "publickey", 10) == 0) {
-    if (auth_meths_enabled & SFTP_AUTH_FL_METH_PUBLICKEY) {
+    if (auth_meths_enabled_flags & SFTP_AUTH_FL_METH_PUBLICKEY) {
       int xerrno;
 
       res = sftp_auth_publickey(pkt, pass_cmd, orig_user, user, *service,
@@ -1082,7 +1261,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     } else {
       pr_trace_msg(trace_channel, 10, "auth method '%s' not enabled", method);
 
-      if (send_userauth_methods() < 0) {
+      if (send_userauth_methods(FALSE) < 0) {
         pr_response_add_err(R_530, "Login incorrect.");
         pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
         pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -1102,7 +1281,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     }
 
   } else if (strncmp(method, "keyboard-interactive", 21) == 0) {
-    if (auth_meths_enabled & SFTP_AUTH_FL_METH_KBDINT) {
+    if (auth_meths_enabled_flags & SFTP_AUTH_FL_METH_KBDINT) {
       int xerrno = errno;
 
       res = sftp_auth_kbdint(pkt, pass_cmd, orig_user, user, *service,
@@ -1121,7 +1300,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     } else {
       pr_trace_msg(trace_channel, 10, "auth method '%s' not enabled", method);
 
-      if (send_userauth_methods() < 0) {
+      if (send_userauth_methods(FALSE) < 0) {
         pr_response_add_err(R_530, "Login incorrect.");
         pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
         pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -1141,7 +1320,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     }
 
   } else if (strncmp(method, "password", 9) == 0) {
-    if (auth_meths_enabled & SFTP_AUTH_FL_METH_PASSWORD) {
+    if (auth_meths_enabled_flags & SFTP_AUTH_FL_METH_PASSWORD) {
       int xerrno;
 
       res = sftp_auth_password(pkt, pass_cmd, orig_user, user, *service,
@@ -1160,7 +1339,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     } else {
       pr_trace_msg(trace_channel, 10, "auth method '%s' not enabled", method);
 
-      if (send_userauth_methods() < 0) {
+      if (send_userauth_methods(FALSE) < 0) {
         pr_response_add_err(R_530, "Login incorrect.");
         pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
         pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -1180,7 +1359,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     }
 
   } else if (strncmp(method, "hostbased", 10) == 0) {
-    if (auth_meths_enabled & SFTP_AUTH_FL_METH_HOSTBASED) {
+    if (auth_meths_enabled_flags & SFTP_AUTH_FL_METH_HOSTBASED) {
       int xerrno;
 
       res = sftp_auth_hostbased(pkt, pass_cmd, orig_user, user, *service,
@@ -1199,7 +1378,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     } else {
       pr_trace_msg(trace_channel, 10, "auth method '%s' not enabled", method);
 
-      if (send_userauth_methods() < 0) {
+      if (send_userauth_methods(FALSE) < 0) {
         pr_response_add_err(R_530, "Login incorrect.");
         pr_cmd_dispatch_phase(pass_cmd, POST_CMD_ERR, 0);
         pr_cmd_dispatch_phase(pass_cmd, LOG_CMD_ERR, 0);
@@ -1263,6 +1442,18 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
     return res;
   }
 
+  res = set_userauth_success(method);
+  if (res == 0) {
+    pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
+    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+
+    if (send_userauth_methods(TRUE) < 0) {
+      return -1;
+    }
+
+    return res;
+  }
+
   /* Past this point we will not call incr_auth_attempts(); the client has
    * successfully authenticated at this point, and should not be penalized
    * if an internal error causes the rest of the login process to fail.
@@ -1320,7 +1511,7 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
    * been tweaked via mod_ifsession's user/group/class-specific sections.
    */
   c = find_config(main_server->conf, CONF_PARAM, "Protocols", FALSE);
-  if (c) {
+  if (c != NULL) {
     register unsigned int i;
     unsigned int services = 0UL;
     array_header *protocols;
@@ -1352,6 +1543,229 @@ static int handle_userauth_req(struct ssh2_packet *pkt, char **service) {
   return 1;
 }
 
+/* Auth Lists API */
+struct sftp_auth_chain *sftp_auth_chain_alloc(pool *p) {
+  pool *sub_pool;
+  struct sftp_auth_chain *auth_chain;
+
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  sub_pool = pr_pool_create_sz(p, 256);
+  pr_pool_tag(sub_pool, "SSH2 Auth Chain Pool");
+
+  auth_chain = pcalloc(sub_pool, sizeof(struct sftp_auth_chain));
+  auth_chain->pool = sub_pool;
+  auth_chain->methods = make_array(sub_pool, 1,
+    sizeof(struct sftp_auth_method *));
+  auth_chain->completed = FALSE;
+
+  return auth_chain;
+}
+
+/* Check if 'password' or 'hostbased' methods appear multiple times in
+ * this chain; if so, it's an invalid chain.  Multiple 'publickey' or
+ * 'keyboard-interactive' methods are allowed, though.
+ */
+int sftp_auth_chain_isvalid(struct sftp_auth_chain *auth_chain) {
+  register unsigned int i;
+  int has_password = FALSE, has_hostbased = FALSE;
+
+  for (i = 0; i < auth_chain->methods->nelts; i++) {
+    struct sftp_auth_method *meth;
+
+    meth = ((struct sftp_auth_method **) auth_chain->methods->elts)[i];
+
+    switch (meth->method_id) {
+      case SFTP_AUTH_FL_METH_PASSWORD:
+        if (has_password == TRUE) {
+          errno = EPERM;
+          return -1;
+        }
+
+        has_password = TRUE;
+        break;
+
+      case SFTP_AUTH_FL_METH_HOSTBASED:
+        if (has_hostbased == TRUE) {
+          errno = EPERM;
+          return -1;
+        }
+
+        has_hostbased = TRUE;
+        break;
+
+      case SFTP_AUTH_FL_METH_PUBLICKEY:
+      case SFTP_AUTH_FL_METH_KBDINT:
+      default:
+        break;
+    }
+  }
+
+  return 0;
+}
+
+int sftp_auth_chain_add_method(struct sftp_auth_chain *auth_chain,
+    unsigned int method_id, const char *method_name,
+    const char *submethod_name) {
+  struct sftp_auth_method *meth;
+
+  if (auth_chain == NULL ||
+      method_name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* We currently only allow submethod names for kbdint methods. */
+  if (submethod_name != NULL &&
+      method_id != SFTP_AUTH_FL_METH_KBDINT) {
+    errno = EPERM;
+    return -1;
+  }
+
+  meth = pcalloc(auth_chain->pool, sizeof(struct sftp_auth_method));
+  meth->method_id = method_id;
+  meth->method_name = pstrdup(auth_chain->pool, method_name);
+  if (submethod_name != NULL) {
+    meth->submethod_name = pstrdup(auth_chain->pool, submethod_name);
+  }
+  meth->succeeded = FALSE;
+  meth->failed = FALSE;
+
+  *((struct sftp_auth_method **) push_array(auth_chain->methods)) = meth;
+  return 0;
+}
+
+int sftp_auth_chain_parse_method(pool *p, const char *name,
+    unsigned int *method_id, const char **method_name,
+    const char **submethod_name) {
+  char *ptr;
+  size_t method_namelen;
+
+  if (name == NULL ||
+      method_id == NULL ||
+      method_name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Look for the syntax indicating a submethod name. */
+  ptr = strchr(name, ':');
+  if (ptr == NULL) {
+    method_namelen = strlen(name);
+  } else {
+    method_namelen = ptr - name - 1;
+  }
+
+  if (strncmp(name, "publickey", method_namelen) == 0) {
+    *method_id = SFTP_AUTH_FL_METH_PUBLICKEY;
+    *method_name = name;
+
+  } else if (strncmp(name, "hostbased", method_namelen) == 0) {
+    *method_id = SFTP_AUTH_FL_METH_HOSTBASED;
+    *method_name = name;
+
+  } else if (strncmp(name, "password", method_namelen) == 0) {
+    *method_id = SFTP_AUTH_FL_METH_PASSWORD;
+    *method_name = name;
+
+  } else if (strncmp(name, "keyboard-interactive", method_namelen) == 0) {
+    *method_id = SFTP_AUTH_FL_METH_KBDINT;
+
+    if (sftp_kbdint_have_drivers() == 0) {
+      errno = EPERM;
+      return -1;
+    }
+
+    /* If we have a submethod name, check whether it matches one of our
+     * loaded kbdint drivers.
+     */
+    if (ptr != NULL) {
+      if (sftp_kbdint_get_driver(ptr) == NULL) {
+        errno = EPERM;
+        return -1;
+      }
+
+      *method_name = pstrndup(p, name, method_namelen);
+      if (submethod_name != NULL) {
+        *submethod_name = ptr;
+      }
+
+    } else {
+      *method_name = name;
+    }
+
+  } else {
+    /* Unknown/unsupported method name. */
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+array_header *sftp_auth_chain_parse_method_chain(pool *p,
+    const char *method_list) {
+  char *ptr;
+  size_t method_listlen;
+  array_header *method_names;
+
+  if (p == NULL ||
+      method_list == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  ptr = strchr(method_list, '+');
+  if (ptr == NULL) {
+    method_names = make_array(p, 0, sizeof(char *));
+    *((char **) push_array(method_names)) = pstrdup(p, method_list);
+    return method_names;
+  }
+
+  if (ptr == method_list) {
+    /* Leading '+'. */
+    errno = EPERM;
+    return NULL;
+  }
+
+  method_listlen = strlen(method_list);
+  if (method_list[method_listlen-1] == '+') {
+    /* Trailing '+'. */
+    errno = EPERM;
+    return NULL;
+  }
+
+  method_names = make_array(p, 0, sizeof(char *));
+
+  while (ptr != NULL) {
+    size_t namelen;
+
+    pr_signals_handle();
+
+    namelen = (ptr - method_list);
+    if (namelen == 0) {
+      /* Double '+' characters. */
+      errno = EPERM;
+      return NULL;
+    }
+
+    *((char **) push_array(method_names)) = pstrndup(p, method_list, namelen);
+
+    method_list = ptr + 1;
+    ptr = strchr(method_list, '+');
+
+    /* Don't forget the last name in the list. */
+    if (ptr == NULL) {
+      *((char **) push_array(method_names)) = pstrdup(p, method_list);
+    }
+  }
+
+  return method_names;
+}
+
 char *sftp_auth_get_default_dir(void) {
   return auth_default_dir;
 }
diff --git a/contrib/mod_sftp/auth.h b/contrib/mod_sftp/auth.h
index 00e307e..ece5b7a 100644
--- a/contrib/mod_sftp/auth.h
+++ b/contrib/mod_sftp/auth.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp user authentication (auth)
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,12 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: auth.h,v 1.8 2012-02-15 23:50:51 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_AUTH_H
 #define MOD_SFTP_AUTH_H
 
+#include "mod_sftp.h"
 #include "packet.h"
 
 #define SFTP_AUTH_FL_METH_PUBLICKEY	0x001
@@ -36,6 +33,48 @@
 #define SFTP_AUTH_FL_METH_PASSWORD	0x004
 #define SFTP_AUTH_FL_METH_HOSTBASED	0x008 
 
+/* Structures which define a chain of authentication methods; when each method
+ * in a chain has been satisfied, authentication succeeds.
+ */
+struct sftp_auth_method {
+  unsigned int method_id;
+  const char *method_name;
+
+  /* For e.g. kbdint driver names. */
+  const char *submethod_name;
+
+  /* For use during authentication. */
+  int succeeded, failed;
+};
+
+struct sftp_auth_chain {
+  pool *pool;
+  array_header *methods;
+  int completed;
+};
+
+struct sftp_auth_chain *sftp_auth_chain_alloc(pool *);
+
+/* Add a new method to this authentication chain. */
+int sftp_auth_chain_add_method(struct sftp_auth_chain *, unsigned int,
+  const char *, const char *);
+
+/* Parse given method name, e.g. "password" or "keyboard-interactive:pam",
+ * into the ID for the method, and the submethod portion (if any).
+ */
+int sftp_auth_chain_parse_method(pool *p, const char *, unsigned int *,
+  const char **, const char **);
+
+/* Parse a chain of methods, e.g. "publickey+password", into its component
+ * method names.  Returns the list of parsed method names, or NULL on error.
+ */
+array_header *sftp_auth_chain_parse_method_chain(pool *p, const char *);
+
+/* Verify that a given auth chain is correct, i.e. no unsupportable
+ * double/repeated methods, etc.
+ */
+int sftp_auth_chain_isvalid(struct sftp_auth_chain *);
+
 char *sftp_auth_get_default_dir(void);
 int sftp_auth_handle(struct ssh2_packet *);
 int sftp_auth_init(void);
@@ -44,20 +83,24 @@ int sftp_auth_init(void);
 int sftp_auth_hostbased(struct ssh2_packet *, cmd_rec *,
   const char *, const char *, const char *, unsigned char **, uint32_t *,
   int *);
+int sftp_auth_hostbased_init(pool *);
 
 /* Handles 'keyboard-interactive' user authentication. */
 int sftp_auth_kbdint(struct ssh2_packet *, cmd_rec *,
   const char *, const char *, const char *, unsigned char **, uint32_t *,
   int *);
+int sftp_auth_kbdint_init(pool *);
 
 /* Handles 'password' user authentication. */
 int sftp_auth_password(struct ssh2_packet *, cmd_rec *,
   const char *, const char *, const char *, unsigned char **, uint32_t *,
   int *);
+int sftp_auth_password_init(pool *);
 
 /* Handles 'publickey' user authentication. */
 int sftp_auth_publickey(struct ssh2_packet *, cmd_rec *,
   const char *, const char *, const char *, unsigned char **, uint32_t *,
   int *);
+int sftp_auth_publickey_init(pool *);
 
-#endif
+#endif /* MOD_SFTP_AUTH_H */
diff --git a/contrib/mod_sftp/blacklist.c b/contrib/mod_sftp/blacklist.c
index 1c8a990..dc86dfc 100644
--- a/contrib/mod_sftp/blacklist.c
+++ b/contrib/mod_sftp/blacklist.c
@@ -23,8 +23,6 @@
  *
  * The file size to encode 294,903 of 48-bit fingerprints is just 1.3 MB,
  * which corresponds to less than 4.5 bytes per fingerprint.
- *
- * $Id: blacklist.c,v 1.6 2012-03-13 20:15:25 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -52,6 +50,11 @@ struct blacklist_header {
 
 };
 
+/* Set a maximum number of records we expect to find in the blacklist file.
+ * The blacklist.dat file shipped with mod_sftp contains 294903 records.
+ */
+#define SFTP_BLACKLIST_MAX_RECORDS	300000
+
 static const char *blacklist_path = PR_CONFIG_DIR "/blacklist.dat";
 
 static const char *trace_channel = "ssh2";
@@ -98,10 +101,17 @@ static int validate_blacklist(int fd, unsigned int *bytes,
   *bytes = (hdr.record_bits >> 3) - 2;
 
   *records = (((hdr.records[0] << 8) + hdr.records[1]) << 8) + hdr.records[2];
+  if (*records > SFTP_BLACKLIST_MAX_RECORDS) {
+    pr_trace_msg(trace_channel, 2,
+      "SFTPKeyBlacklist '%s' contains %u records > max %u records",
+      blacklist_path, *records, (unsigned int) SFTP_BLACKLIST_MAX_RECORDS);
+    *records = SFTP_BLACKLIST_MAX_RECORDS;
+  }
+
   *shift = (hdr.shift[0] << 8) + hdr.shift[1];
 
   expected = sizeof(hdr) + 0x20000 + (*records) * (*bytes);
-  if (st.st_size != expected) {
+  if (st.st_size != (off_t) expected) {
     pr_trace_msg(trace_channel, 4,
       "unexpected SFTPKeyBlacklist '%s' file size: expected %lu, found %lu",
       blacklist_path, (unsigned long) expected, (unsigned long) st.st_size);
@@ -225,6 +235,11 @@ int sftp_blacklist_reject_key(pool *p, unsigned char *key_data,
   char *digest_name = "none", *hex, *ptr;
   size_t hex_len, hex_maxlen;
 
+  if (key_data == NULL ||
+      key_datalen == 0) {
+    return FALSE;
+  }
+
   if (blacklist_path == NULL) {
     /* No key blacklist configured, nothing to do. */
     return FALSE;
diff --git a/contrib/mod_sftp/blacklist.h b/contrib/mod_sftp/blacklist.h
index fe86bea..a570674 100644
--- a/contrib/mod_sftp/blacklist.h
+++ b/contrib/mod_sftp/blacklist.h
@@ -23,16 +23,14 @@
  *
  * The file size to encode 294,903 of 48-bit fingerprints is just 1.3 MB,
  * which corresponds to less than 4.5 bytes per fingerprint.
- *
- * $Id: blacklist.h,v 1.3 2012-02-15 23:50:51 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_BLACKLIST_H
 #define MOD_SFTP_BLACKLIST_H
 
+#include "mod_sftp.h"
+
 int sftp_blacklist_reject_key(pool *, unsigned char *, uint32_t);
 int sftp_blacklist_set_file(const char *);
 
-#endif
+#endif /* MOD_SFTP_BLACKLIST_H */
diff --git a/contrib/mod_sftp/channel.c b/contrib/mod_sftp/channel.c
index d009671..fa7946d 100644
--- a/contrib/mod_sftp/channel.c
+++ b/contrib/mod_sftp/channel.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp channels
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2015 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: channel.c,v 1.47 2012-02-18 22:12:20 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -404,9 +402,10 @@ static int read_channel_open(struct ssh2_packet *pkt, uint32_t *channel_id) {
     "packet size = %lu bytes", channel_type, (unsigned long) *channel_id,
     (unsigned long) initial_windowsz, (unsigned long) max_packetsz);
 
-  cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "CHANNEL_OPEN"));
+  cmd = pr_cmd_alloc(pkt->pool, 2, pstrdup(pkt->pool, "CHANNEL_OPEN"),
+    pstrdup(pkt->pool, channel_type));
   cmd->arg = channel_type;
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SSH;
 
   if (strncmp(channel_type, "session", 8) != 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -444,7 +443,7 @@ static int handle_channel_close(struct ssh2_packet *pkt) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "CHANNEL_CLOSE"));
   cmd->arg = pstrdup(pkt->pool, chan_str);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SSH;
 
   chan = get_channel(channel_id);
   if (chan == NULL) {
@@ -680,7 +679,7 @@ static int handle_channel_eof(struct ssh2_packet *pkt) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "CHANNEL_EOF"));
   cmd->arg = pstrdup(pkt->pool, chan_str);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SSH;
 
   chan = get_channel(channel_id);
   if (chan == NULL) {
@@ -990,9 +989,10 @@ static int handle_channel_req(struct ssh2_packet *pkt) {
     channel_request, (unsigned long) channel_id,
     want_reply ? "true" : "false");
 
-  cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "CHANNEL_REQUEST"));
+  cmd = pr_cmd_alloc(pkt->pool, 2, pstrdup(pkt->pool, "CHANNEL_REQUEST"),
+    pstrdup(pkt->pool, channel_request));
   cmd->arg = channel_request;
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SSH;
 
   chan = get_channel(channel_id);
   if (chan == NULL) {
@@ -1147,7 +1147,7 @@ static int handle_channel_window_adjust(struct ssh2_packet *pkt) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "CHANNEL_WINDOW_ADJUST"));
   cmd->arg = pstrdup(pkt->pool, adjust_str);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SSH;
 
   chan = get_channel(channel_id);
   if (chan == NULL) {
@@ -1641,14 +1641,21 @@ static int channel_write_data(pool *p, uint32_t channel_id,
 
   if (buflen > 0) {
     struct ssh2_channel_databuf *db;
+    const char *reason;
 
     db = get_databuf(channel_id, buflen);
 
     db->buflen = buflen;
     memcpy(db->buf, buf, buflen);
 
+    /* Why are we buffering these bytes? */
+    reason = "remote window size too small";
+    if (sftp_sess_state & SFTP_SESS_STATE_REKEYING) {
+      reason = "rekeying";
+    }
+
     pr_trace_msg(trace_channel, 8, "buffering %lu remaining bytes of "
-      "outgoing data", (unsigned long) buflen);
+      "outgoing data (%s)", (unsigned long) buflen, reason);
   }
 
   return 0;
diff --git a/contrib/mod_sftp/channel.h b/contrib/mod_sftp/channel.h
index 0cb0ec2..16baec6 100644
--- a/contrib/mod_sftp/channel.h
+++ b/contrib/mod_sftp/channel.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp channels
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,12 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: channel.h,v 1.14 2012-02-15 23:50:51 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_CHANNEL_H
 #define MOD_SFTP_CHANNEL_H
 
+#include "mod_sftp.h"
 #include "packet.h"
 
 #define SFTP_SSH2_CHANNEL_OPEN_ADMINISTRATIVELY_PROHIBITED	1
@@ -91,4 +88,4 @@ int sftp_channel_write_ext_data_stderr(pool *, uint32_t, unsigned char *,
  */
 unsigned int sftp_channel_opened(uint32_t *);
 
-#endif
+#endif /* MOD_SFTP_CHANNEL_H */
diff --git a/contrib/mod_sftp/cipher.c b/contrib/mod_sftp/cipher.c
index eefbf38..123d8bc 100644
--- a/contrib/mod_sftp/cipher.c
+++ b/contrib/mod_sftp/cipher.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp ciphers
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -54,7 +54,7 @@ static struct sftp_cipher read_ciphers[2] = {
   { NULL, NULL, NULL, 0, NULL, 0, 0 },
   { NULL, NULL, NULL, 0, NULL, 0, 0 }
 };
-static EVP_CIPHER_CTX *read_ctxs[2]; 
+static EVP_CIPHER_CTX *read_ctxs[2];
 
 static struct sftp_cipher write_ciphers[2] = {
   { NULL, NULL, NULL, 0, NULL, 0, 0 },
@@ -355,25 +355,27 @@ const char *sftp_cipher_get_read_algo(void) {
 
 int sftp_cipher_set_read_algo(const char *algo) {
   unsigned int idx = read_cipher_idx;
+  size_t key_len, discard_len;
 
   if (read_ciphers[idx].key) {
     /* If we have an existing key, it means that we are currently rekeying. */
     idx = get_next_read_index();
   }
 
-  read_ciphers[idx].cipher = sftp_crypto_get_cipher(algo,
-    (size_t *) &(read_ciphers[idx].key_len),
-    &(read_ciphers[idx].discard_len));
-
-  if (read_ciphers[idx].cipher == NULL)
+  read_ciphers[idx].cipher = sftp_crypto_get_cipher(algo, &key_len,
+    &discard_len);
+  if (read_ciphers[idx].cipher == NULL) {
     return -1;
+  }
 
   read_ciphers[idx].algo = algo;
+  read_ciphers[idx].key_len = (uint32_t) key_len;
+  read_ciphers[idx].discard_len = discard_len;
   return 0;
 }
 
 int sftp_cipher_set_read_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
-    const char *h, uint32_t hlen) {
+    const char *h, uint32_t hlen, int role) {
   const unsigned char *id = NULL;
   unsigned char *buf, *ptr;
   char letter;
@@ -400,17 +402,17 @@ int sftp_cipher_set_read_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
 
   id_len = sftp_session_get_id(&id);
 
-  /* First, initialize the cipher, but don't provide the key or IV yet. */
-  if (EVP_CipherInit(cipher_ctx, cipher->cipher, NULL, NULL, 0) != 1) {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error initializing %s cipher for decryption: %s", cipher->algo,
-      sftp_crypto_get_errors());
-    pr_memscrub(ptr, bufsz);
-    return -1;
-  }
+  /* The letters used depend on the role; see:
+   *  https://tools.ietf.org/html/rfc4253#section-7.2
+   *
+   * If we are the SERVER, then we use the letters for the "client to server"
+   * flows, since we are READING from the client.
+   */
 
-  /* IV: HASH(K || H || "A" || session_id) */
-  letter = 'A';
+  /* client-to-server IV: HASH(K || H || "A" || session_id)
+   * server-to-client IV: HASH(K || H || "B" || session_id)
+   */
+  letter = (role == SFTP_ROLE_SERVER ? 'A' : 'B');
   if (set_cipher_iv(cipher, hash, ptr, (bufsz - buflen), h, hlen, &letter, id,
       id_len) < 0) {
     pr_memscrub(ptr, bufsz);
@@ -419,14 +421,25 @@ int sftp_cipher_set_read_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
 
   key_len = (int) cipher->key_len;
 
-  /* Key: HASH(K || H || "C" || session_id) */
-  letter = 'C';
+  /* client-to-server key: HASH(K || H || "C" || session_id)
+   * server-to-client key: HASH(K || H || "D" || session_id)
+   */
+  letter = (role == SFTP_ROLE_SERVER ? 'C' : 'D');
   if (set_cipher_key(cipher, hash, ptr, (bufsz - buflen), h, hlen, &letter,
       id, id_len) < 0) {
     pr_memscrub(ptr, bufsz);
     return -1;
   }
 
+  if (EVP_CipherInit(cipher_ctx, cipher->cipher, cipher->key,
+      cipher->iv, 0) != 1) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error initializing %s cipher for decryption: %s", cipher->algo,
+      sftp_crypto_get_errors());
+    pr_memscrub(ptr, bufsz);
+    return -1;
+  }
+
   if (key_len > 0) {
     /* Next, set the key length. */
     if (EVP_CIPHER_CTX_set_key_length(cipher_ctx, key_len) != 1) {
@@ -438,15 +451,6 @@ int sftp_cipher_set_read_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
     }
   }
 
-  /* Now provide the key and IV. */
-  if (EVP_CipherInit(cipher_ctx, NULL, cipher->key, cipher->iv, -1) != 1) {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error setting key/IV for %s cipher for decryption: %s", cipher->algo,
-      sftp_crypto_get_errors());
-    pr_memscrub(ptr, bufsz);
-    return -1;
-  }
-
   if (set_cipher_discarded(cipher, cipher_ctx) < 0) {
     pr_memscrub(ptr, bufsz);
     return -1;
@@ -518,25 +522,27 @@ const char *sftp_cipher_get_write_algo(void) {
 
 int sftp_cipher_set_write_algo(const char *algo) {
   unsigned int idx = write_cipher_idx;
+  size_t key_len, discard_len;
 
   if (write_ciphers[idx].key) {
     /* If we have an existing key, it means that we are currently rekeying. */
     idx = get_next_write_index();
   }
 
-  write_ciphers[idx].cipher = sftp_crypto_get_cipher(algo,
-    (size_t *) &(write_ciphers[idx].key_len),
-    &(write_ciphers[idx].discard_len));
-
-  if (write_ciphers[idx].cipher == NULL)
+  write_ciphers[idx].cipher = sftp_crypto_get_cipher(algo, &key_len,
+    &discard_len);
+  if (write_ciphers[idx].cipher == NULL) {
     return -1;
+  }
 
   write_ciphers[idx].algo = algo;
+  write_ciphers[idx].key_len = (uint32_t) key_len;
+  write_ciphers[idx].discard_len = discard_len;
   return 0;
 }
 
 int sftp_cipher_set_write_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
-    const char *h, uint32_t hlen) {
+    const char *h, uint32_t hlen, int role) {
   const unsigned char *id = NULL;
   unsigned char *buf, *ptr;
   char letter;
@@ -563,17 +569,17 @@ int sftp_cipher_set_write_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
 
   id_len = sftp_session_get_id(&id);
 
-  /* First, initialize the cipher, but don't provide the key or IV yet. */
-  if (EVP_CipherInit(cipher_ctx, cipher->cipher, NULL, NULL, 1) != 1) {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error initializing %s cipher for encryption: %s", cipher->algo,
-      sftp_crypto_get_errors());
-    pr_memscrub(ptr, bufsz);
-    return -1;
-  }
+  /* The letters used depend on the role; see:
+   *  https://tools.ietf.org/html/rfc4253#section-7.2
+   *
+   * If we are the SERVER, then we use the letters for the "server to client"
+   * flows, since we are WRITING to the client.
+   */
 
-  /* IV: HASH(K || H || "B" || session_id) */
-  letter = 'B';
+  /* client-to-server IV: HASH(K || H || "A" || session_id)
+   * server-to-client IV: HASH(K || H || "B" || session_id)
+   */
+  letter = (role == SFTP_ROLE_SERVER ? 'B' : 'A');
   if (set_cipher_iv(cipher, hash, ptr, (bufsz - buflen), h, hlen, &letter, id,
       id_len) < 0) {
     pr_memscrub(ptr, bufsz);
@@ -582,14 +588,25 @@ int sftp_cipher_set_write_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
 
   key_len = (int) cipher->key_len;
 
-  /* Key: HASH(K || H || "D" || session_id) */
-  letter = 'D';
+  /* client-to-server key: HASH(K || H || "C" || session_id)
+   * server-to-client key: HASH(K || H || "D" || session_id)
+   */
+  letter = (role == SFTP_ROLE_SERVER ? 'D' : 'C');
   if (set_cipher_key(cipher, hash, ptr, (bufsz - buflen), h, hlen, &letter,
       id, id_len) < 0) {
     pr_memscrub(ptr, bufsz);
     return -1;
   }
 
+  if (EVP_CipherInit(cipher_ctx, cipher->cipher, cipher->key,
+      cipher->iv, 1) != 1) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error initializing %s cipher for encryption: %s", cipher->algo,
+      sftp_crypto_get_errors());
+    pr_memscrub(ptr, bufsz);
+    return -1;
+  }
+
   if (key_len > 0) {
     /* Next, set the key length. */
     if (EVP_CIPHER_CTX_set_key_length(cipher_ctx, key_len) != 1) {
@@ -601,15 +618,6 @@ int sftp_cipher_set_write_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
     }
   }
 
-  /* Now provide the key and IV. */
-  if (EVP_CipherInit(cipher_ctx, NULL, cipher->key, cipher->iv, -1) != 1) {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error setting key/IV for %s cipher for encryption: %s", cipher->algo,
-      sftp_crypto_get_errors());
-    pr_memscrub(ptr, bufsz);
-    return -1;
-  }
-
   if (set_cipher_discarded(cipher, cipher_ctx) < 0) {
     pr_memscrub(ptr, bufsz);
     return -1;
@@ -709,4 +717,3 @@ int sftp_cipher_free(void) {
 #endif /* OpenSSL-1.0.0 and later */
   return 0;
 }
-
diff --git a/contrib/mod_sftp/cipher.h b/contrib/mod_sftp/cipher.h
index b6421e0..4ab81c6 100644
--- a/contrib/mod_sftp/cipher.h
+++ b/contrib/mod_sftp/cipher.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp cipher mgmt
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -22,11 +22,11 @@
  * source distribution.
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_CIPHER_H
 #define MOD_SFTP_CIPHER_H
 
+#include "mod_sftp.h"
+
 int sftp_cipher_init(void);
 int sftp_cipher_free(void);
 
@@ -40,14 +40,14 @@ void sftp_cipher_set_block_size(size_t);
 const char *sftp_cipher_get_read_algo(void);
 int sftp_cipher_set_read_algo(const char *);
 int sftp_cipher_set_read_key(pool *, const EVP_MD *, const BIGNUM *,
-  const char *, uint32_t);
+  const char *, uint32_t, int);
 int sftp_cipher_read_data(pool *, unsigned char *, uint32_t,
   unsigned char **, uint32_t *);
 
 const char *sftp_cipher_get_write_algo(void);
 int sftp_cipher_set_write_algo(const char *);
 int sftp_cipher_set_write_key(pool *, const EVP_MD *, const BIGNUM *,
-  const char *, uint32_t);
+  const char *, uint32_t, int);
 int sftp_cipher_write_data(struct ssh2_packet *, unsigned char *, size_t *);
 
-#endif
+#endif /* MOD_SFTP_CIPHER_H */
diff --git a/contrib/mod_sftp/compress.c b/contrib/mod_sftp/compress.c
index 327f13c..b5ed21b 100644
--- a/contrib/mod_sftp/compress.c
+++ b/contrib/mod_sftp/compress.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp compression
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,19 +20,18 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: compress.c,v 1.8 2012-12-13 23:05:15 castaglia Exp $
  */
 
 #include "mod_sftp.h"
 
-#include <zlib.h>
-
 #include "msg.h"
 #include "packet.h"
 #include "crypto.h"
 #include "compress.h"
 
+#ifdef HAVE_ZLIB_H
+#include <zlib.h>
+
 static const char *trace_channel = "ssh2";
 
 struct sftp_compress {
@@ -63,15 +62,17 @@ static unsigned int read_comp_idx = 0;
 static unsigned int write_comp_idx = 0;
 
 static unsigned int get_next_read_index(void) {
-  if (read_comp_idx == 1)
+  if (read_comp_idx == 1) {
     return 0;
+  }
 
   return 1;
 }
 
 static unsigned int get_next_write_index(void) {
-  if (write_comp_idx == 1)
+  if (write_comp_idx == 1) {
     return 0;
+  }
 
   return 1;
 }
@@ -522,3 +523,48 @@ int sftp_compress_write_data(struct ssh2_packet *pkt) {
   return 0;
 }
 
+#else
+
+int sftp_compress_init_read(int flags) {
+  return 0;
+}
+
+const char *sftp_compress_get_read_algo(void) {
+  return "none";
+}
+
+int sftp_compress_set_read_algo(const char *algo) {
+  if (strncmp(algo, "none", 5) == 0) {
+    return 0;
+  }
+
+  errno = EINVAL;
+  return -1;
+}
+
+int sftp_compress_read_data(struct ssh2_packet *pkt) {
+  return 0;
+}
+
+int sftp_compress_init_write(int flags) {
+  return 0;
+}
+
+const char *sftp_compress_get_write_algo(void) {
+  return "none";
+}
+
+int sftp_compress_set_write_algo(const char *algo) {
+  if (strncmp(algo, "none", 5) == 0) {
+    return 0;
+  }
+
+  errno = EINVAL;
+  return -1;
+}
+
+int sftp_compress_write_data(struct ssh2_packet *pkt) {
+  return 0;
+}
+
+#endif /* !HAVE_ZLIB_H */
diff --git a/contrib/mod_sftp/compress.h b/contrib/mod_sftp/compress.h
index 58a4c22..57ae436 100644
--- a/contrib/mod_sftp/compress.h
+++ b/contrib/mod_sftp/compress.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp compression mgmt
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,16 +20,14 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: compress.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-#include "packet.h"
-
 #ifndef MOD_SFTP_COMPRESS_H
 #define MOD_SFTP_COMPRESS_H
 
+#include "mod_sftp.h"
+#include "packet.h"
+
 #define SFTP_COMPRESS_FL_NEW_KEY		1
 #define SFTP_COMPRESS_FL_AUTHENTICATED		2
 
@@ -43,4 +41,4 @@ const char *sftp_compress_get_write_algo(void);
 int sftp_compress_set_write_algo(const char *);
 int sftp_compress_write_data(struct ssh2_packet *);
 
-#endif
+#endif /* MOD_SFTP_COMPRESS_H */
diff --git a/contrib/mod_sftp/configure b/contrib/mod_sftp/configure
index 89d830d..b511417 100755
--- a/contrib/mod_sftp/configure
+++ b/contrib/mod_sftp/configure
@@ -674,6 +674,7 @@ GREP
 EGREP
 INCLUDES
 LIBDIRS
+MODULE_LIBS
 LIBOBJS
 LTLIBOBJS'
 ac_subst_files=''
@@ -3200,7 +3201,7 @@ else
   { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 
 { echo "$as_me:$LINENO: checking for library containing strerror" >&5
@@ -3354,7 +3355,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3375,7 +3376,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3680,6 +3681,38 @@ _ACEOF
 fi
 
 
+
+# Check whether --with-includes was given.
+if test "${with_includes+set}" = set; then
+  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for ainclude in $ac_addl_includes; do
+      if test x"$ac_build_addl_includes" = x ; then
+        ac_build_addl_includes="-I$ainclude"
+      else
+        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
+      fi
+    done
+    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+
+fi
+
+
+
+# Check whether --with-libraries was given.
+if test "${with_libraries+set}" = set; then
+  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for alibdir in $ac_addl_libdirs; do
+      if test x"$ac_build_addl_libdirs" = x ; then
+        ac_build_addl_libdirs="-L$alibdir"
+      else
+        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
+      fi
+    done
+    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+fi
+
+
 { echo "$as_me:$LINENO: checking for ANSI C header files" >&5
 echo $ECHO_N "checking for ANSI C header files... $ECHO_C" >&6; }
 if test "${ac_cv_header_stdc+set}" = set; then
@@ -3748,7 +3781,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3769,7 +3802,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3997,49 +4030,150 @@ fi
 
 done
 
+if test "${ac_cv_header_zlib_h+set}" = set; then
+  { echo "$as_me:$LINENO: checking for zlib.h" >&5
+echo $ECHO_N "checking for zlib.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_zlib_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_zlib_h" >&5
+echo "${ECHO_T}$ac_cv_header_zlib_h" >&6; }
+else
+  # Is the header compilable?
+{ echo "$as_me:$LINENO: checking zlib.h usability" >&5
+echo $ECHO_N "checking zlib.h usability... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+$ac_includes_default
+#include <zlib.h>
+_ACEOF
+rm -f conftest.$ac_objext
+if { (ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_compile") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext; then
+  ac_header_compiler=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
+	ac_header_compiler=no
+fi
 
-# Check whether --with-includes was given.
-if test "${with_includes+set}" = set; then
-  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
-    for ainclude in $ac_addl_includes; do
-      if test x"$ac_build_addl_includes" = x ; then
-        ac_build_addl_includes="-I$ainclude"
-      else
-        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
-      fi
-    done
-    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_compiler" >&5
+echo "${ECHO_T}$ac_header_compiler" >&6; }
+
+# Is the header present?
+{ echo "$as_me:$LINENO: checking zlib.h presence" >&5
+echo $ECHO_N "checking zlib.h presence... $ECHO_C" >&6; }
+cat >conftest.$ac_ext <<_ACEOF
+/* confdefs.h.  */
+_ACEOF
+cat confdefs.h >>conftest.$ac_ext
+cat >>conftest.$ac_ext <<_ACEOF
+/* end confdefs.h.  */
+#include <zlib.h>
+_ACEOF
+if { (ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval "echo \"\$as_me:$LINENO: $ac_try_echo\"") >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.er1
+  ac_status=$?
+  grep -v '^ *+' conftest.er1 >conftest.err
+  rm -f conftest.er1
+  cat conftest.err >&5
+  echo "$as_me:$LINENO: \$? = $ac_status" >&5
+  (exit $ac_status); } >/dev/null && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }; then
+  ac_header_preproc=yes
+else
+  echo "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
 
+  ac_header_preproc=no
 fi
 
+rm -f conftest.err conftest.$ac_ext
+{ echo "$as_me:$LINENO: result: $ac_header_preproc" >&5
+echo "${ECHO_T}$ac_header_preproc" >&6; }
 
+# So?  What about this header?
+case $ac_header_compiler:$ac_header_preproc:$ac_c_preproc_warn_flag in
+  yes:no: )
+    { echo "$as_me:$LINENO: WARNING: zlib.h: accepted by the compiler, rejected by the preprocessor!" >&5
+echo "$as_me: WARNING: zlib.h: accepted by the compiler, rejected by the preprocessor!" >&2;}
+    { echo "$as_me:$LINENO: WARNING: zlib.h: proceeding with the compiler's result" >&5
+echo "$as_me: WARNING: zlib.h: proceeding with the compiler's result" >&2;}
+    ac_header_preproc=yes
+    ;;
+  no:yes:* )
+    { echo "$as_me:$LINENO: WARNING: zlib.h: present but cannot be compiled" >&5
+echo "$as_me: WARNING: zlib.h: present but cannot be compiled" >&2;}
+    { echo "$as_me:$LINENO: WARNING: zlib.h:     check for missing prerequisite headers?" >&5
+echo "$as_me: WARNING: zlib.h:     check for missing prerequisite headers?" >&2;}
+    { echo "$as_me:$LINENO: WARNING: zlib.h: see the Autoconf documentation" >&5
+echo "$as_me: WARNING: zlib.h: see the Autoconf documentation" >&2;}
+    { echo "$as_me:$LINENO: WARNING: zlib.h:     section \"Present But Cannot Be Compiled\"" >&5
+echo "$as_me: WARNING: zlib.h:     section \"Present But Cannot Be Compiled\"" >&2;}
+    { echo "$as_me:$LINENO: WARNING: zlib.h: proceeding with the preprocessor's result" >&5
+echo "$as_me: WARNING: zlib.h: proceeding with the preprocessor's result" >&2;}
+    { echo "$as_me:$LINENO: WARNING: zlib.h: in the future, the compiler will take precedence" >&5
+echo "$as_me: WARNING: zlib.h: in the future, the compiler will take precedence" >&2;}
 
-# Check whether --with-libraries was given.
-if test "${with_libraries+set}" = set; then
-  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
-    for alibdir in $ac_addl_libdirs; do
-      if test x"$ac_build_addl_libdirs" = x ; then
-        ac_build_addl_libdirs="-L$alibdir"
-      else
-        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
-      fi
-    done
-    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+    ;;
+esac
+{ echo "$as_me:$LINENO: checking for zlib.h" >&5
+echo $ECHO_N "checking for zlib.h... $ECHO_C" >&6; }
+if test "${ac_cv_header_zlib_h+set}" = set; then
+  echo $ECHO_N "(cached) $ECHO_C" >&6
+else
+  ac_cv_header_zlib_h=$ac_header_preproc
+fi
+{ echo "$as_me:$LINENO: result: $ac_cv_header_zlib_h" >&5
+echo "${ECHO_T}$ac_cv_header_zlib_h" >&6; }
+
+fi
+if test $ac_cv_header_zlib_h = yes; then
+
+cat >>confdefs.h <<\_ACEOF
+#define HAVE_ZLIB_H 1
+_ACEOF
+
+   MODULE_LIBS="$MODULE_LIBS -lz"
 
 fi
 
 
 
-saved_libs="$LIBS"
-LIBS="$LIBS -lcrypto"
 
 { echo "$as_me:$LINENO: checking whether linking with OpenSSL functions requires -ldl" >&5
 echo $ECHO_N "checking whether linking with OpenSSL functions requires -ldl... $ECHO_C" >&6; }
 saved_libs="$LIBS"
 
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -ldl"
+LIBS="-lcrypto -ldl $LIBS"
 
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
@@ -4103,7 +4237,7 @@ echo $ECHO_N "checking whether linking with OpenSSL functions requires -lz... $E
 saved_libs="$LIBS"
 
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -lz"
+LIBS="-lcrypto -lz $LIBS"
 
 cat >conftest.$ac_ext <<_ACEOF
 /* confdefs.h.  */
@@ -4382,6 +4516,7 @@ LIBDIRS="$ac_build_addl_libdirs"
 
 
 
+
 ac_config_headers="$ac_config_headers mod_sftp.h"
 
 ac_config_files="$ac_config_files Makefile"
@@ -5062,11 +5197,12 @@ GREP!$GREP$ac_delim
 EGREP!$EGREP$ac_delim
 INCLUDES!$INCLUDES$ac_delim
 LIBDIRS!$LIBDIRS$ac_delim
+MODULE_LIBS!$MODULE_LIBS$ac_delim
 LIBOBJS!$LIBOBJS$ac_delim
 LTLIBOBJS!$LTLIBOBJS$ac_delim
 _ACEOF
 
-  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 63; then
+  if test `sed -n "s/.*$ac_delim\$/X/p" conf$$subs.sed | grep -c X` = 64; then
     break
   elif $ac_last_try; then
     { { echo "$as_me:$LINENO: error: could not make $CONFIG_STATUS" >&5
@@ -5422,7 +5558,7 @@ do
     cat >>$CONFIG_STATUS <<_ACEOF
     # First, check the format of the line:
     cat >"\$tmp/defines.sed" <<\\CEOF
-/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*/b def
+/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*\$/b def
 /^[	 ]*#[	 ]*define[	 ][	 ]*$ac_word_re[(	 ]/b def
 b
 :def
diff --git a/contrib/mod_sftp/configure.in b/contrib/mod_sftp/configure.in
index 931b78f..ea2e032 100644
--- a/contrib/mod_sftp/configure.in
+++ b/contrib/mod_sftp/configure.in
@@ -1,3 +1,22 @@
+dnl ProFTPD - mod_sftp
+dnl Copyright (c) 2012-2016 TJ Saunders <tj at castaglia.org>
+dnl
+dnl This program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program; if not, write to the Free Software
+dnl Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+dnl
+dnl Process this file with autoconf to produce a configure script.
+
 AC_INIT(./mod_sftp.c)
 
 AC_CANONICAL_SYSTEM
@@ -10,9 +29,6 @@ AC_AIX
 AC_ISC_POSIX
 AC_MINIX
 
-AC_HEADER_STDC
-AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h)
-
 dnl Need to support/handle the --with-includes and --with-libraries options
 AC_ARG_WITH(includes,
   [AC_HELP_STRING(
@@ -46,6 +62,12 @@ AC_ARG_WITH(libraries,
     LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
   ])
 
+AC_HEADER_STDC
+AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h)
+AC_CHECK_HEADER(zlib.h,
+  [AC_DEFINE(HAVE_ZLIB_H, 1, [Define if zlib.h is present.])
+   MODULE_LIBS="$MODULE_LIBS -lz"
+  ])
 
 dnl Check for a crippled OpenSSL library (e.g. Solaris 10).  More details
 dnl can be found in:
@@ -58,15 +80,13 @@ dnl So for those users stuck using the Solaris 10 whose OpenSSL does
 dnl not provide support for AES ciphers longer than 128 bits, we need to
 dnl check and disable those symbols.  Otherwise mod_sftp fails to build due
 dnl to linker errors.
-saved_libs="$LIBS"
-LIBS="$LIBS -lcrypto"
 
 AC_MSG_CHECKING([whether linking with OpenSSL functions requires -ldl])
 saved_libs="$LIBS"
 
 dnl Splice out -lsupp, since that library hasn't been built yet
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -ldl"
+LIBS="-lcrypto -ldl $LIBS"
 
 AC_TRY_LINK(
   [
@@ -87,7 +107,7 @@ saved_libs="$LIBS"
 
 dnl Splice out -lsupp, since that library hasn't been built yet
 LIBS=`echo "$LIBS" | sed -e 's/-lsupp//g'`;
-LIBS="$LIBS -lcrypto -lz"
+LIBS="-lcrypto -lz $LIBS"
 
 AC_TRY_LINK(
   [
@@ -190,6 +210,7 @@ LIBDIRS="$ac_build_addl_libdirs"
 AC_SUBST(INCLUDES)
 AC_SUBST(LDFLAGS)
 AC_SUBST(LIBDIRS)
+AC_SUBST(MODULE_LIBS)
 
 AC_CONFIG_HEADER(mod_sftp.h)
 AC_OUTPUT(Makefile)
diff --git a/contrib/mod_sftp/crypto.c b/contrib/mod_sftp/crypto.c
index cdb435c..d7c57b2 100644
--- a/contrib/mod_sftp/crypto.c
+++ b/contrib/mod_sftp/crypto.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp OpenSSL interface
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -95,11 +95,19 @@ static struct sftp_cipher ciphers[] = {
   { "aes128-cbc",	"aes-128-cbc",	0,	EVP_aes_128_cbc, TRUE, TRUE },
 #endif
 
+#if !defined(OPENSSL_NO_BF)
   { "blowfish-ctr",	NULL,		0,	NULL,	TRUE, FALSE },
   { "blowfish-cbc",	"bf-cbc",	0,	EVP_bf_cbc, TRUE, FALSE },
+#endif /* !OPENSSL_NO_BF */
+
+#if !defined(OPENSSL_NO_CAST)
   { "cast128-cbc",	"cast5-cbc",	0,	EVP_cast5_cbc, TRUE, FALSE },
+#endif /* !OPENSSL_NO_CAST */
+
+#if !defined(OPENSSL_NO_RC4)
   { "arcfour256",	"rc4",		1536,	EVP_rc4, TRUE, FALSE },
   { "arcfour128",	"rc4",		1536,	EVP_rc4, TRUE, FALSE },
+#endif /* !OPENSSL_NO_RC4 */
 
 #if 0
   /* This cipher is explicitly NOT supported because it does not discard
@@ -112,8 +120,11 @@ static struct sftp_cipher ciphers[] = {
   { "arcfour",		"rc4",		0,	EVP_rc4, FALSE, FALSE },
 #endif
 
+#if !defined(OPENSSL_NO_DES)
   { "3des-ctr",		NULL,		0,	NULL, TRUE, TRUE },
   { "3des-cbc",		"des-ede3-cbc",	0,	EVP_des_ede3_cbc, TRUE, TRUE },
+#endif /* !OPENSSL_NO_DES */
+
   { "none",		"null",		0,	EVP_enc_null, FALSE, TRUE },
   { NULL, NULL, 0, NULL, FALSE, FALSE }
 };
@@ -155,9 +166,12 @@ static struct sftp_digest digests[] = {
   { "hmac-sha1-96",	"sha1",		EVP_sha1,	12,	TRUE, TRUE },
   { "hmac-md5",		"md5",		EVP_md5,	0,	TRUE, FALSE },
   { "hmac-md5-96",	"md5",		EVP_md5,	12,	TRUE, FALSE },
+#if !defined(OPENSSL_NO_RIPEMD)
   { "hmac-ripemd160",	"rmd160",	EVP_ripemd160,	0,	TRUE, FALSE },
+#endif /* !OPENSSL_NO_RIPEMD */
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
   { "umac-64 at openssh.com", NULL,	NULL,		8,	TRUE, FALSE },
+  { "umac-128 at openssh.com", NULL,	NULL,		16,	TRUE, FALSE },
 #endif /* OpenSSL-0.9.7 or later */
   { "none",		"null",		EVP_md_null,	0,	FALSE, TRUE },
   { NULL, NULL, NULL, 0, FALSE, FALSE }
@@ -166,7 +180,11 @@ static struct sftp_digest digests[] = {
 static const char *trace_channel = "ssh2";
 
 static void ctr_incr(unsigned char *ctr, size_t len) {
-  register unsigned int i;
+  register int i;
+
+  if (len == 0) {
+    return;
+  }
 
   for (i = len - 1; i >= 0; i--) {
     /* If we haven't overflowed, we're done. */
@@ -176,6 +194,7 @@ static void ctr_incr(unsigned char *ctr, size_t len) {
   }
 }
 
+#if !defined(OPENSSL_NO_BF)
 /* Blowfish CTR mode implementation */
 
 struct bf_ctr_ex {
@@ -298,7 +317,8 @@ static int do_bf_ctr(EVP_CIPHER_CTX *ctx, unsigned char *dst,
 static const EVP_CIPHER *get_bf_ctr_cipher(void) {
   EVP_CIPHER *cipher;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   /* XXX TODO: At some point, we also need to call EVP_CIPHER_meth_free() on
    * this, to avoid a resource leak.
    */
@@ -329,9 +349,11 @@ static const EVP_CIPHER *get_bf_ctr_cipher(void) {
 
   return cipher;
 }
+#endif /* !OPENSSL_NO_BF */
 
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
 
+# if !defined(OPENSSL_NO_DES)
 /* 3DES CTR mode implementation */
 
 struct des3_ctr_ex {
@@ -464,7 +486,8 @@ static int do_des3_ctr(EVP_CIPHER_CTX *ctx, unsigned char *dst,
 static const EVP_CIPHER *get_des3_ctr_cipher(void) {
   EVP_CIPHER *cipher;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   unsigned long flags;
 
   /* XXX TODO: At some point, we also need to call EVP_CIPHER_meth_free() on
@@ -506,6 +529,7 @@ static const EVP_CIPHER *get_des3_ctr_cipher(void) {
 
   return cipher;
 }
+# endif /* !OPENSSL_NO_DES */
 
 /* AES CTR mode implementation */
 struct aes_ctr_ex {
@@ -595,6 +619,8 @@ static int do_aes_ctr(EVP_CIPHER_CTX *ctx, unsigned char *dst,
    *
    * This change is not documented in OpenSSL's CHANGES file.  Sigh.
    *
+   * And in OpenSSL-1.1.0 and later, the AES CTR code was removed entirely.
+   *
    * Thus for these versions, we have to use our own AES CTR code.
    */
 
@@ -681,7 +707,8 @@ static int get_aes_ctr_cipher_nid(int key_len) {
 static const EVP_CIPHER *get_aes_ctr_cipher(int key_len) {
   EVP_CIPHER *cipher;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   unsigned long flags;
 
   /* XXX TODO: At some point, we also need to call EVP_CIPHER_meth_free() on
@@ -725,16 +752,16 @@ static const EVP_CIPHER *get_aes_ctr_cipher(int key_len) {
   return cipher;
 }
 
-static int update_umac(EVP_MD_CTX *ctx, const void *data, size_t len) {
+static int update_umac64(EVP_MD_CTX *ctx, const void *data, size_t len) {
   int res;
   void *md_data;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   md_data = EVP_MD_CTX_md_data(ctx);
 #else
   md_data = ctx->md_data;
 #endif /* prior to OpenSSL-1.1.0 */
-
   if (md_data == NULL) {
     struct umac_ctx *umac;
     void **ptr;
@@ -753,12 +780,42 @@ static int update_umac(EVP_MD_CTX *ctx, const void *data, size_t len) {
   return res;
 }
 
-static int final_umac(EVP_MD_CTX *ctx, unsigned char *md) {
+static int update_umac128(EVP_MD_CTX *ctx, const void *data, size_t len) {
+  int res;
+  void *md_data;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+  md_data = EVP_MD_CTX_md_data(ctx);
+#else
+  md_data = ctx->md_data;
+#endif /* prior to OpenSSL-1.1.0 */
+
+  if (md_data == NULL) {
+    struct umac_ctx *umac;
+    void **ptr;
+
+    umac = umac128_new((unsigned char *) data);
+    if (umac == NULL) {
+      return 0;
+    }
+
+    ptr = &md_data;
+    *ptr = umac;
+    return 1;
+  }
+
+  res = umac128_update(md_data, (unsigned char *) data, (long) len);
+  return res;
+}
+
+static int final_umac64(EVP_MD_CTX *ctx, unsigned char *md) {
   unsigned char nonce[8];
   int res;
   void *md_data;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   md_data = EVP_MD_CTX_md_data(ctx);
 #else
   md_data = ctx->md_data;
@@ -768,11 +825,28 @@ static int final_umac(EVP_MD_CTX *ctx, unsigned char *md) {
   return res;
 }
 
-static int delete_umac(EVP_MD_CTX *ctx) {
+static int final_umac128(EVP_MD_CTX *ctx, unsigned char *md) {
+  unsigned char nonce[8];
+  int res;
+  void *md_data;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+  md_data = EVP_MD_CTX_md_data(ctx);
+#else
+  md_data = ctx->md_data;
+#endif /* prior to OpenSSL-1.1.0 */
+
+  res = umac128_final(md_data, md, nonce);
+  return res;
+}
+
+static int delete_umac64(EVP_MD_CTX *ctx) {
   struct umac_ctx *umac;
   void *md_data, **ptr;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   md_data = EVP_MD_CTX_md_data(ctx);
 #else
   md_data = ctx->md_data;
@@ -787,10 +861,31 @@ static int delete_umac(EVP_MD_CTX *ctx) {
   return 1;
 }
 
-static const EVP_MD *get_umac_digest(void) {
+static int delete_umac128(EVP_MD_CTX *ctx) {
+  struct umac_ctx *umac;
+  void *md_data, **ptr;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+  md_data = EVP_MD_CTX_md_data(ctx);
+#else
+  md_data = ctx->md_data;
+#endif /* prior to OpenSSL-1.1.0 */
+
+  umac = md_data;
+  umac128_delete(umac);
+
+  ptr = &md_data;
+  *ptr = NULL;
+
+  return 1;
+}
+
+static const EVP_MD *get_umac64_digest(void) {
   EVP_MD *md;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   /* XXX TODO: At some point, we also need to call EVP_MD_meth_free() on
    * this, to avoid a resource leak.
    */
@@ -798,24 +893,60 @@ static const EVP_MD *get_umac_digest(void) {
   EVP_MD_meth_set_input_blocksize(md, 32);
   EVP_MD_meth_set_result_size(md, 8);
   EVP_MD_meth_set_flags(md, 0UL);
-  EVP_MD_meth_set_update(md, update_umac);
-  EVP_MD_meth_set_final(md, final_umac);
-  EVP_MD_meth_set_cleanup(md, delete_umac);
+  EVP_MD_meth_set_update(md, update_umac64);
+  EVP_MD_meth_set_final(md, final_umac64);
+  EVP_MD_meth_set_cleanup(md, delete_umac64);
 #else
-  static EVP_MD umac_digest;
+  static EVP_MD umac64_digest;
 
-  memset(&umac_digest, 0, sizeof(EVP_MD));
+  memset(&umac64_digest, 0, sizeof(EVP_MD));
 
-  umac_digest.type = NID_undef;
-  umac_digest.pkey_type = NID_undef;
-  umac_digest.md_size = 8;
-  umac_digest.flags = 0UL;
-  umac_digest.update = update_umac;
-  umac_digest.final = final_umac;
-  umac_digest.cleanup = delete_umac;
-  umac_digest.block_size = 32;
+  umac64_digest.type = NID_undef;
+  umac64_digest.pkey_type = NID_undef;
+  umac64_digest.md_size = 8;
+  umac64_digest.flags = 0UL;
+  umac64_digest.update = update_umac64;
+  umac64_digest.final = final_umac64;
+  umac64_digest.cleanup = delete_umac64;
+  umac64_digest.block_size = 32;
 
-  md = &umac_digest;
+  md = &umac64_digest;
+#endif /* prior to OpenSSL-1.1.0 */
+
+  return md;
+}
+
+static const EVP_MD *get_umac128_digest(void) {
+  EVP_MD *md;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+  /* XXX TODO: At some point, we also need to call EVP_MD_meth_free() on
+   * this, to avoid a resource leak.
+   */
+  md = EVP_MD_meth_new(NID_undef, NID_undef);
+  EVP_MD_meth_set_input_blocksize(md, 64);
+  EVP_MD_meth_set_result_size(md, 16);
+  EVP_MD_meth_set_flags(md, 0UL);
+  EVP_MD_meth_set_update(md, update_umac128);
+  EVP_MD_meth_set_final(md, final_umac128);
+  EVP_MD_meth_set_cleanup(md, delete_umac128);
+
+#else
+  static EVP_MD umac128_digest;
+
+  memset(&umac128_digest, 0, sizeof(EVP_MD));
+
+  umac128_digest.type = NID_undef;
+  umac128_digest.pkey_type = NID_undef;
+  umac128_digest.md_size = 16;
+  umac128_digest.flags = 0UL;
+  umac128_digest.update = update_umac128;
+  umac128_digest.final = final_umac128;
+  umac128_digest.cleanup = delete_umac128;
+  umac128_digest.block_size = 64;
+
+  md = &umac128_digest;
 #endif /* prior to OpenSSL-1.1.0 */
 
   return md;
@@ -831,11 +962,25 @@ const EVP_CIPHER *sftp_crypto_get_cipher(const char *name, size_t *key_len,
       const EVP_CIPHER *cipher;
 
       if (strncmp(name, "blowfish-ctr", 13) == 0) {
+#if !defined(OPENSSL_NO_BF)
         cipher = get_bf_ctr_cipher();
+#else
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "'%s' cipher unsupported", name);
+        errno = ENOENT;
+        return NULL;
+#endif /* !OPENSSL_NO_BF */
 
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
       } else if (strncmp(name, "3des-ctr", 9) == 0) {
+# if !defined(OPENSSL_NO_DES)
         cipher = get_des3_ctr_cipher();
+# else
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "'%s' cipher unsupported", name);
+        errno = ENOENT;
+        return NULL;
+# endif /* !OPENSSL_NO_DES */
 
       } else if (strncmp(name, "aes256-ctr", 11) == 0) {
         cipher = get_aes_ctr_cipher(32);
@@ -864,8 +1009,9 @@ const EVP_CIPHER *sftp_crypto_get_cipher(const char *name, size_t *key_len,
         }
       }
 
-      if (discard_len)
+      if (discard_len) {
         *discard_len = ciphers[i].discard_len;
+      }
 
       return cipher;
     }
@@ -873,6 +1019,7 @@ const EVP_CIPHER *sftp_crypto_get_cipher(const char *name, size_t *key_len,
 
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
     "no cipher matching '%s' found", name);
+  errno = ENOENT;
   return NULL;
 }
 
@@ -885,7 +1032,10 @@ const EVP_MD *sftp_crypto_get_digest(const char *name, uint32_t *mac_len) {
 
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
       if (strncmp(name, "umac-64 at openssh.com", 12) == 0) {
-        digest = get_umac_digest();
+        digest = get_umac64_digest();
+
+      } else if (strncmp(name, "umac-128 at openssh.com", 13) == 0) {
+        digest = get_umac128_digest();
 #else
       if (FALSE) {
 #endif /* OpenSSL older than 0.9.7 */
@@ -1042,7 +1192,7 @@ const char *sftp_crypto_get_kexinit_digest_list(pool *p) {
    */
 
   c = find_config(main_server->conf, CONF_PARAM, "SFTPDigests", FALSE);
-  if (c) {
+  if (c != NULL) {
     register unsigned int i;
 
     for (i = 0; i < c->argc; i++) {
@@ -1071,8 +1221,9 @@ const char *sftp_crypto_get_kexinit_digest_list(pool *p) {
                 pstrdup(p, digests[j].name), NULL);
 
             } else {
-              /* The umac-64 digest is a special case. */
-              if (strncmp(digests[j].name, "umac-64 at openssh.com", 12) == 0) {
+              /* The umac-64/umac-128 digests are special cases. */
+              if (strncmp(digests[j].name, "umac-64 at openssh.com", 12) == 0 ||
+                  strncmp(digests[j].name, "umac-128 at openssh.com", 13) == 0) {
                 res = pstrcat(p, res, *res ? "," : "",
                   pstrdup(p, digests[j].name), NULL);
 
@@ -1117,8 +1268,9 @@ const char *sftp_crypto_get_kexinit_digest_list(pool *p) {
               pstrdup(p, digests[i].name), NULL);
 
           } else {
-            /* The umac-64 digest is a special case. */
-            if (strncmp(digests[i].name, "umac-64 at openssh.com", 12) == 0) {
+            /* The umac-64/umac-128 digests are special cases. */
+            if (strncmp(digests[i].name, "umac-64 at openssh.com", 12) == 0 ||
+                strncmp(digests[i].name, "umac-128 at openssh.com", 13) == 0) {
               res = pstrcat(p, res, *res ? "," : "",
                 pstrdup(p, digests[i].name), NULL);
 
@@ -1146,33 +1298,46 @@ const char *sftp_crypto_get_kexinit_digest_list(pool *p) {
 
 const char *sftp_crypto_get_errors(void) {
   unsigned int count = 0;
-  unsigned long e = ERR_get_error();
+  unsigned long error_code;
   BIO *bio = NULL;
   char *data = NULL;
   long datalen;
-  const char *str = "(unknown)";
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
 
   /* Use ERR_print_errors() and a memory BIO to build up a string with
    * all of the error messages from the error queue.
    */
 
-  if (e)
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
     bio = BIO_new(BIO_s_mem());
+  }
 
-  while (e) {
-    pr_signals_handle();
-    BIO_printf(bio, "\n  (%u) %s", ++count, ERR_error_string(e, NULL));
-    e = ERR_get_error();
+  while (error_code) {
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
   }
 
   datalen = BIO_get_mem_data(bio, &data);
-  if (data) {
+  if (data != NULL) {
     data[datalen] = '\0';
     str = pstrdup(sftp_pool, data);
   }
 
-  if (bio)
+  if (bio) {
     BIO_free(bio);
+  }
 
   return str;
 }
@@ -1195,10 +1360,13 @@ void sftp_crypto_free(int flags) {
    * and other modules want to use OpenSSL, we may be depriving those modules
    * of OpenSSL functionality.
    *
-   * At the moment, the modules known to use OpenSSL are mod_ldap,
+   * At the moment, the modules known to use OpenSSL are mod_ldap, mod_radius,
    * mod_sftp, mod_sql, and mod_sql_passwd, and mod_tls.
    */
-  if (pr_module_get("mod_ldap.c") == NULL &&
+  if (pr_module_get("mod_auth_otp.c") == NULL &&
+      pr_module_get("mod_digest.c") == NULL &&
+      pr_module_get("mod_ldap.c") == NULL &&
+      pr_module_get("mod_radius.c") == NULL &&
       pr_module_get("mod_sql.c") == NULL &&
       pr_module_get("mod_sql_passwd.c") == NULL &&
       pr_module_get("mod_tls.c") == NULL) {
diff --git a/contrib/mod_sftp/crypto.h b/contrib/mod_sftp/crypto.h
index 829b110..3ad6cda 100644
--- a/contrib/mod_sftp/crypto.h
+++ b/contrib/mod_sftp/crypto.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp misc crypto routines
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: crypto.h,v 1.4 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_CRYPTO_H
 #define MOD_SFTP_CRYPTO_H
 
+#include "mod_sftp.h"
+
 void sftp_crypto_free(int);
 const EVP_CIPHER *sftp_crypto_get_cipher(const char *, size_t *, size_t *);
 const EVP_MD *sftp_crypto_get_digest(const char *, uint32_t *);
@@ -38,4 +36,4 @@ const char *sftp_crypto_get_kexinit_digest_list(pool *);
 
 size_t sftp_crypto_get_size(size_t, size_t);
 
-#endif
+#endif /* MOD_SFTP_CRYPTO_H */
diff --git a/contrib/mod_sftp/date.c b/contrib/mod_sftp/date.c
index 4cde7b4..7784144 100644
--- a/contrib/mod_sftp/date.c
+++ b/contrib/mod_sftp/date.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp date(1) simulation
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: date.c,v 1.3 2012-02-15 23:50:51 castaglia Exp $
  */
 
 #include "mod_sftp.h"
diff --git a/contrib/mod_sftp/date.h b/contrib/mod_sftp/date.h
index 02c534e..d471d8d 100644
--- a/contrib/mod_sftp/date.h
+++ b/contrib/mod_sftp/date.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp date(1) simulation
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: date.h,v 1.2 2012-02-15 23:50:51 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_DATE_H
 #define MOD_SFTP_DATE_H
 
+#include "mod_sftp.h"
+
 int sftp_date_handle_packet(pool *, void *, uint32_t, unsigned char *,
   uint32_t);
 
@@ -41,4 +39,4 @@ int sftp_date_close_session(uint32_t);
  */
 int sftp_date_set_params(pool *, uint32_t, array_header *);
 
-#endif
+#endif /* MOD_SFTP_DATE_H */
diff --git a/contrib/mod_sftp/disconnect.c b/contrib/mod_sftp/disconnect.c
index 6f0c36b..e2faff2 100644
--- a/contrib/mod_sftp/disconnect.c
+++ b/contrib/mod_sftp/disconnect.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp disconnect msgs
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: disconnect.c,v 1.10 2012-02-15 23:50:51 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -74,6 +72,7 @@ const char *sftp_disconnect_get_str(uint32_t reason_code) {
 void sftp_disconnect_send(uint32_t reason, const char *explain,
     const char *file, int lineno, const char *func) {
   struct ssh2_packet *pkt;
+  const pr_netaddr_t *remote_addr;
   const char *lang = "en-US";
   unsigned char *buf, *ptr;
   uint32_t buflen, bufsz;
@@ -82,6 +81,8 @@ void sftp_disconnect_send(uint32_t reason, const char *explain,
   /* Send the client a DISCONNECT mesg. */
   pkt = sftp_ssh2_packet_create(sftp_pool);
 
+  remote_addr = pr_netaddr_get_sess_remote_addr();
+
   buflen = bufsz = 1024;
   ptr = buf = palloc(pkt->pool, bufsz);
 
@@ -92,13 +93,17 @@ void sftp_disconnect_send(uint32_t reason, const char *explain,
       if (explanations[i].code == reason) {
         explain = explanations[i].explain;
         lang = explanations[i].lang;
-        if (lang == NULL)
+        if (lang == NULL) {
           lang = "en-US";
-
+        }
         break;
       }
     }
 
+    if (explain == NULL) {
+      explain = "Unknown reason";
+    }
+
   } else {
     lang = "en-US";
   }
@@ -120,8 +125,8 @@ void sftp_disconnect_send(uint32_t reason, const char *explain,
   pkt->payload = ptr;
   pkt->payload_len = (bufsz - buflen);
 
-  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "disconnecting (%s)",
-    explain);
+  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+    "disconnecting %s (%s)", pr_netaddr_get_ipstr(remote_addr), explain);
 
   /* If we are called very early in the connection lifetime, then the
    * sftp_conn variable may not have been set yet, thus the conditional here.
diff --git a/contrib/mod_sftp/disconnect.h b/contrib/mod_sftp/disconnect.h
index d7aef31..53fcfee 100644
--- a/contrib/mod_sftp/disconnect.h
+++ b/contrib/mod_sftp/disconnect.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp disconnect msgs
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: disconnect.h,v 1.8 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_DISCONNECT_H
 #define MOD_SFTP_DISCONNECT_H
 
+#include "mod_sftp.h"
+
 void sftp_disconnect_conn(uint32_t, const char *, const char *, int,
   const char *);
 void sftp_disconnect_send(uint32_t, const char *, const char *, int,
@@ -57,4 +55,4 @@ const char *sftp_disconnect_get_str(uint32_t);
 
 # endif
 
-#endif
+#endif /* MOD_SFTP_DISCONNECT_H */
diff --git a/contrib/mod_sftp/display.c b/contrib/mod_sftp/display.c
index a5fb70d..e658297 100644
--- a/contrib/mod_sftp/display.c
+++ b/contrib/mod_sftp/display.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp Display files
- * Copyright (c) 2010-2013 TJ Saunders
+ * Copyright (c) 2010-2016 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
@@ -22,23 +22,26 @@
  * source distribution.
  */
 
-/* Display of files
- * $Id: display.c,v 1.12 2013-09-25 16:08:50 castaglia Exp $
- */
+/* Display of files */
 
 #include "mod_sftp.h"
 #include "display.h"
 #include "packet.h"
 #include "msg.h"
 
+/* Note: The size provided by pr_fs_getsize2() is in KB, not bytes. */
 static void format_size_str(char *buf, size_t buflen, off_t size) {
-  char *units[] = {"", "K", "M", "G", "T", "P"};
-  unsigned int nunits = 6;
+  char *units[] = {"K", "M", "G", "T", "P", "E", "Z", "Y"};
+  unsigned int nunits = 8;
   register unsigned int i = 0;
+  int res;
 
-  /* Determine the appropriate units label to use. */
+  /* Determine the appropriate units label to use. Do not exceed the max
+   * possible unit support (yottabytes), by ensuring that i maxes out at
+   * index 7 (of 8 possible units).
+   */
   while (size > 1024 &&
-         i < nunits) {
+         i < (nunits - 1)) {
     pr_signals_handle();
 
     size /= 1024;
@@ -46,29 +49,38 @@ static void format_size_str(char *buf, size_t buflen, off_t size) {
   }
 
   /* Now, prepare the buffer. */
-  snprintf(buf, buflen, "%.3" PR_LU "%sB", (pr_off_t) size, units[i]);
+  res = snprintf(buf, buflen, "%.3" PR_LU "%sB", (pr_off_t) size, units[i]);
+
+  if (res > 2) {
+    /* Check for leading zeroes; it's an aethetic choice. */
+    if (buf[0] == '0' && buf[1] != '.') {
+      memmove(&buf[0], &buf[1], res-1);
+      buf[res-1] = '\0';
+    }
+  }
 }
 
 const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
   struct stat st;
   char buf[PR_TUNABLE_BUFFER_SIZE], *msg = "";
   int len, res;
-  unsigned int *current_clients = NULL;
-  unsigned int *max_clients = NULL;
+  const unsigned int *current_clients = NULL;
+  const unsigned int *max_clients = NULL;
   off_t fs_size = 0;
-  void *v;
+  const void *v;
+  const char *outs, *rfc1413_ident, *user;
   const char *serverfqdn = main_server->ServerFQDN;
-  char *outs, mg_size[12] = {'\0'}, mg_size_units[12] = {'\0'},
+  char mg_size[12] = {'\0'}, mg_size_units[12] = {'\0'},
     mg_max[12] = "unlimited";
   char mg_class_limit[12] = {'\0'}, mg_cur[12] = {'\0'},
     mg_cur_class[12] = {'\0'};
   const char *mg_time;
-  char *rfc1413_ident = NULL, *user = NULL;
 
   /* Stat the opened file to determine the optimal buffer size for IO. */
   memset(&st, 0, sizeof(st));
-  pr_fsio_fstat(fh, &st);
-  fh->fh_iosz = st.st_blksize;
+  if (pr_fsio_fstat(fh, &st) == 0) {
+    fh->fh_iosz = st.st_blksize;
+  }
 
   res = pr_fs_fgetsize(fh->fh_fd, &fs_size);
   if (res < 0 &&
@@ -87,7 +99,7 @@ const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
   max_clients = get_param_ptr(main_server->conf, "MaxClients", FALSE);
 
   v = pr_table_get(session.notes, "client-count", NULL);
-  if (v) {
+  if (v != NULL) {
     current_clients = v;
   }
 
@@ -95,12 +107,12 @@ const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
 
   if (session.conn_class != NULL &&
       session.conn_class->cls_name) {
-    unsigned int *class_clients = NULL;
+    const unsigned int *class_clients = NULL;
     config_rec *maxc = NULL;
     unsigned int maxclients = 0;
 
     v = pr_table_get(session.notes, "class-client-count", NULL);
-    if (v) {
+    if (v != NULL) {
       class_clients = v;
     }
 
@@ -114,8 +126,7 @@ const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
 
     maxc = find_config(main_server->conf, CONF_PARAM, "MaxClientsPerClass",
       FALSE);
-
-    while (maxc) {
+    while (maxc != NULL) {
       pr_signals_handle();
 
       if (strcmp(maxc->argv[0], session.conn_class->cls_name) != 0) {
@@ -130,9 +141,9 @@ const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
 
     if (maxclients == 0) {
       maxc = find_config(main_server->conf, CONF_PARAM, "MaxClients", FALSE);
-
-      if (maxc)
+      if (maxc) {
         maxclients = *((unsigned int *) maxc->argv[0]);
+      }
     }
 
     snprintf(mg_class_limit, sizeof(mg_class_limit), "%u", maxclients);
@@ -146,8 +157,9 @@ const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
   snprintf(mg_max, sizeof(mg_max), "%u", max_clients ? *max_clients : 0);
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-  if (user == NULL)
+  if (user == NULL) {
     user = "";
+  }
 
   rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident", NULL);
   if (rfc1413_ident == NULL) {
@@ -203,15 +215,17 @@ const char *sftp_display_fh_get_msg(pool *p, pr_fh_t *fh) {
       if (strncmp(key, "%{time:", 7) == 0) {
         char time_str[128], *fmt;
         time_t now;
-        struct tm *time_info;
+        struct tm *tm;
 
         fmt = pstrndup(p, key + 7, strlen(key) - 8);
 
         now = time(NULL);
-        time_info = pr_localtime(NULL, &now);
-
         memset(time_str, 0, sizeof(time_str));
-        strftime(time_str, sizeof(time_str), fmt, time_info);
+
+        tm = pr_localtime(NULL, &now);
+        if (tm != NULL) {
+          strftime(time_str, sizeof(time_str), fmt, tm);
+        }
 
         val = pstrdup(p, time_str);
 
diff --git a/contrib/mod_sftp/display.h b/contrib/mod_sftp/display.h
index f8ab836..7e98915 100644
--- a/contrib/mod_sftp/display.h
+++ b/contrib/mod_sftp/display.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp Display files
- * Copyright (c) 2010-2011 TJ Saunders
+ * Copyright (c) 2010-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: display.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_DISPLAY_H
 #define MOD_SFTP_DISPLAY_H
 
+#include "mod_sftp.h"
+
 const char *sftp_display_fh_get_msg(pool *, pr_fh_t *);
 
 #endif /* MOD_SFTP_DISPLAY_H */
diff --git a/contrib/mod_sftp/fxp.c b/contrib/mod_sftp/fxp.c
index b189412..c3eb035 100644
--- a/contrib/mod_sftp/fxp.c
+++ b/contrib/mod_sftp/fxp.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp sftp
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -47,12 +47,21 @@
 #define SSH2_FX_ATTR_OWNERGROUP		0x00000080
 #define SSH2_FX_ATTR_SUBSECOND_TIMES	0x00000100
 #define SSH2_FX_ATTR_BITS		0x00000200
+
+/* Note that these attributes were added in draft-ietf-secsh-filexfer-06,
+ * which is SFTP protocol version 6.
+ */
 #define SSH2_FX_ATTR_ALLOCATION_SIZE	0x00000400
 #define SSH2_FX_ATTR_TEXT_HINT		0x00000800
 #define SSH2_FX_ATTR_MIME_TYPE		0x00001000
 #define SSH2_FX_ATTR_LINK_COUNT		0x00002000
 #define SSH2_FX_ATTR_UNTRANSLATED_NAME	0x00004000
+
 #define SSH2_FX_ATTR_CTIME		0x00008000
+
+/* The EXTENDED attribute was defined in draft-ietf-secsh-filexfer-02,
+ * which is SFTP protocol version 3.
+ */
 #define SSH2_FX_ATTR_EXTENDED		0x80000000
 
 /* FX_ATTR_BITS values (see draft-ietf-secsh-filexfer-13, Section 7.9) */
@@ -128,7 +137,7 @@
 #define SSH2_FXF_ACCESS_WRITE_LOCK		0x00000080
 #define SSH2_FXF_ACCESS_DELETE_LOCK		0x00000100
 
-/* FXP_REALPATH control flags */
+/* FXP_REALPATH control values */
 #define SSH2_FXRP_NO_CHECK		0x00000001
 #define SSH2_FXRP_STAT_IF		0x00000002
 #define SSH2_FXRP_STAT_ALWAYS		0x00000003
@@ -176,6 +185,10 @@
 #define SSH2_FXE_STATVFS_ST_RDONLY		0x1
 #define SSH2_FXE_STATVFS_ST_NOSUID		0x2
 
+/* xattr at proftpd.org extension flags */
+#define SSH2_FXE_XATTR_CREATE			0x1
+#define SSH2_FXE_XATTR_REPLACE			0x2
+
 extern pr_response_t *resp_list, *resp_err_list;
 
 struct fxp_dirent {
@@ -197,6 +210,11 @@ struct fxp_handle {
   /* For supporting the HiddenStores directive */
   char *fh_real_path;
 
+  /* For referencing information about the opened file; NOTE THAT THIS MAY
+   * BE STALE.
+   */
+  struct stat *fh_st;
+
   /* For tracking the number of bytes transferred for this file; for
    * better TransferLog tracking.
    */
@@ -219,6 +237,20 @@ struct fxp_packet {
   unsigned int state;
 };
 
+struct fxp_buffer {
+  /* Pointer to the start of the buffer */
+  unsigned char *ptr;
+
+  /* Total size of the buffer */
+  uint32_t bufsz;
+
+  /* Current pointer */
+  unsigned char *buf;
+
+  /* Length of buffer remaining */
+  uint32_t buflen;
+};
+
 #define	FXP_PACKET_HAVE_PACKET_LEN	0x0001
 #define	FXP_PACKET_HAVE_REQUEST_TYPE	0x0002
 #define	FXP_PACKET_HAVE_REQUEST_ID	0x0004
@@ -235,11 +267,26 @@ static size_t fxp_packet_data_allocsz = 0;
 #define FXP_PACKET_DATA_DEFAULT_SZ		(1024 * 16)
 #define FXP_RESPONSE_DATA_DEFAULT_SZ		512
 
+#ifdef PR_USE_XATTR
+/* Allocate larger buffers for extended attributes */
+# define FXP_RESPONSE_NAME_DEFAULT_SZ		(1024 * 4)
+#endif /* PR_USE_XATTR */
+
+#ifndef FXP_RESPONSE_NAME_DEFAULT_SZ
+# define FXP_RESPONSE_NAME_DEFAULT_SZ		FXP_RESPONSE_DATA_DEFAULT_SZ
+#endif
+
 #define FXP_MAX_PACKET_LEN			(1024 * 512)
-#define FXP_MAX_EXTENDED_ATTRIBUTES		100
+
+/* Maximum number of SFTP extended attributes we accept at one time. */
+#ifndef FXP_MAX_EXTENDED_ATTRIBUTES
+# define FXP_MAX_EXTENDED_ATTRIBUTES		100
+#endif
 
 /* Maximum length of SFTP extended attribute name OR value. */
-#define FXP_MAX_EXTENDED_ATTR_LEN		1024
+#ifndef FXP_MAX_EXTENDED_ATTR_LEN
+# define FXP_MAX_EXTENDED_ATTR_LEN		1024
+#endif
 
 struct fxp_extpair {
   char *ext_name;
@@ -282,79 +329,6 @@ static struct fxp_handle *fxp_handle_get(const char *);
 static struct fxp_packet *fxp_packet_create(pool *, uint32_t);
 static int fxp_packet_write(struct fxp_packet *);
 
-/* XXX These two namelist-related functions are the same as the ones
- * in kex.c; the code should be refactored out into a misc.c or utils.c
- * or somesuch.
- */
-
-static array_header *fxp_parse_namelist(pool *p, const char *names) {
-  char *ptr;
-  array_header *list;
-  size_t names_len;
-
-  list = make_array(p, 0, sizeof(const char *));
-  names_len = strlen(names);
-
-  ptr = memchr(names, ',', names_len);
-  while (ptr) {
-    char *elt;
-    size_t elt_len;
-
-    pr_signals_handle();
-
-    elt_len = ptr - names;
-
-    elt = palloc(p, elt_len + 1);
-    memcpy(elt, names, elt_len);
-    elt[elt_len] = '\0';
-
-    *((const char **) push_array(list)) = elt;
-    names = ++ptr;
-    names_len -= elt_len;
-
-    ptr = memchr(names, ',', names_len);
-  }
-  *((const char **) push_array(list)) = pstrdup(p, names);
-
-  return list;
-}
-
-static const char *fxp_get_shared_name(pool *p, const char *c2s_names,
-    const char *s2c_names) {
-  register unsigned int i;
-  const char *name = NULL, **client_names, **server_names;
-  pool *tmp_pool;
-  array_header *client_list, *server_list;
-
-  tmp_pool = make_sub_pool(p);
-  pr_pool_tag(tmp_pool, "SFTP shared name pool");
-
-  client_list = fxp_parse_namelist(tmp_pool, c2s_names);
-  client_names = (const char **) client_list->elts;
-
-  server_list = fxp_parse_namelist(tmp_pool, s2c_names);
-  server_names = (const char **) server_list->elts;
-
-  for (i = 0; i < client_list->nelts; i++) {
-    register unsigned int j;
-
-    if (name)
-      break;
-
-    for (j = 0; j < server_list->nelts; j++) {
-      if (strcmp(client_names[i], server_names[j]) == 0) {
-        name = client_names[i];
-        break;
-      }
-    }
-  }
-
-  name = pstrdup(p, name);
-  destroy_pool(tmp_pool);
-
-  return name;
-}
-
 static struct fxp_session *fxp_get_session(uint32_t channel_id) {
   struct fxp_session *sess;
 
@@ -493,6 +467,12 @@ static uint32_t fxp_errno2status(int xerrno, const char **reason) {
 #ifdef ENXIO
     case ENXIO:
 #endif
+#if defined(ENODATA)
+    case ENODATA:
+#endif
+#if defined(ENOATTR) && defined(ENODATA) && ENOATTR != ENODATA
+    case ENOATTR:
+#endif
       status_code = SSH2_FX_NO_SUCH_FILE;
       if (reason) {
         *reason = fxp_strerror(status_code);
@@ -515,6 +495,9 @@ static uint32_t fxp_errno2status(int xerrno, const char **reason) {
       break;
 
     case ENOSYS:
+#ifdef ENOTSUP
+    case ENOTSUP:
+#endif
       status_code = SSH2_FX_OP_UNSUPPORTED;
       if (reason) {
         *reason = fxp_strerror(status_code);
@@ -523,6 +506,12 @@ static uint32_t fxp_errno2status(int xerrno, const char **reason) {
 
     case EFAULT:
     case EINVAL:
+#ifdef E2BIG
+    case E2BIG:
+#endif
+#ifdef ERANGE
+    case ERANGE:
+#endif
       if (reason) {
         *reason = fxp_strerror(SSH2_FX_INVALID_PARAMETER);
       }
@@ -1028,25 +1017,46 @@ static const char *fxp_strtime(pool *p, time_t t) {
   static char *mons[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
     "Aug", "Sep", "Oct", "Nov", "Dec" };
   static char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
-  struct tm *tr;
+  struct tm *tm;
 
   memset(buf, '\0', sizeof(buf));
 
-  tr = pr_gmtime(p, &t);
-  if (tr != NULL) {
+  tm = pr_gmtime(p, &t);
+  if (tm != NULL) {
     snprintf(buf, sizeof(buf), "%s %s %2d %02d:%02d:%02d %d",
-      days[tr->tm_wday], mons[tr->tm_mon], tr->tm_mday, tr->tm_hour,
-      tr->tm_min, tr->tm_sec, tr->tm_year + 1900);
+      days[tm->tm_wday], mons[tm->tm_mon], tm->tm_mday, tm->tm_hour,
+      tm->tm_min, tm->tm_sec, tm->tm_year + 1900);
 
   } else {
     buf[0] = '\0';
   }
 
   buf[sizeof(buf)-1] = '\0';
-
   return buf;
 }
 
+static void fxp_cmd_dispatch(cmd_rec *cmd) {
+  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
+  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  pr_response_clear(&resp_list);
+}
+
+static void fxp_cmd_dispatch_err(cmd_rec *cmd) {
+  pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
+  pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+  pr_response_clear(&resp_err_list);
+}
+
+static void fxp_cmd_note_file_status(cmd_rec *cmd, const char *status) {
+  if (pr_table_add(cmd->notes, "mod_sftp.file-status",
+      pstrdup(cmd->pool, status), 0) < 0) {
+    if (errno != EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+        "error stashing file status in command notes: %s", strerror(errno));
+    }
+  }
+}
+
 static const char *fxp_get_request_type_desc(unsigned char request_type) {
   switch (request_type) {
     case SFTP_SSH2_FXP_INIT:
@@ -1173,7 +1183,7 @@ static int fxp_path_pass_regex_filters(pool *p, const char *request,
 }
 
 /* FXP_STATUS messages */
-static void fxp_status_write(unsigned char **buf, uint32_t *buflen,
+static void fxp_status_write(pool *p, unsigned char **buf, uint32_t *buflen,
     uint32_t request_id, uint32_t status_code, const char *status_msg,
     const char *extra_data) {
   char num[32];
@@ -1186,9 +1196,9 @@ static void fxp_status_write(unsigned char **buf, uint32_t *buflen,
   pr_response_clear(&resp_err_list);
 
   memset(num, '\0', sizeof(num));
-  snprintf(num, sizeof(num), "%lu", (unsigned long) status_code);
+  snprintf(num, sizeof(num)-1, "%lu", (unsigned long) status_code);
   num[sizeof(num)-1] = '\0';
-  pr_response_add(pstrdup(fxp_session->pool, num), "%s", status_msg);
+  pr_response_add(pstrdup(p, num), "%s", status_msg);
 
   sftp_msg_write_byte(buf, buflen, SFTP_SSH2_FXP_STATUS);
   sftp_msg_write_int(buf, buflen, request_id);
@@ -1318,8 +1328,8 @@ static void fxp_msg_write_extpair(unsigned char **buf, uint32_t *buflen,
 }
 
 static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
-    uint32_t attr_flags, unsigned char **buf, uint32_t *buflen,
-    struct fxp_packet *fxp) {
+    uint32_t attr_flags, array_header *xattrs, unsigned char **buf,
+    uint32_t *buflen, struct fxp_packet *fxp) {
   struct stat st;
   int res;
 
@@ -1331,6 +1341,7 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
     res = pr_fsio_fstat(fh, &st);
 
   } else {
+    pr_fs_clear_cache2(path);
     res = pr_fsio_lstat(path, &st);
   }
 
@@ -1348,7 +1359,8 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(buf, buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     errno = xerrno;
     return -1;
@@ -1392,17 +1404,16 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
           "('%s' [%d])", (unsigned long) status_code, reason,
           xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-        fxp_status_write(buf, buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+          reason, NULL);
 
         errno = xerrno;
         return -1;
-
-      } else {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "client set permissions on '%s' to 0%o", path,
-          (unsigned int) (attrs->st_mode & ~S_IFMT));
       }
+
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "client set permissions on '%s' to 0%o", path,
+        (unsigned int) (attrs->st_mode & ~S_IFMT));
     }
   }
 
@@ -1449,9 +1460,9 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
         int xerrno = errno;
 
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "error changing ownership of '%s' to UID %lu, GID %lu: %s",
-          path, (unsigned long) client_uid, (unsigned long) client_gid,
-          strerror(xerrno));
+          "error changing ownership of '%s' to UID %s, GID %s: %s",
+          path, pr_uid2str(fxp->pool, client_uid),
+          pr_gid2str(fxp->pool, client_gid), strerror(xerrno));
 
         status_code = fxp_errno2status(xerrno, &reason);
 
@@ -1459,17 +1470,17 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
           "('%s' [%d])", (unsigned long) status_code, reason,
           xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-        fxp_status_write(buf, buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+          reason, NULL);
 
         errno = xerrno;
         return -1;
-
-      } else {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "client set ownership of '%s' to UID %lu, GID %lu",
-          path, (unsigned long) client_uid, (unsigned long) client_gid);
       }
+
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "client set ownership of '%s' to UID %s, GID %s",
+        path, pr_uid2str(fxp->pool, client_uid),
+        pr_gid2str(fxp->pool, client_gid));
     }
   }
 
@@ -1509,17 +1520,16 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
           "('%s' [%d])", (unsigned long) status_code, reason,
           xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-        fxp_status_write(buf, buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+          reason, NULL);
 
         errno = xerrno;
         return -1;
-
-      } else {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "client set size of '%s' to %" PR_LU " bytes", path,
-          (pr_off_t) attrs->st_size);
       }
+
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "client set size of '%s' to %" PR_LU " bytes", path,
+        (pr_off_t) attrs->st_size);
     }
   }
 
@@ -1557,22 +1567,81 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
           "('%s' [%d])", (unsigned long) status_code, reason,
           xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-        fxp_status_write(buf, buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+          reason, NULL);
 
         errno = xerrno;
         return -1;
-
-      } else {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "client set access time of '%s' to %s, modification time to %s",
-          path, fxp_strtime(fxp->pool, attrs->st_atime),
-          fxp_strtime(fxp->pool, attrs->st_mtime));
       }
+
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "client set access time of '%s' to %s, modification time to %s",
+        path, fxp_strtime(fxp->pool, attrs->st_atime),
+        fxp_strtime(fxp->pool, attrs->st_mtime));
     }
   }
 
   if (fxp_session->client_version > 3) {
+    /* Note: we handle the xattrs FIRST, before the timestamps, so that
+     * setting the xattrs does not change the expected timestamps, thus
+     * preserving the principle of least surprise.
+     */
+    if (attr_flags & SSH2_FX_ATTR_EXTENDED) {
+#ifdef PR_USE_XATTR
+      if (xattrs != NULL &&
+          xattrs->nelts > 0) {
+        register unsigned int i;
+        struct fxp_extpair **ext_pairs;
+
+        ext_pairs = xattrs->elts;
+        for (i = 0; i < xattrs->nelts; i++) {
+          struct fxp_extpair *xattr;
+          const char *xattr_name;
+          void *xattr_val;
+          size_t xattr_valsz;
+
+          xattr = ext_pairs[i];
+          xattr_name = xattr->ext_name;
+          xattr_val = xattr->ext_data;
+          xattr_valsz = (size_t) xattr->ext_datalen;
+
+          if (fh != NULL) {
+            res = pr_fsio_fsetxattr(fxp->pool, fh, xattr_name, xattr_val,
+              xattr_valsz, 0);
+
+          } else {
+            res = pr_fsio_lsetxattr(fxp->pool, path, xattr_name, xattr_val,
+              xattr_valsz, 0);
+          }
+
+          if (res < 0) {
+            uint32_t status_code;
+            const char *reason;
+            int xerrno = errno;
+
+            (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+              "error setting xattr '%s' (%lu bytes) on '%s': %s", xattr_name,
+              (unsigned long) xattr_valsz, path, strerror(xerrno));
+
+            status_code = fxp_errno2status(xerrno, &reason);
+
+            pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+              "('%s' [%d])", (unsigned long) status_code, reason,
+              strerror(xerrno), xerrno);
+
+            fxp_status_write(fxp->pool, buf, buflen, fxp->request_id,
+              status_code, reason, NULL);
+
+            errno = xerrno;
+            return -1;
+          }
+        }
+      }
+#else
+      (void) xattrs;
+#endif /* PR_USE_XATTR */
+    }
+
     if (attr_flags & SSH2_FX_ATTR_ACCESSTIME) {
       if (st.st_atime != attrs->st_atime) {
         struct timeval tvs[2];
@@ -1604,17 +1673,16 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
             "('%s' [%d])", (unsigned long) status_code, reason,
             xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-          fxp_status_write(buf, buflen, fxp->request_id, status_code, reason,
-            NULL);
+          fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+            reason, NULL);
 
           errno = xerrno;
           return -1;
-
-        } else {
-          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-            "client set access time of '%s' to %s", path,
-            fxp_strtime(fxp->pool, attrs->st_atime));
         }
+
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "client set access time of '%s' to %s", path,
+          fxp_strtime(fxp->pool, attrs->st_atime));
       }
     }
 
@@ -1650,17 +1718,16 @@ static int fxp_attrs_set(pr_fh_t *fh, const char *path, struct stat *attrs,
             "('%s' [%d])", (unsigned long) status_code, reason,
             xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-          fxp_status_write(buf, buflen, fxp->request_id, status_code, reason,
-            NULL);
+          fxp_status_write(fxp->pool, buf, buflen, fxp->request_id, status_code,
+            reason, NULL);
 
           errno = xerrno;
           return -1;
-
-        } else {
-          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-            "client set modification time of '%s' to %s", path,
-            fxp_strtime(fxp->pool, attrs->st_mtime));
         }
+
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "client set modification time of '%s' to %s", path,
+          fxp_strtime(fxp->pool, attrs->st_mtime));
       }
     }
   }
@@ -1731,6 +1798,13 @@ static char *fxp_strattrs(pool *p, struct stat *st, uint32_t *attr_flags) {
       flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_PERMISSIONS|
         SSH2_FX_ATTR_ACCESSTIME|SSH2_FX_ATTR_MODIFYTIME|
         SSH2_FX_ATTR_OWNERGROUP;
+
+      if (fxp_session->client_version >= 6) {
+        flags |= SSH2_FX_ATTR_LINK_COUNT;
+#ifdef PR_USE_XATTR
+        flags |= SSH2_FX_ATTR_EXTENDED;
+#endif /* PR_USE_XATTR */
+      }
     }
   }
 
@@ -1746,19 +1820,19 @@ static char *fxp_strattrs(pool *p, struct stat *st, uint32_t *attr_flags) {
 
   if ((flags & SSH2_FX_ATTR_UIDGID) ||
       (flags & SSH2_FX_ATTR_OWNERGROUP)) {
-    snprintf(ptr, bufsz - buflen, "UNIX.owner=%lu;",
-      (unsigned long) st->st_uid);
+    snprintf(ptr, bufsz - buflen, "UNIX.owner=%s;",
+      pr_uid2str(NULL, st->st_uid));
     buflen = strlen(buf);
     ptr = buf + buflen;
 
-    snprintf(ptr, bufsz - buflen, "UNIX.group=%lu;",
-      (unsigned long) st->st_gid);
+    snprintf(ptr, bufsz - buflen, "UNIX.group=%s;",
+      pr_gid2str(NULL, st->st_gid));
     buflen = strlen(buf);
     ptr = buf + buflen;
   }
 
   if (flags & SSH2_FX_ATTR_PERMISSIONS) {
-    snprintf(ptr, bufsz - buflen, "UNIX.mode=0%o;",
+    snprintf(ptr, bufsz - buflen, "UNIX.mode=%04o;",
       (unsigned int) st->st_mode & 07777);
     buflen = strlen(buf);
     ptr = buf + buflen;
@@ -1768,17 +1842,30 @@ static char *fxp_strattrs(pool *p, struct stat *st, uint32_t *attr_flags) {
     if (flags & SSH2_FX_ATTR_ACMODTIME) {
       struct tm *tm;
 
-      tm = pr_gmtime(p, &st->st_atime);
-      snprintf(ptr, bufsz - buflen, "access=%04d%02d%02d%02d%02d%02d;",
-        tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
-        tm->tm_sec);
-      buflen = strlen(buf);
-      ptr = buf + buflen;
+      tm = pr_gmtime(p, (const time_t *) &st->st_atime);
+      if (tm != NULL) {
+        snprintf(ptr, bufsz - buflen, "access=%04d%02d%02d%02d%02d%02d;",
+          tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
+          tm->tm_sec);
+        buflen = strlen(buf);
+        ptr = buf + buflen;
+
+      } else {
+        pr_trace_msg(trace_channel, 1,
+          "error obtaining st_atime GMT timestamp: %s", strerror(errno));
+      }
+
+      tm = pr_gmtime(p, (const time_t *) &st->st_mtime);
+      if (tm != NULL) {
+        snprintf(ptr, bufsz - buflen, "modify=%04d%02d%02d%02d%02d%02d;",
+          tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
+          tm->tm_sec);
+
+      } else {
+        pr_trace_msg(trace_channel, 1,
+          "error obtaining st_mtime GMT timestamp: %s", strerror(errno));
+      }
 
-      tm = pr_gmtime(p, &st->st_mtime);
-      snprintf(ptr, bufsz - buflen, "modify=%04d%02d%02d%02d%02d%02d;",
-        tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
-        tm->tm_sec);
       buflen = strlen(buf);
       ptr = buf + buflen;
     }
@@ -1787,10 +1874,17 @@ static char *fxp_strattrs(pool *p, struct stat *st, uint32_t *attr_flags) {
     if (flags & SSH2_FX_ATTR_ACCESSTIME) {
       struct tm *tm;
 
-      tm = pr_gmtime(p, &st->st_atime);
-      snprintf(ptr, bufsz - buflen, "access=%04d%02d%02d%02d%02d%02d;",
-        tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
-        tm->tm_sec);
+      tm = pr_gmtime(p, (const time_t *) &st->st_atime);
+      if (tm != NULL) {
+        snprintf(ptr, bufsz - buflen, "access=%04d%02d%02d%02d%02d%02d;",
+          tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
+          tm->tm_sec);
+
+      } else {
+        pr_trace_msg(trace_channel, 1,
+          "error obtaining st_atime GMT timestamp: %s", strerror(errno));
+      }
+
       buflen = strlen(buf);
       ptr = buf + buflen;
     }
@@ -1798,10 +1892,24 @@ static char *fxp_strattrs(pool *p, struct stat *st, uint32_t *attr_flags) {
     if (flags & SSH2_FX_ATTR_MODIFYTIME) {
       struct tm *tm;
 
-      tm = pr_gmtime(p, &st->st_mtime);
-      snprintf(ptr, bufsz - buflen, "modify=%04d%02d%02d%02d%02d%02d;",
-        tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
-        tm->tm_sec);
+      tm = pr_gmtime(p, (const time_t *) &st->st_mtime);
+      if (tm != NULL) {
+        snprintf(ptr, bufsz - buflen, "modify=%04d%02d%02d%02d%02d%02d;",
+          tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min,
+          tm->tm_sec);
+
+      } else {
+        pr_trace_msg(trace_channel, 1,
+          "error obtaining st_mtime GMT timestamp: %s", strerror(errno));
+      }
+
+      buflen = strlen(buf);
+      ptr = buf + buflen;
+    }
+
+    if (flags & SSH2_FX_ATTR_LINK_COUNT) {
+      snprintf(ptr, bufsz - buflen, "UNIX.nlink=%lu;",
+        (unsigned long) st->st_nlink);
       buflen = strlen(buf);
       ptr = buf + buflen;
     }
@@ -1880,8 +1988,48 @@ static char *fxp_stroflags(pool *p, int flags) {
   return str;
 }
 
+static array_header *fxp_xattrs_read(pool *p, unsigned char **buf,
+    uint32_t *buflen) {
+  register unsigned int i;
+  uint32_t extpair_count;
+  array_header *xattrs = NULL;
+
+  extpair_count = sftp_msg_read_int(p, buf, buflen);
+  pr_trace_msg(trace_channel, 15,
+    "protocol version %lu: read EXTENDED attribute: %lu extensions",
+    (unsigned long) fxp_session->client_version,
+    (unsigned long) extpair_count);
+
+  if (extpair_count > FXP_MAX_EXTENDED_ATTRIBUTES) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "received too many EXTENDED attributes (%lu > max %lu), "
+      "truncating to max", (unsigned long) extpair_count,
+      (unsigned long) FXP_MAX_EXTENDED_ATTRIBUTES);
+    extpair_count = FXP_MAX_EXTENDED_ATTRIBUTES;
+  }
+
+  xattrs = make_array(p, 1, sizeof(struct fxp_extpair *));
+
+  for (i = 0; i < extpair_count; i++) {
+    struct fxp_extpair *ext;
+
+    ext = fxp_msg_read_extpair(p, buf, buflen);
+    if (ext != NULL) {
+      pr_trace_msg(trace_channel, 15,
+        "protocol version %lu: read EXTENDED attribute: "
+        "extension '%s' (%lu bytes of data)",
+        (unsigned long) fxp_session->client_version, ext->ext_name,
+        (unsigned long) ext->ext_datalen);
+
+      *((struct fxp_extpair **) push_array(xattrs)) = ext;
+    }
+  }
+
+  return xattrs;
+}
+
 static struct stat *fxp_attrs_read(struct fxp_packet *fxp, unsigned char **buf,
-    uint32_t *buflen, uint32_t *flags) {
+    uint32_t *buflen, uint32_t *flags, array_header **xattrs) {
   struct stat *st;
 
   st = pcalloc(fxp->pool, sizeof(struct stat));
@@ -1907,8 +2055,6 @@ static struct stat *fxp_attrs_read(struct fxp_packet *fxp, unsigned char **buf,
       st->st_mtime = sftp_msg_read_int(fxp->pool, buf, buflen);
     }
 
-    /* XXX Vendor-specific extensions */
-
   } else {
     char file_type;
 
@@ -2002,8 +2148,8 @@ static struct stat *fxp_attrs_read(struct fxp_packet *fxp, unsigned char **buf,
         pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
           (unsigned long) status_code, fxp_strerror(status_code));
 
-        fxp_status_write(&buf2, &buflen2, fxp->request_id, status_code,
-          fxp_strerror(status_code), name);
+        fxp_status_write(fxp->pool, &buf2, &buflen2, fxp->request_id,
+          status_code, fxp_strerror(status_code), name);
 
         resp = fxp_packet_create(fxp->pool, fxp->channel_id);
         resp->payload = ptr2;
@@ -2038,8 +2184,8 @@ static struct stat *fxp_attrs_read(struct fxp_packet *fxp, unsigned char **buf,
         pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
           (unsigned long) status_code, fxp_strerror(status_code));
 
-        fxp_status_write(&buf2, &buflen2, fxp->request_id, status_code,
-          fxp_strerror(status_code), name);
+        fxp_status_write(fxp->pool, &buf2, &buflen2, fxp->request_id,
+          status_code, fxp_strerror(status_code), name);
 
         resp = fxp_packet_create(fxp->pool, fxp->channel_id);
         resp->payload = ptr2;
@@ -2216,33 +2362,16 @@ static struct stat *fxp_attrs_read(struct fxp_packet *fxp, unsigned char **buf,
         (unsigned long) fxp_session->client_version,
         untranslated ? untranslated : "(nil)");
     }
+  }
 
-    /* Vendor-specific extensions */
-    if (*flags & SSH2_FX_ATTR_EXTENDED) {
-      /* Read (and ignore) the EXTENDED attribute. */
-      register unsigned int i;
-      uint32_t extpair_count;
+  if (*flags & SSH2_FX_ATTR_EXTENDED) {
+    array_header *ext_attrs;
 
-      extpair_count = sftp_msg_read_int(fxp->pool, buf, buflen);
-      pr_trace_msg(trace_channel, 15,
-        "protocol version %lu: read EXTENDED attribute: %lu extensions",
-        (unsigned long) fxp_session->client_version,
-        (unsigned long) extpair_count);
-
-      for (i = 0; i < extpair_count; i++) {
-        struct fxp_extpair *ext;
-
-        ext = fxp_msg_read_extpair(fxp->pool, buf, buflen);
-        if (ext != NULL) {
-          pr_trace_msg(trace_channel, 15,
-            "protocol version %lu: read EXTENDED attribute: "
-            "extension '%s' (%lu bytes of data)",
-            (unsigned long) fxp_session->client_version, ext->ext_name,
-            (unsigned long) ext->ext_datalen);
-        }
-      }
+    /* Read the EXTENDED attribute. */
+    ext_attrs = fxp_xattrs_read(fxp->pool, buf, buflen);
+    if (xattrs != NULL) {
+      *xattrs = ext_attrs;
     }
-
   }
 
   return st;
@@ -2302,23 +2431,134 @@ static char fxp_get_file_type(mode_t mode) {
   return SSH2_FX_ATTR_FTYPE_UNKNOWN;
 }
 
-static uint32_t fxp_attrs_write(pool *p, unsigned char **buf, uint32_t *buflen,
-    struct stat *st, const char *user_owner, const char *group_owner) {
-  uint32_t flags, len = 0;
+static uint32_t fxp_xattrs_write(pool *p, struct fxp_buffer *fxb,
+    const char *path) {
+  uint32_t len = 0;
+
+#ifdef PR_USE_XATTR
+  int res;
+  array_header *names = NULL;
+
+  res = pr_fsio_llistxattr(p, path, &names);
+  if (res > 0) {
+    register unsigned int i;
+    pool *sub_pool;
+    uint32_t xattrsz = 0;
+    array_header *vals;
+
+    sub_pool = make_sub_pool(p);
+    pr_pool_tag(sub_pool, "listxattr pool");
+
+    vals = make_array(sub_pool, names->nelts, sizeof(pr_buffer_t *));
+    xattrsz = sizeof(uint32_t);
+
+    for (i = 0; i < names->nelts; i++) {
+      const char *name;
+      pr_buffer_t *val;
+      ssize_t valsz;
+
+      name = ((const char **) names->elts)[i];
+      xattrsz += (sizeof(uint32_t) + strlen(name));
+
+      val = pcalloc(sub_pool, sizeof(pr_buffer_t));
+
+      valsz = pr_fsio_lgetxattr(p, path, name, NULL, 0);
+      if (valsz > 0) {
+        xattrsz += (sizeof(uint32_t) + valsz);
+
+        val->buflen = valsz;
+        val->buf = palloc(sub_pool, valsz);
+
+        valsz = pr_fsio_lgetxattr(p, path, name, val->buf, valsz);
+        if (valsz > 0) {
+          *((pr_buffer_t **) push_array(vals)) = val;
+        }
+      } else {
+        /* Push the empty buffer into the list, so that the vals list
+         * lines up with the names list.
+         */
+        *((pr_buffer_t **) push_array(vals)) = val;
+      }
+    }
+
+    if (fxb->buflen < xattrsz) {
+      unsigned char *ptr;
+      uint32_t bufsz, resp_len;
+
+      resp_len = fxb->bufsz - fxb->buflen;
+
+      /* Allocate a buffer large enough for the xattrs */
+      pr_trace_msg(trace_channel, 3,
+        "allocating larger response buffer (have %lu bytes, need %lu bytes)",
+        (unsigned long) fxb->bufsz, (unsigned long) fxb->bufsz + xattrsz);
+
+      bufsz = fxb->bufsz + xattrsz;
+      ptr = palloc(p, bufsz);
+
+      /* Copy over our existing response data into the new buffer. */
+      memcpy(ptr, fxb->ptr, resp_len);
+      fxb->ptr = ptr;
+      fxb->bufsz = bufsz;
+      fxb->buf = ptr + resp_len;
+      fxb->buflen = bufsz - resp_len;
+    }
+
+    len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), names->nelts);
+    for (i = 0; i < names->nelts; i++) {
+      const char *name;
+      pr_buffer_t *val;
+
+      name = ((const char **) names->elts)[i];
+      val = ((pr_buffer_t **) vals->elts)[i];
+
+      len += sftp_msg_write_string(&(fxb->buf), &(fxb->buflen), name);
+      len += sftp_msg_write_data(&(fxb->buf), &(fxb->buflen),
+        (const unsigned char *) val->buf, (size_t) val->buflen, TRUE);
+    }
+
+    destroy_pool(sub_pool);
+
+  } else {
+    /* Have to write an extended count of zero. */
+    len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), 0);
+  }
+#endif /* PR_USE_XATTR */
+
+  return len;
+}
+
+static uint32_t fxp_attrs_write(pool *p, struct fxp_buffer *fxb,
+    const char *path, struct stat *st, uint32_t flags,
+    const char *user_owner, const char *group_owner) {
+  uint32_t len = 0;
   mode_t perms;
 
   if (fxp_session->client_version <= 3) {
-    flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
-      SSH2_FX_ATTR_ACMODTIME;
     perms = st->st_mode;
 
-    len += sftp_msg_write_int(buf, buflen, flags);
-    len += sftp_msg_write_long(buf, buflen, st->st_size);
-    len += sftp_msg_write_int(buf, buflen, st->st_uid);
-    len += sftp_msg_write_int(buf, buflen, st->st_gid);
-    len += sftp_msg_write_int(buf, buflen, perms);
-    len += sftp_msg_write_int(buf, buflen, st->st_atime);
-    len += sftp_msg_write_int(buf, buflen, st->st_mtime);
+    len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), flags);
+
+    if (flags & SSH2_FX_ATTR_SIZE) {
+      len += sftp_msg_write_long(&(fxb->buf), &(fxb->buflen), st->st_size);
+    }
+
+    if (flags & SSH2_FX_ATTR_UIDGID) {
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), st->st_uid);
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), st->st_gid);
+    }
+
+    if (flags & SSH2_FX_ATTR_PERMISSIONS) {
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), perms);
+    }
+
+    if (flags & SSH2_FX_ATTR_ACMODTIME) {
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), st->st_atime);
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), st->st_mtime);
+    }
+
+    if (flags & SSH2_FX_ATTR_EXTENDED) {
+      len += fxp_xattrs_write(p, fxb, path);
+    }
 
   } else {
     char file_type;
@@ -2330,34 +2570,55 @@ static uint32_t fxp_attrs_write(pool *p, unsigned char **buf, uint32_t *buflen,
      */
     perms &= ~S_IFMT;
 
-    flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_PERMISSIONS|SSH2_FX_ATTR_ACCESSTIME|
-      SSH2_FX_ATTR_MODIFYTIME|SSH2_FX_ATTR_OWNERGROUP;
-
     file_type = fxp_get_file_type(st->st_mode);
 
-    len += sftp_msg_write_int(buf, buflen, flags);
-    len += sftp_msg_write_byte(buf, buflen, file_type);
-    len += sftp_msg_write_long(buf, buflen, st->st_size);
+    len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), flags);
+    len += sftp_msg_write_byte(&(fxb->buf), &(fxb->buflen), file_type);
 
-    if (user_owner == NULL) {
-      len += sftp_msg_write_string(buf, buflen,
-        pr_auth_uid2name(p, st->st_uid));
+    if (flags & SSH2_FX_ATTR_SIZE) {
+      len += sftp_msg_write_long(&(fxb->buf), &(fxb->buflen), st->st_size);
+    }
 
-    } else {
-      len += sftp_msg_write_string(buf, buflen, user_owner);
+    if (flags & SSH2_FX_ATTR_OWNERGROUP) {
+      const char *user_name, *group_name;
+
+      if (user_owner == NULL) {
+        user_name = pr_auth_uid2name(p, st->st_uid);
+
+      } else {
+        user_name = user_owner;
+      }
+
+      if (group_owner == NULL) {
+        group_name = pr_auth_gid2name(p, st->st_gid);
+
+      } else {
+        group_name = group_owner;
+      }
+
+      len += sftp_msg_write_string(&(fxb->buf), &(fxb->buflen), user_name);
+      len += sftp_msg_write_string(&(fxb->buf), &(fxb->buflen), group_name);
     }
 
-    if (group_owner == NULL) {
-      len += sftp_msg_write_string(buf, buflen,
-        pr_auth_gid2name(p, st->st_gid));
+    if (flags & SSH2_FX_ATTR_PERMISSIONS) {
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), perms);
+    }
 
-    } else {
-      len += sftp_msg_write_string(buf, buflen, group_owner);
+    if (flags & SSH2_FX_ATTR_ACCESSTIME) {
+      len += sftp_msg_write_long(&(fxb->buf), &(fxb->buflen), st->st_atime);
     }
 
-    len += sftp_msg_write_int(buf, buflen, perms);
-    len += sftp_msg_write_long(buf, buflen, st->st_atime);
-    len += sftp_msg_write_long(buf, buflen, st->st_mtime);
+    if (flags & SSH2_FX_ATTR_MODIFYTIME) {
+      len += sftp_msg_write_long(&(fxb->buf), &(fxb->buflen), st->st_mtime);
+    }
+
+    if (flags & SSH2_FX_ATTR_LINK_COUNT) {
+      len += sftp_msg_write_int(&(fxb->buf), &(fxb->buflen), st->st_nlink);
+    }
+
+    if (flags & SSH2_FX_ATTR_EXTENDED) {
+      len += fxp_xattrs_write(p, fxb, path);
+    }
   }
 
   return len;
@@ -2428,7 +2689,7 @@ static char *fxp_strmode(pool *p, mode_t mode) {
 static char *fxp_get_path_listing(pool *p, const char *path, struct stat *st,
     const char *user_owner, const char *group_owner) {
   const char *user, *group;
-  char listing[256], *mode_str, time_str[64];
+  char listing[1024], *mode_str, time_str[64];
   struct tm *t;
   int user_len, group_len;
   size_t time_strlen;
@@ -2440,10 +2701,10 @@ static char *fxp_get_path_listing(pool *p, const char *path, struct stat *st,
   mode_str = fxp_strmode(p, st->st_mode); 
 
   if (fxp_use_gmt) {
-    t = pr_gmtime(p, (time_t *) &st->st_mtime);
+    t = pr_gmtime(p, (const time_t *) &st->st_mtime);
 
   } else {
-    t = pr_localtime(p, (time_t *) &st->st_mtime);
+    t = pr_localtime(p, (const time_t *) &st->st_mtime);
   }
 
   /* Use strftime(3) to format the time entry for us.  Seems some SFTP clients
@@ -2495,6 +2756,7 @@ static struct fxp_dirent *fxp_get_dirent(pool *p, cmd_rec *cmd,
   struct stat st;
   int hidden = 0, res;
 
+  pr_fs_clear_cache2(real_path);
   if (pr_fsio_lstat(real_path, &st) < 0) {
     return NULL;
   }
@@ -2537,33 +2799,31 @@ static struct fxp_dirent *fxp_get_dirent(pool *p, cmd_rec *cmd,
   return fxd;
 }
 
-static uint32_t fxp_name_write(pool *p, unsigned char **buf, uint32_t *buflen,
-    const char *path, struct stat *st, const char *user_owner,
-    const char *group_owner) {
+static uint32_t fxp_name_write(pool *p, struct fxp_buffer *fxb,
+    const char *path, struct stat *st, uint32_t attr_flags,
+    const char *user_owner, const char *group_owner) {
   uint32_t len = 0;
+  const char *encoded_path;
 
+  encoded_path = path;
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-    len += sftp_msg_write_string(buf, buflen, sftp_utf8_encode_str(p, path));
-
-  } else {
-    len += sftp_msg_write_string(buf, buflen, path);
+    encoded_path = sftp_utf8_encode_str(p, encoded_path);
   }
 
+  len += sftp_msg_write_string(&(fxb->buf), &(fxb->buflen), encoded_path);
+
   if (fxp_session->client_version <= 3) {
     char *path_desc;
 
     path_desc = fxp_get_path_listing(p, path, st, user_owner, group_owner);
-
     if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-      len += sftp_msg_write_string(buf, buflen,
-        sftp_utf8_encode_str(p, path_desc));
-
-    } else {
-      len += sftp_msg_write_string(buf, buflen, path_desc);
+      path_desc = sftp_utf8_encode_str(p, path_desc);
     }
+
+    len += sftp_msg_write_string(&(fxb->buf), &(fxb->buflen), path_desc);
   }
 
-  len += fxp_attrs_write(p, buf, buflen, st, user_owner, group_owner);
+  len += fxp_attrs_write(p, fxb, path, st, attr_flags, user_owner, group_owner);
   return len;
 }
 
@@ -2595,6 +2855,7 @@ static struct fxp_handle *fxp_handle_create(pool *p) {
   struct fxp_handle *fxh;
 
   sub_pool = make_sub_pool(p);
+  pr_pool_tag(sub_pool, "SFTP file handle pool");
   fxh = pcalloc(sub_pool, sizeof(struct fxp_handle));
   fxh->pool = sub_pool;
 
@@ -2625,6 +2886,7 @@ static struct fxp_handle *fxp_handle_create(pool *p) {
 
     if (fxp_handle_get(handle) == NULL) {
       fxh->name = handle;
+      fxh->fh_st = pcalloc(fxh->pool, sizeof(struct stat));
       break;
     }
 
@@ -2639,14 +2901,14 @@ static struct fxp_handle *fxp_handle_create(pool *p) {
  * "aborting" any file handles still left open by the client.
  */
 static int fxp_handle_abort(const void *key_data, size_t key_datasz,
-    void *value_data, size_t value_datasz, void *user_data) {
+    const void *value_data, size_t value_datasz, void *user_data) {
   struct fxp_handle *fxh;
   char *abs_path, *curr_path = NULL, *real_path = NULL;
   char direction;
   unsigned char *delete_aborted_stores = NULL;
   cmd_rec *cmd = NULL;
 
-  fxh = value_data;
+  fxh = (struct fxp_handle *) value_data;
   delete_aborted_stores = user_data;
 
   /* Is this a file or a directory handle? */
@@ -2662,9 +2924,7 @@ static int fxp_handle_abort(const void *key_data, size_t key_datasz,
 
     pr_response_clear(&resp_list);
     pr_response_clear(&resp_err_list);
-
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     fxh->dirh = NULL;
     return 0;
@@ -2734,28 +2994,20 @@ static int fxp_handle_abort(const void *key_data, size_t key_datasz,
     }
   }
 
-  /* Add a note indicating that this is a failed transfer. */
-  if (pr_table_add(cmd->notes, "mod_sftp.file-status",
-      pstrdup(fxh->pool, "failed"), 0) < 0) {
-    if (errno != EEXIST) {
-      pr_trace_msg(trace_channel, 3,
-        "error stashing file status in command notes: %s", strerror(errno));
-    }
+  if (cmd != NULL) {
+    /* Add a note indicating that this is a failed transfer. */
+    fxp_cmd_note_file_status(cmd, "failed");
   }
 
   xferlog_write(0, pr_netaddr_get_sess_remote_name(), fxh->fh_bytes_xferred,
     abs_path, 'b', direction, 'r', session.user, 'i', "_");
 
   if (cmd) {
-    /* Ideally we could provide a real response code/message for any
-     * configured ExtendedLogs for these aborted transfers.  Something to
-     * refine in the future...
-     */
     pr_response_clear(&resp_list);
     pr_response_clear(&resp_err_list);
 
-    (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    pr_response_add_err(R_451, "%s: %s", cmd->arg, strerror(ECONNRESET));
+    fxp_cmd_dispatch_err(cmd);
   }
 
   if (pr_fsio_close(fxh->fh) < 0) {
@@ -2774,9 +3026,11 @@ static int fxp_handle_abort(const void *key_data, size_t key_datasz,
           "removing aborted uploaded file '%s'", curr_path);
 
         if (pr_fsio_unlink(curr_path) < 0) {
-          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-            "error unlinking file '%s': %s", curr_path,
-            strerror(errno));
+          if (errno != ENOENT) {
+            (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+              "error unlinking file '%s': %s", curr_path,
+              strerror(errno));
+          }
         }
       }
     }
@@ -2803,8 +3057,8 @@ static struct fxp_handle *fxp_handle_get(const char *handle) {
     return NULL;
   }
 
-  fxh = pr_table_get(fxp_session->handle_tab, handle, NULL);
-
+  fxh = (struct fxp_handle *) pr_table_get(fxp_session->handle_tab, handle,
+    NULL);
   return fxh;
 }
 
@@ -2815,6 +3069,7 @@ static struct fxp_packet *fxp_packet_create(pool *p, uint32_t channel_id) {
   struct fxp_packet *fxp;
 
   sub_pool = make_sub_pool(p);
+  pr_pool_tag(sub_pool, "SFTP packet pool");
   fxp = pcalloc(sub_pool, sizeof(struct fxp_packet));
   fxp->pool = sub_pool;
   fxp->channel_id = channel_id;
@@ -2835,7 +3090,6 @@ static struct fxp_packet *fxp_packet_get_packet(uint32_t channel_id) {
   }
 
   fxp = fxp_packet_create(fxp_pool, channel_id);
-
   return fxp;
 }
 
@@ -3405,6 +3659,30 @@ static void fxp_version_add_openssh_exts(pool *p, unsigned char **buf,
     fxp_msg_write_extpair(buf, buflen, &ext);
   }
 #endif
+
+  if (fxp_ext_flags & SFTP_FXP_EXT_HARDLINK) {
+    struct fxp_extpair ext;
+
+    ext.ext_name = "hardlink at openssh.com";
+    ext.ext_data = (unsigned char *) "1";
+    ext.ext_datalen = 1;
+
+    pr_trace_msg(trace_channel, 11, "+ SFTP extension: %s = '%s'", ext.ext_name,
+      ext.ext_data);
+    fxp_msg_write_extpair(buf, buflen, &ext);
+  }
+
+  if (fxp_ext_flags & SFTP_FXP_EXT_XATTR) {
+    struct fxp_extpair ext;
+
+    ext.ext_name = "xattr at proftpd.org";
+    ext.ext_data = (unsigned char *) "1";
+    ext.ext_datalen = 1;
+
+    pr_trace_msg(trace_channel, 11, "+ SFTP extension: %s = '%s'", ext.ext_name,
+      ext.ext_data);
+    fxp_msg_write_extpair(buf, buflen, &ext);
+  }
 }
 
 static void fxp_version_add_newline_ext(pool *p, unsigned char **buf,
@@ -3539,6 +3817,9 @@ static void fxp_version_add_supported2_ext(pool *p, unsigned char **buf,
 
   file_mask = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_PERMISSIONS|
     SSH2_FX_ATTR_ACCESSTIME|SSH2_FX_ATTR_MODIFYTIME|SSH2_FX_ATTR_OWNERGROUP;
+#ifdef PR_USE_XATTR
+  file_mask |= SSH2_FX_ATTR_EXTENDED;
+#endif /* PR_USE_XATTR */
 
   bits_mask = 0;
 
@@ -3687,7 +3968,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3696,6 +3978,23 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     return fxp_packet_write(resp);
   }
 
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int link_len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      link_len = dir_readlink(fxp->pool, path, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (link_len > 0) {
+        link_path[link_len] = '\0';
+        path = pstrdup(fxp->pool, link_path);
+      }
+    }
+  }
+
+  pr_fs_clear_cache2(path);
   res = pr_fsio_lstat(path, &st);
   if (res < 0) {
     xerrno = errno;
@@ -3709,7 +4008,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3730,7 +4030,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3751,7 +4052,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3772,7 +4074,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3795,7 +4098,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3814,7 +4118,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     NULL);
 #endif
 
-  digest_name = fxp_get_shared_name(fxp->pool, digest_list, supported_digests);
+  digest_name = sftp_misc_namelist_shared(fxp->pool, digest_list,
+    supported_digests);
   if (digest_name == NULL) {
     xerrno = EINVAL;
 
@@ -3828,7 +4133,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3871,7 +4177,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3880,7 +4187,11 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     return fxp_packet_write(resp);
   }
 
-  pr_fsio_set_block(fh);
+  if (pr_fsio_set_block(fh) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error setting fd %d (file '%s') as blocking: %s", fh->fh_fd,
+      fh->fh_path, strerror(errno));
+  }
 
   if (pr_fsio_lseek(fh, offset, SEEK_SET) < 0) {
     xerrno = errno;
@@ -3897,7 +4208,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3927,7 +4239,8 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
     buf = ptr;
     buflen = bufsz;
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -3992,7 +4305,7 @@ static int fxp_handle_ext_check_file(struct fxp_packet *fxp, char *digest_list,
       buf = ptr;
       buflen = bufsz;
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         reason, NULL);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
@@ -4079,16 +4392,16 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "COPY of '%s' to '%s' blocked by '%s' handler", src, dst, cmd->argv[0]);
+      "COPY of '%s' to '%s' blocked by '%s' handler", src, dst,
+      (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4108,11 +4421,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4132,11 +4444,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4154,13 +4465,13 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     status_code = fxp_errno2status(xerrno, &reason);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-      "('%s' [%d])", (unsigned long) status_code, reason,
-      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4169,6 +4480,7 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     return fxp_packet_write(resp);
   }
 
+  pr_fs_clear_cache2(dst);
   res = pr_fsio_stat(dst, &st);
   if (res == 0) {
     unsigned char *allow_overwrite = NULL;
@@ -4205,11 +4517,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, reason);
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-        NULL);
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+        reason, NULL);
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -4228,10 +4539,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4254,10 +4565,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4266,7 +4577,7 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     return fxp_packet_write(resp);
   }
 
-  res = pr_fs_copy_file(src, dst);
+  res = pr_fs_copy_file2(src, dst, 0, NULL);
   if (res < 0) {
     xerrno = errno;
 
@@ -4278,10 +4589,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4293,11 +4604,10 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
   /* No errors. */
   xerrno = errno = 0;
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(dst);
   pr_fsio_stat(dst, &st);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   /* Write a TransferLog entry as well. */
   abs_path = dir_abs_path(fxp->pool, dst, TRUE);
@@ -4309,7 +4619,8 @@ static int fxp_handle_ext_copy_file(struct fxp_packet *fxp, char *src,
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, reason);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -4332,7 +4643,7 @@ static int fxp_handle_ext_fsync(struct fxp_packet *fxp,
   args = pstrdup(fxp->pool, path);
 
   cmd = fxp_cmd_alloc(fxp->pool, "FSYNC", args);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
   pr_cmd_dispatch_phase(cmd, PRE_CMD, 0);
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
@@ -4356,12 +4667,199 @@ static int fxp_handle_ext_fsync(struct fxp_packet *fxp,
 
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
     "('%s' [%d])", (unsigned long) status_code, reason,
-    xerrno != EOF ? strerror(errno) : "End of file", xerrno);
+    xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  if (xerrno == 0) {
+    fxp_cmd_dispatch(cmd);
+
+  } else {
+    fxp_cmd_dispatch_err(cmd);
+  }
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_hardlink(struct fxp_packet *fxp, char *src,
+    char *dst) {
+  unsigned char *buf, *ptr;
+  char *args, *path;
+  const char *reason;
+  uint32_t buflen, bufsz, status_code;
+  struct fxp_packet *resp;
+  cmd_rec *cmd = NULL;
+  int res, xerrno = 0;
+
+  args = pstrcat(fxp->pool, src, " ", dst, NULL);
+
+  pr_scoreboard_entry_update(session.pid,
+    PR_SCORE_CMD, "%s", "HARDLINK", NULL, NULL);
+  pr_scoreboard_entry_update(session.pid,
+    PR_SCORE_CMD_ARG, "%s", args, NULL, NULL);
+
+  pr_proctitle_set("%s - %s: HARDLINK %s %s", session.user, session.proc_prefix,
+    src, dst);
+
+  cmd = fxp_cmd_alloc(fxp->pool, "HARDLINK", args);
+  cmd->cmd_class = CL_MISC|CL_SFTP;
+
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  path = dir_best_path(fxp->pool, src);
+  if (path == NULL) {
+    status_code = SSH2_FX_PERMISSION_DENIED;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "hardlink request denied: unable to access path '%s'", src);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+  src = path;
+
+  path = dir_best_path(fxp->pool, dst);
+  if (path == NULL) {
+    status_code = SSH2_FX_PERMISSION_DENIED;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "hardlink request denied: unable to access path '%s'", dst);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+  dst = path;
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  if (!dir_check(fxp->pool, cmd, G_DIRS, src, NULL) ||
+      !dir_check(fxp->pool, cmd, G_WRITE, dst, NULL)) {
+    status_code = SSH2_FX_PERMISSION_DENIED;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "HARDLINK of '%s' to '%s' blocked by <Limit> configuration",
+      src, dst);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (strcmp(src, dst) == 0) {
+    xerrno = EEXIST;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "HARDLINK of '%s' to same path '%s', rejecting", src, dst);
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (fxp_path_pass_regex_filters(fxp->pool, "HARDLINK", src) < 0 ||
+      fxp_path_pass_regex_filters(fxp->pool, "HARDLINK", dst) < 0) {
+    xerrno = errno;
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  res = pr_fsio_link(src, dst);
+  if (res < 0) {
+    xerrno = errno;
+
+    (void) pr_trace_msg("fileperms", 1, "HARDLINK, user '%s' (UID %s, "
+      "GID %s): error hardlinking '%s' to '%s': %s", session.user,
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      src, dst, strerror(xerrno));
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error hardlinking '%s' to '%s': %s", src, dst, strerror(xerrno));
+
+    errno = xerrno;
+
+  } else {
+    /* No errors. */
+    xerrno = errno = 0;
+  }
+
+  status_code = fxp_errno2status(xerrno, &reason);
+
+  pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+    "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+    xerrno);
 
-  pr_cmd_dispatch_phase(cmd, xerrno == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  if (xerrno == 0) {
+    fxp_cmd_dispatch(cmd);
+
+  } else {
+    fxp_cmd_dispatch_err(cmd);
+  }
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -4391,7 +4889,7 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     src, dst);
 
   cmd = fxp_cmd_alloc(fxp->pool, "RENAME", args);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -4402,20 +4900,18 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "RENAME from '%s' blocked by '%s' handler", src, cmd2->argv[0]);
+      "RENAME from '%s' blocked by '%s' handler", src, (char *) cmd2->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4434,18 +4930,16 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4454,8 +4948,13 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     return fxp_packet_write(resp);
   }
 
-  pr_table_add(session.notes, "mod_core.rnfr-path",
-    pstrdup(session.pool, src), 0);
+  if (pr_table_add(session.notes, "mod_core.rnfr-path",
+      pstrdup(session.pool, src), 0) < 0) {
+    if (errno != EEXIST) {
+      pr_trace_msg(trace_channel, 8,
+        "error setting 'mod_core.rnfr-path' note: %s", strerror(errno));
+    }
+  }
 
   cmd3 = fxp_cmd_alloc(fxp->pool, C_RNTO, dst);
   cmd3->cmd_class = CL_MISC|CL_WRITE;
@@ -4463,25 +4962,21 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "RENAME to '%s' blocked by '%s' handler", dst, cmd3->argv[0]);
+      "RENAME to '%s' blocked by '%s' handler", dst, (char *) cmd3->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4500,23 +4995,19 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4537,19 +5028,15 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4567,23 +5054,19 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     status_code = fxp_errno2status(xerrno, &reason);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-      "('%s' [%d])", (unsigned long) status_code, reason,
-      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EEXIST));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EEXIST));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4602,19 +5085,15 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4628,9 +5107,9 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
     if (errno != EXDEV) {
       xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %lu, "
-        "GID %lu): error renaming '%s' to '%s': %s", session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
+      (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %s, "
+        "GID %s): error renaming '%s' to '%s': %s", session.user,
+        pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
         src, dst, strerror(xerrno));
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -4644,13 +5123,14 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
        */
       errno = 0;
 
-      res = pr_fs_copy_file(src, dst);
+      res = pr_fs_copy_file2(src, dst, 0, NULL);
       if (res < 0) {
         xerrno = errno;
 
-        (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %lu, "
-          "GID %lu): error copying '%s' to '%s': %s", session.user,
-          (unsigned long) session.uid, (unsigned long) session.gid,
+        (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %s, "
+          "GID %s): error copying '%s' to '%s': %s", session.user,
+          pr_uid2str(fxp->pool, session.uid),
+          pr_gid2str(fxp->pool, session.gid),
           src, dst, strerror(xerrno));
 
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -4678,7 +5158,7 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
 
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
     "('%s' [%d])", (unsigned long) status_code, reason,
-    xerrno != EOF ? strerror(errno) : "End of file", xerrno);
+    xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
   /* Clear out any transfer-specific data. */
   if (session.xfer.p) {
@@ -4691,23 +5171,43 @@ static int fxp_handle_ext_posix_rename(struct fxp_packet *fxp, char *src,
    */
 
   session.xfer.p = make_sub_pool(fxp_pool);
+  pr_pool_tag(session.xfer.p, "SFTP session transfer pool");
   memset(&session.xfer.start_time, 0, sizeof(session.xfer.start_time));
   gettimeofday(&session.xfer.start_time, NULL);
 
   session.xfer.path = pstrdup(session.xfer.p, src);
 
-  /* XXX Use pr_response_add(R_250) here for success, add_err/R_550 if not */
-  pr_cmd_dispatch_phase(cmd2, xerrno == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd2, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  if (xerrno == 0) {
+    pr_response_add(R_350,
+      "File or directory exists, ready for destination name");
+    fxp_cmd_dispatch(cmd2);
+
+  } else {
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd2->argv[0],
+      strerror(xerrno));
+    fxp_cmd_dispatch_err(cmd2);
+  }
 
   session.xfer.path = pstrdup(session.xfer.p, dst);
 
-  /* XXX Use pr_response_add(R_250) here for success, add_err/R_550 if not */
-  pr_cmd_dispatch_phase(cmd3, xerrno == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd3, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  if (xerrno == 0) {
+    pr_response_add(R_250, "Rename successful");
+    fxp_cmd_dispatch(cmd3);
+
+  } else {
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd3->argv[0],
+      strerror(xerrno));
+    fxp_cmd_dispatch_err(cmd3);
+  }
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+  if (xerrno == 0) {
+    fxp_cmd_dispatch(cmd);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
-  pr_cmd_dispatch_phase(cmd, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  } else {
+    fxp_cmd_dispatch_err(cmd);
+  }
 
   /* Clear out any transfer-specific data. */
   if (session.xfer.p) {
@@ -4852,7 +5352,8 @@ static int fxp_handle_ext_space_avail(struct fxp_packet *fxp, char *path) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4925,7 +5426,8 @@ static int fxp_handle_ext_statvfs(struct fxp_packet *fxp, const char *path) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -4995,28 +5497,658 @@ static int fxp_handle_ext_statvfs(struct fxp_packet *fxp, const char *path) {
 }
 #endif /* !HAVE_SYS_STATVFS_H */
 
-static int fxp_handle_ext_vendor_id(struct fxp_packet *fxp) {
+#ifdef PR_USE_XATTR
+static int fxp_handle_ext_getxattr(struct fxp_packet *fxp, const char *path,
+    const char *name, uint32_t valsz) {
+  ssize_t res;
+  void *val;
   unsigned char *buf, *ptr;
-  char *vendor_name, *product_name, *product_version;
   uint32_t buflen, bufsz, status_code;
-  uint64_t build_number;
   const char *reason;
   struct fxp_packet *resp;
 
-  vendor_name = sftp_msg_read_string(fxp->pool, &fxp->payload,
-    &fxp->payload_sz);
+  val = pcalloc(fxp->pool, (size_t) valsz+1);
 
-  product_name = sftp_msg_read_string(fxp->pool, &fxp->payload,
-    &fxp->payload_sz);
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ + valsz;
+  buf = ptr = palloc(fxp->pool, bufsz);
 
-  product_version = sftp_msg_read_string(fxp->pool, &fxp->payload,
-    &fxp->payload_sz);
+  res = pr_fsio_lgetxattr(fxp->pool, path, name, val, (size_t) valsz);
+  if (res < 0) {
+    int xerrno = errno;
 
-  build_number = sftp_msg_read_long(fxp->pool, &fxp->payload, &fxp->payload_sz);
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "getxattr(2) error on '%s' for attribute '%s': %s", path, name,
+      strerror(xerrno));
 
-  if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-    vendor_name = sftp_utf8_decode_str(fxp->pool, vendor_name);
-    product_name = sftp_utf8_decode_str(fxp->pool, product_name);
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  pr_trace_msg(trace_channel, 8,
+    "sending response: EXTENDED_REPLY (%lu bytes)", (unsigned long) res);
+
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_EXTENDED_REPLY);
+  sftp_msg_write_int(&buf, &buflen, fxp->request_id);
+  sftp_msg_write_data(&buf, &buflen, val, res, TRUE);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_fgetxattr(struct fxp_packet *fxp, const char *handle,
+    const char *name, uint32_t valsz) {
+  ssize_t res;
+  void *val;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *path, *reason;
+  struct fxp_handle *fxh;
+  struct fxp_packet *resp;
+
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ + valsz;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  fxh = fxp_handle_get(handle);
+  if (fxh == NULL) {
+    pr_trace_msg(trace_channel, 17,
+      "fgetxattr at proftpd.org: unable to find handle for name '%s': %s", handle,
+      strerror(errno));
+
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (fxh->dirh != NULL) {
+    /* Request for extended attributes on a directory handle.  It's not
+     * easy to get the file descriptor on a directory, so we'll just do
+     * by path instead.
+     */
+    return fxp_handle_ext_getxattr(fxp, fxh->fh->fh_path, name, valsz);
+  }
+
+  if (fxh->fh == NULL) {
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  path = fxh->fh->fh_path;
+  val = pcalloc(fxp->pool, (size_t) valsz+1);
+
+  res = pr_fsio_fgetxattr(fxp->pool, fxh->fh, name, val, (size_t) valsz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "fgetxattr(2) error on '%s' for attribute '%s': %s", path, name,
+      strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  pr_trace_msg(trace_channel, 8,
+    "sending response: EXTENDED_REPLY (%lu bytes)", (unsigned long) res);
+
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_EXTENDED_REPLY);
+  sftp_msg_write_int(&buf, &buflen, fxp->request_id);
+  sftp_msg_write_data(&buf, &buflen, val, res, TRUE);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_listxattr(struct fxp_packet *fxp, const char *path) {
+  register unsigned int i;
+  int res;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *reason;
+  struct fxp_packet *resp;
+  array_header *names = NULL;
+
+  buflen = bufsz = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  res = pr_fsio_llistxattr(fxp->pool, path, &names);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "listxattr(2) error on '%s': %s", path, strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  pr_trace_msg(trace_channel, 8,
+    "sending response: EXTENDED_REPLY (%d attribute names)", names->nelts);
+
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_EXTENDED_REPLY);
+  sftp_msg_write_int(&buf, &buflen, fxp->request_id);
+  sftp_msg_write_int(&buf, &buflen, names->nelts);
+  for (i = 0; i < names->nelts; i++) {
+    const char *name;
+
+    name = ((const char **) names->elts)[i];
+    sftp_msg_write_string(&buf, &buflen, name);
+  }
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_flistxattr(struct fxp_packet *fxp,
+    const char *handle) {
+  register unsigned int i;
+  int res;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *path, *reason;
+  struct fxp_handle *fxh;
+  struct fxp_packet *resp;
+  array_header *names = NULL;
+
+  buflen = bufsz = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  fxh = fxp_handle_get(handle);
+  if (fxh == NULL) {
+    pr_trace_msg(trace_channel, 17,
+      "flistxattr at proftpd.org: unable to find handle for name '%s': %s", handle,
+      strerror(errno));
+
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (fxh->dirh != NULL) {
+    /* Request for extended attributes on a directory handle.  It's not
+     * easy to get the file descriptor on a directory, so we'll just do
+     * by path instead.
+     */
+    return fxp_handle_ext_listxattr(fxp, fxh->fh->fh_path);
+  }
+
+  if (fxh->fh == NULL) {
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  path = fxh->fh->fh_path;
+  res = pr_fsio_flistxattr(fxp->pool, fxh->fh, &names);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "flistxattr(2) error on '%s': %s", path, strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  pr_trace_msg(trace_channel, 8,
+    "sending response: EXTENDED_REPLY (%d attributes)", names->nelts);
+
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_EXTENDED_REPLY);
+  sftp_msg_write_int(&buf, &buflen, fxp->request_id);
+  sftp_msg_write_int(&buf, &buflen, names->nelts);
+  for (i = 0; i < names->nelts; i++) {
+    const char *name;
+
+    name = ((const char **) names->elts)[i];
+    sftp_msg_write_string(&buf, &buflen, name);
+  }
+
+  sftp_msg_write_data(&buf, &buflen, (const unsigned char *) names, res, TRUE);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_removexattr(struct fxp_packet *fxp, const char *path,
+    const char *name) {
+  int res;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *reason;
+  struct fxp_packet *resp;
+
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  res = pr_fsio_lremovexattr(fxp->pool, path, name);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "removexattr(2) error on '%s' for attribute '%s': %s", path, name,
+      strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason,
+      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  status_code = SSH2_FX_OK;
+  reason = "OK";
+
+  pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+    (unsigned long) status_code, reason);
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_fremovexattr(struct fxp_packet *fxp,
+    const char *handle, const char *name) {
+  int res;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *path, *reason;
+  struct fxp_handle *fxh;
+  struct fxp_packet *resp;
+
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  fxh = fxp_handle_get(handle);
+  if (fxh == NULL) {
+    pr_trace_msg(trace_channel, 17,
+      "fremovexattr at proftpd.org: unable to find handle for name '%s': %s",
+      handle, strerror(errno));
+
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (fxh->dirh != NULL) {
+    /* Request for extended attributes on a directory handle.  It's not
+     * easy to get the file descriptor on a directory, so we'll just do
+     * by path instead.
+     */
+    return fxp_handle_ext_removexattr(fxp, fxh->fh->fh_path, name);
+  }
+
+  if (fxh->fh == NULL) {
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  path = fxh->fh->fh_path;
+
+  res = pr_fsio_fremovexattr(fxp->pool, fxh->fh, name);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "fremovexattr(2) error on '%s' for attribute '%s': %s", path, name,
+      strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason,
+      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  status_code = SSH2_FX_OK;
+  reason = "OK";
+
+  pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+    (unsigned long) status_code, reason);
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_setxattr(struct fxp_packet *fxp, const char *path,
+    const char *name, void *val, uint32_t valsz, uint32_t pflags) {
+  int res, flags = 0;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *reason;
+  struct fxp_packet *resp;
+
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  if (pflags & SSH2_FXE_XATTR_CREATE) {
+    flags |= PR_FSIO_XATTR_FL_CREATE;
+  }
+
+  if (pflags & SSH2_FXE_XATTR_REPLACE) {
+    flags |= PR_FSIO_XATTR_FL_REPLACE;
+  }
+
+  res = pr_fsio_lsetxattr(fxp->pool, path, name, val, (size_t) valsz, flags);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "setxattr(2) error on '%s' for attribute '%s': %s", path, name,
+      strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason,
+      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  status_code = SSH2_FX_OK;
+  reason = "OK";
+
+  pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+    (unsigned long) status_code, reason);
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+
+static int fxp_handle_ext_fsetxattr(struct fxp_packet *fxp, const char *handle,
+    const char *name, void *val, uint32_t valsz, uint32_t pflags) {
+  int res, flags = 0;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz, status_code;
+  const char *path, *reason;
+  struct fxp_handle *fxh;
+  struct fxp_packet *resp;
+
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  fxh = fxp_handle_get(handle);
+  if (fxh == NULL) {
+    pr_trace_msg(trace_channel, 17,
+      "fsetxattr at proftpd.org: unable to find handle for name '%s': %s", handle,
+      strerror(errno));
+
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (fxh->dirh != NULL) {
+    /* Request for extended attributes on a directory handle.  It's not
+     * easy to get the file descriptor on a directory, so we'll just do
+     * by path instead.
+     */
+    return fxp_handle_ext_setxattr(fxp, fxh->fh->fh_path, name, val, valsz,
+      pflags);
+  }
+
+  if (fxh->fh == NULL) {
+    status_code = SSH2_FX_INVALID_HANDLE;
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  if (pflags & SSH2_FXE_XATTR_CREATE) {
+    flags |= PR_FSIO_XATTR_FL_CREATE;
+  }
+
+  if (pflags & SSH2_FXE_XATTR_REPLACE) {
+    flags |= PR_FSIO_XATTR_FL_REPLACE;
+  }
+
+  path = fxh->fh->fh_path;
+
+  res = pr_fsio_fsetxattr(fxp->pool, fxh->fh, name, val, (size_t) valsz, flags);
+  if (res < 0) {
+    int xerrno = errno;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "fsetxattr(2) error on '%s' for attribute '%s': %s", path, name,
+      strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason,
+      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  status_code = SSH2_FX_OK;
+  reason = "OK";
+
+  pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+    (unsigned long) status_code, reason);
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+  resp->payload = ptr;
+  resp->payload_sz = (bufsz - buflen);
+
+  return fxp_packet_write(resp);
+}
+#endif /* PR_USE_XATTR */
+
+static int fxp_handle_ext_vendor_id(struct fxp_packet *fxp) {
+  unsigned char *buf, *ptr;
+  char *vendor_name, *product_name, *product_version;
+  uint32_t buflen, bufsz, status_code;
+  uint64_t build_number;
+  const char *reason;
+  struct fxp_packet *resp;
+
+  vendor_name = sftp_msg_read_string(fxp->pool, &fxp->payload,
+    &fxp->payload_sz);
+
+  product_name = sftp_msg_read_string(fxp->pool, &fxp->payload,
+    &fxp->payload_sz);
+
+  product_version = sftp_msg_read_string(fxp->pool, &fxp->payload,
+    &fxp->payload_sz);
+
+  build_number = sftp_msg_read_long(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+  if (fxp_session->client_version >= fxp_utf8_protocol_version) {
+    vendor_name = sftp_utf8_decode_str(fxp->pool, vendor_name);
+    product_name = sftp_utf8_decode_str(fxp->pool, product_name);
     product_version = sftp_utf8_decode_str(fxp->pool, product_version);
   }
 
@@ -5034,7 +6166,8 @@ static int fxp_handle_ext_vendor_id(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, reason);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -5049,7 +6182,8 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
   uint32_t buflen, bufsz, status_code;
   const char *reason;
   struct fxp_packet *resp;
-  int res = 0, version = 0;
+  int res = 0, val = 0;
+  unsigned int version = 0;
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -5067,7 +6201,8 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5079,19 +6214,29 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
     return -1;
   }
 
-  version = atoi(version_str);
+  val = atoi(version_str);
+  if (val < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "client requested invalid SFTP protocol version %d via 'version-select'",
+      val);
+    res = -1;
+  }
+
+  version = val;
 
-  if (version > fxp_max_client_version) {
+  if (res == 0 &&
+      version > fxp_max_client_version) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "client requested SFTP protocol version %d via 'version-select', "
+      "client requested SFTP protocol version %u via 'version-select', "
       "which exceeds SFTPClientMatch max SFTP protocol version %u, rejecting",
       version, fxp_max_client_version);
     res = -1;
   }
 
-  if (version < fxp_min_client_version) {
+  if (res == 0 &&
+      version < fxp_min_client_version) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "client requested SFTP protocol version %d via 'version-select', "
+      "client requested SFTP protocol version %u via 'version-select', "
       "which is less than SFTPClientMatch min SFTP protocol version %u, "
       "rejecting", version, fxp_min_client_version);
     res = -1;
@@ -5102,9 +6247,10 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
    * UTF8, and thus every other version of SFTP.  Otherwise, we can only
    * support up to version 3.
    */
-  if (version > 3) {
+  if (res == 0 &&
+      version > 3) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "client requested SFTP protocol version %d via 'version-select', "
+      "client requested SFTP protocol version %u via 'version-select', "
       "but we can only support protocol version 3 due to lack of "
       "UTF8 support (requires --enable-nls)", version);
     res = -1;
@@ -5123,7 +6269,8 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5136,7 +6283,7 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
   }
 
   pr_trace_msg(trace_channel, 7, "client requested switch to SFTP protocol "
-    "version %d via 'version-select'", version);
+    "version %u via 'version-select'", version);
   fxp_session->client_version = (unsigned long) version;
 
   status_code = SSH2_FX_OK;
@@ -5145,7 +6292,8 @@ static int fxp_handle_ext_version_select(struct fxp_packet *fxp,
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, reason);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -5178,7 +6326,7 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
   /* Set the command class to MISC for now; we'll change it later to
    * READ or WRITE once we know which it is.
    */
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "CLOSE", NULL, NULL);
@@ -5196,19 +6344,18 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5224,14 +6371,13 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
  
     fxp_handle_delete(fxh);
     destroy_pool(fxh->pool);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5258,16 +6404,19 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
 
     /* Set session.curr_cmd appropriately here, for any FSIO callbacks. */ 
     if (fxh->fh_flags & O_APPEND) {
-      cmd->cmd_class = CL_WRITE;
+      cmd->cmd_class &= ~CL_MISC;
+      cmd->cmd_class |= CL_WRITE;
       session.curr_cmd = C_APPE;
 
     } else if ((fxh->fh_flags & O_WRONLY) ||
                (fxh->fh_flags & O_RDWR)) {
-      cmd->cmd_class = CL_WRITE;
+      cmd->cmd_class &= ~CL_MISC;
+      cmd->cmd_class |= CL_WRITE;
       session.curr_cmd = C_STOR;
 
     } else if (fxh->fh_flags == O_RDONLY) {
-      cmd->cmd_class = CL_READ;
+      cmd->cmd_class &= ~CL_MISC;
+      cmd->cmd_class |= CL_READ;
       session.curr_cmd = C_RETR;
     }
 
@@ -5359,8 +6508,6 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
     xfer_total_bytes = session.xfer.total_bytes;
 
     if (cmd2) {
-      int post_phase = POST_CMD, log_phase = LOG_CMD;
-
       if (fxh->fh_existed &&
           (pr_cmd_cmp(cmd2, PR_CMD_STOR_ID) == 0 ||
            pr_cmd_cmp(cmd2, PR_CMD_APPE_ID) == 0)) {
@@ -5381,7 +6528,7 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
         (void) pr_table_remove(cmd2->notes, "mod_xfer.file-modified", NULL);
 
         if (pr_table_add(cmd2->notes, "mod_xfer.file-modified",
-            pstrdup(cmd->pool, "true"), 0) < 0) {
+            pstrdup(cmd2->pool, "true"), 0) < 0) {
           if (errno != EEXIST) {
             pr_log_pri(PR_LOG_NOTICE,
               "notice: error adding 'mod_xfer.file-modified' note: %s",
@@ -5394,21 +6541,12 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
           xerrno != EOF) {
 
         pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-        post_phase = POST_CMD_ERR;
-        log_phase = LOG_CMD_ERR;
+        fxp_cmd_dispatch_err(cmd2);
 
       } else {
-        pr_response_add(R_226, "Transfer complete");
+        pr_response_add(R_226, "%s", "Transfer complete");
+        fxp_cmd_dispatch(cmd2);
       }
-
-      /* XXX We don't really care about the success of this dispatch, since
-       * there's not much that we can do, in this code, at this point.
-       */
-      (void) pr_cmd_dispatch_phase(cmd2, post_phase, 0);
-      (void) pr_cmd_dispatch_phase(cmd2, log_phase, 0);
-
-      pr_response_clear(&resp_list);
-      pr_response_clear(&resp_err_list);
     }
 
   } else if (fxh->dirh != NULL) {
@@ -5427,13 +6565,10 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error closing directory '%s': %s", fxh->dir, strerror(xerrno));
-
-      pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd2);
 
     } else {
-      pr_cmd_dispatch_phase(cmd2, POST_CMD, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD, 0);
+      fxp_cmd_dispatch(cmd2);
     }
 
     fxh->dirh = NULL;
@@ -5457,11 +6592,16 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
   fxp_handle_delete(fxh);
   destroy_pool(fxh->pool);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
   /* Now re-populate the session.xfer struct, for mod_log's handling of
    * the CLOSE request.
    */
+  if (session.xfer.p) {
+    destroy_pool(session.xfer.p);
+  }
+
   session.xfer.p = fxp->pool;
   session.xfer.direction = xfer_direction;
   session.xfer.filename = xfer_filename;
@@ -5470,8 +6610,12 @@ static int fxp_handle_close(struct fxp_packet *fxp) {
   session.xfer.file_size = xfer_file_size;
   session.xfer.total_bytes = xfer_total_bytes;
 
-  pr_cmd_dispatch_phase(cmd, res < 0 ? POST_CMD_ERR : POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, res < 0 ? LOG_CMD_ERR : LOG_CMD, 0);
+  if (res < 0) {
+    fxp_cmd_dispatch_err(cmd);
+
+  } else {
+    fxp_cmd_dispatch(cmd);
+  }
 
   /* Clear out session.xfer again. */
   memset(&session.xfer, 0, sizeof(session.xfer));
@@ -5495,7 +6639,7 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     &fxp->payload_sz);
 
   cmd = fxp_cmd_alloc(fxp->pool, "EXTENDED", ext_request_name);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "EXTENDED", NULL, NULL);
@@ -5517,7 +6661,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
    */
   if (strncmp(ext_request_name, "vendor-id", 10) == 0) {
     res = fxp_handle_ext_vendor_id(fxp);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5530,7 +6679,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
       &fxp->payload_sz);
 
     res = fxp_handle_ext_version_select(fxp, version_str);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5550,7 +6704,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
 
     res = fxp_handle_ext_check_file(fxp, digest_list, path, offset, len,
       blocksz);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5572,10 +6731,10 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -5597,10 +6756,10 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -5619,7 +6778,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
 
     res = fxp_handle_ext_check_file(fxp, digest_list, path, offset, len,
       blocksz);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5634,7 +6798,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     overwrite = sftp_msg_read_bool(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
     res = fxp_handle_ext_copy_file(fxp, src, dst, overwrite);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5649,18 +6818,18 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     fxh = fxp_handle_get(handle);
     if (fxh == NULL) {
       pr_trace_msg(trace_channel, 17,
-        "%s: unable to find handle for name '%s': %s", cmd->argv[0], handle,
-        strerror(errno));
+        "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+        handle, strerror(errno));
 
       status_code = SSH2_FX_INVALID_HANDLE;
 
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -5673,17 +6842,17 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
       errno = EISDIR;
 
       pr_trace_msg(trace_channel, 17,
-        "%s: handle '%s': %s", cmd->argv[0], handle, strerror(errno));
+        "%s: handle '%s': %s", (char *) cmd->argv[0], handle, strerror(errno));
 
       status_code = SSH2_FX_INVALID_HANDLE;
 
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -5693,7 +6862,35 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     }
 
     res = fxp_handle_ext_fsync(fxp, fxh);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
+
+    return res;
+  }
+
+  if ((fxp_ext_flags & SFTP_FXP_EXT_HARDLINK) &&
+      strncmp(ext_request_name, "hardlink at openssh.com", 21) == 0) {
+    char *src, *dst;
+
+    src = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+    dst = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+    if (fxp_session->client_version >= fxp_utf8_protocol_version) {
+      src = sftp_utf8_decode_str(fxp->pool, src);
+      dst = sftp_utf8_decode_str(fxp->pool, dst);
+    }
+
+    res = fxp_handle_ext_hardlink(fxp, src, dst);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5711,7 +6908,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     }
 
     res = fxp_handle_ext_posix_rename(fxp, src, dst);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5724,7 +6926,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
     res = fxp_handle_ext_space_avail(fxp, path);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5736,7 +6943,12 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
     res = fxp_handle_ext_statvfs(fxp, path);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
@@ -5751,18 +6963,18 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     fxh = fxp_handle_get(handle);
     if (fxh == NULL) {
       pr_trace_msg(trace_channel, 17,
-        "%s: unable to find handle for name '%s': %s", cmd->argv[0], handle,
-        strerror(errno));
+        "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+        handle, strerror(errno));
 
       status_code = SSH2_FX_INVALID_HANDLE;
 
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -5774,22 +6986,181 @@ static int fxp_handle_extended(struct fxp_packet *fxp) {
     path = fxh->fh ? fxh->fh->fh_path : fxh->dir;
 
     res = fxp_handle_ext_statvfs(fxp, path);
-    pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+    if (res == 0) {
+      fxp_cmd_dispatch(cmd);
+
+    } else {
+      fxp_cmd_dispatch_err(cmd);
+    }
 
     return res;
   }
 #endif
 
+#ifdef PR_USE_XATTR
+  if (fxp_ext_flags & SFTP_FXP_EXT_XATTR) {
+    if (strcmp(ext_request_name, "fgetxattr at proftpd.org") == 0) {
+      const char *handle, *name;
+      uint32_t valsz;
+
+      handle = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      valsz = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_fgetxattr(fxp, handle, name, valsz);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "flistxattr at proftpd.org") == 0) {
+      const char *handle;
+
+      handle = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_flistxattr(fxp, handle);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "fremovexattr at proftpd.org") == 0) {
+      const char *handle, *name;
+
+      handle = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_fremovexattr(fxp, handle, name);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "fsetxattr at proftpd.org") == 0) {
+      const char *handle, *name;
+      void *val;
+      uint32_t pflags, valsz;
+
+      handle = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      valsz = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      val = (void *) sftp_msg_read_data(fxp->pool, &fxp->payload,
+        &fxp->payload_sz, valsz);
+      pflags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_fsetxattr(fxp, handle, name, val, valsz, pflags);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "getxattr at proftpd.org") == 0) {
+      const char *path, *name;
+      uint32_t valsz;
+
+      path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      valsz = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_getxattr(fxp, path, name, valsz);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "listxattr at proftpd.org") == 0) {
+      const char *path;
+
+      path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_listxattr(fxp, path);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "removexattr at proftpd.org") == 0) {
+      const char *path, *name;
+
+      path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_removexattr(fxp, path, name);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+
+    if (strcmp(ext_request_name, "setxattr at proftpd.org") == 0) {
+      const char *path, *name;
+      void *val;
+      uint32_t pflags, valsz;
+
+      path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      valsz = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
+      val = (void *) sftp_msg_read_data(fxp->pool, &fxp->payload,
+        &fxp->payload_sz, valsz);
+      pflags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
+
+      res = fxp_handle_ext_setxattr(fxp, path, name, val, valsz, pflags);
+      if (res == 0) {
+        fxp_cmd_dispatch(cmd);
+
+      } else {
+        fxp_cmd_dispatch_err(cmd);
+      }
+
+      return res;
+    }
+  }
+#endif /* PR_USE_XATTR */
+
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
     "client requested '%s' extension, rejecting", ext_request_name);
   status_code = SSH2_FX_OP_UNSUPPORTED;
 
-  pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+  fxp_cmd_dispatch_err(cmd);
 
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, fxp_strerror(status_code));
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
     fxp_strerror(status_code), NULL);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
@@ -5809,20 +7180,24 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
   cmd_rec *cmd;
+  array_header *xattrs = NULL;
 
   name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
   cmd = fxp_cmd_alloc(fxp->pool, "FSETSTAT", name);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "FSETSTAT", NULL, NULL);
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD_ARG, "%s", name, NULL, NULL);
 
-  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags);
+  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags,
+    &xattrs);
   if (attrs == NULL) {
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
+
+    /* XXX TODO: Provide a response to the client here! */
     return 0;
   }
 
@@ -5840,19 +7215,18 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5870,16 +7244,16 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "FSETSTAT of '%s' blocked by '%s' handler", cmd->arg, cmd->argv[0]);
+      "FSETSTAT of '%s' blocked by '%s' handler", cmd->arg,
+      (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5899,11 +7273,10 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5932,18 +7305,17 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "FSETSTAT of '%s' blocked by <Limit %s> configuration", path,
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
 
     pr_cmd_set_name(cmd, cmd_name);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -5966,6 +7338,17 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
     attr_flags &= ~SSH2_FX_ATTR_OWNERGROUP;
   }
 
+  /* If the SFTPOption for ignoring the xattrs for SFTP setstat requests is set,
+   * handle it by clearing the SSH2_FX_ATTR_EXTENDED flag.
+   */
+  if ((sftp_opts & SFTP_OPT_IGNORE_SFTP_SET_XATTRS) &&
+      (attr_flags & SSH2_FX_ATTR_EXTENDED)) {
+    pr_trace_msg(trace_channel, 7,
+      "SFTPOption 'IgnoreSFTPSetExtendedAttributes' configured, ignoring "
+      "xattrs sent by client");
+    attr_flags &= ~SSH2_FX_ATTR_EXTENDED;
+  }
+
   /* If the SFTPOption for ignoring the perms for SFTP setstat requests is set,
    * handle it by clearing the SSH2_FX_ATTR_PERMISSIONS flag.
    */
@@ -5990,11 +7373,12 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
   }
 
   if (fxh->fh != NULL) {
-    res = fxp_attrs_set(fxh->fh, fxh->fh->fh_path, attrs, attr_flags, &buf,
-      &buflen, fxp);
+    res = fxp_attrs_set(fxh->fh, fxh->fh->fh_path, attrs, attr_flags, xattrs,
+      &buf, &buflen, fxp);
 
   } else {
-    res = fxp_attrs_set(NULL, fxh->dir, attrs, attr_flags, &buf, &buflen, fxp);
+    res = fxp_attrs_set(NULL, fxh->dir, attrs, attr_flags, xattrs, &buf,
+      &buflen, fxp);
   }
 
   if (res < 0) {
@@ -6006,10 +7390,10 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6023,10 +7407,10 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, reason);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -6036,10 +7420,11 @@ static int fxp_handle_fsetstat(struct fxp_packet *fxp) {
 }
 
 static int fxp_handle_fstat(struct fxp_packet *fxp) {
-  unsigned char *buf, *ptr;
+  unsigned char *buf;
   char *cmd_name, *name;
-  uint32_t buflen, bufsz;
+  uint32_t attr_flags, buflen;
   struct stat st;
+  struct fxp_buffer *fxb;
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
   cmd_rec *cmd;
@@ -6048,7 +7433,7 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
   name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
   cmd = fxp_cmd_alloc(fxp->pool, "FSTAT", name);
-  cmd->cmd_class = CL_READ;
+  cmd->cmd_class = CL_READ|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "FSTAT", NULL, NULL);
@@ -6059,11 +7444,6 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
     name);
 
   if (fxp_session->client_version > 3) {
-    uint32_t attr_flags;
-
-    /* These are hints from the client about what file attributes are
-     * of particular interest.  We do not currently honor them.
-     */
     attr_flags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
     pr_trace_msg(trace_channel, 7, "received request: FSTAT %s %s", name,
@@ -6071,33 +7451,35 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
 
   } else {
     pr_trace_msg(trace_channel, 7, "received request: FSTAT %s", name);
+    attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
+      SSH2_FX_ATTR_ACMODTIME;
   }
 
-  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
-  buf = ptr = palloc(fxp->pool, bufsz);
+  fxb = pcalloc(fxp->pool, sizeof(struct fxp_buffer));
+  fxb->bufsz = buflen = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  fxb->ptr = buf = palloc(fxp->pool, fxb->bufsz);
 
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     uint32_t status_code;
 
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -6108,15 +7490,14 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
   
     return fxp_packet_write(resp);
   }
@@ -6138,43 +7519,41 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
   pr_cmd_set_name(cmd, cmd_name);
 
-  pr_fs_clear_cache();
   if (pr_fsio_fstat(fxh->fh, &st) < 0) {
     uint32_t status_code;
     const char *reason;
+    int xerrno = errno;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error checking '%s' for FSTAT: %s", fxh->fh->fh_path, strerror(errno));
+      "error checking '%s' for FSTAT: %s", fxh->fh->fh_path, strerror(xerrno));
 
-    status_code = fxp_errno2status(errno, &reason);
+    status_code = fxp_errno2status(xerrno, &reason);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
       "('%s' [%d])", (unsigned long) status_code, reason,
-      errno != EOF ? strerror(errno) : "End of file", errno);
+      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
   
     return fxp_packet_write(resp);
   }
@@ -6199,14 +7578,21 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
     fake_group = session.group;
   }
 
-  fxp_attrs_write(fxp->pool, &buf, &buflen, &st, fake_user, fake_group);
+  fxb->buf = buf;
+  fxb->buflen = buflen;
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_attrs_write(fxp->pool, fxb, fxh->fh->fh_path, &st, attr_flags, fake_user,
+    fake_group);
+
+  /* fxp_attrs_write will have changed the buf/buflen values in the buffer. */
+  buf = fxb->buf;
+  buflen = fxb->buflen;
+
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-  resp->payload = ptr;
-  resp->payload_sz = (bufsz - buflen);
+  resp->payload = fxb->ptr;
+  resp->payload_sz = (fxb->bufsz - buflen);
   
   return fxp_packet_write(resp);
 }
@@ -6226,7 +7612,7 @@ static int fxp_handle_init(struct fxp_packet *fxp) {
     (unsigned long) fxp_session->client_version);
 
   cmd = fxp_cmd_alloc(fxp->pool, "INIT", version_str);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "INIT", NULL, NULL);
@@ -6291,7 +7677,6 @@ static int fxp_handle_init(struct fxp_packet *fxp) {
   }
 
   fxp_version_add_version_ext(fxp->pool, &buf, &buflen);
-  fxp_version_add_openssh_exts(fxp->pool, &buf, &buflen);
 
   if (fxp_session->client_version >= 4) {
     fxp_version_add_newline_ext(fxp->pool, &buf, &buflen);
@@ -6305,10 +7690,12 @@ static int fxp_handle_init(struct fxp_packet *fxp) {
     fxp_version_add_supported2_ext(fxp->pool, &buf, &buflen);
   }
 
+  fxp_version_add_openssh_exts(fxp->pool, &buf, &buflen);
+
   pr_event_generate("mod_sftp.sftp.protocol-version",
     &(fxp_session->client_version));
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -6319,7 +7706,7 @@ static int fxp_handle_init(struct fxp_packet *fxp) {
 
 static int fxp_handle_link(struct fxp_packet *fxp) {
   unsigned char *buf, *ptr;
-  char *args, *cmd_name, *src_path, *dst_path;
+  char *args, *cmd_name, *link_path, *target_path;
   const char *reason;
   char is_symlink;
   int have_error = FALSE, res;
@@ -6327,20 +7714,21 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
   struct fxp_packet *resp;
   cmd_rec *cmd;
 
-  src_path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+  link_path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-    src_path = sftp_utf8_decode_str(fxp->pool, src_path);
+    link_path = sftp_utf8_decode_str(fxp->pool, link_path);
   }
 
-  dst_path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+  target_path = sftp_msg_read_string(fxp->pool, &fxp->payload,
+    &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-    dst_path = sftp_utf8_decode_str(fxp->pool, dst_path);
+    target_path = sftp_utf8_decode_str(fxp->pool, target_path);
   }
 
-  args = pstrcat(fxp->pool, src_path, " ", dst_path, NULL);
+  args = pstrcat(fxp->pool, link_path, " ", target_path, NULL);
 
   cmd = fxp_cmd_alloc(fxp->pool, "LINK", args);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "LINK", NULL, NULL);
@@ -6350,30 +7738,30 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
   is_symlink = sftp_msg_read_byte(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
   pr_proctitle_set("%s - %s: LINK %s %s %s", session.user, session.proc_prefix,
-    src_path, dst_path, is_symlink ? "true" : "false");
+    link_path, target_path, is_symlink ? "true" : "false");
 
-  pr_trace_msg(trace_channel, 7, "received request: LINK %s %s %d", src_path,
-    dst_path, is_symlink);
+  pr_trace_msg(trace_channel, 7, "received request: LINK %s %s %s", link_path,
+    target_path, is_symlink ? "true" : "false");
 
-  if (strlen(src_path) == 0) {
+  if (strlen(link_path) == 0) {
     /* Use the default directory if the path is empty. */
-    src_path = sftp_auth_get_default_dir();
+    link_path = sftp_auth_get_default_dir();
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "empty link path given in LINK request, using '%s'", src_path);
+      "empty link path given in LINK request, using '%s'", link_path);
   }
 
-  if (strlen(dst_path) == 0) {
+  if (strlen(target_path) == 0) {
     /* Use the default directory if the path is empty. */
-    dst_path = sftp_auth_get_default_dir();
+    target_path = sftp_auth_get_default_dir();
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "empty target path given in LINK request, using '%s'", dst_path);
+      "empty target path given in LINK request, using '%s'", target_path);
   }
 
   /* Make sure we use the full paths. */
-  src_path = dir_canonical_vpath(fxp->pool, src_path);
-  dst_path = dir_canonical_vpath(fxp->pool, dst_path);
+  link_path = dir_canonical_vpath(fxp->pool, link_path);
+  target_path = dir_canonical_vpath(fxp->pool, target_path);
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -6381,27 +7769,29 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
   cmd_name = cmd->argv[0];
   pr_cmd_set_name(cmd, "LINK");
 
-  if (!dir_check(fxp->pool, cmd, G_READ, src_path, NULL)) {
+  if (!dir_check(fxp->pool, cmd, G_READ, target_path, NULL)) {
     have_error = TRUE;
   }
 
   if (!have_error) {
-    if (!dir_check(fxp->pool, cmd, G_WRITE, dst_path, NULL)) {
+    if (!dir_check(fxp->pool, cmd, G_WRITE, link_path, NULL)) {
       have_error = TRUE;
     }
   }
 
-  if (!have_error) {
-    pr_cmd_set_name(cmd, "SYMLINK");
-
-    if (!dir_check(fxp->pool, cmd, G_READ, src_path, NULL)) {
-      have_error = TRUE;
-    }
-
+  if (is_symlink) {
     if (!have_error) {
-      if (!dir_check(fxp->pool, cmd, G_WRITE, dst_path, NULL)) {
+      pr_cmd_set_name(cmd, "SYMLINK");
+
+      if (!dir_check(fxp->pool, cmd, G_READ, target_path, NULL)) {
         have_error = TRUE;
       }
+
+      if (!have_error) {
+        if (!dir_check(fxp->pool, cmd, G_WRITE, link_path, NULL)) {
+          have_error = TRUE;
+        }
+      }
     }
   }
 
@@ -6410,18 +7800,17 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "LINK of '%s' to '%s' blocked by <Limit %s> configuration",
-      src_path, dst_path, cmd->argv[0]);
+      target_path, link_path, (char *) cmd->argv[0]);
 
     pr_cmd_set_name(cmd, cmd_name);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6433,19 +7822,18 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
   pr_cmd_set_name(cmd, cmd_name);
 
   if (is_symlink) {
-    res = pr_fsio_symlink(src_path, dst_path);
+    res = pr_fsio_symlink(target_path, link_path);
 
   } else {
-    res = pr_fsio_link(src_path, dst_path);
+    res = pr_fsio_link(target_path, link_path);
   }
 
   if (res < 0) {
     int xerrno = errno;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error %s symlinking '%s' to '%s': %s",
-      is_symlink ? "symlinking" : "linking", src_path, dst_path,
-      strerror(xerrno));
+      "error %s '%s' to '%s': %s", is_symlink ? "symlinking" : "linking",
+      target_path, link_path, strerror(xerrno));
 
     status_code = fxp_errno2status(xerrno, &reason);
 
@@ -6453,8 +7841,7 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
   } else {
     errno = 0;
@@ -6463,11 +7850,11 @@ static int fxp_handle_link(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+    fxp_cmd_dispatch(cmd);
   }
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -6493,7 +7880,7 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
   lock_flags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
   cmd = fxp_cmd_alloc(fxp->pool, "LOCK", name);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "LOCK", NULL, NULL);
@@ -6512,19 +7899,18 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6543,11 +7929,10 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6565,11 +7950,10 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6591,11 +7975,10 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6616,11 +7999,10 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
-  
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -6694,11 +8076,10 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
         xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
     }
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -6715,11 +8096,10 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, fxp_strerror(status_code));
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
     fxp_strerror(status_code), NULL);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -6729,10 +8109,11 @@ static int fxp_handle_lock(struct fxp_packet *fxp) {
 }
 
 static int fxp_handle_lstat(struct fxp_packet *fxp) {
-  unsigned char *buf, *ptr;
+  unsigned char *buf;
   char *cmd_name, *path;
-  uint32_t buflen, bufsz;
+  uint32_t attr_flags, buflen;
   struct stat st;
+  struct fxp_buffer *fxb;
   struct fxp_packet *resp;
   cmd_rec *cmd;
   const char *fake_user = NULL, *fake_group = NULL;
@@ -6751,11 +8132,6 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
     path);
 
   if (fxp_session->client_version > 3) {
-    uint32_t attr_flags;
-
-    /* These are hints from the client about what file attributes are
-     * of particular interest.  We do not currently honor them.
-     */
     attr_flags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
     pr_trace_msg(trace_channel, 7, "received request: LSTAT %s %s", path,
@@ -6763,6 +8139,11 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
 
   } else {
     pr_trace_msg(trace_channel, 7, "received request: LSTAT %s", path);
+    attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
+      SSH2_FX_ATTR_ACMODTIME;
+#ifdef PR_USE_XATTR
+    attr_flags |= SSH2_FX_ATTR_EXTENDED;
+#endif /* PR_USE_XATTR */
   }
 
   if (strlen(path) == 0) {
@@ -6774,29 +8155,29 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "LSTAT", path);
-  cmd->cmd_class = CL_READ;
+  cmd->cmd_class = CL_READ|CL_SFTP;
 
-  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
-  buf = ptr = palloc(fxp->pool, bufsz);
+  fxb = pcalloc(fxp->pool, sizeof(struct fxp_buffer));
+  fxb->bufsz = buflen = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  fxb->ptr = buf = palloc(fxp->pool, fxb->bufsz);
 
   if (pr_cmd_dispatch_phase(cmd, PRE_CMD, 0) < 0) {
     uint32_t status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "LSTAT of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "LSTAT of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -6817,14 +8198,14 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
        xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -6843,21 +8224,20 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
   pr_cmd_set_name(cmd, cmd_name);
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   if (pr_fsio_lstat(path, &st) < 0) {
     uint32_t status_code;
     const char *reason;
@@ -6872,15 +8252,14 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -6905,14 +8284,20 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
     fake_group = session.group;
   }
 
-  fxp_attrs_write(fxp->pool, &buf, &buflen, &st, fake_user, fake_group);
+  fxb->buf = buf;
+  fxb->buflen = buflen;
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_attrs_write(fxp->pool, fxb, path, &st, attr_flags, fake_user, fake_group);
+
+  /* fxp_attrs_write will have changed the buf/buflen fields in the buffer. */
+  buf = fxb->buf;
+  buflen = fxb->buflen;
+
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-  resp->payload = ptr;
-  resp->payload_sz = (bufsz - buflen);
+  resp->payload = fxb->ptr;
+  resp->payload_sz = (fxb->bufsz - buflen);
 
   return fxp_packet_write(resp);
 }
@@ -6920,12 +8305,13 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
 static int fxp_handle_mkdir(struct fxp_packet *fxp) {
   unsigned char *buf, *ptr;
   char *attrs_str, *cmd_name, *path;
-  struct stat *attrs;
+  struct stat *attrs, st;
   int have_error = FALSE, res = 0;
   mode_t dir_mode;
   uint32_t attr_flags, buflen, bufsz, status_code;
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2;
+  array_header *xattrs = NULL;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
@@ -6937,8 +8323,11 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD_ARG, "%s", path, NULL, NULL);
 
-  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags);
+  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags,
+    &xattrs);
   if (attrs == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "MKDIR request missing required attributes, ignoring");
     return 0;
   }
 
@@ -6952,6 +8341,17 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
     attr_flags &= ~SSH2_FX_ATTR_PERMISSIONS;
   }
 
+  /* If the SFTPOption for ignoring xattrs for SFTP uploads is set, handle it
+   * by clearing the SSH2_FX_ATTR_EXTENDED flag.
+   */
+  if ((sftp_opts & SFTP_OPT_IGNORE_SFTP_UPLOAD_XATTRS) &&
+      (attr_flags & SSH2_FX_ATTR_EXTENDED)) {
+    pr_trace_msg(trace_channel, 7,
+      "SFTPOption 'IgnoreSFTPUploadExtendedAttributes' configured, "
+      "ignoring xattrs sent by client");
+    attr_flags &= ~SSH2_FX_ATTR_EXTENDED;
+  }
+
   attrs_str = fxp_strattrs(fxp->pool, attrs, &attr_flags);
 
   pr_proctitle_set("%s - %s: MKDIR %s %s", session.user, session.proc_prefix,
@@ -6968,26 +8368,25 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
       "empty path given in MKDIR request, using '%s'", path);
   }
 
-  cmd = fxp_cmd_alloc(fxp->pool, "MKDIR", path);
-  cmd->cmd_class = CL_WRITE;
-
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
 
+  cmd = fxp_cmd_alloc(fxp->pool, "MKDIR", path);
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
+
   if (pr_cmd_dispatch_phase(cmd, PRE_CMD, 0) < 0) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "MKDIR of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "MKDIR of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7003,21 +8402,18 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "MKDIR of '%s' blocked by '%s' handler", path, cmd2->argv[0]);
+      "MKDIR of '%s' blocked by '%s' handler", path, (char *) cmd2->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7037,15 +8433,12 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7065,7 +8458,8 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "MKDIR of '%s' blocked by <Limit %s> configuration", path, cmd->argv[0]);
+      "MKDIR of '%s' blocked by <Limit %s> configuration", path,
+      (char *) cmd->argv[0]);
 
     pr_cmd_set_name(cmd, cmd_name);
 
@@ -7073,15 +8467,12 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7095,21 +8486,58 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
   if (fxp_path_pass_regex_filters(fxp->pool, "MKDIR", path) < 0) {
     int xerrno = errno;
 
-    status_code = fxp_errno2status(xerrno, NULL);
+    status_code = fxp_errno2status(xerrno, NULL);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
+      (unsigned long) status_code, fxp_strerror(status_code));
+
+    pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
+    fxp_cmd_dispatch_err(cmd2);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      fxp_strerror(status_code), NULL);
+
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  dir_mode = (attr_flags & SSH2_FX_ATTR_PERMISSIONS) ? attrs->st_mode : 0777;
+
+  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+    "creating directory '%s' with mode 0%o", path, (unsigned int) dir_mode);
+
+  /* Check if the path already exists, to avoid unnecessary work. */
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    const char *reason;
+    int xerrno = EEXIST;
+
+    (void) pr_trace_msg("fileperms", 1, "MKDIR, user '%s' (UID %s, GID %s): "
+      "error making directory '%s': %s", session.user,
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      path, strerror(xerrno));
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "MKDIR of '%s' failed: %s", path, strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
 
-    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
-      (unsigned long) status_code, fxp_strerror(status_code));
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
-      fxp_strerror(status_code), NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7118,20 +8546,15 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
-  dir_mode = (attr_flags & SSH2_FX_ATTR_PERMISSIONS) ? attrs->st_mode : 0777;
-
-  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-    "creating directory '%s' with mode 0%o", path, (unsigned int) dir_mode);
-
   res = pr_fsio_smkdir(fxp->pool, path, dir_mode, (uid_t) -1, (gid_t) -1);
   if (res < 0) {
     const char *reason;
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "MKDIR, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "MKDIR, user '%s' (UID %s, GID %s): "
       "error making directory '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, path,
-      strerror(xerrno));
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "MKDIR of '%s' failed: %s", path, strerror(xerrno));
@@ -7143,15 +8566,12 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7163,7 +8583,7 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
   /* Handle any possible UserOwner/GroupOwner directives for created
    * directories.
    */
-  if (sftp_misc_chown_path(path) < 0) {
+  if (sftp_misc_chown_path(fxp->pool, path) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error changing ownership on path '%s': %s", path, strerror(errno));
   }
@@ -7173,14 +8593,14 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, fxp_strerror(status_code));
 
-  pr_cmd_dispatch_phase(cmd2, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd2, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd2);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
     fxp_strerror(status_code), NULL);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  pr_response_add(R_257, "\"%s\" - Directory successfully created",
+    quote_dir(cmd->tmp_pool, path));
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -7191,14 +8611,16 @@ static int fxp_handle_mkdir(struct fxp_packet *fxp) {
 
 static int fxp_handle_open(struct fxp_packet *fxp) {
   unsigned char *buf, *ptr;
-  char *path, *orig_path, *hiddenstore_path = NULL;
+  const char *hiddenstore_path = NULL;
+  char *path, *orig_path;
   uint32_t attr_flags, buflen, bufsz, desired_access = 0, flags;
   int file_existed = FALSE, open_flags, res, timeout_stalled;
   pr_fh_t *fh;
-  struct stat *attrs;
+  struct stat *attrs, st;
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2 = NULL;
+  array_header *xattrs = NULL;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
@@ -7211,7 +8633,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
   /* Set the command class to MISC for now; we'll change it later to
    * READ or WRITE once we know which it is.
    */
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "OPEN", NULL, NULL);
@@ -7270,11 +8692,11 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_note_file_status(cmd, "failed");
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -7325,11 +8747,11 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_note_file_status(cmd, "failed");
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -7339,8 +8761,9 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     }
 
     /* Make sure the requested path exists. */
+    pr_fs_clear_cache2(path);
     if ((flags & SSH2_FXF_OPEN_EXISTING) &&
-        !exists(path)) {
+        !exists2(fxp->pool, path)) {
       uint32_t status_code;
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -7352,11 +8775,11 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_note_file_status(cmd, "failed");
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -7375,10 +8798,13 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     open_flags = fxp_get_v5_open_flags(desired_access, flags);
   }
 
-  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags);
+  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags,
+    &xattrs);
   if (attrs == NULL) {
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_note_file_status(cmd, "failed");
+    fxp_cmd_dispatch_err(cmd);
+
+    /* XXX TODO: Provide a response to the client here */
     return 0;
   }
 
@@ -7387,7 +8813,8 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     fxp_stroflags(fxp->pool, open_flags));
 
   if (open_flags & O_APPEND) {
-    cmd->cmd_class = CL_WRITE;
+    cmd->cmd_class &= ~CL_MISC;
+    cmd->cmd_class |= CL_WRITE;
     cmd2 = fxp_cmd_alloc(fxp->pool, C_APPE, path);
     cmd2->cmd_id = pr_cmd_get_id(C_APPE);
     session.curr_cmd = C_APPE;
@@ -7406,10 +8833,12 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     cmd2->cmd_id = pr_cmd_get_id(C_STOR);
 
     if (open_flags & O_WRONLY) {
-      cmd->cmd_class = CL_WRITE;
+      cmd->cmd_class &= ~CL_MISC;
+      cmd->cmd_class |= CL_WRITE;
 
     } else if (open_flags & O_RDWR) {
-      cmd->cmd_class = CL_READ|CL_WRITE;
+      cmd->cmd_class &= ~CL_MISC;
+      cmd->cmd_class |= (CL_READ|CL_WRITE);
     }
 
     session.curr_cmd = C_STOR;
@@ -7423,7 +8852,8 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     }
 
   } else if (open_flags == O_RDONLY) {
-    cmd->cmd_class = CL_READ;
+    cmd->cmd_class &= ~CL_MISC;
+    cmd->cmd_class |= CL_READ;
     cmd2 = fxp_cmd_alloc(fxp->pool, C_RETR, path);
     cmd2->cmd_id = pr_cmd_get_id(C_RETR);
     session.curr_cmd = C_RETR;
@@ -7459,13 +8889,13 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       /* One of the PRE_CMD phase handlers rejected the command. */
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "OPEN command for '%s' blocked by '%s' handler", path, cmd2->argv[0]);
+        "OPEN command for '%s' blocked by '%s' handler", path,
+        (char *) cmd2->argv[0]);
 
       /* Hopefully the command handlers set an appropriate errno value.  If
        * they didn't, however, we need to be prepared with a fallback.
        */
       if (xerrno != ENOENT &&
-          xerrno != EACCES &&
           xerrno != EPERM &&
 #if defined(EDQUOT)
           xerrno != EDQUOT &&
@@ -7483,19 +8913,18 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       status_code = fxp_errno2status(xerrno, &reason);
 
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-        "('%s' [%d])", (unsigned long) status_code, reason,
-        xerrno != EOF ? strerror(errno) : "End of file", xerrno);
+        "('%s' [%d])", (unsigned long) status_code, reason, strerror(errno),
+        xerrno);
 
       pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-      pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-      pr_response_clear(&resp_err_list);
+      fxp_cmd_note_file_status(cmd2, "failed");
+      fxp_cmd_dispatch_err(cmd2);
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_note_file_status(cmd, "failed");
+      fxp_cmd_dispatch_err(cmd);
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-        NULL);
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+        reason, NULL);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -7507,7 +8936,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     path = cmd2->arg;
 
     if (session.xfer.xfer_type == STOR_HIDDEN) {
-      void *nfs;
+      const void *nfs;
 
       hiddenstore_path = pr_table_get(cmd2->notes,
         "mod_xfer.store-hidden-path", NULL);
@@ -7518,10 +8947,37 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       }
 
     } else {
-      path = dir_best_path(fxp->pool, path);
+      pr_fs_clear_cache2(path);
+      if (pr_fsio_lstat(path, &st) == 0) {
+        if (S_ISLNK(st.st_mode)) {
+          char link_path[PR_TUNABLE_PATH_MAX];
+          int len;
+
+          memset(link_path, '\0', sizeof(link_path));
+          len = dir_readlink(fxp->pool, path, link_path, sizeof(link_path)-1,
+            PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+          if (len > 0) {
+            link_path[len] = '\0';
+            path = pstrdup(fxp->pool, link_path);
+          } else {
+            path = dir_best_path(fxp->pool, path);
+          }
+
+        } else {
+          path = dir_best_path(fxp->pool, path);
+        }
+
+      } else {
+        path = dir_best_path(fxp->pool, path);
+      }
     }
 
-    file_existed = exists(hiddenstore_path ? hiddenstore_path : path);
+    if (hiddenstore_path != NULL) {
+      pr_fs_clear_cache2(hiddenstore_path);
+    }
+
+    file_existed = exists2(fxp->pool,
+      hiddenstore_path ? hiddenstore_path : path);
 
     if (file_existed &&
         (pr_cmd_cmp(cmd2, PR_CMD_STOR_ID) == 0 ||
@@ -7543,7 +8999,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       (void) pr_table_remove(cmd2->notes, "mod_xfer.file-modified", NULL);
 
       if (pr_table_add(cmd2->notes, "mod_xfer.file-modified",
-          pstrdup(cmd->pool, "true"), 0) < 0) {
+          pstrdup(cmd2->pool, "true"), 0) < 0) {
         if (errno != EEXIST) {
           pr_log_pri(PR_LOG_NOTICE,
             "notice: error adding 'mod_xfer.file-modified' note: %s",
@@ -7553,7 +9009,8 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     }
   }
 
-  if (exists(path)) {
+  pr_fs_clear_cache2(path);
+  if (exists2(fxp->pool, path)) {
     /* draft-ietf-secsh-filexfer-06.txt, section 7.1.1 specifically
      * states that any attributes in a OPEN request are ignored if the
      * file already exists.
@@ -7585,9 +9042,9 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     const char *reason;
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "OPEN, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "OPEN, user '%s' (UID %s, GID %s): "
       "error opening '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
       hiddenstore_path ? hiddenstore_path : path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -7600,18 +9057,64 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    if (cmd2) {
+    if (cmd2 != NULL) {
+      pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
+      fxp_cmd_note_file_status(cmd2, "failed");
+      fxp_cmd_dispatch_err(cmd2);
+    }
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    fxp_cmd_note_file_status(cmd, "failed");
+    fxp_cmd_dispatch_err(cmd);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  memset(&st, 0, sizeof(st));
+  if (pr_fsio_fstat(fh, &st) < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "fstat error on '%s' (fd %d): %s", path, fh->fh_fd, strerror(errno));
+  }
+
+#ifdef S_ISFIFO
+  /* The path in question might be a FIFO.  The FIFO case requires some special
+   * handling, modulo any IgnoreFIFOs SFTPOption that might be in effect.
+   */
+  if (S_ISFIFO(st.st_mode) &&
+      (sftp_opts & SFTP_OPT_IGNORE_FIFOS)) {
+    uint32_t status_code;
+    const char *reason;
+    int xerrno = EPERM;
+
+    (void) pr_fsio_close(fh);
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error opening FIFO '%s': %s (IgnoreFIFOs SFTPOption in effect)",
+      hiddenstore_path ? hiddenstore_path : path, strerror(xerrno));
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    if (cmd2 != NULL) {
       pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-      pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-      pr_response_clear(&resp_err_list);
+      fxp_cmd_note_file_status(cmd2, "failed");
+      fxp_cmd_dispatch_err(cmd2);
     }
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_note_file_status(cmd, "failed");
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7619,8 +9122,13 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
 
     return fxp_packet_write(resp);
   }
+#endif /* S_ISFIFO */
 
-  pr_fsio_set_block(fh);
+  if (pr_fsio_set_block(fh) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error setting fd %d (file '%s') as blocking: %s", fh->fh_fd,
+      fh->fh_path, strerror(errno));
+  }
  
   /* If the SFTPOption for ignoring perms for SFTP uploads is set, handle
    * it by clearing the SSH2_FX_ATTR_PERMISSIONS flag.
@@ -7632,6 +9140,17 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     attr_flags &= ~SSH2_FX_ATTR_PERMISSIONS;
   }
 
+  /* If the SFTPOption for ignoring xattrs for SFTP uploads is set, handle it
+   * by clearing the SSH2_FX_ATTR_EXTENDED flag.
+   */
+  if ((sftp_opts & SFTP_OPT_IGNORE_SFTP_UPLOAD_XATTRS) &&
+      (attr_flags & SSH2_FX_ATTR_EXTENDED)) {
+    pr_trace_msg(trace_channel, 7,
+      "SFTPOption 'IgnoreSFTPUploadExtendedAttributes' configured, "
+      "ignoring xattrs sent by client");
+    attr_flags &= ~SSH2_FX_ATTR_EXTENDED;
+  }
+
   /* If the client provided a suggested size in the OPEN, ignore it.
    * Trying to honor the suggested size by truncating the file here can
    * cause problems, as when the client is resuming a transfer and the
@@ -7654,21 +9173,21 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
 
   attr_flags &= ~SSH2_FX_ATTR_SIZE;
 
-  res = fxp_attrs_set(fh, fh->fh_path, attrs, attr_flags, &buf, &buflen, fxp);
+  res = fxp_attrs_set(fh, fh->fh_path, attrs, attr_flags, xattrs, &buf,
+    &buflen, fxp);
   if (res < 0) {
     int xerrno = errno;
 
     pr_fsio_close(fh);
 
-    if (cmd2) {
+    if (cmd2 != NULL) {
       pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-      pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-      pr_response_clear(&resp_err_list);
+      fxp_cmd_note_file_status(cmd2, "failed");
+      fxp_cmd_dispatch_err(cmd2);
     }
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_note_file_status(cmd, "failed");
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7682,7 +9201,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     /* Handle any possible UserOwner/GroupOwner directives for uploaded
      * files.
      */
-    if (sftp_misc_chown_file(fh) < 0) {
+    if (sftp_misc_chown_file(fxp->pool, fh) < 0) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error changing ownership on file '%s': %s", fh->fh_path,
         strerror(errno));
@@ -7704,18 +9223,17 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    if (cmd2) {
+    if (cmd2 != NULL) {
       pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-      pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-      pr_response_clear(&resp_err_list);
+      fxp_cmd_note_file_status(cmd2, "failed");
+      fxp_cmd_dispatch_err(cmd2);
     }
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_note_file_status(cmd, "failed");
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7727,6 +9245,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
   fxh->fh = fh;
   fxh->fh_flags = open_flags;
   fxh->fh_existed = file_existed;
+  memcpy(fxh->fh_st, &st, sizeof(struct stat));
 
   if (hiddenstore_path) {
     fxh->fh_real_path = pstrdup(fxh->pool, path);
@@ -7749,18 +9268,17 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
     pr_fsio_close(fh);
     destroy_pool(fxh->pool);
 
-    if (cmd2) {
+    if (cmd2 != NULL) {
       pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-      pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-      pr_response_clear(&resp_err_list);
+      fxp_cmd_note_file_status(cmd2, "failed");
+      fxp_cmd_dispatch_err(cmd2);
     }
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_note_file_status(cmd, "failed");
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7782,6 +9300,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
   memset(&session.xfer, 0, sizeof(session.xfer));
 
   session.xfer.p = make_sub_pool(fxp_pool);
+  pr_pool_tag(session.xfer.p, "SFTP session transfer pool");
   session.xfer.path = pstrdup(session.xfer.p, orig_path);
   memset(&session.xfer.start_time, 0, sizeof(session.xfer.start_time));
   gettimeofday(&session.xfer.start_time, NULL);
@@ -7806,8 +9325,7 @@ static int fxp_handle_open(struct fxp_packet *fxp) {
   /* Add a note containing the file handle for logging (Bug#3707). */
   fxp_set_filehandle_note(cmd, fxh);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -7825,6 +9343,7 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2;
+  struct stat st;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
@@ -7850,7 +9369,7 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "OPENDIR", path);
-  cmd->cmd_class = CL_DIRS;
+  cmd->cmd_class = CL_DIRS|CL_SFTP;
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -7859,16 +9378,15 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
     uint32_t status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "OPENDIR of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "OPENDIR of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7878,7 +9396,25 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
   }
 
   /* The path may have been changed by any PRE_CMD handlers. */
-  path = dir_best_path(fxp->pool, cmd->arg);
+  path = cmd->arg;
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(fxp->pool, path, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        path = pstrdup(fxp->pool, link_path);
+      }
+    }
+  }
+
+  path = dir_best_path(fxp->pool, path);
   if (path == NULL) {
     int xerrno = EACCES;
     const char *reason;
@@ -7893,10 +9429,10 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
        xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7914,11 +9450,10 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7938,7 +9473,8 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
 
     /* One of the PRE_CMD phase handlers rejected the command. */
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "OPENDIR command for '%s' blocked by '%s' handler", path, cmd2->argv[0]);
+      "OPENDIR command for '%s' blocked by '%s' handler", path,
+      (char *) cmd2->argv[0]);
 
     /* Hopefully the command handlers set an appropriate errno value.  If
      * they didn't, however, we need to be prepared with a fallback.
@@ -7957,14 +9493,12 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
        xerrno);
 
     pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -7992,15 +9526,12 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
     pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8017,10 +9548,10 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
     const char *reason;
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "OPENDIR, user '%s' (UID %lu, "
-      "GID %lu): error opening '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, path,
-      strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "OPENDIR, user '%s' (UID %s, "
+      "GID %s): error opening '%s': %s", session.user,
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error opening '%s': %s", path, strerror(xerrno));
@@ -8032,15 +9563,12 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
     pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8065,15 +9593,12 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
     pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8107,15 +9632,12 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
     destroy_pool(fxh->pool);
 
     pr_response_add_err(R_451, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8142,6 +9664,7 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
     memset(&session.xfer, 0, sizeof(session.xfer));
 
     session.xfer.p = make_sub_pool(fxp_pool);
+    pr_pool_tag(session.xfer.p, "SFTP session transfer pool");
     memset(&session.xfer.start_time, 0, sizeof(session.xfer.start_time));
     gettimeofday(&session.xfer.start_time, NULL);
     session.xfer.direction = PR_NETIO_IO_WR;
@@ -8155,8 +9678,7 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
       fxp_timeout_stalled_cb, "TimeoutStalled");
   }
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -8167,14 +9689,14 @@ static int fxp_handle_opendir(struct fxp_packet *fxp) {
 
 static int fxp_handle_read(struct fxp_packet *fxp) {
   unsigned char *buf, *data = NULL, *ptr;
-  char *cmd_name, *name;
+  char *file, *name, *ptr2;
   int res;
   uint32_t buflen, bufsz, datalen;
   uint64_t offset;
-  struct stat st;
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2;
+  pr_buffer_t *pbuf;
 
   name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   offset = sftp_msg_read_long(fxp->pool, &fxp->payload, &fxp->payload_sz);
@@ -8193,7 +9715,7 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
 #endif
 
   cmd = fxp_cmd_alloc(fxp->pool, "READ", name);
-  cmd->cmd_class = CL_READ;
+  cmd->cmd_class = CL_READ|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "READ", NULL, NULL);
@@ -8214,19 +9736,18 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
     uint32_t status_code;
 
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8241,11 +9762,10 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8260,33 +9780,7 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD_ARG, "%s", fxh->fh->fh_path, NULL, NULL);
 
-  if (pr_fsio_fstat(fxh->fh, &st) < 0) {
-    uint32_t status_code;
-    const char *reason;
-    int xerrno = errno;
-
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error checking '%s' for READ: %s", fxh->fh->fh_path, strerror(xerrno));
-
-    status_code = fxp_errno2status(errno, &reason);
-
-    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-      "('%s' [%d])", (unsigned long) status_code, reason,
-      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
-
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
-
-    pr_cmd_dispatch_phase(cmd, xerrno == EOF ? LOG_CMD : LOG_CMD_ERR, 0);
-
-    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
-
-    return fxp_packet_write(resp);
-  }
-
-  if (offset > st.st_size) {
+  if ((off_t) offset > fxh->fh_st->st_size) {
     uint32_t status_code;
     const char *reason;
     int xerrno = EOF;
@@ -8294,7 +9788,7 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "requested read offset (%" PR_LU " bytes) greater than size of "
       "'%s' (%" PR_LU " bytes)", (pr_off_t) offset, fxh->fh->fh_path,
-      (pr_off_t) st.st_size);
+      (pr_off_t) fxh->fh_st->st_size);
 
     status_code = fxp_errno2status(xerrno, &reason);
 
@@ -8302,11 +9796,10 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason, "End of file",
       xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8316,29 +9809,36 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
   }
 
   pr_scoreboard_entry_update(session.pid,
-    PR_SCORE_XFER_SIZE, st.st_size,
+    PR_SCORE_XFER_SIZE, fxh->fh_st->st_size,
     PR_SCORE_XFER_DONE, (off_t) offset,
     NULL);
 
-  cmd_name = cmd->argv[0];
-  pr_cmd_set_name(cmd, C_RETR);
+  /* Trim the full path to just the filename, for our RETR command. */
+  ptr2 = strrchr(fxh->fh->fh_path, '/');
+  if (ptr2 != NULL &&
+      ptr2 != fxh->fh->fh_path) {
+    file = pstrdup(fxp->pool, ptr2 + 1);
+
+  } else {
+    file = fxh->fh->fh_path;
+  }
+
+  cmd2 = fxp_cmd_alloc(fxp->pool, C_RETR, file);
+  cmd2->cmd_class = CL_READ|CL_SFTP;
 
   if (!dir_check(fxp->pool, cmd, G_READ, fxh->fh->fh_path, NULL)) {
     uint32_t status_code = SSH2_FX_PERMISSION_DENIED;
 
-    pr_cmd_set_name(cmd, cmd_name);
-
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "READ of '%s' blocked by <Limit> configuration", fxh->fh->fh_path);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8346,7 +9846,6 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
 
     return fxp_packet_write(resp);
   }
-  pr_cmd_set_name(cmd, cmd_name);
 
   /* XXX Check MaxRetrieveFileSize */
 
@@ -8359,10 +9858,10 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8371,7 +9870,7 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
-  if (S_ISREG(st.st_mode)) {
+  if (S_ISREG(fxh->fh_st->st_mode)) {
     if (pr_fsio_lseek(fxh->fh, offset, SEEK_SET) < 0) {
       uint32_t status_code;
       const char *reason;
@@ -8387,11 +9886,10 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
         "('%s' [%d])", (unsigned long) status_code, reason,
         xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-        NULL);
-  
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+        reason, NULL);
+
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
@@ -8400,6 +9898,14 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
       return fxp_packet_write(resp);
 
     } else {
+      off_t *file_offset;
+
+      /* Stash the offset at which we're reading from this file. */
+      file_offset = palloc(cmd->pool, sizeof(off_t));
+      *file_offset = (off_t) offset;
+      (void) pr_table_add(cmd->notes, "mod_xfer.file-offset", file_offset,
+        sizeof(off_t));
+
       /* No error. */
       errno = 0;
     }
@@ -8430,9 +9936,9 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
     if (res < 0) {
       xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "READ, user '%s' (UID %lu, GID %lu): "
+      (void) pr_trace_msg("fileperms", 1, "READ, user '%s' (UID %s, GID %s): "
         "error reading from '%s': %s", session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
+        pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
         fxh->fh->fh_path, strerror(xerrno));
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -8452,11 +9958,15 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    if (xerrno != EOF) {
+      fxp_cmd_dispatch_err(cmd);
 
-    pr_cmd_dispatch_phase(cmd, xerrno != EOF ? POST_CMD_ERR : POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd, xerrno != EOF ? LOG_CMD_ERR : LOG_CMD, 0);
+    } else {
+      fxp_cmd_dispatch(cmd);
+    }
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -8470,6 +9980,13 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: DATA (%lu bytes)",
     (unsigned long) res);
 
+  pbuf = pcalloc(fxp->pool, sizeof(pr_buffer_t));
+  pbuf->buf = (char *) data;
+  pbuf->buflen = res;
+  pbuf->current = pbuf->buf;
+  pbuf->remaining = 0;
+  pr_event_generate("mod_sftp.sftp.data-write", pbuf);
+
   sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_DATA);
   sftp_msg_write_int(&buf, &buflen, fxp->request_id);
   sftp_msg_write_data(&buf, &buflen, data, res, TRUE);
@@ -8481,9 +9998,8 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
   fxh->fh_bytes_xferred += res;
   session.xfer.total_bytes += res;
   session.total_bytes += res;
-  
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+
+  fxp_cmd_dispatch(cmd);
 
   res = fxp_packet_write(resp);
   return res;
@@ -8491,10 +10007,11 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
 
 static int fxp_handle_readdir(struct fxp_packet *fxp) {
   register unsigned int i;
-  unsigned char *buf, *ptr;
+  unsigned char *buf;
   char *cmd_name, *name;
-  uint32_t buflen, bufsz, curr_packet_pathsz = 0, max_packetsz;
+  uint32_t attr_flags, buflen, curr_packet_pathsz = 0, max_packetsz;
   struct dirent *dent;
+  struct fxp_buffer *fxb;
   struct fxp_dirent **paths;
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
@@ -8507,7 +10024,7 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
   name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
   cmd = fxp_cmd_alloc(fxp->pool, "READDIR", name);
-  cmd->cmd_class = CL_DIRS;
+  cmd->cmd_class = CL_DIRS|CL_SFTP;
   cmd->group = G_DIRS;
 
   pr_scoreboard_entry_update(session.pid,
@@ -8522,32 +10039,33 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
 
   /* XXX What's a good size here? */
 
+  fxb = pcalloc(fxp->pool, sizeof(struct fxp_buffer));
+
   max_packetsz = sftp_channel_get_max_packetsz();
-  buflen = bufsz = max_packetsz;
-  buf = ptr = palloc(fxp->pool, bufsz);
+  fxb->bufsz = buflen = max_packetsz;
+  fxb->ptr = buf = palloc(fxp->pool, fxb->bufsz);
 
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     uint32_t status_code;
 
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -8558,15 +10076,14 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
   
     return fxp_packet_write(resp);
   }
@@ -8608,22 +10125,21 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
  
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "READDIR of '%s' blocked by <Limit %s> configuration", fxh->dir,
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
 
     pr_cmd_set_name(cmd, cmd_name);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD, 0); 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -8653,15 +10169,14 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -8686,12 +10201,20 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
   while ((dent = pr_fsio_readdir(fxh->dirh)) != NULL) {
     char *real_path;
     struct fxp_dirent *fxd;
-    uint32_t curr_packetsz, max_entry_metadata = 256;
-    uint32_t max_entrysz = (PR_TUNABLE_PATH_MAX + 1 + max_entry_metadata);
+    uint32_t curr_packetsz, max_entry_metadata, max_entrysz;
     size_t dent_len;
 
     pr_signals_handle();
 
+    /* How much non-path data do we expect to be associated with this entry? */
+#ifdef PR_USE_XATTR
+    max_entry_metadata = (1024 * 4);
+#else
+    max_entry_metadata = 256;
+#endif /* PR_USE_XATTR */
+
+    max_entrysz = (PR_TUNABLE_PATH_MAX + 1 + max_entry_metadata);
+
     /* Do not expand/resolve dot directories; it will be handled automatically
      * lower down in the ACL-checking code.  Plus, this allows regex filters
      * that rely on the dot directory name to work properly.
@@ -8724,7 +10247,7 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
      * the maximum packet size and the max entry size.
      *
      * We assume that each entry will need up to PR_TUNABLE_PATH_MAX+1 bytes for
-     * the filename, and 256 bytes of associated data.
+     * the filename, and max_entry_metadata bytes of associated data.
      *
      * We have the total number of entries for this message when there is less
      * than enough space for one more maximum-sized entry.
@@ -8761,15 +10284,14 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -8781,15 +10303,14 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+
+    fxp_cmd_dispatch(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
   
     return fxp_packet_write(resp);
   }
@@ -8801,39 +10322,77 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
   sftp_msg_write_int(&buf, &buflen, fxp->request_id);
   sftp_msg_write_int(&buf, &buflen, path_list->nelts);
 
+  fxb->buf = buf;
+  fxb->buflen = buflen;
   paths = path_list->elts;
+
+  /* For READDIR requests, since they do NOT contain a flags field for clients
+   * to express which attributes they want, we ASSUME some standard fields.
+   */
+
+  if (fxp_session->client_version <= 3) {
+    attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
+      SSH2_FX_ATTR_ACMODTIME;
+
+  } else {
+    attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_PERMISSIONS|
+      SSH2_FX_ATTR_ACCESSTIME|SSH2_FX_ATTR_MODIFYTIME|SSH2_FX_ATTR_OWNERGROUP;
+  }
+
+  /* The FX_ATTR_LINK_COUNT attribute was defined in
+   * draft-ietf-secsh-filexfer-06, which is SFTP protocol version 6.
+   */
+  if (fxp_session->client_version >= 6) {
+    attr_flags |= SSH2_FX_ATTR_LINK_COUNT;
+
+    /* The FX_ATTR_EXTENDED attribute was defined in
+     * draft-ietf-secsh-filexfer-02, which is SFTP protocol version 3.
+     * However, many SFTP clients may not be prepared for handling these.
+     * Thus we CHOOSE to only provide these extended attributes, if supported,
+     * to protocol version 6 clients.
+     */
+#ifdef PR_USE_XATTR
+    attr_flags |= SSH2_FX_ATTR_EXTENDED;
+#endif /* PR_USE_XATTR */
+  }
+
   for (i = 0; i < path_list->nelts; i++) {
     uint32_t name_len = 0;
 
-    name_len = fxp_name_write(fxp->pool, &buf, &buflen, paths[i]->client_path,
-      paths[i]->st, fake_user, fake_group);
+    name_len = fxp_name_write(fxp->pool, fxb, paths[i]->client_path,
+      paths[i]->st, attr_flags, fake_user, fake_group);
 
     pr_trace_msg(trace_channel, 19, "READDIR: FXP_NAME entry size: %lu bytes",
       (unsigned long) name_len);
   }
 
+  /* fxp_name_write will have changed the values stashed in the buffer. */
+  buf = fxb->buf;
+  buflen = fxb->buflen;
+
   if (fxp_session->client_version > 5) {
     sftp_msg_write_bool(&buf, &buflen, have_eod ? TRUE : FALSE);
   }
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-  resp->payload = ptr;
-  resp->payload_sz = (bufsz - buflen);
+  resp->payload = fxb->ptr;
+  resp->payload_sz = (fxb->bufsz - buflen);
 
   session.xfer.total_bytes += resp->payload_sz;
   session.total_bytes += resp->payload_sz;
-  
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+
+  fxp_cmd_dispatch(cmd);
+
   return fxp_packet_write(resp);
 }
 
 static int fxp_handle_readlink(struct fxp_packet *fxp) {
   char data[PR_TUNABLE_PATH_MAX + 1];
-  unsigned char *buf, *ptr;
+  unsigned char *buf;
   char *path, *resolved_path;
   int res;
-  uint32_t buflen, bufsz;
+  uint32_t buflen;
+  struct fxp_buffer *fxb;
   struct fxp_packet *resp;
   cmd_rec *cmd;
 
@@ -8861,35 +10420,38 @@ static int fxp_handle_readlink(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "READLINK", path);
-  cmd->cmd_class = CL_READ;
+  cmd->cmd_class = CL_READ|CL_SFTP;
 
-  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
-  buf = ptr = palloc(fxp->pool, bufsz);
+  fxb = pcalloc(fxp->pool, sizeof(struct fxp_buffer));
+  fxb->bufsz = buflen = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  fxb->ptr = buf = palloc(fxp->pool, fxb->bufsz);
 
   if (pr_cmd_dispatch_phase(cmd, PRE_CMD, 0) < 0) {
     uint32_t status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "READLINK of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "READLINK of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
 
   /* The path may have been changed by any PRE_CMD handlers. */
-  resolved_path = dir_best_path(fxp->pool, cmd->arg);
+  path = cmd->arg;
+  pr_fs_clear_cache2(path);
+
+  resolved_path = dir_best_path(fxp->pool, path);
   if (resolved_path == NULL) {
     int xerrno = EACCES;
     const char *reason;
@@ -8904,14 +10466,14 @@ static int fxp_handle_readlink(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
        xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -8921,49 +10483,54 @@ static int fxp_handle_readlink(struct fxp_packet *fxp) {
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "READLINK of '%s' (resolved to '%s') blocked by <Limit %s> configuration",
-      path, resolved_path, cmd->argv[0]);
+      path, resolved_path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
 
-  res = pr_fsio_readlink(path, data, sizeof(data) - 1);
+  memset(data, '\0', sizeof(data));
+
+  /* Note: do NOT use the resolved_path variable here, as it will have
+   * resolved by following any symlinks; readlink(2) would then return EINVAL
+   * for reading a non-symlink path.
+   */
+  res = dir_readlink(fxp->pool, path, data, sizeof(data) - 1,
+    PR_DIR_READLINK_FL_HANDLE_REL_PATH);
   if (res < 0) {
     uint32_t status_code;
     const char *reason;
     int xerrno = errno;
 
-    buf = ptr;
-    buflen = bufsz;
+    buf = fxb->ptr;
+    buflen = fxb->bufsz;
 
     status_code = fxp_errno2status(xerrno, &reason);
 
-    (void) pr_trace_msg("fileperms", 1, "READLINK, user '%s' (UID %lu, "
-      "GID %lu): error using readlink() on  '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, path,
-      strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "READLINK, user '%s' (UID %s, "
+      "GID %s): error using readlink() on  '%s': %s", session.user,
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      path, strerror(xerrno));
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
   } else {
     struct stat st;
@@ -8994,20 +10561,26 @@ static int fxp_handle_readlink(struct fxp_packet *fxp) {
       fake_group = session.group;
     }
 
-    fxp_name_write(fxp->pool, &buf, &buflen, data, &st, fake_user, fake_group);
+    fxb->buf = buf;
+    fxb->buflen = buflen;
+
+    fxp_name_write(fxp->pool, fxb, data, &st, 0, fake_user, fake_group);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+    buf = fxb->buf;
+    buflen = fxb->buflen;
+
+    fxp_cmd_dispatch(cmd);
   }
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-  resp->payload = ptr;
-  resp->payload_sz = (bufsz - buflen);
+  resp->payload = fxb->ptr;
+  resp->payload_sz = (fxb->bufsz - buflen);
 
   return fxp_packet_write(resp);
 }
 
-static void fxp_trace_v6_realpath_flags(pool *p, unsigned char flags) {
+static void fxp_trace_v6_realpath_flags(pool *p, unsigned char flags,
+    int client_sent) {
   char *flags_str = "";
   int trace_level = 15;
 
@@ -9015,34 +10588,36 @@ static void fxp_trace_v6_realpath_flags(pool *p, unsigned char flags) {
     return;
   }
 
-  if (flags & SSH2_FXRP_NO_CHECK) {
-    flags_str = pstrcat(p, flags_str, *flags_str ? "|" : "",
-      "FX_REALPATH_NO_CHECK", NULL);
-  }
+  switch (flags) {
+    case SSH2_FXRP_NO_CHECK:
+      flags_str = "FX_REALPATH_NO_CHECK";
+      break;
 
-  if (flags & SSH2_FXRP_STAT_IF) {
-    flags_str = pstrcat(p, flags_str, *flags_str ? "|" : "",
-      "FX_REALPATH_STAT_IF", NULL);
-  }
+    case SSH2_FXRP_STAT_IF:
+      flags_str = "FX_REALPATH_STAT_IF";
+      break;
 
-  if (flags & SSH2_FXRP_STAT_ALWAYS) {
-    flags_str = pstrcat(p, flags_str, *flags_str ? "|" : "",
-      "FX_REALPATH_STAT_ALWAYS", NULL);
+    case SSH2_FXRP_STAT_ALWAYS:
+      flags_str = "FX_REALPATH_STAT_ALWAYS";
+      break;
   }
 
-  pr_trace_msg(trace_channel, trace_level, "REALPATH flags = %s", flags_str);
+  pr_trace_msg(trace_channel, trace_level, "REALPATH flags = %s (%s)",
+    flags_str, client_sent == TRUE ? "explicit" : "default");
 }
 
 static int fxp_handle_realpath(struct fxp_packet *fxp) {
-  unsigned char *buf, *ptr, realpath_flags = SSH2_FXRP_NO_CHECK;
+  int res, xerrno;
+  unsigned char *buf, realpath_flags = SSH2_FXRP_NO_CHECK;
   char *path;
-  uint32_t buflen, bufsz;
+  uint32_t buflen;
   struct stat st;
+  struct fxp_buffer *fxb;
   struct fxp_packet *resp;
   cmd_rec *cmd;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
-  if (fxp_session->client_version > fxp_utf8_protocol_version) {
+  if (fxp_session->client_version >= fxp_utf8_protocol_version) {
     path = sftp_utf8_decode_str(fxp->pool, path);
   }
 
@@ -9065,7 +10640,7 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "REALPATH", path);
-  cmd->cmd_class = CL_INFO;
+  cmd->cmd_class = CL_INFO|CL_SFTP;
 
   if (fxp_session->client_version >= 6) {
     /* See Section 8.9 of:
@@ -9076,11 +10651,11 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
      */
 
     if (fxp->payload_sz >= sizeof(char)) {
-      char *composite_path;
+      char *composite_path = NULL;
 
       realpath_flags = sftp_msg_read_byte(fxp->pool, &fxp->payload,
         &fxp->payload_sz);
-      fxp_trace_v6_realpath_flags(fxp->pool, realpath_flags);
+      fxp_trace_v6_realpath_flags(fxp->pool, realpath_flags, TRUE);
 
       if (fxp->payload_sz > 0) {
         composite_path = sftp_msg_read_string(fxp->pool, &fxp->payload,
@@ -9091,28 +10666,36 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
          * may send.  The format of the REALPATH request, currently, only allows
          * for one composite-path element; the description of this feature
          * implies that multiple such composite-path elements could be supplied.
-         * Sigh.  I'll need to provide feedback on this, I see.
+         * Sigh.  Maybe it's meant to a blob of strings?  Or we keep reading
+         * a string until the remaining payload size is zero?
          */
+        pr_trace_msg(trace_channel, 13,
+          "REALPATH request set composite-path: '%s'", composite_path);
       }
+
+    } else {
+      fxp_trace_v6_realpath_flags(fxp->pool, realpath_flags, FALSE);
     }
   }
 
-  buflen = bufsz = PR_TUNABLE_PATH_MAX + 32;
-  buf = ptr = palloc(fxp->pool, bufsz);
+  fxb = pcalloc(fxp->pool, sizeof(struct fxp_buffer));
+  fxb->bufsz = buflen = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  fxb->ptr = buf = palloc(fxp->pool, fxb->bufsz);
 
-  if (pr_cmd_dispatch_phase(cmd, PRE_CMD, 0) < 0) {
+  res = pr_cmd_dispatch_phase(cmd, PRE_CMD, 0);
+  if (res < 0) {
     uint32_t status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "REALPATH of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "REALPATH of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     if (fxp_session->client_version <= 5 ||
         (fxp_session->client_version >= 6 &&
-         !(realpath_flags & SSH2_FXRP_NO_CHECK))) {
+         realpath_flags != SSH2_FXRP_NO_CHECK)) {
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
         (unsigned long) status_code, fxp_strerror(status_code));
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
         fxp_strerror(status_code), NULL);
 
     } else {
@@ -9128,16 +10711,21 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
       sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_NAME);
       sftp_msg_write_int(&buf, &buflen, fxp->request_id);
       sftp_msg_write_int(&buf, &buflen, 1);
-      fxp_name_write(fxp->pool, &buf, &buflen, path, &st, "nobody",
-        "nobody");
+
+      fxb->buf = buf;
+      fxb->buflen = buflen;
+
+      fxp_name_write(fxp->pool, fxb, path, &st, 0, "nobody", "nobody");
+
+      buf = fxb->buf;
+      buflen = fxb->buflen;
     }
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
-    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -9156,7 +10744,8 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
     if (vpath == NULL) {
       uint32_t status_code;
       const char *reason;
-      int xerrno = errno;
+
+      xerrno = errno;
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error resolving '%s': %s", path, strerror(xerrno));
@@ -9165,13 +10754,13 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
 
       if (fxp_session->client_version <= 5 ||
           (fxp_session->client_version >= 6 &&
-           !(realpath_flags & SSH2_FXRP_NO_CHECK))) {
+           realpath_flags != SSH2_FXRP_NO_CHECK)) {
         pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-          "('%s' [%d])", (unsigned long) status_code, reason,
-          xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+          "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+          xerrno);
 
-        fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+          reason, NULL);
 
       } else {
         uint32_t attr_flags = 0;
@@ -9186,16 +10775,21 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
         sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_NAME);
         sftp_msg_write_int(&buf, &buflen, fxp->request_id);
         sftp_msg_write_int(&buf, &buflen, 1);
-        fxp_name_write(fxp->pool, &buf, &buflen, path, &st, "nobody",
-          "nobody");
+
+        fxb->buf = buf;
+        fxb->buflen = buflen;
+
+        fxp_name_write(fxp->pool, fxb, path, &st, 0, "nobody", "nobody");
+
+        buf = fxb->buf;
+        buflen = fxb->buflen;
       }
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-      resp->payload = ptr;
-      resp->payload_sz = (bufsz - buflen);
+      resp->payload = fxb->ptr;
+      resp->payload_sz = (fxb->bufsz - buflen);
 
       return fxp_packet_write(resp);
     }
@@ -9206,31 +10800,33 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
   }
 
   /* Force a full lookup. */
+  pr_fs_clear_cache2(path);
   if (!dir_check_full(fxp->pool, cmd, G_DIRS, path, NULL)) {
     uint32_t status_code;
     const char *reason;
-    int xerrno = errno;
+
+    xerrno = errno;
 
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "REALPATH of '%s' blocked by <Limit> configuration", path);
 
-    buf = ptr;
-    buflen = bufsz;
+    buf = fxb->ptr;
+    buflen = fxb->bufsz;
 
     status_code = fxp_errno2status(xerrno, &reason);
 
     if (fxp_session->client_version <= 5 ||
         (fxp_session->client_version >= 6 &&
-         !(realpath_flags & SSH2_FXRP_NO_CHECK))) {
+         realpath_flags != SSH2_FXRP_NO_CHECK)) {
 
       pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-        "('%s' [%d])", (unsigned long) status_code, reason,
-        xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+        "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+        xerrno);
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-        NULL);
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+        reason, NULL);
 
     } else {
       uint32_t attr_flags = 0;
@@ -9245,37 +10841,70 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
       sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_NAME);
       sftp_msg_write_int(&buf, &buflen, fxp->request_id);
       sftp_msg_write_int(&buf, &buflen, 1);
-      fxp_name_write(fxp->pool, &buf, &buflen, path, &st, "nobody",
-        "nobody");
+
+      fxb->buf = buf;
+      fxb->buflen = buflen;
+
+      fxp_name_write(fxp->pool, fxb, path, &st, 0, "nobody", "nobody");
+
+      buf = fxb->buf;
+      buflen = fxb->buflen;
     }
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
   } else {
-    if (pr_fsio_lstat(path, &st) < 0) {
+   /* draft-ietf-secsh-filexfer-13 says:
+    *
+    *  SSH_FXP_REALPATH_NO_CHECK:
+    *    NOT resolve symbolic links (thus use lstat(2))
+    *
+    *  SSH_FXP_REALPATH_STAT_IF:
+    *    stat(2) the file, but if the stat(2) fails, do NOT fail the request,
+    *    but send a NAME with type UNKNOWN.
+    *
+    *  SSH_FXP_REALPATH_STAT_ALWAYS:
+    *   stat(2) the file, and return any error.
+    */
+
+    pr_fs_clear_cache2(path);
+    switch (realpath_flags) {
+      case SSH2_FXRP_NO_CHECK:
+        res = pr_fsio_lstat(path, &st);
+        xerrno = errno;
+        break;
+
+      case SSH2_FXRP_STAT_IF:
+      case SSH2_FXRP_STAT_ALWAYS:
+        res = pr_fsio_stat(path, &st);
+        xerrno = errno;
+        break;
+    }
+
+    if (res < 0) {
       uint32_t status_code;
       const char *reason;
-      int xerrno = errno;
+
+      xerrno = errno;
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error checking '%s' for REALPATH: %s", path, strerror(xerrno));
 
-      buf = ptr;
-      buflen = bufsz;
+      buf = fxb->ptr;
+      buflen = fxb->bufsz;
 
       status_code = fxp_errno2status(xerrno, &reason);
 
       if (fxp_session->client_version <= 5 ||
           (fxp_session->client_version >= 6 &&
-           !(realpath_flags & SSH2_FXRP_NO_CHECK))) {
+           realpath_flags == SSH2_FXRP_STAT_ALWAYS)) {
 
         pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-          "('%s' [%d])", (unsigned long) status_code, reason,
-          xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+          "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+          xerrno);
 
-        fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+          reason, NULL);
 
       } else {
         uint32_t attr_flags = 0;
@@ -9290,12 +10919,17 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
         sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_FXP_NAME);
         sftp_msg_write_int(&buf, &buflen, fxp->request_id);
         sftp_msg_write_int(&buf, &buflen, 1);
-        fxp_name_write(fxp->pool, &buf, &buflen, path, &st, "nobody",
-          "nobody");
+
+        fxb->buf = buf;
+        fxb->buflen = buflen;
+
+        fxp_name_write(fxp->pool, fxb, path, &st, 0, "nobody", "nobody");
+
+        buf = fxb->buf;
+        buflen = fxb->buflen;
       }
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_cmd_dispatch_err(cmd);
 
     } else {
       const char *fake_user = NULL, *fake_group = NULL;
@@ -9321,17 +10955,21 @@ static int fxp_handle_realpath(struct fxp_packet *fxp) {
         fake_group = session.group;
       }
 
-      fxp_name_write(fxp->pool, &buf, &buflen, path, &st, fake_user,
-        fake_group);
+      fxb->buf = buf;
+      fxb->buflen = buflen;
 
-      pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+      fxp_name_write(fxp->pool, fxb, path, &st, 0, fake_user, fake_group);
+
+      buf = fxb->buf;
+      buflen = fxb->buflen;
+
+      fxp_cmd_dispatch(cmd);
     }
   }
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-  resp->payload = ptr;
-  resp->payload_sz = (bufsz - buflen);
+  resp->payload = fxb->ptr;
+  resp->payload_sz = (fxb->bufsz - buflen);
 
   return fxp_packet_write(resp);
 }
@@ -9370,7 +11008,7 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "REMOVE", path);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -9379,16 +11017,15 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "REMOVE of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "REMOVE of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9404,21 +11041,18 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "DELE of '%s' blocked by '%s' handler", path, cmd2->argv[0]);
+      "DELE of '%s' blocked by '%s' handler", path, (char *) cmd2->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(EPERM));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9445,15 +11079,12 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(EPERM));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9473,15 +11104,12 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9504,15 +11132,12 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9521,6 +11146,7 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
+  pr_fs_clear_cache2(real_path);
   res = pr_fsio_lstat(real_path, &st);
   if (res < 0) {
     int xerrno = errno;
@@ -9535,15 +11161,12 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9561,19 +11184,16 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
     status_code = fxp_errno2status(xerrno, &reason);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-      "('%s' [%d])", (unsigned long) status_code, reason,
-      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9586,18 +11206,16 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
   if (res < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "REMOVE, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "REMOVE, user '%s' (UID %s, GID %s): "
       "error deleting '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, real_path,
-      strerror(xerrno));
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      real_path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error unlinking '%s': %s", real_path, strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
     errno = xerrno;
 
@@ -9612,10 +11230,8 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
     xferlog_write(0, session.c->remote_name, st.st_size, abs_path,
       'b', 'd', 'r', session.user, 'c', "_");
 
-    pr_response_add(R_250, "%s command successful", cmd2->argv[0]);
-    pr_cmd_dispatch_phase(cmd2, POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD, 0);
-    pr_response_clear(&resp_list);
+    pr_response_add(R_250, "%s command successful", (char *) cmd2->argv[0]);
+    fxp_cmd_dispatch(cmd2);
 
     errno = 0;
   }
@@ -9626,10 +11242,15 @@ static int fxp_handle_remove(struct fxp_packet *fxp) {
     "('%s' [%d])", (unsigned long) status_code, reason,
     errno != EOF ? strerror(errno) : "End of file", errno);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  if (res == 0) {
+    fxp_cmd_dispatch(cmd);
 
-  pr_cmd_dispatch_phase(cmd, res == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  } else {
+    fxp_cmd_dispatch_err(cmd);
+  }
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -9708,7 +11329,7 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "RENAME", args);
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SFTP;
  
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -9719,21 +11340,19 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "RENAME from '%s' blocked by '%s' handler", old_path, cmd2->argv[0]);
+      "RENAME from '%s' blocked by '%s' handler", old_path,
+      (char *) cmd2->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9753,14 +11372,12 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9769,8 +11386,13 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
-  pr_table_add(session.notes, "mod_core.rnfr-path",
-    pstrdup(session.pool, old_path), 0);
+  if (pr_table_add(session.notes, "mod_core.rnfr-path",
+      pstrdup(session.pool, old_path), 0) < 0) {
+    if (errno != EEXIST) {
+      pr_trace_msg(trace_channel, 8,
+        "error setting 'mod_core.rnfr-path' note: %s", strerror(errno));
+    }
+  }
 
   cmd3 = fxp_cmd_alloc(fxp->pool, C_RNTO, new_path);
   cmd3->cmd_class = CL_MISC|CL_WRITE;
@@ -9778,26 +11400,22 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "RENAME to '%s' blocked by '%s' handler", new_path, cmd3->argv[0]);
+      "RENAME to '%s' blocked by '%s' handler", new_path,
+      (char *) cmd3->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9817,19 +11435,15 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9850,20 +11464,15 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9881,24 +11490,19 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
     status_code = fxp_errno2status(xerrno, &reason);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-      "('%s' [%d])", (unsigned long) status_code, reason,
-      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9908,7 +11512,7 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
   }
 
   if (!(flags & SSH2_FXR_OVERWRITE) &&
-      exists(new_path)) {
+      exists2(fxp->pool, new_path)) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "denying RENAME of '%s' to '%s': '%s' already exists and client did not "
       "specify OVERWRITE flag", old_path, new_path, new_path);
@@ -9919,20 +11523,15 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9951,20 +11550,15 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
     pr_response_add_err(R_550, "%s: %s", cmd3->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd3, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd3, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd3);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -9977,9 +11571,9 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
     if (errno != EXDEV) {
       xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %lu, "
-        "GID %lu): error renaming '%s' to '%s': %s", session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
+      (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %s, "
+        "GID %s): error renaming '%s' to '%s': %s", session.user,
+        pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
         old_path, new_path, strerror(xerrno));
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -9993,13 +11587,14 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
        * path to the destination path.
        */
       errno = 0;
-      if (pr_fs_copy_file(old_path, new_path) < 0) {
+      if (pr_fs_copy_file2(old_path, new_path, 0, NULL) < 0) {
         xerrno = errno;
 
-        (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %lu, "
-          "GID %lu): error copying '%s' to '%s': %s", session.user,
-          (unsigned long) session.uid, (unsigned long) session.gid,
-          old_path, new_path, strerror(xerrno));
+        (void) pr_trace_msg("fileperms", 1, "RENAME, user '%s' (UID %s, "
+          "GID %s): error copying '%s' to '%s': %s", session.user,
+          pr_uid2str(fxp->pool, session.uid),
+          pr_gid2str(fxp->pool, session.gid), old_path, new_path,
+          strerror(xerrno));
 
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "error copying '%s' to '%s': %s", old_path, new_path,
@@ -10027,7 +11622,7 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
 
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
     "('%s' [%d])", (unsigned long) status_code, reason,
-    xerrno != EOF ? strerror(errno) : "End of file", xerrno);
+    xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
   /* Clear out any transfer-specific data. */
   if (session.xfer.p) {
@@ -10040,24 +11635,43 @@ static int fxp_handle_rename(struct fxp_packet *fxp) {
    */
 
   session.xfer.p = make_sub_pool(fxp_pool);
+  pr_pool_tag(session.xfer.p, "SFTP session transfer pool");
   memset(&session.xfer.start_time, 0, sizeof(session.xfer.start_time));
   gettimeofday(&session.xfer.start_time, NULL);
 
   session.xfer.path = pstrdup(session.xfer.p, old_path);
 
-  /* XXX Use pr_response_add(R_250) here for success, add_err/R_550 if not */
-  pr_cmd_dispatch_phase(cmd2, xerrno == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd2, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  if (xerrno == 0) {
+    pr_response_add(R_350,
+      "File or directory exists, ready for destination name");
+    fxp_cmd_dispatch(cmd2);
+
+  } else {
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd2->argv[0],
+      strerror(xerrno));
+    fxp_cmd_dispatch_err(cmd2);
+  }
 
   session.xfer.path = pstrdup(session.xfer.p, new_path);
 
-  /* XXX Use pr_response_add(R_250) here for success, add_err/R_550 if not */
-  pr_cmd_dispatch_phase(cmd3, xerrno == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd3, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  if (xerrno == 0) {
+    pr_response_add(R_250, "Rename successful");
+    fxp_cmd_dispatch(cmd3);
+
+  } else {
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd3->argv[0],
+      strerror(xerrno));
+    fxp_cmd_dispatch_err(cmd3);
+  }
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+  if (xerrno == 0) {
+    fxp_cmd_dispatch(cmd);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
-  pr_cmd_dispatch_phase(cmd, xerrno == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd, xerrno == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  } else {
+    fxp_cmd_dispatch_err(cmd);
+  }
 
   /* Clear out any transfer-specific data. */
   if (session.xfer.p) {
@@ -10080,6 +11694,7 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2;
   int have_error = FALSE, res = 0;
+  struct stat st;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
@@ -10105,7 +11720,7 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "RMDIR", path);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -10114,16 +11729,15 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "RMDIR of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "RMDIR of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10133,7 +11747,25 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
   }
 
   /* The path may have been changed by any PRE_CMD handlers. */
-  path = dir_best_path(fxp->pool, cmd->arg);
+  path = cmd->arg;
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(fxp->pool, path, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        path = pstrdup(fxp->pool, link_path);
+      }
+    }
+  }
+
+  path = dir_best_path(fxp->pool, path);
   if (path == NULL) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
@@ -10143,11 +11775,10 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10161,21 +11792,18 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "RMDIR of '%s' blocked by '%s' handler", path, cmd2->argv[0]);
+      "RMDIR of '%s' blocked by '%s' handler", path, (char *) cmd2->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10213,15 +11841,12 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10239,15 +11864,12 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(xerrno));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10260,10 +11882,10 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
   if (res < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "RMDIR, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "RMDIR, user '%s' (UID %s, GID %s): "
       "error removing directory '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, path,
-      strerror(xerrno));
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
+      path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error removing directory '%s': %s", path, strerror(xerrno));
@@ -10314,12 +11936,22 @@ static int fxp_handle_rmdir(struct fxp_packet *fxp) {
     "('%s' [%d])", (unsigned long) status_code, reason,
     errno != EOF ? strerror(errno) : "End of file", errno);
 
-  pr_cmd_dispatch_phase(cmd2, res == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd2, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  if (res == 0) {
+    fxp_cmd_dispatch(cmd2);
+
+  } else {
+    fxp_cmd_dispatch_err(cmd2);
+  }
+
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
+
+  if (res == 0) {
+    fxp_cmd_dispatch(cmd);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
-  pr_cmd_dispatch_phase(cmd, res == 0 ? POST_CMD : POST_CMD_ERR, 0);
-  pr_cmd_dispatch_phase(cmd, res == 0 ? LOG_CMD : LOG_CMD_ERR, 0);
+  } else {
+    fxp_cmd_dispatch_err(cmd);
+  }
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -10337,6 +11969,8 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
   struct stat *attrs;
   struct fxp_packet *resp;
   cmd_rec *cmd;
+  struct stat st;
+  array_header *xattrs = NULL;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
@@ -10348,7 +11982,8 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD_ARG, "%s", path, NULL, NULL);
 
-  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags);
+  attrs = fxp_attrs_read(fxp, &fxp->payload, &fxp->payload_sz, &attr_flags,
+    &xattrs);
   if (attrs == NULL) {
     return 0;
   }
@@ -10370,7 +12005,7 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "SETSTAT", path);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
@@ -10379,16 +12014,15 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "SETSTAT of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "SETSTAT of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10398,7 +12032,25 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
   }
 
   /* The path may have been changed by any PRE_CMD handlers. */
-  path = dir_best_path(fxp->pool, cmd->arg);
+  path = cmd->arg;
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(fxp->pool, path, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        path = pstrdup(fxp->pool, link_path);
+      }
+    }
+  }
+
+  path = dir_best_path(fxp->pool, path);
   if (path == NULL) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
@@ -10408,11 +12060,10 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10435,11 +12086,10 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10462,6 +12112,17 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
     attr_flags &= ~SSH2_FX_ATTR_OWNERGROUP;
   }
 
+  /* If the SFTPOption for ignoring the xattrs for SFTP setstat requests is set,
+   * handle it by clearing the SSH2_FX_ATTR_EXTENDED flag.
+   */
+  if ((sftp_opts & SFTP_OPT_IGNORE_SFTP_SET_XATTRS) &&
+      (attr_flags & SSH2_FX_ATTR_EXTENDED)) {
+    pr_trace_msg(trace_channel, 7,
+      "SFTPOption 'IgnoreSFTPSetExtendedAttributes' configured, ignoring "
+      "xattrs sent by client");
+    attr_flags &= ~SSH2_FX_ATTR_EXTENDED;
+  }
+
   /* If the SFTPOption for ignoring the perms for SFTP setstat requests is set,
    * handle it by clearing the SSH2_FX_ATTR_PERMISSIONS flag.
    */
@@ -10485,10 +12146,10 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
     }
   }
 
-  res = fxp_attrs_set(NULL, path, attrs, attr_flags, &buf, &buflen, fxp);
+  res = fxp_attrs_set(NULL, path, attrs, attr_flags, xattrs, &buf, &buflen,
+    fxp);
   if (res < 0) {
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10502,10 +12163,10 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, reason);
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -10515,16 +12176,17 @@ static int fxp_handle_setstat(struct fxp_packet *fxp) {
 }
 
 static int fxp_handle_stat(struct fxp_packet *fxp) {
-  unsigned char *buf, *ptr;
+  unsigned char *buf;
   char *cmd_name, *path;
-  uint32_t buflen, bufsz;
+  uint32_t attr_flags, buflen;
   struct stat st;
+  struct fxp_buffer *fxb;
   struct fxp_packet *resp;
   cmd_rec *cmd;
   const char *fake_user = NULL, *fake_group = NULL;
 
   path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
-  if (fxp_session->client_version > fxp_utf8_protocol_version) {
+  if (fxp_session->client_version >= fxp_utf8_protocol_version) {
     path = sftp_utf8_decode_str(fxp->pool, path);
   }
 
@@ -10536,11 +12198,6 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
   pr_proctitle_set("%s - %s: STAT %s", session.user, session.proc_prefix, path);
 
   if (fxp_session->client_version > 3) {
-    uint32_t attr_flags;
-
-    /* These are hints from the client about what file attributes are
-     * of particular interest.  We do not currently honor them.
-     */
     attr_flags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
     pr_trace_msg(trace_channel, 7, "received request: STAT %s %s", path,
@@ -10548,6 +12205,11 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
 
   } else {
     pr_trace_msg(trace_channel, 7, "received request: STAT %s", path);
+    attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
+      SSH2_FX_ATTR_ACMODTIME;
+#ifdef PR_USE_XATTR
+    attr_flags |= SSH2_FX_ATTR_EXTENDED;
+#endif /* PR_USE_XATTR */
   }
 
   if (strlen(path) == 0) {
@@ -10559,35 +12221,53 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
   }
 
   cmd = fxp_cmd_alloc(fxp->pool, "STAT", path);
-  cmd->cmd_class = CL_READ;
+  cmd->cmd_class = CL_READ|CL_SFTP;
 
-  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
-  buf = ptr = palloc(fxp->pool, bufsz);
+  fxb = pcalloc(fxp->pool, sizeof(struct fxp_buffer));
+  fxb->bufsz = buflen = FXP_RESPONSE_NAME_DEFAULT_SZ;
+  fxb->ptr = buf = palloc(fxp->pool, fxb->bufsz);
 
   if (pr_cmd_dispatch_phase(cmd, PRE_CMD, 0) < 0) {
     uint32_t status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "STAT of '%s' blocked by '%s' handler", path, cmd->argv[0]);
+      "STAT of '%s' blocked by '%s' handler", path, (char *) cmd->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
 
   /* The path may have been changed by any PRE_CMD handlers. */
-  path = dir_best_path(fxp->pool, cmd->arg);
+  path = cmd->arg;
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(fxp->pool, path, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        path = pstrdup(fxp->pool, link_path);
+      }
+    }
+  }
+
+  path = dir_best_path(fxp->pool, path);
   if (path == NULL) {
     int xerrno = EACCES;
     const char *reason;
@@ -10602,14 +12282,14 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
        xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -10628,21 +12308,20 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
   pr_cmd_set_name(cmd, cmd_name);
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   if (pr_fsio_stat(path, &st) < 0) {
     uint32_t status_code;
     const char *reason;
@@ -10659,15 +12338,14 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
+    resp->payload = fxb->ptr;
+    resp->payload_sz = (fxb->bufsz - buflen);
 
     return fxp_packet_write(resp);
   }
@@ -10692,41 +12370,57 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
     fake_group = session.group;
   }
 
-  fxp_attrs_write(fxp->pool, &buf, &buflen, &st, fake_user, fake_group);
+  fxb->buf = buf;
+  fxb->buflen = buflen;
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_attrs_write(fxp->pool, fxb, path, &st, attr_flags, fake_user, fake_group);
+
+  buf = fxb->buf;
+  buflen = fxb->buflen;
+
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-  resp->payload = ptr;
-  resp->payload_sz = (bufsz - buflen);
+  resp->payload = fxb->ptr;
+  resp->payload_sz = (fxb->bufsz - buflen);
 
   return fxp_packet_write(resp);
 }
 
 static int fxp_handle_symlink(struct fxp_packet *fxp) {
   unsigned char *buf, *ptr;
-  char *args, *args2, *cmd_name, *src_path, *dst_path, *vpath;
+  char *args, *args2, *cmd_name, *link_path, *link_vpath, *target_path,
+    *target_vpath, *vpath;
   const char *reason;
   int have_error = FALSE, res;
   uint32_t buflen, bufsz, status_code;
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2;
 
-  src_path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+  /* Note: The ietf-secsh-filexfer drafts define the arguments for SYMLINK
+   * as "linkpath" (the file being created), followed by "targetpath" (the
+   * target of the link).  The following code reads the arguments in the
+   * opposite (thus wrong) order.  This is done deliberately, to match
+   * the behavior that OpenSSH uses; see:
+   *
+   *  https://bugzilla.mindrot.org/show_bug.cgi?id=861
+   */
+
+  target_path = sftp_msg_read_string(fxp->pool, &fxp->payload,
+    &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-    src_path = sftp_utf8_decode_str(fxp->pool, src_path);
+    target_path = sftp_utf8_decode_str(fxp->pool, target_path);
   }
 
-  dst_path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
+  link_path = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   if (fxp_session->client_version >= fxp_utf8_protocol_version) {
-    dst_path = sftp_utf8_decode_str(fxp->pool, dst_path);
+    link_path = sftp_utf8_decode_str(fxp->pool, link_path);
   }
 
-  args = pstrcat(fxp->pool, src_path, " ", dst_path, NULL);
+  args = pstrcat(fxp->pool, target_path, " ", link_path, NULL);
 
   cmd = fxp_cmd_alloc(fxp->pool, "SYMLINK", args);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "SYMLINK", NULL, NULL);
@@ -10734,37 +12428,37 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
     PR_SCORE_CMD_ARG, "%s", args, NULL, NULL);
 
   pr_proctitle_set("%s - %s: SYMLINK %s %s", session.user, session.proc_prefix,
-    src_path, dst_path);
+    link_path, target_path);
 
-  pr_trace_msg(trace_channel, 7, "received request: SYMLINK %s %s", src_path,
-    dst_path);
+  pr_trace_msg(trace_channel, 7, "received request: SYMLINK %s %s", target_path,
+    link_path);
 
-  if (strlen(src_path) == 0) {
+  if (strlen(target_path) == 0) {
     /* Use the default directory if the path is empty. */
-    src_path = sftp_auth_get_default_dir();
+    target_path = sftp_auth_get_default_dir();
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "empty link path given in SYMLINK request, using '%s'", src_path);
+      "empty target path given in SYMLINK request, using '%s'", target_path);
   }
 
-  if (strlen(dst_path) == 0) {
+  if (strlen(link_path) == 0) {
     /* Use the default directory if the path is empty. */
-    dst_path = sftp_auth_get_default_dir();
+    link_path = sftp_auth_get_default_dir();
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "empty target path given in SYMLINK request, using '%s'", dst_path);
+      "empty link path given in SYMLINK request, using '%s'", link_path);
   }
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
 
   /* Make sure we use the full paths. */
-  vpath = dir_canonical_vpath(fxp->pool, src_path);
+  vpath = dir_canonical_vpath(fxp->pool, target_path);
   if (vpath == NULL) {
     int xerrno = errno;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error resolving '%s': %s", src_path, strerror(xerrno));
+      "error resolving '%s': %s", target_path, strerror(xerrno));
 
     status_code = fxp_errno2status(xerrno, &reason);
 
@@ -10772,11 +12466,10 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10784,14 +12477,14 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
 
     return fxp_packet_write(resp);
   }
-  src_path = vpath;
+  target_vpath = vpath;
 
-  vpath = dir_canonical_vpath(fxp->pool, dst_path);
+  vpath = dir_canonical_vpath(fxp->pool, link_path);
   if (vpath == NULL) {
     int xerrno = errno;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error resolving '%s': %s", dst_path, strerror(xerrno));
+      "error resolving '%s': %s", link_path, strerror(xerrno));
 
     status_code = fxp_errno2status(xerrno, &reason);
 
@@ -10799,11 +12492,10 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10811,7 +12503,7 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
 
     return fxp_packet_write(resp);
   }
-  dst_path = vpath;
+  link_vpath = vpath;
 
   /* We use a slightly different cmd_rec here, for the benefit of PRE_CMD
    * handlers such as mod_rewrite.  It is impossible for a client to
@@ -10823,7 +12515,7 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
    * paths.
    */
 
-  args2 = pstrcat(fxp->pool, src_path, "\t", dst_path, NULL);
+  args2 = pstrcat(fxp->pool, target_vpath, "\t", link_vpath, NULL);
   cmd2 = fxp_cmd_alloc(fxp->pool, "SYMLINK", args2);
   cmd2->cmd_class = CL_WRITE;
 
@@ -10831,22 +12523,19 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
     status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "SYMLINK of '%s' to '%s' blocked by '%s' handler", src_path, dst_path,
-      cmd2->argv[0]);
+      "SYMLINK of '%s' to '%s' blocked by '%s' handler", target_path, link_path,
+      (char *) cmd2->argv[0]);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
     pr_response_add_err(R_550, "%s: %s", cmd2->arg, strerror(EACCES));
-    pr_cmd_dispatch_phase(cmd2, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd2, LOG_CMD_ERR, 0);
-    pr_response_clear(&resp_err_list);
+    fxp_cmd_dispatch_err(cmd2);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10862,21 +12551,21 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
     ptr2 = strchr(cmd2->arg, '\t');
     if (ptr2) {
       *ptr2 = '\0';
-      src_path = cmd2->arg;
-      dst_path = ptr2 + 1;
+      target_path = cmd2->arg;
+      link_path = ptr2 + 1;
     }
   }
 
   cmd_name = cmd->argv[0];
   pr_cmd_set_name(cmd, "SYMLINK");
 
-  if (!dir_check(fxp->pool, cmd, G_READ, src_path, NULL)) {
+  if (!dir_check(fxp->pool, cmd, G_READ, target_vpath, NULL)) {
     pr_cmd_set_name(cmd, cmd_name);
     have_error = TRUE;
   }
 
   if (!have_error &&
-      !dir_check(fxp->pool, cmd, G_WRITE, dst_path, NULL)) {
+      !dir_check(fxp->pool, cmd, G_WRITE, link_vpath, NULL)) {
     pr_cmd_set_name(cmd, cmd_name);
     have_error = TRUE;
   }
@@ -10888,16 +12577,15 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "SYMLINK of '%s' to '%s' blocked by <Limit> configuration",
-      src_path, dst_path);
+      target_path, link_path);
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -10906,13 +12594,13 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
-  res = pr_fsio_symlink(src_path, dst_path);
+  res = pr_fsio_symlink(target_path, link_path);
 
   if (res < 0) {
     int xerrno = errno;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error symlinking '%s' to '%s': %s", src_path, dst_path,
+      "error symlinking '%s' to '%s': %s", target_path, link_path,
       strerror(xerrno));
 
     status_code = fxp_errno2status(xerrno, &reason);
@@ -10921,8 +12609,7 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
   } else {
     errno = 0;
@@ -10931,11 +12618,11 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, reason);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+    fxp_cmd_dispatch(cmd);
   }
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason, NULL);
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+    reason, NULL);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -10946,14 +12633,14 @@ static int fxp_handle_symlink(struct fxp_packet *fxp) {
 
 static int fxp_handle_write(struct fxp_packet *fxp) {
   unsigned char *buf, *data, *ptr;
-  char cmd_arg[256], *cmd_name, *name;
-  int res;
+  char cmd_arg[256], *file, *name, *ptr2;
+  int res, xerrno = 0;
   uint32_t buflen, bufsz, datalen, status_code;
   uint64_t offset;
-  struct stat st;
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
   cmd_rec *cmd, *cmd2;
+  pr_buffer_t *pbuf;
 
   name = sftp_msg_read_string(fxp->pool, &fxp->payload, &fxp->payload_sz);
   offset = sftp_msg_read_long(fxp->pool, &fxp->payload, &fxp->payload_sz);
@@ -10961,14 +12648,11 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
   data = sftp_msg_read_data(fxp->pool, &fxp->payload, &fxp->payload_sz,
     datalen);
 
-  session.xfer.total_bytes += datalen;
-  session.total_bytes += datalen;
- 
   memset(cmd_arg, '\0', sizeof(cmd_arg)); 
   snprintf(cmd_arg, sizeof(cmd_arg)-1, "%s %" PR_LU " %lu", name,
     (pr_off_t) offset, (unsigned long) datalen);
   cmd = fxp_cmd_alloc(fxp->pool, "WRITE", cmd_arg);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "WRITE", NULL, NULL);
@@ -10987,19 +12671,18 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11014,11 +12697,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11034,32 +12716,6 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     PR_SCORE_CMD_ARG, "%s", fxh->fh->fh_path, NULL, NULL);
   fxh->fh_bytes_xferred += datalen;
 
-  if (pr_fsio_fstat(fxh->fh, &st) < 0) {
-    const char *reason;
-    int xerrno = errno;
-
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error checking '%s' for WRITE: %s", fxh->fh->fh_path, strerror(xerrno));
-
-    status_code = fxp_errno2status(xerrno, &reason);
-
-    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
-      "('%s' [%d])", (unsigned long) status_code, reason,
-      xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
-
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
-
-    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
-    resp->payload = ptr;
-    resp->payload_sz = (bufsz - buflen);
-  
-    return fxp_packet_write(resp);
-  }
-
   /* It would be nice to check the requested offset against the size of
    * the file.  However, the protocol specifically allows for sparse files,
    * where the requested offset is far beyond the end of the file.
@@ -11081,10 +12737,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason, strerror(EINVAL),
       EINVAL);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
-  
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11094,13 +12750,21 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
   }
 #endif
 
-  cmd_name = cmd->argv[0];
-  pr_cmd_set_name(cmd, C_STOR);
+  /* Trim the full path to just the filename, for our STOR command. */
+  ptr2 = strrchr(fxh->fh->fh_path, '/');
+  if (ptr2 != NULL &&
+      ptr2 != fxh->fh->fh_path) {
+    file = pstrdup(fxp->pool, ptr2 + 1);
 
-  if (!dir_check(fxp->pool, cmd, G_WRITE, fxh->fh->fh_path, NULL)) {
-    status_code = SSH2_FX_PERMISSION_DENIED;
+  } else {
+    file = fxh->fh->fh_path;
+  }
 
-    pr_cmd_set_name(cmd, cmd_name);
+  cmd2 = fxp_cmd_alloc(fxp->pool, C_STOR, file);
+  cmd2->cmd_class = CL_WRITE|CL_SFTP;
+
+  if (!dir_check(fxp->pool, cmd2, G_WRITE, fxh->fh->fh_path, NULL)) {
+    status_code = SSH2_FX_PERMISSION_DENIED;
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "WRITE of '%s' blocked by <Limit> configuration", fxh->fh->fh_path);
@@ -11108,11 +12772,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11120,7 +12783,6 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
 
     return fxp_packet_write(resp);
   }
-  pr_cmd_set_name(cmd, cmd_name);
 
   if (fxp_path_pass_regex_filters(fxp->pool, "WRITE", fxh->fh->fh_path) < 0) {
     status_code = fxp_errno2status(errno, NULL);
@@ -11128,11 +12790,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11141,10 +12802,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
-  if (S_ISREG(st.st_mode)) {
+  if (S_ISREG(fxh->fh_st->st_mode)) {
     if (pr_fsio_lseek(fxh->fh, offset, SEEK_SET) < 0) {
       const char *reason;
-      int xerrno = errno;
+      xerrno = errno;
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error seeking to offset (%" PR_LU " bytes) for '%s': %s",
@@ -11156,17 +12817,25 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
         "('%s' [%d])", (unsigned long) status_code, reason,
         xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-      fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-        NULL);
-  
-      pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-      pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+      fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+        reason, NULL);
+
+      fxp_cmd_dispatch_err(cmd);
 
       resp = fxp_packet_create(fxp->pool, fxp->channel_id);
       resp->payload = ptr;
       resp->payload_sz = (bufsz - buflen);
   
       return fxp_packet_write(resp);
+
+    } else {
+      off_t *file_offset;
+
+      /* Stash the offset at which we're writing to this file. */
+      file_offset = palloc(cmd->pool, sizeof(off_t));
+      *file_offset = (off_t) offset;
+      (void) pr_table_add(cmd->notes, "mod_xfer.file-offset", file_offset,
+        sizeof(off_t));
     }
   }
 
@@ -11180,9 +12849,39 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     cmd2 = fxp_cmd_alloc(fxp->pool, C_APPE, NULL);
   }
 
+  pbuf = pcalloc(fxp->pool, sizeof(pr_buffer_t));
+  pbuf->buf = (char *) data;
+  pbuf->buflen = datalen;
+  pbuf->current = pbuf->buf;
+  pbuf->remaining = 0;
+  pr_event_generate("mod_sftp.sftp.data-read", pbuf);
+
   pr_throttle_init(cmd2);
   
   res = pr_fsio_write(fxh->fh, (char *) data, datalen);
+  xerrno = errno;
+
+  /* Increment the "on-disk" file size with the number of bytes written.
+   * We do this, rather than using fstat(2), to avoid performance penalties
+   * associated with fstat(2) on network filesystems such as NFS.  And we
+   * want to track the on-disk size for enforcing limits such as
+   * MaxStoreFileSize.
+   *
+   * Note that we only want to increment the file size if the chunk we
+   * just wrote is PAST the current end of the file; we could be just
+   * overwriting a chunk of the file.
+   */
+  if (res > 0) {
+    size_t new_size;
+
+    new_size = offset + res;
+    if ((off_t) new_size > fxh->fh_st->st_size) {
+      fxh->fh_st->st_size = new_size;
+    }
+
+    session.xfer.total_bytes += datalen;
+    session.total_bytes += datalen;
+  }
 
   if (pr_data_get_timeout(PR_DATA_TIMEOUT_NO_TRANSFER) > 0) {
     pr_timer_reset(PR_TIMER_NOXFER, ANY_MODULE);
@@ -11196,11 +12895,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
 
   if (res < 0) {
     const char *reason;
-    int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "WRITE, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "WRITE, user '%s' (UID %s, GID %s): "
       "error writing to '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
+      pr_uid2str(fxp->pool, session.uid), pr_gid2str(fxp->pool, session.gid),
       fxh->fh->fh_path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -11212,11 +12910,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11225,7 +12922,7 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     return fxp_packet_write(resp);
   }
 
-  if (pr_fsio_fstat(fxh->fh, &st) == 0) {
+  if (fxh->fh_st->st_size > 0) {
     config_rec *c;
     off_t nbytes_max_store = 0;
 
@@ -11237,14 +12934,14 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
     }
 
     if (nbytes_max_store > 0) {
-      if (st.st_size > nbytes_max_store) {
+      if (fxh->fh_st->st_size > nbytes_max_store) {
         const char *reason;
 #if defined(EFBIG)
-        int xerrno = EFBIG;
+        xerrno = EFBIG;
 #elif defined(ENOSPC)
-        int xerrno = ENOSPC;
+        xerrno = ENOSPC;
 #else
-        int xerno = EIO;
+        xerrno = EIO;
 #endif
 
         pr_log_pri(PR_LOG_NOTICE, "MaxStoreFileSize (%" PR_LU " %s) reached: "
@@ -11260,13 +12957,12 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
 
         pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
           "('%s' [%d])", (unsigned long) status_code, reason,
-          xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
+          strerror(xerrno), xerrno);
 
-        fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-          NULL);
+        fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+          reason, NULL);
 
-        pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-        pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+        fxp_cmd_dispatch_err(cmd);
 
         resp = fxp_packet_create(fxp->pool, fxp->channel_id);
         resp->payload = ptr;
@@ -11282,11 +12978,10 @@ static int fxp_handle_write(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, fxp_strerror(status_code));
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
     fxp_strerror(status_code), NULL);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -11311,7 +13006,7 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
   lock_flags = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
   cmd = fxp_cmd_alloc(fxp->pool, "UNLOCK", name);
-  cmd->cmd_class = CL_WRITE;
+  cmd->cmd_class = CL_WRITE|CL_SFTP;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_CMD, "%s", "UNLOCK", NULL, NULL);
@@ -11331,19 +13026,18 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
   fxh = fxp_handle_get(name);
   if (fxh == NULL) {
     pr_trace_msg(trace_channel, 17,
-      "%s: unable to find handle for name '%s': %s", cmd->argv[0], name,
-      strerror(errno));
+      "%s: unable to find handle for name '%s': %s", (char *) cmd->argv[0],
+      name, strerror(errno));
 
     status_code = SSH2_FX_INVALID_HANDLE;
 
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11362,11 +13056,10 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11389,11 +13082,10 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11419,11 +13111,10 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
       (unsigned long) status_code, fxp_strerror(status_code));
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
       fxp_strerror(status_code), NULL);
-  
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11468,11 +13159,10 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
       "('%s' [%d])", (unsigned long) status_code, reason,
       xerrno != EOF ? strerror(xerrno) : "End of file", xerrno);
 
-    fxp_status_write(&buf, &buflen, fxp->request_id, status_code, reason,
-      NULL);
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
 
-    pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+    fxp_cmd_dispatch_err(cmd);
 
     resp = fxp_packet_create(fxp->pool, fxp->channel_id);
     resp->payload = ptr;
@@ -11488,11 +13178,10 @@ static int fxp_handle_unlock(struct fxp_packet *fxp) {
   pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s'",
     (unsigned long) status_code, fxp_strerror(status_code));
 
-  fxp_status_write(&buf, &buflen, fxp->request_id, status_code,
+  fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
     fxp_strerror(status_code), NULL);
 
-  pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
-  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  fxp_cmd_dispatch(cmd);
 
   resp = fxp_packet_create(fxp->pool, fxp->channel_id);
   resp->payload = ptr;
@@ -11589,12 +13278,22 @@ int sftp_fxp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
         (unsigned long) channel_id);
     }
 
+    if (fxp->packet_len > FXP_MAX_PACKET_LEN) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "received excessive SFTP packet (len %lu > max %lu bytes), rejecting",
+        (unsigned long) fxp->packet_len, (unsigned long) FXP_MAX_PACKET_LEN);
+      destroy_pool(fxp->pool);
+      errno = EPERM;
+      return -1;
+    }
+
     fxp_session = fxp_get_session(channel_id);
     if (fxp_session == NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "no existing SFTP session for channel ID %lu, rejecting request",
         (unsigned long) channel_id);
       destroy_pool(fxp->pool);
+      errno = EPERM;
       return -1;
     }
 
@@ -11741,11 +13440,13 @@ int sftp_fxp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "unhandled SFTP request type %d", fxp->request_type);
         destroy_pool(fxp->pool);
+        fxp_packet_set_packet(NULL);
         fxp_session = NULL;
         return -1;
     }
 
     destroy_pool(fxp->pool);
+    fxp_packet_set_packet(NULL);
 
     if (res < 0) {
       fxp_session = NULL;
@@ -11871,6 +13572,10 @@ int sftp_fxp_open_session(uint32_t channel_id) {
   (void) fxp_send_display_login_file(channel_id);
 
   pr_session_set_protocol("sftp");
+
+  /* Clear any ASCII flags (set by default for FTP sessions. */
+  session.sf_flags &= ~SF_ASCII;
+
   return 0;
 }
 
diff --git a/contrib/mod_sftp/fxp.h b/contrib/mod_sftp/fxp.h
index 69e89ed..54e14dc 100644
--- a/contrib/mod_sftp/fxp.h
+++ b/contrib/mod_sftp/fxp.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp SFTP
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: fxp.h,v 1.12 2013-04-15 16:14:25 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -74,9 +72,11 @@
 #define SFTP_FXP_EXT_VENDOR_ID		0x0020
 #define SFTP_FXP_EXT_SPACE_AVAIL	0x0040
 #define SFTP_FXP_EXT_FSYNC		0x0080
+#define SFTP_FXP_EXT_HARDLINK		0x0100
+#define SFTP_FXP_EXT_XATTR		0x0200
 
 #define SFTP_FXP_EXT_DEFAULT \
-  (SFTP_FXP_EXT_CHECK_FILE|SFTP_FXP_EXT_COPY_FILE|SFTP_FXP_EXT_VERSION_SELECT|SFTP_FXP_EXT_POSIX_RENAME|SFTP_FXP_EXT_SPACE_AVAIL|SFTP_FXP_EXT_STATVFS|SFTP_FXP_EXT_FSYNC)
+  (SFTP_FXP_EXT_CHECK_FILE|SFTP_FXP_EXT_COPY_FILE|SFTP_FXP_EXT_VERSION_SELECT|SFTP_FXP_EXT_POSIX_RENAME|SFTP_FXP_EXT_SPACE_AVAIL|SFTP_FXP_EXT_STATVFS|SFTP_FXP_EXT_FSYNC|SFTP_FXP_EXT_HARDLINK)
 
 int sftp_fxp_handle_packet(pool *, void *, uint32_t, unsigned char *, uint32_t);
 
@@ -98,4 +98,4 @@ int sftp_fxp_set_utf8_protocol_version(unsigned int);
 
 void sftp_fxp_use_gmt(int);
 
-#endif
+#endif /* MOD_SFTP_FXP_H */
diff --git a/contrib/mod_sftp/interop.c b/contrib/mod_sftp/interop.c
index fbb736c..2d5c536 100644
--- a/contrib/mod_sftp/interop.c
+++ b/contrib/mod_sftp/interop.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp interoperability
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: interop.c,v 1.16 2013-03-14 21:49:19 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -36,7 +34,7 @@ extern module sftp_module;
 /* By default, each client is assumed to support all of the features in
  * which we are interested.
  */
-static unsigned int interop_flags =
+static unsigned int default_flags =
   SFTP_SSH2_FEAT_IGNORE_MSG |
   SFTP_SSH2_FEAT_MAC_LEN |
   SFTP_SSH2_FEAT_CIPHER_USE_K |
@@ -45,11 +43,12 @@ static unsigned int interop_flags =
   SFTP_SSH2_FEAT_HAVE_PUBKEY_ALGO |
   SFTP_SSH2_FEAT_SERVICE_IN_HOST_SIG |
   SFTP_SSH2_FEAT_SERVICE_IN_PUBKEY_SIG |
-  SFTP_SSH2_FEAT_HAVE_PUBKEY_ALGO_IN_DSA_SIG;
+  SFTP_SSH2_FEAT_HAVE_PUBKEY_ALGO_IN_DSA_SIG |
+  SFTP_SSH2_FEAT_NO_DATA_WHILE_REKEYING;
 
 struct sftp_version_pattern {
   const char *pattern;
-  int interop_flags;
+  int disabled_flags;
   pr_regex_t *pre;
 };
 
@@ -72,6 +71,8 @@ static struct sftp_version_pattern known_versions[] = {
 
   { "^OpenSSH.*",		0,					NULL },
 
+  { ".*J2SSH_Maverick.*",	SFTP_SSH2_FEAT_REKEYING,		NULL },
+
   { ".*MindTerm.*",		0,					NULL },
 
   { "^Sun_SSH_1\\.0.*",		SFTP_SSH2_FEAT_REKEYING,		NULL },
@@ -116,6 +117,10 @@ static struct sftp_version_pattern known_versions[] = {
     "^1\\.3\\.2.*|"		
     "^3\\.2\\.9.*",		SFTP_SSH2_FEAT_IGNORE_MSG,		NULL },
 
+  { ".*PuTTY.*|"
+    ".*PUTTY.*|"
+    ".*WinSCP.*",		SFTP_SSH2_FEAT_NO_DATA_WHILE_REKEYING,	NULL },
+
   { ".*SSH_Version_Mapper.*",	SFTP_SSH2_FEAT_SCANNER,			NULL },
 
   { "^Probe-.*", 		SFTP_SSH2_FEAT_PROBE,			NULL },
@@ -125,10 +130,11 @@ static struct sftp_version_pattern known_versions[] = {
 
 static const char *trace_channel = "ssh2";
 
-int sftp_interop_handle_version(const char *client_version) {
+int sftp_interop_handle_version(pool *p, const char *client_version) {
   register unsigned int i;
   size_t version_len;
   const char *version = NULL;
+  char *ptr = NULL;
   int is_probe = FALSE, is_scan = FALSE;
   config_rec *c;
 
@@ -165,10 +171,28 @@ int sftp_interop_handle_version(const char *client_version) {
    * client info.
    */
   if (strncmp(client_version, "SSH-2.0-", 8) == 0) {
-    version = client_version + 8;
+    version = pstrdup(p, client_version + 8);
 
   } else if (strncmp(client_version, "SSH-1.99-", 9) == 0) {
-    version = client_version + 9;
+    version = pstrdup(p, client_version + 9);
+
+  } else {
+    /* An illegally formatted client version.  How did it get here? */
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "client-sent version (%s) is illegally formmated, disconnecting client",
+      client_version);
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED,
+      NULL);
+  }
+
+  /* Look for the optional comments field in the received client version; if
+   * present, trim it out, so that we do not try to match on it.
+   */
+  ptr = strchr(version, ' ');
+  if (ptr != NULL) {
+    pr_trace_msg(trace_channel, 11, "read client version with comments: '%s'",
+      version);
+    *ptr = '\0';
   }
 
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -193,13 +217,13 @@ int sftp_interop_handle_version(const char *client_version) {
         known_versions[i].pattern);
 
       /* We have a match. */
-      interop_flags &= ~(known_versions[i].interop_flags);
+      default_flags &= ~(known_versions[i].disabled_flags);
 
-      if (known_versions[i].interop_flags == SFTP_SSH2_FEAT_PROBE) {
+      if (known_versions[i].disabled_flags == SFTP_SSH2_FEAT_PROBE) {
         is_probe = TRUE;
       }
 
-      if (known_versions[i].interop_flags == SFTP_SSH2_FEAT_SCANNER) {
+      if (known_versions[i].disabled_flags == SFTP_SSH2_FEAT_SCANNER) {
         is_scan = TRUE;
       }
 
@@ -255,7 +279,7 @@ int sftp_interop_handle_version(const char *client_version) {
     res = pr_regexp_exec(pre, version, 0, NULL, 0, 0, 0);
     if (res == 0) {
       pr_table_t *tab;
-      void *v, *v2;
+      const void *v, *v2;
 
       /* We have a match. */
 
@@ -272,7 +296,7 @@ int sftp_interop_handle_version(const char *client_version) {
        */
 
       v = pr_table_get(tab, "channelWindowSize", NULL);
-      if (v) {
+      if (v != NULL) {
         uint32_t window_size;
 
         window_size = *((uint32_t *) v);
@@ -285,7 +309,7 @@ int sftp_interop_handle_version(const char *client_version) {
       }
       
       v = pr_table_get(tab, "channelPacketSize", NULL);
-      if (v) {
+      if (v != NULL) {
         uint32_t packet_size;
 
         packet_size = *((uint32_t *) v);
@@ -298,7 +322,7 @@ int sftp_interop_handle_version(const char *client_version) {
       }
 
       v = pr_table_get(tab, "pessimisticNewkeys", NULL);
-      if (v) {
+      if (v != NULL) {
         int pessimistic_newkeys;
 
         pessimistic_newkeys = *((int *) v);
@@ -308,13 +332,14 @@ int sftp_interop_handle_version(const char *client_version) {
           pessimistic_newkeys ? "true" : "false");
 
         if (pessimistic_newkeys) {
-          interop_flags |= SFTP_SSH2_FEAT_PESSIMISTIC_NEWKEYS;
+          default_flags |= SFTP_SSH2_FEAT_PESSIMISTIC_NEWKEYS;
         } 
       }
 
       v = pr_table_get(tab, "sftpMinProtocolVersion", NULL);
       v2 = pr_table_get(tab, "sftpMaxProtocolVersion", NULL);
-      if (v && v2) {
+      if (v != NULL &&
+          v2 != NULL) {
         unsigned int min_version, max_version;
 
         min_version = *((unsigned int *) v);
@@ -335,7 +360,7 @@ int sftp_interop_handle_version(const char *client_version) {
 
 #ifdef PR_USE_NLS
       v = pr_table_get(tab, "sftpUTF8ProtocolVersion", NULL);
-      if (v) {
+      if (v != NULL) {
         unsigned int protocol_version;
 
         protocol_version = *((unsigned int *) v);
@@ -371,8 +396,9 @@ int sftp_interop_supports_feature(int feat_flag) {
       return FALSE;
 
     default:
-      if (!(interop_flags & feat_flag))
+      if (!(default_flags & feat_flag)) {
         return FALSE;
+      }
   }
 
   return TRUE;
diff --git a/contrib/mod_sftp/interop.h b/contrib/mod_sftp/interop.h
index 5bfbad3..6af3f4b 100644
--- a/contrib/mod_sftp/interop.h
+++ b/contrib/mod_sftp/interop.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp interoperability
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: interop.h,v 1.6 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_INTEROP_H
 #define MOD_SFTP_INTEROP_H
 
+#include "mod_sftp.h"
+
 /* For clients which do not support IGNORE packets */
 #define SFTP_SSH2_FEAT_IGNORE_MSG			0x0001
 
@@ -76,6 +74,11 @@
  */
 #define SFTP_SSH2_FEAT_PESSIMISTIC_NEWKEYS		0x0200
 
+/* For clients which cannot/do not tolerate non-kex related packets after a
+ * server has requested rekeying.
+ */
+#define SFTP_SSH2_FEAT_NO_DATA_WHILE_REKEYING		0x0400
+
 /* For scanners. */
 #define SFTP_SSH2_FEAT_SCANNER				0xfffe
 
@@ -85,7 +88,7 @@
 /* Compares the given client version string against a table of known client
  * client versions and their interoperability/compatibility issues.
  */
-int sftp_interop_handle_version(const char *);
+int sftp_interop_handle_version(pool *, const char *);
 
 /* Returns TRUE if the client supports the requested feature, FALSE
  * otherwise.
@@ -95,4 +98,4 @@ int sftp_interop_supports_feature(int);
 int sftp_interop_init(void);
 int sftp_interop_free(void);
 
-#endif
+#endif /* MOD_SFTP_INTEROP_H */
diff --git a/contrib/mod_sftp/kbdint.h b/contrib/mod_sftp/kbdint.h
index 1d8a1b0..37e9061 100644
--- a/contrib/mod_sftp/kbdint.h
+++ b/contrib/mod_sftp/kbdint.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp keyboard-interactive API
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: kbdint.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_KBDINT_H
 #define MOD_SFTP_KBDINT_H
 
+#include "mod_sftp.h"
+
 /* Returns the registered driver by name, or NULL if no such driver has
  * been registered.
  */
@@ -45,4 +43,4 @@ sftp_kbdint_driver_t *sftp_kbdint_first_driver(void);
  */
 sftp_kbdint_driver_t *sftp_kbdint_next_driver(void);
 
-#endif
+#endif /* MOD_SFTP_KBDINT_H */
diff --git a/contrib/mod_sftp/kex.c b/contrib/mod_sftp/kex.c
index 79d4ac8..01e9e28 100644
--- a/contrib/mod_sftp/kex.c
+++ b/contrib/mod_sftp/kex.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp key exchange (kex)
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -36,6 +36,17 @@
 #include "disconnect.h"
 #include "interop.h"
 #include "tap.h"
+#include "misc.h"
+
+#ifdef PR_USE_SODIUM
+# include <sodium.h>
+# define CURVE25519_SIZE	32
+#endif /* PR_USE_SODIUM */
+
+/* Define the minimum DH group length we allow (unless the AllowWeakDH
+ * SFTPOption is used).
+ */
+#define SFTP_DH_MIN_LEN			2048
 
 extern pr_response_t *resp_list, *resp_err_list;
 extern module sftp_module;
@@ -62,6 +73,8 @@ struct sftp_kex_names {
 };
 
 struct sftp_kex {
+  pool *pool;
+
   /* Versions */
   const char *client_version;
   const char *server_version;
@@ -101,6 +114,9 @@ struct sftp_kex {
   /* Using ECDH? */
   int use_ecdh;
 
+  /* Using Curve25519? */
+  int use_curve25519;
+
   /* For generating the session ID */
   DH *dh;
   BIGNUM *e;
@@ -118,6 +134,9 @@ struct sftp_kex {
   EC_KEY *ec;
   EC_POINT *client_point;
 #endif /* PR_USE_OPENSSL_ECC */
+#if defined(PR_USE_SODIUM) && defined(HAVE_SHA256_OPENSSL)
+  unsigned char *client_curve25519;
+#endif /* PR_USE_SODIUM and HAVE_SHA256_OPENSSL */
 };
 
 static struct sftp_kex *kex_first_kex = NULL;
@@ -142,6 +161,76 @@ static const char *dh_group14_str =
   "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718"
   "3995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF";
 
+static const char *dh_group16_str =
+  "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
+  "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
+  "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
+  "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
+  "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
+  "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
+  "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
+  "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
+  "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
+  "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
+  "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
+  "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
+  "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
+  "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
+  "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
+  "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7"
+  "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA"
+  "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6"
+  "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED"
+  "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9"
+  "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199"
+  "FFFFFFFFFFFFFFFF";
+
+static const char *dh_group18_str =
+  "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
+  "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
+  "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
+  "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
+  "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
+  "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
+  "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
+  "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
+  "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
+  "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
+  "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
+  "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
+  "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
+  "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
+  "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
+  "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7"
+  "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA"
+  "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6"
+  "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED"
+  "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9"
+  "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492"
+  "36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BD"
+  "F8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831"
+  "179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B"
+  "DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF"
+  "5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6"
+  "D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F3"
+  "23A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA"
+  "CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE328"
+  "06A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55C"
+  "DA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE"
+  "12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E4"
+  "38777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300"
+  "741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F568"
+  "3423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9"
+  "22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B"
+  "4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A"
+  "062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A36"
+  "4597E899A0255DC164F31CC50846851DF9AB48195DED7EA1"
+  "B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F92"
+  "4009438B481C6CD7889A002ED5EE382BC9190DA6FC026E47"
+  "9558E4475677E9AA9E3050E2765694DFC81F56E880B96E71"
+  "60C980DD98EDD3DFFFFFFFFFFFFFFFFF";
+
+
 #define SFTP_DH_GROUP1_SHA1		1
 #define SFTP_DH_GROUP14_SHA1		2
 #define SFTP_DH_GEX_SHA1		3
@@ -151,6 +240,9 @@ static const char *dh_group14_str =
 #define SFTP_ECDH_SHA256		7
 #define SFTP_ECDH_SHA384		8
 #define SFTP_ECDH_SHA512		9
+#define SFTP_DH_GROUP14_SHA256		10
+#define SFTP_DH_GROUP16_SHA512		11
+#define SFTP_DH_GROUP18_SHA512		12
 
 #define SFTP_KEXRSA_SHA1_SIZE		2048
 #define SFTP_KEXRSA_SHA256_SIZE		3072
@@ -172,21 +264,28 @@ static const char *trace_channel = "ssh2";
 
 static int kex_rekey_timeout_cb(CALLBACK_FRAME) {
   pr_trace_msg(trace_channel, 5,
-    "Failed to rekey before timeout, disconnecting client");
+    "Failed to rekey before %d %s timeout, disconnecting client",
+    kex_rekey_timeout, kex_rekey_timeout != 1 ? "seconds" : "second");
+  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+    "Failed to rekey before %d %s timeout, disconnecting client",
+    kex_rekey_timeout, kex_rekey_timeout != 1 ? "seconds" : "second");
   SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, NULL);
   return 0;
 }
 
 static int kex_rekey_timer_cb(CALLBACK_FRAME) {
-  pr_trace_msg(trace_channel, 17, "SFTPRekey timer expired, requesting rekey");
+  pr_trace_msg(trace_channel, 17,
+    "SFTPRekey timer (%d %s) expired, requesting rekey", kex_rekey_interval,
+    kex_rekey_interval != 1 ? "secs" : "sec");
   sftp_kex_rekey();
   return 0;
 }
 
 static const unsigned char *calculate_h(struct sftp_kex *kex,
-    const unsigned char *hostkey_data, size_t hostkey_datalen, const BIGNUM *k,
-    uint32_t *hlen) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+    const unsigned char *hostkey_data, uint32_t hostkey_datalen,
+    const BIGNUM *k, uint32_t *hlen) {
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -194,7 +293,7 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
   unsigned char *buf, *ptr;
   uint32_t buflen, bufsz;
 
-  bufsz = buflen = 4096;
+  bufsz = buflen = 8192;
 
   /* XXX Is this buffer large enough? Too large? */
   ptr = buf = sftp_msg_getbuf(kex_pool, bufsz);
@@ -224,7 +323,8 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
   sftp_msg_write_mpint(&buf, &buflen, kex->e);
 
   /* Server's key */
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_key(kex->dh, &dh_pub_key, NULL);
 #else
   dh_pub_key = kex->dh->pub_key;
@@ -234,7 +334,8 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
   /* Shared secret */
   sftp_msg_write_mpint(&buf, &buflen, k);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -252,7 +353,8 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -268,7 +370,8 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -284,7 +387,8 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -293,7 +397,8 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
   EVP_DigestFinal(pctx, kex_digest_buf, hlen);
 #endif
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
@@ -305,9 +410,11 @@ static const unsigned char *calculate_h(struct sftp_kex *kex,
 }
 
 static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
-    const unsigned char *hostkey_data, size_t hostkey_datalen, const BIGNUM *k,
-    uint32_t min, uint32_t pref, uint32_t max, uint32_t *hlen) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+    const unsigned char *hostkey_data, uint32_t hostkey_datalen,
+    const BIGNUM *k, uint32_t min, uint32_t pref, uint32_t max,
+    uint32_t *hlen) {
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -353,7 +460,8 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
     sftp_msg_write_int(&buf, &buflen, max);
   }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_pqg(kex->dh, &dh_p, NULL, &dh_g);
 #else
   dh_p = kex->dh->p;
@@ -366,7 +474,8 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
   sftp_msg_write_mpint(&buf, &buflen, kex->e);
 
   /* Server's key */
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_key(kex->dh, &dh_pub_key, NULL);
 #else
   dh_pub_key = kex->dh->pub_key;
@@ -376,7 +485,8 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
   /* Shared secret */
   sftp_msg_write_mpint(&buf, &buflen, k);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -394,7 +504,8 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -410,7 +521,8 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -426,7 +538,8 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -435,10 +548,10 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
   EVP_DigestFinal(pctx, kex_digest_buf, hlen);
 #endif
 
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
-# endif /* OpenSSL-1.1.0 and later */
-
+#endif /* OpenSSL-1.1.0 and later */
   BN_clear_free(kex->e);
   kex->e = NULL;
   pr_memscrub(ptr, bufsz);
@@ -447,9 +560,11 @@ static const unsigned char *calculate_gex_h(struct sftp_kex *kex,
 }
 
 static const unsigned char *calculate_kexrsa_h(struct sftp_kex *kex,
-    const unsigned char *hostkey_data, size_t hostkey_datalen, const BIGNUM *k,
-    unsigned char *rsa_key, uint32_t rsa_keylen, uint32_t *hlen) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+    const unsigned char *hostkey_data, uint32_t hostkey_datalen,
+    const BIGNUM *k, unsigned char *rsa_key, uint32_t rsa_keylen,
+    uint32_t *hlen) {
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -492,7 +607,8 @@ static const unsigned char *calculate_kexrsa_h(struct sftp_kex *kex,
   /* Shared secret. */
   sftp_msg_write_mpint(&buf, &buflen, k);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -508,7 +624,8 @@ static const unsigned char *calculate_kexrsa_h(struct sftp_kex *kex,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error initializing message digest: %s", sftp_crypto_get_errors());
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -522,7 +639,8 @@ static const unsigned char *calculate_kexrsa_h(struct sftp_kex *kex,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error updating message digest: %s", sftp_crypto_get_errors());
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -536,7 +654,8 @@ static const unsigned char *calculate_kexrsa_h(struct sftp_kex *kex,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error finalizing message digest: %s", sftp_crypto_get_errors());
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -545,19 +664,21 @@ static const unsigned char *calculate_kexrsa_h(struct sftp_kex *kex,
   EVP_DigestFinal(pctx, kex_digest_buf, hlen);
 #endif
 
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
-# endif /* OpenSSL-1.1.0 and later */
-
+#endif /* OpenSSL-1.1.0 and later */
   pr_memscrub(ptr, bufsz);
+
   return kex_digest_buf;
 }
 
 #ifdef PR_USE_OPENSSL_ECC
 static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
-    const unsigned char *hostkey_data, size_t hostkey_datalen, const BIGNUM *k,
-    uint32_t *hlen) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+    const unsigned char *hostkey_data, uint32_t hostkey_datalen,
+    const BIGNUM *k, uint32_t *hlen) {
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -603,7 +724,8 @@ static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
   /* Shared secret */
   sftp_msg_write_mpint(&buf, &buflen, k);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -621,7 +743,8 @@ static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -637,7 +760,8 @@ static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -653,7 +777,8 @@ static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
     BN_clear_free(kex->e);
     kex->e = NULL;
     pr_memscrub(ptr, bufsz);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return NULL;
@@ -662,10 +787,10 @@ static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
   EVP_DigestFinal(pctx, kex_digest_buf, hlen);
 #endif
 
-# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
-# endif /* OpenSSL-1.1.0 and later */
-
+#endif /* OpenSSL-1.1.0 and later */
   BN_clear_free(kex->e);
   kex->e = NULL;
   pr_memscrub(ptr, bufsz);
@@ -676,7 +801,7 @@ static const unsigned char *calculate_ecdh_h(struct sftp_kex *kex,
 
 /* Make sure that the DH key we're generating is good enough. */
 static int have_good_dh(DH *dh, BIGNUM *pub_key) {
-  register unsigned int i;
+  register int i;
   unsigned int nbits = 0;
   const BIGNUM *dh_p = NULL;
   BIGNUM *tmp;
@@ -696,7 +821,8 @@ static int have_good_dh(DH *dh, BIGNUM *pub_key) {
     return -1;
   }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_pqg(dh, &dh_p, NULL, NULL);
 #else
   dh_p = dh->p;
@@ -824,7 +950,10 @@ static int create_dh(struct sftp_kex *kex, int type) {
   DH *dh;
 
   if (type != SFTP_DH_GROUP1_SHA1 &&
-      type != SFTP_DH_GROUP14_SHA1) {
+      type != SFTP_DH_GROUP14_SHA1 &&
+      type != SFTP_DH_GROUP14_SHA256 &&
+      type != SFTP_DH_GROUP16_SHA512 &&
+      type != SFTP_DH_GROUP18_SHA512) {
     errno = EINVAL;
     return -1;
   }
@@ -876,27 +1005,52 @@ static int create_dh(struct sftp_kex *kex, int type) {
     }
 
     dh_p = BN_new();
-  
-    if (type == SFTP_DH_GROUP1_SHA1) {
-      if (BN_hex2bn(&dh_p, dh_group1_str) == 0) {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "error setting DH (group1) P: %s", sftp_crypto_get_errors());
-        BN_clear_free(dh_p);
-        DH_free(dh);
-        return -1;
-      }
 
-    } else if (type == SFTP_DH_GROUP14_SHA1) {
-      if (BN_hex2bn(&dh_p, dh_group14_str) == 0) {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "error setting DH (group14) P: %s", sftp_crypto_get_errors());
-        BN_clear_free(dh_p);
-        DH_free(dh);
-        return -1;
-      }
+    switch (type) {
+      case SFTP_DH_GROUP18_SHA512:
+        if (BN_hex2bn(&dh_p, dh_group18_str) == 0) {
+          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+            "error setting DH (group18) P: %s", sftp_crypto_get_errors());
+          BN_clear_free(dh_p);
+          DH_free(dh);
+          return -1;
+        }
+        break;
+
+      case SFTP_DH_GROUP16_SHA512:
+        if (BN_hex2bn(&dh_p, dh_group16_str) == 0) {
+          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+            "error setting DH (group16) P: %s", sftp_crypto_get_errors());
+          BN_clear_free(dh_p);
+          DH_free(dh);
+          return -1;
+        }
+        break;
+
+      case SFTP_DH_GROUP14_SHA1:
+      case SFTP_DH_GROUP14_SHA256:
+        if (BN_hex2bn(&dh_p, dh_group14_str) == 0) {
+          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+            "error setting DH (group14) P: %s", sftp_crypto_get_errors());
+          BN_clear_free(dh_p);
+          DH_free(dh);
+          return -1;
+        }
+        break;
+
+      default:
+        if (BN_hex2bn(&dh_p, dh_group1_str) == 0) {
+          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+            "error setting DH (group1) P: %s", sftp_crypto_get_errors());
+          BN_clear_free(dh_p);
+          DH_free(dh);
+          return -1;
+        }
+        break;
     }
 
     dh_g = BN_new();
+
     if (BN_hex2bn(&dh_g, "2") == 0) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error setting DH G: %s", sftp_crypto_get_errors());
@@ -906,7 +1060,8 @@ static int create_dh(struct sftp_kex *kex, int type) {
       return -1;
     }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     DH_set0_pqg(dh, dh_p, NULL, dh_g);
 #else
     dh->p = dh_p;
@@ -926,7 +1081,8 @@ static int create_dh(struct sftp_kex *kex, int type) {
     }
 
     dh_pub_key = BN_new();
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     DH_set0_key(dh, dh_pub_key, dh_priv_key);
 #else
     dh->pub_key = dh_pub_key;
@@ -941,7 +1097,8 @@ static int create_dh(struct sftp_kex *kex, int type) {
       return -1;
     }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     DH_get0_key(dh, &dh_pub_key, NULL);
 #else
     dh_pub_key = dh->pub_key;
@@ -953,7 +1110,25 @@ static int create_dh(struct sftp_kex *kex, int type) {
     }
 
     kex->dh = dh;
-    kex->hash = EVP_sha1();
+
+    switch (type) {
+#ifdef HAVE_SHA512_OPENSSL
+      case SFTP_DH_GROUP16_SHA512:
+      case SFTP_DH_GROUP18_SHA512:
+        kex->hash = EVP_sha512();
+        break;
+#endif /* HAVE_SHA512_OPENSSL */
+
+#ifdef HAVE_SHA256_OPENSSL
+      case SFTP_DH_GROUP14_SHA256:
+        kex->hash = EVP_sha256();
+        break;
+#endif /* HAVE_SHA256_OPENSSL */
+
+      default:
+        kex->hash = EVP_sha1();
+    }
+
     return 0;
   }
 
@@ -1009,8 +1184,9 @@ static int prepare_dh(struct sftp_kex *kex, int type) {
   if (type == SFTP_DH_GEX_SHA1) {
     kex->hash = EVP_sha1();
 
-#if (OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
-    (OPENSSL_VERSION_NUMBER > 0x000908000L)
+#if ((OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
+     (OPENSSL_VERSION_NUMBER > 0x000908000L)) && \
+     defined(HAVE_SHA256_OPENSSL)
   } else if (type == SFTP_DH_GEX_SHA256) {
     kex->hash = EVP_sha256();
 #endif
@@ -1035,8 +1211,8 @@ static int finish_dh(struct sftp_kex *kex) {
       attempts);
 
     dh_priv_key = BN_new();
- 
-    /* Generate a random private exponent of the desired size, in bits. */ 
+  
+    /* Generate a random private exponent of the desired size, in bits. */
     if (!BN_rand(dh_priv_key, dh_nbits, 0, 0)) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error generating DH random key (%d bits): %s", dh_nbits,
@@ -1047,7 +1223,8 @@ static int finish_dh(struct sftp_kex *kex) {
 
     dh_pub_key = BN_new();
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     DH_set0_key(kex->dh, dh_pub_key, dh_priv_key);
 #else
     kex->dh->pub_key = dh_pub_key;
@@ -1065,7 +1242,8 @@ static int finish_dh(struct sftp_kex *kex) {
       dh_pub_key = NULL;
       dh_priv_key = NULL;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       DH_get0_key(kex->dh, &dh_pub_key, &dh_priv_key);
 #else
       dh_pub_key = kex->dh->pub_key;
@@ -1155,12 +1333,13 @@ static int create_kexrsa(struct sftp_kex *kex, int type) {
 
     kex->hash = EVP_sha1();
 
-#if (OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
-    (OPENSSL_VERSION_NUMBER > 0x000908000L)
+#if ((OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
+     (OPENSSL_VERSION_NUMBER > 0x000908000L)) && \
+     defined(HAVE_SHA256_OPENSSL)
   } else if (type == SFTP_KEXRSA_SHA256) {
     BIGNUM *e = NULL;
 
-#if OPENSSL_VERSION_NUMBER > 0x000908000L
+# if OPENSSL_VERSION_NUMBER > 0x000908000L
     e = BN_new();
     if (e == NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -1176,10 +1355,10 @@ static int create_kexrsa(struct sftp_kex *kex, int type) {
     }
 
     if (RSA_generate_key_ex(rsa, SFTP_KEXRSA_SHA256_SIZE, e, NULL) != 1) {
-#else
+# else
     rsa = RSA_generate_key(SFTP_KEXRSA_SHA256_SIZE, 65537, NULL, NULL);
     if (rsa == NULL) {
-#endif /* OpenSSL version 0.9.8 and later */
+# endif /* OpenSSL version 0.9.8 and later */
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error generating %u-bit RSA key: %s", SFTP_KEXRSA_SHA256_SIZE,
         sftp_crypto_get_errors());
@@ -1228,6 +1407,7 @@ static int create_ecdh(struct sftp_kex *kex, int type) {
   }
 
   switch (type) {
+# if defined(HAVE_SHA256_OPENSSL)
     case SFTP_ECDH_SHA256:
       curve_nid = NID_X9_62_prime256v1;
       curve_name = "NID_X9_62_prime256v1";
@@ -1239,12 +1419,15 @@ static int create_ecdh(struct sftp_kex *kex, int type) {
       curve_name = "NID_secp384r1";
       kex->hash = EVP_sha384();
       break;
+# endif /* HAVE_SHA256_OPENSSL */
 
+# if defined(HAVE_SHA512_OPENSSL)
     case SFTP_ECDH_SHA512:
       curve_nid = NID_secp521r1;
       curve_name = "NID_secp521r1";
       kex->hash = EVP_sha512();
       break;
+# endif /* HAVE_SHA512_OPENSSL */
   }
 
   ec = EC_KEY_new_by_curve_name(curve_nid);
@@ -1279,47 +1462,8 @@ static int finish_ecdh(struct sftp_kex *kex) {
 
   return 0;
 }
-
 #endif /* PR_USE_OPENSSL_ECC */
 
-static array_header *parse_namelist(pool *p, const char *names) {
-  char *ptr;
-  array_header *list;
-  size_t names_len;
-
-  list = make_array(p, 0, sizeof(const char *));
-
-  names_len = strlen(names);
-  if (names_len == 0) {
-    return list;
-  }
-
-  ptr = memchr(names, ',', names_len);
-  while (ptr != NULL) {
-    char *elt;
-    size_t elt_len;
-
-    pr_signals_handle();
-
-    elt_len = ptr - names;
-
-    elt = palloc(p, elt_len + 1);
-    memcpy(elt, names, elt_len);
-    elt[elt_len] = '\0';
-
-    *((const char **) push_array(list)) = elt;
-    names = ++ptr;
-
-    /* Add one for the ',' character we skipped over. */
-    names_len -= (elt_len + 1);
-
-    ptr = memchr(names, ',', names_len);
-  }
-  *((const char **) push_array(list)) = pstrdup(p, names);
-
-  return list;
-}
-
 /* Given a name-list, return the first (i.e. preferred) name in the list. */
 static const char *get_preferred_name(pool *p, const char *names) {
   register unsigned int i;
@@ -1339,63 +1483,42 @@ static const char *get_preferred_name(pool *p, const char *names) {
 
   /* This should never happen. */
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-    "unable to find preferred name in '%s'", names ? names : "(null)");
+    "unable to find preferred name in '%s'", names);
   return NULL;
 }
 
-/* Given name-lists from the client and server, find the first name from the
- * client list which appears on the server list.
+/* Note that in this default list of key exchange algorithms, one of the
+ * REQUIRED algorithms is conspicuously absent:
+ *
+ *   diffie-hellman-group1-sha1
+ *
+ * This exchange has a weak hardcoded DH group, and will thus only be used
+ * if explicitly requested via SFTPKeyExchanges, or if the AllowWeakDH
+ * SFTPOption is used.
  */
-static const char *get_shared_name(pool *p, const char *c2s_names,
-    const char *s2c_names) {
-  register unsigned int i;
-  const char *name = NULL, **client_names, **server_names;
-  pool *tmp_pool;
-  array_header *client_list, *server_list;
-
-  tmp_pool = make_sub_pool(p);
-  pr_pool_tag(tmp_pool, "SSH2 session shared name pool");
-
-  client_list = parse_namelist(tmp_pool, c2s_names);
-  client_names = (const char **) client_list->elts;
-
-  server_list = parse_namelist(tmp_pool, s2c_names);
-  server_names = (const char **) server_list->elts;
-
-  for (i = 0; i < client_list->nelts; i++) {
-    register unsigned int j;
-
-    if (name)
-      break;
-
-    for (j = 0; j < server_list->nelts; j++) {
-      if (strcmp(client_names[i], server_names[j]) == 0) {
-        name = client_names[i];
-        break;
-      }
-    }
-  }
-
-  name = pstrdup(p, name);
-  destroy_pool(tmp_pool);
-
-  return name;
-}
-
 static const char *kex_exchanges[] = {
+#if defined(PR_USE_SODIUM) && defined(HAVE_SHA256_OPENSSL)
+  "curve25519-sha256 at libssh.org",
+#endif /* PR_USE_SODIUM and HAVE_SHA256_OPENSSL */
 #ifdef PR_USE_OPENSSL_ECC
-  "ecdh-sha2-nistp256",
-  "ecdh-sha2-nistp384",
   "ecdh-sha2-nistp521",
+  "ecdh-sha2-nistp384",
+  "ecdh-sha2-nistp256",
 #endif /* PR_USE_OPENSSL_ECC */
 
 #if (OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
     (OPENSSL_VERSION_NUMBER > 0x000908000L)
+# if defined(HAVE_SHA512_OPENSSL)
+  "diffie-hellman-group18-sha512",
+  "diffie-hellman-group16-sha512",
+# endif /* HAVE_SHA512_OPENSSL */
+# if defined(HAVE_SHA256_OPENSSL)
+  "diffie-hellman-group14-sha256",
   "diffie-hellman-group-exchange-sha256",
+# endif /* HAVE_SHA256_OPENSSL */
 #endif
   "diffie-hellman-group-exchange-sha1",
   "diffie-hellman-group14-sha1",
-  "diffie-hellman-group1-sha1",
 
 #if 0
 /* We cannot currently support rsa2048-sha256, since it requires support
@@ -1403,8 +1526,9 @@ static const char *kex_exchanges[] = {
  * at present, which only allows EME-OAEP using SHA1.  v2.1 allows for
  * using other message digests, e.g. SHA256, for EME-OAEP.
  */
-#if (OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
-    (OPENSSL_VERSION_NUMBER > 0x000908000L)
+#if ((OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
+     (OPENSSL_VERSION_NUMBER > 0x000908000L)) && \
+     defined(HAVE_SHA256_OPENSSL)
   "rsa2048-sha256",
 #endif
 #endif
@@ -1418,7 +1542,7 @@ static const char *get_kexinit_exchange_list(pool *p) {
   config_rec *c;
 
   c = find_config(main_server->conf, CONF_PARAM, "SFTPKeyExchanges", FALSE);
-  if (c) {
+  if (c != NULL) {
     res = pstrdup(p, c->argv[0]);
 
   } else {
@@ -1428,6 +1552,16 @@ static const char *get_kexinit_exchange_list(pool *p) {
       res = pstrcat(p, res, *res ? "," : "", pstrdup(p, kex_exchanges[i]),
         NULL);
     }
+
+    if (sftp_opts & SFTP_OPT_ALLOW_WEAK_DH) {
+      /* The hardcoded group for this exchange is rather weak in the face of
+       * the "Logjam" vulnerability (see https://weakdh.org).  Thus it is
+       * only appended to the end of the default exchanges if the AllowWeakDH
+       * SFTPOption is in effect.
+       */
+      res = pstrcat(p, res, ",", pstrdup(p, "diffie-hellman-group1-sha1"),
+        NULL);
+    }
   }
 
   return res;
@@ -1449,7 +1583,7 @@ static const char *get_kexinit_hostkey_algo_list(pool *p) {
 #ifdef PR_USE_OPENSSL_ECC
   res = sftp_keys_have_ecdsa_hostkey(p, &nids);
   if (res > 0) {
-    register unsigned int i;
+    register int i;
 
     for (i = 0; i < res; i++) {
       char *algo_name = NULL;
@@ -1495,13 +1629,18 @@ static struct sftp_kex *create_kex(pool *p) {
   struct sftp_kex *kex;
   const char *list;
   config_rec *c;
+  pool *tmp_pool;
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "Kex KEXINIT Pool");
 
-  kex = pcalloc(p, sizeof(struct sftp_kex));
+  kex = pcalloc(tmp_pool, sizeof(struct sftp_kex));
+  kex->pool = tmp_pool;
   kex->client_version = kex_client_version;
   kex->server_version = kex_server_version;
-  kex->client_names = pcalloc(p, sizeof(struct sftp_kex_names));
-  kex->server_names = pcalloc(p, sizeof(struct sftp_kex_names));
-  kex->session_names = pcalloc(p, sizeof(struct sftp_kex_names));
+  kex->client_names = pcalloc(kex->pool, sizeof(struct sftp_kex_names));
+  kex->server_names = pcalloc(kex->pool, sizeof(struct sftp_kex_names));
+  kex->session_names = pcalloc(kex->pool, sizeof(struct sftp_kex_names));
   kex->use_hostkey_type = SFTP_KEY_UNKNOWN;
   kex->dh = NULL;
   kex->e = NULL;
@@ -1513,17 +1652,17 @@ static struct sftp_kex *create_kex(pool *p) {
   kex->rsa_encrypted = NULL;
   kex->rsa_encrypted_len = 0;
 
-  list = get_kexinit_exchange_list(kex_pool);
+  list = get_kexinit_exchange_list(kex->pool);
   kex->server_names->kex_algo = list;
 
-  list = get_kexinit_hostkey_algo_list(kex_pool);
+  list = get_kexinit_hostkey_algo_list(kex->pool);
   kex->server_names->server_hostkey_algo = list;
 
-  list = sftp_crypto_get_kexinit_cipher_list(kex_pool);
+  list = sftp_crypto_get_kexinit_cipher_list(kex->pool);
   kex->server_names->c2s_encrypt_algo = list;
   kex->server_names->s2c_encrypt_algo = list;
 
-  list = sftp_crypto_get_kexinit_digest_list(kex_pool);
+  list = sftp_crypto_get_kexinit_digest_list(kex->pool);
   kex->server_names->c2s_mac_algo = list;
   kex->server_names->s2c_mac_algo = list;
 
@@ -1617,6 +1756,11 @@ static void destroy_kex(struct sftp_kex *kex) {
       pr_memscrub((char *) kex->h, kex->hlen);
       kex->hlen = 0;
     }
+
+    if (kex->pool) {
+      destroy_pool(kex->pool);
+      kex->pool = NULL;
+    }
   }
 
   kex_first_kex = kex_rekey_kex = NULL;
@@ -1646,6 +1790,39 @@ static int setup_kex_algo(struct sftp_kex *kex, const char *algo) {
     kex->session_names->kex_algo = algo;
     return 0;
 
+  } else if (strncmp(algo, "diffie-hellman-group14-sha256", 30) == 0) {
+    if (create_dh(kex, SFTP_DH_GROUP14_SHA256) < 0) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "error using '%s' as the key exchange algorithm: %s", algo,
+        strerror(errno));
+      return -1;
+    }
+
+    kex->session_names->kex_algo = algo;
+    return 0;
+
+  } else if (strncmp(algo, "diffie-hellman-group16-sha512", 30) == 0) {
+    if (create_dh(kex, SFTP_DH_GROUP16_SHA512) < 0) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "error using '%s' as the key exchange algorithm: %s", algo,
+        strerror(errno));
+      return -1;
+    }
+
+    kex->session_names->kex_algo = algo;
+    return 0;
+
+  } else if (strncmp(algo, "diffie-hellman-group18-sha512", 30) == 0) {
+    if (create_dh(kex, SFTP_DH_GROUP18_SHA512) < 0) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "error using '%s' as the key exchange algorithm: %s", algo,
+        strerror(errno));
+      return -1;
+    }
+
+    kex->session_names->kex_algo = algo;
+    return 0;
+
   } else if (strncmp(algo, "diffie-hellman-group-exchange-sha1", 35) == 0) {
     if (prepare_dh(kex, SFTP_DH_GEX_SHA1) < 0) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -1670,8 +1847,9 @@ static int setup_kex_algo(struct sftp_kex *kex, const char *algo) {
     kex->use_kexrsa = TRUE;
     return 0;
 
-#if (OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
-    (OPENSSL_VERSION_NUMBER > 0x000908000L)
+#if ((OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
+     (OPENSSL_VERSION_NUMBER > 0x000908000L)) && \
+     defined(HAVE_SHA256_OPENSSL)
   } else if (strncmp(algo, "diffie-hellman-group-exchange-sha256", 37) == 0) {
     if (prepare_dh(kex, SFTP_DH_GEX_SHA256) < 0) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -1733,8 +1911,15 @@ static int setup_kex_algo(struct sftp_kex *kex, const char *algo) {
     kex->session_names->kex_algo = algo;
     kex->use_ecdh = TRUE;
     return 0;
-
 #endif /* PR_USE_OPENSSL_ECC */
+
+#if defined(PR_USE_SODIUM) && defined(HAVE_SHA256_OPENSSL)
+  } else if (strncmp(algo, "curve25519-sha256 at libssh.org", 22) == 0) {
+    kex->hash = EVP_sha256();
+    kex->session_names->kex_algo = algo;
+    kex->use_curve25519 = TRUE;
+    return 0;
+#endif /* PR_USE_SODIUM and HAVE_SHA256_OPENSSL */
   }
 
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -1836,11 +2021,13 @@ static int setup_s2c_comp_algo(struct sftp_kex *kex, const char *algo) {
 }
 
 static int setup_c2s_lang(struct sftp_kex *kex, const char *lang) {
+  /* XXX Need to implement the functionality here. */
   kex->session_names->c2s_lang = lang;
   return 0;
 }
 
 static int setup_s2c_lang(struct sftp_kex *kex, const char *lang) {
+  /* XXX Need to implement the functionality here. */
   kex->session_names->s2c_lang = lang;
   return 0;
 }
@@ -1850,7 +2037,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   const char *client_pref, *server_pref;
   pool *tmp_pool;
 
-  tmp_pool = make_sub_pool(kex_pool);
+  tmp_pool = make_sub_pool(kex->pool);
   pr_pool_tag(tmp_pool, "SSH2 session shared name pool");
 
   client_list = kex->client_names->kex_algo;
@@ -1886,7 +2073,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
     }
   }
 
-  kex_algo = get_shared_name(kex_pool, client_list, server_list);
+  kex_algo = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (kex_algo != NULL) {
     /* Unlike the following algorithms, we wait to setup the chosen kex algo
      * until the end.  Why?  The kex algo setup may require knowledge of the
@@ -1913,7 +2100,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8,
     "server-sent host key algorithms: %s", server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_hostkey_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -1941,7 +2128,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8, "server-sent client encryption algorithms: %s",
     server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_c2s_encrypt_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -1969,7 +2156,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8, "server-sent server encryption algorithms: %s",
     server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_s2c_encrypt_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -1997,7 +2184,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8, "server-sent client MAC algorithms: %s",
     server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_c2s_mac_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -2025,7 +2212,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8, "server-sent server MAC algorithms: %s",
     server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_s2c_mac_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -2053,7 +2240,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8,
     "server-sent client compression algorithms: %s", server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_c2s_comp_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -2081,7 +2268,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8,
     "server-sent server compression algorithms: %s", server_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_s2c_comp_algo(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -2109,7 +2296,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8,
     "server-sent client languages: %s", client_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_c2s_lang(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -2140,7 +2327,7 @@ static int get_session_names(struct sftp_kex *kex, int *correct_guess) {
   pr_trace_msg(trace_channel, 8,
     "server-sent server languages: %s", client_list);
 
-  shared = get_shared_name(kex_pool, client_list, server_list);
+  shared = sftp_misc_namelist_shared(kex->pool, client_list, server_list);
   if (shared) {
     if (setup_s2c_lang(kex, shared) < 0) {
       destroy_pool(tmp_pool);
@@ -2184,43 +2371,43 @@ static int read_kexinit(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   buflen = pkt->payload_len;
 
   /* Make a copy of the payload for later. */
-  kex->client_kexinit_payload = palloc(kex_pool, pkt->payload_len);
+  kex->client_kexinit_payload = palloc(kex->pool, pkt->payload_len);
   kex->client_kexinit_payload_len = pkt->payload_len;
   memcpy(kex->client_kexinit_payload, pkt->payload, pkt->payload_len);
 
   /* Read the cookie, which is a mandated length of 16 bytes. */
   (void) sftp_msg_read_data(pkt->pool, &buf, &buflen, 16);
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->kex_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->server_hostkey_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->c2s_encrypt_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->s2c_encrypt_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->c2s_mac_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->s2c_mac_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->c2s_comp_algo = list;
 
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->s2c_comp_algo = list;
 
   /* Client-to-server languages */
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->c2s_lang = list;
 
   /* Server-to-client languages */
-  list = sftp_msg_read_string(kex_pool, &buf, &buflen);
+  list = sftp_msg_read_string(kex->pool, &buf, &buflen);
   kex->client_names->s2c_lang = list;
 
   /* Read the "first kex packet follows" byte */
@@ -2319,7 +2506,7 @@ static int write_kexinit(struct ssh2_packet *pkt, struct sftp_kex *kex) {
    * is the KEXINIT identifier.
    */
   kex->server_kexinit_payload_len = pkt->payload_len - 1;
-  kex->server_kexinit_payload = palloc(kex_pool, pkt->payload_len - 1);
+  kex->server_kexinit_payload = palloc(kex->pool, pkt->payload_len - 1);
   memcpy(kex->server_kexinit_payload, pkt->payload + 1, pkt->payload_len - 1);
 
   return 0;
@@ -2343,25 +2530,51 @@ static int read_dh_init(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   return 0;
 }
 
+/* Only set the given environment variable/value IFF it is not already
+ * present.
+ */
+static void set_env_var(pool *p, const char *k, const char *v) {
+  const char *val;
+  int have_val = FALSE;
+
+  val = pr_env_get(p, k);
+  if (val != NULL) {
+    if (strcmp(val, v) == 0) {
+      have_val = TRUE;
+    }
+  }
+
+  if (have_val == FALSE) {
+    k = pstrdup(p, k);
+    v = pstrdup(p, v);
+    pr_env_unset(p, k);
+    pr_env_set(p, k, v);
+  }
+}
+
 static int set_session_keys(struct sftp_kex *kex) {
-  const char *k, *v;
+  const char *k;
   int comp_read_flags, comp_write_flags;
 
   if (sftp_cipher_set_read_key(kex_pool, kex->hash, kex->k, kex->h,
-      kex->hlen) < 0)
+      kex->hlen, SFTP_ROLE_SERVER) < 0) {
     return -1;
+  }
 
   if (sftp_cipher_set_write_key(kex_pool, kex->hash, kex->k, kex->h,
-      kex->hlen) < 0)
+      kex->hlen, SFTP_ROLE_SERVER) < 0) {
     return -1;
+  }
 
   if (sftp_mac_set_read_key(kex_pool, kex->hash, kex->k, kex->h,
-      kex->hlen) < 0)
+      kex->hlen, SFTP_ROLE_SERVER) < 0) {
     return -1;
+  }
 
   if (sftp_mac_set_write_key(kex_pool, kex->hash, kex->k, kex->h,
-      kex->hlen) < 0)
+      kex->hlen, SFTP_ROLE_SERVER) < 0) {
     return -1;
+  }
 
   comp_read_flags = comp_write_flags = SFTP_COMPRESS_FL_NEW_KEY;
 
@@ -2382,46 +2595,28 @@ static int set_session_keys(struct sftp_kex *kex) {
     }
   }
 
-  if (sftp_compress_init_read(comp_read_flags) < 0)
+  if (sftp_compress_init_read(comp_read_flags) < 0) {
     return -1;
+  }
 
-  if (sftp_compress_init_write(comp_write_flags) < 0)
+  if (sftp_compress_init_write(comp_write_flags) < 0) {
     return -1;
+  }
 
-  k = pstrdup(session.pool, "SFTP_CLIENT_CIPHER_ALGO");
-  v = pstrdup(session.pool, sftp_cipher_get_read_algo());
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
-
-  k = pstrdup(session.pool, "SFTP_SERVER_CIPHER_ALGO");
-  v = pstrdup(session.pool, sftp_cipher_get_write_algo());
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
-
-  k = pstrdup(session.pool, "SFTP_CLIENT_MAC_ALGO");
-  v = pstrdup(session.pool, sftp_mac_get_read_algo());
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
-
-  k = pstrdup(session.pool, "SFTP_SERVER_MAC_ALGO");
-  v = pstrdup(session.pool, sftp_mac_get_write_algo());
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
-
-  k = pstrdup(session.pool, "SFTP_CLIENT_COMPRESSION_ALGO");
-  v = pstrdup(session.pool, sftp_compress_get_read_algo());
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
-
-  k = pstrdup(session.pool, "SFTP_SERVER_COMPRESSION_ALGO");
-  v = pstrdup(session.pool, sftp_compress_get_write_algo());
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
-
-  k = pstrdup(session.pool, "SFTP_KEX_ALGO");
-  v = pstrdup(session.pool, kex->session_names->kex_algo);
-  pr_env_unset(session.pool, k);
-  pr_env_set(session.pool, k, v);
+  set_env_var(session.pool, "SFTP_CLIENT_CIPHER_ALGO",
+    sftp_cipher_get_read_algo());
+  set_env_var(session.pool, "SFTP_SERVER_CIPHER_ALGO",
+    sftp_cipher_get_write_algo());
+  set_env_var(session.pool, "SFTP_CLIENT_MAC_ALGO",
+    sftp_mac_get_read_algo());
+  set_env_var(session.pool, "SFTP_SERVER_MAC_ALGO",
+    sftp_mac_get_write_algo());
+  set_env_var(session.pool, "SFTP_CLIENT_COMPRESSION_ALGO",
+    sftp_compress_get_read_algo());
+  set_env_var(session.pool, "SFTP_SERVER_COMPRESSION_ALGO",
+    sftp_compress_get_write_algo());
+  set_env_var(session.pool, "SFTP_KEX_ALGO",
+    kex->session_names->kex_algo);
 
   if (kex_rekey_interval > 0 &&
       kex_rekey_timerno == -1) {
@@ -2466,14 +2661,14 @@ static int write_dh_reply(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   const unsigned char *h;
   const unsigned char *hostkey_data, *hsig;
   unsigned char *buf, *ptr;
-  uint32_t bufsz, buflen, hlen = 0;
-  size_t dhlen, hostkey_datalen, hsiglen;
+  uint32_t bufsz, buflen, hlen = 0, hostkey_datalen = 0;
+  size_t dhlen, hsiglen;
   BIGNUM *k = NULL, *dh_pub_key = NULL;
   int res;
 
   /* Compute the shared secret */
   dhlen = DH_size(kex->dh);
-  buf = palloc(kex_pool, dhlen);
+  buf = palloc(pkt->pool, dhlen);
 
   pr_trace_msg(trace_channel, 12, "computing DH key");
   res = DH_compute_key((unsigned char *) buf, kex->e, kex->dh);
@@ -2543,7 +2738,8 @@ static int write_dh_reply(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_KEX_DH_REPLY);
   sftp_msg_write_data(&buf, &buflen, hostkey_data, hostkey_datalen, TRUE);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_key(kex->dh, &dh_pub_key, NULL);
 #else
   dh_pub_key = kex->dh->pub_key;
@@ -2584,7 +2780,7 @@ static int handle_kex_dh(struct ssh2_packet *pkt, struct sftp_kex *kex) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "DH_INIT"));
   cmd->arg = "(data)";
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   pr_trace_msg(trace_channel, 9, "reading DH_INIT message from client");
 
@@ -2672,10 +2868,24 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
 
   dhparam_path = PR_CONFIG_DIR "/dhparams.pem";
   c = find_config(main_server->conf, CONF_PARAM, "SFTPDHParamFile", FALSE);
-  if (c) {
+  if (c != NULL) {
     dhparam_path = c->argv[0];
   }
 
+  /* If the preferred DH is less than SFTP_DH_MIN_LEN, AND the AllowWeakDH
+   * SFTPOption is not used, then use a pref of SFTP_DH_MIN_LEN (Bug#4184).
+   */
+  if (pref < SFTP_DH_MIN_LEN) {
+    if (!(sftp_opts & SFTP_OPT_ALLOW_WEAK_DH)) {
+      pref = SFTP_DH_MIN_LEN;
+
+    } else {
+      pr_trace_msg(trace_channel, 14,
+       "client prefers relatively weak DH group size (%lu) but AllowWeakDH "
+       "SFTPOption in effect", (unsigned long) pref);
+    }
+  }
+
   if (dhparam_path) {
     if (kex_dhparams_fp != NULL) {
       /* Rewind to the start of the file. */
@@ -2689,14 +2899,14 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
       register unsigned int i;
       pool *tmp_pool;
       array_header *smaller_dhs, *pref_dhs, *larger_dhs;
-      DH *dh, **dhs;
-      int smaller_dh_nbits = 0, larger_dh_nbits = 0;
+      DH *chosen_dh, **dhs;
+      uint32_t smaller_dh_nbits = 0, larger_dh_nbits = 0;
 
       pr_trace_msg(trace_channel, 15,
         "using DH parameters from SFTPDHParamFile '%s' for group exchange",
         dhparam_path);
 
-      tmp_pool = make_sub_pool(kex_pool);
+      tmp_pool = make_sub_pool(kex->pool);
       pr_pool_tag(tmp_pool, "Kex DHparams selection pool");
 
       smaller_dhs = make_array(tmp_pool, 1, sizeof(DH *)); 
@@ -2723,12 +2933,12 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
        */
 
       while (TRUE) {
-        int nbits;
+        uint32_t nbits;
 
         pr_signals_handle();
 
-        dh = PEM_read_DHparams(kex_dhparams_fp, NULL, NULL, NULL);
-        if (dh == NULL) {
+        chosen_dh = PEM_read_DHparams(kex_dhparams_fp, NULL, NULL, NULL);
+        if (chosen_dh == NULL) {
           if (!feof(kex_dhparams_fp)) {
             pr_trace_msg(trace_channel, 5, "error reading DH params from "
               "SFTPDHParamFile '%s': %s", dhparam_path,
@@ -2738,16 +2948,20 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
           break;
         }
 
-        nbits = DH_size(dh) * 8;
+        nbits = DH_size(chosen_dh) * 8;
 
         if (nbits < min ||
             nbits > max) {
-          DH_free(dh);
+          pr_trace_msg(trace_channel, 17,
+            "skipping %lu-bit DH from %s (exceeds min %lu, max %lu bits)",
+            (unsigned long) nbits, dhparam_path, (unsigned long) min,
+            (unsigned long) max);
+          DH_free(chosen_dh);
           continue;
         }
 
         if (nbits == pref) {
-          *((DH **) push_array(pref_dhs)) = dh;
+          *((DH **) push_array(pref_dhs)) = chosen_dh;
 
         } else if (nbits < pref) {
           if (nbits > smaller_dh_nbits) {
@@ -2761,13 +2975,13 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
             }
 
             smaller_dh_nbits = nbits;
-            *((DH **) push_array(smaller_dhs)) = dh;
+            *((DH **) push_array(smaller_dhs)) = chosen_dh;
 
           } else if (nbits == smaller_dh_nbits) {
-            *((DH **) push_array(smaller_dhs)) = dh;
+            *((DH **) push_array(smaller_dhs)) = chosen_dh;
 
           } else {
-            DH_free(dh);
+            DH_free(chosen_dh);
           }
 
         } else {
@@ -2784,42 +2998,54 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
             }
 
             larger_dh_nbits = nbits;
-            *((DH **) push_array(larger_dhs)) = dh;
+            *((DH **) push_array(larger_dhs)) = chosen_dh;
 
           } else if (nbits == larger_dh_nbits) {
-            *((DH **) push_array(larger_dhs)) = dh;
+            *((DH **) push_array(larger_dhs)) = chosen_dh;
 
           } else {
-            DH_free(dh);
+            DH_free(chosen_dh);
           }
         }
       }
 
-      dh = NULL;
+      chosen_dh = NULL;
 
       /* The use of rand(3) below is NOT intended to be perfect, or even
        * uniformly distributed.  It simply needs to be good enough to pick
        * a single item from a small list, where all items are equally
        * usable and valid.
+       *
+       * Ideally we want to find a preferred DH first.  Failing that, a larger
+       * DH is better; if none found there, then we settle for a smaller DH.
        */
 
       if (pref_dhs->nelts > 0) {
         int r = (int) (rand() / (RAND_MAX / pref_dhs->nelts + 1));
 
+        pr_trace_msg(trace_channel, 17,
+          "%s DH selection: preferred DHs (count %u, idx %d)", dhparam_path,
+          pref_dhs->nelts, r);
         dhs = pref_dhs->elts;
-        dh = dhs[r];
+        chosen_dh = dhs[r];
 
       } else if (larger_dhs->nelts > 0) {
         int r = (int) (rand() / (RAND_MAX / larger_dhs->nelts + 1));
 
+        pr_trace_msg(trace_channel, 17,
+          "%s DH selection: larger DHs (count %u, idx %d)", dhparam_path,
+          larger_dhs->nelts, r);
         dhs = larger_dhs->elts;
-        dh = dhs[r];
+        chosen_dh = dhs[r];
 
       } else if (smaller_dhs->nelts > 0) {
         int r = (int) (rand() / (RAND_MAX / smaller_dhs->nelts + 1));
 
+        pr_trace_msg(trace_channel, 17,
+          "%s DH selection: smaller DHs (count %u, idx %d)", dhparam_path,
+          smaller_dhs->nelts, r);
         dhs = smaller_dhs->elts;
-        dh = dhs[r];
+        chosen_dh = dhs[r];
 
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -2830,19 +3056,24 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
         use_fixed_modulus = TRUE;
       }
 
-      if (dh) {
+      if (chosen_dh != NULL) {
         BIGNUM *dh_p = NULL, *dh_g = NULL, *dup_p, *dup_g;
 
         pr_trace_msg(trace_channel, 20, "client requested min %lu, pref %lu, "
           "max %lu sizes for DH group exchange, selected DH of %lu bits",
           (unsigned long) min, (unsigned long) pref, (unsigned long) max,
-          (unsigned long) DH_size(dh) * 8);
+          (unsigned long) DH_size(chosen_dh) * 8);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-        DH_get0_pqg(dh, &dh_p, NULL, &dh_g);
+        /* Get the P, G parameters of the chosen DH group, and make copies
+         * of them for our KEX DH.
+         */
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+        DH_get0_pqg(chosen_dh, &dh_p, NULL, &dh_g);
 #else
-        dh_p = dh->p;
-        dh_g = dh->g;
+        dh_p = chosen_dh->p;
+        dh_g = chosen_dh->g;
 #endif /* prior to OpenSSL-1.1.0 */
 
         dup_p = BN_dup(dh_p);
@@ -2860,12 +3091,13 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
               "error copying selected DH G: %s", sftp_crypto_get_errors());
             (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
               "WARNING: using fixed modulus for DH group exchange");
-
             BN_clear_free(dup_p);
             use_fixed_modulus = TRUE;
 
           } else {
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+            /* Now set those P, G copies into our KEX DH. */
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
             DH_set0_pqg(kex->dh, dup_p, NULL, dup_g);
 #else
             kex->dh->p = dup_p;
@@ -2911,6 +3143,7 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
 
     dh_p = BN_new();
 
+    /* Note: Consider using a stronger fixed DH group here! */
     if (BN_hex2bn(&dh_p, dh_group14_str) == 0) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "error setting DH P: %s", sftp_crypto_get_errors());
@@ -2929,7 +3162,8 @@ static int get_dh_gex_group(struct sftp_kex *kex, uint32_t min,
       return -1;
     }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     DH_set0_pqg(kex->dh, dh_p, NULL, dh_g);
 #else
     kex->dh->p = dh_p;
@@ -2956,7 +3190,8 @@ static int write_dh_gex_group(struct ssh2_packet *pkt, struct sftp_kex *kex,
 
   sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_KEX_DH_GEX_GROUP);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_pqg(kex->dh, &dh_p, NULL, &dh_g);
 #else
   dh_p = kex->dh->p;
@@ -2993,8 +3228,8 @@ static int write_dh_gex_reply(struct ssh2_packet *pkt, struct sftp_kex *kex,
     uint32_t min, uint32_t pref, uint32_t max, int old_request) {
   const unsigned char *h, *hostkey_data, *hsig;
   unsigned char *buf, *ptr;
-  uint32_t bufsz, buflen, hlen = 0;
-  size_t dhlen, hostkey_datalen, hsiglen;
+  uint32_t bufsz, buflen, hlen = 0, hostkey_datalen = 0;
+  size_t dhlen, hsiglen = 0;
   BIGNUM *k = NULL, *dh_pub_key = NULL;
   int res;
 
@@ -3003,7 +3238,7 @@ static int write_dh_gex_reply(struct ssh2_packet *pkt, struct sftp_kex *kex,
   buf = palloc(kex_pool, dhlen);
 
   pr_trace_msg(trace_channel, 12, "computing DH key");
-  res = DH_compute_key((unsigned char *) buf, kex->e, kex->dh);
+  res = DH_compute_key(buf, kex->e, kex->dh);
   if (res < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error computing DH shared secret: %s", sftp_crypto_get_errors());
@@ -3011,11 +3246,12 @@ static int write_dh_gex_reply(struct ssh2_packet *pkt, struct sftp_kex *kex,
   }
 
   k = BN_new();
-  if (BN_bin2bn((unsigned char *) buf, res, k) == NULL) {
+  if (BN_bin2bn(buf, res, k) == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error converting DH shared secret to BN: %s", sftp_crypto_get_errors());
 
     pr_memscrub(buf, res);
+    BN_clear_free(k);
     return -1;
   }
 
@@ -3075,7 +3311,8 @@ static int write_dh_gex_reply(struct ssh2_packet *pkt, struct sftp_kex *kex,
   sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_KEX_DH_GEX_REPLY);
   sftp_msg_write_data(&buf, &buflen, hostkey_data, hostkey_datalen, TRUE);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DH_get0_key(kex->dh, &dh_pub_key, NULL);
 #else
   dh_pub_key = kex->dh->pub_key;
@@ -3106,7 +3343,7 @@ static int handle_kex_dh_gex(struct ssh2_packet *pkt, struct sftp_kex *kex,
 
     cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "DH_GEX_REQUEST"));
     cmd->arg = "(data)";
-    cmd->cmd_class = CL_AUTH;
+    cmd->cmd_class = CL_AUTH|CL_SSH;
 
   } else {
     pr_trace_msg(trace_channel, 9,
@@ -3114,7 +3351,7 @@ static int handle_kex_dh_gex(struct ssh2_packet *pkt, struct sftp_kex *kex,
 
     cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "DH_GEX_REQUEST_OLD"));
     cmd->arg = "(data)";
-    cmd->cmd_class = CL_AUTH;
+    cmd->cmd_class = CL_AUTH|CL_SSH;
   }
 
   res = read_dh_gex(pkt, &min, &pref, &max, old_request);
@@ -3150,7 +3387,7 @@ static int handle_kex_dh_gex(struct ssh2_packet *pkt, struct sftp_kex *kex,
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "DH_GEX_INIT"));
   cmd->arg = "(data)";
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   pr_trace_msg(trace_channel, 9, "reading DH_GEX_INIT message from client");
 
@@ -3250,7 +3487,7 @@ static int write_kexrsa_pubkey(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   uint32_t buflen, bufsz, buflen2, bufsz2, hostkey_datalen;
 
   hostkey_data = sftp_keys_get_hostkey_data(pkt->pool, kex->use_hostkey_type,
-    (size_t *) &hostkey_datalen);
+    &hostkey_datalen);
   if (hostkey_data == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error obtaining hostkey for KEXRSA key exchange: %s", strerror(errno));
@@ -3268,7 +3505,8 @@ static int write_kexrsa_pubkey(struct ssh2_packet *pkt, struct sftp_kex *kex) {
    */
   sftp_msg_write_string(&buf, &buflen, "ssh-rsa");
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   RSA_get0_key(kex->rsa, &rsa_n, &rsa_e, NULL);
 #else
   rsa_e = kex->rsa->e;
@@ -3297,8 +3535,8 @@ static int write_kexrsa_done(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   BIGNUM *rsa_e = NULL, *rsa_n = NULL;
   unsigned char *buf, *ptr, *buf2, *ptr2;
   const unsigned char *h, *hostkey_data, *hsig;
-  uint32_t buflen, bufsz, buflen2, bufsz2, hlen;
-  size_t hostkey_datalen, hsiglen;
+  uint32_t buflen, bufsz, buflen2, bufsz2, hlen, hostkey_datalen = 0;
+  size_t hsiglen;
 
   hostkey_data = sftp_keys_get_hostkey_data(pkt->pool, kex->use_hostkey_type,
     &hostkey_datalen);
@@ -3326,7 +3564,8 @@ static int write_kexrsa_done(struct ssh2_packet *pkt, struct sftp_kex *kex) {
    */
   sftp_msg_write_string(&buf2, &buflen2, "ssh-rsa");
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   RSA_get0_key(kex->rsa, &rsa_n, &rsa_e, NULL);
 #else
   rsa_e = kex->rsa->e;
@@ -3424,7 +3663,7 @@ static int handle_kex_rsa(struct sftp_kex *kex) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "KEXRSA_SECRET"));
   cmd->arg = "(data)";
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   pr_trace_msg(trace_channel, 9, "reading KEXRSA_SECRET message from client");
 
@@ -3458,18 +3697,365 @@ static int handle_kex_rsa(struct sftp_kex *kex) {
   return 0;
 }
 
-#ifdef PR_USE_OPENSSL_ECC
-static int read_ecdh_init(struct ssh2_packet *pkt, struct sftp_kex *kex) {
+#if defined(PR_USE_SODIUM) && defined(HAVE_SHA256_OPENSSL)
+static int generate_curve25519_keys(unsigned char *priv_key,
+    unsigned char *pub_key) {
+  static const unsigned char basepoint[CURVE25519_SIZE] = {9};
+  unsigned char zero_curve25519[CURVE25519_SIZE];
+  int res;
+
+  randombytes_buf(priv_key, CURVE25519_SIZE);
+  res = crypto_scalarmult_curve25519(pub_key, priv_key, basepoint);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error performing Curve25519 scalar multiplication");
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Check for all-zero public keys. */
+  sodium_memzero(zero_curve25519, CURVE25519_SIZE);
+  if (sodium_memcmp(pub_key, zero_curve25519, CURVE25519_SIZE) == 0) {
+    pr_trace_msg(trace_channel, 12,
+      "generated all-zero Curve25519 public key, trying again");
+    return generate_curve25519_keys(priv_key, pub_key);
+  }
+
+  return 0;
+}
+
+static int read_curve25519_init(struct ssh2_packet *pkt, struct sftp_kex *kex) {
+  unsigned char zero_curve25519[CURVE25519_SIZE];
+  unsigned char *client_curve25519;
   unsigned char *buf;
-  uint32_t buflen;
-  const EC_GROUP *curve;
-  EC_POINT *point;
+  uint32_t buflen, data_len;
+  char *data;
 
   buf = pkt->payload;
   buflen = pkt->payload_len;
 
-  curve = EC_KEY_get0_group(kex->ec);
-
+  data = sftp_msg_read_string(pkt->pool, &buf, &buflen);
+  data_len = strlen(data);
+  if (data_len != CURVE25519_SIZE) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "rejecting invalid length (%lu bytes) client Curve25519 key",
+      (unsigned long) data_len);
+    errno = EINVAL;
+    return -1;
+  }
+
+  client_curve25519 = (unsigned char *) data;
+
+  /* Watch for all-zero public keys, and reject them. */
+  sodium_memzero(zero_curve25519, CURVE25519_SIZE);
+  if (sodium_memcmp(client_curve25519, zero_curve25519, CURVE25519_SIZE) == 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "rejecting invalid (all-zero) client Curve25519 key");
+    errno = EINVAL;
+    return -1;
+  }
+
+  kex->client_curve25519 = client_curve25519;
+  return 0;
+}
+
+static int get_curve25519_shared_key(unsigned char *shared_key,
+    unsigned char *client_curve25519, unsigned char *server_key) {
+  int res;
+
+  res = crypto_scalarmult_curve25519(shared_key, server_key, client_curve25519);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error performing Curve25519 scalar multiplication");
+    errno = EINVAL;
+    return -1;
+  }
+
+  return CURVE25519_SIZE;
+}
+
+static const unsigned char *calculate_curve25519_h(struct sftp_kex *kex,
+    const unsigned char *hostkey_data, uint32_t hostkey_datalen,
+    const BIGNUM *k, unsigned char *client_curve25519,
+    unsigned char *server_curve25519, uint32_t *hlen) {
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  EVP_MD_CTX ctx;
+#endif /* prior to OpenSSL-1.1.0 */
+  EVP_MD_CTX *pctx;
+  unsigned char *buf, *ptr;
+  uint32_t buflen, bufsz;
+
+  bufsz = buflen = 4096;
+
+  /* XXX Is this buffer large enough? Too large? */
+  ptr = buf = sftp_msg_getbuf(kex_pool, bufsz);
+
+  /* Write all of the data into the buffer in the SSH2 format, and hash it.
+   * The ordering of these fields is described in RFC5656.
+   */
+
+  /* First, the version strings */
+  sftp_msg_write_string(&buf, &buflen, kex->client_version);
+  sftp_msg_write_string(&buf, &buflen, kex->server_version);
+
+  /* Client's KEXINIT */
+  sftp_msg_write_int(&buf, &buflen, kex->client_kexinit_payload_len + 1);
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_KEXINIT);
+  sftp_msg_write_data(&buf, &buflen, kex->client_kexinit_payload,
+    kex->client_kexinit_payload_len, FALSE);
+
+  /* Server's KEXINIT */
+  sftp_msg_write_int(&buf, &buflen, kex->server_kexinit_payload_len + 1);
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_KEXINIT);
+  sftp_msg_write_data(&buf, &buflen, kex->server_kexinit_payload,
+    kex->server_kexinit_payload_len, FALSE);
+
+  /* Hostkey data */
+  sftp_msg_write_data(&buf, &buflen, hostkey_data, hostkey_datalen, TRUE);
+
+  /* Client's key */
+  sftp_msg_write_data(&buf, &buflen, client_curve25519, CURVE25519_SIZE, TRUE);
+
+  /* Server's key */
+  sftp_msg_write_data(&buf, &buflen, server_curve25519, CURVE25519_SIZE, TRUE);
+
+  /* Shared secret */
+  sftp_msg_write_mpint(&buf, &buflen, k);
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+    pctx = EVP_MD_CTX_new();
+#else
+    pctx = &ctx;
+#endif /* OpenSSL-1.1.0 and later */
+
+  /* In OpenSSL 0.9.6, many of the EVP_Digest* functions returned void, not
+   * int.  Without these ugly OpenSSL version preprocessor checks, the
+   * compiler will error out with "void value not ignored as it ought to be".
+   */
+
+#if OPENSSL_VERSION_NUMBER >= 0x000907000L
+  if (EVP_DigestInit(pctx, kex->hash) != 1) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error initializing message digest: %s", sftp_crypto_get_errors());
+    BN_clear_free(kex->e);
+    kex->e = NULL;
+    pr_memscrub(ptr, bufsz);
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+    EVP_MD_CTX_free(pctx);
+# endif /* OpenSSL-1.1.0 and later */
+    return NULL;
+  }
+#else
+  EVP_DigestInit(pctx, kex->hash);
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x000907000L
+  if (EVP_DigestUpdate(pctx, ptr, (bufsz - buflen)) != 1) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error updating message digest: %s", sftp_crypto_get_errors());
+    BN_clear_free(kex->e);
+    kex->e = NULL;
+    pr_memscrub(ptr, bufsz);
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+    EVP_MD_CTX_free(pctx);
+# endif /* OpenSSL-1.1.0 and later */
+    return NULL;
+  }
+#else
+  EVP_DigestUpdate(pctx, ptr, (bufsz - buflen));
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x000907000L
+  if (EVP_DigestFinal(pctx, kex_digest_buf, hlen) != 1) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error finalizing message digest: %s", sftp_crypto_get_errors());
+    BN_clear_free(kex->e);
+    kex->e = NULL;
+    pr_memscrub(ptr, bufsz);
+# if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+    EVP_MD_CTX_free(pctx);
+# endif /* OpenSSL-1.1.0 and later */
+    return NULL;
+  }
+#else
+  EVP_DigestFinal(pctx, kex_digest_buf, hlen);
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000LL
+  EVP_MD_CTX_free(pctx);
+#endif /* OpenSSL-1.1.0 and later */
+  BN_clear_free(kex->e);
+  kex->e = NULL;
+  pr_memscrub(ptr, bufsz);
+
+  return kex_digest_buf;
+}
+
+static int write_curve25519_reply(struct ssh2_packet *pkt,
+    struct sftp_kex *kex) {
+  const unsigned char *h, *hostkey_data, *hsig;
+  unsigned char *buf, *ptr;
+  unsigned char server_curve25519[CURVE25519_SIZE];
+  unsigned char server_key[CURVE25519_SIZE];
+  uint32_t bufsz, buflen, hlen = 0, hostkey_datalen = 0;
+  size_t hsiglen;
+  BIGNUM *k = NULL;
+  int res;
+
+  if (generate_curve25519_keys(server_key, server_curve25519) < 0) {
+    return -1;
+  }
+
+  /* Compute the shared secret. */
+  buf = palloc(kex_pool, CURVE25519_SIZE);
+
+  pr_trace_msg(trace_channel, 12, "computing Curve25519 key");
+  res = get_curve25519_shared_key((unsigned char *) buf, kex->client_curve25519,
+    server_key);
+  if (res < 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error computing Curve25519 shared secret: %s", strerror(errno));
+    return -1;
+  }
+
+  k = BN_new();
+  if (k == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error allocating new BIGNUM: %s", sftp_crypto_get_errors());
+    pr_memscrub(buf, res);
+    return -1;
+  }
+
+  if (BN_bin2bn((unsigned char *) buf, res, k) == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error converting Curve25519 shared secret to BN: %s",
+      sftp_crypto_get_errors());
+    pr_memscrub(buf, res);
+    return -1;
+  }
+
+  pr_memscrub(buf, res);
+  kex->k = k;
+
+  /* Get the hostkey data; it will be part of the data we hash in order
+   * to create the session key.
+   */
+  hostkey_data = sftp_keys_get_hostkey_data(pkt->pool, kex->use_hostkey_type,
+    &hostkey_datalen);
+  if (hostkey_data == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error converting hostkey for signing: %s", strerror(errno));
+
+    BN_clear_free(kex->k);
+    kex->k = NULL;
+    return -1;
+  }
+
+  /* Calculate H */
+  h = calculate_curve25519_h(kex, hostkey_data, hostkey_datalen, k,
+    kex->client_curve25519, server_curve25519, &hlen);
+  if (h == NULL) {
+    pr_memscrub((char *) hostkey_data, hostkey_datalen);
+    BN_clear_free(kex->k);
+    kex->k = NULL;
+    return -1;
+  }
+
+  kex->h = palloc(pkt->pool, hlen);
+  kex->hlen = hlen;
+  memcpy((char *) kex->h, h, kex->hlen);
+
+  /* Save H as the session ID */
+  sftp_session_set_id(h, hlen);
+
+  /* Sign H with our hostkey */
+  hsig = sftp_keys_sign_data(pkt->pool, kex->use_hostkey_type, h, hlen,
+    &hsiglen);
+  if (hsig == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "error signing H");
+    pr_memscrub((char *) hostkey_data, hostkey_datalen);
+    BN_clear_free(kex->k);
+    kex->k = NULL;
+    return -1;
+  }
+
+  /* XXX Is this large enough?  Too large? */
+  buflen = bufsz = 4096;
+  ptr = buf = palloc(pkt->pool, bufsz);
+
+  sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_KEX_ECDH_REPLY);
+  sftp_msg_write_data(&buf, &buflen, hostkey_data, hostkey_datalen, TRUE);
+  sftp_msg_write_data(&buf, &buflen, server_curve25519, CURVE25519_SIZE, TRUE);
+  sftp_msg_write_data(&buf, &buflen, hsig, hsiglen, TRUE);
+
+  /* Scrub any sensitive data when done */
+  pr_memscrub((char *) server_key, CURVE25519_SIZE);
+  pr_memscrub((char *) hostkey_data, hostkey_datalen);
+  pr_memscrub((char *) hsig, hsiglen);
+
+  pkt->payload = ptr;
+  pkt->payload_len = (bufsz - buflen);
+
+  return 0;
+}
+
+static int handle_kex_curve25519(struct ssh2_packet *pkt,
+    struct sftp_kex *kex) {
+  int res;
+  cmd_rec *cmd;
+  const char *req;
+
+  req = "ECDH_INIT";
+  cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, req));
+  cmd->arg = "(data)";
+  cmd->cmd_class = CL_AUTH|CL_SSH;
+
+  pr_trace_msg(trace_channel, 9, "reading %s message from client", req);
+
+  res = read_curve25519_init(pkt, kex);
+  if (res < 0) {
+    pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    destroy_pool(pkt->pool);
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, NULL);
+  }
+
+  pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
+  destroy_pool(pkt->pool);
+
+  /* Send our key exchange reply. */
+  pkt = sftp_ssh2_packet_create(kex_pool);
+  res = write_curve25519_reply(pkt, kex);
+  if (res < 0) {
+    destroy_pool(pkt->pool);
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, NULL);
+  }
+
+  pr_trace_msg(trace_channel, 9, "writing %s message to client", req);
+
+  res = sftp_ssh2_packet_write(sftp_conn->wfd, pkt);
+  if (res < 0) {
+    destroy_pool(pkt->pool);
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, NULL);
+  }
+
+  destroy_pool(pkt->pool);
+  return 0;
+}
+#endif /* PR_USE_SODIUM and HAVE_SHA256_OPENSSL */
+
+#ifdef PR_USE_OPENSSL_ECC
+static int read_ecdh_init(struct ssh2_packet *pkt, struct sftp_kex *kex) {
+  unsigned char *buf;
+  uint32_t buflen;
+  const EC_GROUP *curve;
+  EC_POINT *point;
+
+  buf = pkt->payload;
+  buflen = pkt->payload_len;
+
+  curve = EC_KEY_get0_group(kex->ec);
+
   point = EC_POINT_new(curve);
   if (point == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -3503,8 +4089,8 @@ static int write_ecdh_reply(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   const unsigned char *h;
   const unsigned char *hostkey_data, *hsig;
   unsigned char *buf, *ptr;
-  uint32_t bufsz, buflen, hlen = 0;
-  size_t ecdhlen, hostkey_datalen, hsiglen;
+  uint32_t bufsz, buflen, hlen = 0, hostkey_datalen = 0;
+  size_t ecdhlen, hsiglen;
   BIGNUM *k = NULL;
   int res;
 
@@ -3521,7 +4107,7 @@ static int write_ecdh_reply(struct ssh2_packet *pkt, struct sftp_kex *kex) {
     return -1;
   }
 
-  if (res != ecdhlen) {
+  if ((size_t) res != ecdhlen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "computed ECDH shared secret length (%d) does not match needed length "
       "(%lu), rejecting", res, (unsigned long) ecdhlen);
@@ -3617,7 +4203,7 @@ static int handle_kex_ecdh(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   req = "ECDH_INIT";
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, req));
   cmd->arg = "(data)";
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   pr_trace_msg(trace_channel, 9, "reading %s message from client", req);
 
@@ -3646,6 +4232,7 @@ static int handle_kex_ecdh(struct ssh2_packet *pkt, struct sftp_kex *kex) {
   if (finish_ecdh(kex) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error finishing ECDH key: %s", strerror(errno));
+    destroy_pool(pkt->pool);
     return -1;
   }
 
@@ -3808,7 +4395,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "KEXINIT"));
   cmd->arg = "(data)";
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   pr_trace_msg(trace_channel, 9, "reading KEXINIT message from client");
 
@@ -3823,6 +4410,7 @@ int sftp_kex_handle(struct ssh2_packet *pkt) {
 
   pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
   destroy_pool(pkt->pool);
+  cmd = NULL;
 
   pr_trace_msg(trace_channel, 9,
     "determining shared algorithms for SSH session");
@@ -3949,6 +4537,12 @@ int sftp_kex_handle(struct ssh2_packet *pkt) {
         /* This handles the case of SFTP_SSH2_MSG_KEX_DH_GEX_REQUEST_OLD as
          * well; that ID has the same value as the KEX_DH_INIT ID.
          */
+#if defined(PR_USE_SODIUM) && defined(HAVE_SHA256_OPENSSL)
+        if (kex->use_curve25519) {
+          res = handle_kex_curve25519(pkt, kex);
+
+        } else
+#endif /* PR_USE_SODIUM and HAVE_SHA256_OPENSSL */
 #ifdef PR_USE_OPENSSL_ECC
         if (kex->use_ecdh) {
           res = handle_kex_ecdh(pkt, kex);
@@ -3993,6 +4587,10 @@ int sftp_kex_handle(struct ssh2_packet *pkt) {
         SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_PROTOCOL_ERROR, NULL);
     }
 
+    /* Note: All of the above handle_kex_*() functions are REQUIRED to have
+     * destroyed the pkt->pool themselves, thus we do NOT need to do it here.
+     */
+
   } else {
     res = handle_kex_rsa(kex);
     if (res < 0) {
@@ -4065,10 +4663,11 @@ int sftp_kex_handle(struct ssh2_packet *pkt) {
 
   cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "NEWKEYS"));
   cmd->arg = "";
-  cmd->cmd_class = CL_AUTH;
+  cmd->cmd_class = CL_AUTH|CL_SSH;
 
   pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
   destroy_pool(pkt->pool);
+  cmd = NULL;
 
   /* Reset this flag for the next time through. */
   kex_sent_kexinit = FALSE;
@@ -4078,11 +4677,27 @@ int sftp_kex_handle(struct ssh2_packet *pkt) {
 }
 
 int sftp_kex_free(void) {
+  struct sftp_kex *first_kex, *rekey_kex;
+
   if (kex_dhparams_fp != NULL) {
     (void) fclose(kex_dhparams_fp);
     kex_dhparams_fp = NULL;
   }
 
+  /* destroy_kex() will set the kex_first_kex AND kex_rekey_kex pointers to
+   * null, so we need to keep our own copies of those pointers here.
+   */
+  first_kex = kex_first_kex;
+  rekey_kex = kex_rekey_kex;
+
+  if (first_kex != NULL) {
+    destroy_kex(first_kex);
+  }
+
+  if (rekey_kex != NULL) {
+    destroy_kex(rekey_kex);
+  }
+
   if (kex_pool) {
     destroy_pool(kex_pool);
     kex_pool = NULL;
@@ -4188,7 +4803,15 @@ int sftp_kex_rekey(void) {
 
   pr_trace_msg(trace_channel, 17, "sending rekey KEXINIT");
 
-  sftp_sess_state |= SFTP_SESS_STATE_REKEYING;
+  /* Some SSH2 clients are very particular about rekeying, and do NOT want
+   * other data while they are rekeying.  Other clients are more forgiving.
+   * For the strict clients, we set the REKEYING flag here, such that the
+   * Channel API will buffer up its responses until the rekeying completes.
+   */
+  if (sftp_interop_supports_feature(SFTP_SSH2_FEAT_NO_DATA_WHILE_REKEYING)) {
+    sftp_sess_state |= SFTP_SESS_STATE_REKEYING;
+  }
+
   sftp_kex_init(NULL, NULL);
 
   kex_rekey_kex = create_kex(kex_pool);
diff --git a/contrib/mod_sftp/kex.h b/contrib/mod_sftp/kex.h
index 1e8306d..6c43ed0 100644
--- a/contrib/mod_sftp/kex.h
+++ b/contrib/mod_sftp/kex.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp key exchange (kex)
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: kex.h,v 1.4 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_KEX_H
 #define MOD_SFTP_KEX_H
 
+#include "mod_sftp.h"
+
 int sftp_kex_handle(struct ssh2_packet *);
 int sftp_kex_init(const char *, const char *);
 int sftp_kex_free(void);
@@ -42,4 +40,4 @@ int sftp_kex_send_first_kexinit(void);
 #define SFTP_KEX_DH_GROUP_MIN	1024
 #define SFTP_KEX_DH_GROUP_MAX	8192
 
-#endif
+#endif /* MOD_SFTP_KEX_H */
diff --git a/contrib/mod_sftp/keys.c b/contrib/mod_sftp/keys.c
index c448341..19bf8ed 100644
--- a/contrib/mod_sftp/keys.c
+++ b/contrib/mod_sftp/keys.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp key mgmt (keys)
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -91,6 +91,20 @@ struct sftp_pkey_data {
   const char *prompt;
 };
 
+/* Default minimum key sizes, in BITS.  The RSA minimum of 768 bits comes from
+ * the OpenSSH-7.2 implementation.  And the others follow from that, based on
+ * the assumptions described here:
+ *   https://en.wikipedia.org/wiki/Key_size#Asymmetric_algorithm_key_lengths
+ *   http://www.emc.com/emc-plus/rsa-labs/standards-initiatives/key-size.htm
+ *
+ * Note that the RSA size refers to the size of the modulus.  The DSA size
+ * refers to the size of the modulus.  The EC size refers to the minimum
+ * order of the base point on the elliptic curve.
+ */
+static int keys_rsa_min_nbits = 768;
+static int keys_dsa_min_nbits = 384;
+static int keys_ec_min_nbits = 160;
+
 static const char *trace_channel = "ssh2";
 
 static void prepare_provider_fds(int stdout_fd, int stderr_fd) {
@@ -129,8 +143,12 @@ static void prepare_provider_fds(int stdout_fd, int stderr_fd) {
 # elif defined(RLIMIT_OFILE)
   if (getrlimit(RLIMIT_OFILE, &rlim) < 0) {
 # endif
-    pr_log_debug(DEBUG0, MOD_SFTP_VERSION ": getrlimit error: %s",
-      strerror(errno));
+    /* Ignore ENOSYS (and EPERM, since some libc's use this as ENOSYS). */
+    if (errno != ENOSYS &&
+        errno != EPERM) {
+      pr_log_debug(DEBUG0, MOD_SFTP_VERSION ": getrlimit error: %s",
+        strerror(errno));
+    }
 
     /* Pick some arbitrary high number. */
     nfiles = 255;
@@ -230,17 +248,20 @@ static int exec_passphrase_provider(server_rec *s, char *buf, int buflen,
   sigemptyset(&sa_ignore.sa_mask);
   sa_ignore.sa_flags = 0;
 
-  if (sigaction(SIGINT, &sa_ignore, &sa_intr) < 0)
+  if (sigaction(SIGINT, &sa_ignore, &sa_intr) < 0) {
     return -1;
+  }
 
-  if (sigaction(SIGQUIT, &sa_ignore, &sa_quit) < 0)
+  if (sigaction(SIGQUIT, &sa_ignore, &sa_quit) < 0) {
     return -1;
+  }
 
   sigemptyset(&set_chldmask);
   sigaddset(&set_chldmask, SIGCHLD);
 
-  if (sigprocmask(SIG_BLOCK, &set_chldmask, &set_save) < 0)
+  if (sigprocmask(SIG_BLOCK, &set_chldmask, &set_save) < 0) {
     return -1;
+  }
 
   prepare_provider_pipes(stdout_pipe, stderr_pipe);
 
@@ -396,12 +417,15 @@ static int exec_passphrase_provider(server_rec *s, char *buf, int buflen,
         if (FD_ISSET(stdout_pipe[0], &readfds)) {
           res = read(stdout_pipe[0], buf, buflen);
           if (res > 0) {
-              while (res &&
-                     (buf[res-1] == '\r' ||
-                      buf[res-1] == '\n')) {
-                res--;
-              }
-              buf[res] = '\0';
+            buf[buflen-1] = '\0';
+
+            while (res &&
+                   (buf[res-1] == '\r' ||
+                    buf[res-1] == '\n')) {
+              pr_signals_handle();
+              res--;
+            }
+            buf[res] = '\0';
 
           } else if (res < 0) {
             pr_log_debug(DEBUG2, MOD_SFTP_VERSION
@@ -411,11 +435,14 @@ static int exec_passphrase_provider(server_rec *s, char *buf, int buflen,
         }
 
         if (FD_ISSET(stderr_pipe[0], &readfds)) {
-          int stderrlen;
-          char stderrbuf[PIPE_BUF];
+          long stderrlen, stderrsz;
+          char *stderrbuf;
+          pool *tmp_pool = make_sub_pool(s->pool);
+
+          stderrbuf = pr_fsio_getpipebuf(tmp_pool, stderr_pipe[0], &stderrsz);
+          memset(stderrbuf, '\0', stderrsz);
 
-          memset(stderrbuf, '\0', sizeof(stderrbuf));
-          stderrlen = read(stderr_pipe[0], stderrbuf, sizeof(stderrbuf)-1);
+          stderrlen = read(stderr_pipe[0], stderrbuf, stderrsz-1);
           if (stderrlen > 0) {
             while (stderrlen &&
                    (stderrbuf[stderrlen-1] == '\r' ||
@@ -432,6 +459,9 @@ static int exec_passphrase_provider(server_rec *s, char *buf, int buflen,
               ": error reading stderr from '%s': %s",
               passphrase_provider, strerror(errno));
           }
+
+          destroy_pool(tmp_pool);
+          tmp_pool = NULL;
         }
       }
 
@@ -440,14 +470,17 @@ static int exec_passphrase_provider(server_rec *s, char *buf, int buflen,
   }
 
   /* Restore the previous signal actions. */
-  if (sigaction(SIGINT, &sa_intr, NULL) < 0)
+  if (sigaction(SIGINT, &sa_intr, NULL) < 0) {
     return -1;
+  }
 
-  if (sigaction(SIGQUIT, &sa_quit, NULL) < 0)
+  if (sigaction(SIGQUIT, &sa_quit, NULL) < 0) {
     return -1;
+  }
 
-  if (sigprocmask(SIG_SETMASK, &set_save, NULL) < 0)
+  if (sigprocmask(SIG_SETMASK, &set_save, NULL) < 0) {
     return -1;
+  }
 
   if (WIFSIGNALED(status)) {
     pr_log_debug(DEBUG2, MOD_SFTP_VERSION ": '%s' died from signal %d",
@@ -480,7 +513,7 @@ static char *get_page(size_t sz, void **ptr) {
   void *d;
   long pagesz = get_pagesz(), p;
 
-  d = malloc(sz + (pagesz-1));
+  d = calloc(1, sz + (pagesz-1));
   if (d == NULL) {
     pr_log_pri(PR_LOG_ALERT, MOD_SFTP_VERSION ": Out of memory!");
     exit(1);
@@ -525,6 +558,8 @@ static int get_passphrase_cb(char *buf, int buflen, int rwflag, void *d) {
          continue;
       }
 
+      /* Ensure that the buffer is NUL-terminated. */
+      buf[buflen-1] = '\0';
       pwlen = strlen(buf);
       if (pwlen < 1) {
         fprintf(stderr, "Error: passphrase must be at least one character\n");
@@ -547,7 +582,11 @@ static int get_passphrase_cb(char *buf, int buflen, int rwflag, void *d) {
         passphrase_provider, strerror(errno));
 
     } else {
-      size_t pwlen = strlen(buf);
+      size_t pwlen;
+      /* Ensure that the buffer is NUL-terminated. */
+      buf[buflen-1] = '\0';
+
+      pwlen = strlen(buf);
 
       sstrncpy(pdata->buf, buf, pdata->bufsz);
       pdata->buflen = pwlen;
@@ -575,9 +614,11 @@ static int get_passphrase(struct sftp_pkey *k, const char *path) {
   register unsigned int attempt;
 
   memset(prompt, '\0', sizeof(prompt));
-  snprintf(prompt, sizeof(prompt)-1, "Host key for the %s#%d (%s) server: ",
+  res = snprintf(prompt, sizeof(prompt)-1,
+    "Host key for the %s#%d (%s) server: ",
     pr_netaddr_get_ipstr(k->server->addr), k->server->ServerPort,
     k->server->ServerName);
+  prompt[res] = '\0';
   prompt[sizeof(prompt)-1] = '\0';
 
   PRIVS_ROOT
@@ -611,6 +652,11 @@ static int get_passphrase(struct sftp_pkey *k, const char *path) {
     return -1;
   }
 
+  /* As the file contains sensitive data, we do not want it lingering
+   * around in stdio buffers.
+   */
+  (void) setvbuf(fp, NULL, _IONBF, 0);
+
   k->host_pkey = get_page(PEM_BUFSIZE, &k->host_pkey_ptr);
   if (k->host_pkey == NULL) {
     pr_log_pri(PR_LOG_ALERT, MOD_SFTP_VERSION ": Out of memory!");
@@ -766,15 +812,23 @@ static int pkey_cb(char *buf, int buflen, int rwflag, void *d) {
   return 0;
 }
 
-static int has_req_perms(int fd) {
+static int has_req_perms(int fd, const char *path) {
   struct stat st;
 
-  if (fstat(fd, &st) < 0)
+  if (fstat(fd, &st) < 0) {
     return -1;
+  }
 
   if (st.st_mode & (S_IRWXG|S_IRWXO)) {
-    errno = EACCES;
-    return -1;
+    if (!(sftp_opts & SFTP_OPT_INSECURE_HOSTKEY_PERMS)) {
+      errno = EACCES;
+      return -1;
+    }
+
+    pr_log_pri(PR_LOG_INFO, MOD_SFTP_VERSION
+      "notice: the permissions on SFTPHostKey '%s' (%04o) allow "
+      "group-readable and/or world-readable access, increasing chances of "
+      "system users reading the private key", path, st.st_mode);
   }
 
   return 0;
@@ -809,7 +863,8 @@ static EVP_PKEY *get_pkey_from_data(pool *p, unsigned char *pkey_data,
     rsa_e = sftp_msg_read_mpint(p, &pkey_data, &pkey_datalen);
     rsa_n = sftp_msg_read_mpint(p, &pkey_data, &pkey_datalen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     RSA_set0_key(rsa, rsa_n, rsa_e, NULL);
 #else
     rsa->e = rsa_e;
@@ -825,6 +880,7 @@ static EVP_PKEY *get_pkey_from_data(pool *p, unsigned char *pkey_data,
     }
 
   } else if (strncmp(pkey_type, "ssh-dss", 8) == 0) {
+#if !defined(OPENSSL_NO_DSA)
     DSA *dsa;
     BIGNUM *dsa_p, *dsa_q, *dsa_g, *dsa_pub_key;
 
@@ -848,7 +904,8 @@ static EVP_PKEY *get_pkey_from_data(pool *p, unsigned char *pkey_data,
     dsa_g = sftp_msg_read_mpint(p, &pkey_data, &pkey_datalen);
     dsa_pub_key = sftp_msg_read_mpint(p, &pkey_data, &pkey_datalen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     DSA_set0_pqg(dsa, dsa_p, dsa_q, dsa_g);
     DSA_set0_key(dsa, dsa_pub_key, NULL);
 #else
@@ -865,6 +922,12 @@ static EVP_PKEY *get_pkey_from_data(pool *p, unsigned char *pkey_data,
       EVP_PKEY_free(pkey);
       return NULL;
     }
+#else
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "unsupported public key algorithm '%s'", pkey_type);
+    errno = EINVAL;
+    return NULL;
+#endif /* !OPENSSL_NO_DSA */
 
 #ifdef PR_USE_OPENSSL_ECC
   } else if (strncmp(pkey_type, "ecdsa-sha2-nistp256", 20) == 0 ||
@@ -1299,10 +1362,32 @@ int sftp_keys_validate_ecdsa_params(const EC_GROUP *group,
 }
 #endif /* PR_USE_OPENSSL_ECC */
 
+#ifdef SFTP_DEBUG_KEYS
+static void debug_rsa_key(pool *p, const char *label, RSA *rsa) {
+  BIO *bio = NULL;
+  char *data;
+  long datalen;
+
+  bio = BIO_new(BIO_s_mem());
+  RSA_print(bio, rsa, 0);
+  BIO_flush(bio);
+  datalen = BIO_get_mem_data(bio, &data);
+  if (data != NULL &&
+      datalen > 0) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "%s",label);
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "%.*s",
+      (int) datalen, data);
+  }
+
+  BIO_free(bio);
+}
+#endif
+
 static int get_pkey_type(EVP_PKEY *pkey) {
   int pkey_type;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESS)
   pkey_type = EVP_PKEY_id(pkey);
 #else
   pkey_type = EVP_PKEY_type(pkey->type);
@@ -1312,43 +1397,73 @@ static int get_pkey_type(EVP_PKEY *pkey) {
 }
 
 /* Compare a "blob" of pubkey data sent by the client for authentication
- * with a file pubkey (from an RFC4716 formatted file).  Returns -1 if
+ * with a local file pubkey (from an RFC4716 formatted file).  Returns -1 if
  * there was an error, TRUE if the keys are equals, and FALSE if not.
  */
-int sftp_keys_compare_keys(pool *p, unsigned char *client_pubkey_data,
-    uint32_t client_pubkey_datalen, unsigned char *file_pubkey_data,
-    uint32_t file_pubkey_datalen) {
-  EVP_PKEY *client_pkey, *file_pkey;
+int sftp_keys_compare_keys(pool *p,
+    unsigned char *remote_pubkey_data, uint32_t remote_pubkey_datalen,
+    unsigned char *local_pubkey_data, uint32_t local_pubkey_datalen) {
+  EVP_PKEY *remote_pkey, *local_pkey;
   int res = -1;
 
-  if (client_pubkey_data == NULL ||
-      file_pubkey_data == NULL) {
+  if (remote_pubkey_data == NULL ||
+      local_pubkey_data == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  client_pkey = get_pkey_from_data(p, client_pubkey_data,
-    client_pubkey_datalen);
-  if (client_pkey == NULL) {
+  remote_pkey = get_pkey_from_data(p, remote_pubkey_data,
+    remote_pubkey_datalen);
+  if (remote_pkey == NULL) {
     return -1;
   }
 
-  file_pkey = get_pkey_from_data(p, file_pubkey_data, file_pubkey_datalen);
-  if (file_pkey == NULL) {
+  local_pkey = get_pkey_from_data(p, local_pubkey_data, local_pubkey_datalen);
+  if (local_pkey == NULL) {
+    int xerrno = errno;
+
+    EVP_PKEY_free(remote_pkey);
+
+    errno = xerrno;
     return -1;
   }
 
-  if (get_pkey_type(client_pkey) == get_pkey_type(file_pkey)) {
-    switch (get_pkey_type(client_pkey)) {
+  if (get_pkey_type(remote_pkey) == get_pkey_type(local_pkey)) {
+    switch (get_pkey_type(remote_pkey)) {
       case EVP_PKEY_RSA: {
         RSA *remote_rsa = NULL, *local_rsa = NULL;
         BIGNUM *remote_rsa_e = NULL, *local_rsa_e = NULL;
         BIGNUM *remote_rsa_n = NULL, *local_rsa_n = NULL;
 
-        remote_rsa = EVP_PKEY_get1_RSA(client_pkey);
-        local_rsa = EVP_PKEY_get1_RSA(file_pkey);
+        local_rsa = EVP_PKEY_get1_RSA(local_pkey);
+        if (keys_rsa_min_nbits > 0) {
+          int rsa_nbits;
+
+          rsa_nbits = RSA_size(local_rsa) * 8;
+          if (rsa_nbits < keys_rsa_min_nbits) {
+            (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+              "local RSA key size (%d bits) less than required "
+              "minimum (%d bits)", rsa_nbits, keys_rsa_min_nbits);
+            RSA_free(local_rsa);
+            EVP_PKEY_free(local_pkey);
+            EVP_PKEY_free(remote_pkey);
+
+            return FALSE;
+          }
+
+          pr_trace_msg(trace_channel, 19,
+            "comparing RSA keys using local RSA key (%d bits, min %d)", rsa_nbits, keys_rsa_min_nbits);
+        }
+
+        remote_rsa = EVP_PKEY_get1_RSA(remote_pkey);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#ifdef SFTP_DEBUG_KEYS
+        debug_rsa_key(p, "remote RSA key:", remote_rsa);
+        debug_rsa_key(p, "local RSA key:", local_rsa);
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
         RSA_get0_key(remote_rsa, &remote_rsa_n, &remote_rsa_e, NULL);
         RSA_get0_key(local_rsa, &local_rsa_n, &local_rsa_e, NULL);
 #else
@@ -1376,21 +1491,42 @@ int sftp_keys_compare_keys(pool *p, unsigned char *client_pubkey_data,
           }
         } 
 
-        RSA_free(local_rsa);
         RSA_free(remote_rsa);
+        RSA_free(local_rsa);
         break;
       }
 
+#if !defined(OPENSSL_NO_DSA)
       case EVP_PKEY_DSA: {
         DSA *remote_dsa = NULL, *local_dsa = NULL;
         BIGNUM *remote_dsa_p, *remote_dsa_q, *remote_dsa_g;
         BIGNUM *local_dsa_p, *local_dsa_q, *local_dsa_g;
         BIGNUM *remote_dsa_pub_key, *local_dsa_pub_key;
 
-        local_dsa = EVP_PKEY_get1_DSA(client_pkey);
-        remote_dsa = EVP_PKEY_get1_DSA(file_pkey);
+        local_dsa = EVP_PKEY_get1_DSA(local_pkey);
+        if (keys_dsa_min_nbits > 0) {
+          int dsa_nbits;
+
+          dsa_nbits = DSA_size(local_dsa) * 8;
+          if (dsa_nbits < keys_dsa_min_nbits) {
+            (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+              "local DSA key size (%d bits) less than required "
+              "minimum (%d bits)", dsa_nbits, keys_dsa_min_nbits);
+            DSA_free(local_dsa);
+            EVP_PKEY_free(local_pkey);
+            EVP_PKEY_free(remote_pkey);
+
+            return FALSE;
+          }
+
+          pr_trace_msg(trace_channel, 19,
+            "comparing DSA keys using local DSA key (%d bits)", dsa_nbits);
+        }
+
+        remote_dsa = EVP_PKEY_get1_DSA(remote_pkey);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
         DSA_get0_pqg(remote_dsa, &remote_dsa_p, &remote_dsa_q, &remote_dsa_g);
         DSA_get0_pqg(local_dsa, &local_dsa_p, &local_dsa_q, &local_dsa_g);
         DSA_get0_key(remote_dsa, &remote_dsa_pub_key, NULL);
@@ -1445,25 +1581,45 @@ int sftp_keys_compare_keys(pool *p, unsigned char *client_pubkey_data,
 
         break;
       }
+#endif /* !OPENSSL_NO_DSA */
 
 #ifdef PR_USE_OPENSSL_ECC
       case EVP_PKEY_EC: {
-        EC_KEY *client_ec, *file_ec;
+        EC_KEY *remote_ec, *local_ec;
+
+        local_ec = EVP_PKEY_get1_EC_KEY(local_pkey);
+        if (keys_ec_min_nbits > 0) {
+          int ec_nbits;
+
+          ec_nbits = EVP_PKEY_bits(local_pkey) * 8;
+          if (ec_nbits < keys_ec_min_nbits) {
+            (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+              "local EC key size (%d bits) less than required "
+              "minimum (%d bits)", ec_nbits, keys_ec_min_nbits);
+            EC_KEY_free(local_ec);
+            EVP_PKEY_free(local_pkey);
+            EVP_PKEY_free(remote_pkey);
+
+            return FALSE;
+          }
 
-        file_ec = EVP_PKEY_get1_EC_KEY(file_pkey);
-        client_ec = EVP_PKEY_get1_EC_KEY(client_pkey);
+          pr_trace_msg(trace_channel, 19,
+            "comparing EC keys using local EC key (%d bits)", ec_nbits);
+        }
+
+        remote_ec = EVP_PKEY_get1_EC_KEY(remote_pkey);
 
-        if (EC_GROUP_cmp(EC_KEY_get0_group(file_ec),
-            EC_KEY_get0_group(client_ec), NULL) != 0) {
+        if (EC_GROUP_cmp(EC_KEY_get0_group(local_ec),
+            EC_KEY_get0_group(remote_ec), NULL) != 0) {
           pr_trace_msg(trace_channel, 17, "%s",
             "ECC key mismatch: client-sent curve does not "
             "match local ECC curve");
           res = FALSE;
 
         } else {
-          if (EC_POINT_cmp(EC_KEY_get0_group(file_ec),
-              EC_KEY_get0_public_key(file_ec),
-              EC_KEY_get0_public_key(client_ec), NULL) != 0) {
+          if (EC_POINT_cmp(EC_KEY_get0_group(local_ec),
+              EC_KEY_get0_public_key(local_ec),
+              EC_KEY_get0_public_key(remote_ec), NULL) != 0) {
             pr_trace_msg(trace_channel, 17, "%s",
               "ECC key mismatch: client-sent public key 'Q' does not "
               "match local ECC public key 'Q'");
@@ -1474,8 +1630,8 @@ int sftp_keys_compare_keys(pool *p, unsigned char *client_pubkey_data,
           }
         }
 
-        EC_KEY_free(client_ec);
-        EC_KEY_free(file_ec);
+        EC_KEY_free(remote_ec);
+        EC_KEY_free(local_ec);
 
         break;
       }
@@ -1484,34 +1640,35 @@ int sftp_keys_compare_keys(pool *p, unsigned char *client_pubkey_data,
       default:
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "unable to compare %s keys: unsupported key type",
-          get_key_type_desc(get_pkey_type(client_pkey)));
+          get_key_type_desc(get_pkey_type(remote_pkey)));
         errno = ENOSYS;
         break;
     }
 
   } else {
     if (pr_trace_get_level(trace_channel) >= 17) {
-      const char *client_key_desc, *file_key_desc;
+      const char *remote_key_desc, *local_key_desc;
 
-      client_key_desc = get_key_type_desc(get_pkey_type(client_pkey));
-      file_key_desc = get_key_type_desc(get_pkey_type(file_pkey));
+      remote_key_desc = get_key_type_desc(get_pkey_type(remote_pkey));
+      local_key_desc = get_key_type_desc(get_pkey_type(local_pkey));
 
       pr_trace_msg(trace_channel, 17, "key mismatch: cannot compare %s key "
-        "(client-sent) with %s key (local)", client_key_desc, file_key_desc);
+        "(client-sent) with %s key (local)", remote_key_desc, local_key_desc);
     }
 
     res = FALSE;
   }
 
-  EVP_PKEY_free(client_pkey);
-  EVP_PKEY_free(file_pkey);
+  EVP_PKEY_free(remote_pkey);
+  EVP_PKEY_free(local_pkey);
 
   return res;
 }
 
 const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
     uint32_t key_datalen, int digest_algo) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -1532,6 +1689,13 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
       digest_name = "sha1";
       break;
 
+#ifdef HAVE_SHA256_OPENSSL
+    case SFTP_KEYS_FP_DIGEST_SHA256:
+      digest = EVP_sha256();
+      digest_name = "sha256";
+      break;
+#endif /* HAVE_SHA256_OPENSSL */
+
     default:
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "unsupported key fingerprint digest algorithm (%d)", digest_algo);
@@ -1539,7 +1703,8 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
       return NULL;
   }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -1555,7 +1720,8 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error initializing %s digest: %s", digest_name,
       sftp_crypto_get_errors());
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     errno = EPERM;
@@ -1569,7 +1735,8 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
   if (EVP_DigestUpdate(pctx, key_data, key_datalen) != 1) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error updating %s digest: %s", digest_name, sftp_crypto_get_errors());
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     errno = EPERM;
@@ -1585,6 +1752,10 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
   if (EVP_DigestFinal(pctx, fp_data, &fp_datalen) != 1) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error finishing %s digest: %s", digest_name, sftp_crypto_get_errors());
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
+    EVP_MD_CTX_free(pctx);
+# endif /* OpenSSL-1.1.0 and later */
     errno = EPERM;
     return NULL;
   }
@@ -1592,9 +1763,10 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
   EVP_DigestFinal(pctx, fp_data, &fp_datalen);
 #endif
 
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
-# endif /* OpenSSL-1.1.0 and later */
+#endif /* OpenSSL-1.1.0 and later */
 
   /* Now encode that digest in fp_data as hex characters. */
   fp = "";
@@ -1607,7 +1779,7 @@ const char *sftp_keys_get_fingerprint(pool *p, unsigned char *key_data,
     fp = pstrcat(p, fp, &c, NULL);
   }
   fp[strlen(fp)-1] = '\0';
-  
+
   return fp;
 }
 
@@ -2011,32 +2183,45 @@ static int load_file_hostkey(pool *p, const char *path) {
   if (fd < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error reading '%s': %s", path, strerror(xerrno));
+    errno = xerrno;
     return -1;
   }
 
-  if (has_req_perms(fd) < 0) {
-    if (errno == EACCES) {
+  if (has_req_perms(fd, path) < 0) {
+    xerrno = errno;
+
+    if (xerrno == EACCES) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "'%s' is accessible by group or world, which is not allowed", path);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error checking '%s' perms: %s", path, strerror(errno));
+        "error checking '%s' perms: %s", path, strerror(xerrno));
     }
 
-    close(fd);
+    (void) close(fd);
+    errno = xerrno;
     return -1;
   }
 
   /* OpenSSL's APIs prefer stdio file handles. */
   fp = fdopen(fd, "r");
   if (fp == NULL) {
+    xerrno = errno;
+
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error opening stdio fp on fd %d: %s", fd, strerror(errno));
-    close(fd);
+      "error opening stdio handle on fd %d: %s", fd, strerror(xerrno));
+    (void) close(fd);
+
+    errno = xerrno;
     return -1;
   }
 
+  /* As the file contains sensitive data, we do not want it lingering
+   * around in stdio buffers.
+   */
+  (void) setvbuf(fp, NULL, _IONBF, 0);
+
   if (server_pkey == NULL) {
     server_pkey = lookup_pkey();
   }
@@ -2086,7 +2271,7 @@ int sftp_keys_get_hostkey(pool *p, const char *path) {
 }
 
 const unsigned char *sftp_keys_get_hostkey_data(pool *p,
-    enum sftp_key_type_e key_type, size_t *datalen) {
+    enum sftp_key_type_e key_type, uint32_t *datalen) {
   unsigned char *buf = NULL, *ptr = NULL;
   uint32_t buflen = SFTP_DEFAULT_HOSTKEY_SZ;
 
@@ -2106,7 +2291,8 @@ const unsigned char *sftp_keys_get_hostkey_data(pool *p,
       ptr = buf = palloc(p, buflen);
       sftp_msg_write_string(&buf, &buflen, "ssh-rsa");
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       RSA_get0_key(rsa, &rsa_n, &rsa_e, NULL);
 #else
       rsa_e = rsa->e;
@@ -2119,6 +2305,7 @@ const unsigned char *sftp_keys_get_hostkey_data(pool *p,
       break;
     }
 
+#if !defined(OPENSSL_NO_DSA)
     case SFTP_KEY_DSA: {
       DSA *dsa;
       BIGNUM *dsa_p = NULL, *dsa_q = NULL, *dsa_g = NULL, *dsa_pub_key = NULL;
@@ -2134,7 +2321,8 @@ const unsigned char *sftp_keys_get_hostkey_data(pool *p,
       ptr = buf = palloc(p, buflen);
       sftp_msg_write_string(&buf, &buflen, "ssh-dss");
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       DSA_get0_pqg(dsa, &dsa_p, &dsa_q, &dsa_g);
       DSA_get0_key(dsa, &dsa_pub_key, NULL);
 #else
@@ -2151,6 +2339,7 @@ const unsigned char *sftp_keys_get_hostkey_data(pool *p,
       DSA_free(dsa);
       break;
     }
+#endif /* !OPENSSL_NO_DSA */
 
 #ifdef PR_USE_OPENSSL_ECC
     case SFTP_KEY_ECDSA_256: {
@@ -2236,15 +2425,79 @@ const unsigned char *sftp_keys_get_hostkey_data(pool *p,
    * we allocate out of the pool for writing the data in the first place.
    * Hence the copy.
    */
-  if (p) {
-    buf = palloc(p, *datalen);
-    memcpy(buf, ptr, *datalen);
+  buf = palloc(p, *datalen);
+  memcpy(buf, ptr, *datalen);
 
-    pr_memscrub(ptr, *datalen);
-    return buf;
+  pr_memscrub(ptr, *datalen);
+  return buf;
+}
+
+int sftp_keys_clear_dsa_hostkey(void) {
+  if (sftp_dsa_hostkey != NULL) {
+    if (sftp_dsa_hostkey->pkey != NULL) {
+      EVP_PKEY_free(sftp_dsa_hostkey->pkey);
+    }
+
+    sftp_dsa_hostkey = NULL;
+    return 0;
   }
 
-  return ptr;
+  errno = ENOENT;
+  return -1;
+}
+
+int sftp_keys_clear_ecdsa_hostkey(void) {
+#ifdef PR_USE_OPENSSL_ECC
+  int count = 0;
+
+  if (sftp_ecdsa256_hostkey != NULL) {
+    if (sftp_ecdsa256_hostkey->pkey != NULL) {
+      EVP_PKEY_free(sftp_ecdsa256_hostkey->pkey);
+    }
+
+    sftp_ecdsa256_hostkey = NULL;
+    count++;
+  }
+
+  if (sftp_ecdsa384_hostkey != NULL) {
+    if (sftp_ecdsa384_hostkey->pkey != NULL) {
+      EVP_PKEY_free(sftp_ecdsa384_hostkey->pkey);
+    }
+
+    sftp_ecdsa384_hostkey = NULL;
+    count++;
+  }
+
+  if (sftp_ecdsa521_hostkey != NULL) {
+    if (sftp_ecdsa521_hostkey->pkey != NULL) {
+      EVP_PKEY_free(sftp_ecdsa521_hostkey->pkey);
+    }
+
+    sftp_ecdsa521_hostkey = NULL;
+    count++;
+  }
+
+  if (count > 0) {
+    return 0;
+  }
+
+#endif /* PR_USE_OPENSSL_ECC */
+  errno = ENOENT;
+  return -1;
+}
+
+int sftp_keys_clear_rsa_hostkey(void) {
+  if (sftp_rsa_hostkey != NULL) {
+    if (sftp_rsa_hostkey->pkey != NULL) {
+      EVP_PKEY_free(sftp_rsa_hostkey->pkey);
+    }
+
+    sftp_rsa_hostkey = NULL;
+    return 0;
+  }
+
+  errno = ENOENT;
+  return -1;
 }
 
 int sftp_keys_have_dsa_hostkey(void) {
@@ -2352,7 +2605,8 @@ static const unsigned char *agent_sign_data(pool *p, const char *agent_path,
 static const unsigned char *rsa_sign_data(pool *p, const unsigned char *data,
     size_t datalen, size_t *siglen) {
   RSA *rsa;
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -2376,7 +2630,23 @@ static const unsigned char *rsa_sign_data(pool *p, const unsigned char *data,
     return NULL;
   }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+  if (keys_rsa_min_nbits > 0) {
+    int rsa_nbits;
+
+    rsa_nbits = RSA_size(rsa) * 8;
+    if (rsa_nbits < keys_rsa_min_nbits) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "RSA hostkey size (%d bits) less than required minimum (%d bits)",
+        rsa_nbits, keys_rsa_min_nbits);
+      RSA_free(rsa);
+
+      errno = EINVAL;
+      return NULL;
+    }
+  }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -2386,7 +2656,8 @@ static const unsigned char *rsa_sign_data(pool *p, const unsigned char *data,
   EVP_DigestUpdate(pctx, data, datalen);
   EVP_DigestFinal(pctx, dgst, &dgstlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
@@ -2433,12 +2704,14 @@ static const unsigned char *rsa_sign_data(pool *p, const unsigned char *data,
 #define SFTP_DSA_INTEGER_LEN			20
 #define SFTP_DSA_SIGNATURE_LEN			(SFTP_DSA_INTEGER_LEN * 2)
 
+#if !defined(OPENSSL_NO_DSA)
 static const unsigned char *dsa_sign_data(pool *p, const unsigned char *data,
     size_t datalen, size_t *siglen) {
   DSA *dsa;
   DSA_SIG *sig;
   BIGNUM *sig_r = NULL, *sig_s = NULL;
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -2462,7 +2735,23 @@ static const unsigned char *dsa_sign_data(pool *p, const unsigned char *data,
     return NULL;
   }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+  if (keys_dsa_min_nbits > 0) {
+    int dsa_nbits;
+
+    dsa_nbits = DSA_size(dsa) * 8;
+    if (dsa_nbits < keys_dsa_min_nbits) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "DSA hostkey size (%d bits) less than required minimum (%d bits)",
+        dsa_nbits, keys_dsa_min_nbits);
+      DSA_free(dsa);
+
+      errno = EINVAL;
+      return NULL;
+    }
+  }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -2472,7 +2761,8 @@ static const unsigned char *dsa_sign_data(pool *p, const unsigned char *data,
   EVP_DigestUpdate(pctx, data, datalen);
   EVP_DigestFinal(pctx, dgst, &dgstlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
@@ -2488,7 +2778,8 @@ static const unsigned char *dsa_sign_data(pool *p, const unsigned char *data,
   /* Got the signature, no need for the digest memory. */
   pr_memscrub(dgst, dgstlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   DSA_SIG_get0(&sig_r, &sig_s, sig);
 #else
   sig_r = sig->r;
@@ -2537,14 +2828,17 @@ static const unsigned char *dsa_sign_data(pool *p, const unsigned char *data,
   *siglen = (bufsz - buflen);
   return ptr;
 }
+#endif /* !OPENSSL_NO_DSA */
 
 #ifdef PR_USE_OPENSSL_ECC
 static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
     size_t datalen, size_t *siglen, int nid) {
+  EVP_PKEY *pkey = NULL;
   EC_KEY *ec = NULL;
   ECDSA_SIG *sig;
   BIGNUM *sig_r = NULL, *sig_s = NULL;
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -2568,6 +2862,7 @@ static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
         return NULL;
       }
 
+      pkey = sftp_ecdsa256_hostkey->pkey;
       md = EVP_sha256();
       break;
 
@@ -2585,6 +2880,7 @@ static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
         return NULL;
       }
 
+      pkey = sftp_ecdsa384_hostkey->pkey;
       md = EVP_sha384();
       break;
 
@@ -2602,6 +2898,7 @@ static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
         return NULL;
       }
 
+      pkey = sftp_ecdsa521_hostkey->pkey;
       md = EVP_sha512();
       break;
 
@@ -2611,10 +2908,26 @@ static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
       return NULL;
   }
 
+  if (keys_ec_min_nbits > 0) {
+    int ec_nbits;
+
+    ec_nbits = EVP_PKEY_bits(pkey) * 8;
+    if (ec_nbits < keys_ec_min_nbits) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "EC hostkey size (%d bits) less than required minimum (%d bits)",
+        ec_nbits, keys_ec_min_nbits);
+      EC_KEY_free(ec);
+
+      errno = EINVAL;
+      return NULL;
+    }
+  }
+
   buflen = bufsz = SFTP_MAX_SIG_SZ;
   ptr = buf = sftp_msg_getbuf(p, bufsz);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   pctx = EVP_MD_CTX_new();
 #else
   pctx = &ctx;
@@ -2624,7 +2937,8 @@ static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
   EVP_DigestUpdate(pctx, data, datalen);
   EVP_DigestFinal(pctx, dgst, &dgstlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
@@ -2644,7 +2958,8 @@ static const unsigned char *ecdsa_sign_data(pool *p, const unsigned char *data,
    * selected, so we do no sanity checking of their lengths.
    */
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   ECDSA_SIG_get0(&sig_r, &sig_s, sig);
 #else
   sig_r = sig->r;
@@ -2703,9 +3018,11 @@ const unsigned char *sftp_keys_sign_data(pool *p,
       res = rsa_sign_data(p, data, datalen, siglen);
       break;
 
+#if !defined(OPENSSL_NO_DSA)
     case SFTP_KEY_DSA:
       res = dsa_sign_data(p, data, datalen, siglen);
       break;
+#endif /* !OPENSSL_NO_DSA */
 
 #ifdef PR_USE_OPENSSL_ECC
     case SFTP_KEY_ECDSA_256:
@@ -2727,8 +3044,11 @@ const unsigned char *sftp_keys_sign_data(pool *p,
       return NULL;
   }
 
-  if (p) {
-    unsigned char *buf = palloc(p, *siglen);
+  if (res != NULL &&
+      p != NULL) {
+    unsigned char *buf;
+
+    buf = palloc(p, *siglen);
     memcpy(buf, res, *siglen);
 
     pr_memscrub((char *) res, *siglen);
@@ -2743,7 +3063,8 @@ int sftp_keys_verify_pubkey_type(pool *p, unsigned char *pubkey_data,
   EVP_PKEY *pkey;
   int res = FALSE;
 
-  if (pubkey_data == NULL) {
+  if (pubkey_data == NULL ||
+      pubkey_len == 0) {
     errno = EINVAL;
     return -1;
   }
@@ -2755,11 +3076,11 @@ int sftp_keys_verify_pubkey_type(pool *p, unsigned char *pubkey_data,
 
   switch (pubkey_type) {
     case SFTP_KEY_RSA:
-      res = (get_pkey_type(pkey) == EVP_PKEY_RSA); 
+      res = (get_pkey_type(pkey) == EVP_PKEY_RSA);
       break;
 
     case SFTP_KEY_DSA:
-      res = (get_pkey_type(pkey) == EVP_PKEY_DSA); 
+      res = (get_pkey_type(pkey) == EVP_PKEY_DSA);
       break;
 
 #ifdef PR_USE_OPENSSL_ECC
@@ -2807,7 +3128,8 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
     unsigned char *signature, uint32_t signaturelen,
     unsigned char *sig_data, size_t sig_datalen) {
   EVP_PKEY *pkey;
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -2815,8 +3137,8 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
   uint32_t sig_len;
   unsigned char digest[EVP_MAX_MD_SIZE];
   char *sig_type;
-  unsigned int digestlen;
-  int res;
+  unsigned int digestlen = 0;
+  int res = 0;
 
   if (pubkey_algo == NULL ||
       pubkey_data == NULL ||
@@ -2852,75 +3174,106 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
   }
 
   if (strncmp(sig_type, "ssh-rsa", 8) == 0) {
-    RSA *rsa;
-    int ok;
-    unsigned int modulus_len;
-
-    rsa = EVP_PKEY_get1_RSA(pkey);
-    modulus_len = RSA_size(rsa);
-
     sig_len = sftp_msg_read_int(p, &signature, &signaturelen);
     sig = (unsigned char *) sftp_msg_read_data(p, &signature, &signaturelen,
       sig_len);
+    if (sig != NULL) {
+      RSA *rsa;
+      unsigned int modulus_len;
+      int ok;
 
-    /* If the signature provided by the client is less than the expected
-     * key length, the verification will fail.  In such cases, we need to
-     * pad the provided signature with trailing zeros (Bug#3992).
-     */
-    if (sig_len < modulus_len) {
-      unsigned int padding_len;
-      unsigned char *padded_sig;
+      rsa = EVP_PKEY_get1_RSA(pkey);
+
+      if (keys_rsa_min_nbits > 0) {
+        int rsa_nbits;
+
+        rsa_nbits = RSA_size(rsa) * 8;
+        if (rsa_nbits < keys_rsa_min_nbits) {
+          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+            "RSA key size (%d bits) less than required minimum (%d bits)",
+            rsa_nbits, keys_rsa_min_nbits);
+          RSA_free(rsa);
+
+          errno = EINVAL;
+          return -1;
+        }
+      }
+
+      modulus_len = RSA_size(rsa);
+
+      /* If the signature provided by the client is more than the expected
+       * key length, the verification will fail.
+       */
+      if (sig_len > modulus_len) {
+        RSA_free(rsa);
+
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error verifying RSA signature: "
+          "signature len (%lu) > RSA modulus len (%u)",
+          (unsigned long) sig_len, modulus_len);
+        errno = EINVAL;
+        return -1;
+      }
+
+      /* If the signature provided by the client is less than the expected
+       * key length, the verification will fail.  In such cases, we need to
+       * pad the provided signature with leading zeros (Bug#3992).
+       */
+      if (sig_len < modulus_len) {
+        unsigned int padding_len;
+        unsigned char *padded_sig;
 
-      padding_len = modulus_len - sig_len;
-      padded_sig = pcalloc(p, modulus_len);
+        padding_len = modulus_len - sig_len;
+        padded_sig = pcalloc(p, modulus_len);
      
-      pr_trace_msg(trace_channel, 12, "padding client-sent "
-        "RSA signature (%lu) bytes with %u bytes of zeroed data",
-        (unsigned long) sig_len, padding_len);
-      memmove(padded_sig + padding_len, sig, sig_len);
+        pr_trace_msg(trace_channel, 12, "padding client-sent "
+          "RSA signature (%lu) bytes with %u bytes of zeroed data",
+          (unsigned long) sig_len, padding_len);
+        memmove(padded_sig + padding_len, sig, sig_len);
 
-      sig = padded_sig;
-      sig_len = (uint32_t) modulus_len;
-    }
+        sig = padded_sig;
+        sig_len = (uint32_t) modulus_len;
+      }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       pctx = EVP_MD_CTX_new();
 #else
       pctx = &ctx;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    EVP_DigestInit(pctx, EVP_sha1());
-    EVP_DigestUpdate(pctx, sig_data, sig_datalen);
-    EVP_DigestFinal(pctx, digest, &digestlen);
+      EVP_DigestInit(pctx, EVP_sha1());
+      EVP_DigestUpdate(pctx, sig_data, sig_datalen);
+      EVP_DigestFinal(pctx, digest, &digestlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
-    ok = RSA_verify(NID_sha1, digest, digestlen, sig, sig_len, rsa);
-    if (ok == 1) {
-      res = 0;
+      ok = RSA_verify(NID_sha1, digest, digestlen, sig, sig_len, rsa);
+      if (ok == 1) {
+        res = 0;
+
+      } else {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error verifying RSA signature: %s", sftp_crypto_get_errors());
+        res = -1;
+      }
+
+      RSA_free(rsa);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error verifying RSA signature: %s", sftp_crypto_get_errors());
+        "error verifying RSA signature: missing signature data");
       res = -1;
     }
 
-    RSA_free(rsa);
-
+#if !defined(OPENSSL_NO_DSA)
   } else if (strncmp(sig_type, "ssh-dss", 8) == 0) {
-    DSA *dsa;
-    DSA_SIG *dsa_sig;
-    BIGNUM *sig_r, *sig_s;
-    int ok;
-
-    dsa = EVP_PKEY_get1_DSA(pkey);
-
     sig_len = sftp_msg_read_int(p, &signature, &signaturelen);
 
     /* A DSA signature string is composed of 2 20 character parts. */
-
     if (sig_len != 40) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "bad DSA signature len (%lu)", (unsigned long) sig_len);
@@ -2928,82 +3281,111 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
 
     sig = (unsigned char *) sftp_msg_read_data(p, &signature, &signaturelen,
       sig_len);
+    if (sig != NULL) {
+      DSA *dsa;
+      DSA_SIG *dsa_sig;
+      BIGNUM *sig_r, *sig_s;
+      int ok;
+
+      dsa = EVP_PKEY_get1_DSA(pkey);
+
+      if (keys_dsa_min_nbits > 0) {
+        int dsa_nbits;
+
+        dsa_nbits = DSA_size(dsa) * 8;
+        if (dsa_nbits < keys_dsa_min_nbits) {
+          (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+            "DSA key size (%d bits) less than required minimum (%d bits)",
+            dsa_nbits, keys_dsa_min_nbits);
+          DSA_free(dsa);
+
+          errno = EINVAL;
+          return -1;
+        }
+      }
 
-    dsa_sig = DSA_SIG_new();
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-    DSA_SIG_get0(&sig_r, &sig_s, dsa_sig);
+      dsa_sig = DSA_SIG_new();
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+      DSA_SIG_get0(&sig_r, &sig_s, dsa_sig);
 #else
-    sig_r = dsa_sig->r;
-    sig_s = dsa_sig->s;
+      sig_r = dsa_sig->r;
+      sig_s = dsa_sig->s;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    sig_r = BN_bin2bn(sig, 20, sig_r);
-    if (sig_r == NULL) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error obtaining 'r' DSA signature component: %s",
-        sftp_crypto_get_errors());
-      DSA_free(dsa);
-      DSA_SIG_free(dsa_sig);
-      res = -1;
-    }
+      sig_r = BN_bin2bn(sig, 20, sig_r);
+      if (sig_r == NULL) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error obtaining 'r' DSA signature component: %s",
+          sftp_crypto_get_errors());
+        DSA_free(dsa);
+        DSA_SIG_free(dsa_sig);
+        return -1;
+      }
 
-    sig_s = BN_bin2bn(sig + 20, 20, sig_s);
-    if (sig_s == NULL) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error obtaining 's' DSA signature component: %s",
-        sftp_crypto_get_errors());
-      DSA_free(dsa);
-      DSA_SIG_free(dsa_sig);
-      res = -1;
-    }
+      sig_s = BN_bin2bn(sig + 20, 20, sig_s);
+      if (sig_s == NULL) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error obtaining 's' DSA signature component: %s",
+          sftp_crypto_get_errors());
+        BN_clear_free(sig_r);
+        DSA_free(dsa);
+        DSA_SIG_free(dsa_sig);
+        return -1;
+      }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-    pctx = EVP_MD_CTX_new();
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+      pctx = EVP_MD_CTX_new();
 #else
-    pctx = &ctx;
+      pctx = &ctx;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    EVP_DigestInit(pctx, EVP_sha1());
-    EVP_DigestUpdate(pctx, sig_data, sig_datalen);
-    EVP_DigestFinal(pctx, digest, &digestlen);
+      EVP_DigestInit(pctx, EVP_sha1());
+      EVP_DigestUpdate(pctx, sig_data, sig_datalen);
+      EVP_DigestFinal(pctx, digest, &digestlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-    EVP_MD_CTX_free(pctx);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+      EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
 # if OPENSSL_VERSION_NUMBER >= 0x10100006L
-    DSA_SIG_set0(dsa_sig, sig_r, sig_s);
+      DSA_SIG_set0(dsa_sig, sig_r, sig_s);
 # else
-    /* XXX What to do here? */
+      /* XXX What to do here? */
 # endif /* prior to OpenSSL-1.1.0-pre6 */
 #else
-    dsa_sig->r = sig_r;
-    dsa_sig->s = sig_s;
+      dsa_sig->r = sig_r;
+      dsa_sig->s = sig_s;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    ok = DSA_do_verify(digest, digestlen, dsa_sig, dsa);
-    if (ok == 1) {
-      res = 0;
+      ok = DSA_do_verify(digest, digestlen, dsa_sig, dsa);
+      if (ok == 1) {
+        res = 0;
+
+      } else {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error verifying DSA signature: %s", sftp_crypto_get_errors());
+        res = -1;
+      }
+
+      DSA_free(dsa);
+      DSA_SIG_free(dsa_sig);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error verifying DSA signature: %s", sftp_crypto_get_errors());
+        "error verifying DSA signature: missing signature data");
       res = -1;
     }
-
-    DSA_free(dsa);
-    DSA_SIG_free(dsa_sig);
+#endif /* !OPENSSL_NO_DSA */
 
 #ifdef PR_USE_OPENSSL_ECC
   } else if (strncmp(sig_type, "ecdsa-sha2-nistp256", 20) == 0 ||
              strncmp(sig_type, "ecdsa-sha2-nistp384", 20) == 0 ||
              strncmp(sig_type, "ecdsa-sha2-nistp521", 20) == 0) {
-    EC_KEY *ec;
-    ECDSA_SIG *ecdsa_sig;
-    BIGNUM *sig_r, *sig_s;
-    const EVP_MD *md = NULL;
-    int ok;
 
     if (strcmp(pubkey_algo, sig_type) != 0) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -3012,95 +3394,125 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
       return -1;
     }
 
-    ecdsa_sig = ECDSA_SIG_new();
-    if (ecdsa_sig == NULL) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error allocating new ECDSA_SIG: %s", sftp_crypto_get_errors());
-      return -1;
+    if (keys_ec_min_nbits > 0) {
+      int ec_nbits;
+
+      ec_nbits = EVP_PKEY_bits(pkey) * 8;
+      if (ec_nbits < keys_ec_min_nbits) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "EC key size (%d bits) less than required minimum (%d bits)",
+          ec_nbits, keys_ec_min_nbits);
+        errno = EINVAL;
+        return -1;
+      }
     }
 
     sig_len = sftp_msg_read_int(p, &signature, &signaturelen);
     sig = (unsigned char *) sftp_msg_read_data(p, &signature, &signaturelen,
       sig_len);
+    if (sig != NULL) {
+      EC_KEY *ec;
+      ECDSA_SIG *ecdsa_sig;
+      BIGNUM *sig_r, *sig_s;
+      const EVP_MD *md = NULL;
+      int ok;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-    ECDSA_SIG_get0(&sig_r, &sig_s, ecdsa_sig);
+      ecdsa_sig = ECDSA_SIG_new();
+      if (ecdsa_sig == NULL) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error allocating new ECDSA_SIG: %s", sftp_crypto_get_errors());
+        return -1;
+      }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+      ECDSA_SIG_get0(&sig_r, &sig_s, ecdsa_sig);
 #else
-    sig_r = ecdsa_sig->r;
-    sig_s = ecdsa_sig->s;
+      sig_r = ecdsa_sig->r;
+      sig_s = ecdsa_sig->s;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    sig_r = sftp_msg_read_mpint(p, &sig, &sig_len);
-    if (sig_r == NULL) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error reading 'r' ECDSA signature component: %s",
-        sftp_crypto_get_errors());
-      ECDSA_SIG_free(ecdsa_sig);
-      return -1;
-    }
+      sig_r = sftp_msg_read_mpint(p, &sig, &sig_len);
+      if (sig_r == NULL) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error reading 'r' ECDSA signature component: %s",
+          sftp_crypto_get_errors());
+        ECDSA_SIG_free(ecdsa_sig);
+        return -1;
+      }
 
-    sig_s = sftp_msg_read_mpint(p, &sig, &sig_len);
-    if (sig_s == NULL) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error reading 's' ECDSA signature component: %s",
-        sftp_crypto_get_errors());
-      ECDSA_SIG_free(ecdsa_sig);
-      return -1;
-    }
+      sig_s = sftp_msg_read_mpint(p, &sig, &sig_len);
+      if (sig_s == NULL) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error reading 's' ECDSA signature component: %s",
+          sftp_crypto_get_errors());
+        ECDSA_SIG_free(ecdsa_sig);
+        return -1;
+      }
 
-    /* Skip past the common leading prefix "ecdsa-sha2-" to compare just
-     * last 9 characters.
-     */
+      /* Skip past the common leading prefix "ecdsa-sha2-" to compare just
+       * last 9 characters.
+       */
 
-    if (strncmp(sig_type + 11, "nistp256", 9) == 0) {
-      md = EVP_sha256();
+      if (strncmp(sig_type + 11, "nistp256", 9) == 0) {
+        md = EVP_sha256();
 
-    } else if (strncmp(sig_type + 11, "nistp384", 9) == 0) {
-      md = EVP_sha384();
+      } else if (strncmp(sig_type + 11, "nistp384", 9) == 0) {
+        md = EVP_sha384();
 
-    } else if (strncmp(sig_type + 11, "nistp521", 9) == 0) {
-      md = EVP_sha512();
-    }
+      } else if (strncmp(sig_type + 11, "nistp521", 9) == 0) {
+        md = EVP_sha512();
+      }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-    pctx = EVP_MD_CTX_new();
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+      pctx = EVP_MD_CTX_new();
 #else
-    pctx = &ctx;
+      pctx = &ctx;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    EVP_DigestInit(pctx, md);
-    EVP_DigestUpdate(pctx, sig_data, sig_datalen);
-    EVP_DigestFinal(pctx, digest, &digestlen);
+      EVP_DigestInit(pctx, md);
+      EVP_DigestUpdate(pctx, sig_data, sig_datalen);
+      EVP_DigestFinal(pctx, digest, &digestlen);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
-    EVP_MD_CTX_free(pctx);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
+      EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
-    ec = EVP_PKEY_get1_EC_KEY(pkey);
+      ec = EVP_PKEY_get1_EC_KEY(pkey);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
 # if OPENSSL_VERSION_NUMBER >= 0x10100006L
-    ECDSA_SIG_set0(ecdsa_sig, sig_r, sig_s);
+      ECDSA_SIG_set0(ecdsa_sig, sig_r, sig_s);
 # else
-    /* XXX What to do here? */
+      /* XXX What to do here? */
 # endif /* prior to OpenSSL-1.1.0-pre6 */
 #else
-    ecdsa_sig->r = sig_r;
-    ecdsa_sig->s = sig_s;
+      ecdsa_sig->r = sig_r;
+      ecdsa_sig->s = sig_s;
 #endif /* prior to OpenSSL-1.1.0 */
 
-    ok = ECDSA_do_verify(digest, digestlen, ecdsa_sig, ec);
-    if (ok == 1) {
-      res = 0;
+      ok = ECDSA_do_verify(digest, digestlen, ecdsa_sig, ec);
+      if (ok == 1) {
+        res = 0;
+
+      } else {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error verifying ECDSA signature: %s", sftp_crypto_get_errors());
+        res = -1;
+      }
+
+      EC_KEY_free(ec);
+      ECDSA_SIG_free(ecdsa_sig);
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error verifying ECDSA signature: %s", sftp_crypto_get_errors());
+        "error verifying ECDSA signature: missing signature data");
       res = -1;
     }
 
-    EC_KEY_free(ec);
-    ECDSA_SIG_free(ecdsa_sig);
 #endif /* PR_USE_OPENSSL_ECC */
 
   } else {
@@ -3115,6 +3527,24 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
   return res;
 }
 
+int sftp_keys_set_key_limits(int rsa_min, int dsa_min, int ec_min) {
+  /* Ignore any negative values. */
+
+  if (rsa_min >= 0) {
+    keys_rsa_min_nbits = (unsigned int) rsa_min;
+  }
+
+  if (dsa_min >= 0) {
+    keys_dsa_min_nbits = (unsigned int) dsa_min;
+  }
+
+  if (ec_min >= 0) {
+    keys_ec_min_nbits = (unsigned int) ec_min;
+  }
+
+  return 0;
+}
+
 int sftp_keys_set_passphrase_provider(const char *provider) {
   if (provider == NULL) {
     errno = EINVAL;
@@ -3134,16 +3564,23 @@ void sftp_keys_get_passphrases(void) {
 
     c = find_config(s->conf, CONF_PARAM, "SFTPHostKey", FALSE);
     while (c) {
+      int flags;
+
       pr_signals_handle();
 
-      /* Skip any agent-provided SFTPHostKey directives. */
-      if (strncmp(c->argv[0], "agent:", 6) == 0) {
+      flags = *((int *) c->argv[1]);
+
+      /* Skip any agent-provided SFTPHostKey directives, as well as any
+       * "disabling key" directives.
+       */
+      if (flags != 0 ||
+          strncmp(c->argv[0], "agent:", 6) == 0) {
         c = find_config_next(c, c->next, CONF_PARAM, "SFTPHostKey", FALSE);
         continue;
       }
 
       k = pcalloc(s->pool, sizeof(struct sftp_pkey));      
-      k->pkeysz = PEM_BUFSIZE;
+      k->pkeysz = PEM_BUFSIZE-1;
       k->server = s;
 
       if (get_passphrase(k, c->argv[0]) < 0) {
@@ -3177,46 +3614,8 @@ void sftp_keys_get_passphrases(void) {
  */
 void sftp_keys_free(void) {
   scrub_pkeys();
- 
-  if (sftp_dsa_hostkey != NULL) {
-    if (sftp_dsa_hostkey->pkey != NULL) {
-      EVP_PKEY_free(sftp_dsa_hostkey->pkey);
-    } 
-
-    sftp_dsa_hostkey = NULL;
-  }
-
-  if (sftp_rsa_hostkey != NULL) {
-    if (sftp_rsa_hostkey->pkey != NULL) {
-      EVP_PKEY_free(sftp_rsa_hostkey->pkey);
-    }
-
-    sftp_rsa_hostkey = NULL;
-  }
-
-#ifdef PR_USE_OPENSSL_ECC
-  if (sftp_ecdsa256_hostkey != NULL) {
-    if (sftp_ecdsa256_hostkey->pkey != NULL) {
-      EVP_PKEY_free(sftp_ecdsa256_hostkey->pkey);
-    }
-
-    sftp_ecdsa256_hostkey = NULL;
-  }
-
-  if (sftp_ecdsa384_hostkey != NULL) {
-    if (sftp_ecdsa384_hostkey->pkey != NULL) {
-      EVP_PKEY_free(sftp_ecdsa384_hostkey->pkey);
-    }
 
-    sftp_ecdsa384_hostkey = NULL;
-  }
-
-  if (sftp_ecdsa521_hostkey != NULL) {
-    if (sftp_ecdsa521_hostkey->pkey != NULL) {
-      EVP_PKEY_free(sftp_ecdsa521_hostkey->pkey);
-    }
-
-    sftp_ecdsa256_hostkey = NULL;
-  }
-#endif /* PR_USE_OPENSSL_ECC */
+  sftp_keys_clear_dsa_hostkey();
+  sftp_keys_clear_ecdsa_hostkey();
+  sftp_keys_clear_rsa_hostkey();
 }
diff --git a/contrib/mod_sftp/keys.h b/contrib/mod_sftp/keys.h
index dfda248..e3cee6b 100644
--- a/contrib/mod_sftp/keys.h
+++ b/contrib/mod_sftp/keys.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp key mgmt (keys)
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: keys.h,v 1.9 2012-03-13 18:58:48 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_KEYS_H
 #define MOD_SFTP_KEYS_H
 
+#include "mod_sftp.h"
+
 enum sftp_key_type_e {
   SFTP_KEY_UNKNOWN = 0,
   SFTP_KEY_DSA,
@@ -48,15 +46,13 @@ enum sftp_key_type_e {
 const char *sftp_keys_get_fingerprint(pool *, unsigned char *, uint32_t, int);
 #define SFTP_KEYS_FP_DIGEST_MD5		1
 #define SFTP_KEYS_FP_DIGEST_SHA1	2
+#define SFTP_KEYS_FP_DIGEST_SHA256	3
 
 void sftp_keys_free(void);
 int sftp_keys_get_hostkey(pool *p, const char *);
 const unsigned char *sftp_keys_get_hostkey_data(pool *, enum sftp_key_type_e,
-  size_t *);
+  uint32_t *);
 void sftp_keys_get_passphrases(void);
-int sftp_keys_have_dsa_hostkey(void);
-int sftp_keys_have_ecdsa_hostkey(pool *, int **);
-int sftp_keys_have_rsa_hostkey(void);
 int sftp_keys_set_passphrase_provider(const char *);
 const unsigned char *sftp_keys_sign_data(pool *, enum sftp_key_type_e,
   const unsigned char *, size_t, size_t *);
@@ -69,4 +65,14 @@ int sftp_keys_verify_signed_data(pool *, const char *,
   unsigned char *, uint32_t, unsigned char *, uint32_t,
   unsigned char *, size_t);
 
-#endif
+/* Sets minimum key sizes. */
+int sftp_keys_set_key_limits(int rsa_min, int dsa_min, int ec_min);
+
+int sftp_keys_clear_dsa_hostkey(void);
+int sftp_keys_clear_ecdsa_hostkey(void);
+int sftp_keys_clear_rsa_hostkey(void);
+int sftp_keys_have_dsa_hostkey(void);
+int sftp_keys_have_ecdsa_hostkey(pool *, int **);
+int sftp_keys_have_rsa_hostkey(void);
+
+#endif /* MOD_SFTP_KEYS_H */
diff --git a/contrib/mod_sftp/keystore.c b/contrib/mod_sftp/keystore.c
index f26c7b7..e459fd0 100644
--- a/contrib/mod_sftp/keystore.c
+++ b/contrib/mod_sftp/keystore.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp keystores
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: keystore.c,v 1.7 2012-02-15 23:50:51 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -163,6 +161,14 @@ int sftp_keystore_verify_host_key(pool *p, const char *user,
   int res = -1;
   config_rec *c;
 
+  if (host_fqdn == NULL ||
+      host_user == NULL ||
+      key_data == NULL ||
+      key_len == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "SFTPAuthorizedHostKeys",
     FALSE);
   if (c == NULL) {
@@ -205,6 +211,12 @@ int sftp_keystore_verify_host_key(pool *p, const char *user,
       "user '%s', host %s", store_type, user, host_fqdn);
 
     ptr = strchr(store_type, ':');
+    if (ptr == NULL) {
+      pr_trace_msg(trace_channel, 2,
+        "skipping badly formatted SFTPAuthorizedHostKeys '%s'", store_type);
+      continue;
+    }
+
     *ptr = '\0';
 
     sks = keystore_get_store(store_type, SFTP_SSH2_HOST_KEY_STORE);
@@ -264,6 +276,12 @@ int sftp_keystore_verify_user_key(pool *p, const char *user,
   int res = -1;
   config_rec *c;
 
+  if (key_data == NULL ||
+      key_len == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "SFTPAuthorizedUserKeys",
     FALSE);
   if (c == NULL) {
@@ -275,7 +293,8 @@ int sftp_keystore_verify_user_key(pool *p, const char *user,
 
   for (i = 0; i < c->argc; i++) {
     struct sftp_keystore_store *sks;
-    char *store_type, *path, *ptr, *session_user;
+    const char *path, *sess_user;
+    char *store_type, *ptr;
 
     pr_signals_handle();
 
@@ -283,8 +302,13 @@ int sftp_keystore_verify_user_key(pool *p, const char *user,
     store_type = c->argv[i];
 
     ptr = strchr(store_type, ':');
-    *ptr = '\0';
+    if (ptr == NULL) {
+      pr_trace_msg(trace_channel, 2,
+        "skipping badly formatted SFTPAuthorizedUserKeys '%s'", store_type);
+      continue;
+    }
 
+    *ptr = '\0';
     path = ptr + 1;
 
     /* Check for any variables in the configured path.
@@ -292,14 +316,14 @@ int sftp_keystore_verify_user_key(pool *p, const char *user,
      * Note that path_subst_uservar() relies on the session.user variable
      * being set, hence why we cache/restore its value.
      */
-    session_user = session.user;
-    session.user = (char *) user;
+    sess_user = session.user;
+    session.user = user;
     path = path_subst_uservar(p, &path);
-    session.user = session_user;
+    session.user = sess_user;
 
     pr_trace_msg(trace_channel, 2,
-      "using SFTPAuthorizedUserKeys '%s' for public key authentication for "
-      "user '%s'", path, user);
+      "using SFTPAuthorizedUserKeys '%s:%s' for public key authentication for "
+      "user '%s'", store_type, path, user);
 
     sks = keystore_get_store(store_type, SFTP_SSH2_USER_KEY_STORE);
     if (sks) {
diff --git a/contrib/mod_sftp/keystore.h b/contrib/mod_sftp/keystore.h
index 9d5e03d..3e9145a 100644
--- a/contrib/mod_sftp/keystore.h
+++ b/contrib/mod_sftp/keystore.h
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - mod_sftp public key store (including RFC4716 public key file
  *                                      format)
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -21,15 +21,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: keystore.h,v 1.5 2012-02-15 23:50:51 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_KEYSTORE_H
 #define MOD_SFTP_KEYSTORE_H
 
+#include "mod_sftp.h"
+
 int sftp_keystore_init(void);
 int sftp_keystore_free(void);
 
@@ -39,4 +37,4 @@ int sftp_keystore_verify_host_key(pool *, const char *, const char *,
 int sftp_keystore_verify_user_key(pool *, const char *, unsigned char *,
   uint32_t);
 
-#endif
+#endif /* MOD_SFTP_KEYSTORE_H */
diff --git a/contrib/mod_sftp/mac.c b/contrib/mod_sftp/mac.c
index 2b12e04..bc313b9 100644
--- a/contrib/mod_sftp/mac.c
+++ b/contrib/mod_sftp/mac.c
@@ -52,6 +52,7 @@ struct sftp_mac {
 
 #define SFTP_MAC_ALGO_TYPE_HMAC		1
 #define SFTP_MAC_ALGO_TYPE_UMAC64	2
+#define SFTP_MAC_ALGO_TYPE_UMAC128	3
 
 #define SFTP_MAC_FL_READ_MAC	1
 #define SFTP_MAC_FL_WRITE_MAC	2
@@ -87,15 +88,17 @@ static unsigned int write_mac_idx = 0;
 static void clear_mac(struct sftp_mac *);
 
 static unsigned int get_next_read_index(void) {
-  if (read_mac_idx == 1)
+  if (read_mac_idx == 1) {
     return 0;
+  }
 
   return 1;
 }
 
 static unsigned int get_next_write_index(void) {
-  if (write_mac_idx == 1)
+  if (write_mac_idx == 1) {
     return 0;
+  }
 
   return 1;
 }
@@ -104,16 +107,19 @@ static void switch_read_mac(void) {
   /* First we can clear the read MAC, kept from rekeying. */
   if (read_macs[read_mac_idx].key) {
     clear_mac(&(read_macs[read_mac_idx]));
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     HMAC_CTX_reset(hmac_read_ctxs[read_mac_idx]);
 #elif OPENSSL_VERSION_NUMBER > 0x000907000L
     HMAC_CTX_cleanup(hmac_read_ctxs[read_mac_idx]);
 #else
     HMAC_cleanup(hmac_read_ctxs[read_mac_idx]);
 #endif
-
-    if (umac_read_ctxs[read_mac_idx] != NULL) {
+    if (read_macs[read_mac_idx].algo_type == SFTP_MAC_ALGO_TYPE_UMAC64) {
       umac_reset(umac_read_ctxs[read_mac_idx]);
+
+    } else if (read_macs[read_mac_idx].algo_type == SFTP_MAC_ALGO_TYPE_UMAC128) {
+      umac128_reset(umac_read_ctxs[read_mac_idx]);
     }
 
     mac_blockszs[read_mac_idx] = 0; 
@@ -132,16 +138,19 @@ static void switch_write_mac(void) {
   /* First we can clear the write MAC, kept from rekeying. */
   if (write_macs[write_mac_idx].key) {
     clear_mac(&(write_macs[write_mac_idx]));
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     HMAC_CTX_reset(hmac_write_ctxs[write_mac_idx]);
 #elif OPENSSL_VERSION_NUMBER > 0x000907000L
     HMAC_CTX_cleanup(hmac_write_ctxs[write_mac_idx]);
 #else
     HMAC_cleanup(hmac_write_ctxs[write_mac_idx]);
 #endif
-
-    if (umac_write_ctxs[write_mac_idx] != NULL) {
+    if (write_macs[write_mac_idx].algo_type == SFTP_MAC_ALGO_TYPE_UMAC64) {
       umac_reset(umac_write_ctxs[write_mac_idx]);
+
+    } else if (write_macs[write_mac_idx].algo_type == SFTP_MAC_ALGO_TYPE_UMAC128) {
+      umac128_reset(umac_write_ctxs[write_mac_idx]);
     }
 
     /* Now we can switch the index. */
@@ -169,7 +178,8 @@ static void clear_mac(struct sftp_mac *mac) {
 
 static int init_mac(pool *p, struct sftp_mac *mac, HMAC_CTX *hmac_ctx,
     struct umac_ctx *umac_ctx) {
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   HMAC_CTX_reset(hmac_ctx);
 #elif OPENSSL_VERSION_NUMBER > 0x000907000L
   HMAC_CTX_init(hmac_ctx);
@@ -200,6 +210,10 @@ static int init_mac(pool *p, struct sftp_mac *mac, HMAC_CTX *hmac_ctx,
   } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC64) {
     umac_reset(umac_ctx);
     umac_init(umac_ctx, mac->key);
+
+  } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC128) {
+    umac128_reset(umac_ctx);
+    umac128_init(umac_ctx, mac->key);
   }
 
   return 0;
@@ -260,7 +274,8 @@ static int get_mac(struct ssh2_packet *pkt, struct sftp_mac *mac,
     HMAC_Final(hmac_ctx, mac_data, &mac_len);
 #endif /* OpenSSL-1.0.0 and later */
 
-  } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC64) {
+  } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC64 ||
+             mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC128) {
     unsigned char nonce[8], *nonce_ptr;
     uint32_t nonce_len = 0;
 
@@ -279,10 +294,18 @@ static int get_mac(struct ssh2_packet *pkt, struct sftp_mac *mac,
     nonce_len = sizeof(nonce);
     sftp_msg_write_long(&nonce_ptr, &nonce_len, pkt->seqno);
 
-    umac_reset(umac_ctx);
-    umac_update(umac_ctx, ptr, (bufsz - buflen));
-    umac_final(umac_ctx, mac_data, nonce);
-    mac_len = 8;
+    if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC64) {
+      umac_reset(umac_ctx);
+      umac_update(umac_ctx, ptr, (bufsz - buflen));
+      umac_final(umac_ctx, mac_data, nonce);
+      mac_len = 8;
+
+    } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC128) {
+      umac128_reset(umac_ctx);
+      umac128_update(umac_ctx, ptr, (bufsz - buflen));
+      umac128_final(umac_ctx, mac_data, nonce);
+      mac_len = 16;
+    }
   }
 
   if (mac_len == 0) {
@@ -375,7 +398,8 @@ static int get_mac(struct ssh2_packet *pkt, struct sftp_mac *mac,
 static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
     const unsigned char *k, uint32_t klen, const char *h, uint32_t hlen,
     char *letter, const unsigned char *id, uint32_t id_len) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   EVP_MD_CTX ctx;
 #endif /* prior to OpenSSL-1.1.0 */
   EVP_MD_CTX *pctx;
@@ -398,7 +422,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
     _exit(1);
   }
 
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   pctx = &ctx;
 #else
   pctx = EVP_MD_CTX_new();
@@ -414,7 +439,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error initializing message digest: %s", sftp_crypto_get_errors());
     free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return -1;
@@ -428,7 +454,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error updating message digest with K: %s", sftp_crypto_get_errors());
     free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return -1;
@@ -442,7 +469,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error updating message digest with H: %s", sftp_crypto_get_errors());
     free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return -1;
@@ -457,7 +485,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
       "error updating message digest with '%c': %s", *letter,
       sftp_crypto_get_errors());
     free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return -1;
@@ -471,7 +500,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error updating message digest with ID: %s", sftp_crypto_get_errors());
     free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return -1;
@@ -486,7 +516,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
       "error finalizing message digest: %s", sftp_crypto_get_errors());
     pr_memscrub(key, key_sz);
     free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
     EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
     return -1;
@@ -510,7 +541,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
         "error initializing message digest: %s", sftp_crypto_get_errors());
       pr_memscrub(key, key_sz);
       free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
       EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
       return -1;
@@ -525,7 +557,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
         "error updating message digest with K: %s", sftp_crypto_get_errors());
       pr_memscrub(key, key_sz);
       free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
       EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
       return -1;
@@ -540,7 +573,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
         "error updating message digest with H: %s", sftp_crypto_get_errors());
       pr_memscrub(key, key_sz);
       free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
       EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
       return -1;
@@ -556,7 +590,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
         sftp_crypto_get_errors());
       pr_memscrub(key, key_sz);
       free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
       EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
       return -1;
@@ -571,7 +606,8 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
         "error finalizing message digest: %s", sftp_crypto_get_errors());
       pr_memscrub(key, key_sz);
       free(key);
-# if OPENSSL_VERSION_NUMBER >= 0x10100000L
+# if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+     !defined(HAVE_LIBRESSL)
       EVP_MD_CTX_free(pctx);
 # endif /* OpenSSL-1.1.0 and later */
       return -1;
@@ -586,14 +622,16 @@ static int set_mac_key(struct sftp_mac *mac, const EVP_MD *hash,
   mac->key = key;
   mac->keysz = key_sz;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   EVP_MD_CTX_free(pctx);
 #endif /* OpenSSL-1.1.0 and later */
 
   if (mac->algo_type == SFTP_MAC_ALGO_TYPE_HMAC) {
     mac->key_len = EVP_MD_size(mac->digest);
 
-  } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC64) {
+  } else if (mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC64 ||
+             mac->algo_type == SFTP_MAC_ALGO_TYPE_UMAC128) {
     mac->key_len = EVP_MD_block_size(mac->digest);
   }
 
@@ -631,9 +669,19 @@ int sftp_mac_set_read_algo(const char *algo) {
     idx = get_next_read_index();
   }
 
+  /* Clear any potential UMAC contexts at this index. */
   if (umac_read_ctxs[idx] != NULL) {
-    umac_delete(umac_read_ctxs[idx]);
-    umac_read_ctxs[idx] = NULL;
+    switch (read_macs[idx].algo_type) {
+      case SFTP_MAC_ALGO_TYPE_UMAC64:
+        umac_delete(umac_read_ctxs[idx]);
+        umac_read_ctxs[idx] = NULL;
+        break;
+
+      case SFTP_MAC_ALGO_TYPE_UMAC128:
+        umac128_delete(umac_read_ctxs[idx]);
+        umac_read_ctxs[idx] = NULL;
+        break;
+    }
   }
 
   read_macs[idx].digest = sftp_crypto_get_digest(algo, &mac_len);
@@ -646,6 +694,10 @@ int sftp_mac_set_read_algo(const char *algo) {
     read_macs[idx].algo_type = SFTP_MAC_ALGO_TYPE_UMAC64;
     umac_read_ctxs[idx] = umac_alloc();
 
+  } else if (strncmp(read_macs[idx].algo, "umac-128 at openssh.com", 13) == 0) {
+    read_macs[idx].algo_type = SFTP_MAC_ALGO_TYPE_UMAC128;
+    umac_read_ctxs[idx] = umac128_alloc();
+
   } else {
     read_macs[idx].algo_type = SFTP_MAC_ALGO_TYPE_HMAC;
   }
@@ -655,7 +707,7 @@ int sftp_mac_set_read_algo(const char *algo) {
 }
 
 int sftp_mac_set_read_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
-    const char *h, uint32_t hlen) {
+    const char *h, uint32_t hlen, int role) {
   const unsigned char *id = NULL;
   unsigned char *buf, *ptr;
   uint32_t buflen, bufsz, id_len;
@@ -679,8 +731,17 @@ int sftp_mac_set_read_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
 
   id_len = sftp_session_get_id(&id);
 
-  /* HASH(K || H || "E" || session_id) */
-  letter = 'E';
+  /* The letters used depend on the role; see:
+   *  https://tools.ietf.org/html/rfc4253#section-7.2
+   *
+   * If we are the SERVER, then we use the letters for the "client to server"
+   * flows, since we are READING from the client.
+   */
+
+  /* client-to-server HASH(K || H || "E" || session_id)
+   * server-to-client HASH(K || H || "F" || session_id)
+   */
+  letter = (role == SFTP_ROLE_SERVER ? 'E' : 'F');
   set_mac_key(mac, hash, ptr, (bufsz - buflen), h, hlen, &letter, id, id_len);
 
   if (init_mac(p, mac, hmac_ctx, umac_ctx) < 0) {
@@ -741,9 +802,19 @@ int sftp_mac_set_write_algo(const char *algo) {
     idx = get_next_write_index();
   }
 
+  /* Clear any potential UMAC contexts at this index. */
   if (umac_write_ctxs[idx] != NULL) {
-    umac_delete(umac_write_ctxs[idx]);
-    umac_write_ctxs[idx] = NULL;
+    switch (write_macs[idx].algo_type) {
+      case SFTP_MAC_ALGO_TYPE_UMAC64:
+        umac_delete(umac_write_ctxs[idx]);
+        umac_write_ctxs[idx] = NULL;
+        break;
+
+      case SFTP_MAC_ALGO_TYPE_UMAC128:
+        umac128_delete(umac_write_ctxs[idx]);
+        umac_write_ctxs[idx] = NULL;
+        break;
+    }
   }
 
   write_macs[idx].digest = sftp_crypto_get_digest(algo, &mac_len);
@@ -756,6 +827,10 @@ int sftp_mac_set_write_algo(const char *algo) {
     write_macs[idx].algo_type = SFTP_MAC_ALGO_TYPE_UMAC64;
     umac_write_ctxs[idx] = umac_alloc();
 
+  } else if (strncmp(write_macs[idx].algo, "umac-128 at openssh.com", 13) == 0) {
+    write_macs[idx].algo_type = SFTP_MAC_ALGO_TYPE_UMAC128;
+    umac_write_ctxs[idx] = umac128_alloc();
+
   } else {
     write_macs[idx].algo_type = SFTP_MAC_ALGO_TYPE_HMAC;
   }
@@ -765,7 +840,7 @@ int sftp_mac_set_write_algo(const char *algo) {
 }
 
 int sftp_mac_set_write_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
-    const char *h, uint32_t hlen) {
+    const char *h, uint32_t hlen, int role) {
   const unsigned char *id = NULL;
   unsigned char *buf, *ptr;
   uint32_t buflen, bufsz, id_len;
@@ -788,8 +863,17 @@ int sftp_mac_set_write_key(pool *p, const EVP_MD *hash, const BIGNUM *k,
 
   id_len = sftp_session_get_id(&id);
 
-  /* HASH(K || H || "F" || session_id) */
-  letter = 'F';
+  /* The letters used depend on the role; see:
+   *  https://tools.ietf.org/html/rfc4253#section-7.2
+   *
+   * If we are the SERVER, then we use the letters for the "server to client"
+   * flows, since we are WRITING to the client.
+   */
+
+  /* client-to-server HASH(K || H || "E" || session_id)
+   * server-to-client HASH(K || H || "F" || session_id)
+   */
+  letter = (role == SFTP_ROLE_SERVER ? 'F' : 'E');
   set_mac_key(mac, hash, ptr, (bufsz - buflen), h, hlen, &letter, id, id_len);
 
   if (init_mac(p, mac, hmac_ctx, umac_ctx) < 0) {
@@ -825,7 +909,8 @@ int sftp_mac_write_data(struct ssh2_packet *pkt) {
   return 0;
 }
 
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
 /* In older versions of OpenSSL, there was not a way to dynamically allocate
  * an HMAC_CTX object.  Thus we have these static objects for those
  * older versions.
@@ -835,7 +920,8 @@ static HMAC_CTX write_ctx1, write_ctx2;
 #endif /* prior to OpenSSL-1.1.0 */
 
 int sftp_mac_init(void) {
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || \
+    defined(HAVE_LIBRESSL)
   hmac_read_ctxs[0] = &read_ctx1;
   hmac_read_ctxs[1] = &read_ctx2;
   hmac_write_ctxs[0] = &write_ctx1;
@@ -856,12 +942,12 @@ int sftp_mac_init(void) {
 }
 
 int sftp_mac_free(void) {
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   HMAC_CTX_free(hmac_read_ctxs[0]);
   HMAC_CTX_free(hmac_read_ctxs[1]);
   HMAC_CTX_free(hmac_write_ctxs[0]);
   HMAC_CTX_free(hmac_write_ctxs[1]);
 #endif /* OpenSSL-1.1.0 and later */
-
   return 0;
 }
diff --git a/contrib/mod_sftp/mac.h b/contrib/mod_sftp/mac.h
index 32d371b..40dd92e 100644
--- a/contrib/mod_sftp/mac.h
+++ b/contrib/mod_sftp/mac.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp MAC mgmt
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -20,15 +20,12 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mac.h,v 1.4 2013-03-28 18:48:31 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_MAC_H
 #define MOD_SFTP_MAC_H
 
+#include "mod_sftp.h"
 #include "packet.h"
 
 int sftp_mac_init(void);
@@ -43,13 +40,13 @@ void sftp_mac_set_block_size(size_t);
 const char *sftp_mac_get_read_algo(void);
 int sftp_mac_set_read_algo(const char *);
 int sftp_mac_set_read_key(pool *, const EVP_MD *, const BIGNUM *, const char *,
-  uint32_t);
+  uint32_t, int);
 int sftp_mac_read_data(struct ssh2_packet *);
 
 const char *sftp_mac_get_write_algo(void);
 int sftp_mac_set_write_algo(const char *);
 int sftp_mac_set_write_key(pool *, const EVP_MD *, const BIGNUM *, const char *,
-  uint32_t);
+  uint32_t, int);
 int sftp_mac_write_data(struct ssh2_packet *);
 
-#endif
+#endif /* MOD_SFTP_MAC_H */
diff --git a/contrib/mod_sftp/misc.c b/contrib/mod_sftp/misc.c
index 06285c3..1c4b6fa 100644
--- a/contrib/mod_sftp/misc.c
+++ b/contrib/mod_sftp/misc.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp miscellaneous
- * Copyright (c) 2010-2012 TJ Saunders
+ * Copyright (c) 2010-2017 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
@@ -20,14 +20,12 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: misc.c,v 1.4 2012-12-26 23:18:58 castaglia Exp $
  */
 
 #include "mod_sftp.h"
 #include "misc.h"
 
-int sftp_misc_chown_file(pr_fh_t *fh) {
+int sftp_misc_chown_file(pool *p, pr_fh_t *fh) {
   struct stat st;
   int res, xerrno;
  
@@ -40,7 +38,6 @@ int sftp_misc_chown_file(pr_fh_t *fh) {
    * requested via GroupOwner.
    */
   if (session.fsuid != (uid_t) -1) {
-
     PRIVS_ROOT
     res = pr_fsio_fchown(fh, session.fsuid, session.fsgid);
     xerrno = errno;
@@ -53,17 +50,20 @@ int sftp_misc_chown_file(pr_fh_t *fh) {
     } else {
       if (session.fsgid != (gid_t) -1) {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "root chown(%s) to UID %lu, GID %lu successful", fh->fh_path,
-          (unsigned long) session.fsuid, (unsigned long) session.fsgid);
+          "root chown(%s) to UID %s, GID %s successful", fh->fh_path,
+          pr_uid2str(p, session.fsuid), pr_gid2str(p, session.fsgid));
 
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "root chown(%s) to UID %lu successful", fh->fh_path,
-          (unsigned long) session.fsuid);
+          "root chown(%s) to UID %s successful", fh->fh_path,
+          pr_uid2str(NULL, session.fsuid));
       }
 
-      pr_fs_clear_cache();
-      pr_fsio_fstat(fh, &st);
+      if (pr_fsio_fstat(fh, &st) < 0) {
+        pr_log_debug(DEBUG0,
+          "'%s' fstat(2) error for root chmod: %s", fh->fh_path,
+          strerror(errno));
+      }
 
       /* The chmod happens after the chown because chown will remove the
        * S{U,G}ID bits on some files (namely, directories); the subsequent
@@ -123,12 +123,15 @@ int sftp_misc_chown_file(pr_fh_t *fh) {
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "%schown(%s) to GID %lu successful",
+        "%schown(%s) to GID %s successful",
         use_root_privs ? "root " : "", fh->fh_path,
-        (unsigned long) session.fsgid);
+        pr_gid2str(NULL, session.fsgid));
 
-      pr_fs_clear_cache();
-      pr_fsio_fstat(fh, &st);
+      if (pr_fsio_fstat(fh, &st) < 0) {
+        pr_log_debug(DEBUG0,
+          "'%s' fstat(2) error for %sfchmod: %s", fh->fh_path,
+          use_root_privs ? "root " : "", strerror(errno));
+      }
 
       if (use_root_privs) {
         PRIVS_ROOT
@@ -152,7 +155,7 @@ int sftp_misc_chown_file(pr_fh_t *fh) {
   return 0;
 }
 
-int sftp_misc_chown_path(const char *path) {
+int sftp_misc_chown_path(pool *p, const char *path) {
   struct stat st;
   int res, xerrno;
 
@@ -178,17 +181,20 @@ int sftp_misc_chown_path(const char *path) {
     } else {
       if (session.fsgid != (gid_t) -1) {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "root lchown(%s) to UID %lu, GID %lu successful", path,
-          (unsigned long) session.fsuid, (unsigned long) session.fsgid);
+          "root lchown(%s) to UID %s, GID %s successful", path,
+          pr_uid2str(p, session.fsuid), pr_gid2str(p, session.fsgid));
 
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "root lchown(%s) to UID %lu successful", path,
-          (unsigned long) session.fsuid);
+          "root lchown(%s) to UID %s successful", path,
+          pr_uid2str(NULL, session.fsuid));
       }
 
-      pr_fs_clear_cache();
-      pr_fsio_stat(path, &st);
+      pr_fs_clear_cache2(path);
+      if (pr_fsio_stat(path, &st) < 0) {
+        pr_log_debug(DEBUG0,
+          "'%s' stat(2) error for root chmod: %s", path, strerror(errno));
+      }
 
       /* The chmod happens after the chown because chown will remove the
        * S{U,G}ID bits on some files (namely, directories); the subsequent
@@ -248,12 +254,16 @@ int sftp_misc_chown_path(const char *path) {
 
     } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "%slchown(%s) to GID %lu successful",
+        "%slchown(%s) to GID %s successful",
         use_root_privs ? "root " : "", path,
-        (unsigned long) session.fsgid);
+        pr_gid2str(NULL, session.fsgid));
 
-      pr_fs_clear_cache();
-      pr_fsio_stat(path, &st);
+      pr_fs_clear_cache2(path);
+      if (pr_fsio_stat(path, &st) < 0) {
+        pr_log_debug(DEBUG0,
+          "'%s' stat(2) error for %schmod: %s", path,
+          use_root_privs ? "root " : "", strerror(errno));
+      }
 
       if (use_root_privs) {
         PRIVS_ROOT
@@ -276,3 +286,40 @@ int sftp_misc_chown_path(const char *path) {
 
   return 0;
 }
+
+const char *sftp_misc_namelist_shared(pool *p, const char *c2s_names,
+    const char *s2c_names) {
+  register unsigned int i;
+  const char *name = NULL, **client_names, **server_names;
+  pool *tmp_pool;
+  array_header *client_list, *server_list;
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "Share name pool");
+
+  client_list = pr_str_text_to_array(tmp_pool, c2s_names, ',');
+  client_names = (const char **) client_list->elts;
+
+  server_list = pr_str_text_to_array(tmp_pool, s2c_names, ',');
+  server_names = (const char **) server_list->elts;
+
+  for (i = 0; i < client_list->nelts; i++) {
+    register unsigned int j;
+
+    if (name != NULL) {
+      break;
+    }
+
+    for (j = 0; j < server_list->nelts; j++) {
+      if (strcmp(client_names[i], server_names[j]) == 0) {
+        name = client_names[i];
+        break;
+      }
+    }
+  }
+
+  name = pstrdup(p, name);
+  destroy_pool(tmp_pool);
+
+  return name;
+}
diff --git a/contrib/mod_sftp/misc.h b/contrib/mod_sftp/misc.h
index 82aa127..35b6c9e 100644
--- a/contrib/mod_sftp/misc.h
+++ b/contrib/mod_sftp/misc.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp miscellaneous
- * Copyright (c) 2010-2012 TJ Saunders
+ * Copyright (c) 2010-2016 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
@@ -20,16 +20,15 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: misc.h,v 1.3 2012-03-23 05:35:48 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_MISC_H
 #define MOD_SFTP_MISC_H
 
-int sftp_misc_chown_file(pr_fh_t *);
-int sftp_misc_chown_path(const char *);
+#include "mod_sftp.h"
+
+int sftp_misc_chown_file(pool *, pr_fh_t *);
+int sftp_misc_chown_path(pool *, const char *);
+const char *sftp_misc_namelist_shared(pool *, const char *, const char *);
 
 #endif /* MOD_SFTP_MISC_H */
diff --git a/contrib/mod_sftp/mod_sftp.c b/contrib/mod_sftp/mod_sftp.c
index eecf501..2b9058c 100644
--- a/contrib/mod_sftp/mod_sftp.c
+++ b/contrib/mod_sftp/mod_sftp.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -22,8 +22,8 @@
  * source distribution.
  *
  * -----DO NOT EDIT BELOW THIS LINE-----
- * $Archive: mod_sftp.a $
- * $Libraries: -lcrypto -lz $
+ * $Archive: mod_sftp.a$
+ * $Libraries: -lcrypto$
  */
 
 #include "mod_sftp.h"
@@ -46,6 +46,12 @@
 #include "fxp.h"
 #include "utf8.h"
 
+#if defined(HAVE_SODIUM_H)
+# include <sodium.h>
+#endif /* HAVE_SODIUM_H */
+
+extern xaset_t *server_list;
+
 module sftp_module;
 
 int sftp_logfd = -1;
@@ -60,6 +66,13 @@ static int sftp_engine = 0;
 static const char *sftp_client_version = NULL;
 static const char *sftp_server_version = SFTP_ID_DEFAULT_STRING;
 
+/* Flags for changing how hostkeys are handled. */
+#define SFTP_HOSTKEY_FL_CLEAR_RSA_KEY		0x001
+#define SFTP_HOSTKEY_FL_CLEAR_DSA_KEY		0x002
+#define SFTP_HOSTKEY_FL_CLEAR_ECDSA_KEY		0x004
+
+static const char *trace_channel = "ssh2";
+
 static int sftp_have_authenticated(cmd_rec *cmd) {
   return (sftp_sess_state & SFTP_SESS_STATE_HAVE_AUTH);
 }
@@ -68,7 +81,8 @@ static int sftp_get_client_version(conn_t *conn) {
   int res;
 
   /* 255 is the RFC-defined maximum banner/ID string size */
-  char buf[256];
+  char buf[256], *banner = NULL;
+  size_t buflen = 0;
 
   /* Read client version.  This looks ugly, reading one byte at a time.
    * It is necessary, though.  The banner sent by the client is not of any
@@ -89,7 +103,9 @@ static int sftp_get_client_version(conn_t *conn) {
     for (i = 0; i < sizeof(buf) - 1; i++) {
       res = sftp_ssh2_packet_sock_read(conn->rfd, &buf[i], 1, 0);
       while (res <= 0) {
-        if (errno == EINTR) {
+        int xerrno = errno;
+
+        if (xerrno == EINTR) {
           pr_signals_handle();
 
           res = sftp_ssh2_packet_sock_read(conn->rfd, &buf[i], 1, 0);
@@ -98,9 +114,11 @@ static int sftp_get_client_version(conn_t *conn) {
 
         if (res < 0) {
           (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-            "error reading from client rfd %d: %s", conn->rfd, strerror(errno));
+            "error reading from client rfd %d: %s", conn->rfd,
+            strerror(xerrno));
         }
 
+        errno = xerrno;
         return res;
       }
 
@@ -118,7 +136,13 @@ static int sftp_get_client_version(conn_t *conn) {
       }
     }
 
-    buf[sizeof(buf)-1] = '\0';
+    if (i == sizeof(buf)-1) {
+      bad_proto = TRUE;
+
+    } else {
+      buf[sizeof(buf)-1] = '\0';
+      buflen = strlen(buf);
+    }
 
     /* If the line does not begin with "SSH-2.0-", skip it.  RFC4253, Section
      * 4.2 does not specify what should happen if the client sends data
@@ -131,14 +155,45 @@ static int sftp_get_client_version(conn_t *conn) {
      * if the client's version string does not begin with "SSH-2.0-"
      * (or "SSH-1.99-").  Works for me.
      */
-    if (strncmp(buf, "SSH-2.0-", 8) != 0) {
-      bad_proto = TRUE;
+    if (bad_proto == FALSE) {
+      if (strncmp(buf, "SSH-2.0-", 8) != 0) {
+        bad_proto = TRUE;
+
+        if (sftp_opts & SFTP_OPT_OLD_PROTO_COMPAT) {
+          if (strncmp(buf, "SSH-1.99-", 9) == 0) {
+            if (buflen == 9) {
+              /* The client sent ONLY "SSH-1.99-".  OpenSSH handles this as a
+               * "Protocol mismatch", so shall we.
+               */
+              bad_proto = TRUE;
+
+            } else {
+              banner = buf + 9;
+              bad_proto = FALSE;
+            }
+          }
+        }
 
-      if (sftp_opts & SFTP_OPT_OLD_PROTO_COMPAT) {
-        if (strncmp(buf, "SSH-1.99-", 9) == 0) {
-          bad_proto = FALSE;
+      } else {
+        if (buflen == 8) {
+          /* The client sent ONLY "SSH-2.0-".  OpenSSH handles this as a
+           * "Protocol mismatch", so shall we.
+           */
+          bad_proto = TRUE;
+
+        } else {
+          banner = buf + 8;
         }
-      } 
+      }
+    }
+
+    if (banner != NULL) {
+      char *k, *v;
+
+      k = pstrdup(session.pool, "SFTP_CLIENT_BANNER");
+      v = pstrdup(session.pool, banner);
+      pr_env_unset(session.pool, k);
+      pr_env_set(session.pool, k, v);
     }
 
     if (bad_proto) {
@@ -165,7 +220,7 @@ static int sftp_get_client_version(conn_t *conn) {
   (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
     "received client version '%s'", sftp_client_version);
 
-  if (sftp_interop_handle_version(sftp_client_version) < 0) {
+  if (sftp_interop_handle_version(sftp_pool, sftp_client_version) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error checking client version '%s' for interoperability: %s",
       sftp_client_version, strerror(errno));
@@ -279,55 +334,72 @@ MODRET set_sftpacceptenv(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: SFTPAuthMethods meth1 ... methN */
+/* usage: SFTPAuthMethods method-list1 ... method-listN */
 MODRET set_sftpauthmeths(cmd_rec *cmd) {
   register unsigned int i;
   config_rec *c;
-  char *meths = "";
-  unsigned int enabled = 0;
+  array_header *auth_chains;
 
-  if (cmd->argc < 2 ||
-      cmd->argc > 5) {
+  if (cmd->argc < 2) {
     CONF_ERROR(cmd, "Wrong number of parameters");
   }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  auth_chains = make_array(c->pool, 0, sizeof(struct sftp_auth_chain *));
+
   for (i = 1; i < cmd->argc; i++) {
-    if (strncasecmp(cmd->argv[i], "publickey", 10) == 0) {
-      enabled |= SFTP_AUTH_FL_METH_PUBLICKEY;
+    array_header *method_names;
+    register unsigned int j;
+    struct sftp_auth_chain *auth_chain;
 
-    } else if (strncasecmp(cmd->argv[i], "hostbased", 10) == 0) {
-      enabled |= SFTP_AUTH_FL_METH_HOSTBASED;
+    method_names = sftp_auth_chain_parse_method_chain(cmd->tmp_pool,
+      cmd->argv[i]);
+    if (method_names == NULL) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+        "invalid authentication parameter: ", (char *) cmd->argv[i], NULL));
+    }
 
-    } else if (strncasecmp(cmd->argv[i], "password", 9) == 0) {
-      enabled |= SFTP_AUTH_FL_METH_PASSWORD;
+    auth_chain = sftp_auth_chain_alloc(c->pool);
+    for (j = 0; j < method_names->nelts; j++) {
+      int res;
+      char *name;
+      unsigned int method_id = 0;
+      const char *method_name = NULL, *submethod_name = NULL;
 
-    } else if (strncasecmp(cmd->argv[i], "keyboard-interactive", 21) == 0) {
-      if (sftp_kbdint_have_drivers() == 0) {
-        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-          "unable to support '", cmd->argv[i],
-            "' authentication: No drivers loaded", NULL));
+      name = ((char **) method_names->elts)[j];
+
+      res = sftp_auth_chain_parse_method(c->pool, name, &method_id,
+        &method_name, &submethod_name);
+      if (res < 0) {
+        /* Make for a slightly better/more informative error message. */
+        if (method_id == SFTP_AUTH_FL_METH_KBDINT) {
+          CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+            "unsupported authentication method '", name,
+            "': No drivers loaded", NULL));
+
+        } else {
+          CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+            "unsupported authentication method '", name, "': ",
+            strerror(errno), NULL));
+        }
       }
 
-      enabled |= SFTP_AUTH_FL_METH_KBDINT;
+      sftp_auth_chain_add_method(auth_chain, method_id, method_name,
+        submethod_name);
+    }
 
-    } else {
+    if (sftp_auth_chain_isvalid(auth_chain) < 0) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-        "unsupported authentication method: ", cmd->argv[i], NULL));
+        "unsupportable chain of authentication methods '",
+        (char *) cmd->argv[i], "': ", strerror(errno), NULL));
     }
-  }
 
-  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
-
-  for (i = 1; i < cmd->argc; i++) {
-    meths = pstrcat(c->pool, meths, *meths ? "," : "", cmd->argv[i], NULL);
+    *((struct sftp_auth_chain **) push_array(auth_chains)) = auth_chain;
   }
-  c->argv[0] = meths;
-
-  c->argv[1] = pcalloc(c->pool, sizeof(unsigned int));
-  *((unsigned int *) c->argv[1]) = enabled;
 
+  c->argv[0] = auth_chains;
   return PR_HANDLED(cmd);
 }
 
@@ -357,7 +429,7 @@ MODRET set_sftpauthorizedkeys(cmd_rec *cmd) {
     ptr = strchr(cmd->argv[i], ':');
     if (ptr == NULL) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "badly formatted parameter: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
     }
     *ptr = '\0';
 
@@ -366,7 +438,7 @@ MODRET set_sftpauthorizedkeys(cmd_rec *cmd) {
      */
     if (sftp_keystore_supports_store(cmd->argv[i], requested_key_type) < 0) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unsupported key store: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
     }
 
     *ptr = ':';
@@ -396,7 +468,7 @@ MODRET set_sftpciphers(cmd_rec *cmd) {
   for (i = 1; i < cmd->argc; i++) {
     if (sftp_crypto_get_cipher(cmd->argv[i], NULL, NULL) == NULL) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-        "unsupported cipher algorithm: ", cmd->argv[i], NULL));
+        "unsupported cipher algorithm: ", (char *) cmd->argv[i], NULL));
     }
   }
 
@@ -418,13 +490,13 @@ MODRET set_sftpclientalive(cmd_rec *cmd) {
 
   count = atoi(cmd->argv[1]);
   if (count < 0) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "max count '", cmd->argv[1],
-      "' must be equal to or greater than zero", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "max count '",
+      (char *) cmd->argv[1], "' must be equal to or greater than zero", NULL));
   }
 
   interval = atoi(cmd->argv[2]);
   if (interval < 0) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "interval '", cmd->argv[2],
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "interval '", (char *) cmd->argv[2],
       "' must be equal to or greater than zero", NULL));
   }
 
@@ -472,8 +544,8 @@ MODRET set_sftpclientmatch(cmd_rec *cmd) {
     pr_regexp_error(res, pre, errstr, sizeof(errstr));
     pr_regexp_free(NULL, pre);
 
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1], "' failed regex "
-      "compilation: ", errstr, NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", (char *) cmd->argv[1],
+      "' failed regex compilation: ", errstr, NULL));
   }
 
   c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
@@ -813,6 +885,7 @@ MODRET set_sftpcompression(cmd_rec *cmd) {
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+#ifdef HAVE_ZLIB_H
   bool = get_boolean(cmd, 1);
   if (bool == -1) {
     if (strncasecmp(cmd->argv[1], "delayed", 8) != 0) {
@@ -822,6 +895,10 @@ MODRET set_sftpcompression(cmd_rec *cmd) {
 
     bool = 2;
   }
+#else
+  pr_log_debug(DEBUG0, MOD_SFTP_VERSION ": platform lacks zlib support, ignoring SFTPCompression");
+  bool = 0;
+#endif /* !HAVE_ZLIB_H */
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(int));
@@ -1037,6 +1114,34 @@ MODRET set_sftpextensions(cmd_rec *cmd) {
         "on this system; requires statvfs(3) support", cmd->argv[0]);
 #endif /* !HAVE_SYS_STATVFS_H */
 
+    } else if (strncasecmp(ext, "hardlink", 9) == 0) {
+      switch (action) {
+        case '-':
+          ext_flags &= ~SFTP_FXP_EXT_HARDLINK;
+          break;
+
+        case '+':
+          ext_flags |= SFTP_FXP_EXT_HARDLINK;
+          break;
+      }
+
+    } else if (strncasecmp(ext, "xattr", 8) == 0) {
+#ifdef HAVE_SYS_XATTR_H
+      switch (action) {
+        case '-':
+          ext_flags &= ~SFTP_FXP_EXT_XATTR;
+          break;
+
+        case '+':
+          ext_flags |= SFTP_FXP_EXT_XATTR;
+          break;
+      }
+#else
+      pr_log_debug(DEBUG0, "%s: xattr at proftpd.org extension not supported "
+        "on this system; requires extended attribute support",
+        (char *) cmd->argv[0]);
+#endif /* HAVE_SYS_XATTR_H */
+
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown extension: '",
         ext, "'", NULL)); 
@@ -1050,38 +1155,79 @@ MODRET set_sftpextensions(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: SFTPHostKey path|"agent:/..." */
+/* usage: SFTPHostKey path|"agent:/..."|"NoRSA"|"NoDSA"|"NoECDSA"" */
 MODRET set_sftphostkey(cmd_rec *cmd) {
   struct stat st;
+  int flags = 0;
+  config_rec *c;
+  const char *path = NULL;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (strncmp(cmd->argv[1], "agent:", 6) != 0) {
+  if (strncasecmp(cmd->argv[1], "NoRSA", 6) == 0) {
+    flags |= SFTP_HOSTKEY_FL_CLEAR_RSA_KEY;
+
+  } else if (strncasecmp(cmd->argv[1], "NoDSA", 6) == 0) {
+    flags |= SFTP_HOSTKEY_FL_CLEAR_DSA_KEY;
+
+  } else if (strncasecmp(cmd->argv[1], "NoECDSA", 8) == 0) {
+    flags |= SFTP_HOSTKEY_FL_CLEAR_ECDSA_KEY;
+  }
+
+  if (strncmp(cmd->argv[1], "agent:", 6) != 0 &&
+      flags == 0) {
     int res, xerrno;
 
-    if (*cmd->argv[1] != '/') {
-      CONF_ERROR(cmd, "must be an absolute path");
+    path = cmd->argv[1];
+    if (*path != '/') {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be an absolute path: ",
+        path, NULL));
     }
 
     PRIVS_ROOT
-    res = stat(cmd->argv[1], &st);
+    res = stat(path, &st);
     xerrno = errno;
     PRIVS_RELINQUISH
 
     if (res < 0) {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to check '", cmd->argv[1],
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to check '", path,
         "': ", strerror(xerrno), NULL));
     }
 
     if ((st.st_mode & S_IRWXG) ||
         (st.st_mode & S_IRWXO)) {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", cmd->argv[1],
-        "' as host key, as it is group- or world-accessible", NULL));
+      int insecure_hostkey_perms = FALSE;
+
+      /* Check for the InsecureHostKeyPerms SFTPOption. */
+      c = find_config(cmd->server->conf, CONF_PARAM, "SFTPOptions", FALSE);
+      while (c != NULL) {
+        unsigned long opts;
+
+        pr_signals_handle();
+
+        opts = *((unsigned long *) c->argv[0]);
+        if (opts & SFTP_OPT_INSECURE_HOSTKEY_PERMS) {
+          insecure_hostkey_perms = TRUE;
+          break;
+        }
+      }
+
+      if (insecure_hostkey_perms) {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_VERSION ": unable to use '%s' "
+          "as host key, as it is group- or world-accessible", path);
+
+      } else {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", path,
+          "' as host key, as it is group- or world-accessible", NULL));
+      }
     }
   }
 
-  (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  c = add_config_param_str(cmd->argv[0], 2, NULL, NULL);
+  c->argv[0] = pstrdup(c->pool, path);
+  c->argv[1] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[1]) = flags;
   return PR_HANDLED(cmd);
 }
 
@@ -1096,7 +1242,7 @@ MODRET set_sftpkeyblacklist(cmd_rec *cmd) {
         "' not an absolute path", NULL));
     }
 
-    if (!exists(cmd->argv[1])) {
+    if (!exists2(cmd->tmp_pool, cmd->argv[1])) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "path '", cmd->argv[1],
         "' not found", NULL));
     }
@@ -1123,6 +1269,9 @@ MODRET set_sftpkeyexchanges(cmd_rec *cmd) {
         strncmp(cmd->argv[i], "diffie-hellman-group14-sha1", 28) != 0 &&
 #if (OPENSSL_VERSION_NUMBER > 0x000907000L && defined(OPENSSL_FIPS)) || \
     (OPENSSL_VERSION_NUMBER > 0x000908000L)
+        strncmp(cmd->argv[i], "diffie-hellman-group14-sha256", 30) != 0 &&
+        strncmp(cmd->argv[i], "diffie-hellman-group16-sha512", 30) != 0 &&
+        strncmp(cmd->argv[i], "diffie-hellman-group18-sha512", 30) != 0 &&
         strncmp(cmd->argv[i], "diffie-hellman-group-exchange-sha256", 37) != 0 &&
 #endif
         strncmp(cmd->argv[i], "diffie-hellman-group-exchange-sha1", 35) != 0 &&
@@ -1131,6 +1280,9 @@ MODRET set_sftpkeyexchanges(cmd_rec *cmd) {
         strncmp(cmd->argv[i], "ecdh-sha2-nistp384", 19) != 0 &&
         strncmp(cmd->argv[i], "ecdh-sha2-nistp521", 19) != 0 &&
 #endif /* PR_USE_OPENSSL_ECC */
+#if defined(HAVE_SODIUM_H) && defined(HAVE_SHA256_OPENSSL)
+        strncmp(cmd->argv[i], "curve25519-sha256 at libssh.org", 22) != 0 &&
+#endif /* HAVE_SODIUM_H and HAVE_SHA256_OPENSSL */
         strncmp(cmd->argv[i], "rsa1024-sha1", 13) != 0) {
 
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
@@ -1149,6 +1301,62 @@ MODRET set_sftpkeyexchanges(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: SFTPKeyLimits limit1 ... limitN */
+MODRET set_sftpkeylimits(cmd_rec *cmd) {
+  register unsigned int i;
+  config_rec *c;
+
+  if (cmd->argc < 3 ||
+      ((cmd->argc-1) % 2 != 0)) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcasecmp(cmd->argv[i], "MinimumRSASize") == 0) {
+      int nbits;
+
+      nbits = atoi(cmd->argv[++i]);
+      if (nbits < 0) {
+        CONF_ERROR(cmd, "minimum key size must be zero or greater");
+      }
+
+      c->argv[0] = palloc(c->pool, sizeof(int));
+      *((int *) c->argv[0]) = nbits;
+
+    } else if (strcasecmp(cmd->argv[i], "MinimumDSASize") == 0) {
+      int nbits;
+
+      nbits = atoi(cmd->argv[++i]);
+      if (nbits < 0) {
+        CONF_ERROR(cmd, "minimum key size must be zero or greater");
+      }
+
+      c->argv[1] = palloc(c->pool, sizeof(int));
+      *((int *) c->argv[1]) = nbits;
+
+    } else if (strcasecmp(cmd->argv[i], "MinimumECSize") == 0) {
+      int nbits;
+
+      nbits = atoi(cmd->argv[++i]);
+      if (nbits < 0) {
+        CONF_ERROR(cmd, "minimum key size must be zero or greater");
+      }
+
+      c->argv[2] = palloc(c->pool, sizeof(int));
+      *((int *) c->argv[2]) = nbits;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown SFTPKeyLimit '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  return PR_HANDLED(cmd);
+}
+
 /* usage: SFTPLog path|"none" */
 MODRET set_sftplog(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
@@ -1232,9 +1440,25 @@ MODRET set_sftpoptions(cmd_rec *cmd) {
     } else if (strncmp(cmd->argv[i], "MatchKeySubject", 16) == 0) {
       opts |= SFTP_OPT_MATCH_KEY_SUBJECT;
 
-    } else if (strcmp(cmd->argv[1], "AllowInsecureLogin") == 0) {
+    } else if (strcmp(cmd->argv[i], "AllowInsecureLogin") == 0) {
       opts |= SFTP_OPT_ALLOW_INSECURE_LOGIN;
 
+    } else if (strcmp(cmd->argv[i], "InsecureHostKeyPerms") == 0) {
+      opts |= SFTP_OPT_INSECURE_HOSTKEY_PERMS;
+
+    } else if (strcmp(cmd->argv[i], "AllowWeakDH") == 0) {
+      opts |= SFTP_OPT_ALLOW_WEAK_DH;
+
+    } else if (strcmp(cmd->argv[i], "IgnoreFIFOs") == 0) {
+      opts |= SFTP_OPT_IGNORE_FIFOS;
+
+    } else if (strcmp(cmd->argv[i],
+               "IgnoreSFTPUploadExtendedAttributes") == 0) {
+      opts |= SFTP_OPT_IGNORE_SFTP_UPLOAD_XATTRS;
+
+    } else if (strcmp(cmd->argv[i], "IgnoreSFTPSetExtendedAttributes") == 0) {
+      opts |= SFTP_OPT_IGNORE_SFTP_SET_XATTRS;
+
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown SFTPOption '",
         cmd->argv[i], "'", NULL));
@@ -1250,26 +1474,29 @@ MODRET set_sftpoptions(cmd_rec *cmd) {
 /* usage: SFTPPassPhraseProvider path */
 MODRET set_sftppassphraseprovider(cmd_rec *cmd) {
   struct stat st;
+  char *path;
  
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
+
+  path = cmd->argv[1];
  
-  if (*cmd->argv[1] != '/') {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be a full path: '",
-      cmd->argv[1], "'", NULL));
+  if (*path != '/') {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be a full path: '", path, "'",
+      NULL));
   }
  
-  if (stat(cmd->argv[1], &st) < 0) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '", 
-      cmd->argv[1], "': ", strerror(errno), NULL));
+  if (stat(path, &st) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '", path, "': ",
+      strerror(errno), NULL));
   }
 
   if (!S_ISREG(st.st_mode)) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '",
-      cmd->argv[1], ": Not a regular file", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", path,
+      ": Not a regular file", NULL));
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
@@ -1414,7 +1641,14 @@ static void sftp_ban_host_ev(const void *event_data, void *user_data) {
 
   /* Only send an SSH2 DISCONNECT if we're dealing with an SSH2 client. */
   if (strncmp(proto, "SSH2", 5) == 0) {
-    sftp_disconnect_send(SFTP_SSH2_DISCONNECT_BY_APPLICATION, "Banned",
+    char *ban_msg = "Banned", *name;
+
+    name = user_data;
+    if (name != NULL) {
+      ban_msg = pstrcat(sftp_pool, "Host ", name, " has been banned", NULL);
+    }
+
+    sftp_disconnect_send(SFTP_SSH2_DISCONNECT_BY_APPLICATION, ban_msg,
       __FILE__, __LINE__, "");
   }
 }
@@ -1426,7 +1660,14 @@ static void sftp_ban_user_ev(const void *event_data, void *user_data) {
 
   /* Only send an SSH2 DISCONNECT if we're dealing with an SSH2 client. */
   if (strncmp(proto, "SSH2", 5) == 0) {
-    sftp_disconnect_send(SFTP_SSH2_DISCONNECT_BY_APPLICATION, "Banned",
+    char *ban_msg = "Banned", *name;
+
+    name = user_data;
+    if (name != NULL) {
+      ban_msg = pstrcat(sftp_pool, "User ", name, " has been banned", NULL);
+    }
+
+    sftp_disconnect_send(SFTP_SSH2_DISCONNECT_BY_APPLICATION, ban_msg,
       __FILE__, __LINE__, "");
   }
 }
@@ -1471,8 +1712,12 @@ static void sftp_mod_unload_ev(const void *event_data, void *user_data) {
 
 static void sftp_postparse_ev(const void *event_data, void *user_data) {
   config_rec *c;
+  server_rec *s;
 
   /* Initialize OpenSSL. */
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  OPENSSL_config(NULL);
+#endif /* prior to OpenSSL-1.1.x */
   ERR_load_crypto_strings();
   OpenSSL_add_all_algorithms();
 
@@ -1491,6 +1736,68 @@ static void sftp_postparse_ev(const void *event_data, void *user_data) {
     pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_VERSION
       ": error preparing interoperability checks: %s", strerror(errno));
   }
+
+  /* Check for incompatible SFTPAuthMethods configurations.  For example,
+   * configuring:
+   *
+   *  SFTPAuthMethods hostbased+password
+   *
+   * without also configuring SFTPAuthorizedHostKeys means that authentication
+   * will never succeed.
+   */
+  for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
+    int supports_hostbased = FALSE, supports_publickey = FALSE;
+
+    c = find_config(s->conf, CONF_PARAM, "SFTPAuthorizedHostKeys", FALSE);
+    if (c != NULL) {
+      supports_hostbased = TRUE;
+    }
+
+    c = find_config(s->conf, CONF_PARAM, "SFTPAuthorizedUserKeys", FALSE);
+    if (c != NULL) {
+      supports_publickey = TRUE;
+    }
+
+    c = find_config(s->conf, CONF_PARAM, "SFTPAuthMethods", FALSE);
+    if (c != NULL) {
+      register unsigned int i;
+      array_header *auth_chains;
+
+      auth_chains = c->argv[0];
+
+      for (i = 0; i < auth_chains->nelts; i++) {
+        register unsigned int j;
+        struct sftp_auth_chain *auth_chain;
+
+        auth_chain = ((struct sftp_auth_chain **) auth_chains->elts)[i];
+        for (j = 0; j < auth_chain->methods->nelts; j++) {
+          struct sftp_auth_method *meth;
+
+          meth = ((struct sftp_auth_method **) auth_chain->methods->elts)[j];
+
+          if (meth->method_id == SFTP_AUTH_FL_METH_HOSTBASED &&
+              supports_hostbased == FALSE) {
+            pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_VERSION
+              ": Server %s: cannot support authentication method '%s' "
+              "without SFTPAuthorizedHostKeys configuraion", s->ServerName,
+              meth->method_name);
+            pr_session_disconnect(&sftp_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+              NULL);
+          }
+
+          if (meth->method_id == SFTP_AUTH_FL_METH_PUBLICKEY &&
+              supports_publickey == FALSE) {
+            pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_VERSION
+              ": Server %s: cannot support authentication method '%s' "
+              "without SFTPAuthorizedUserKeys configuraion", s->ServerName,
+              meth->method_name);
+            pr_session_disconnect(&sftp_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+              NULL);
+          }
+        }
+      }
+    }
+  }
 }
 
 static void sftp_restart_ev(const void *event_data, void *user_data) {
@@ -1522,6 +1829,42 @@ static void sftp_shutdown_ev(const void *event_data, void *user_data) {
   }
 }
 
+static void sftp_timeoutlogin_ev(const void *event_data, void *user_data) {
+  if (sftp_sess_state & SFTP_SESS_STATE_HAVE_KEX) {
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
+  }
+}
+
+#ifdef PR_USE_DEVEL
+static void pool_printf(const char *fmt, ...) {
+  char buf[PR_TUNABLE_BUFFER_SIZE];
+  va_list msg;
+
+  memset(buf, '\0', sizeof(buf));
+
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf), fmt, msg);
+  va_end(msg);
+
+  buf[sizeof(buf)-1] = '\0';
+  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "%s", buf);
+}
+
+static void sftp_sigusr2_ev(const void *event_data, void *user_data) {
+  /* Note: the mod_shaper module deliberately uses the SIGUSR2 signal
+   * for handling shaping.  Thus we only want to dump out the pools
+   * IFF mod_shaper is NOT present.
+   */
+  if (pr_module_exists("mod_shaper.c") == FALSE) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "%s",
+      "-----BEGIN POOL DUMP-----");
+    pr_pool_debug_memory(pool_printf);
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION, "%s",
+      "-----END POOL DUMP-----");
+  }
+}
+#endif /* PR_USE_DEVEL */
+
 static void sftp_wrap_conn_denied_ev(const void *event_data, void *user_data) {
   const char *proto;
 
@@ -1529,16 +1872,21 @@ static void sftp_wrap_conn_denied_ev(const void *event_data, void *user_data) {
 
   /* Only send an SSH2 DISCONNECT if we're dealing with an SSH2 client. */
   if (strncmp(proto, "SSH2", 5) == 0) {
-    char *msg;
+    const char *msg;
 
     msg = get_param_ptr(main_server->conf, "WrapDenyMsg", FALSE);
     if (msg != NULL) {
+      const char *user;
+
+      user = session.user;
+      if (user == NULL) {
+        user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
+      }
+
       /* If the client has authenticated, we can interpolate any '%u'
        * variable in the configured deny message.
        */
-      if (sftp_sess_state & SFTP_SESS_STATE_HAVE_AUTH) {
-        msg = sreplace(sftp_pool, msg, "%u", session.user, NULL);
-      }
+      msg = sreplace(sftp_pool, msg, "%u", user, NULL);
 
     } else {
       /* XXX This needs to be properly localized.  However, trying to use
@@ -1612,6 +1960,20 @@ static int sftp_init(void) {
 
   pr_log_debug(DEBUG2, MOD_SFTP_VERSION ": using " OPENSSL_VERSION_TEXT);
 
+#if defined(HAVE_SODIUM_H)
+  if (sodium_init() < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_VERSION
+      ": error initializing libsodium");
+
+  } else {
+    const char *sodium_version;
+
+    sodium_version = sodium_version_string();
+    pr_log_debug(DEBUG2, MOD_SFTP_VERSION ": using libsodium-%s",
+      sodium_version);
+  }
+#endif /* HAVE_SODIUM_H */
+
   sftp_keystore_init();
   sftp_cipher_init();
   sftp_mac_init();
@@ -1634,6 +1996,8 @@ static int sftp_init(void) {
   pr_event_register(&sftp_module, "core.postparse", sftp_postparse_ev, NULL);
   pr_event_register(&sftp_module, "core.restart", sftp_restart_ev, NULL);
   pr_event_register(&sftp_module, "core.shutdown", sftp_shutdown_ev, NULL);
+  pr_event_register(&sftp_module, "core.timeout-login", sftp_timeoutlogin_ev,
+    NULL);
 
   return 0;
 }
@@ -1651,6 +2015,9 @@ static int sftp_sess_init(void) {
     return 0;
 
   pr_event_register(&sftp_module, "core.exit", sftp_exit_ev, NULL);
+#ifdef PR_USE_DEVEL
+  pr_event_register(&sftp_module, "core.signal.USR2", sftp_sigusr2_ev, NULL);
+#endif /* PR_USE_DEVEL */
   pr_event_register(&sftp_module, "mod_auth.max-clients",
     sftp_max_conns_ev, NULL);
   pr_event_register(&sftp_module, "mod_auth.max-clients-per-class",
@@ -1747,24 +2114,84 @@ static int sftp_sess_init(void) {
   sftp_pool = make_sub_pool(session.pool);
   pr_pool_tag(sftp_pool, MOD_SFTP_VERSION);
 
+  c = find_config(main_server->conf, CONF_PARAM, "SFTPOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    sftp_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "SFTPOptions", FALSE);
+  }
+
+  /* We do two passes through the configured hostkeys.  On the first pass,
+   * we focus on loading all of the configured keys.  On the second pass,
+   * we focus on handling any of the hostkey flags that would e.g. clear the
+   * previously loaded keys.
+   */
+
   c = find_config(main_server->conf, CONF_PARAM, "SFTPHostKey", FALSE);
   while (c) {
     const char *path = c->argv[0];
+    int flags = *((int *) c->argv[1]);
 
-    /* This pool needs to have the lifetime of the session, since the hostkey
-     * data is needed for rekeying, and rekeying can happen at any time
-     * during the session.
-     */
-    if (sftp_keys_get_hostkey(sftp_pool, path) < 0) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "error loading hostkey '%s', skipping key", path);
+    if (path != NULL &&
+        flags == 0) {
+      /* This pool needs to have the lifetime of the session, since the hostkey
+       * data is needed for rekeying, and rekeying can happen at any time
+       * during the session.
+       */
+      if (sftp_keys_get_hostkey(sftp_pool, path) < 0) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error loading hostkey '%s', skipping key", path);
+      }
     }
 
     c = find_config_next(c, c->next, CONF_PARAM, "SFTPHostKey", FALSE);
   }
 
-  /* Support having either an RSA hostkey, a DSA hostkey, or both.  But
-   * we have to have at least one hostkey.
+  c = find_config(main_server->conf, CONF_PARAM, "SFTPHostKey", FALSE);
+  while (c) {
+    int flags = *((int *) c->argv[1]);
+
+    if (flags != 0) {
+      /* Handle any flags, such as for clearing previous host keys. */
+      if (flags & SFTP_HOSTKEY_FL_CLEAR_RSA_KEY) {
+        if (sftp_keys_clear_rsa_hostkey() < 0) {
+          pr_trace_msg(trace_channel, 13,
+            "error clearing RSA hostkey: %s", strerror(errno));
+
+        } else {
+          pr_trace_msg(trace_channel, 9, "cleared RSA hostkey");
+        }
+
+      } else if (flags & SFTP_HOSTKEY_FL_CLEAR_DSA_KEY) {
+        if (sftp_keys_clear_dsa_hostkey() < 0) {
+          pr_trace_msg(trace_channel, 13,
+            "error clearing DSA hostkey: %s", strerror(errno));
+
+        } else {
+          pr_trace_msg(trace_channel, 9, "cleared DSA hostkey");
+        }
+
+      } else if (flags & SFTP_HOSTKEY_FL_CLEAR_ECDSA_KEY) {
+        if (sftp_keys_clear_ecdsa_hostkey() < 0) {
+          pr_trace_msg(trace_channel, 13,
+            "error clearing ECDSA hostkey(s): %s", strerror(errno));
+
+        } else {
+          pr_trace_msg(trace_channel, 9, "cleared ECDSA hostkey(s)");
+        }
+      }
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "SFTPHostKey", FALSE);
+  }
+
+  /* Support having either an RSA hostkey, a DSA hostkey, an ECDSA hostkey,
+   * or any combination thereof.  But we have to have at least one hostkey.
    */
   if (sftp_keys_have_dsa_hostkey() < 0 &&
       sftp_keys_have_rsa_hostkey() < 0 &&
@@ -1775,8 +2202,34 @@ static int sftp_sess_init(void) {
     return -1;
   }
 
+  c = find_config(main_server->conf, CONF_PARAM, "SFTPKeyLimits", FALSE);
+  if (c != NULL) {
+    int rsa_min = -1, dsa_min = -1, ec_min = -1;
+
+    if (c->argv[0] != NULL) {
+      rsa_min = *((int *) c->argv[0]);
+    }
+
+    if (c->argv[1] != NULL) {
+      dsa_min = *((int *) c->argv[1]);
+    }
+
+    if (c->argv[2] != NULL) {
+      ec_min = *((int *) c->argv[2]);
+    }
+
+    if (rsa_min > -1 ||
+        dsa_min > -1 ||
+        ec_min > -1) {
+      if (sftp_keys_set_key_limits(rsa_min, dsa_min, ec_min) < 0) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "error setting SFTPKeyLimits: %s", strerror(errno));
+      }
+    }
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "SFTPKeyBlacklist", FALSE);
-  if (c) {
+  if (c != NULL) {
     if (strncasecmp((char *) c->argv[0], "none", 5) != 0) {
       sftp_blacklist_set_file(c->argv[0]);
 
@@ -1791,18 +2244,6 @@ static int sftp_sess_init(void) {
     sftp_channel_set_max_count(*((unsigned int *) c->argv[0]));
   }
 
-  c = find_config(main_server->conf, CONF_PARAM, "SFTPOptions", FALSE);
-  while (c != NULL) {
-    unsigned long opts;
-
-    pr_signals_handle();
-
-    opts = *((unsigned long *) c->argv[0]);
-    sftp_opts |= opts;
-
-    c = find_config_next(c, c->next, CONF_PARAM, "SFTPOptions", FALSE);
-  }
-
   c = find_config(main_server->conf, CONF_PARAM, "DisplayLogin", FALSE);
   if (c) {
     const char *path;
@@ -1994,6 +2435,7 @@ static conftable sftp_conftab[] = {
   { "SFTPHostKey",		set_sftphostkey,		NULL },
   { "SFTPKeyBlacklist",		set_sftpkeyblacklist,		NULL },
   { "SFTPKeyExchanges",		set_sftpkeyexchanges,		NULL },
+  { "SFTPKeyLimits",		set_sftpkeylimits,		NULL },
   { "SFTPLog",			set_sftplog,			NULL },
   { "SFTPMaxChannels",		set_sftpmaxchannels,		NULL },
   { "SFTPOptions",		set_sftpoptions,		NULL },
diff --git a/contrib/mod_sftp/mod_sftp.h.in b/contrib/mod_sftp/mod_sftp.h.in
index 1765bde..ee093fe 100644
--- a/contrib/mod_sftp/mod_sftp.h.in
+++ b/contrib/mod_sftp/mod_sftp.h.in
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_sftp.h.in,v 1.30 2014-03-02 22:05:43 castaglia Exp $
  */
 
 #ifndef MOD_SFTP_H
@@ -45,6 +43,9 @@
 # define MIN(x, y) (((x) < (y)) ? (x) : (y))
 #endif
 
+/* Define if you have the <zlib.h> header.  */
+#undef HAVE_ZLIB_H
+
 /* Define if you have OpenSSL with crippled AES support. */
 #undef HAVE_AES_CRIPPLED_OPENSSL
 
@@ -54,7 +55,7 @@
 /* Define if you have OpenSSL with SHA512 support. */
 #undef HAVE_SHA512_OPENSSL
 
-#define MOD_SFTP_VERSION	"mod_sftp/0.9.9"
+#define MOD_SFTP_VERSION	"mod_sftp/1.0.0"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030402
@@ -62,16 +63,23 @@
 #endif
 
 #include <openssl/bio.h>
-#include <openssl/blowfish.h>
+#if !defined(OPENSSL_NO_BF)
+# include <openssl/blowfish.h>
+#endif /* !OPENSSL_NO_BF */
 #include <openssl/bn.h>
-#include <openssl/des.h>
+#include <openssl/conf.h>
+#if !defined(OPENSSL_NO_DES)
+# include <openssl/des.h>
+#endif /* !OPENSSL_NO_DES */
 #include <openssl/evp.h>
 #include <openssl/hmac.h>
 #include <openssl/x509v3.h>
 #include <openssl/err.h>
 #include <openssl/rand.h>
 #include <openssl/pem.h>
-#include <openssl/dsa.h>
+#if !defined(OPENSSL_NO_DSA)
+# include <openssl/dsa.h>
+#endif /* !OPENSSL_NO_DSA */
 #include <openssl/rsa.h>
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
 # include <openssl/aes.h>
@@ -83,10 +91,17 @@
 # include <openssl/ecdh.h>
 #endif /* PR_USE_OPENSSL_ECC */
 
-#include <zlib.h>
+/* Define if you have the LibreSSL library.  */
+#if defined(LIBRESSL_VERSION_NUMBER)
+# define HAVE_LIBRESSL	1
+#endif
 
 #define SFTP_ID_PREFIX		"SSH-2.0-"
-#define SFTP_ID_DEFAULT_STRING	SFTP_ID_PREFIX MOD_SFTP_VERSION
+
+/* Omit the version information in the default banner.  Sites wishing to use
+ * or see that version information can configure it explicitly via ServerIdent.
+ */
+#define SFTP_ID_DEFAULT_STRING	SFTP_ID_PREFIX "mod_sftp"
 
 /* mod_sftp session state flags */
 #define SFTP_SESS_STATE_HAVE_KEX	0x00001
@@ -105,6 +120,11 @@
 #define SFTP_OPT_IGNORE_SFTP_SET_OWNERS		0x0080
 #define SFTP_OPT_IGNORE_SCP_UPLOAD_TIMES	0x0100
 #define SFTP_OPT_ALLOW_INSECURE_LOGIN		0x0200
+#define SFTP_OPT_INSECURE_HOSTKEY_PERMS		0x0400
+#define SFTP_OPT_ALLOW_WEAK_DH			0x0800
+#define SFTP_OPT_IGNORE_FIFOS			0x1000
+#define SFTP_OPT_IGNORE_SFTP_UPLOAD_XATTRS	0x2000
+#define SFTP_OPT_IGNORE_SFTP_SET_XATTRS		0x2000
 
 /* mod_sftp service flags */
 #define SFTP_SERVICE_FL_SFTP		0x0001
@@ -114,6 +134,10 @@
 #define SFTP_SERVICE_DEFAULT \
 	(SFTP_SERVICE_FL_SFTP|SFTP_SERVICE_FL_SCP)
 
+/* mod_sftp roles */
+#define SFTP_ROLE_SERVER		1
+#define SFTP_ROLE_CLIENT		2
+
 /* Miscellaneous */
 extern int sftp_logfd;
 extern const char *sftp_logname;
diff --git a/contrib/mod_sftp/msg.c b/contrib/mod_sftp/msg.c
index e00d24a..1a2b297 100644
--- a/contrib/mod_sftp/msg.c
+++ b/contrib/mod_sftp/msg.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp message format
- * Copyright (c) 2008-2016 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -28,10 +28,6 @@
 #include "crypto.h"
 #include "disconnect.h"
 
-#ifdef HAVE_EXECINFO_H
-# include <execinfo.h>
-#endif
-
 #ifdef PR_USE_OPENSSL_ECC
 /* Max GFp field length = 528 bits.  SEC1 uncompressed encoding uses 2
  * bitstring points.  SEC1 specifies a 1 byte point type header.
@@ -45,44 +41,7 @@
  */
 static unsigned char msg_buf[8 * 1024];
 
-static void log_stacktrace(void) {
-#if defined(HAVE_EXECINFO_H) && \
-    defined(HAVE_BACKTRACE) && \
-    defined(HAVE_BACKTRACE_SYMBOLS)
-  void *trace[PR_TUNABLE_CALLER_DEPTH];
-  char **strings;
-  int tracesz;
-
-  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-    "-----BEGIN STACK TRACE-----");
-
-  tracesz = backtrace(trace, PR_TUNABLE_CALLER_DEPTH);
-  if (tracesz < 0) {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "backtrace(3) error: %s", strerror(errno));
-  }
-
-  strings = backtrace_symbols(trace, tracesz);
-  if (strings != NULL) {
-    register unsigned int i;
-
-    for (i = 1; i < tracesz; i++) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "[%u] %s", i-1, strings[i]);
-    }
-
-    /* Prevent memory leaks. */
-    free(strings);
-
-  } else {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error obtaining stacktrace symbols: %s", strerror(errno));
-  }
- 
-  (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-    "-----END STACK TRACE-----");
-#endif
-}
+static const char *trace_channel = "ssh2";
 
 unsigned char *sftp_msg_getbuf(pool *p, size_t sz) {
   if (sz <= sizeof(msg_buf)) {
@@ -101,7 +60,7 @@ char sftp_msg_read_byte(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read byte (buflen = %lu)",
       (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -132,10 +91,14 @@ unsigned char *sftp_msg_read_data(pool *p, unsigned char **buf,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read %lu bytes of raw data "
       "(buflen = %lu)", (unsigned long) datalen, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
+  if (datalen == 0) {
+    return NULL;
+  }
+
   data = palloc(p, datalen);
 
   memcpy(data, *buf, datalen);
@@ -154,7 +117,7 @@ uint32_t sftp_msg_read_int(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read int (buflen = %lu)",
       (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -174,6 +137,7 @@ uint64_t sftp_msg_read_long(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read long (buflen = %lu)",
       (unsigned long) *buflen);
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -204,7 +168,7 @@ BIGNUM *sftp_msg_read_mpint(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read %lu bytes of mpint (buflen = %lu)",
       (unsigned long) len, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -212,7 +176,7 @@ BIGNUM *sftp_msg_read_mpint(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to handle mpint of %lu bytes",
       (unsigned long) len);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -221,14 +185,14 @@ BIGNUM *sftp_msg_read_mpint(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read %lu bytes of mpint data",
       (unsigned long) len);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
   if ((ptr[0] & 0x80) != 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: negative mpint numbers not supported");
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -247,7 +211,7 @@ BIGNUM *sftp_msg_read_mpint(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to convert binary mpint: %s",
       sftp_crypto_get_errors());
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -258,6 +222,16 @@ char *sftp_msg_read_string(pool *p, unsigned char **buf, uint32_t *buflen) {
   uint32_t len = 0;
   char *str = NULL;
 
+  /* If there is no data remaining, treat this as if the string is empty
+   * (see Bug#4093).
+   */
+  if (*buflen == 0) {
+    pr_trace_msg(trace_channel, 9,
+      "malformed message format (buflen = %lu) for reading string, using \"\"",
+      (unsigned long) *buflen);
+    return "";
+  }
+
   len = sftp_msg_read_int(p, buf, buflen);
 
   /* We can't use sftp_msg_read_data() here, since we need to allocate and
@@ -269,15 +243,17 @@ char *sftp_msg_read_string(pool *p, unsigned char **buf, uint32_t *buflen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read %lu bytes of string data "
       "(buflen = %lu)", (unsigned long) len, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
   str = palloc(p, len + 1);
 
-  memcpy(str, *buf, len);
-  (*buf) += len;
-  (*buflen) -= len;
+  if (len > 0) {
+    memcpy(str, *buf, len);
+    (*buf) += len;
+    (*buflen) -= len;
+  }
   str[len] = '\0';
 
   return str;
@@ -290,20 +266,13 @@ EC_POINT *sftp_msg_read_ecpoint(pool *p, unsigned char **buf, uint32_t *buflen,
   unsigned char *data = NULL;
   uint32_t datalen = 0;
 
-  bn_ctx = BN_CTX_new();
-  if (bn_ctx == NULL) {
-    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-      "error allocating new BN_CTX: %s", sftp_crypto_get_errors());
-    return NULL;
-  }
-
   datalen = sftp_msg_read_int(p, buf, buflen);
 
   if (*buflen < datalen) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read %lu bytes of EC point"
       " (buflen = %lu)", (unsigned long) datalen, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -311,6 +280,7 @@ EC_POINT *sftp_msg_read_ecpoint(pool *p, unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: EC point length too long (%lu > max %lu)",
       (unsigned long) datalen, (unsigned long) MAX_ECPOINT_LEN);
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -319,7 +289,7 @@ EC_POINT *sftp_msg_read_ecpoint(pool *p, unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to read %lu bytes of EC point data",
       (unsigned long) datalen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -328,14 +298,23 @@ EC_POINT *sftp_msg_read_ecpoint(pool *p, unsigned char **buf, uint32_t *buflen,
       "message format error: EC point data formatted incorrectly "
       "(leading byte 0x%02x should be 0x%02x)", data[0],
       POINT_CONVERSION_UNCOMPRESSED);
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
+  bn_ctx = BN_CTX_new();
+  if (bn_ctx == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error allocating new BN_CTX: %s", sftp_crypto_get_errors());
+    return NULL;
+  }
+
   if (EC_POINT_oct2point(curve, point, data, datalen, bn_ctx) != 1) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to convert binary EC point data: %s",
       sftp_crypto_get_errors());
-    log_stacktrace();
+    BN_CTX_free(bn_ctx);
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -353,7 +332,7 @@ uint32_t sftp_msg_write_byte(unsigned char **buf, uint32_t *buflen, char byte) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write byte (buflen = %lu)",
       (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -382,7 +361,7 @@ uint32_t sftp_msg_write_data(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write %lu bytes of raw data "
       "(buflen = %lu)", (unsigned long) datalen, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -405,7 +384,7 @@ uint32_t sftp_msg_write_int(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write int (buflen = %lu)",
       (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -427,6 +406,7 @@ uint32_t sftp_msg_write_long(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write long (buflen = %lu)",
       (unsigned long) *buflen);
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -458,7 +438,7 @@ uint32_t sftp_msg_write_mpint(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write mpint (negative numbers not "
       "supported)");
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 #endif /* OpenSSL-0.9.8a or later */
@@ -469,7 +449,7 @@ uint32_t sftp_msg_write_mpint(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write %lu bytes of mpint (buflen = %lu)",
       (unsigned long) datalen, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -483,7 +463,7 @@ uint32_t sftp_msg_write_mpint(unsigned char **buf, uint32_t *buflen,
 
   res = BN_bn2bin(mpint, data + 1);
   if (res < 0 ||
-      res != (datalen - 1)) {
+      res != (int) (datalen - 1)) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: BN_bn2bin() failed: expected %lu bytes, got %d",
       (unsigned long) (datalen - 1), res);
@@ -531,7 +511,7 @@ uint32_t sftp_msg_write_ecpoint(unsigned char **buf, uint32_t *buflen,
   if (bn_ctx == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "error allocating new BN_CTX: %s", sftp_crypto_get_errors());
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -541,6 +521,7 @@ uint32_t sftp_msg_write_ecpoint(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: EC point length too long (%lu > max %lu)",
       (unsigned long) datalen, (unsigned long) MAX_ECPOINT_LEN);
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
@@ -548,7 +529,7 @@ uint32_t sftp_msg_write_ecpoint(unsigned char **buf, uint32_t *buflen,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "message format error: unable to write %lu bytes of EC point "
       "(buflen = %lu)", (unsigned long) datalen, (unsigned long) *buflen);
-    log_stacktrace();
+    pr_log_stacktrace(sftp_logfd, MOD_SFTP_VERSION);
     SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
   }
 
diff --git a/contrib/mod_sftp/msg.h b/contrib/mod_sftp/msg.h
index b74d618..6d65954 100644
--- a/contrib/mod_sftp/msg.h
+++ b/contrib/mod_sftp/msg.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp message format
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: msg.h,v 1.7 2013-03-28 19:56:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_MSG_H
 #define MOD_SFTP_MSG_H
 
+#include "mod_sftp.h"
+
 char sftp_msg_read_byte(pool *, unsigned char **, uint32_t *);
 int sftp_msg_read_bool(pool *, unsigned char **, uint32_t *);
 unsigned char *sftp_msg_read_data(pool *, unsigned char **, uint32_t *, size_t);
@@ -59,4 +57,4 @@ uint32_t sftp_msg_write_string(unsigned char **, uint32_t *, const char *);
  */
 unsigned char *sftp_msg_getbuf(pool *, size_t);
 
-#endif
+#endif /* MOD_SFTP_MSG_H */
diff --git a/contrib/mod_sftp/packet.c b/contrib/mod_sftp/packet.c
index 45a8265..2f09f2a 100644
--- a/contrib/mod_sftp/packet.c
+++ b/contrib/mod_sftp/packet.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp packet IO
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2017 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: packet.c,v 1.46 2013-03-08 16:22:18 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -85,11 +83,15 @@ static unsigned int client_alive_max = 0, client_alive_count = 0;
 static unsigned int client_alive_interval = 0;
 
 static const char *trace_channel = "ssh2";
+static const char *timing_channel = "timing";
+
+#define MAX_POLL_TIMEOUTS	3
 
 static int packet_poll(int sockfd, int io) {
   fd_set rfds, wfds;
   struct timeval tv;
   int res, timeout, using_client_alive = FALSE;
+  unsigned int ntimeouts = 0;
 
   if (poll_timeout == -1) {
     /* If we have "client alive" timeout interval configured, use that --
@@ -117,8 +119,9 @@ static int packet_poll(int sockfd, int io) {
   tv.tv_usec = 0;
 
   pr_trace_msg(trace_channel, 19,
-    "waiting for max of %lu secs while polling socket %d using select(2)",
-    (unsigned long) tv.tv_sec, sockfd);
+    "waiting for max of %lu secs while polling socket %d for %s "
+    "using select(2)", (unsigned long) tv.tv_sec, sockfd,
+    io == SFTP_PACKET_IO_RD ? "reading" : "writing");
 
   while (1) {
     pr_signals_handle();
@@ -162,16 +165,29 @@ static int packet_poll(int sockfd, int io) {
       tv.tv_sec = timeout;
       tv.tv_usec = 0;
 
+      ntimeouts++;
+
+      if (ntimeouts > MAX_POLL_TIMEOUTS) {
+        pr_trace_msg(trace_channel, 18,
+          "polling on socket %d timed out after %lu sec, failing", sockfd,
+          (unsigned long) tv.tv_sec);
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "polling on socket %d timed out after %lu sec, failing", sockfd,
+          (unsigned long) tv.tv_sec);
+        errno = ETIMEDOUT;
+        return -1;
+      }
+
       if (using_client_alive) {
         is_client_alive();
 
       } else {
         pr_trace_msg(trace_channel, 18,
-          "polling on socket %d timed out after %lu sec, trying again", sockfd,
-          (unsigned long) tv.tv_sec);
+          "polling on socket %d timed out after %lu sec, trying again "
+          "(timeout #%u)", sockfd, (unsigned long) tv.tv_sec, ntimeouts);
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "polling on socket %d timed out after %lu sec, trying again", sockfd,
-          (unsigned long) tv.tv_sec);
+          "polling on socket %d timed out after %lu sec, trying again "
+          "(timeout #%u)", sockfd, (unsigned long) tv.tv_sec, ntimeouts);
       }
 
       continue;
@@ -216,13 +232,13 @@ int sftp_ssh2_packet_sock_read(int sockfd, void *buf, size_t reqlen,
      * EAGAIN/EWOULDBLOCK errors.
      */
     res = read(sockfd, ptr, remainlen);
-
     while (res <= 0) {
       if (res < 0) {
         int xerrno = errno;
 
         if (xerrno == EINTR) {
           pr_signals_handle();
+          res = read(sockfd, ptr, remainlen);
           continue;
         }
 
@@ -286,8 +302,9 @@ int sftp_ssh2_packet_sock_read(int sockfd, void *buf, size_t reqlen,
     session.total_raw_in += reqlen;
     time(&last_recvd);
 
-    if (res == remainlen)
+    if ((size_t) res == remainlen) {
       break;
+    }
 
     if (flags & SFTP_PACKET_READ_FL_PESSIMISTIC) {
       pr_trace_msg(trace_channel, 20, "read %lu bytes, expected %lu bytes; "
@@ -906,8 +923,9 @@ int sftp_ssh2_packet_read(int sockfd, struct ssh2_packet *pkt) {
 
     if (pkt->packet_len < 5) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "packet length too long (%lu), less than minimum packet length (5)",
+        "packet length too short (%lu), less than minimum packet length (5)",
         (unsigned long) pkt->packet_len);
+      read_packet_discard(sockfd);
       return -1;
     }
 
@@ -915,6 +933,7 @@ int sftp_ssh2_packet_read(int sockfd, struct ssh2_packet *pkt) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "packet length too long (%lu), exceeds maximum packet length (%lu)",
         (unsigned long) pkt->packet_len, (unsigned long) SFTP_MAX_PACKET_LEN);
+      read_packet_discard(sockfd);
       return -1;
     }
 
@@ -1074,7 +1093,21 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
   unsigned char buf[SFTP_MAX_PACKET_LEN * 2], mesg_type;
   size_t buflen = 0, bufsz = SFTP_MAX_PACKET_LEN;
   uint32_t packet_len = 0;
-  int res, write_len = 0;
+  int res, write_len = 0, block_alarms = FALSE;
+
+  /* No interruptions, please.  If, for example, we are interrupted here
+   * by the SFTPRekey timer, that timer will cause this same function to
+   * be called -- but the packet_iov/packet_niov values will be different.
+   * Which in turn leads to malformed packets, and thus badness (Bug#4216).
+   */
+
+  if (sftp_sess_state & SFTP_SESS_STATE_HAVE_AUTH) {
+    block_alarms = TRUE;
+  }
+
+  if (block_alarms == TRUE) {
+    pr_alarms_block();
+  }
 
   /* Clear the iovec array before sending the data, if possible. */
   if (packet_niov == 0) {
@@ -1084,10 +1117,22 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
   mesg_type = peek_mesg_type(pkt);
 
   if (sftp_compress_write_data(pkt) < 0) {
+    int xerrno = errno;
+
+    if (block_alarms == TRUE) {
+      pr_alarms_unblock();
+    }
+    errno = xerrno;
     return -1;
   }
 
   if (write_packet_padding(pkt) < 0) {
+    int xerrno = errno;
+
+    if (block_alarms == TRUE) {
+      pr_alarms_unblock();
+    }
+    errno = xerrno;
     return -1;
   }
 
@@ -1098,6 +1143,12 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
   pkt->seqno = packet_server_seqno;
 
   if (sftp_mac_write_data(pkt) < 0) {
+    int xerrno = errno;
+
+    if (block_alarms == TRUE) {
+      pr_alarms_unblock();
+    }
+    errno = xerrno;
     return -1;
   }
 
@@ -1105,6 +1156,12 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
   buflen = bufsz;
 
   if (sftp_cipher_write_data(pkt, buf, &buflen) < 0) {
+    int xerrno = errno;
+
+    if (block_alarms == TRUE) {
+      pr_alarms_unblock();
+    }
+    errno = xerrno;
     return -1;
   }
 
@@ -1180,6 +1237,9 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
     memset(packet_iov, 0, sizeof(packet_iov));
     packet_niov = 0;
 
+    if (block_alarms == TRUE) {
+      pr_alarms_unblock();
+    }
     errno = xerrno;
     return -1;
   }
@@ -1212,6 +1272,10 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
         xerrno == ECONNABORTED ||
         xerrno == EPIPE) {
 
+      if (block_alarms == TRUE) {
+        pr_alarms_unblock();
+      }
+
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "disconnecting client (%s)", strerror(xerrno));
       pr_session_disconnect(&sftp_module, PR_SESS_DISCONNECT_BY_APPLICATION,
@@ -1222,6 +1286,9 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
     memset(packet_iov, 0, sizeof(packet_iov));
     packet_niov = 0;
 
+    if (block_alarms == TRUE) {
+      pr_alarms_unblock();
+    }
     errno = xerrno;
     return -1;
   }
@@ -1242,6 +1309,14 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
 
   packet_server_seqno++;
 
+  pr_trace_msg(trace_channel, 3, "sent %s (%d) packet (%d bytes)",
+    sftp_ssh2_packet_get_mesg_type_desc(mesg_type), mesg_type, res);
+
+  if (block_alarms == TRUE) {
+    /* Now that we've written out the packet, we can be interrupted again. */
+    pr_alarms_unblock();
+  }
+
   if (rekey_size > 0) {
     rekey_server_len += pkt->packet_len;
 
@@ -1261,9 +1336,6 @@ int sftp_ssh2_packet_send(int sockfd, struct ssh2_packet *pkt) {
     sftp_kex_rekey();
   }
 
-  pr_trace_msg(trace_channel, 3, "sent %s (%d) packet (%d bytes)",
-    sftp_ssh2_packet_get_mesg_type_desc(mesg_type), mesg_type, res);
- 
   return 0;
 }
 
@@ -1449,12 +1521,29 @@ int sftp_ssh2_packet_handle(void) {
       sftp_ssh2_packet_handle_unimplemented(pkt);
       break;
 
-    case SFTP_SSH2_MSG_KEXINIT:
+    case SFTP_SSH2_MSG_KEXINIT: {
+      uint64_t start_ms;
+
+      if (pr_trace_get_level(timing_channel) > 0) {
+        pr_gettimeofday_millis(&start_ms);
+      }
+
       /* The client might be initiating a rekey; watch for this. */
-      if (sftp_sess_state & SFTP_SESS_STATE_HAVE_KEX) {
-        sftp_sess_state |= SFTP_SESS_STATE_REKEYING;
+      if (!(sftp_sess_state & SFTP_SESS_STATE_HAVE_KEX)) {
+        if (pr_trace_get_level(timing_channel)) {
+          unsigned long elapsed_ms;
+          uint64_t finish_ms;
+
+          pr_gettimeofday_millis(&finish_ms);
+          elapsed_ms = (unsigned long) (finish_ms - session.connect_time_ms);
+
+          pr_trace_msg(timing_channel, 4,
+            "Time before first SSH key exchange: %lu ms", elapsed_ms);
+        }
       }
  
+      sftp_sess_state |= SFTP_SESS_STATE_REKEYING;
+
       /* Clear any current "have KEX" state. */
       sftp_sess_state &= ~SFTP_SESS_STATE_HAVE_KEX;
 
@@ -1465,6 +1554,17 @@ int sftp_ssh2_packet_handle(void) {
 
       sftp_sess_state |= SFTP_SESS_STATE_HAVE_KEX;
 
+      if (pr_trace_get_level(timing_channel)) {
+        unsigned long elapsed_ms;
+        uint64_t finish_ms;
+
+        pr_gettimeofday_millis(&finish_ms);
+        elapsed_ms = (unsigned long) (finish_ms - start_ms);
+
+        pr_trace_msg(timing_channel, 4,
+          "SSH key exchange duration: %lu ms", elapsed_ms);
+      }
+
       /* If we just finished rekeying, drain any of the pending channel
        * data which may have built up during the rekeying exchange.
        */
@@ -1473,6 +1573,7 @@ int sftp_ssh2_packet_handle(void) {
         sftp_channel_drain_data();
       }
       break;
+    }
 
     case SFTP_SSH2_MSG_SERVICE_REQUEST:
       if (sftp_sess_state & SFTP_SESS_STATE_HAVE_KEX) {
diff --git a/contrib/mod_sftp/packet.h b/contrib/mod_sftp/packet.h
index f76bd19..6cb3710 100644
--- a/contrib/mod_sftp/packet.h
+++ b/contrib/mod_sftp/packet.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp packet IO
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: packet.h,v 1.10 2012-03-11 18:44:03 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_PACKET_H
 #define MOD_SFTP_PACKET_H
 
+#include "mod_sftp.h"
+
 /* From RFC 4253, Section 6 */
 struct ssh2_packet {
   pool *pool;
@@ -114,4 +112,4 @@ int sftp_ssh2_packet_set_version(const char *);
 
 int sftp_ssh2_packet_set_client_alive(unsigned int, unsigned int);
 
-#endif
+#endif /* MOD_SFTP_PACKET_H */
diff --git a/contrib/mod_sftp/rfc4716.c b/contrib/mod_sftp/rfc4716.c
index 4bedd30..4550c12 100644
--- a/contrib/mod_sftp/rfc4716.c
+++ b/contrib/mod_sftp/rfc4716.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp RFC4716 keystore
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: rfc4716.c,v 1.19 2013-06-06 16:45:55 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -190,6 +188,13 @@ static char *filestore_getline(sftp_keystore_t *store, pool *p) {
 
         continue;
 
+      } else if (linelen < sizeof(linebuf)) {
+        /* No CR or LF terminator; maybe a badly formatted file?  Try to
+         * work with the data, if we can.
+         */
+        line = pstrcat(p, line, linebuf, NULL);
+        return line;
+
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "line too long (%lu) on line %u of '%s'", (unsigned long) linelen,
@@ -285,7 +290,7 @@ static struct filestore_key *filestore_get_key(sftp_keystore_t *store,
 
         if (data != NULL &&
             datalen > 0) {
-          key->key_data = pcalloc(p, datalen + 1);
+          key->key_data = palloc(p, datalen);
           key->key_datalen = datalen;
           memcpy(key->key_data, data, datalen);
 
@@ -390,6 +395,7 @@ static int filestore_verify_user_key(sftp_keystore_t *store, pool *p,
     const char *user, unsigned char *key_data, uint32_t key_len) {
   struct filestore_key *key = NULL;
   struct filestore_data *store_data = store->keystore_data;
+  unsigned int count = 0;
 
   int res = -1;
 
@@ -407,6 +413,7 @@ static int filestore_verify_user_key(sftp_keystore_t *store, pool *p,
     int ok;
 
     pr_signals_handle();
+    count++;
 
     ok = sftp_keys_compare_keys(p, key_data, key_len, key->key_data,
       key->key_datalen);
@@ -415,10 +422,14 @@ static int filestore_verify_user_key(sftp_keystore_t *store, pool *p,
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "error comparing keys from '%s': %s", store_data->path,
           strerror(errno));
+
+      } else {
+        pr_trace_msg(trace_channel, 10,
+          "failed to match key #%u from file '%s'", count, store_data->path);
       }
 
     } else {
-      /* If we are configured to check for Subject headers, and If the file key
+      /* If we are configured to check for Subject headers, and if the file key
        * has a Subject header, and that header value does not match the
        * logging in user, then continue looking.
        */
@@ -513,14 +524,31 @@ static sftp_keystore_t *filestore_open(pool *parent_pool,
     return NULL;
   }
 
-  pr_fsio_set_block(fh);
+  if (pr_fsio_set_block(fh) < 0) {
+   xerrno = errno;
+
+    destroy_pool(filestore_pool);
+    (void) pr_fsio_close(fh);
+
+    errno = xerrno;
+    return NULL;
+  }
 
   /* Stat the opened file to determine the optimal buffer size for IO. */
   memset(&st, 0, sizeof(st));
-  pr_fsio_fstat(fh, &st);
+  if (pr_fsio_fstat(fh, &st) < 0) {
+    xerrno = errno;
+
+    destroy_pool(filestore_pool);
+    (void) pr_fsio_close(fh);
+
+    errno = xerrno;
+    return NULL;
+  }
+
   if (S_ISDIR(st.st_mode)) {
     destroy_pool(filestore_pool);
-    pr_fsio_close(fh);
+    (void) pr_fsio_close(fh);
 
     errno = EISDIR;
     return NULL;
diff --git a/contrib/mod_sftp/rfc4716.h b/contrib/mod_sftp/rfc4716.h
index ecc2a3e..28902c4 100644
--- a/contrib/mod_sftp/rfc4716.h
+++ b/contrib/mod_sftp/rfc4716.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp public key store (RFC4716 public key file format)
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,16 +20,14 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: rfc4716.h,v 1.4 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_RFC4716_H
 #define MOD_SFTP_RFC4716_H
 
+#include "mod_sftp.h"
+
 int sftp_rfc4716_init(void);
 int sftp_rfc4716_free(void);
 
-#endif
+#endif /* MOD_SFTP_RFC4716_H */
diff --git a/contrib/mod_sftp/scp.c b/contrib/mod_sftp/scp.c
index e7e81af..80dab2e 100644
--- a/contrib/mod_sftp/scp.c
+++ b/contrib/mod_sftp/scp.c
@@ -38,6 +38,8 @@
  */
 #define SFTP_SCP_MAX_CTL_LEN	(PR_TUNABLE_PATH_MAX + 256)
 
+extern pr_response_t *resp_list, *resp_err_list;
+
 struct scp_path {
   char *path;
 
@@ -709,9 +711,27 @@ static int recv_filename(pool *p, uint32_t channel_id, char *name_str,
     sp->filename = pdircat(scp_pool, sp->path, name_str, NULL);
   }
 
-  if (sp->filename) {
+  if (sp->filename != NULL) {
+    struct stat st;
+
     sp->best_path = dir_canonical_vpath(scp_pool, sp->filename);
 
+    pr_fs_clear_cache2(sp->best_path);
+    if (pr_fsio_lstat(sp->best_path, &st) == 0) {
+      if (S_ISLNK(st.st_mode)) {
+        char link_path[PR_TUNABLE_PATH_MAX];
+        int len;
+
+        memset(link_path, '\0', sizeof(link_path));
+        len = dir_readlink(scp_pool, sp->best_path, link_path,
+          sizeof(link_path)-1, PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+        if (len > 0) {
+          link_path[len] = '\0';
+          sp->best_path = pstrdup(scp_pool, link_path);
+        }
+      }
+    }
+
     /* Update the session.xfer.path value with this better, fuller path. */
     session.xfer.path = pstrdup(session.xfer.p, sp->best_path);
   }
@@ -724,7 +744,8 @@ static int recv_filename(pool *p, uint32_t channel_id, char *name_str,
 static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
     unsigned char *buf, uint32_t buflen) {
   register unsigned int i;
-  char *hiddenstore_path = NULL;
+  const char *hiddenstore_path = NULL;
+  struct stat st;
   unsigned char *data = NULL, *msg;
   uint32_t datalen = 0;
   char *ptr = NULL;
@@ -823,9 +844,9 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
   sp->recvd_finfo = TRUE;
 
   if (have_dir) {
-    struct stat st;
     struct scp_path *parent_sp;
 
+    pr_fs_clear_cache2(sp->filename);
     if (pr_fsio_stat(sp->filename, &st) < 0) {
       int xerrno = errno;
 
@@ -837,6 +858,7 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
          * recursive directory uploads via SCP?
          */
 
+        pr_fs_clear_cache2(sp->filename);
         if (pr_fsio_smkdir(p, sp->filename, 0777, (uid_t) -1, (gid_t) -1) < 0) {
           xerrno = errno;
 
@@ -851,7 +873,7 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
           return 1;
         }
 
-        sftp_misc_chown_path(sp->filename);
+        sftp_misc_chown_path(p, sp->filename);
 
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -923,7 +945,8 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
 
   cmd = scp_cmd_alloc(p, C_STOR, sp->best_path);
 
-  if (exists(sp->best_path)) {
+  pr_fs_clear_cache2(sp->best_path);
+  if (exists2(p, sp->best_path)) {
     if (pr_table_add(cmd->notes, "mod_xfer.file-modified",
         pstrdup(cmd->pool, "true"), 0) < 0) {
       if (errno != EEXIST) {
@@ -939,7 +962,7 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
   if (pr_cmd_dispatch_phase(cmd, PRE_CMD, 0) < 0) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "scp upload to '%s' blocked by '%s' handler", sp->path,
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
 
     (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
     (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
@@ -986,9 +1009,9 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
   if (sp->fh == NULL) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
       "error opening '%s': %s", "scp upload", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
+      pr_uid2str(cmd->tmp_pool, session.uid), pr_gid2str(NULL, session.gid),
       hiddenstore_path ? hiddenstore_path : sp->best_path, strerror(xerrno));
 
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -1004,15 +1027,69 @@ static int recv_finfo(pool *p, uint32_t channel_id, struct scp_path *sp,
 
     errno = xerrno;
     return 1;
+
+  } else {
+    off_t curr_offset;
+
+    /* Stash the offset at which we're writing to this file. */
+    curr_offset = pr_fsio_lseek(sp->fh, (off_t) 0, SEEK_CUR);
+    if (curr_offset != (off_t) -1) {
+      off_t *file_offset;
+
+      file_offset = palloc(cmd->pool, sizeof(off_t));
+      *file_offset = (off_t) curr_offset;
+      (void) pr_table_add(cmd->notes, "mod_xfer.file-offset", file_offset,
+        sizeof(off_t));
+    }
   }
 
   if (hiddenstore_path) {
     sp->hiddenstore = TRUE;
   }
 
-  pr_fsio_set_block(sp->fh);
+  if (pr_fsio_fstat(sp->fh, &st) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "fstat(2) error on '%s': %s", sp->fh->fh_path, strerror(errno));
+
+  } else {
+    /* The path in question might be a FIFO.  The FIFO case requires some
+     * special handling, modulo any IgnoreFIFOs SFTPOption that might be in
+     * effect.
+     */
+#ifdef S_ISFIFO
+    if (S_ISFIFO(st.st_mode)) {
+      if (sftp_opts & SFTP_OPT_IGNORE_FIFOS) {
+        int xerrno = EPERM;
+
+        (void) pr_fsio_close(sp->fh);
+        sp->fh = NULL;
+
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "scp: error using FIFO '%s': %s (IgnoreFIFOs SFTPOption in effect)",
+          hiddenstore_path ? hiddenstore_path : sp->best_path,
+          strerror(xerrno));
+
+        (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
+        (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+        write_confirm(p, channel_id, 1,
+          pstrcat(p, sp->filename, ": ", strerror(xerrno), NULL));
+        sp->wrote_errors = TRUE;
+
+        errno = xerrno;
+        return 1;
+      }
+    }
+#endif /* S_ISFIFO */
+  }
+
+  if (pr_fsio_set_block(sp->fh) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error setting fd %d (file '%s') as blocking: %s", sp->fh->fh_fd,
+      sp->fh->fh_path, strerror(errno));
+  }
 
-  sftp_misc_chown_file(sp->fh);
+  sftp_misc_chown_file(p, sp->fh);
 
   write_confirm(p, channel_id, 0, NULL);
   return 0;
@@ -1070,8 +1147,11 @@ static int recv_data(pool *p, uint32_t channel_id, struct scp_path *sp,
 
   if (writelen > 0) {
     while (TRUE) {
+      int res;
+
       /* XXX Do we need to properly handle short writes here? */
-      if (pr_fsio_write(sp->fh, (char *) data, writelen) != writelen) {
+      res = pr_fsio_write(sp->fh, (char *) data, writelen);
+      if ((uint32_t) res != writelen) {
         int xerrno = errno;
 
         if (xerrno == EINTR ||
@@ -1172,7 +1252,7 @@ static int recv_eod(pool *p, uint32_t channel_id, struct scp_path *sp,
       write_confirm(p, channel_id, 1,
         pstrcat(p, parent_sp->path, ": error setting mode: ", strerror(xerrno),
         NULL));
-      sp->wrote_errors = TRUE;
+      parent_sp->wrote_errors = TRUE;
       ok = FALSE;
     }
 
@@ -1238,6 +1318,7 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
   if (!sp->have_mode) {
     struct stat st;
 
+    pr_fs_clear_cache2(sp->path);
     res = pr_fsio_stat(sp->path, &st);
     if (res == 0) {
       sp->st_mode = st.st_mode;
@@ -1273,6 +1354,7 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
       if (ptr != NULL) {
         *ptr = '\0';
 
+        pr_fs_clear_cache2(sp->path);
         res = pr_fsio_stat(sp->path, &st);
         *ptr = '/';
 
@@ -1404,6 +1486,7 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
 
         pr_trace_msg(trace_channel, 2, "error truncating '%s' to %" PR_LU
           " bytes: %s", sp->best_path, (pr_off_t) sp->filesz, strerror(xerrno));
+
         write_confirm(p, channel_id, 1,
           pstrcat(p, sp->filename, ": error truncating file: ",
           strerror(xerrno), NULL));
@@ -1425,6 +1508,7 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
 
         pr_trace_msg(trace_channel, 2, "error setting mode %04o on '%s': %s",
           (unsigned int) sp->perms, sp->best_path, strerror(xerrno));
+
         write_confirm(p, channel_id, 1,
           pstrcat(p, sp->filename, ": error setting mode: ", strerror(xerrno),
           NULL));
@@ -1449,6 +1533,7 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
         "scp: error closing '%s': %s", sp->best_path, strerror(xerrno));
+
       write_confirm(p, channel_id, 1,
         pstrcat(p, sp->filename, ": ", strerror(xerrno), NULL));
       sp->wrote_errors = TRUE;
@@ -1459,13 +1544,12 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
 
   if (sp->hiddenstore == TRUE &&
       curr_path != NULL) {
-
     if (sp->wrote_errors == TRUE) {
       /* There was an error writing this HiddenStores file; be sure to clean
        * things up.
        */
       pr_trace_msg(trace_channel, 8, "deleting HiddenStores path '%s'",
-        curr_path); 
+        curr_path);
 
       if (pr_fsio_unlink(curr_path) < 0) {
         if (errno != ENOENT) {
@@ -1479,7 +1563,6 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
       /* This is a HiddenStores file, and needs to be renamed to the real
        * path (i.e. sp->best_path).
        */
-
       pr_trace_msg(trace_channel, 8,
         "renaming HiddenStores path '%s' to '%s'", curr_path, sp->best_path);
 
@@ -1495,11 +1578,9 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
           curr_path, sp->best_path, strerror(xerrno));
 
         if (pr_fsio_unlink(curr_path) < 0) {
-          if (errno != ENOENT) {
-            pr_trace_msg(trace_channel, 1,
-              "error deleting HiddenStores file '%s': %s", curr_path,
-              strerror(errno));
-          }
+          pr_trace_msg(trace_channel, 1,
+            "error deleting HiddenStores file '%s': %s", curr_path,
+            strerror(errno));
         }
       }
     }
@@ -1522,6 +1603,7 @@ static int recv_path(pool *p, uint32_t channel_id, struct scp_path *sp,
           "error setting atime %lu, mtime %lu on '%s': %s",
           (unsigned long) sp->times[0].tv_sec,
           (unsigned long) sp->times[1].tv_sec, sp->best_path, strerror(xerrno));
+
         write_confirm(p, channel_id, 1,
           pstrcat(p, sp->filename, ": error setting times: ", strerror(xerrno),
           NULL));
@@ -1808,6 +1890,7 @@ static int send_data(pool *p, uint32_t channel_id, struct scp_path *sp,
 static int send_dir(pool *p, uint32_t channel_id, struct scp_path *sp,
     struct stat *st) {
   struct dirent *dent;
+  struct stat link_st;
   int res = 0;
 
   if (sp->dirh == NULL) {
@@ -1831,8 +1914,9 @@ static int send_dir(pool *p, uint32_t channel_id, struct scp_path *sp,
 
   if (sp->dir_spi) { 
     res = send_path(p, channel_id, sp->dir_spi);
-    if (res <= 0)
+    if (res <= 0) {
       return res;
+    }
 
     /* Clear out any transfer-specific data. */
     if (session.xfer.p) {
@@ -1871,6 +1955,22 @@ static int send_dir(pool *p, uint32_t channel_id, struct scp_path *sp,
 
     spi->best_path = dir_canonical_vpath(scp_pool, spi->path);
 
+    pr_fs_clear_cache2(spi->best_path);
+    if (pr_fsio_lstat(spi->best_path, &link_st) == 0) {
+      if (S_ISLNK(link_st.st_mode)) {
+        char link_path[PR_TUNABLE_PATH_MAX];
+        int len;
+
+        memset(link_path, '\0', sizeof(link_path));
+        len = dir_readlink(scp_pool, spi->best_path, link_path,
+          sizeof(link_path)-1, PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+        if (len > 0) {
+          link_path[len] = '\0';
+          spi->best_path = pstrdup(scp_pool, link_path);
+        }
+      }
+    }
+
     if (pathlen > 0) {
       sp->dir_spi = spi;
 
@@ -1896,8 +1996,9 @@ static int send_dir(pool *p, uint32_t channel_id, struct scp_path *sp,
 
     need_confirm = TRUE;
     res = sftp_channel_write_data(p, channel_id, (unsigned char *) "E\n", 2);
-    if (res < 0)
+    if (res < 0) {
       return res;
+    }
   }
 
   return 1;
@@ -1908,7 +2009,7 @@ static int send_dir(pool *p, uint32_t channel_id, struct scp_path *sp,
  * never send it (due to some error).
  */
 static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
-  int res;
+  int res, is_file = FALSE;
   struct stat st;
   cmd_rec *cmd = NULL;
 
@@ -1939,7 +2040,7 @@ static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
       if (xerrno != EISDIR) {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "scp download of '%s' blocked by '%s' handler", sp->path,
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
 
         (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
         (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
@@ -1959,6 +2060,22 @@ static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
     }
   }
 
+  pr_fs_clear_cache2(sp->path);
+  if (pr_fsio_lstat(sp->path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(scp_pool, sp->path, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        sp->path = pstrdup(scp_pool, link_path);
+      }
+    }
+  }
+
   if (pr_fsio_stat(sp->path, &st) < 0) {
     int xerrno = errno;
 
@@ -1987,37 +2104,36 @@ static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
     return 1;
   }
 
-  if (!S_ISREG(st.st_mode)
+  /* The path in question might be a file, a directory, or a FIFO.  The FIFO
+   * case requires some special handling, modulo any IgnoreFIFOs SFTPOption
+   * that might be in effect.
+   */
+  if (S_ISREG(st.st_mode)) {
+    is_file = TRUE;
+
+  } else {
 #ifdef S_ISFIFO
-      && !S_ISFIFO(st.st_mode)
-#endif
-     ) {
+    if (S_ISFIFO(st.st_mode)) {
+      is_file = TRUE;
+
+      if (sftp_opts & SFTP_OPT_IGNORE_FIFOS) {
+        is_file = FALSE;
+      }
+    }
+#endif /* S_ISFIFO */
+  }
+
+  if (is_file == FALSE) {
     if (S_ISDIR(st.st_mode)) {
       if (scp_opts & SFTP_SCP_OPT_RECURSE) {
         res = send_dir(p, channel_id, sp, &st);
         destroy_pool(cmd->pool);
         session.curr_cmd_rec = NULL;
         return res;
-
-      } else {
-        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-          "cannot send directory '%s' (no -r option)", sp->path);
-
-        (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-        (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
-
-        destroy_pool(cmd->pool);
-        session.curr_cmd_rec = NULL;
-
-        write_confirm(p, channel_id, 1,
-          pstrcat(p, sp->path, ": ", strerror(EPERM), NULL));
-        sp->wrote_errors = TRUE;
-        return 1;
       }
 
-    } else {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "cannot send '%s': Not a regular file", sp->path);
+        "cannot send directory '%s' (no -r option)", sp->path);
 
       (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
       (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
@@ -2030,6 +2146,20 @@ static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
       sp->wrote_errors = TRUE;
       return 1;
     }
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "cannot send '%s': Not a regular file", sp->path);
+
+    (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
+    (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+
+    destroy_pool(cmd->pool);
+    session.curr_cmd_rec = NULL;
+
+    write_confirm(p, channel_id, 1,
+      pstrcat(p, sp->path, ": ", strerror(EPERM), NULL));
+    sp->wrote_errors = TRUE;
+    return 1;
   }
 
   if (sp->fh == NULL) {
@@ -2055,9 +2185,9 @@ static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
     if (sp->fh == NULL) {
       int xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
         "error opening '%s': %s", "scp download", session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
+        pr_uid2str(cmd->tmp_pool, session.uid), pr_gid2str(NULL, session.gid),
         sp->best_path, strerror(xerrno));
 
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
@@ -2075,10 +2205,28 @@ static int send_path(pool *p, uint32_t channel_id, struct scp_path *sp) {
 
       errno = xerrno;
       return 1;
+
+    } else {
+      off_t curr_offset;
+
+      /* Stash the offset at which we're reading from this file. */
+      curr_offset = pr_fsio_lseek(sp->fh, (off_t) 0, SEEK_CUR);
+      if (curr_offset != (off_t) -1) {
+        off_t *file_offset;
+
+        file_offset = palloc(cmd->pool, sizeof(off_t));
+        *file_offset = (off_t) curr_offset;
+        (void) pr_table_add(cmd->notes, "mod_xfer.file-offset", file_offset,
+          sizeof(off_t));
+      }
     }
   }
 
-  pr_fsio_set_block(sp->fh);
+  if (pr_fsio_set_block(sp->fh) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error setting fd %d (file '%s') as blocking: %s", sp->fh->fh_fd,
+      sp->fh->fh_path, strerror(errno));
+  }
 
   if (session.xfer.p == NULL) {
     session.xfer.p = pr_pool_create_sz(scp_pool, 64);
@@ -2175,6 +2323,8 @@ int sftp_scp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
     pr_timer_reset(PR_TIMER_STALLED, ANY_MODULE);
   }
 
+  pr_response_set_pool(pkt->pool);
+
   if (need_confirm) {
     /* Handle the confirmation/response from the client. */
     if (read_confirm(pkt, &data, &datalen) < 0) {
@@ -2201,8 +2351,9 @@ int sftp_scp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
       pr_signals_handle();
 
       res = send_path(pkt->pool, channel_id, paths[scp_session->path_idx]);
-      if (res < 0)
+      if (res < 0) {
         return -1;
+      }
 
       if (res == 1) {
         /* If send_path() returns 1, it means we've finished that path,
@@ -2215,6 +2366,12 @@ int sftp_scp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
           destroy_pool(session.xfer.p);
         }
         memset(&session.xfer, 0, sizeof(session.xfer));
+
+        /* Make sure to clear the response lists of any cruft from previous
+         * requests.
+         */
+        pr_response_clear(&resp_list);
+        pr_response_clear(&resp_err_list);
       }
     }
 
@@ -2256,8 +2413,9 @@ int sftp_scp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
 
     res = recv_path(pkt->pool, channel_id, paths[scp_session->path_idx], data,
       datalen);
-    if (res < 0)
+    if (res < 0) {
       return -1;
+    }
 
     if (res == 1) {
       /* Clear out any transfer-specific data. */
@@ -2266,6 +2424,12 @@ int sftp_scp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
       }
       memset(&session.xfer, 0, sizeof(session.xfer));
 
+      /* Make sure to clear the response lists of any cruft from previous
+       * requests.
+       */
+      pr_response_clear(&resp_list);
+      pr_response_clear(&resp_err_list);
+
       /* Note: we don't increment path_idx here because when we're receiving
        * files (i.e. it's an SCP upload), we either receive a single file,
        * or a single (recursive) directory.  Therefore, there are not
@@ -2500,6 +2664,7 @@ int sftp_scp_set_params(pool *p, uint32_t channel_id, array_header *req) {
       paths->paths->nelts != 1) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "'scp' request provided more than one destination path, ignoring");
+    errno = EINVAL;
     return -1;
   }
 
@@ -2600,6 +2765,10 @@ int sftp_scp_open_session(uint32_t channel_id) {
   }
 
   pr_session_set_protocol("scp");
+
+  /* Clear any ASCII flags (set by default for FTP sessions. */
+  session.sf_flags &= ~SF_ASCII;
+
   return 0;
 }
 
diff --git a/contrib/mod_sftp/scp.h b/contrib/mod_sftp/scp.h
index 11ec668..2708e95 100644
--- a/contrib/mod_sftp/scp.h
+++ b/contrib/mod_sftp/scp.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp SCP (Secure Copy Protocol)
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: scp.h,v 1.5 2012-02-15 23:50:51 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_SCP_H
 #define MOD_SFTP_SCP_H
 
+#include "mod_sftp.h"
+
 int sftp_scp_handle_packet(pool *, void *, uint32_t, unsigned char *, uint32_t);
 
 int sftp_scp_open_session(uint32_t);
@@ -39,4 +37,4 @@ int sftp_scp_close_session(uint32_t);
  */
 int sftp_scp_set_params(pool *, uint32_t, array_header *);
 
-#endif
+#endif /* MOD_SFTP_SCP_H */
diff --git a/contrib/mod_sftp/service.c b/contrib/mod_sftp/service.c
index 437992c..5c4c04c 100644
--- a/contrib/mod_sftp/service.c
+++ b/contrib/mod_sftp/service.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp services
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2015 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: service.c,v 1.8 2012-02-15 23:50:51 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -46,9 +44,10 @@ static int read_service_req(struct ssh2_packet *pkt, char **service) {
   service_name = sftp_msg_read_string(pkt->pool, &buf, &buflen);
   pr_trace_msg(trace_channel, 10, "'%s' service requested", service_name);
 
-  cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "SERVICE_REQUEST"));
+  cmd = pr_cmd_alloc(pkt->pool, 1, pstrdup(pkt->pool, "SERVICE_REQUEST"),
+    pstrdup(pkt->pool, service_name));
   cmd->arg = service_name;
-  cmd->cmd_class = CL_MISC;
+  cmd->cmd_class = CL_MISC|CL_SSH;
 
   if (strncmp(service_name, "ssh-userauth", 13) == 0 ||
       strncmp(service_name, "ssh-connection", 14) == 0) {
diff --git a/contrib/mod_sftp/service.h b/contrib/mod_sftp/service.h
index 0be2b36..3141fb7 100644
--- a/contrib/mod_sftp/service.h
+++ b/contrib/mod_sftp/service.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp services (service)
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,18 +20,15 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: service.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_SERVICE_H
 #define MOD_SFTP_SERVICE_H
 
+#include "mod_sftp.h"
 #include "packet.h"
 
 int sftp_service_handle(struct ssh2_packet *);
 int sftp_service_init(void);
 
-#endif
+#endif /* MOD_SFTP_SERVICE_H */
diff --git a/contrib/mod_sftp/session.c b/contrib/mod_sftp/session.c
index c345a67..0dc51d4 100644
--- a/contrib/mod_sftp/session.c
+++ b/contrib/mod_sftp/session.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp session
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: session.c,v 1.3 2011-05-23 21:03:12 castaglia Exp $
  */
 
 #include "mod_sftp.h"
diff --git a/contrib/mod_sftp/session.h b/contrib/mod_sftp/session.h
index 3bdb1f1..41f4473 100644
--- a/contrib/mod_sftp/session.h
+++ b/contrib/mod_sftp/session.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp session
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,16 +20,14 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: session.h,v 1.3 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_SESSION_H
 #define MOD_SFTP_SESSION_H
 
+#include "mod_sftp.h"
+
 uint32_t sftp_session_get_id(const unsigned char **);
 int sftp_session_set_id(const unsigned char *, uint32_t);
 
-#endif
+#endif /* MOD_SFTP_SESSION_H */
diff --git a/contrib/mod_sftp/ssh2.h b/contrib/mod_sftp/ssh2.h
index 0ff61a5..cb074d3 100644
--- a/contrib/mod_sftp/ssh2.h
+++ b/contrib/mod_sftp/ssh2.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp SSH2 constants
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: ssh2.h,v 1.7 2013-01-29 07:08:05 castaglia Exp $
  */
 
 #ifndef MOD_SFTP_SSH2_H
@@ -108,4 +106,4 @@
 #define SFTP_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE	14
 #define SFTP_SSH2_DISCONNECT_ILLEGAL_USER_NAME			15
 
-#endif
+#endif /* MOD_SFTP_SSH2_H */
diff --git a/contrib/mod_sftp/tap.c b/contrib/mod_sftp/tap.c
index bcb64b5..95f388e 100644
--- a/contrib/mod_sftp/tap.c
+++ b/contrib/mod_sftp/tap.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp traffic analysis protection
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: tap.c,v 1.12 2012-02-15 23:50:51 castaglia Exp $
  */
 
 #include "mod_sftp.h"
@@ -71,7 +69,8 @@ static struct sftp_tap_policy curr_policy = { NULL, 0, 0, 0, 0, 0, 0, 0 };
 static int check_packet_times_cb(CALLBACK_FRAME) {
   time_t last_recvd, last_sent, now;
   unsigned long since_recvd, since_sent;
-  int chance;
+  unsigned int chance;
+  int rnd;
 
   /* Always return 1 so that this timer is rescheduled. */
 
@@ -104,12 +103,13 @@ static int check_packet_times_cb(CALLBACK_FRAME) {
 
   /* Otherwise, pick a random number, see if it's time to send a packet. */
   if (curr_policy.chance_max != 1) {
-    chance = (int) (rand() / (RAND_MAX / curr_policy.chance_max + 1));
+    rnd = (int) (rand() / (RAND_MAX / curr_policy.chance_max + 1));
 
   } else {
-    chance = 1;
+    rnd = 1;
   }
 
+  chance = rnd;
   if (chance == curr_policy.chance) {
     pr_trace_msg(trace_channel, 15, "perhaps too inactive, attempting to send "
       "a TAP packet");
@@ -174,7 +174,8 @@ int sftp_tap_have_policy(const char *policy) {
 }
 
 int sftp_tap_send_packet(void) {
-  int chance;
+  int rnd;
+  unsigned int chance;
 
   if (!sftp_interop_supports_feature(SFTP_SSH2_FEAT_IGNORE_MSG)) {
     pr_trace_msg(trace_channel, 3,
@@ -191,12 +192,13 @@ int sftp_tap_send_packet(void) {
    * policy.
    */
   if (curr_policy.chance_max != 1) {
-    chance = (int) (rand() / (RAND_MAX / curr_policy.chance_max + 1));
+    rnd = (int) (rand() / (RAND_MAX / curr_policy.chance_max + 1));
 
   } else {
-    chance = 1;
+    rnd = 1;
   }
 
+  chance = rnd;
   if (chance == curr_policy.chance) {
     unsigned char *buf, *ptr, *rand_data;
     uint32_t bufsz, buflen, rand_datalen;
@@ -220,10 +222,7 @@ int sftp_tap_send_packet(void) {
 
     rand_data = palloc(pkt->pool, rand_datalen);
 
-    /* We don't need cryptographically secure random bytes here, just
-     * pseudo-random data.
-     */
-    RAND_pseudo_bytes(rand_data, rand_datalen);
+    RAND_bytes(rand_data, rand_datalen);
 
     sftp_msg_write_byte(&buf, &buflen, SFTP_SSH2_MSG_IGNORE);
     sftp_msg_write_data(&buf, &buflen, rand_data, rand_datalen, TRUE);
diff --git a/contrib/mod_sftp/tap.h b/contrib/mod_sftp/tap.h
index 9ea3ae8..4a4c065 100644
--- a/contrib/mod_sftp/tap.h
+++ b/contrib/mod_sftp/tap.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp traffic analysis protection
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: tap.h,v 1.6 2011-05-23 20:40:13 castaglia Exp $
  */
 
-#include "mod_sftp.h"
-
 #ifndef MOD_SFTP_TAP_H
 #define MOD_SFTP_TAP_H
 
+#include "mod_sftp.h"
+
 int sftp_tap_have_policy(const char *);
 
 /* May send an SSH2_MSG_IGNORE packet of random length, filled with random
@@ -65,4 +63,4 @@ int sftp_tap_send_packet(void);
  */
 int sftp_tap_set_policy(const char *);
 
-#endif
+#endif /* MOD_SFTP_TAP_H */
diff --git a/contrib/mod_sftp/umac.c b/contrib/mod_sftp/umac.c
index 8036a25..1c4df84 100644
--- a/contrib/mod_sftp/umac.c
+++ b/contrib/mod_sftp/umac.c
@@ -51,7 +51,15 @@
 /* --- User Switches ---------------------------------------------------- */
 /* ---------------------------------------------------------------------- */
 
-#define UMAC_OUTPUT_LEN     8  /* Alowable: 4, 8, 12, 16                  */
+#ifndef UMAC_OUTPUT_LEN
+# define UMAC_OUTPUT_LEN       8  /* Alowable: 4, 8, 12, 16               */
+#endif
+
+#if UMAC_OUTPUT_LEN != 4 && UMAC_OUTPUT_LEN != 8 && \
+    UMAC_OUTPUT_LEN != 12 && UMAC_OUTPUT_LEN != 16
+# error UMAC_OUTPUT_LEN must be defined to 4, 8, 12 or 16
+#endif
+
 /* #define FORCE_C_ONLY        1  ANSI C and 64-bit integers req'd        */
 /* #define AES_IMPLEMENTAION   1  1 = OpenSSL, 2 = Barreto, 3 = Gladman   */
 /* #define SSE2                0  Is SSE2 is available?                   */
@@ -995,49 +1003,6 @@ static void uhash_init(uhash_ctx_t ahc, aes_int_key prf_key)
 
 /* ---------------------------------------------------------------------- */
 
-#if 0
-static uhash_ctx_t uhash_alloc(unsigned char key[])
-{
-/* Allocate memory and force to a 16-byte boundary. */
-    uhash_ctx_t ctx;
-    unsigned char bytes_to_add;
-    aes_int_key prf_key;
-    
-    ctx = (uhash_ctx_t)malloc(sizeof(uhash_ctx)+ALLOC_BOUNDARY);
-    if (ctx) {
-        if (ALLOC_BOUNDARY) {
-            bytes_to_add = ALLOC_BOUNDARY -
-                              ((ptrdiff_t)ctx & (ALLOC_BOUNDARY -1));
-            ctx = (uhash_ctx_t)((unsigned char *)ctx + bytes_to_add);
-            *((unsigned char *)ctx - 1) = bytes_to_add;
-        }
-        aes_key_setup(key,prf_key);
-        uhash_init(ctx, prf_key);
-    }
-    return (ctx);
-}
-#endif
-
-/* ---------------------------------------------------------------------- */
-
-#if 0
-static int uhash_free(uhash_ctx_t ctx)
-{
-/* Free memory allocated by uhash_alloc */
-    unsigned char bytes_to_sub;
-    
-    if (ctx) {
-        if (ALLOC_BOUNDARY) {
-            bytes_to_sub = *((unsigned char *)ctx - 1);
-            ctx = (uhash_ctx_t)((unsigned char *)ctx - bytes_to_sub);
-        }
-        free(ctx);
-    }
-    return (1);
-}
-#endif
-/* ---------------------------------------------------------------------- */
-
 static int uhash_update(uhash_ctx_t ctx, unsigned char *input, long len)
 /* Given len bytes of data, we parse it into L1_KEY_LEN chunks and
  * hash each one with NH, calling the polyhash on each NH output.
@@ -1115,55 +1080,6 @@ static int uhash_final(uhash_ctx_t ctx, unsigned char *res)
 }
 
 /* ---------------------------------------------------------------------- */
-
-#if 0
-static int uhash(uhash_ctx_t ahc, unsigned char *msg, long len, unsigned char *res)
-/* assumes that msg is in a writable buffer of length divisible by */
-/* L1_PAD_BOUNDARY. Bytes beyond msg[len] may be zeroed.           */
-{
-    UINT8 nh_result[STREAMS*sizeof(UINT64)];
-    UINT32 nh_len;
-    int extra_zeroes_needed;
-        
-    /* If the message to be hashed is no longer than L1_HASH_LEN, we skip
-     * the polyhash.
-     */
-    if (len <= L1_KEY_LEN) {
-    	if (len == 0)                  /* If zero length messages will not */
-    		nh_len = L1_PAD_BOUNDARY;  /* be seen, comment out this case   */ 
-    	else
-        	nh_len = ((len + (L1_PAD_BOUNDARY - 1)) & ~(L1_PAD_BOUNDARY - 1));
-        extra_zeroes_needed = nh_len - len;
-        zero_pad((UINT8 *)msg + len, extra_zeroes_needed);
-        nh(&ahc->hash, (UINT8 *)msg, nh_len, len, nh_result);
-        ip_short(ahc,nh_result, res);
-    } else {
-        /* Otherwise, we hash each L1_KEY_LEN chunk with NH, passing the NH
-         * output to poly_hash().
-         */
-        do {
-            nh(&ahc->hash, (UINT8 *)msg, L1_KEY_LEN, L1_KEY_LEN, nh_result);
-            poly_hash(ahc,(UINT32 *)nh_result);
-            len -= L1_KEY_LEN;
-            msg += L1_KEY_LEN;
-        } while (len >= L1_KEY_LEN);
-        if (len) {
-            nh_len = ((len + (L1_PAD_BOUNDARY - 1)) & ~(L1_PAD_BOUNDARY - 1));
-            extra_zeroes_needed = nh_len - len;
-            zero_pad((UINT8 *)msg + len, extra_zeroes_needed);
-            nh(&ahc->hash, (UINT8 *)msg, nh_len, len, nh_result);
-            poly_hash(ahc,(UINT32 *)nh_result);
-        }
-
-        ip_long(ahc, res);
-    }
-    
-    uhash_reset(ahc);
-    return 1;
-}
-#endif
-
-/* ---------------------------------------------------------------------- */
 /* ---------------------------------------------------------------------- */
 /* ----- Begin UMAC Section --------------------------------------------- */
 /* ---------------------------------------------------------------------- */
@@ -1179,7 +1095,7 @@ struct umac_ctx {
     uhash_ctx hash;          /* Hash function for message compression    */
     pdf_ctx pdf;             /* PDF for hashed output                    */
     void *free_ptr;          /* Address to free this struct via          */
-} umac_ctx;
+};
 
 /* ---------------------------------------------------------------------- */
 
@@ -1205,10 +1121,6 @@ int umac_delete(struct umac_ctx *ctx)
 
 /* ---------------------------------------------------------------------- */
 
-size_t umac_ctx_size(void) {
-  return sizeof(struct umac_ctx);
-}
-
 struct umac_ctx *umac_alloc(void) {
     struct umac_ctx *ctx, *octx;
     size_t bytes_to_add;
@@ -1275,7 +1187,7 @@ int umac_update(struct umac_ctx *ctx, unsigned char *input, long len)
 
 struct umac_ctx {
     void *free_ptr;          /* Address to free this struct via          */
-} umac_ctx;
+};
 
 int umac_reset(struct umac_ctx *ctx)
 /* Reset the hash function to begin a new authentication.        */
@@ -1293,10 +1205,6 @@ int umac_delete(struct umac_ctx *ctx)
 
 /* ---------------------------------------------------------------------- */
 
-size_t umac_ctx_size(void) {
-  return sizeof(struct umac_ctx);
-}
-
 struct umac_ctx *umac_alloc(void) {
     return (NULL);
 }
@@ -1332,21 +1240,6 @@ int umac_update(struct umac_ctx *ctx, unsigned char *input, long len)
 #endif /* OpenSSL-0.9.7 or later */
 
 /* ---------------------------------------------------------------------- */
-
-#if 0
-int umac(struct umac_ctx *ctx, unsigned char *input, 
-         long len, unsigned char tag[],
-         unsigned char nonce[8])
-/* All-in-one version simply calls umac_update() and umac_final().        */
-{
-    uhash(&ctx->hash, input, len, (unsigned char *)tag);
-    pdf_gen_xor(&ctx->pdf, (UINT8 *)nonce, (UINT8 *)tag);
-    
-    return (1);
-}
-#endif
-
-/* ---------------------------------------------------------------------- */
 /* ---------------------------------------------------------------------- */
 /* ----- End UMAC Section ----------------------------------------------- */
 /* ---------------------------------------------------------------------- */
diff --git a/contrib/mod_sftp/umac.h b/contrib/mod_sftp/umac.h
index 9694ab4..aa0a117 100644
--- a/contrib/mod_sftp/umac.h
+++ b/contrib/mod_sftp/umac.h
@@ -50,9 +50,6 @@
     extern "C" {
 #endif
 
-size_t umac_ctx_size(void);
-/* Returns size of umac_ctx struct. */
-
 struct umac_ctx *umac_alloc(void);
 /* Dynamically allocate a umac_ctx struct. */
 
@@ -78,47 +75,17 @@ int umac_final(struct umac_ctx *ctx, unsigned char tag[], unsigned char nonce[8]
 int umac_delete(struct umac_ctx *ctx);
 /* Deallocate the context structure */
 
-#if 0
-int umac(struct umac_ctx *ctx, unsigned char *input, 
-         long len, unsigned char tag[],
-         unsigned char nonce[8]);
-/* All-in-one implementation of the functions Reset, Update and Final */
-#endif
-
-/* uhash.h */
-
-
-#if 0
-typedef struct uhash_ctx *uhash_ctx_t;
-  /* The uhash_ctx structure is defined by the implementation of the    */
-  /* UHASH functions.                                                   */
- 
-uhash_ctx_t uhash_alloc(unsigned char key[16]);
-  /* Dynamically allocate a uhash_ctx struct and generate subkeys using */
-  /* the kdf and kdf_key passed in. If kdf_key_len is 0 then RC6 is     */
-  /* used to generate key with a fixed key. If kdf_key_len > 0 but kdf  */
-  /* is NULL then the first 16 bytes pointed at by kdf_key is used as a */
-  /* key for an RC6 based KDF.                                          */
-  
-int uhash_free(uhash_ctx_t ctx);
-
-int uhash_set_params(uhash_ctx_t ctx,
-                   void       *params);
-
-int uhash_reset(uhash_ctx_t ctx);
-
-int uhash_update(uhash_ctx_t ctx,
-               unsigned char       *input,
-               long        len);
-
-int uhash_final(uhash_ctx_t ctx,
-              unsigned char        ouput[]);
-
-int uhash(uhash_ctx_t ctx,
-        unsigned char       *input,
-        long        len,
-        unsigned char        output[]);
-#endif
+/* ProFTPD Note: We reuse umac_ctx for the umac-128 implementation, as the
+ * structure is opaque.  We simply recompile the umac.c file with different
+ * preprocessor macros to get the umac-128 implementation.
+ */
+struct umac_ctx *umac128_alloc(void);
+struct umac_ctx *umac128_new(unsigned char key[]);
+void umac128_init(struct umac_ctx *ctx, unsigned char key[]);
+int umac128_reset(struct umac_ctx *ctx);
+int umac128_update(struct umac_ctx *ctx, unsigned char *input, long len);
+int umac128_final(struct umac_ctx *ctx, unsigned char tag[], unsigned char nonce[8]);
+int umac128_delete(struct umac_ctx *ctx);
 
 #ifdef __cplusplus
     }
diff --git a/contrib/mod_sftp/utf8.h b/contrib/mod_sftp/utf8.h
index 6c6e9e5..c2c623e 100644
--- a/contrib/mod_sftp/utf8.h
+++ b/contrib/mod_sftp/utf8.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp UTF8 encoding
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: utf8.h,v 1.4 2011-05-23 20:40:13 castaglia Exp $
  */
 
 #ifndef MOD_SFTP_UTF8_H
diff --git a/contrib/mod_sftp_pam.c b/contrib/mod_sftp_pam.c
index 44c1022..7d88e7e 100644
--- a/contrib/mod_sftp_pam.c
+++ b/contrib/mod_sftp_pam.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_sftp_pam -- a module which provides an SSH2
  *                          "keyboard-interactive" driver using PAM
  *
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -26,8 +26,8 @@
  * This is mod_sftp_pam, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
  *
- * $Id: mod_sftp_pam.c,v 1.20 2013-10-07 01:29:04 castaglia Exp $
- * $Libraries: -lpam $
+ * -----DO NOT EDIT BELOW THIS LINE-----
+ * $Libraries: -lpam$
  */
 
 #include "conf.h"
@@ -117,7 +117,7 @@ static const char *trace_channel = "ssh2";
 
 static int sftppam_converse(int nmsgs, PR_PAM_CONST struct pam_message **msgs,
     struct pam_response **resps, void *app_data) {
-  register unsigned int i = 0, j = 0;
+  register int i = 0, j = 0;
   array_header *list;
   uint32_t recvd_count = 0;
   const char **recvd_responses = NULL;
@@ -352,13 +352,13 @@ static int sftppam_driver_open(sftp_kbdint_driver_t *driver, const char *user) {
 
   res = pam_start(sftppam_service, sftppam_user, &sftppam_conv, &sftppam_pamh);
   if (res != PAM_SUCCESS) {
+    PRIVS_RELINQUISH
+    pr_signals_unblock();
+
     free(sftppam_user);
     sftppam_user = NULL;
     sftppam_userlen = 0;
 
-    PRIVS_RELINQUISH
-    pr_signals_unblock();
-
     switch (res) {
       case PAM_SYSTEM_ERR:
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_PAM_VERSION,
@@ -441,6 +441,9 @@ static int sftppam_driver_authenticate(sftp_kbdint_driver_t *driver,
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_PAM_VERSION,
       "PAM authentication error (%d) for user '%s': %s", res, user,
       pam_strerror(sftppam_pamh, res));
+    (void) pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_PAM_VERSION
+      ": PAM authentication error (%d) for user '%s': %s", res, user,
+      pam_strerror(sftppam_pamh, res));
 
     PRIVS_RELINQUISH
     pr_signals_unblock();
@@ -584,8 +587,9 @@ MODRET sftppam_auth(cmd_rec *cmd) {
   }
 
   if (sftppam_auth_code != PR_AUTH_OK) {
-    if (sftppam_authoritative)
+    if (sftppam_authoritative) {
       return PR_ERROR_INT(cmd, sftppam_auth_code);
+    }
 
     return PR_DECLINED(cmd);
   }
@@ -600,20 +604,20 @@ MODRET sftppam_auth(cmd_rec *cmd) {
 
 /* usage: SFTPPAMEngine on|off */
 MODRET set_sftppamengine(cmd_rec *cmd) {
-  int bool = -1;
+  int engine = -1;
   config_rec *c;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  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(int));
-  *((int *) c->argv[0]) = bool;
+  *((int *) c->argv[0]) = engine;
 
   return PR_HANDLED(cmd);
 }
@@ -666,26 +670,26 @@ MODRET set_sftppamservicename(cmd_rec *cmd) {
  */
 
 static void sftppam_exit_ev(const void *event_data, void *user_data) {
-  int res;
 
   /* Close the PAM session */
 
-  if (sftppam_pamh == NULL)
-    return;
+  if (sftppam_pamh != NULL) {
+    int res;
 
 #ifdef PAM_CRED_DELETE
-  res = pam_setcred(sftppam_pamh, PAM_CRED_DELETE);
+    res = pam_setcred(sftppam_pamh, PAM_CRED_DELETE);
 #else
-  res = pam_setcred(sftppam_pamh, PAM_DELETE_CRED);
+    res = pam_setcred(sftppam_pamh, PAM_DELETE_CRED);
 #endif
-  if (res != PAM_SUCCESS) {
-    pr_trace_msg(trace_channel, 9, "PAM error setting PAM_DELETE_CRED: %s",
-      pam_strerror(sftppam_pamh, res));
-  }
+    if (res != PAM_SUCCESS) {
+      pr_trace_msg(trace_channel, 9, "PAM error setting PAM_DELETE_CRED: %s",
+        pam_strerror(sftppam_pamh, res));
+    }
 
-  res = pam_close_session(sftppam_pamh, PAM_SILENT);
-  pam_end(sftppam_pamh, res);
-  sftppam_pamh = NULL;
+    res = pam_close_session(sftppam_pamh, PAM_SILENT);
+    pam_end(sftppam_pamh, res);
+    sftppam_pamh = NULL;
+  }
 
   if (sftppam_user != NULL) {
     free(sftppam_user);
@@ -726,9 +730,13 @@ static int sftppam_init(void) {
 
   /* Register ourselves with mod_sftp. */
   if (sftp_kbdint_register_driver("pam", &sftppam_driver) < 0) {
+    int xerrno = errno;
+
     pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_PAM_VERSION
       ": notice: error registering 'keyboard-interactive' driver: %s",
-      strerror(errno));
+      strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
@@ -739,11 +747,11 @@ static int sftppam_sess_init(void) {
   config_rec *c;
 
   c = find_config(main_server->conf, CONF_PARAM, "SFTPPAMEngine", FALSE);
-  if (c) {
+  if (c != NULL) {
     int engine;
 
     engine = *((int *) c->argv[0]);
-    if (!engine) {
+    if (engine == FALSE) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_PAM_VERSION,
         "disabled by SFTPPAMEngine setting, unregistered 'pam' driver");
       sftp_kbdint_unregister_driver("pam");
@@ -751,8 +759,24 @@ static int sftppam_sess_init(void) {
     }
   }
 
+  /* To preserve the principle of least surprise, also check for the AuthPAM
+   * directive.
+   */
+  c = find_config(main_server->conf, CONF_PARAM, "AuthPAM", FALSE);
+  if (c != NULL) {
+    unsigned char auth_pam;
+
+    auth_pam = *((unsigned char *) c->argv[0]);
+    if (auth_pam == FALSE) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_PAM_VERSION,
+        "disabled by AuthPAM setting, unregistered 'pam' driver");
+      sftp_kbdint_unregister_driver("pam");
+      return 0;
+    }
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "SFTPPAMServiceName", FALSE);
-  if (c) {
+  if (c != NULL) {
     sftppam_service = c->argv[0];
   }
 
diff --git a/contrib/mod_sftp_sql.c b/contrib/mod_sftp_sql.c
index b39e5c8..1fde013 100644
--- a/contrib/mod_sftp_sql.c
+++ b/contrib/mod_sftp_sql.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_sftp_sql -- SQL backend module for retrieving authorized keys
- *
- * Copyright (c) 2008-2015 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -35,7 +34,7 @@
 
 module sftp_sql_module;
 
-#define SFTP_SQL_BUFSZ		1024
+#define SFTP_SQL_BUFSZ			1024
 
 struct sqlstore_key {
   const char *subject;
@@ -51,10 +50,10 @@ struct sqlstore_data {
 
 static const char *trace_channel = "ssh2";
 
-static cmd_rec *sqlstore_cmd_create(pool *parent_pool, int argc, ...) {
+static cmd_rec *sqlstore_cmd_create(pool *parent_pool, unsigned int argc, ...) {
+  register unsigned int i = 0;
   pool *cmd_pool = NULL;
   cmd_rec *cmd = NULL;
-  register unsigned int i = 0;
   va_list argp;
 
   cmd_pool = make_sub_pool(parent_pool);
@@ -62,14 +61,15 @@ static cmd_rec *sqlstore_cmd_create(pool *parent_pool, int argc, ...) {
   cmd->pool = cmd_pool;
 
   cmd->argc = argc;
-  cmd->argv = (char **) pcalloc(cmd->pool, argc * sizeof(char *));
+  cmd->argv = pcalloc(cmd->pool, argc * sizeof(void *));
 
   /* Hmmm... */
   cmd->tmp_pool = cmd->pool;
 
   va_start(argp, argc);
-  for (i = 0; i < argc; i++)
+  for (i = 0; i < argc; i++) {
     cmd->argv[i] = va_arg(argp, char *);
+  }
   va_end(argp);
 
   return cmd;
@@ -87,8 +87,6 @@ static char *sqlstore_getline(pool *p, char **blob, size_t *bloblen) {
 
   if (data == NULL ||
       datalen == 0) {
-    pr_trace_msg(trace_channel, 10,
-      "reached end of data, no matching key found");
     errno = EOF;
     return NULL;
   }
@@ -155,7 +153,8 @@ static char *sqlstore_getline(pool *p, char **blob, size_t *bloblen) {
     datalen -= (linelen + delimlen);
 
     /* Check for continued lines. */
-    if (linebuf[linelen-2] == '\\') {
+    if (linelen >= 2 &&
+        linebuf[linelen-2] == '\\') {
       linebuf[linelen-2] = '\0';
       have_line_continuation = TRUE;
     }
@@ -417,7 +416,7 @@ static char *sqlstore_get_str(pool *p, char *str) {
     return str;
 
   /* Find the cmdtable for the sql_escapestr command. */
-  cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_escapestr", NULL, NULL);
+  cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_escapestr", NULL, NULL, NULL);
   if (cmdtab == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_SQL_VERSION,
       "unable to find SQL hook symbol 'sql_escapestr'");
@@ -523,7 +522,8 @@ static int sqlstore_verify_host_key(sftp_keystore_t *store, pool *p,
   store_data = store->keystore_data;
 
   /* Find the cmdtable for the sql_lookup command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_SQL_VERSION,
       "unable to find SQL hook symbol 'sql_lookup'");
@@ -615,7 +615,8 @@ static int sqlstore_verify_user_key(sftp_keystore_t *store, pool *p,
   store_data = store->keystore_data;
 
   /* Find the cmdtable for the sql_lookup command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_SQL_VERSION,
       "unable to find SQL hook symbol 'sql_lookup'");
diff --git a/contrib/mod_shaper.c b/contrib/mod_shaper.c
index 23d5c6d..bbc4e4e 100644
--- a/contrib/mod_shaper.c
+++ b/contrib/mod_shaper.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_shaper -- a module implementing daemon-wide rate throttling
  *                        via IPC
- *
- * Copyright (c) 2004-2014 TJ Saunders
+ * Copyright (c) 2004-2016 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
@@ -25,8 +24,6 @@
  *
  * This is mod_shaper, contrib software for proftpd 1.2 and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_shaper.c,v 1.18 2013-10-13 22:51:36 castaglia Exp $
  */
 
 #include "conf.h"
@@ -213,6 +210,8 @@ static int shaper_remove_queue(void) {
   struct msqid_ds ds;
   int res;
 
+  memset(&ds, 0, sizeof(ds));
+
   res = msgctl(shaper_qid, IPC_RMID, &ds);
   if (res < 0) {
     (void) pr_log_writefile(shaper_logfd, MOD_SHAPER_VERSION,
@@ -968,6 +967,14 @@ static int shaper_table_send(void) {
       sess_list[i].sess_upincr);
   }
 
+  if (total_downshares == 0) {
+    total_downshares = 1;
+  }
+
+  if (total_upshares == 0) {
+    total_upshares = 1;
+  }
+
   (void) pr_log_writefile(shaper_logfd, MOD_SHAPER_VERSION,
     "total session shares: %u down, %u up", total_downshares, total_upshares);
 
@@ -1005,8 +1012,9 @@ static int shaper_table_sess_add(pid_t sess_pid, unsigned int prio,
     int downincr, int upincr) {
   struct shaper_sess *sess;
 
-  if (shaper_table_lock(LOCK_EX) < 0)
+  if (shaper_table_lock(LOCK_EX) < 0) {
     return -1;
+  }
 
   if (shaper_table_refresh() < 0) {
     int xerrno = errno;
@@ -1019,7 +1027,14 @@ static int shaper_table_sess_add(pid_t sess_pid, unsigned int prio,
   shaper_tab.nsessions++;
   sess = push_array(shaper_tab.sess_list);
   sess->sess_pid = sess_pid;
-  sess->sess_prio = (prio != (unsigned int) -1) ? prio : shaper_tab.def_prio;
+
+  if (prio != (unsigned int) -1) {
+    sess->sess_prio = prio;
+
+  } else {
+    sess->sess_prio = shaper_tab.def_prio;
+  }
+
   sess->sess_downincr = downincr;
   sess->sess_downrate = 0.0;
   sess->sess_upincr = upincr;
@@ -1216,10 +1231,12 @@ static int shaper_table_sess_remove(pid_t sess_pid) {
  */
 static int shaper_handle_all(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i;
+  register int i;
   int send_tab = TRUE;
 
-  if (reqargc < 2 || reqargc > 14 || reqargc % 2 != 0) {
+  if (reqargc < 2 ||
+      reqargc > 14 ||
+      reqargc % 2 != 0) {
     pr_ctrls_add_response(ctrl, "wrong number of parameters");
     return -1;
   }
@@ -1499,11 +1516,13 @@ static int shaper_handle_info(pr_ctrls_t *ctrl, int reqargc,
  */
 static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i;
+  register int i;
   int adjusted = FALSE, send_tab = TRUE;
   int prio = -1, downincr = 0, upincr = 0;
 
-  if (reqargc < 4 || reqargc > 6 || reqargc % 2 != 0) {
+  if (reqargc < 4 ||
+      reqargc > 6 ||
+      reqargc % 2 != 0) {
     pr_ctrls_add_response(ctrl, "wrong number of parameters");
     return -1;
   }
@@ -1620,7 +1639,7 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
       (void) pr_log_writefile(shaper_logfd, MOD_SHAPER_VERSION,
         "error rewinding scoreboard: %s", strerror(errno));
 
-    while ((score = pr_scoreboard_read_entry()) != NULL) {
+    while ((score = pr_scoreboard_entry_read()) != NULL) {
       pr_signals_handle();
 
       if (strcmp(score->sce_user, user) == 0) {
@@ -1642,10 +1661,10 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
   } else if (strcmp(reqargv[0], "host") == 0) {
     pr_scoreboard_entry_t *score;
     const char *addr;
-    pr_netaddr_t *na;
+    const pr_netaddr_t *na;
 
     na = pr_netaddr_get_addr(ctrl->ctrls_tmp_pool, reqargv[1], NULL);
-    if (!na) {
+    if (na == NULL) {
       pr_ctrls_add_response(ctrl, "error resolving '%s': %s", reqargv[1],
         strerror(errno));
       return -1;
@@ -1657,7 +1676,7 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
       (void) pr_log_writefile(shaper_logfd, MOD_SHAPER_VERSION,
         "error rewinding scoreboard: %s", strerror(errno));
 
-    while ((score = pr_scoreboard_read_entry()) != NULL) {
+    while ((score = pr_scoreboard_entry_read()) != NULL) {
       pr_signals_handle();
 
       if (strcmp(score->sce_client_addr, addr) == 0) {
@@ -1669,8 +1688,9 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
           pr_ctrls_add_response(ctrl, "error adjusting pid %u: %s",
             (unsigned int) score->sce_pid, strerror(errno));
 
-        } else
+        } else {
           adjusted = TRUE;
+        }
       }
     }
 
@@ -1684,7 +1704,7 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
       (void) pr_log_writefile(shaper_logfd, MOD_SHAPER_VERSION,
         "error rewinding scoreboard: %s", strerror(errno));
 
-    while ((score = pr_scoreboard_read_entry()) != NULL) {
+    while ((score = pr_scoreboard_entry_read()) != NULL) {
       pr_signals_handle();
 
       if (strcmp(score->sce_class, class) == 0) {
@@ -1696,8 +1716,9 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
           pr_ctrls_add_response(ctrl, "error adjusting pid %u: %s",
             (unsigned int) score->sce_pid, strerror(errno));
 
-        } else
+        } else {
           adjusted = TRUE;
+        }
       }
     }
 
@@ -1709,8 +1730,9 @@ static int shaper_handle_sess(pr_ctrls_t *ctrl, int reqargc,
     return -1;
   }
 
-  if (adjusted)
+  if (adjusted) {
     pr_ctrls_add_response(ctrl, "sessions adjusted");
+  }
 
   return 0;
 }
@@ -1943,53 +1965,68 @@ MODRET set_shapersession(cmd_rec *cmd) {
 
   register unsigned int i;
 
-  if (cmd->argc-1 < 2 || cmd->argc-1 > 8 || (cmd->argc-1) % 2 != 0)
+  if (cmd->argc-1 < 2 ||
+      cmd->argc-1 > 8 ||
+      (cmd->argc-1) % 2 != 0) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
 
   for (i = 1; i < cmd->argc;) {
     if (strcmp(cmd->argv[i], "downshares") == 0) {
-      if (*cmd->argv[i+1] != '+' &&
-          *cmd->argv[i+1] != '-')
+      char *shareno;
+
+      shareno = cmd->argv[i+1];
+      if (*shareno != '+' &&
+          *shareno != '-') {
         CONF_ERROR(cmd, "downshares parameter must start with '+' or '-'");
+      }
 
-      downshares = atoi(cmd->argv[i+1]);
+      downshares = atoi(shareno);
       i += 2;
 
     } else if (strcmp(cmd->argv[i], "priority") == 0) {
       prio = atoi(cmd->argv[i+1]);
-
-      if (prio < 0)
+      if (prio < 0) {
         CONF_ERROR(cmd, "priority must be greater than 0");
+      }
 
       i += 2;
 
     } else if (strcmp(cmd->argv[i], "shares") == 0) {
-      if (*cmd->argv[i+1] != '+' &&
-          *cmd->argv[i+1] != '-')
-        CONF_ERROR(cmd, "shares parameter must start with '+' or '-'");
+      char *shareno;
 
-      downshares = upshares = atoi(cmd->argv[i+1]);
+      shareno = cmd->argv[i+1];
+      if (*shareno != '+' &&
+          *shareno != '-') {
+        CONF_ERROR(cmd, "shares parameter must start with '+' or '-'");
+      }
 
+      downshares = upshares = atoi(shareno);
       i += 2;
 
     } else if (strcmp(cmd->argv[i], "upshares") == 0) {
-      if (*cmd->argv[i+1] != '+' &&
-          *cmd->argv[i+1] != '-')
+      char *shareno;
+
+      shareno = cmd->argv[i+1];
+      if (*shareno != '+' &&
+          *shareno != '-') {
         CONF_ERROR(cmd, "upshares parameter must start with '+' or '-'");
+      }
 
-      upshares = atoi(cmd->argv[i+1]);
+      upshares = atoi(shareno);
       i += 2;
 
-    } else
+    } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown option: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
+    }
   }
 
   c = add_config_param(cmd->argv[0], 3, NULL, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
-  *((unsigned int *) c->argv[0]) = prio;
+  *((unsigned int *) c->argv[0]) = (unsigned int) prio;
   c->argv[1] = pcalloc(c->pool, sizeof(int));
   *((int *) c->argv[1]) = downshares;
   c->argv[2] = pcalloc(c->pool, sizeof(int));
@@ -2004,8 +2041,9 @@ MODRET set_shapertable(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  if (pr_fs_valid_path(cmd->argv[1]) < 0)
+  if (pr_fs_valid_path(cmd->argv[1]) < 0) {
     CONF_ERROR(cmd, "must be an absolute path");
+  }
 
   shaper_tab_path = pstrdup(shaper_pool, cmd->argv[1]);
   return PR_HANDLED(cmd);
@@ -2040,11 +2078,11 @@ MODRET shaper_post_pass(cmd_rec *cmd) {
   unsigned int prio = -1;
 
   c = find_config(TOPLEVEL_CONF, CONF_PARAM, "ShaperEngine", FALSE);
-  if (c && *((unsigned char *) c->argv[0]) == TRUE)
+  if (c != NULL &&
+      *((unsigned char *) c->argv[0]) == TRUE) {
     shaper_engine = TRUE;
 
-  else {
-
+  } else {
     /* Don't need the ShaperTable open anymore. */
     close(shaper_tabfd);
     shaper_tabfd = -1;
@@ -2085,9 +2123,10 @@ MODRET shaper_post_pass(cmd_rec *cmd) {
   }
 
   /* Update the ShaperTable, adding a new entry for the current session. */
-  if (shaper_table_sess_add(getpid(), prio, downincr, upincr) < 0)
+  if (shaper_table_sess_add(getpid(), prio, downincr, upincr) < 0) {
     (void) pr_log_writefile(shaper_logfd, MOD_SHAPER_VERSION,
       "error adding session to ShaperTable: %s", strerror(errno));
+  }
 
   return PR_DECLINED(cmd);
 }
@@ -2118,7 +2157,10 @@ static void shaper_shutdown_ev(const void *event_data, void *user_data) {
     }
 
     if (shaper_tab_path) {
-      pr_fsio_unlink(shaper_tab_path);
+      if (pr_fsio_unlink(shaper_tab_path) < 0) {
+        pr_log_debug(DEBUG9, MOD_SHAPER_VERSION
+          ": error unlinking '%s': %s", shaper_tab_path, strerror(errno));
+      }
     }
   }
 
diff --git a/contrib/mod_site_misc.c b/contrib/mod_site_misc.c
index 50b0628..96ef2da 100644
--- a/contrib/mod_site_misc.c
+++ b/contrib/mod_site_misc.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_site_misc -- a module implementing miscellaneous SITE commands
- *
- * Copyright (c) 2004-2011 The ProFTPD Project
+ * Copyright (c) 2004-2017 The ProFTPD Project
  *
  * 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
@@ -21,18 +20,21 @@
  * 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.
- *
- * $Id: mod_site_misc.c,v 1.21 2011-12-11 02:33:14 castaglia Exp $
  */
 
 #include "conf.h"
 
-#define MOD_SITE_MISC_VERSION		"mod_site_misc/1.5"
+#define MOD_SITE_MISC_VERSION		"mod_site_misc/1.6"
 
 extern pr_response_t *resp_list, *resp_err_list;
 
+module site_misc_module;
+
 static unsigned int site_misc_engine = TRUE;
 
+/* Necessary prototypes */
+static int site_misc_sess_init(void);
+
 static int site_misc_check_filters(cmd_rec *cmd, const char *path) {
 #ifdef PR_USE_REGEX
   pr_regex_t *pre = get_param_ptr(CURRENT_CONF, "PathAllowFilter", FALSE);
@@ -59,8 +61,7 @@ static int site_misc_create_dir(const char *dir) {
   struct stat st;
   int res;
 
-  pr_fs_clear_cache();
-
+  pr_fs_clear_cache2(dir);
   res = pr_fsio_stat(dir, &st);
   if (res < 0 &&
       errno != ENOENT) {
@@ -95,10 +96,10 @@ static int site_misc_create_path(pool *p, const char *path) {
   struct stat st;
   char *curr_path, *tmp_path;
 
-  pr_fs_clear_cache();
-
-  if (pr_fsio_stat(path, &st) == 0)
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) == 0) {
     return 0;
+  }
 
   /* The given path should already be canonicalized; we do not need to worry
    * if it is relative to the current working directory or not.
@@ -220,6 +221,7 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
       cmd->arg = pstrdup(cmd->pool, file);
       cmd->cmd_class = CL_WRITE;
 
+      pr_response_block(TRUE);
       res = pr_cmd_dispatch_phase(cmd, PRE_CMD, 0);
       if (res < 0) {
         int xerrno = errno;
@@ -231,6 +233,7 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
         pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
         pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
         pr_response_clear(&resp_err_list);
+        pr_response_block(FALSE);
 
         destroy_pool(sub_pool);
         pr_fsio_closedir(dirh);
@@ -248,6 +251,7 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
         pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
         pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
         pr_response_clear(&resp_err_list);
+        pr_response_block(FALSE);
 
         destroy_pool(sub_pool);
         pr_fsio_closedir(dirh);
@@ -256,10 +260,12 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
         return -1;
       }
 
+      pr_response_add(R_250, _("%s command successful"), (char *) cmd->argv[0]);
       pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
       pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
       pr_response_clear(&resp_list);
       destroy_pool(sub_pool);
+      pr_response_block(FALSE);
     }
   }
 
@@ -272,6 +278,7 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
   cmd->arg = pstrdup(cmd->pool, dir);
   cmd->cmd_class = CL_DIRS|CL_WRITE;
 
+  pr_response_block(TRUE);
   res = pr_cmd_dispatch_phase(cmd, PRE_CMD, 0);
   if (res < 0) {
     int xerrno = errno;
@@ -280,9 +287,11 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
       ": removing directory '%s' blocked by RMD handler: %s", dir,
       strerror(xerrno));
 
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
     pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
     pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
     pr_response_clear(&resp_err_list);
+    pr_response_block(FALSE);
 
     destroy_pool(sub_pool);
 
@@ -294,18 +303,23 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
   if (res < 0) {
     int xerrno = errno;
 
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
     pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
     pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
     pr_response_clear(&resp_err_list);
+    pr_response_block(FALSE);
 
     destroy_pool(sub_pool);
     errno = xerrno;
     return -1;
   }
 
+  pr_response_add(R_257, _("\"%s\" - Directory successfully created"),
+    quote_dir(cmd->tmp_pool, (char *) dir));
   pr_cmd_dispatch_phase(cmd, POST_CMD, 0);
   pr_cmd_dispatch_phase(cmd, LOG_CMD, 0);
   pr_response_clear(&resp_list);
+  pr_response_block(FALSE);
   destroy_pool(sub_pool);
 
   return 0;
@@ -314,10 +328,10 @@ static int site_misc_delete_dir(pool *p, const char *dir) {
 static int site_misc_delete_path(pool *p, const char *path) {
   struct stat st;
 
-  pr_fs_clear_cache();
-
-  if (pr_fsio_stat(path, &st) < 0)
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
     return -1;
+  }
 
   if (!S_ISDIR(st.st_mode)) {
     errno = EINVAL;
@@ -327,6 +341,119 @@ static int site_misc_delete_path(pool *p, const char *path) {
   return site_misc_delete_dir(p, path);
 }
 
+/* Parse a timestamp string of the form "YYYYMMDDhhmm[ss]" into its
+ * individual components.
+ *
+ * We assume that the caller has already ensured that the given timestamp
+ * string is long enough, i.e. 12 or 14 characters long.
+ */
+static int site_misc_parsetime(char *timestamp, size_t timestamp_len,
+    unsigned int *year, unsigned int *month, unsigned int *day,
+    unsigned int *hour, unsigned int *min, unsigned int *sec) {
+  register unsigned int i;
+  char c, *ptr;
+  int have_secs = FALSE, valid_timestamp = TRUE;
+
+  /* Make sure the timestamp is comprised of all digits. */
+  for (i = 0; i < timestamp_len; i++) {
+    if (PR_ISDIGIT((int) timestamp[i]) == 0) {
+      valid_timestamp = FALSE;
+      break;
+    }
+  }
+
+  if (!valid_timestamp) {
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": timestamp '%s' contains non-digits", timestamp);
+    errno = EINVAL;
+    return -1;
+  }
+ 
+  if (timestamp_len == 14) {
+    have_secs = TRUE;
+  }
+  
+  ptr = timestamp;
+  c = timestamp[4];
+  timestamp[4] = '\0';
+  *year = atoi(ptr);
+  timestamp[4] = c; 
+
+  ptr = &(timestamp[4]);
+  c = timestamp[6];
+  timestamp[6] = '\0';
+  *month = atoi(ptr);
+  timestamp[6] = c; 
+
+  if (*month > 12) {
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": bad number of months in '%s' (%u)", timestamp, *month);
+    errno = EINVAL;
+    return -1;
+  }
+
+  ptr = &(timestamp[6]);
+  c = timestamp[8];
+  timestamp[8] = '\0';
+  *day = atoi(ptr);
+  timestamp[8] = c;
+
+  if (*day > 31) {
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": bad number of days in '%s' (%u)", timestamp, *day);
+    errno = EINVAL;
+    return -1;
+  }
+
+  ptr = &(timestamp[8]);
+  c = timestamp[10];
+  timestamp[10] = '\0';
+  *hour = atoi(ptr);
+  timestamp[10] = c;
+
+  if (*hour > 24) {
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": bad number of hours in '%s' (%u)", timestamp, *hour);
+    errno = EINVAL;
+    return -1;
+  }
+
+  ptr = &(timestamp[10]);
+
+  /* Handle optional seconds. */
+  if (have_secs) {
+    c = timestamp[12];
+    timestamp[12] = '\0';
+  }
+
+  *min = atoi(ptr);
+
+  if (have_secs) {
+    timestamp[12] = c;
+  }
+
+  if (*min > 60) {
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": bad number of minutes in '%s' (%u)", timestamp, *min);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (have_secs) {
+    ptr = &(timestamp[12]);
+    *sec = atoi(ptr);
+
+    if (*sec > 60) {
+      pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+        ": bad number of seconds in '%s' (%u)", timestamp, *sec);
+      errno = EINVAL;
+      return -1;
+    }
+  }
+
+  return 0;
+}
+
 static time_t site_misc_mktime(unsigned int year, unsigned int month,
     unsigned int mday, unsigned int hour, unsigned int min, unsigned int sec) {
   struct tm tm;
@@ -430,37 +557,56 @@ MODRET site_misc_mkdir(cmd_rec *cmd) {
 
   if (cmd->argc < 2) {
     pr_log_debug(DEBUG5, MOD_SITE_MISC_VERSION
-      "%s : wrong number of arguments (%d)", cmd->argv[0], cmd->argc);
+      "%s : wrong number of parameters (%d)", (char *) cmd->argv[0], cmd->argc);
     return PR_DECLINED(cmd);
   }
 
   if (strncasecmp(cmd->argv[1], "MKDIR", 6) == 0) {
     register unsigned int i;
-    char *cmd_name, *path = "";
+    char *cmd_name, *decoded_path, *path = "";
     unsigned char *authenticated;
 
-    if (cmd->argc < 3)
+    if (cmd->argc < 3) {
       return PR_DECLINED(cmd);
+    }
 
     authenticated = get_param_ptr(cmd->server->conf, "authenticated", FALSE);
-
-    if (!authenticated ||
+    if (authenticated == NULL ||
         *authenticated == FALSE) {
       pr_response_add_err(R_530, _("Please login with USER and PASS"));
-      errno = EACCES;
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
-    for (i = 2; i < cmd->argc; i++)
+    for (i = 2; i < cmd->argc; i++) {
       path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", cmd->argv[i], NULL);
+    }
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+        strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), path);
 
-    path = pr_fs_decode_path(cmd->tmp_pool, path);
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    path = decoded_path;
 
     if (site_misc_check_filters(cmd, path) < 0) {
       int xerrno = EPERM;
 
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -471,6 +617,7 @@ MODRET site_misc_mkdir(cmd_rec *cmd) {
 
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -483,9 +630,10 @@ MODRET site_misc_mkdir(cmd_rec *cmd) {
       cmd->argv[0] = cmd_name;
 
       pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
-        ": %s command denied by <Limit>", cmd->argv[0]);
+        ": %s command denied by <Limit>", (char *) cmd->argv[0]);
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -496,11 +644,13 @@ MODRET site_misc_mkdir(cmd_rec *cmd) {
 
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
 
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[1]);
+    pr_response_add(R_200, _("SITE %s command successful"),
+      (char *) cmd->argv[1]);
     return PR_HANDLED(cmd);
   }
 
@@ -518,13 +668,13 @@ MODRET site_misc_rmdir(cmd_rec *cmd) {
 
   if (cmd->argc < 2) {
     pr_log_debug(DEBUG5, MOD_SITE_MISC_VERSION
-      "%s : wrong number of arguments (%d)", cmd->argv[0], cmd->argc);
+      "%s : wrong number of parameters (%d)", (char *) cmd->argv[0], cmd->argc);
     return PR_DECLINED(cmd);
   }
 
   if (strncasecmp(cmd->argv[1], "RMDIR", 6) == 0) {
     register unsigned int i;
-    char *cmd_name, *path = "";
+    char *cmd_name, *decoded_path, *path = "";
     unsigned char *authenticated;
 
     if (cmd->argc < 3)
@@ -535,21 +685,38 @@ MODRET site_misc_rmdir(cmd_rec *cmd) {
     if (!authenticated ||
         *authenticated == FALSE) {
       pr_response_add_err(R_530, _("Please login with USER and PASS"));
-      errno = EACCES;
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
-    for (i = 2; i < cmd->argc; i++)
+    for (i = 2; i < cmd->argc; i++) {
       path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", cmd->argv[i], NULL);
+    }
 
-    path = pr_fs_decode_path(cmd->tmp_pool, path);
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
 
-    path = dir_canonical_path(cmd->tmp_pool, path);
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+        strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), path);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    path = dir_canonical_path(cmd->tmp_pool, decoded_path);
     if (path == NULL) {
       int xerrno = EINVAL;
 
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -562,9 +729,10 @@ MODRET site_misc_rmdir(cmd_rec *cmd) {
       cmd->argv[0] = cmd_name;
 
       pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
-        ": %s command denied by <Limit>", cmd->argv[0]);
+        ": %s command denied by <Limit>", (char *) cmd->argv[0]);
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -575,11 +743,13 @@ MODRET site_misc_rmdir(cmd_rec *cmd) {
 
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
 
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[1]);
+    pr_response_add(R_200, _("SITE %s command successful"),
+      (char *) cmd->argv[1]);
     return PR_HANDLED(cmd);
   } 
 
@@ -597,14 +767,14 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
 
   if (cmd->argc < 2) {
     pr_log_debug(DEBUG5, MOD_SITE_MISC_VERSION
-      "%s : wrong number of arguments (%d)", cmd->argv[0], cmd->argc);
+      "%s : wrong number of parameters (%d)", (char *) cmd->argv[0], cmd->argc);
     return PR_DECLINED(cmd);
   }
 
   if (strncasecmp(cmd->argv[1], "SYMLINK", 8) == 0) {
     struct stat st;
     int res;
-    char *cmd_name, *src, *dst;
+    char *cmd_name, *decoded_path, *src, *dst;
     unsigned char *authenticated;
 
     if (cmd->argc < 4)
@@ -615,12 +785,28 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
     if (!authenticated ||
         *authenticated == FALSE) {
       pr_response_add_err(R_530, _("Please login with USER and PASS"));
-      errno = EACCES;
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
+      return PR_ERROR(cmd);
+    }
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[2],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[2], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), (char *) cmd->argv[2]);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
       return PR_ERROR(cmd);
     }
 
-    src = pr_fs_decode_path(cmd->tmp_pool, cmd->argv[2]);
-    src = dir_canonical_path(cmd->tmp_pool, src);
+    src = dir_canonical_path(cmd->tmp_pool, decoded_path);
     if (src == NULL) {
       int xerrno = EINVAL;
 
@@ -638,15 +824,30 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
       cmd->argv[0] = cmd_name;
 
       pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
-        ": %s command denied by <Limit>", cmd->argv[0]);
-      pr_response_add_err(R_550, "%s: %s", cmd->argv[2], strerror(xerrno));
+        ": %s command denied by <Limit>", (char *) cmd->argv[0]);
+      pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[2],
+        strerror(xerrno));
+
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[3],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[3], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), (char *) cmd->argv[3]);
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
 
-    dst = pr_fs_decode_path(cmd->tmp_pool, cmd->argv[3]);
-    dst = dir_canonical_path(cmd->tmp_pool, dst);
+    dst = dir_canonical_path(cmd->tmp_pool, decoded_path);
     if (dst == NULL) {
       int xerrno = EINVAL;
 
@@ -662,8 +863,9 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
       cmd->argv[0] = cmd_name;
 
       pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
-        ": %s command denied by <Limit>", cmd->argv[0]);
-      pr_response_add_err(R_550, "%s: %s", cmd->argv[3], strerror(xerrno));
+        ": %s command denied by <Limit>", (char *) cmd->argv[0]);
+      pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[3],
+        strerror(xerrno));
 
       errno = xerrno;
       return PR_ERROR(cmd);
@@ -685,7 +887,7 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
      * in the filesystem.
      */
        
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(src);
     res = pr_fsio_stat(src, &st);
     if (res < 0) {
       int xerrno = errno;
@@ -705,7 +907,8 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
       return PR_ERROR(cmd);
     }
 
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[1]);
+    pr_response_add(R_200, _("SITE %s command successful"),
+      (char *) cmd->argv[1]);
     return PR_HANDLED(cmd);
   } 
 
@@ -716,205 +919,355 @@ MODRET site_misc_symlink(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
-MODRET site_misc_utime(cmd_rec *cmd) {
-  if (!site_misc_engine) {
-    return PR_DECLINED(cmd);
+/* Handle: SITE UTIME mtime path-with-spaces */
+MODRET site_misc_utime_mtime(cmd_rec *cmd) {
+  register unsigned int i;
+  char *cmd_name, *decoded_path, *path = "";
+  size_t timestamp_len;
+  unsigned int year, month, day, hour, min, sec = 0;
+  struct timeval tvs[2];
+  struct stat st;
+
+  /* Accept both 'YYYYMMDDhhmm' and 'YYYYMMDDhhmmss' formats. */
+  timestamp_len = strlen(cmd->argv[2]);
+  if (timestamp_len != 12 &&
+      timestamp_len != 14) {
+    int xerrno = EINVAL;
+
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": wrong number of digits in timestamp argument '%s' (%lu)",
+      (char *) cmd->argv[2], (unsigned long) timestamp_len);
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+
+    errno = xerrno;
+    return PR_ERROR(cmd);
   }
 
-  if (cmd->argc < 2) {
-    pr_log_debug(DEBUG5, MOD_SITE_MISC_VERSION
-      "%s : wrong number of arguments (%d)", cmd->argv[0], cmd->argc);
-    return PR_DECLINED(cmd);
+  for (i = 3; i < cmd->argc; i++) {
+    path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", cmd->argv[i], NULL);
   }
 
-  if (strncasecmp(cmd->argv[1], "UTIME", 6) == 0) {
-    register unsigned int i;
-    char c, *cmd_name, *p, *path = "";
-    unsigned int year, month, day, hour, min, sec = 0;
-    struct timeval tvs[2];
-    unsigned char *authenticated;
-    int have_secs_value = FALSE;
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
 
-    if (cmd->argc < 4)
-      return PR_DECLINED(cmd);
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      path);
 
-    authenticated = get_param_ptr(cmd->server->conf, "authenticated", FALSE);
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    if (!authenticated ||
-        *authenticated == FALSE) {
-      pr_response_add_err(R_530, _("Please login with USER and PASS"));
-      errno = EACCES;
-      return PR_ERROR(cmd);
+  if (pr_fsio_lstat(decoded_path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(cmd->tmp_pool, decoded_path, link_path,
+        sizeof(link_path)-1, PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        decoded_path = pstrdup(cmd->tmp_pool, link_path);
+      }
     }
+  }
 
-    /* Accept both 'YYYYMMDDhhmm' and 'YYYYMMDDhhmmss' formats. */
-    if (strlen(cmd->argv[2]) != 12 &&
-        strlen(cmd->argv[2]) != 14) {
-      int xerrno = EINVAL;
+  path = dir_canonical_path(cmd->tmp_pool, decoded_path);
+  if (path == NULL) {
+    int xerrno = EINVAL;
 
-      pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
-        ": wrong number of digits in timestamp argument '%s' (%lu)",
-        cmd->argv[2], (unsigned long) strlen(cmd->argv[2]));
-      pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    if (strlen(cmd->argv[2]) == 14) {
-      have_secs_value = TRUE;
-    }
+  cmd_name = cmd->argv[0];
+  cmd->argv[0] = "SITE_UTIME";
+  if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
+    int xerrno = EPERM;
 
-    for (i = 3; i < cmd->argc; i++)
-      path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", cmd->argv[i], NULL);
+    cmd->argv[0] = cmd_name;
 
-    path = pr_fs_decode_path(cmd->tmp_pool, path);
+    pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
+      ": %s command denied by <Limit>", (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
-    path = dir_canonical_path(cmd->tmp_pool, path);
-    if (path == NULL) {
-      int xerrno = EINVAL;
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+  cmd->argv[0] = cmd_name;
 
-      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+  if (site_misc_check_filters(cmd, path) < 0) {
+    int xerrno = EPERM;
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
-    cmd_name = cmd->argv[0];
-    cmd->argv[0] = "SITE_UTIME";
-    if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
-      int xerrno = EPERM;
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-      cmd->argv[0] = cmd_name;
+  if (site_misc_parsetime(cmd->argv[2], timestamp_len, &year, &month, &day,
+      &hour, &min, &sec) < 0) {
+    int xerrno = errno;
 
-      pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
-        ": %s command denied by <Limit>", cmd->argv[0]);
-      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  tvs[0].tv_usec = tvs[1].tv_usec = 0;
+  tvs[0].tv_sec = tvs[1].tv_sec = site_misc_mktime(year, month, day, hour,
+    min, sec);
+
+  if (pr_fsio_utimes_with_root(path, tvs) < 0) {
+    int xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+ 
+  pr_response_add(R_200, _("SITE %s command successful"),
+    (char *) cmd->argv[1]);
+  return PR_HANDLED(cmd);
+}
+
+/* Handle: SITE UTIME path-with-spaces atime mtime ctime UTC */
+MODRET site_misc_utime_atime_mtime_ctime(cmd_rec *cmd) {
+  register unsigned int i;
+  char *cmd_name, *decoded_path, *path = "", *timestamp;
+  size_t timestamp_len;
+  unsigned int year, month, day, hour, min, sec = 0;
+  time_t parsed_atime, parsed_mtime, parsed_ctime;
+  struct timeval tvs[2];
+
+  for (i = 2; i < cmd->argc-4; i++) {
+    path = pstrcat(cmd->tmp_pool, path, *path ? " " : "", cmd->argv[i], NULL);
+  }
+
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      path);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  path = dir_canonical_path(cmd->tmp_pool, decoded_path);
+  if (path == NULL) {
+    int xerrno = EINVAL;
+
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  cmd_name = cmd->argv[0];
+  cmd->argv[0] = "SITE_UTIME";
+  if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
+    int xerrno = EPERM;
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
     cmd->argv[0] = cmd_name;
 
-    if (site_misc_check_filters(cmd, path) < 0) {
-      int xerrno = EPERM;
+    pr_log_debug(DEBUG4, MOD_SITE_MISC_VERSION
+      ": %s command denied by <Limit>", (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
-      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+  cmd->argv[0] = cmd_name;
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+  if (site_misc_check_filters(cmd, path) < 0) {
+    int xerrno = EPERM;
 
-    p = cmd->argv[2];
-    c = cmd->argv[2][4];
-    cmd->argv[2][4] = '\0';
-    year = atoi(p);
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
-    cmd->argv[2][4] = c;
-    p = &(cmd->argv[2][4]);
-    c = cmd->argv[2][6];
-    cmd->argv[2][6] = '\0';
-    month = atoi(p);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    if (month > 12) {
-      int xerrno = EINVAL;
+  /* Handle the atime. Accept both 'YYYYMMDDhhmm' and 'YYYYMMDDhhmmss'
+   * formats.
+   */
+  timestamp = cmd->argv[cmd->argc-4];
+  timestamp_len = strlen(timestamp);
+  if (timestamp_len != 12 &&
+      timestamp_len != 14) {
+    int xerrno = EINVAL;
 
-      pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
-        ": bad number of months in '%s' (%d)", cmd->argv[2], month);
-      pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": wrong number of digits in timestamp argument '%s' (%lu)",
+      timestamp, (unsigned long) timestamp_len);
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    cmd->argv[2][6] = c;
-    p = &(cmd->argv[2][6]);
-    c = cmd->argv[2][8];
-    cmd->argv[2][8] = '\0';
-    day = atoi(p);
+  if (site_misc_parsetime(timestamp, timestamp_len, &year, &month, &day,
+      &hour, &min, &sec) < 0) {
+    int xerrno = errno;
 
-    if (day > 31) {
-      int xerrno = EINVAL;
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
 
-      pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
-        ": bad number of days in '%s' (%d)", cmd->argv[2], day);
-      pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+  parsed_atime = site_misc_mktime(year, month, day, hour, min, sec);
 
-    cmd->argv[2][8] = c;
-    p = &(cmd->argv[2][8]);
-    c = cmd->argv[2][10];
-    cmd->argv[2][10] = '\0';
-    hour = atoi(p);
+  /* Handle the mtime. Accept both 'YYYYMMDDhhmm' and 'YYYYMMDDhhmmss'
+   * formats.
+   */
 
-    if (hour > 24) {
-      int xerrno = EINVAL;
+  sec = 0;
+  timestamp = cmd->argv[cmd->argc-3];
+  timestamp_len = strlen(timestamp);
+  if (timestamp_len != 12 &&
+      timestamp_len != 14) {
+    int xerrno = EINVAL;
 
-      pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
-        ": bad number of hours in '%s' (%d)", cmd->argv[2], hour);
-      pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": wrong number of digits in timestamp argument '%s' (%lu)",
+      timestamp, (unsigned long) timestamp_len);
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    cmd->argv[2][10] = c;
-    p = &(cmd->argv[2][10]);
+  if (site_misc_parsetime(timestamp, timestamp_len, &year, &month, &day,
+      &hour, &min, &sec) < 0) {
+    int xerrno = errno;
 
-    /* Handle a 'YYYYMMDDhhmmss' argument. */
-    if (have_secs_value) {
-      c = cmd->argv[2][12];
-      cmd->argv[2][12] = '\0';
-    }
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
 
-    min = atoi(p);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    if (min > 60) {
-      int xerrno = EINVAL;
+  parsed_mtime = site_misc_mktime(year, month, day, hour, min, sec);
 
-      pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
-        ": bad number of minutes in '%s' (%d)", cmd->argv[2], min);
-      pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+  /* Handle the ctime. Accept both 'YYYYMMDDhhmm' and 'YYYYMMDDhhmmss'
+   * formats.
+   */
 
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
+  sec = 0;
+  timestamp = cmd->argv[cmd->argc-2];
+  timestamp_len = strlen(timestamp);
+  if (timestamp_len != 12 &&
+      timestamp_len != 14) {
+    int xerrno = EINVAL;
 
-    if (have_secs_value) {
-      cmd->argv[2][12] = c;
-      p = &(cmd->argv[2][12]);
-      sec = atoi(p);
+    pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
+      ": wrong number of digits in timestamp argument '%s' (%lu)",
+      timestamp, (unsigned long) timestamp_len);
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
 
-      if (sec > 60) {
-        int xerrno = EINVAL;
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-        pr_log_debug(DEBUG7, MOD_SITE_MISC_VERSION
-          ": bad number of seconds in '%s' (%d)", cmd->argv[2], sec);
-        pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+  if (site_misc_parsetime(timestamp, timestamp_len, &year, &month, &day,
+      &hour, &min, &sec) < 0) {
+    int xerrno = errno;
 
-        errno = xerrno;
-        return PR_ERROR(cmd);
-      }
-    }
+    pr_response_add_err(R_500, "%s: %s", cmd->arg, strerror(xerrno));
+
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-    tvs[0].tv_usec = tvs[1].tv_usec = 0;
-    tvs[0].tv_sec = tvs[1].tv_sec = site_misc_mktime(year, month, day, hour,
-      min, sec);
+  /* Unix filesystems typically do not allow changing/setting the creation
+   * timestamp.  Thus we parse the timestamp provided by the client, but
+   * do nothing but log it.
+   */
+  parsed_ctime = site_misc_mktime(year, month, day, hour, min, sec);
+  pr_trace_msg("command", 9,
+    "SITE UTIME command sent ctime timestamp of %lu secs",
+    (unsigned long) parsed_ctime);
 
-    if (pr_fsio_utimes(path, tvs) < 0) {
-      int xerrno = errno;
+  tvs[0].tv_usec = tvs[1].tv_usec = 0;
+  tvs[0].tv_sec = parsed_atime;
+  tvs[1].tv_sec = parsed_mtime;
 
-      pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+  if (pr_fsio_utimes_with_root(path, tvs) < 0) {
+    int xerrno = errno;
 
-      errno = xerrno;
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+ 
+  pr_response_add(R_200, _("SITE %s command successful"),
+    (char *) cmd->argv[1]);
+  return PR_HANDLED(cmd);
+}
+
+MODRET site_misc_utime(cmd_rec *cmd) {
+  if (!site_misc_engine) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (cmd->argc < 2) {
+    pr_log_debug(DEBUG5, MOD_SITE_MISC_VERSION
+      "%s : wrong number of parameters (%d)", (char *) cmd->argv[0], cmd->argc);
+    return PR_DECLINED(cmd);
+  }
+
+  if (strncasecmp(cmd->argv[1], "UTIME", 6) == 0) {
+    unsigned char *authenticated;
+
+    authenticated = get_param_ptr(cmd->server->conf, "authenticated", FALSE);
+    if (authenticated == NULL ||
+        *authenticated == FALSE) {
+      pr_response_add_err(R_530, _("Please login with USER and PASS"));
+      errno = EACCES;
       return PR_ERROR(cmd);
     }
- 
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[1]);
-    return PR_HANDLED(cmd);
+
+    /* Now try to determine whether we are dealing with the mtime-only
+     * SITE UTIME variant (one timestamp only), or with the atime/mtime/ctime
+     * variant (three timestamps).  What makes this trickier is making sure
+     * to handle filenames that contain spaces.
+     */
+
+    if (cmd->argc < 4) {
+      /* Not enough arguments for any SITE UTIME variant. */
+      pr_log_debug(DEBUG9, MOD_SITE_MISC_VERSION
+        ": SITE UTIME command has wrong number of parameters (%d), ignoring",
+        cmd->argc);
+      return PR_DECLINED(cmd);
+    }
+
+    /* If we have at least 7 parameters, AND the last paramter is "UTC"
+     * (case-insensitive), then it's a candidate for the atime/mtime/ctime
+     * variant.
+     */
+    if (cmd->argc >= 7 &&
+        strncasecmp(cmd->argv[cmd->argc-1], "UTC", 4) == 0) {
+      return site_misc_utime_atime_mtime_ctime(cmd);
+    }
+
+    return site_misc_utime_mtime(cmd);
   }
 
   if (strncasecmp(cmd->argv[1], "HELP", 5) == 0) {
@@ -924,12 +1277,39 @@ MODRET site_misc_utime(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void site_misc_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&site_misc_module, "core.session-reinit",
+    site_misc_sess_reinit_ev);
+
+  site_misc_engine = TRUE;
+  pr_feat_remove("SITE MKDIR");
+  pr_feat_remove("SITE RMDIR");
+  pr_feat_remove("SITE SYMLINK");
+  pr_feat_remove("SITE UTIME");
+
+  res = site_misc_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&site_misc_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization functions
  */
 
 static int site_misc_sess_init(void) {
   config_rec *c;
 
+  pr_event_register(&site_misc_module, "core.session-reinit",
+    site_misc_sess_reinit_ev, NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "SiteMiscEngine", FALSE);
   if (c) {
     site_misc_engine = *((unsigned int *) c->argv[0]);
diff --git a/contrib/mod_snmp/Makefile.in b/contrib/mod_snmp/Makefile.in
index 1274e13..920101a 100644
--- a/contrib/mod_snmp/Makefile.in
+++ b/contrib/mod_snmp/Makefile.in
@@ -11,10 +11,10 @@ SHARED_LDFLAGS=-avoid-version -export-dynamic -module
 VPATH=@srcdir@
 
 MODULE_NAME=mod_snmp
-MODULE_OBJS=mod_snmp.o stacktrace.o asn1.o smi.o pdu.o msg.o db.o mib.o \
-  packet.o uptime.o notify.o
-SHARED_MODULE_OBJS=mod_snmp.lo stacktrace.lo asn1.lo smi.lo pdu.lo msg.lo \
-  db.lo mib.lo packet.lo uptime.lo notify.lo
+MODULE_OBJS=mod_snmp.o asn1.o smi.o pdu.o msg.o db.o mib.o packet.o \
+  uptime.o notify.o
+SHARED_MODULE_OBJS=mod_snmp.lo asn1.lo smi.lo pdu.lo msg.lo db.lo mib.lo \
+  packet.lo uptime.lo notify.lo
 
 # Necessary redefinitions
 INCLUDES=-I. -I../.. -I../../include @INCLUDES@
@@ -46,5 +46,5 @@ clean:
 	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).a $(MODULE_NAME).la *.o *.lo .libs/*.o
 
 dist: clean
-	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log
+	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log *.gcda *.gcno
 	-$(RM) -r .git/ CVS/ RCS/
diff --git a/contrib/mod_snmp/PROFTPD-MIB.txt b/contrib/mod_snmp/PROFTPD-MIB.txt
index 6565949..bc02ca9 100644
--- a/contrib/mod_snmp/PROFTPD-MIB.txt
+++ b/contrib/mod_snmp/PROFTPD-MIB.txt
@@ -2,8 +2,6 @@
 
 PROFTPD-MIB DEFINITIONS ::= BEGIN
 --
--- $Id: PROFTPD-MIB.txt,v 1.1 2013-05-15 15:20:25 castaglia Exp $
---
 
 IMPORTS
         enterprises, Integer32, Unsigned32, TimeTicks, Gauge32, Counter32,
diff --git a/contrib/mod_snmp/agentx.h b/contrib/mod_snmp/agentx.h
index 702579d..9ecf656 100644
--- a/contrib/mod_snmp/agentx.h
+++ b/contrib/mod_snmp/agentx.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp AgentX support
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: agentx.h,v 1.1 2013-05-15 15:20:25 castaglia Exp $
  */
 
-#include "mod_snmp.h"
-
 #ifndef MOD_SNMP_AGENTX_H
 #define MOD_SNMP_AGENTX_H
 
+#include "mod_snmp.h"
+
 /* See RFC2741 */
 
-#endif
+#endif /* MOD_SNMP_AGENTX_H */
diff --git a/contrib/mod_snmp/asn1.c b/contrib/mod_snmp/asn1.c
index da66841..ccf93d1 100644
--- a/contrib/mod_snmp/asn1.c
+++ b/contrib/mod_snmp/asn1.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp ASN.1 support
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,14 +20,11 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: asn1.c,v 1.1 2013-05-15 15:20:25 castaglia Exp $
  */
 
 #include "mod_snmp.h"
 #include "asn1.h"
 #include "mib.h"
-#include "stacktrace.h"
 
 static const char *trace_channel = "snmp.asn1";
 
@@ -148,7 +145,7 @@ static int asn1_read_byte(pool *p, unsigned char **buf, size_t *buflen,
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
       "ASN.1 format error: unable to read type (buflen = %lu)",
       (unsigned long) *buflen);
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -191,7 +188,7 @@ static int asn1_read_len(pool *p, unsigned char **buf, size_t *buflen,
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
       "ASN.1 format error: unable to read length (buflen = %lu)",
       (unsigned long) *buflen);
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -210,7 +207,7 @@ static int asn1_read_len(pool *p, unsigned char **buf, size_t *buflen,
     if (byte == 0) {
       (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
         "ASN.1 format error: invalid ASN1 length value %c", byte);
-      snmp_stacktrace_log();
+      pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
       errno = EINVAL;
       return -1;
     }
@@ -219,7 +216,7 @@ static int asn1_read_len(pool *p, unsigned char **buf, size_t *buflen,
       (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
         "ASN.1 format error: invalid ASN1 length value %c (> %lu)", byte,
         (unsigned long) sizeof(unsigned int));
-      snmp_stacktrace_log();
+      pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
       errno = EINVAL;
       return -1;
     }
@@ -252,7 +249,7 @@ int snmp_asn1_read_header(pool *p, unsigned char **buf, size_t *buflen,
     pr_trace_msg(trace_channel, 3,
       "failed reading object header: extension length bit set (%c)", (*buf)[0]);
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EPERM;
     return -1;
   }
@@ -274,7 +271,7 @@ int snmp_asn1_read_header(pool *p, unsigned char **buf, size_t *buflen,
     pr_trace_msg(trace_channel, 3,
       "failed reading object header: object length (%u bytes) is greater "
       "than max object length (%u bytes)", objlen, SNMP_ASN1_MAX_OBJECT_LEN);
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -284,7 +281,7 @@ int snmp_asn1_read_header(pool *p, unsigned char **buf, size_t *buflen,
       "failed reading object header: object length (%u bytes) is greater "
       "than remaining data (%lu bytes)", objlen, (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -327,7 +324,7 @@ int snmp_asn1_read_int(pool *p, unsigned char **buf, size_t *buflen,
       "failed reading object header: object length (%u bytes) is greater "
       "than remaining data (%lu bytes)", objlen, (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -418,7 +415,7 @@ int snmp_asn1_read_null(pool *p, unsigned char **buf, size_t *buflen,
       "failed reading NULL object: object length (%u bytes) is not zero, "
       "as expected", objlen);
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -469,7 +466,7 @@ int snmp_asn1_read_oid(pool *p, unsigned char **buf, size_t *buflen,
       "failed reading OID object: object length (%u bytes) is greater "
       "than remaining data (%lu bytes)", objlen, (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -510,7 +507,7 @@ int snmp_asn1_read_oid(pool *p, unsigned char **buf, size_t *buflen,
         "failed reading OID object: sub-identifer (%u is greater "
         "than maximum allowed OID value (%u)", sub_id, SNMP_ASN1_OID_MAX_ID);
 
-      snmp_stacktrace_log();
+      pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
       errno = EINVAL;
       return -1;
     }
@@ -576,7 +573,7 @@ int snmp_asn1_read_string(pool *p, unsigned char **buf, size_t *buflen,
       "failed reading OCTET_STRING object: object length (%u bytes) is greater "
       "than remaining data (%lu bytes)", objlen, (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -596,7 +593,7 @@ static int asn1_write_byte(unsigned char **buf, size_t *buflen,
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
       "ASN.1 format error: unable to write byte %c (buflen = %lu)", byte,
       (unsigned long) *buflen);
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -644,7 +641,7 @@ static int asn1_write_len(unsigned char **buf, size_t *buflen,
           "ASN.1 format error: unable to write length %u (buflen = %lu)",
           asn1_len, (unsigned long) *buflen);
 
-        snmp_stacktrace_log();
+        pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
         errno = EINVAL;
         return -1;
       }
@@ -663,7 +660,7 @@ static int asn1_write_len(unsigned char **buf, size_t *buflen,
           "ASN.1 format error: unable to write length %u (buflen = %lu)",
           asn1_len, (unsigned long) *buflen);
 
-        snmp_stacktrace_log();
+        pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
         errno = EINVAL;
         return -1;
       }
@@ -692,7 +689,7 @@ static int asn1_write_len(unsigned char **buf, size_t *buflen,
           "ASN.1 format error: unable to write length %u (buflen = %lu)",
           asn1_len, (unsigned long) *buflen);
 
-        snmp_stacktrace_log();
+        pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
         errno = EINVAL;
         return -1;
       }
@@ -726,7 +723,7 @@ static int asn1_write_len(unsigned char **buf, size_t *buflen,
         "ASN.1 format error: unable to write length %u (buflen = %lu)",
         asn1_len, (unsigned long) *buflen);
 
-      snmp_stacktrace_log();
+      pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
       errno = EINVAL;
       return -1;
     }
@@ -817,7 +814,7 @@ int snmp_asn1_write_int(pool *p, unsigned char **buf, size_t *buflen,
       "than remaining buffer (%lu bytes)", asn1_intsz,
       (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -894,7 +891,7 @@ int snmp_asn1_write_uint(pool *p, unsigned char **buf, size_t *buflen,
       "than remaining buffer (%lu bytes)", asn1_uintsz,
       (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -986,7 +983,7 @@ int snmp_asn1_write_oid(pool *p, unsigned char **buf, size_t *buflen,
      */
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
       "invalid first sub-identifier (%lu) in OID", (unsigned long) asn1_oid[0]);
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
 
@@ -995,7 +992,7 @@ int snmp_asn1_write_oid(pool *p, unsigned char **buf, size_t *buflen,
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
       "OID sub-identifier count (%u) exceeds max supported (%u)", asn1_oidlen,
       SNMP_MIB_MAX_OIDLEN);
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
 
@@ -1060,7 +1057,7 @@ int snmp_asn1_write_oid(pool *p, unsigned char **buf, size_t *buflen,
       "failed writing OID object: object length (%u bytes) is greater "
       "than remaining buffer (%lu bytes)", asn1_len, (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -1219,7 +1216,7 @@ int snmp_asn1_write_string(pool *p, unsigned char **buf, size_t *buflen,
       "than remaining buffer (%lu bytes)", (unsigned long) asn1_strlen,
       (unsigned long) (*buflen));
 
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
diff --git a/contrib/mod_snmp/asn1.h b/contrib/mod_snmp/asn1.h
index 5e155fe..ef903b2 100644
--- a/contrib/mod_snmp/asn1.h
+++ b/contrib/mod_snmp/asn1.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp ASN.1 support
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: asn1.h,v 1.1 2013-05-15 15:20:26 castaglia Exp $
  */
 
-#include "mod_snmp.h"
-
 #ifndef MOD_SNMP_ASN1_H
 #define MOD_SNMP_ASN1_H
 
+#include "mod_snmp.h"
+
 typedef uint32_t oid_t;
 
 /* ASN.1 OIDs */
@@ -120,4 +118,4 @@ int snmp_asn1_write_exception(pool *p, unsigned char **buf, size_t *buflen,
 
 /* XXX Need an snmp_asn1_write_sequence() function? */
 
-#endif
+#endif /* MOD_SNMP_ASN1_H */
diff --git a/contrib/mod_snmp/configure b/contrib/mod_snmp/configure
index 9d81e16..bd9ed02 100755
--- a/contrib/mod_snmp/configure
+++ b/contrib/mod_snmp/configure
@@ -3199,7 +3199,7 @@ else
   { echo "$as_me:$LINENO: result: no" >&5
 echo "${ECHO_T}no" >&6; }
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 
 { echo "$as_me:$LINENO: checking for library containing strerror" >&5
@@ -3353,7 +3353,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3374,7 +3374,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3679,6 +3679,38 @@ _ACEOF
 fi
 
 
+
+# Check whether --with-includes was given.
+if test "${with_includes+set}" = set; then
+  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for ainclude in $ac_addl_includes; do
+      if test x"$ac_build_addl_includes" = x ; then
+        ac_build_addl_includes="-I$ainclude"
+      else
+        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
+      fi
+    done
+    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
+
+fi
+
+
+
+# Check whether --with-libraries was given.
+if test "${with_libraries+set}" = set; then
+  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
+    for alibdir in $ac_addl_libdirs; do
+      if test x"$ac_build_addl_libdirs" = x ; then
+        ac_build_addl_libdirs="-L$alibdir"
+      else
+        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
+      fi
+    done
+    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
+
+fi
+
+
 { echo "$as_me:$LINENO: checking for ANSI C header files" >&5
 echo $ECHO_N "checking for ANSI C header files... $ECHO_C" >&6; }
 if test "${ac_cv_header_stdc+set}" = set; then
@@ -3747,7 +3779,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -3768,7 +3800,7 @@ if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
 else
   ac_cv_header_stdc=no
 fi
-rm -f -r conftest*
+rm -f conftest*
 
 fi
 
@@ -4095,38 +4127,6 @@ fi
 done
 
 
-
-# Check whether --with-includes was given.
-if test "${with_includes+set}" = set; then
-  withval=$with_includes;  ac_addl_includes=`echo "$withval" | sed -e 's/:/ /g'` ;
-    for ainclude in $ac_addl_includes; do
-      if test x"$ac_build_addl_includes" = x ; then
-        ac_build_addl_includes="-I$ainclude"
-      else
-        ac_build_addl_includes="-I$ainclude $ac_build_addl_includes"
-      fi
-    done
-    CPPFLAGS="$CPPFLAGS $ac_build_addl_includes"
-
-fi
-
-
-
-# Check whether --with-libraries was given.
-if test "${with_libraries+set}" = set; then
-  withval=$with_libraries;  ac_addl_libdirs=`echo "$withval" | sed -e 's/:/ /g'` ;
-    for alibdir in $ac_addl_libdirs; do
-      if test x"$ac_build_addl_libdirs" = x ; then
-        ac_build_addl_libdirs="-L$alibdir"
-      else
-        ac_build_addl_libdirs="-L$alibdir $ac_build_addl_libdirs"
-      fi
-    done
-    LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
-
-fi
-
-
 INCLUDES="$ac_build_addl_includes"
 
 
@@ -5171,7 +5171,7 @@ do
     cat >>$CONFIG_STATUS <<_ACEOF
     # First, check the format of the line:
     cat >"\$tmp/defines.sed" <<\\CEOF
-/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*/b def
+/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*\$/b def
 /^[	 ]*#[	 ]*define[	 ][	 ]*$ac_word_re[(	 ]/b def
 b
 :def
diff --git a/contrib/mod_snmp/configure.in b/contrib/mod_snmp/configure.in
index 8855d72..d35e7f2 100644
--- a/contrib/mod_snmp/configure.in
+++ b/contrib/mod_snmp/configure.in
@@ -29,10 +29,6 @@ AC_AIX
 AC_ISC_POSIX
 AC_MINIX
 
-AC_HEADER_STDC
-AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h sys/sysctl.h sys/sysinfo.h)
-AC_CHECK_FUNCS(random sysctl sysinfo)
-
 dnl Need to support/handle the --with-includes and --with-libraries options
 AC_ARG_WITH(includes,
   [AC_HELP_STRING(
@@ -66,6 +62,10 @@ AC_ARG_WITH(libraries,
     LDFLAGS="$LDFLAGS $ac_build_addl_libdirs"
   ])
 
+AC_HEADER_STDC
+AC_CHECK_HEADERS(stdlib.h unistd.h limits.h fcntl.h sys/sysctl.h sys/sysinfo.h)
+AC_CHECK_FUNCS(random sysctl sysinfo)
+
 INCLUDES="$ac_build_addl_includes"
 
 AC_SUBST(INCLUDES)
diff --git a/contrib/mod_snmp/db.c b/contrib/mod_snmp/db.c
index eed0ebb..67a86fe 100644
--- a/contrib/mod_snmp/db.c
+++ b/contrib/mod_snmp/db.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp database storage
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -1081,7 +1081,7 @@ int snmp_db_get_value(pool *p, unsigned int field, int32_t *int_value,
       return 0;
 
     case SNMP_DB_CONN_F_USER_NAME: {
-      char *orig_user;
+      const char *orig_user;
 
       orig_user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
       if (orig_user == NULL) {
@@ -1089,7 +1089,7 @@ int snmp_db_get_value(pool *p, unsigned int field, int32_t *int_value,
         return -1;
       }
     
-      *str_value = orig_user;
+      *str_value = (char *) orig_user;
       *str_valuelen = strlen(*str_value);  
 
       pr_trace_msg(trace_channel, 19,
@@ -1131,7 +1131,7 @@ int snmp_db_get_value(pool *p, unsigned int field, int32_t *int_value,
       return 0;
 
     case SNMP_DB_DAEMON_F_ADMIN:
-      *str_value = main_server->ServerAdmin; 
+      *str_value = (char *) main_server->ServerAdmin;
       *str_valuelen = strlen(*str_value);
 
       pr_trace_msg(trace_channel, 19,
diff --git a/contrib/mod_snmp/db.h b/contrib/mod_snmp/db.h
index cda106e..b54fb97 100644
--- a/contrib/mod_snmp/db.h
+++ b/contrib/mod_snmp/db.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp database tables
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -22,11 +22,11 @@
  * source distribution.
  */
 
-#include "mod_snmp.h"
-
 #ifndef MOD_SNMP_DB_H
 #define MOD_SNMP_DB_H
 
+#include "mod_snmp.h"
+
 /* Database IDs */
 #define SNMP_DB_ID_UNKNOWN		0
 #define SNMP_DB_ID_NOTIFY		1
@@ -242,4 +242,4 @@ int snmp_db_reset_value(pool *p, unsigned int field);
  */
 int snmp_db_set_root(const char *path);
 
-#endif
+#endif /* MOD_SNMP_DB_H */
diff --git a/contrib/mod_snmp/mib.c b/contrib/mod_snmp/mib.c
index e45be71..922966a 100644
--- a/contrib/mod_snmp/mib.c
+++ b/contrib/mod_snmp/mib.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp MIB support
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -27,7 +27,6 @@
 #include "mib.h"
 #include "smi.h"
 #include "db.h"
-#include "stacktrace.h"
 
 /* This table maps the OIDs in the PROFTPD-MIB to the database field where
  * that value is stored.
@@ -1134,7 +1133,7 @@ int snmp_mib_get_max_idx(void) {
 }
 
 struct snmp_mib *snmp_mib_get_by_idx(unsigned int mib_idx) {
-  if (mib_idx > snmp_mib_get_max_idx()) {
+  if (mib_idx > (unsigned int) snmp_mib_get_max_idx()) {
     errno = EINVAL;
     return NULL;
   }
diff --git a/contrib/mod_snmp/mib.h b/contrib/mod_snmp/mib.h
index a0b3927..a33a00b 100644
--- a/contrib/mod_snmp/mib.h
+++ b/contrib/mod_snmp/mib.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp MIB
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -22,12 +22,12 @@
  * source distribution.
  */
 
-#include "mod_snmp.h"
-#include "asn1.h"
-
 #ifndef MOD_SNMP_MIB_H
 #define MOD_SNMP_MIB_H
 
+#include "mod_snmp.h"
+#include "asn1.h"
+
 /* SNMPv2-MIB
  *
  * .iso.org.dod.internet.mgmt.mib-2.system
@@ -758,4 +758,4 @@ int snmp_mib_reset_counters(void);
 /* Initialize the MIB. */
 int snmp_mib_init(void);
 
-#endif
+#endif /* MOD_SNMP_MIB_H */
diff --git a/contrib/mod_snmp/mod_snmp.c b/contrib/mod_snmp/mod_snmp.c
index aff1d9b..647f8c6 100644
--- a/contrib/mod_snmp/mod_snmp.c
+++ b/contrib/mod_snmp/mod_snmp.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp
- * Copyright (c) 2008-2013 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -21,7 +21,7 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * DO NOT EDIT BELOW THIS LINE
+ * -----DO NOT EDIT BELOW THIS LINE-----
  * $Archive: mod_snmp.a $
  */
 
@@ -362,6 +362,8 @@ static int snmp_limits_allow(xaset_t *set, struct snmp_packet *pkt) {
       switch (snmp_check_limit(c, pkt)) {
         case 1:
           ok = TRUE;
+          found++;
+          break;
 
         case -1:
         case -2:
@@ -421,7 +423,7 @@ static int snmp_mkdir(const char *dir, uid_t uid, gid_t gid, mode_t mode) {
   struct stat st;
   int res = -1;
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(dir);
   res = pr_fsio_stat(dir, &st);
 
   if (res == -1 &&
@@ -459,7 +461,7 @@ static int snmp_mkpath(pool *p, const char *path, uid_t uid, gid_t gid,
   char *currpath = NULL, *tmppath = NULL;
   struct stat st;
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   if (pr_fsio_stat(path, &st) == 0) {
     /* Path already exists, nothing to be done. */
     errno = EEXIST;
@@ -793,8 +795,7 @@ static int snmp_agent_handle_getnext(struct snmp_packet *pkt) {
           case SNMP_PROTOCOL_VERSION_2:
           case SNMP_PROTOCOL_VERSION_3:
             resp_var = snmp_smi_create_exception(pkt->pool, iter_var->name,
-              iter_var->namelen, lacks_instance_id ? SNMP_SMI_NO_SUCH_INSTANCE :
-                SNMP_SMI_NO_SUCH_OBJECT);
+              iter_var->namelen, SNMP_SMI_NO_SUCH_OBJECT);
             break;
         }
 
@@ -1443,11 +1444,9 @@ static int snmp_agent_handle_packet(int sockfd, pr_netaddr_t *agent_addr) {
     return -1;
   }
 
-  if (pkt->req_pdu != NULL) {
-    /* We're done with the request PDU here. */
-    destroy_pool(pkt->req_pdu->pool);
-    pkt->req_pdu = NULL;
-  }
+  /* We're done with the request PDU here. */
+  destroy_pool(pkt->req_pdu->pool);
+  pkt->req_pdu = NULL;
 
   (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
     "writing SNMP message for %s, community = '%s', request ID %ld, "
@@ -1473,14 +1472,14 @@ static int snmp_agent_handle_packet(int sockfd, pr_netaddr_t *agent_addr) {
 }
 
 static int snmp_agent_listen(pr_netaddr_t *agent_addr) {
-  int res, sockfd;
+  int family, res, sockfd;
 
-  /* XXX Support IPv6? */
-
-  sockfd = socket(AF_INET, SOCK_DGRAM, snmp_proto_udp);
+  family = pr_netaddr_get_family(agent_addr);
+  sockfd = socket(family, SOCK_DGRAM, snmp_proto_udp);
   if (sockfd < 0) {
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-      "unable to create UDP socket: %s", strerror(errno));
+      "unable to create %s UDP socket: %s",
+      family == AF_INET ? "IPv4" : "IPv6", strerror(errno));
     exit(1);
   }
 
@@ -1488,21 +1487,33 @@ static int snmp_agent_listen(pr_netaddr_t *agent_addr) {
     pr_netaddr_get_sockaddr_len(agent_addr));
   if (res < 0) {
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-      "unable to bind UDP socket to %s#%u: %s",
+      "unable to bind %s UDP socket to %s#%u: %s",
+      family == AF_INET ? "IPv4" : "IPv6",
       pr_netaddr_get_ipstr(agent_addr),
       ntohs(pr_netaddr_get_port(agent_addr)), strerror(errno));
+    (void) close(sockfd);
     exit(1);
+
+  } else {
+    (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
+      "bound %s UDP socket to %s#%u", family == AF_INET ? "IPv4" : "IPv6",
+      pr_netaddr_get_ipstr(agent_addr),
+      ntohs(pr_netaddr_get_port(agent_addr)));
   }
 
-  return 0;
+  return sockfd;
 }
 
-static void snmp_agent_loop(int sockfd, pr_netaddr_t *agent_addr) {
-  fd_set listenfds;
+static void snmp_agent_loop(array_header *sockfds, array_header *addrs) {
+  fd_set listen_fds;
   struct timeval tv;
-  int res;
+  int fd, res;
 
   while (TRUE) {
+    register unsigned int i;
+    int maxfd = -1, *fds;
+    pr_netaddr_t **agent_addrs;
+
     /* XXX Is it necessary to even have a timeout?  We could simply block
      * in select(2) indefinitely, until either an event arrives or we are
      * interrupted by a signal.
@@ -1519,10 +1530,21 @@ static void snmp_agent_loop(int sockfd, pr_netaddr_t *agent_addr) {
      */
     snmp_notify_poll_cond();
 
-    FD_ZERO(&listenfds);
-    FD_SET(sockfd, &listenfds);
+    FD_ZERO(&listen_fds);
 
-    res = select(sockfd + 1, &listenfds, NULL, NULL, &tv);
+    fds = sockfds->elts; 
+    agent_addrs = addrs->elts;
+
+    for (i = 0; i < sockfds->nelts; i++) {
+      fd = fds[i];
+      FD_SET(fd, &listen_fds);
+
+      if (fd > maxfd) {
+        maxfd = fd;
+      }
+    }
+
+    res = select(maxfd + 1, &listen_fds, NULL, NULL, &tv);
     if (res == 0) {
       /* Select timeout reached.  Just try again. */
       continue;
@@ -1535,11 +1557,18 @@ static void snmp_agent_loop(int sockfd, pr_netaddr_t *agent_addr) {
       }
 
     } else {
-      if (FD_ISSET(sockfd, &listenfds)) {
-        res = snmp_agent_handle_packet(sockfd, agent_addr);
-        if (res < 0) {
-          (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-            "error handling SNMP packet: %s", strerror(errno));
+      for (i = 0; i < sockfds->nelts; i++) {
+        pr_netaddr_t *agent_addr;
+
+        fd = fds[i];
+        agent_addr = agent_addrs[i];
+
+        if (FD_ISSET(fd, &listen_fds)) {
+          res = snmp_agent_handle_packet(fd, agent_addr);
+          if (res < 0) {
+            (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
+              "error handling SNMP packet: %s", strerror(errno));
+          }
         } 
       }
     }
@@ -1547,11 +1576,12 @@ static void snmp_agent_loop(int sockfd, pr_netaddr_t *agent_addr) {
 }
 
 static pid_t snmp_agent_start(const char *tables_dir, int agent_type,
-    pr_netaddr_t *agent_addr) {
-  int agent_fd;
+    array_header *agent_addrs) {
+  register unsigned int i;
   pid_t agent_pid;
   char *agent_chroot = NULL;
   rlim_t curr_nproc, max_nproc;
+  array_header *agent_fds = NULL;
 
   agent_pid = fork();
   switch (agent_pid) {
@@ -1590,17 +1620,32 @@ static pid_t snmp_agent_start(const char *tables_dir, int agent_type,
    * an AgentX sub-agent.
    */
 
-  agent_fd = snmp_agent_listen(agent_addr);
-  if (agent_fd < 0) {
+  for (i = 0; i < agent_addrs->nelts; i++) {
+    pr_netaddr_t *agent_addr, **addrs;
+    int agent_fd;
+
+    addrs = agent_addrs->elts;
+    agent_addr = addrs[i];
+
+    agent_fd = snmp_agent_listen(agent_addr);
+    if (agent_fd < 0) {
+      (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
+        "unable to create listening socket for SNMP agent process: %s",
+       strerror(errno));
+      exit(0);
+    }
+
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-      "unable to create listening socket for SNMP agent process: %s",
-      strerror(errno));
-    exit(0);
-  }
+      "SNMP agent process listening on %s UDP %s#%u",
+      pr_netaddr_get_family(agent_addr) == AF_INET ? "IPv4" : "IPv6",
+      pr_netaddr_get_ipstr(agent_addr), ntohs(pr_netaddr_get_port(agent_addr)));
 
-  (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-    "SNMP agent process listening on UDP %s#%u",
-    pr_netaddr_get_ipstr(agent_addr), ntohs(pr_netaddr_get_port(agent_addr)));
+    if (agent_fds == NULL) {
+      agent_fds = make_array(snmp_pool, 1, sizeof(int));
+    }
+
+    *((int *) push_array(agent_fds)) = agent_fd;
+  }
 
   PRIVS_ROOT
 
@@ -1645,13 +1690,15 @@ static pid_t snmp_agent_start(const char *tables_dir, int agent_type,
 
   if (agent_chroot != NULL) {
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-      "SNMP agent process running with UID %lu, GID %lu, restricted to '%s'",
-      (unsigned long) getuid(), (unsigned long) getgid(), agent_chroot);
+      "SNMP agent process running with UID %s, GID %s, restricted to '%s'",
+      pr_uid2str(snmp_pool, getuid()), pr_gid2str(snmp_pool, getgid()),
+      agent_chroot);
 
   } else {
     (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-      "SNMP agent process running with UID %lu, GID %lu, located in '%s'",
-      (unsigned long) getuid(), (unsigned long) getgid(), getcwd(NULL, 0));
+      "SNMP agent process running with UID %s, GID %s, located in '%s'",
+      pr_uid2str(snmp_pool, getuid()), pr_gid2str(snmp_pool, getgid()),
+      getcwd(NULL, 0));
   }
 
   /* Once we have chrooted, and dropped root privs completely, we can now
@@ -1660,8 +1707,11 @@ static pid_t snmp_agent_start(const char *tables_dir, int agent_type,
    * possible exploitation.
    */
   if (pr_rlimit_get_nproc(&curr_nproc, NULL) == 0) {
+    /* Override whatever the configured nproc is; we only want 1. */
+    curr_nproc = 1;
+
     max_nproc = curr_nproc;
- 
+
     if (pr_rlimit_set_nproc(curr_nproc, max_nproc) < 0) {
       (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
         "error setting nproc resource limits to %lu: %s",
@@ -1677,7 +1727,7 @@ static pid_t snmp_agent_start(const char *tables_dir, int agent_type,
       "error getting nproc limits: %s", strerror(errno));
   }
 
-  snmp_agent_loop(agent_fd, agent_addr);
+  snmp_agent_loop(agent_fds, agent_addrs);
 
   /* When we are done, we simply exit. */;
   pr_trace_msg("snmp", 3, "SNMP agent PID %lu exiting",
@@ -1791,15 +1841,16 @@ static void snmp_agent_stop(pid_t agent_pid) {
 /* Configuration handlers
  */
 
-/* usage: SNMPAgent "master"|"agentx" address[:port] */
+/* usage: SNMPAgent "master"|"agentx" address[:port] [...] */
 MODRET set_snmpagent(cmd_rec *cmd) {
+  register unsigned int i;
   config_rec *c;
+  array_header *agent_addrs;
   int agent_type;
-  pr_netaddr_t *agent_addr;
-  int agent_port = SNMP_DEFAULT_AGENT_PORT;
-  char *ptr;
 
-  CHECK_ARGS(cmd, 2);
+  if (cmd->argc < 2) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
   CHECK_CONF(cmd, CONF_ROOT);
 
   if (strncasecmp(cmd->argv[1], "master", 7) == 0) {
@@ -1813,35 +1864,76 @@ MODRET set_snmpagent(cmd_rec *cmd) {
       cmd->argv[1], "'", NULL));
   }
 
-  /* Separate the port out from the address, if present.
-   *
-   * XXX Make sure we can handle an IPv6 address here, e.g.:
-   *
-   *   [::1]:162
-   */
-  ptr = strrchr(cmd->argv[2], ':');
-  if (ptr != NULL) {
-    *ptr = '\0';
+  agent_addrs = make_array(snmp_pool, 1, sizeof(pr_netaddr_t *));
 
-    agent_port = atoi(ptr + 1);
-    if (agent_port < 1 ||
-        agent_port > 65535) {
-      CONF_ERROR(cmd, "port must be between 1-65535");
+  for (i = 2; i < cmd->argc; i++) {
+    const pr_netaddr_t *agent_addr;
+    int agent_port = SNMP_DEFAULT_AGENT_PORT;
+    char *addr = NULL, *ptr;
+    size_t addrlen;
+
+    /* Separate the port out from the address, if present. */
+    ptr = strrchr(cmd->argv[i], ':');
+
+    if (ptr != NULL) {
+      char *ptr2;
+
+      /* We need to handle the following possibilities:
+       *
+       *  ipv4-addr
+       *  ipv4-addr:port
+       *  [ipv6-addr]
+       *  [ipv6-addr]:port
+       *
+       * Thus we check to see if the last ':' occurs before, or after,
+       * a ']' for an IPv6 address.
+       */
+
+      ptr2 = strrchr(cmd->argv[i], ']');
+      if (ptr2 != NULL) {
+        if (ptr2 > ptr) {
+          /* The found ':' is part of an IPv6 address, not a port delimiter. */
+          ptr = NULL;
+        }
+      }
+
+      if (ptr != NULL) {
+        *ptr = '\0';
+
+        agent_port = atoi(ptr + 1);
+        if (agent_port < 1 ||
+            agent_port > 65535) {
+          CONF_ERROR(cmd, "port must be between 1-65535");
+        }
+      }
     }
-  }
 
-  agent_addr = pr_netaddr_get_addr(snmp_pool, cmd->argv[2], NULL);
-  if (agent_addr == NULL) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to resolve \"",
-      cmd->argv[2], "\"", NULL));
-  }
+    addr = cmd->argv[i];
+    addrlen = strlen(addr);
 
-  pr_netaddr_set_port(agent_addr, htons(agent_port));
+    /* Make sure we can handle an IPv6 address here, e.g.:
+     *
+     *   [::1]:162
+     */
+    if (addrlen > 0 &&
+        (addr[0] == '[' && addr[addrlen-1] == ']')) {
+      addr = pstrndup(cmd->pool, addr + 1, addrlen - 2);
+    }
+
+    agent_addr = pr_netaddr_get_addr(snmp_pool, addr, NULL);
+    if (agent_addr == NULL) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to resolve \"", addr, "\"",
+        NULL));
+    }
+
+    pr_netaddr_set_port((pr_netaddr_t *) agent_addr, htons(agent_port));
+    *((pr_netaddr_t **) push_array(agent_addrs)) = (pr_netaddr_t *) agent_addr;
+  }
 
   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
   c->argv[0] = palloc(c->pool, sizeof(int));
   *((int *) c->argv[0]) = agent_type;
-  c->argv[1] = agent_addr;
+  c->argv[1] = agent_addrs;
  
   return PR_HANDLED(cmd);
 }
@@ -1857,20 +1949,20 @@ MODRET set_snmpcommunity(cmd_rec *cmd) {
 
 /* usage: SNMPEnable on|off */
 MODRET set_snmpenable(cmd_rec *cmd) {
-  int bool = -1;
+  int enabled = -1;
   config_rec *c;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  bool = get_boolean(cmd, 1);
-  if (bool == -1) {
+  enabled = get_boolean(cmd, 1);
+  if (enabled == -1) {
     CONF_ERROR(cmd, "expected Boolean parameter");
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = palloc(c->pool, sizeof(int));
-  *((int *) c->argv[0]) = bool;
+  *((int *) c->argv[0]) = enabled;
 
   return PR_HANDLED(cmd);
 }
@@ -1930,7 +2022,7 @@ MODRET set_snmpmaxvariables(cmd_rec *cmd) {
  */
 MODRET set_snmpnotify(cmd_rec *cmd) {
   config_rec *c;
-  pr_netaddr_t *notify_addr;
+  const pr_netaddr_t *notify_addr;
   int notify_port = SNMP_DEFAULT_TRAP_PORT;
   char *ptr;
 
@@ -1965,8 +2057,8 @@ MODRET set_snmpnotify(cmd_rec *cmd) {
       "': ", strerror(errno), NULL));
   }
 
-  pr_netaddr_set_port(notify_addr, htons(notify_port));
-  c->argv[0] = notify_addr;
+  pr_netaddr_set_port((pr_netaddr_t *) notify_addr, htons(notify_port));
+  c->argv[0] = (void *) notify_addr;
 
   return PR_HANDLED(cmd);
 }
@@ -2019,36 +2111,38 @@ MODRET set_snmpoptions(cmd_rec *cmd) {
 MODRET set_snmptables(cmd_rec *cmd) {
   int res;
   struct stat st;
+  char *path;
  
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
- 
-  if (*cmd->argv[1] != '/') {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be a full path: '",
-      cmd->argv[1], "'", NULL));
+
+  path = cmd->argv[1]; 
+  if (*path != '/') {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be a full path: '", path, "'",
+      NULL));
   }
 
-  res = stat(cmd->argv[1], &st);
+  res = stat(path, &st);
   if (res < 0) {
     char *agent_chroot;
 
     if (errno != ENOENT) {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to stat '", cmd->argv[1],
-        "': ", strerror(errno), NULL));
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to stat '", path, "': ",
+        strerror(errno), NULL));
     }
 
     pr_log_debug(DEBUG0, MOD_SNMP_VERSION
-      ": SNMPTables directory '%s' does not exist, creating it", cmd->argv[1]);
+      ": SNMPTables directory '%s' does not exist, creating it", path);
 
     /* Create the directory. */
-    res = snmp_mkpath(cmd->tmp_pool, cmd->argv[1], geteuid(), getegid(), 0755);
+    res = snmp_mkpath(cmd->tmp_pool, path, geteuid(), getegid(), 0755);
     if (res < 0) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to create directory '",
-        cmd->argv[1], "': ", strerror(errno), NULL));
+        path, "': ", strerror(errno), NULL));
     }
 
     /* Also create the empty/ directory underneath, for the chroot. */
-    agent_chroot = pdircat(cmd->tmp_pool, cmd->argv[1], "empty", NULL);
+    agent_chroot = pdircat(cmd->tmp_pool, path, "empty", NULL);
 
     res = snmp_mkpath(cmd->tmp_pool, agent_chroot, geteuid(), getegid(), 0111);
     if (res < 0) {
@@ -2057,20 +2151,20 @@ MODRET set_snmptables(cmd_rec *cmd) {
     }
 
     pr_log_debug(DEBUG2, MOD_SNMP_VERSION
-      ": created SNMPTables directory '%s'", cmd->argv[1]);
+      ": created SNMPTables directory '%s'", path);
 
   } else {
     char *agent_chroot;
 
     if (!S_ISDIR(st.st_mode)) {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", cmd->argv[1],
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", path,
         ": Not a directory", NULL));
     }
 
     /* See if the chroot directory empty/ already exists as well.  And enforce
      * the permissions on that directory.
      */
-    agent_chroot = pdircat(cmd->tmp_pool, cmd->argv[1], "empty", NULL);
+    agent_chroot = pdircat(cmd->tmp_pool, path, "empty", NULL);
 
     res = stat(agent_chroot, &st);
     if (res < 0) {
@@ -2100,7 +2194,7 @@ MODRET set_snmptables(cmd_rec *cmd) {
     }
   }
 
-  (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  (void) add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
@@ -3130,7 +3224,7 @@ static void ev_incr_value(unsigned int field_id, const char *field_str,
 
 static void snmp_auth_code_ev(const void *event_data, void *user_data) {
   int auth_code, res;
-  unsigned int field_id, is_ftps = FALSE, notify_id = 0;
+  unsigned int field_id = SNMP_DB_ID_UNKNOWN, is_ftps = FALSE, notify_id = 0;
   const char *notify_str = NULL, *proto;
 
   if (snmp_engine == FALSE) {
@@ -3322,7 +3416,7 @@ static void snmp_postparse_ev(const void *event_data, void *user_data) {
   unsigned int nvhosts = 0;
   const char *tables_dir;
   int agent_type, res;
-  pr_netaddr_t *agent_addr;
+  array_header *agent_addrs;
   unsigned char ban_loaded = FALSE, sftp_loaded = FALSE, tls_loaded = FALSE;
 
   c = find_config(main_server->conf, CONF_PARAM, "SNMPEngine", FALSE);
@@ -3467,9 +3561,9 @@ static void snmp_postparse_ev(const void *event_data, void *user_data) {
   }
 
   agent_type = *((int *) c->argv[0]);
-  agent_addr = c->argv[1];
+  agent_addrs = c->argv[1];
 
-  snmp_agent_pid = snmp_agent_start(tables_dir, agent_type, agent_addr);
+  snmp_agent_pid = snmp_agent_start(tables_dir, agent_type, agent_addrs);
   if (snmp_agent_pid == 0) {
     snmp_engine = FALSE;
     pr_log_debug(DEBUG0, MOD_SNMP_VERSION
@@ -3821,23 +3915,14 @@ static void snmp_ssh2_scp_sess_closed_ev(const void *event_data,
 
 /* mod_ban-generated events */
 static void snmp_ban_ban_user_ev(const void *event_data, void *user_data) {
-  const char *ban_name = NULL;
-
-  ban_name = (const char *) event_data;
-
   ev_incr_value(SNMP_DB_BAN_BANS_F_USER_BAN_COUNT, "ban.bans.userBanCount", 1);
   ev_incr_value(SNMP_DB_BAN_BANS_F_USER_BAN_TOTAL, "ban.bans.userBanTotal", 1);
 
   ev_incr_value(SNMP_DB_BAN_BANS_F_BAN_COUNT, "ban.bans.banCount", 1);
   ev_incr_value(SNMP_DB_BAN_BANS_F_BAN_TOTAL, "ban.bans.banTotal", 1);
-
 }
 
 static void snmp_ban_ban_host_ev(const void *event_data, void *user_data) {
-  const char *ban_name = NULL;
-
-  ban_name = (const char *) event_data;
-
   ev_incr_value(SNMP_DB_BAN_BANS_F_HOST_BAN_COUNT, "ban.bans.hostBanCount", 1);
   ev_incr_value(SNMP_DB_BAN_BANS_F_HOST_BAN_TOTAL, "ban.bans.hostBanTotal", 1);
 
@@ -3846,10 +3931,6 @@ static void snmp_ban_ban_host_ev(const void *event_data, void *user_data) {
 }
 
 static void snmp_ban_ban_class_ev(const void *event_data, void *user_data) {
-  const char *ban_name = NULL;
-
-  ban_name = (const char *) event_data;
-
   ev_incr_value(SNMP_DB_BAN_BANS_F_CLASS_BAN_COUNT,
     "ban.bans.classBanCount", 1);
   ev_incr_value(SNMP_DB_BAN_BANS_F_CLASS_BAN_TOTAL,
diff --git a/contrib/mod_snmp/mod_snmp.h.in b/contrib/mod_snmp/mod_snmp.h.in
index c9b76d1..870e208 100644
--- a/contrib/mod_snmp/mod_snmp.h.in
+++ b/contrib/mod_snmp/mod_snmp.h.in
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_snmp.h.in,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
 #ifndef MOD_SNMP_H
diff --git a/contrib/mod_snmp/msg.c b/contrib/mod_snmp/msg.c
index 9441902..25bed9f 100644
--- a/contrib/mod_snmp/msg.c
+++ b/contrib/mod_snmp/msg.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp message routines
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: msg.c,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
 #include "mod_snmp.h"
@@ -31,7 +29,6 @@
 #include "asn1.h"
 #include "packet.h"
 #include "db.h"
-#include "stacktrace.h"
 
 static const char *trace_channel = "snmp.msg";
 
diff --git a/contrib/mod_snmp/msg.h b/contrib/mod_snmp/msg.h
index 93522eb..92c124d 100644
--- a/contrib/mod_snmp/msg.h
+++ b/contrib/mod_snmp/msg.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp message routines
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,16 +20,14 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: msg.h,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
-#include "mod_snmp.h"
-#include "pdu.h"
-
 #ifndef MOD_SNMP_MSG_H
 #define MOD_SNMP_MSG_H
 
+#include "mod_snmp.h"
+#include "pdu.h"
+
 const char *snmp_msg_get_versionstr(long snmp_version);
 
 int snmp_msg_read(pool *p, unsigned char **buf, size_t *buflen,
@@ -39,4 +37,4 @@ int snmp_msg_write(pool *p, unsigned char **buf, size_t *buflen,
   char *community, unsigned int community_len, long snmp_version,
   struct snmp_pdu *pdu);
 
-#endif
+#endif /* MOD_SNMP_MSG_H */
diff --git a/contrib/mod_snmp/notify.c b/contrib/mod_snmp/notify.c
index 3a4e03a..0998ba5 100644
--- a/contrib/mod_snmp/notify.c
+++ b/contrib/mod_snmp/notify.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp notification routines
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -94,7 +94,7 @@ static oid_t *get_notify_oid(pool *p, unsigned int notify_id,
 }
 
 static struct snmp_packet *get_notify_pkt(pool *p, const char *community,
-    pr_netaddr_t *dst_addr, unsigned int notify_id,
+    const pr_netaddr_t *dst_addr, unsigned int notify_id,
     struct snmp_var **head_var, struct snmp_var **tail_var) {
   struct snmp_packet *pkt = NULL;
   struct snmp_mib *mib = NULL;
@@ -333,7 +333,8 @@ static int get_notify_varlist(pool *p, unsigned int notify_id,
 }
 
 int snmp_notify_generate(pool *p, int sockfd, const char *community,
-    pr_netaddr_t *src_addr, pr_netaddr_t *dst_addr, unsigned int notify_id) {
+    const pr_netaddr_t *src_addr, const pr_netaddr_t *dst_addr,
+    unsigned int notify_id) {
   const char *notify_str;
   struct snmp_packet *pkt;
   struct snmp_var *notify_varlist = NULL, *head_var = NULL, *tail_var = NULL,
diff --git a/contrib/mod_snmp/notify.h b/contrib/mod_snmp/notify.h
index 461a10c..80aae8b 100644
--- a/contrib/mod_snmp/notify.h
+++ b/contrib/mod_snmp/notify.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp notification types
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -22,20 +22,21 @@
  * source distribution.
  */
 
-#include "mod_snmp.h"
-#include "asn1.h"
-
 #ifndef MOD_SNMP_NOTIFY_H
 #define MOD_SNMP_NOTIFY_H
 
+#include "mod_snmp.h"
+#include "asn1.h"
+
 /* ftp.notifications */
 #define SNMP_NOTIFY_DAEMON_MAX_INSTANCES	100
 #define SNMP_NOTIFY_FTP_BAD_PASSWD		1000
 #define SNMP_NOTIFY_FTP_BAD_USER		1001
 
 int snmp_notify_generate(pool *p, int sockfd, const char *community,
-  pr_netaddr_t *src_addr, pr_netaddr_t *dst_addr, unsigned int notify_id);
+  const pr_netaddr_t *src_addr, const pr_netaddr_t *dst_addr,
+  unsigned int notify_id);
 long snmp_notify_get_request_id(void);
 void snmp_notify_poll_cond(void);
 
-#endif
+#endif /* MOD_SNMP_NOTIFY */
diff --git a/contrib/mod_snmp/packet.c b/contrib/mod_snmp/packet.c
index b128dc6..ab506c6 100644
--- a/contrib/mod_snmp/packet.c
+++ b/contrib/mod_snmp/packet.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp packet routines
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2015 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: packet.c,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
 #include "mod_snmp.h"
@@ -118,15 +116,9 @@ int snmp_packet_write(pool *p, int sockfd, struct snmp_packet *pkt) {
     }
 
   } else {
-    if (res == 0) {
-      (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-        "dropping response after waiting %u secs for available socket space",
-        (unsigned int) tv.tv_sec);
-
-    } else {
-      (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-        "dropping response due to select(2) failure: %s", strerror(errno));
-    }
+    (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
+      "dropping response after waiting %u secs for available socket space",
+      (unsigned int) tv.tv_sec);
 
     res = snmp_db_incr_value(pkt->pool, SNMP_DB_SNMP_F_PKTS_DROPPED_TOTAL, 1);
     if (res < 0) {
diff --git a/contrib/mod_snmp/packet.h b/contrib/mod_snmp/packet.h
index 4d5be58..6f154cb 100644
--- a/contrib/mod_snmp/packet.h
+++ b/contrib/mod_snmp/packet.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp packet routines
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,24 +20,22 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: packet.h,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
-#include "mod_snmp.h"
-#include "pdu.h"
-
 #ifndef MOD_SNMP_PACKET_H
 #define MOD_SNMP_PACKET_H
 
+#include "mod_snmp.h"
+#include "pdu.h"
+
 /* SNMP packets shouldn't be larger than 4K, right? */
 #define SNMP_PACKET_MAX_LEN		4096
 
 struct snmp_packet {
   pool *pool;
 
-  pr_netaddr_t *remote_addr;
-  pr_class_t *remote_class;
+  const pr_netaddr_t *remote_addr;
+  const pr_class_t *remote_class;
 
   /* Request packet data */
   unsigned char *req_data;
@@ -62,4 +60,4 @@ struct snmp_packet {
 struct snmp_packet *snmp_packet_create(pool *p);
 int snmp_packet_write(pool *p, int sockfd, struct snmp_packet *pkt);
 
-#endif
+#endif /* MOD_SNMP_PACKET_H */
diff --git a/contrib/mod_snmp/pdu.c b/contrib/mod_snmp/pdu.c
index 4728b2d..cf30d23 100644
--- a/contrib/mod_snmp/pdu.c
+++ b/contrib/mod_snmp/pdu.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp PDU routines
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: pdu.c,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
 #include "mod_snmp.h"
@@ -29,7 +27,6 @@
 #include "pdu.h"
 #include "smi.h"
 #include "asn1.h"
-#include "stacktrace.h"
 
 static const char *trace_channel = "snmp.pdu";
 
diff --git a/contrib/mod_snmp/pdu.h b/contrib/mod_snmp/pdu.h
index dbddbe4..cb6eed1 100644
--- a/contrib/mod_snmp/pdu.h
+++ b/contrib/mod_snmp/pdu.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp PDU routines
- * Copyright (c) 2008-2011 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,17 +20,15 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: pdu.h,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
+#ifndef MOD_SNMP_PDU_H
+#define MOD_SNMP_PDU_H
+
 #include "mod_snmp.h"
 #include "asn1.h"
 #include "smi.h"
 
-#ifndef MOD_SNMP_PDU_H
-#define MOD_SNMP_PDU_H
-
 /* RFC1905 SNMPv2 ResponsePDU error status codes */
 #define SNMP_ERR_NO_ERROR		0
 #define SNMP_ERR_TOO_BIG		1
@@ -119,4 +117,4 @@ int snmp_pdu_read(pool *p, unsigned char **buf, size_t *buflen,
 int snmp_pdu_write(pool *p, unsigned char **buf, size_t *buflen,
     struct snmp_pdu *pdu, long snmp_version);
 
-#endif
+#endif /* MOD_SNMP_PDU_H */
diff --git a/contrib/mod_snmp/smi.c b/contrib/mod_snmp/smi.c
index 42ffebc..2b829b7 100644
--- a/contrib/mod_snmp/smi.c
+++ b/contrib/mod_snmp/smi.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp SMI routines
- * Copyright (c) 2008-2014 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: smi.c,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
 #include "mod_snmp.h"
@@ -29,7 +27,6 @@
 #include "smi.h"
 #include "mib.h"
 #include "msg.h"
-#include "stacktrace.h"
 
 static const char *trace_channel = "snmp.smi";
 
@@ -292,7 +289,7 @@ struct snmp_var *snmp_smi_dup_var(pool *p, struct snmp_var *src_var) {
 
           /* XXX Destroy the entire chain? */
           destroy_pool(var->pool);
-          snmp_stacktrace_log();
+          pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
           errno = EINVAL;
           return NULL;
       }
@@ -336,7 +333,7 @@ int snmp_smi_read_vars(pool *p, unsigned char **buf, size_t *buflen,
     pr_trace_msg(trace_channel, 1,
       "unable to parse tag (%s) as list of variables",
       snmp_asn1_get_tagstr(p, asn1_type));
-    snmp_stacktrace_log();
+    pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
     errno = EINVAL;
     return -1;
   }
@@ -361,7 +358,7 @@ int snmp_smi_read_vars(pool *p, unsigned char **buf, size_t *buflen,
       pr_trace_msg(trace_channel, 1,
         "unable to parse tag (%s) as variable binding",
         snmp_asn1_get_tagstr(p, asn1_type));
-      snmp_stacktrace_log();
+      pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
       errno = EINVAL;
       return -1;
     }
@@ -383,7 +380,7 @@ int snmp_smi_read_vars(pool *p, unsigned char **buf, size_t *buflen,
         snmp_asn1_get_tagstr(p, asn1_type));
 
       destroy_pool(var->pool);
-      snmp_stacktrace_log();
+      pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
       errno = EINVAL;
       return -1;
     }
@@ -498,7 +495,7 @@ int snmp_smi_read_vars(pool *p, unsigned char **buf, size_t *buflen,
         pr_trace_msg(trace_channel, 1,
           "unable to read variable type %x", var->smi_type);
         destroy_pool(var->pool);
-        snmp_stacktrace_log(); 
+        pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
         errno = EINVAL;
         return -1;
     }
@@ -639,7 +636,7 @@ int snmp_smi_write_vars(pool *p, unsigned char **buf, size_t *buflen,
         /* Unsupported type */
         pr_trace_msg(trace_channel, 1, "%s",
           "unable to encode unsupported SMI variable type");
-        snmp_stacktrace_log();
+        pr_log_stacktrace(snmp_logfd, MOD_SNMP_VERSION);
         errno = ENOSYS;
         return -1;
     }
diff --git a/contrib/mod_snmp/smi.h b/contrib/mod_snmp/smi.h
index 572b5ee..b51193d 100644
--- a/contrib/mod_snmp/smi.h
+++ b/contrib/mod_snmp/smi.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp SMI routines
- * Copyright (c) 2008-2012 TJ Saunders
+ * Copyright (c) 2008-2016 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
@@ -20,16 +20,14 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: smi.h,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
-#include "mod_snmp.h"
-#include "asn1.h"
-
 #ifndef MOD_SNMP_SMI_H
 #define MOD_SNMP_SMI_H
 
+#include "mod_snmp.h"
+#include "asn1.h"
+
 /* RFC1902 Structure of Management Information (SMI) for SNMPv2 */
 #define SNMP_SMI_INTEGER	SNMP_ASN1_TYPE_INTEGER
 #define SNMP_SMI_STRING		SNMP_ASN1_TYPE_OCTETSTRING
@@ -112,4 +110,4 @@ int snmp_smi_write_vars(pool *p, unsigned char **buf, size_t *buflen,
 unsigned int snmp_smi_util_add_list_var(struct snmp_var **head,
   struct snmp_var **tail, struct snmp_var *var);
 
-#endif
+#endif /* MOD_SNMP_SMI_H */
diff --git a/contrib/mod_snmp/stacktrace.c b/contrib/mod_snmp/stacktrace.c
deleted file mode 100644
index a0098e3..0000000
--- a/contrib/mod_snmp/stacktrace.c
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * ProFTPD - mod_snmp stacktrace logging
- * Copyright (c) 2008-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.
- *
- * $Id: stacktrace.c,v 1.1 2013-05-15 15:20:27 castaglia Exp $
- */
-
-#include "mod_snmp.h"
-#include "stacktrace.h"
-
-#ifdef HAVE_EXECINFO_H
-# include <execinfo.h>
-#endif
-
-void snmp_stacktrace_log(void) {
-#if defined(HAVE_EXECINFO_H) && \
-    defined(HAVE_BACKTRACE) && \
-    defined(HAVE_BACKTRACE_SYMBOLS)
-  void *trace[PR_TUNABLE_CALLER_DEPTH];
-  char **strings;
-  size_t tracesz;
-
-  (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-    "-----BEGIN STACK TRACE-----");
-
-  tracesz = backtrace(trace, PR_TUNABLE_CALLER_DEPTH);
-  strings = backtrace_symbols(trace, tracesz);
-  if (strings != NULL) {
-    register unsigned int i;
-
-    for (i = 1; i < tracesz; i++) {
-      (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-        "[%u] %s", i-1, strings[i]);
-    }
-
-    /* Prevent memory leaks. */
-    free(strings);
-
-  } else {
-    (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-      "error obtaining stacktrace symbols: %s", strerror(errno));
-  }
-
-  (void) pr_log_writefile(snmp_logfd, MOD_SNMP_VERSION,
-    "-----END STACK TRACE-----");
-#endif
-}
diff --git a/contrib/mod_snmp/stacktrace.h b/contrib/mod_snmp/stacktrace.h
deleted file mode 100644
index f4d7962..0000000
--- a/contrib/mod_snmp/stacktrace.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * ProFTPD - mod_snmp stacktrace logging
- * Copyright (c) 2008-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.
- *
- * $Id: stacktrace.h,v 1.1 2013-05-15 15:20:27 castaglia Exp $
- */
-
-#include "mod_snmp.h"
-
-#ifndef MOD_SNMP_STACKTRACE_H
-#define MOD_SNMP_STACKTRACE_H
-
-void snmp_stacktrace_log(void);
-
-#endif
diff --git a/contrib/mod_snmp/uptime.c b/contrib/mod_snmp/uptime.c
index 753695d..581f65d 100644
--- a/contrib/mod_snmp/uptime.c
+++ b/contrib/mod_snmp/uptime.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp uptime
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: uptime.c,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
 #include "mod_snmp.h"
diff --git a/contrib/mod_snmp/uptime.h b/contrib/mod_snmp/uptime.h
index 9262ae9..569f821 100644
--- a/contrib/mod_snmp/uptime.h
+++ b/contrib/mod_snmp/uptime.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_snmp uptime 
- * Copyright (c) 2012 TJ Saunders
+ * Copyright (c) 2012-2016 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
@@ -20,15 +20,13 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: uptime.h,v 1.1 2013-05-15 15:20:27 castaglia Exp $
  */
 
-#include "mod_snmp.h"
-
 #ifndef MOD_SNMP_UPTIME_H
 #define MOD_SNMP_UPTIME_H
 
+#include "mod_snmp.h"
+
 int snmp_uptime_get(pool *p, struct timeval *tv);
 
-#endif
+#endif /* MOD_SNMP_UPTIME_H */
diff --git a/contrib/mod_sql.c b/contrib/mod_sql.c
index 1fd2170..cbb6b2b 100644
--- a/contrib/mod_sql.c
+++ b/contrib/mod_sql.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_sql -- SQL frontend
  * Copyright (c) 1998-1999 Johnie Ingram.
  * Copyright (c) 2001 Andrew Houghton.
- * Copyright (c) 2004-2016 TJ Saunders
+ * Copyright (c) 2004-2017 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
@@ -113,6 +113,8 @@ static off_t sql_dele_filesz = 0;
  */
 #define SQL_MAX_STMT_LEN	4096
 
+static int sql_sess_init(void);
+
 static char *sql_prepare_where(int, cmd_rec *, int, ...);
 #define SQL_PREPARE_WHERE_FL_NO_TAGS	0x00001
 
@@ -410,30 +412,38 @@ static void *cache_findvalue(cache_t *cache, void *data) {
   int hashval;
 
   if (cache == NULL ||
-      data == NULL)
+      data == NULL) {
+    errno = EINVAL;
     return NULL;
-  
+  }
+
   hashval = cache->hash_val(data) % CACHE_SIZE;
 
   entry = cache->buckets[hashval];
   while (entry != NULL) {
     pr_signals_handle();
 
-    if (cache->cmp(data, entry->data))
+    if (cache->cmp(data, entry->data)) {
       break;
-    else
-      entry = entry->bucket_next;
+    }
+
+    entry = entry->bucket_next;
   }
 
   return (entry == NULL ? NULL : entry->data);
 }
 
-cmd_rec *_sql_make_cmd(pool *p, int argc, ...) {
-  register unsigned int i = 0;
+cmd_rec *sql_make_cmd(pool *p, int argc, ...) {
+  register int i = 0;
   pool *newpool = NULL;
   cmd_rec *cmd = NULL;
   va_list args;
 
+  if (argc < 0) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   newpool = make_sub_pool(p);
   cmd = pcalloc(newpool, sizeof(cmd_rec));
   cmd->argc = argc;
@@ -446,19 +456,19 @@ cmd_rec *_sql_make_cmd(pool *p, int argc, ...) {
 
   va_start(args, argc);
 
-  for (i = 0; i < argc; i++)
+  for (i = 0; i < argc; i++) {
     cmd->argv[i] = (void *) va_arg(args, char *);
-
+  }
   va_end(args);
 
   cmd->argv[argc] = NULL;
-
   return cmd;
 }
 
 static int check_response(modret_t *mr, int flags) {
-  if (!MODRET_ISERROR(mr))
+  if (!MODRET_ISERROR(mr)) {
     return 0;
+  }
 
   sql_log(DEBUG_WARN, "%s", "unrecoverable backend error");
   sql_log(DEBUG_WARN, "error: '%s'", mr->mr_numeric);
@@ -469,6 +479,8 @@ static int check_response(modret_t *mr, int flags) {
   pr_log_pri(PR_LOG_ERR, MOD_SQL_VERSION
     ": check the SQLLogFile for more details");
 
+  pr_event_generate("mod_sql.db.error", mr->mr_message);
+
   if (!(flags & SQL_LOG_FL_IGNORE_ERRORS) &&
       !(pr_sql_opts & SQL_OPT_NO_DISCONNECT_ON_ERROR)) {
     pr_session_disconnect(&sql_module, PR_SESS_DISCONNECT_BY_APPLICATION,
@@ -485,10 +497,12 @@ static int check_response(modret_t *mr, int flags) {
   return -1;
 }
 
-static modret_t *_sql_dispatch(cmd_rec *cmd, char *cmdname) {
+static modret_t *sql_dispatch(cmd_rec *cmd, char *cmdname) {
   modret_t *mr = NULL;
   register unsigned int i = 0;
 
+  pr_trace_msg(trace_channel, 19, "dispatching SQL command '%s'", cmdname);
+
   for (i = 0; sql_cmdtable[i].command; i++) {
     if (strcmp(cmdname, sql_cmdtable[i].command) == 0) {
       pr_signals_block();
@@ -736,7 +750,7 @@ static modret_t *sql_auth_backend(cmd_rec *cmd, const char *plaintext,
     return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
   }
 
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
     plaintext, ciphertext), "sql_checkauth");
   return mr;
 }
@@ -915,13 +929,13 @@ int sql_unregister_authtype(const char *name) {
  * version of that name */
 static char *_sql_realuser(cmd_rec *cmd) {
   modret_t *mr = NULL;
-  char *user = NULL;
+  const char *user = NULL;
 
   /* this is the userid given by the user */
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
 
   /* Do we need to check for useralias? see mod_time.c, get_user_cmd_times(). */
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
     user), "sql_escapestring");
   if (check_response(mr, 0) < 0) {
     return NULL;
@@ -930,13 +944,29 @@ static char *_sql_realuser(cmd_rec *cmd) {
   return mr ? (char *) mr->data : NULL;
 }
 
-static int sql_define_conn(pool *p, char *conn_name, char *user, char *passwd,
-    char *info, char *ttl) {
+static int sql_define_conn(pool *p, const char *conn_name, const char *user,
+    const char *passwd, const char *info, const char *ttl,
+    const char *ssl_cert_file, const char *ssl_key_file,
+    const char *ssl_ca_file, const char *ssl_ca_dir, const char *ssl_ciphers) {
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
 
-  cmd = _sql_make_cmd(p, 5, conn_name, user, passwd, info, ttl);
-  mr = _sql_dispatch(cmd, "sql_defineconnection");
+  /* For backward compatibility of sub-modules' 'defineconn' handler, only
+   * provide the SSL-related parameters if they are present.
+   */
+  if (ssl_cert_file != NULL ||
+      ssl_key_file != NULL ||
+      ssl_ca_file != NULL ||
+      ssl_ca_dir != NULL ||
+      ssl_ciphers != NULL) {
+    cmd = sql_make_cmd(p, 10, conn_name, user, passwd, info, ttl, ssl_cert_file,
+      ssl_key_file, ssl_ca_file, ssl_ca_dir, ssl_ciphers);
+
+  } else {
+    cmd = sql_make_cmd(p, 5, conn_name, user, passwd, info, ttl);
+  }
+
+  mr = sql_dispatch(cmd, "sql_defineconnection");
   if (check_response(mr, 0) < 0) {
     return -1;
   }
@@ -947,8 +977,8 @@ static int sql_define_conn(pool *p, char *conn_name, char *user, char *passwd,
     /* Open a database connection now, so that we have a database connection
      * for the lifetime of the client's connection to the server.
      */
-    cmd = _sql_make_cmd(p, 1, conn_name);
-    mr = _sql_dispatch(cmd, "sql_open");
+    cmd = sql_make_cmd(p, 1, conn_name);
+    mr = sql_dispatch(cmd, "sql_open");
     if (check_response(mr, 0) < 0) {
       return -1;
     }
@@ -976,15 +1006,17 @@ static char *sql_prepare_where(int flags, cmd_rec *cmd, int cnt, ...) {
         *clause != '\0') {
       nclauses++;
 
-      if (flag++)
+      if (flag++) {
         buf = pstrcat(cmd->tmp_pool, buf, " AND ", NULL);
+      }
       buf = pstrcat(cmd->tmp_pool, buf, "(", clause, ")", NULL);
     }
   }
   va_end(dummy);
 
-  if (nclauses == 0)
+  if (nclauses == 0) {
     return NULL;
+  }
 
   if (!(flags & SQL_PREPARE_WHERE_FL_NO_TAGS)) {
     char *curr, *tmp;
@@ -1023,14 +1055,15 @@ static char *sql_prepare_where(int flags, cmd_rec *cmd, int cnt, ...) {
               str = pstrdup(cmd->tmp_pool, "");
             }
 
-            mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2,
+            mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2,
               MOD_SQL_DEF_CONN_NAME, str), "sql_escapestring");
-            if (check_response(mr, 0) < 0)
+            if (check_response(mr, 0) < 0) {
               return NULL;
+            }
 
             /* Make sure we don't write too much data. */
             taglen = strlen(mr->data);
-            if (curr_avail > taglen) {
+            if ((size_t) curr_avail > taglen) {
               sstrcat(curr, mr->data, curr_avail);
               curr += taglen;
               curr_avail -= taglen;
@@ -1055,14 +1088,15 @@ static char *sql_prepare_where(int flags, cmd_rec *cmd, int cnt, ...) {
 
         } else {
           str = resolve_short_tag(cmd, *tmp);
-          mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2,
+          mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2,
             MOD_SQL_DEF_CONN_NAME, str), "sql_escapestring");
-          if (check_response(mr, 0) < 0)
+          if (check_response(mr, 0) < 0) {
             return NULL;
+          }
 
           /* Make sure we don't write too much data. */
           taglen = strlen(mr->data);
-          if (curr_avail > taglen) {
+          if ((size_t) curr_avail > taglen) {
             sstrcat(curr, mr->data, curr_avail);
             curr += taglen;
             curr_avail -= taglen;
@@ -1114,93 +1148,108 @@ static char *sql_prepare_where(int flags, cmd_rec *cmd, int cnt, ...) {
 }
 
 static int _sql_strcmp(const char *s1, const char *s2) {
-  if ((s1 == NULL) || (s2 == NULL))
+  if ((s1 == NULL) || (s2 == NULL)) {
     return 1;
+  }
 
   return strcmp(s1, s2);
 }
 
 static unsigned int _group_gid(const void *val) {
-  if (val == NULL)
+  if (val == NULL) {
     return 0;
+  }
 
   return ((struct group *) val)->gr_gid;
 } 
 
 static unsigned int _group_name(const void *val) {
+  register unsigned int i;
+  size_t namelen;
   char *name;
-  int cnt;
   unsigned int nameval = 0;
 
-  if (val == NULL)
+  if (val == NULL) {
     return 0;
+  }
 
   name = ((struct group *) val)->gr_name;
-
-  if (name == NULL)
+  if (name == NULL) {
     return 0;
+  }
 
-  for (cnt = 0; cnt < strlen(name); cnt++) {
-    nameval += name[cnt];
+  namelen = strlen(name);
+  for (i = 0; i < namelen; i++) {
+    nameval += name[i];
   }
 
   return nameval;
 }
 
 static int _groupcmp(const void *val1, const void *val2) {
-  if ((val1 == NULL) || (val2 == NULL))
+  if ((val1 == NULL) || (val2 == NULL)) {
     return 0;
-  
+  }
+
   /* either the groupnames match or the GIDs match */
   
   if (_sql_strcmp(((struct group *) val1)->gr_name,
-      ((struct group *) val2)->gr_name) == 0)
+      ((struct group *) val2)->gr_name) == 0) {
     return 1;
+  }
 
-  if (((struct group *) val1)->gr_gid == ((struct group *) val2)->gr_gid)
+  if (((struct group *) val1)->gr_gid == ((struct group *) val2)->gr_gid) {
     return 1;
+  }
 
   return 0;
 }
 
 static unsigned int _passwd_uid(const void *val) {
-  if (val == NULL)
+  if (val == NULL) {
     return 0;
+  }
 
   return ((struct passwd *) val)->pw_uid;
 } 
 
 static unsigned int _passwd_name(const void *val) {
+  register unsigned int i;
   char *name;
-  int cnt;
+  size_t namelen;
   unsigned int nameval = 0;
 
-  if (val == NULL)
+  if (val == NULL) {
     return 0;
+  }
 
   name = ((struct passwd *) val)->pw_name;
-
-  if (name == NULL)
+  if (name == NULL) {
     return 0;
+  }
 
-  for (cnt = 0; cnt < strlen(name); cnt++) {
-    nameval += name[cnt];
+  namelen = strlen(name);
+  for (i = 0; i < namelen; i++) {
+    nameval += name[i];
   }
 
   return nameval;
 }
 
 static int _passwdcmp(const void *val1, const void *val2) {
-  if ((val1 == NULL) || (val2 == NULL))
+  if ((val1 == NULL) || (val2 == NULL)) {
      return 0;
-  
+  }
+
   /* either the usernames match or the UIDs match */
   if (_sql_strcmp(((struct passwd *) val1)->pw_name,
-      ((struct passwd *) val2)->pw_name)  == 0)
+      ((struct passwd *) val2)->pw_name) == 0) {
     return 1;
+  }
 
-  if (((struct passwd *) val1)->pw_uid == ((struct passwd *) val2)->pw_uid)
+  if (((struct passwd *) val1)->pw_uid == ((struct passwd *) val2)->pw_uid) {
     return 1;
+  }
 
   return 0;
 }
@@ -1227,7 +1276,7 @@ static void show_group(pool *p, struct group *g) {
   }
 
   sql_log(DEBUG_INFO, "+ grp.gr_name : %s", g->gr_name);
-  sql_log(DEBUG_INFO, "+ grp.gr_gid  : %lu", (unsigned long) g->gr_gid);
+  sql_log(DEBUG_INFO, "+ grp.gr_gid  : %s", pr_gid2str(NULL, g->gr_gid));
   sql_log(DEBUG_INFO, "+ grp.gr_mem  : %s", members);
 
   return;
@@ -1240,8 +1289,8 @@ static void show_passwd(struct passwd *p) {
   }
 
   sql_log(DEBUG_INFO, "+ pwd.pw_name  : %s", p->pw_name);
-  sql_log(DEBUG_INFO, "+ pwd.pw_uid   : %lu", (unsigned long) p->pw_uid);
-  sql_log(DEBUG_INFO, "+ pwd.pw_gid   : %lu", (unsigned long) p->pw_gid);
+  sql_log(DEBUG_INFO, "+ pwd.pw_uid   : %s", pr_uid2str(NULL, p->pw_uid));
+  sql_log(DEBUG_INFO, "+ pwd.pw_gid   : %s", pr_gid2str(NULL, p->pw_gid));
   sql_log(DEBUG_INFO, "+ pwd.pw_dir   : %s", p->pw_dir ?
     p->pw_dir : "(null)");
   sql_log(DEBUG_INFO, "+ pwd.pw_shell : %s", p->pw_shell ?
@@ -1324,9 +1373,9 @@ static struct passwd *_sql_addpasswd(cmd_rec *cmd, char *username,
 static int sql_getuserprimarykey(cmd_rec *cmd, const char *username) {
   sql_data_t *sd = NULL;
   modret_t *mr = NULL;
-  char *key_field = NULL, *key_value = NULL;
+  char *key_field = NULL, *key_value = NULL, *ptr = NULL;
   config_rec *c;
-  void *ptr = NULL, *v = NULL;
+  const void *v = NULL;
  
   v = pr_table_get(session.notes, "sql.user-primary-key", NULL); 
   if (v != NULL) {
@@ -1360,7 +1409,7 @@ static int sql_getuserprimarykey(cmd_rec *cmd, const char *username) {
 
     where = pstrcat(cmd->tmp_pool, cmap.usrfield, " = '", username, "'", NULL);
 
-    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
       cmap.usrtable, key_field, where, "1"), "sql_select");
     if (check_response(mr, 0) < 0) {
       return -1;
@@ -1371,7 +1420,7 @@ static int sql_getuserprimarykey(cmd_rec *cmd, const char *username) {
     }
 
   } else {
-    mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME, ptr,
+    mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME, ptr,
       username));
     if (check_response(mr, 0) < 0) {
       return -1;
@@ -1419,9 +1468,9 @@ static int sql_getuserprimarykey(cmd_rec *cmd, const char *username) {
 static int sql_getgroupprimarykey(cmd_rec *cmd, const char *groupname) {
   sql_data_t *sd = NULL;
   modret_t *mr = NULL;
-  char *key_field = NULL, *key_value = NULL;
+  char *key_field = NULL, *key_value = NULL, *ptr = NULL;
   config_rec *c;
-  void *ptr = NULL, *v = NULL;
+  const void *v = NULL;
  
   v = pr_table_get(session.notes, "sql.group-primary-key", NULL); 
   if (v != NULL) {
@@ -1455,7 +1504,7 @@ static int sql_getgroupprimarykey(cmd_rec *cmd, const char *groupname) {
 
     where = pstrcat(cmd->tmp_pool, cmap.grpfield, " = '", groupname, "'", NULL);
 
-    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
       cmap.grptable, key_field, where, "1"), "sql_select");
     if (check_response(mr, 0) < 0) {
       return -1;
@@ -1466,7 +1515,7 @@ static int sql_getgroupprimarykey(cmd_rec *cmd, const char *groupname) {
     }
 
   } else {
-    mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME, ptr,
+    mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME, ptr,
       groupname));
     if (check_response(mr, 0) < 0) {
       return -1;
@@ -1515,7 +1564,6 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
   sql_data_t *sd = NULL;
   modret_t *mr = NULL;
   struct passwd *pwd = NULL;
-  char uidstr[MOD_SQL_BUFSIZE];
   char *usrwhere, *where;
   char *realname = NULL;
   int i = 0;
@@ -1562,7 +1610,7 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
   if (p->pw_name != NULL) {
     realname = p->pw_name;
 
-    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
       realname), "sql_escapestring");
     if (check_response(mr, 0) < 0) {
       return NULL;
@@ -1586,7 +1634,7 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
       where = sql_prepare_where(SQL_PREPARE_WHERE_FL_NO_TAGS, cmd, 2, usrwhere,
         sql_prepare_where(0, cmd, 1, cmap.userwhere, NULL), NULL);
 
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
         cmap.usrtable, cmap.usrfields, where, "1"), "sql_select");
       if (check_response(mr, 0) < 0) {
         return NULL;
@@ -1597,7 +1645,7 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
       }
 
     } else {
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
         cmap.usercustom, realname ? realname : "NULL"));
 
       if (check_response(mr, 0) < 0) {
@@ -1627,8 +1675,9 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
 
   } else {
     /* Assume we have a UID */
-    memset(uidstr, '\0', sizeof(uidstr));
-    snprintf(uidstr, sizeof(uidstr)-1, "%lu", (unsigned long) p->pw_uid);
+    const char *uidstr;
+
+    uidstr = pr_uid2str(cmd->tmp_pool, p->pw_uid);
     sql_log(DEBUG_WARN, "cache miss for UID '%s'", uidstr);
 
     if (!cmap.usercustombyid) {
@@ -1638,7 +1687,7 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
         where = sql_prepare_where(SQL_PREPARE_WHERE_FL_NO_TAGS, cmd, 2,
           usrwhere, sql_prepare_where(0, cmd, 1, cmap.userwhere, NULL), NULL);
 
-        mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5,
+        mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 5,
           MOD_SQL_DEF_CONN_NAME, cmap.usrtable, cmap.usrfields, where, "1"),
           "sql_select");
         if (check_response(mr, 0) < 0) {
@@ -1662,7 +1711,7 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
     } else {
       array_header *ah = NULL;
 
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
         cmap.usercustombyid, uidstr));
       if (check_response(mr, 0) < 0) {
         return NULL;
@@ -1708,7 +1757,9 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
   uid = cmap.defaultuid;
   if (cmap.uidfield) {
     if (sd->data[i]) {
-      uid = atoi(sd->data[i++]);
+      if (pr_str2uid(sd->data[i++], &uid) < 0) {
+        uid = cmap.defaultuid;
+      }
 
     } else {
       i++;
@@ -1718,7 +1769,9 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
   gid = cmap.defaultgid;
   if (cmap.gidfield) {
     if (sd->data[i]) {
-      gid = atoi(sd->data[i++]);
+      if (pr_str2gid(sd->data[i++], &gid) < 0) {
+        gid = cmap.defaultgid;
+      }
 
     } else {
       i++;
@@ -1739,7 +1792,7 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
   }
 
   if (cmap.shellfield) {
-    if (sd->fnum-1 < i ||
+    if (sd->fnum-1 < (unsigned long) i ||
         !sd->data[i]) {
 
       /* Make sure that, if configured, the shell value is valid, and scream
@@ -1757,16 +1810,18 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
   }
 
   if (uid < cmap.minuseruid) {
-    sql_log(DEBUG_INFO, "user UID %lu below SQLMinUserUID %lu, using "
-      "SQLDefaultUID %lu", (unsigned long) uid, (unsigned long) cmap.minuseruid,
-      (unsigned long) cmap.defaultuid);
+    sql_log(DEBUG_INFO, "user UID %s below SQLMinUserUID %s, using "
+      "SQLDefaultUID %s", pr_uid2str(cmd->tmp_pool, uid),
+      pr_uid2str(cmd->tmp_pool, cmap.minuseruid),
+      pr_uid2str(cmd->tmp_pool, cmap.defaultuid));
     uid = cmap.defaultuid;
   }
 
   if (gid < cmap.minusergid) {
-    sql_log(DEBUG_INFO, "user GID %lu below SQLMinUserGID %lu, using "
-      "SQLDefaultGID %lu", (unsigned long) gid, (unsigned long) cmap.minusergid,
-      (unsigned long) cmap.defaultgid);
+    sql_log(DEBUG_INFO, "user GID %s below SQLMinUserGID %s, using "
+      "SQLDefaultGID %s", pr_gid2str(cmd->tmp_pool, gid),
+      pr_gid2str(cmd->tmp_pool, cmap.minusergid),
+      pr_gid2str(cmd->tmp_pool, cmap.defaultgid));
     gid = cmap.defaultgid;
   }
 
@@ -1843,7 +1898,6 @@ static struct group *sql_getgroup(cmd_rec *cmd, struct group *g) {
   int cnt = 0;
   sql_data_t *sd = NULL;
   char *groupname = NULL;
-  char gidstr[MOD_SQL_BUFSIZE] = {'\0'};
   char **rows = NULL;
   int numrows = 0;
   array_header *ah = NULL;
@@ -1880,8 +1934,10 @@ static struct group *sql_getgroup(cmd_rec *cmd, struct group *g) {
     sql_log(DEBUG_WARN, "cache miss for group '%s'", groupname);
 
   } else {
+    const char *gidstr = NULL;
+
     /* Get groupname from GID */
-    snprintf(gidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) g->gr_gid);
+    gidstr = pr_gid2str(NULL, g->gr_gid);
 
     sql_log(DEBUG_WARN, "cache miss for GID '%s'", gidstr);
 
@@ -1903,7 +1959,7 @@ static struct group *sql_getgroup(cmd_rec *cmd, struct group *g) {
       where = sql_prepare_where(SQL_PREPARE_WHERE_FL_NO_TAGS, cmd, 2, grpwhere,
         sql_prepare_where(0, cmd, 1, cmap.groupwhere, NULL), NULL);
 
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
         cmap.grptable, cmap.grpfield, where, "1"), "sql_select");
       if (check_response(mr, 0) < 0) {
         return NULL;
@@ -1912,7 +1968,7 @@ static struct group *sql_getgroup(cmd_rec *cmd, struct group *g) {
       sd = (sql_data_t *) mr->data;
 
     } else {
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
         cmap.groupcustombyid, gidstr));
       if (check_response(mr, 0) < 0) {
         return NULL;
@@ -1948,7 +2004,7 @@ static struct group *sql_getgroup(cmd_rec *cmd, struct group *g) {
     where = sql_prepare_where(SQL_PREPARE_WHERE_FL_NO_TAGS, cmd, 2, grpwhere,
       sql_prepare_where(0, cmd, 1, cmap.groupwhere, NULL), NULL);
 
-    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
       cmap.grptable, cmap.grpfields, where), "sql_select");
     if (check_response(mr, 0) < 0) {
       return NULL;
@@ -1957,7 +2013,7 @@ static struct group *sql_getgroup(cmd_rec *cmd, struct group *g) {
     sd = (sql_data_t *) mr->data;
 
   } else {
-    mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
       cmap.groupcustombyname, groupname ? groupname : "NULL"));
     if (check_response(mr, 0) < 0) {
       return NULL;
@@ -2051,7 +2107,7 @@ static void _setstats(cmd_rec *cmd, int fstor, int fretr, int bstor,
   where = sql_prepare_where(SQL_PREPARE_WHERE_FL_NO_TAGS, cmd, 2, usrwhere,
     sql_prepare_where(0, cmd, 1, cmap.userwhere, NULL), NULL);
 
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
     cmap.usrtable, query, where), "sql_update");
   (void) check_response(mr, 0);
 }
@@ -2065,7 +2121,7 @@ static int sql_getgroups(cmd_rec *cmd) {
   array_header *gids = NULL, *groups = NULL;
   char *name = cmd->argv[0], *username = NULL;
   int argc, numrows = 0, res = -1;
-  register unsigned int i = 0;
+  register int i = 0;
 
   /* Check for NULL values */
   if (cmd->argv[1]) {
@@ -2107,7 +2163,7 @@ static int sql_getgroups(cmd_rec *cmd) {
     *((char **) push_array(groups)) = pstrdup(permanent_pool, grp->gr_name);
   }
 
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
     name), "sql_escapestring");
   if (check_response(mr, 0) < 0) {
     cmd->argc = argc;
@@ -2143,7 +2199,7 @@ static int sql_getgroups(cmd_rec *cmd) {
     where = sql_prepare_where(SQL_PREPARE_WHERE_FL_NO_TAGS, cmd, 2, grpwhere,
       sql_prepare_where(0, cmd, 1, cmap.groupwhere, NULL), NULL);
   
-    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
       cmap.grptable, cmap.grpfields, where), "sql_select");
     if (check_response(mr, 0) < 0) {
       cmd->argc = argc;
@@ -2158,7 +2214,7 @@ static int sql_getgroups(cmd_rec *cmd) {
     /* The username has been escaped according to the backend database' rules
      * at this point.
      */
-    mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
+    mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
       cmap.groupcustommembers, username));
     if (check_response(mr, 0) < 0) {
       cmd->argc = argc;
@@ -2193,11 +2249,15 @@ static int sql_getgroups(cmd_rec *cmd) {
   numrows = sd->rnum;
 
   for (i = 0; i < numrows; i++) {
+    gid_t gid;
     char *groupname = sd->data[(i * 3)];
-    gid_t gid = (gid_t) atoi(sd->data[(i * 3) +1]);
     char *memberstr = sd->data[(i * 3) + 2], *member = NULL;
     array_header *members = make_array(cmd->tmp_pool, 2, sizeof(char *));
 
+    if (pr_str2gid(sd->data[(i * 3) +1], &gid) < 0) {
+      gid = (gid_t) -1;
+    }
+
     *((gid_t *) push_array(gids)) = gid;
     *((char **) push_array(groups)) = pstrdup(permanent_pool, groupname);
 
@@ -2250,13 +2310,14 @@ MODRET sql_pre_dele(cmd_rec *cmd) {
     /* Briefly cache the size of the file being deleted, so that it can be
      * logged properly using %b.
      */
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(path);
     if (pr_fsio_stat(path, &st) < 0) {
       sql_log(DEBUG_INFO, "%s: unable to stat '%s': %s", cmd->argv[0],
         path, strerror(errno));
     
-    } else
+    } else {
       sql_dele_filesz = st.st_size;
+    }
   }
 
   return PR_DECLINED(cmd);
@@ -2264,7 +2325,7 @@ MODRET sql_pre_dele(cmd_rec *cmd) {
 
 MODRET sql_pre_pass(cmd_rec *cmd) {
   config_rec *c = NULL;
-  char *user = NULL;
+  const char *user = NULL;
 
   if (cmap.engine == 0) {
     return PR_DECLINED(cmd);
@@ -2273,7 +2334,7 @@ MODRET sql_pre_pass(cmd_rec *cmd) {
   sql_log(DEBUG_FUNC, "%s", ">>> sql_pre_pass");
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-  if (user) {
+  if (user != NULL) {
     config_rec *anon_config;
 
     /* Use the looked-up user name to determine whether this is to be
@@ -2300,12 +2361,25 @@ MODRET sql_pre_pass(cmd_rec *cmd) {
 }
 
 MODRET sql_post_pass(cmd_rec *cmd) {
+  int res;
+
   if (cmap.engine == 0) {
     return PR_DECLINED(cmd);
   }
 
-  sql_getuserprimarykey(cmd, session.user);
-  sql_getgroupprimarykey(cmd, session.group);
+  res = sql_getuserprimarykey(cmd, session.user);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 9,
+      "error getting primary lookup key for user '%s': %s", session.user,
+      strerror(errno));
+  }
+
+  res = sql_getgroupprimarykey(cmd, session.group);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 9,
+      "error getting primary lookup key for group '%s': %s", session.group,
+      strerror(errno));
+  }
 
   return PR_DECLINED(cmd);
 }
@@ -2340,65 +2414,126 @@ MODRET sql_post_retr(cmd_rec *cmd) {
 
 static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
   const char *long_tag = NULL;
-  size_t taglen;
+  size_t tag_len;
 
-  if (strncmp(tag, "uid", 4) == 0) {
-    char buf[64];
+  tag_len = strlen(tag);
 
-    memset(buf, '\0', sizeof(buf));
-    snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) session.login_uid);
-    
-    long_tag = pstrdup(cmd->tmp_pool, buf);
+  if (tag_len == 3 &&
+      strncmp(tag, "uid", 4) == 0) {
+    long_tag = pr_uid2str(cmd->tmp_pool, session.login_uid); 
   }
 
   if (long_tag == NULL &&
+      tag_len == 3 &&
       strncmp(tag, "gid", 4) == 0) {
-    char buf[64];
+    long_tag = pr_gid2str(cmd->tmp_pool, session.login_gid); 
+  }
 
-    memset(buf, '\0', sizeof(buf));
-    snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) session.login_gid);
-    
-    long_tag = pstrdup(cmd->tmp_pool, buf);
+  if (long_tag == NULL &&
+      tag_len == 13 &&
+      strncasecmp(tag, "file-modified", 14) == 0) {
+    const char *modified;
+
+    modified = pr_table_get(cmd->notes, "mod_xfer.file-modified", NULL);
+    if (modified != NULL) {
+      long_tag = pstrdup(cmd->tmp_pool, modified);
+
+    } else {
+      long_tag = pstrdup(cmd->tmp_pool, "false");
+    }
   }
 
   if (long_tag == NULL &&
+      tag_len == 11 &&
+      strncasecmp(tag, "file-offset", 12) == 0) {
+    const off_t *offset;
+
+    offset = pr_table_get(cmd->notes, "mod_xfer.file-offset", NULL);
+    if (offset != NULL) {
+      char offset_str[1024];
+      size_t len = 0;
+
+      memset(offset_str, '\0', sizeof(offset_str));
+      len = snprintf(offset_str, sizeof(offset_str)-1, "%" PR_LU,
+        (pr_off_t) *offset);
+      long_tag = pstrndup(cmd->tmp_pool, offset_str, len);
+
+    } else {
+      long_tag = pstrdup(cmd->tmp_pool, "-");
+    }
+  }
+
+  if (long_tag == NULL &&
+      tag_len == 9 &&
+      strncasecmp(tag, "file-size", 10) == 0) {
+    const off_t *file_size;
+
+    file_size = pr_table_get(cmd->notes, "mod_xfer.file-size", NULL);
+    if (file_size != NULL) {
+      char size_str[1024];
+      size_t len = 0;
+
+      memset(size_str, '\0', sizeof(size_str));
+      len = snprintf(size_str, sizeof(size_str)-1, "%" PR_LU,
+        (pr_off_t) *file_size);
+      long_tag = pstrndup(cmd->tmp_pool, size_str, len);
+
+    } else {
+      long_tag = pstrdup(cmd->tmp_pool, "-");
+    }
+  }
+
+  if (long_tag == NULL &&
+      tag_len == 7 &&
       strncasecmp(tag, "iso8601", 8) == 0) {
     char buf[32];
     struct timeval now;
     struct tm *tm;
-    size_t len;
+    size_t fmt_len, len = 0;
     unsigned long millis;
 
     memset(buf, '\0', sizeof(buf));
     gettimeofday(&now, NULL);
+
     tm = pr_localtime(NULL, (const time_t *) &(now.tv_sec));
+    if (tm != NULL) {
+      fmt_len = strftime(buf, sizeof(buf)-1, "%Y-%m-%d %H:%M:%S", tm);
+      len += fmt_len;
 
-    len = strftime(buf, sizeof(buf)-1, "%Y-%m-%d %H:%M:%S", tm);
+      /* Convert microsecs to millisecs. */
+      millis = now.tv_usec / 1000;
 
-    /* Convert microsecs to millisecs. */
-    millis = now.tv_usec / 1000;
+      len += snprintf(buf + fmt_len, sizeof(buf) - fmt_len, ",%03lu", millis);
 
-    snprintf(buf + len, sizeof(buf) - len, ",%03lu", millis);
-    long_tag = pstrdup(cmd->tmp_pool, buf);
+    } else {
+      pr_trace_msg(trace_channel, 1,
+        "error obtaining local timestamp: %s", strerror(errno));
+    }
+
+    long_tag = pstrndup(cmd->tmp_pool, buf, len);
   }
 
   if (long_tag == NULL &&
+      tag_len == 9 &&
       strncmp(tag, "microsecs", 10) == 0) {
     char buf[7];
     struct timeval now;
+    size_t len = 0;
 
     memset(buf, '\0', sizeof(buf));
     gettimeofday(&now, NULL); 
 
-    snprintf(buf, sizeof(buf), "%06lu", (unsigned long) now.tv_usec);
-    long_tag = pstrdup(cmd->tmp_pool, buf);
+    len = snprintf(buf, sizeof(buf), "%06lu", (unsigned long) now.tv_usec);
+    long_tag = pstrndup(cmd->tmp_pool, buf, len);
   }
 
   if (long_tag == NULL &&
+      tag_len == 9 &&
       strncmp(tag, "millisecs", 10) == 0) {
     char buf[4];
     struct timeval now;
     unsigned long millis;
+    size_t len = 0;
 
     memset(buf, '\0', sizeof(buf));
     gettimeofday(&now, NULL);
@@ -2406,19 +2541,18 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
     /* Convert microsecs to millisecs. */
     millis = now.tv_usec / 1000;
 
-    snprintf(buf, sizeof(buf), "%03lu", millis);
-    long_tag = pstrdup(cmd->tmp_pool, buf);
+    len = snprintf(buf, sizeof(buf), "%03lu", millis);
+    long_tag = pstrndup(cmd->tmp_pool, buf, len);
   }
 
   if (long_tag == NULL &&
+      tag_len == 8 &&
       strncmp(tag, "protocol", 9) == 0) {
     long_tag = pr_session_get_protocol(0);
   }
 
-  taglen = strlen(tag);
-
   if (long_tag == NULL &&
-      taglen > 5 &&
+      tag_len > 5 &&
       strncmp(tag, "env:", 4) == 0) {
     char *env;
 
@@ -2427,9 +2561,10 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
   }
 
   if (long_tag == NULL &&
-      taglen > 5 &&
+      tag_len > 5 &&
       strncmp(tag, "note:", 5) == 0) {
-    char *key = NULL, *note = NULL;
+    const char *note = NULL;
+    char *key = NULL;
 
     key = tag + 5;
 
@@ -2443,23 +2578,30 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
   }
 
   if (long_tag == NULL &&
-      taglen > 6 &&
+      tag_len > 6 &&
       strncmp(tag, "time:", 5) == 0) {
     char time_str[128], *fmt;
     time_t now;
-    struct tm *time_info;
+    struct tm *tm;
 
     fmt = pstrdup(cmd->tmp_pool, tag + 5);
-    now = time(NULL);
-    time_info = pr_localtime(NULL, &now);
-
+    time(&now);
     memset(time_str, 0, sizeof(time_str));
-    strftime(time_str, sizeof(time_str), fmt, time_info);
+
+    tm = pr_localtime(NULL, &now);
+    if (tm != NULL) {
+      strftime(time_str, sizeof(time_str), fmt, tm);
+
+    } else {
+      pr_trace_msg(trace_channel, 1,
+        "error obtaining local timestamp: %s", strerror(errno));
+    }
 
     long_tag = pstrdup(cmd->tmp_pool, time_str);
   }
 
   if (long_tag == NULL &&
+      tag_len == 8 &&
       strncmp(tag, "basename", 9) == 0) {
     const char *path = NULL;
 
@@ -2555,6 +2697,19 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
   }
 
   if (long_tag == NULL &&
+      tag_len == 11 &&
+      strncmp(tag, "remote-port", 12) == 0) {
+    char buf[64];
+    const pr_netaddr_t *addr;
+
+    addr = pr_netaddr_get_sess_remote_addr();
+    memset(buf, '\0', sizeof(buf));
+    snprintf(buf, sizeof(buf)-1, "%d", ntohs(pr_netaddr_get_port(addr)));
+    long_tag = pstrdup(cmd->tmp_pool, buf);
+  }
+
+  if (long_tag == NULL &&
+      tag_len == 16 &&
       strncmp(tag, "transfer-failure", 17) == 0) {
 
     /* If the current command is one that incurs a data transfer, then we
@@ -2579,7 +2734,7 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
 
         } else {
           int res;
-          char *resp_code = NULL, *resp_msg = NULL;
+          const char *resp_code = NULL, *resp_msg = NULL;
 
           /* Get the last response code/message.  We use heuristics here to
            * determine when to use "failed" versus "success".
@@ -2622,6 +2777,31 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
   }
 
   if (long_tag == NULL &&
+      tag_len == 18 &&
+      strncmp(tag, "transfer-millisecs", 19) == 0) {
+
+    if (session.xfer.p != NULL &&
+        session.xfer.start_time.tv_sec > 0) {
+      uint64_t start_ms = 0, end_ms = 0;
+      off_t transfer_ms;
+      char transfer_str[256];
+
+      pr_timeval2millis(&(session.xfer.start_time), &start_ms);
+      pr_gettimeofday_millis(&end_ms);
+
+      transfer_ms = end_ms - start_ms;
+      memset(transfer_str, '\0', sizeof(transfer_str));
+      snprintf(transfer_str, sizeof(transfer_str)-1, "%" PR_LU,
+        (pr_off_t) transfer_ms);
+      long_tag = pstrdup(cmd->tmp_pool, transfer_str);
+
+    } else {
+      long_tag = pstrdup(cmd->tmp_pool, "-");
+    }
+  }
+
+  if (long_tag == NULL &&
+      tag_len == 15 &&
       strncmp(tag, "transfer-status", 16) == 0) {
 
     /* If the current command is one that incurs a data transfer, then we
@@ -2644,7 +2824,7 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
 
         if (!(XFER_ABORTED)) {
           int res;
-          char *resp_code = NULL, *resp_msg = NULL;
+          const char *resp_code = NULL, *resp_msg = NULL;
 
           /* Get the last response code/message.  We use heuristics here to
            * determine when to use "failed" versus "success".
@@ -2688,14 +2868,53 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
         /* mod_sftp stashes a note for us in the command notes if the
          * transfer failed.
          */
-        char *status;
+        const char *status;
 
         status = pr_table_get(cmd->notes, "mod_sftp.file-status", NULL);
         if (status == NULL) {
           long_tag = pstrdup(cmd->tmp_pool, "success");
 
         } else {
-          long_tag = pstrdup(cmd->tmp_pool, "failed");
+          long_tag = pstrdup(cmd->tmp_pool, status);
+        }
+      }
+
+    } else {
+      long_tag = pstrdup(cmd->tmp_pool, "-");
+    }
+  }
+
+  if (long_tag == NULL &&
+      tag_len == 13 &&
+      strncmp(tag, "transfer-type", 14) == 0) {
+
+    /* If the current command is one that incurs a data transfer, then we
+     * need to do more work.  If not, it's an easy substitution.
+     */
+    if (session.curr_cmd_id == PR_CMD_APPE_ID ||
+        session.curr_cmd_id == PR_CMD_LIST_ID ||
+        session.curr_cmd_id == PR_CMD_MLSD_ID ||
+        session.curr_cmd_id == PR_CMD_NLST_ID ||
+        session.curr_cmd_id == PR_CMD_RETR_ID ||
+        session.curr_cmd_id == PR_CMD_STOR_ID ||
+        session.curr_cmd_id == PR_CMD_STOU_ID) {
+      const char *proto;
+
+      proto = pr_session_get_protocol(0);
+
+      if (strncmp(proto, "sftp", 5) == 0 ||
+          strncmp(proto, "scp", 4) == 0) {
+
+          /* Always binary. */
+          long_tag = pstrdup(cmd->tmp_pool, "binary");
+
+      } else {
+        if ((session.sf_flags & SF_ASCII) ||
+            (session.sf_flags & SF_ASCII_OVERRIDE)) {
+          long_tag = pstrdup(cmd->tmp_pool, "ASCII");
+
+        } else {
+          long_tag = pstrdup(cmd->tmp_pool, "binary");
         }
       }
 
@@ -2710,61 +2929,67 @@ static const char *resolve_long_tag(cmd_rec *cmd, char *tag) {
 }
 
 static int resolve_numeric_tag(cmd_rec *cmd, char *tag) {
-  int num = -1;
+  int argc, num = -1;
   char *endp = NULL;
 
   num = strtol(tag, &endp, 10);
-  if (*endp != '\0')
+  if (*endp != '\0') {
     return -1;
+  }
 
-  if (num < 0 || (cmd->argc - 3) < num)
+  argc = cmd->argc;
+  if (num < 0 || (argc - 3) < num) {
     return -1;
+  }
 
   return num;
 }
 
 static char *resolve_short_tag(cmd_rec *cmd, char tag) {
   char arg[PR_TUNABLE_PATH_MAX+1], *argp = NULL, *short_tag = NULL;
+  int len = 0;
 
   memset(arg, '\0', sizeof(arg));
 
   switch (tag) {
     case 'A': {
-      char *pass;
+      const char *pass;
 
       argp = arg;
       pass = pr_table_get(session.notes, "mod_auth.anon-passwd", NULL);
-      if (!pass)
+      if (pass == NULL) {
 	pass = "UNKNOWN";
-      
-      sstrncpy(argp, pass, sizeof(arg));
+      }
+ 
+      len = sstrncpy(argp, pass, sizeof(arg));
       break;
     }
 
     case 'a':
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr()),
-        sizeof(arg));
+      len = sstrncpy(argp,
+        pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr()), sizeof(arg));
       break;
 
     case 'b':
       argp = arg;
       if (session.xfer.p) {
-        snprintf(argp, sizeof(arg), "%" PR_LU,
+        len = snprintf(argp, sizeof(arg), "%" PR_LU,
           (pr_off_t) session.xfer.total_bytes);
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_DELE_ID) == 0) {
-        snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) sql_dele_filesz);
+        len = snprintf(argp, sizeof(arg), "%" PR_LU,
+          (pr_off_t) sql_dele_filesz);
 
       } else {
-        sstrncpy(argp, "0", sizeof(arg));
+        len = sstrncpy(argp, "0", sizeof(arg));
       }
       break;
 
     case 'c':
       argp = arg;
-      sstrncpy(argp, session.conn_class ? session.conn_class->cls_name : "-",
-        sizeof(arg));
+      len = sstrncpy(argp,
+        session.conn_class ? session.conn_class->cls_name : "-", sizeof(arg));
       break;
 
     case 'd':
@@ -2781,25 +3006,26 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
           pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
-        char *tmp = strrchr(cmd->arg, '/');
+        char *ptr;
 
-        if (tmp != NULL) {
-          if (tmp != cmd->arg) {
-            sstrncpy(argp, tmp + 1, sizeof(arg));
+        ptr = strrchr(cmd->arg, '/');
+        if (ptr != NULL) {
+          if (ptr != cmd->arg) {
+            len = sstrncpy(argp, ptr + 1, sizeof(arg));
 
-          } else if (*(tmp + 1) != '\0') {
-            sstrncpy(argp, tmp + 1, sizeof(arg));
+          } else if (*(ptr + 1) != '\0') {
+            len = sstrncpy(argp, ptr + 1, sizeof(arg));
 
           } else {
-            sstrncpy(argp, cmd->arg, sizeof(arg));
+            len = sstrncpy(argp, cmd->arg, sizeof(arg));
           }
 
         } else {
-          sstrncpy(argp, cmd->arg, sizeof(arg));
+          len = sstrncpy(argp, cmd->arg, sizeof(arg));
         }
 
       } else {
-        sstrncpy(argp, pr_fs_getvwd(), sizeof(arg));
+        len = sstrncpy(argp, pr_fs_getvwd(), sizeof(arg));
       }
       break;
 
@@ -2815,7 +3041,7 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
           pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
-        sstrncpy(argp, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE),
+        len = sstrncpy(argp, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE),
           sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
@@ -2831,32 +3057,36 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
 
         if (session.chroot_path) {
           /* Chrooted session. */
-          sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
+          len = sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
             pdircat(cmd->tmp_pool, session.chroot_path, pr_fs_getvwd(), NULL) :
             session.chroot_path, sizeof(arg));
 
         } else {
-
           /* Non-chrooted session. */
-          sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
+          len = sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
         }
 
       } else {
-        sstrncpy(argp, "", sizeof(arg));
+        len = sstrncpy(argp, "", sizeof(arg));
       }
       break;
 
     case 'E': {
-      const char *reason_str;
-      char *details = NULL;
+      const char *details = NULL, *reason_str;
 
       argp = arg;
 
       reason_str = pr_session_get_disconnect_reason(&details);
-      sstrncpy(argp, reason_str, sizeof(arg));
+      len = sstrncpy(argp, reason_str, sizeof(arg));
       if (details != NULL) {
+        size_t details_len;
+
+        details_len = strlen(details);
+
         sstrcat(argp, ": ", sizeof(arg));
         sstrcat(argp, details, sizeof(arg));
+
+        len += details_len + 2;
       }
 
       break;
@@ -2866,32 +3096,34 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       argp = arg;
 
       if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
-        sstrncpy(argp, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE),
+        len = sstrncpy(argp, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE),
           sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0) {
-        char *path;
+        const char *path;
 
         path = pr_table_get(cmd->notes, "mod_xfer.retr-path", NULL);
-        sstrncpy(arg, dir_abs_path(cmd->tmp_pool, path, TRUE), sizeof(arg));
+        len = sstrncpy(arg, dir_abs_path(cmd->tmp_pool, path, TRUE),
+          sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0) {
-        char *path;
+        const char *path;
 
         path = pr_table_get(cmd->notes, "mod_xfer.store-path", NULL);
-        sstrncpy(arg, dir_abs_path(cmd->tmp_pool, path, TRUE), sizeof(arg));
+        len = sstrncpy(arg, dir_abs_path(cmd->tmp_pool, path, TRUE),
+          sizeof(arg));
 
       } else if (session.xfer.p &&
                  session.xfer.path) {
-        sstrncpy(argp, dir_abs_path(cmd->tmp_pool, session.xfer.path, TRUE),
-          sizeof(arg));
+        len = sstrncpy(argp,
+          dir_abs_path(cmd->tmp_pool, session.xfer.path, TRUE), sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_CDUP_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_PWD_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_XPWD_ID) == 0) {
-        sstrncpy(argp, dir_abs_path(cmd->tmp_pool, pr_fs_getcwd(), TRUE),
+        len = sstrncpy(argp, dir_abs_path(cmd->tmp_pool, pr_fs_getcwd(), TRUE),
           sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
@@ -2906,13 +3138,13 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
          */
         if (session.chroot_path) {
           /* Chrooted session. */
-          sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
+          len = sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
             pdircat(cmd->tmp_pool, session.chroot_path, pr_fs_getvwd(), NULL) :
             session.chroot_path, sizeof(arg));
 
         } else {
           /* Non-chrooted session. */
-          sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
+          len = sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
         }
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_SITE_ID) == 0 &&
@@ -2927,7 +3159,8 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
             NULL);
         }
 
-        sstrncpy(argp, dir_abs_path(cmd->tmp_pool, tmp, TRUE), sizeof(arg));
+        len = sstrncpy(argp, dir_abs_path(cmd->tmp_pool, tmp, TRUE),
+          sizeof(arg));
 
       } else {
 
@@ -2945,17 +3178,17 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
             pr_cmd_cmp(cmd, PR_CMD_RMD_ID) == 0 ||
             pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
             pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
-          sstrncpy(arg, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE),
+          len = sstrncpy(arg, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE),
             sizeof(arg));
 
         } else if (pr_cmd_cmp(cmd, PR_CMD_MFMT_ID) == 0) {
           /* MFMT has, as its filename, the second argument. */
-          sstrncpy(arg, dir_abs_path(cmd->tmp_pool, cmd->argv[2], TRUE),
+          len = sstrncpy(arg, dir_abs_path(cmd->tmp_pool, cmd->argv[2], TRUE),
             sizeof(arg));
 
         } else {
           /* All other situations get a "-".  */
-          sstrncpy(argp, "-", sizeof(arg));
+          len = sstrncpy(argp, "-", sizeof(arg));
         }
       }
       break;
@@ -2968,11 +3201,11 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
 
         path = dir_best_path(cmd->tmp_pool,
           pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
-        sstrncpy(arg, path, sizeof(arg));
+        len = sstrncpy(arg, path, sizeof(arg));
 
       } else if (session.xfer.p &&
                  session.xfer.path) {
-        sstrncpy(argp, session.xfer.path, sizeof(arg));
+        len = sstrncpy(argp, session.xfer.path, sizeof(arg));
 
       } else {
         /* Some commands (i.e. DELE, MKD, RMD, XMKD, and XRMD) have associated
@@ -2989,10 +3222,10 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
 
           path = dir_best_path(cmd->tmp_pool,
             pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
-          sstrncpy(arg, path, sizeof(arg));
+          len = sstrncpy(arg, path, sizeof(arg));
 
         } else {
-          sstrncpy(argp, "-", sizeof(arg));
+          len = sstrncpy(argp, "-", sizeof(arg));
         }
       }
       break;
@@ -3001,10 +3234,10 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       argp = arg;
 
       if (session.group != NULL) {
-        sstrncpy(argp, session.group, sizeof(arg));
+        len = sstrncpy(argp, session.group, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       break;
@@ -3012,82 +3245,108 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
 
     case 'H':
       argp = arg;
-      sstrncpy(argp, cmd->server->ServerAddress, sizeof(arg));
+      len = sstrncpy(argp, cmd->server->ServerAddress, sizeof(arg));
       break;
 
     case 'h':
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_sess_remote_name(), sizeof(arg));
+      len = sstrncpy(argp, pr_netaddr_get_sess_remote_name(), sizeof(arg));
       break;
 
     case 'I':
       argp = arg;
-      snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) session.total_raw_in);
+      len = snprintf(argp, sizeof(arg), "%" PR_LU,
+        (pr_off_t) session.total_raw_in);
       break;
 
     case 'J':
       argp = arg;
       if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
           session.hide_password) {
-        sstrncpy(argp, "(hidden)", sizeof(arg));
+        len = sstrncpy(argp, "(hidden)", sizeof(arg));
 
       } else {
-        sstrncpy(argp, cmd->arg, sizeof(arg));
+        len = sstrncpy(argp, cmd->arg, sizeof(arg));
       }
       break;
 
     case 'L':
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_ipstr(pr_netaddr_get_sess_local_addr()),
-        sizeof(arg));
+      len = sstrncpy(argp,
+        pr_netaddr_get_ipstr(pr_netaddr_get_sess_local_addr()), sizeof(arg));
       break;
 
     case 'l': {
-      char *rfc1413_ident;
+      const char *rfc1413_ident;
 
       argp = arg;
       rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident",
         NULL);
-      if (rfc1413_ident == NULL)
+      if (rfc1413_ident == NULL) {
         rfc1413_ident = "UNKNOWN";
+      }
 
-      sstrncpy(argp, rfc1413_ident, sizeof(arg));
+      len = sstrncpy(argp, rfc1413_ident, sizeof(arg));
       break;
     }
 
     case 'm':
       argp = arg;
-      sstrncpy(argp, cmd->argv[0], sizeof(arg));
+      len = sstrncpy(argp, cmd->argv[0], sizeof(arg));
       break;
 
     case 'O':
       argp = arg;
-      snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) session.total_raw_out);
+      len = snprintf(argp, sizeof(arg), "%" PR_LU,
+        (pr_off_t) session.total_raw_out);
       break;
 
     case 'P':
       argp = arg;
-      snprintf(argp, sizeof(arg), "%lu", (unsigned long) getpid());
+      len = snprintf(argp, sizeof(arg), "%lu", (unsigned long) session.pid);
       break;
 
     case 'p': 
       argp = arg;
-      snprintf(argp, sizeof(arg), "%d", main_server->ServerPort);
+      len = snprintf(argp, sizeof(arg), "%d", main_server->ServerPort);
       break;
 
     case 'r':
       argp = arg;
       if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
           session.hide_password) {
-        sstrncpy(argp, C_PASS " (hidden)", sizeof(arg));
+        len = sstrncpy(argp, C_PASS " (hidden)", sizeof(arg));
+
+      } else {
+        len = sstrncpy(argp, pr_cmd_get_displayable_str(cmd, NULL),
+          sizeof(arg));
+      }
+      break;
+
+    case 'R': {
+      const uint64_t *start_ms = NULL;
+
+      argp = arg;
+
+      start_ms = pr_table_get(cmd->notes, "start_ms", NULL);
+      if (start_ms != NULL) {
+        uint64_t end_ms = 0;
+        off_t response_ms;
+
+        pr_gettimeofday_millis(&end_ms);
+
+        response_ms = end_ms - *start_ms;
+        len = snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) response_ms);
 
       } else {
-        sstrncpy(argp, pr_cmd_get_displayable_str(cmd, NULL), sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
+
       break;
+    }
 
     case 's': {
-      char *resp_code = NULL;
+      const char *resp_code = NULL;
       int res;
 
       argp = arg;
@@ -3095,17 +3354,17 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       res = pr_response_get_last(cmd->tmp_pool, &resp_code, NULL);
       if (res == 0 &&
           resp_code != NULL) {
-        sstrncpy(argp, resp_code, sizeof(arg));
+        len = sstrncpy(argp, resp_code, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       break;
     }
 
     case 'S': {
-      char *resp_msg = NULL;
+      const char *resp_msg = NULL;
       int res;
 
       argp = arg;
@@ -3113,10 +3372,10 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       res = pr_response_get_last(cmd->tmp_pool, NULL, &resp_msg);
       if (res == 0 &&
           resp_msg != NULL) {
-        sstrncpy(argp, resp_msg, sizeof(arg));
+        len = sstrncpy(argp, resp_msg, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       break;
@@ -3126,31 +3385,22 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       argp = arg;
       if (session.xfer.p &&
           session.xfer.start_time.tv_sec > 0) {
-        struct timeval end_time;
-      
-        gettimeofday(&end_time, NULL);
-        end_time.tv_sec -= session.xfer.start_time.tv_sec;
+        uint64_t start_ms = 0 , end_ms = 0;
+        float transfer_secs = 0.0;
 
-        if (end_time.tv_usec >= session.xfer.start_time.tv_usec) {
-          end_time.tv_usec -= session.xfer.start_time.tv_usec;
+        pr_timeval2millis(&(session.xfer.start_time), &start_ms);
+        pr_gettimeofday_millis(&end_ms);
 
-        } else {
-          end_time.tv_usec = 1000000L - (session.xfer.start_time.tv_usec -
-            end_time.tv_usec);
-          end_time.tv_sec--;
-        }
-      
-        snprintf(argp, sizeof(arg), "%lu.%03lu",
-          (unsigned long) end_time.tv_sec,
-          (unsigned long) (end_time.tv_usec / 1000));
+        transfer_secs = (end_ms - start_ms) / 1000.0;
+        len = snprintf(argp, sizeof(arg), "%0.3f", transfer_secs);
 
       } else {
-        sstrncpy(argp, "0.0", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
       break;
 
     case 'U': {
-      char *login_user;
+      const char *login_user;
 
       argp = arg;
 
@@ -3159,7 +3409,7 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
         login_user = "-";
       }
 
-      sstrncpy(argp, login_user, sizeof(arg));
+      len = sstrncpy(argp, login_user, sizeof(arg));
       break;
     }
 
@@ -3167,10 +3417,10 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       argp = arg;
 
       if (session.user != NULL) {
-        sstrncpy(argp, session.user, sizeof(arg));
+        len = sstrncpy(argp, session.user, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       break;
@@ -3178,17 +3428,17 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
 
     case 'V':
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_dnsstr(pr_netaddr_get_sess_local_addr()),
-        sizeof(arg));
+      len = sstrncpy(argp,
+        pr_netaddr_get_dnsstr(pr_netaddr_get_sess_local_addr()), sizeof(arg));
       break;
 
     case 'v':
       argp = arg;
-      sstrncpy(argp, main_server->ServerName, sizeof(arg));
+      len = sstrncpy(argp, main_server->ServerName, sizeof(arg));
       break;
 
     case 'w': {
-      char *rnfr_path = "-";
+      const char *rnfr_path = "-";
 
       if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
         rnfr_path = pr_table_get(session.notes, "mod_core.rnfr-path", NULL);
@@ -3202,7 +3452,7 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       }
  
       argp = arg; 
-      sstrncpy(argp, rnfr_path, sizeof(arg));
+      len = sstrncpy(argp, rnfr_path, sizeof(arg));
       break;
     }
 
@@ -3215,7 +3465,13 @@ static char *resolve_short_tag(cmd_rec *cmd, char tag) {
       break;
   }
 
-  short_tag = pstrdup(cmd->tmp_pool, argp);
+  if (len > 0) {
+    short_tag = pstrndup(cmd->tmp_pool, argp, len);
+
+  } else {
+    short_tag = pstrdup(cmd->tmp_pool, argp);
+  }
+
   pr_trace_msg(trace_channel, 15, "returning short tag '%s' for tag '%%%c'",
     short_tag, tag);
 
@@ -3251,7 +3507,7 @@ static modret_t *process_named_query(cmd_rec *cmd, char *name, int flags) {
   query = pstrcat(cmd->tmp_pool, "SQLNamedQuery_", name, NULL);
 
   c = find_config(main_server->conf, CONF_PARAM, query, FALSE);
-  if (c) {
+  if (c != NULL) {
     size_t arglen, outs_remain = sizeof(outs)-1;
 
     conn_name = get_query_named_conn(c);
@@ -3267,16 +3523,18 @@ static modret_t *process_named_query(cmd_rec *cmd, char *name, int flags) {
       if (*tmp == '%') {
         if (*(++tmp) == '{') {
           char *tmp_query = NULL;
-	  
-          if (*tmp != '\0')
+
+          if (*tmp != '\0') {
             tmp_query = ++tmp;
+          }
 
           /* Find the full tag to use */
-          while (*tmp && *tmp != '}')
+          while (*tmp && *tmp != '}') {
             tmp++;
+          }
 
           tag = pstrndup(cmd->tmp_pool, tmp_query, (tmp - tmp_query));
-          if (tag) {
+          if (tag != NULL) {
             register unsigned int i;
             size_t taglen = strlen(tag);
             unsigned char is_numeric_tag = TRUE;
@@ -3306,7 +3564,7 @@ static modret_t *process_named_query(cmd_rec *cmd, char *name, int flags) {
                   "malformed reference %{?} in query");
               }
 
-              mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, conn_name,
+              mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, conn_name,
                 argp), "sql_escapestring");
               if (check_response(mr, flags) < 0) {
                 set_named_conn_backend(NULL);
@@ -3325,7 +3583,7 @@ static modret_t *process_named_query(cmd_rec *cmd, char *name, int flags) {
         } else {
           argp = resolve_short_tag(cmd, *tmp);
 
-          mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, conn_name,
+          mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, conn_name,
             argp), "sql_escapestring");
           if (check_response(mr, flags) < 0) {
             set_named_conn_backend(NULL);
@@ -3346,13 +3604,14 @@ static modret_t *process_named_query(cmd_rec *cmd, char *name, int flags) {
            * space.
            */
           sql_log(DEBUG_FUNC, "insufficient statement buffer size "
-            "(%lu of %lu bytes) for tag (%Lu bytes) when processing named "
+            "(%lu of %lu bytes) for tag (%lu bytes) when processing named "
             "query '%s', ignoring tag", (unsigned long) outs_remain,
             (unsigned long) SQL_MAX_STMT_LEN, (unsigned long) arglen, name);
         }
 
-        if (*tmp != '\0')
+        if (*tmp != '\0') {
           tmp++;
+        }
 
       } else {
         if (outs_remain > 0) {
@@ -3370,33 +3629,57 @@ static modret_t *process_named_query(cmd_rec *cmd, char *name, int flags) {
           break;
         }
 
-        if (*tmp != '\0')
+        if (*tmp != '\0') {
           tmp++;
+        }
       }
     }
-      
+
     *outsp = '\0';
 
     /* Construct our return data based on the type of query */
     if (strcasecmp(c->argv[0], SQL_UPDATE_C) == 0) {
       query = pstrcat(cmd->tmp_pool, c->argv[2], " SET ", outs, NULL);
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, conn_name, query), 
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, conn_name, query), 
         "sql_update");
 
     } else if (strcasecmp(c->argv[0], SQL_INSERT_C) == 0) {
       query = pstrcat(cmd->tmp_pool, "INTO ", c->argv[2], " VALUES (",
         outs, ")", NULL);
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, conn_name, query),
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, conn_name, query),
         "sql_insert");
 
     } else if (strcasecmp(c->argv[0], SQL_FREEFORM_C) == 0) {
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, conn_name, outs),
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, conn_name, outs),
         "sql_query");
 
     } else if (strcasecmp(c->argv[0], SQL_SELECT_C) == 0) {
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, conn_name, outs),
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, conn_name, outs),
         "sql_select");
 
+      if (MODRET_ISHANDLED(mr) &&
+          MODRET_HASDATA(mr) &&
+          pr_trace_get_level(trace_channel) >= 9) {
+        register unsigned long i, idx;
+        sql_data_t *sd;
+
+        sd = mr->data;
+
+        pr_trace_msg(trace_channel, 9, "SQLNamedQuery %s results:", name);
+        pr_trace_msg(trace_channel, 9, "  row count: %lu", sd->rnum);
+        pr_trace_msg(trace_channel, 9, "  col count: %lu", sd->fnum);
+
+        for (i = 0, idx = 0; i < sd->rnum; i++) {
+          register unsigned long j;
+
+          pr_trace_msg(trace_channel, 9, "    row #%lu:", i+1);
+          for (j = 0; j < sd->fnum; j++) {
+            pr_trace_msg(trace_channel, 9, "      col #%lu: '%s'", j+1,
+              sd->data[idx++]);
+          }
+        }
+      }
+
     } else {
       mr = PR_ERROR_MSG(cmd, MOD_SQL_VERSION, "unknown NamedQuery type");
     }
@@ -3458,7 +3741,7 @@ static int eventlog_master(const char *event_name) {
    * fake/unknown name (i.e. cmd->argv[0], cmd->cmd_id), so that it does
    * not run afoul of other logging variables.
    */
-  cmd = _sql_make_cmd(session.pool, 1, "EVENT");
+  cmd = sql_make_cmd(session.pool, 1, "EVENT");
  
   name = pstrcat(cmd->tmp_pool, "SQLLog_Event_", event_name, NULL);
 
@@ -3748,7 +4031,7 @@ MODRET info_master(cmd_rec *cmd) {
            * space.
            */
           sql_log(DEBUG_FUNC, "insufficient statement buffer size "
-            "(%lu of %lu bytes) for tag (%Lu bytes) when processing "
+            "(%lu of %lu bytes) for tag (%lu bytes) when processing "
             "SQLShowInfo query '%s', ignoring tag",
             (unsigned long) outs_remain, (unsigned long) SQL_MAX_STMT_LEN,
             (unsigned long) arglen, name);
@@ -3888,7 +4171,7 @@ MODRET info_master(cmd_rec *cmd) {
            * space.
            */
           sql_log(DEBUG_FUNC, "insufficient statement buffer size "
-            "(%lu of %lu bytes) for tag (%Lu bytes) when processing "
+            "(%lu of %lu bytes) for tag (%lu bytes) when processing "
             "SQLShowInfo query '%s', ignoring tag",
             (unsigned long) outs_remain, (unsigned long) SQL_MAX_STMT_LEN,
             (unsigned long) arglen, name);
@@ -3970,7 +4253,7 @@ MODRET errinfo_master(cmd_rec *cmd) {
     outsp = outs;
 
     pr_trace_msg(trace_channel, 15, "processing SQLShowInfo ERR_%s '%s'",
-      cmd->argv[0], cmd->argv[1]);
+      (char *) cmd->argv[0], (char *) cmd->argv[1]);
 
     for (tmp = c->argv[1]; *tmp; ) {
       pr_signals_handle();
@@ -4008,7 +4291,7 @@ MODRET errinfo_master(cmd_rec *cmd) {
 
               pr_trace_msg(trace_channel, 13,
                 "SQLShowInfo ERR_%s query '%s' returned row count %lu",
-                cmd->argv[0], query, sd->rnum);
+                (char *) cmd->argv[0], query, sd->rnum);
 
               if (sd->rnum == 0 ||
                   sd->data[0] == NULL) {
@@ -4055,7 +4338,7 @@ MODRET errinfo_master(cmd_rec *cmd) {
            * space.
            */
           sql_log(DEBUG_FUNC, "insufficient statement buffer size "
-            "(%lu of %lu bytes) for tag (%Lu bytes) when processing "
+            "(%lu of %lu bytes) for tag (%lu bytes) when processing "
             "SQLShowInfo query '%s', ignoring tag",
             (unsigned long) outs_remain, (unsigned long) SQL_MAX_STMT_LEN,
             (unsigned long) arglen, name);
@@ -4099,14 +4382,14 @@ MODRET errinfo_master(cmd_rec *cmd) {
           *resp_code == '5') {
         pr_trace_msg(trace_channel, 15,
           "adding error response code %s, msg '%s' for SQLShowInfo ERR_%s",
-          resp_code, outs, cmd->argv[0]);
+          resp_code, outs, (char *) cmd->argv[0]);
 
         pr_response_add_err(resp_code, "%s", outs);
 
       } else {
         pr_trace_msg(trace_channel, 15,
           "adding response code %s, msg '%s' for SQLShowInfo ERR_%s", resp_code,
-          outs, cmd->argv[0]);
+          outs, (char *) cmd->argv[0]);
 
         pr_response_add(resp_code, "%s", outs);
       }
@@ -4212,7 +4495,7 @@ MODRET errinfo_master(cmd_rec *cmd) {
            * space.
            */
           sql_log(DEBUG_FUNC, "insufficient statement buffer size "
-            "(%lu of %lu bytes) for tag (%Lu bytes) when processing "
+            "(%lu of %lu bytes) for tag (%lu bytes) when processing "
             "SQLShowInfo query '%s', ignoring tag",
             (unsigned long) outs_remain, (unsigned long) SQL_MAX_STMT_LEN,
             (unsigned long) arglen, name);
@@ -4282,7 +4565,7 @@ MODRET sql_cleanup(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_cleanup");
 
-  res = _sql_dispatch(cmd, "sql_cleanup");
+  res = sql_dispatch(cmd, "sql_cleanup");
   if (check_response(res, 0) < 0) {
     sql_log(DEBUG_FUNC, "%s", "<<< sql_cleanup");
     return res;
@@ -4296,7 +4579,7 @@ MODRET sql_closeconn(cmd_rec *cmd) {
   modret_t *res;
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_closeconn");
-  res = _sql_dispatch(cmd, "sql_close");
+  res = sql_dispatch(cmd, "sql_close");
   sql_log(DEBUG_FUNC, "%s", "<<< sql_closeconn");
 
   return res;
@@ -4306,7 +4589,7 @@ MODRET sql_defineconn(cmd_rec *cmd) {
   modret_t *res;
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_defineconn");
-  res = _sql_dispatch(cmd, "sql_defineconnection");
+  res = sql_dispatch(cmd, "sql_defineconnection");
   sql_log(DEBUG_FUNC, "%s", "<<< sql_defineconn");
 
   return res;
@@ -4334,7 +4617,7 @@ MODRET sql_openconn(cmd_rec *cmd) {
   modret_t *res;
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_openconn");
-  res = _sql_dispatch(cmd, "sql_open");
+  res = sql_dispatch(cmd, "sql_open");
   sql_log(DEBUG_FUNC, "%s", "<<< sql_openconn");
 
   return res;
@@ -4344,7 +4627,7 @@ MODRET sql_prepare(cmd_rec *cmd) {
   modret_t *res;
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_prepare");
-  res = _sql_dispatch(cmd, "sql_prepare");
+  res = sql_dispatch(cmd, "sql_prepare");
   sql_log(DEBUG_FUNC, "%s", "<<< sql_prepare");
 
   return res;
@@ -4354,7 +4637,7 @@ MODRET sql_select(cmd_rec *cmd) {
   modret_t *res;
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_select");
-  res = _sql_dispatch(cmd, "sql_select");
+  res = sql_dispatch(cmd, "sql_select");
   sql_log(DEBUG_FUNC, "%s", "<<< sql_select");
 
   return res;
@@ -4470,7 +4753,7 @@ MODRET sql_escapestr(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_escapestr");
 
-  mr =_sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+  mr =sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
     cmd->argv[0]), "sql_escapestring");
   if (check_response(mr, 0) < 0) {
     sql_log(DEBUG_FUNC, "%s", "<<< sql_escapestr");
@@ -4491,7 +4774,8 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
   sql_data_t *sd = NULL;
   modret_t *mr = NULL;
   char *where = NULL;
-  int i = 0, cnt = 0;
+  int i = 0;
+  unsigned long cnt = 0;
 
   char *username = NULL;
   char *password = NULL;
@@ -4522,7 +4806,7 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
     if (!cmap.usercustomusersetfast) {
       where = sql_prepare_where(0, cmd, 1, cmap.userwhere, NULL);
 
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
         cmap.usrtable, cmap.usrfields, where), "sql_select");
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4531,7 +4815,7 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
       sd = (sql_data_t *) mr->data;
 
     } else {
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
         cmap.usercustomusersetfast));
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4566,7 +4850,10 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
         uid = cmap.defaultuid;
         if (cmap.uidfield) {
           if (sd->data[i]) {
-            uid = atoi(sd->data[i++]);
+            if (pr_str2uid(sd->data[i++], &uid) < 0) {
+              uid = cmap.defaultuid;
+            }
+
           } else {
             i++;
           }
@@ -4575,7 +4862,10 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
         gid = cmap.defaultgid;
         if (cmap.gidfield) {
           if (sd->data[i]) {
-            gid = atoi(sd->data[i++]);
+            if (pr_str2gid(sd->data[i++], &gid) < 0) {
+              gid = cmap.defaultgid;
+            }
+
           } else {
             i++;
           }
@@ -4601,16 +4891,18 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
         }
 
         if (uid < cmap.minuseruid) {
-          sql_log(DEBUG_INFO, "user UID %lu below SQLMinUserUID %lu, using "
-            "SQLDefaultUID %lu", (unsigned long) uid,
-            (unsigned long) cmap.minuseruid, (unsigned long) cmap.defaultuid);
+          sql_log(DEBUG_INFO, "user UID %s below SQLMinUserUID %s, using "
+            "SQLDefaultUID %s", pr_uid2str(cmd->tmp_pool, uid),
+            pr_uid2str(cmd->tmp_pool, cmap.minuseruid),
+            pr_uid2str(cmd->tmp_pool, cmap.defaultuid));
           uid = cmap.defaultuid;
         }
       
         if (gid < cmap.minusergid) {
-          sql_log(DEBUG_INFO, "user GID %lu below SQLMinUserGID %lu, using "
-            "SQLDefaultGID %lu", (unsigned long) gid,
-            (unsigned long) cmap.minusergid, (unsigned long) cmap.defaultgid);
+          sql_log(DEBUG_INFO, "user GID %s below SQLMinUserGID %s, using "
+            "SQLDefaultGID %s", pr_gid2str(cmd->tmp_pool, gid),
+            pr_gid2str(cmd->tmp_pool, cmap.minusergid),
+            pr_gid2str(cmd->tmp_pool, cmap.defaultgid));
           gid = cmap.defaultgid;
         }
 
@@ -4624,7 +4916,7 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
     if (!cmap.usercustomuserset) {
       where = sql_prepare_where(0, cmd, 1, cmap.userwhere, NULL);
 
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
         cmap.usrtable, cmap.usrfield, where), "sql_select");
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4633,7 +4925,7 @@ MODRET cmd_setpwent(cmd_rec *cmd) {
       sd = (sql_data_t *) mr->data;
 
     } else {
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
         cmap.usercustomuserset));
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4726,7 +5018,7 @@ MODRET cmd_endpwent(cmd_rec *cmd) {
 MODRET cmd_setgrent(cmd_rec *cmd) {
   modret_t *mr = NULL;
   sql_data_t *sd = NULL;
-  int cnt = 0;
+  unsigned long cnt = 0;
   struct group lgr;
   gid_t gid;
   char *groupname = NULL;
@@ -4737,8 +5029,9 @@ MODRET cmd_setgrent(cmd_rec *cmd) {
   char *member = NULL;
 
   if (!SQL_GROUPSET ||
-      !(cmap.engine & SQL_ENGINE_FL_AUTH))
+      !(cmap.engine & SQL_ENGINE_FL_AUTH)) {
     return PR_DECLINED(cmd);
+  }
 
   sql_log(DEBUG_FUNC, "%s", ">>> cmd_setgrent");
 
@@ -4755,7 +5048,7 @@ MODRET cmd_setgrent(cmd_rec *cmd) {
     if (!cmap.groupcustomgroupsetfast) {
       where = sql_prepare_where(0, cmd, 1, cmap.groupwhere, NULL);
 
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 5, MOD_SQL_DEF_CONN_NAME,
         cmap.grptable, cmap.grpfields, where, "1"), "sql_select");
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4764,7 +5057,7 @@ MODRET cmd_setgrent(cmd_rec *cmd) {
       sd = (sql_data_t *) mr->data;
    
     } else {
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
         cmap.groupcustomgroupsetfast));
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4790,8 +5083,9 @@ MODRET cmd_setgrent(cmd_rec *cmd) {
     for (cnt = 0; cnt < sd->rnum; cnt ++) {
       /* if the groupname is NULL for whatever reason, skip the row */
       groupname = sd->data[cnt * 3];
-      if (groupname == NULL)
+      if (groupname == NULL) {
         continue;
+      }
 
       gid = (gid_t) atol(sd->data[(cnt * 3) + 1]);
       grp_mem = sd->data[(cnt * 3) + 2];
@@ -4816,7 +5110,7 @@ MODRET cmd_setgrent(cmd_rec *cmd) {
     if (!cmap.groupcustomgroupset) {
       where = sql_prepare_where(0, cmd, 1, cmap.groupwhere, NULL);
  
-      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 6, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 6, MOD_SQL_DEF_CONN_NAME,
         cmap.grptable, cmap.grpfield, where, NULL, "DISTINCT"), "sql_select");
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -4825,7 +5119,7 @@ MODRET cmd_setgrent(cmd_rec *cmd) {
       sd = (sql_data_t *) mr->data;
 
     } else {
-      mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+      mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
         cmap.groupcustomgroupset));
       if (check_response(mr, 0) < 0) {
         return mr;
@@ -5022,15 +5316,16 @@ MODRET cmd_auth(cmd_rec *cmd) {
   modret_t *mr = NULL;
 
   if (!SQL_USERS ||
-      !(cmap.engine & SQL_ENGINE_FL_AUTH))
+      !(cmap.engine & SQL_ENGINE_FL_AUTH)) {
     return PR_DECLINED(cmd);
+  }
 
   sql_log(DEBUG_FUNC, "%s", ">>> cmd_auth");
 
   user = cmd->argv[0];
 
   /* escape our username */
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 2, MOD_SQL_DEF_CONN_NAME,
     user), "sql_escapestring");
   if (check_response(mr, 0) < 0) {
     return mr;
@@ -5047,11 +5342,10 @@ MODRET cmd_auth(cmd_rec *cmd) {
     sql_log(DEBUG_FUNC, "%s", "<<< cmd_auth");
     session.auth_mech = "mod_sql.c";
     return PR_HANDLED(cmd);
-
-  } else {
-    sql_log(DEBUG_FUNC, "%s", "<<< cmd_auth");
-    return PR_DECLINED(cmd);
   }
+
+  sql_log(DEBUG_FUNC, "%s", "<<< cmd_auth");
+  return PR_DECLINED(cmd);
 }
 
 MODRET cmd_check(cmd_rec *cmd) {
@@ -5104,7 +5398,7 @@ MODRET cmd_check(cmd_rec *cmd) {
 
       } else {
         if (MODRET_HASMSG(mr)) {
-          char *err_msg;
+          const char *err_msg;
 
           err_msg = MODRET_ERRMSG(mr);
           sql_log(DEBUG_AUTH, "'%s' SQLAuthType handler reports failure: %s",
@@ -5146,11 +5440,11 @@ MODRET cmd_uid2name(cmd_rec *cmd) {
   char *uid_name = NULL;
   struct passwd *pw;
   struct passwd lpw;
-  char uidstr[MOD_SQL_BUFSIZE] = {'\0'};
 
   if (!SQL_USERS ||
-      !(cmap.engine & SQL_ENGINE_FL_AUTH))
+      !(cmap.engine & SQL_ENGINE_FL_AUTH)) {
     return PR_DECLINED(cmd);
+  }
 
   sql_log(DEBUG_FUNC, "%s", ">>> cmd_uid2name");
 
@@ -5169,8 +5463,9 @@ MODRET cmd_uid2name(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "<<< cmd_uid2name");
 
-  if (pw == NULL)
+  if (pw == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   /* In the case of a lookup of a negatively cached UID, the pw_name
    * member will be NULL, which causes an undesired handling by
@@ -5180,9 +5475,10 @@ MODRET cmd_uid2name(cmd_rec *cmd) {
     uid_name = pw->pw_name;
 
   } else {
-    snprintf(uidstr, MOD_SQL_BUFSIZE, "%lu",
-      (unsigned long) *((uid_t *) cmd->argv[0]));
-    uid_name = uidstr;
+    const char *uidstr = NULL;
+
+    uidstr = pr_uid2str(cmd->pool, *((uid_t *) cmd->argv[0]));
+    uid_name = (char *) uidstr;
   }
 
   return mod_create_data(cmd, uid_name);
@@ -5192,11 +5488,11 @@ MODRET cmd_gid2name(cmd_rec *cmd) {
   char *gid_name = NULL;
   struct group *gr;
   struct group lgr;
-  char gidstr[MOD_SQL_BUFSIZE];
 
   if (!SQL_GROUPS ||
-      !(cmap.engine & SQL_ENGINE_FL_AUTH))
+      !(cmap.engine & SQL_ENGINE_FL_AUTH)) {
     return PR_DECLINED(cmd);
+  }
 
   sql_log(DEBUG_FUNC, "%s", ">>> cmd_gid2name");
 
@@ -5206,8 +5502,9 @@ MODRET cmd_gid2name(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "<<< cmd_gid2name");
 
-  if (gr == NULL)
+  if (gr == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   /* In the case of a lookup of a negatively cached GID, the gr_name
    * member will be NULL, which causes an undesired handling by
@@ -5217,10 +5514,10 @@ MODRET cmd_gid2name(cmd_rec *cmd) {
     gid_name = gr->gr_name;
 
   } else {
-    memset(gidstr, '\0', sizeof(gidstr));
-    snprintf(gidstr, sizeof(gidstr)-1, "%lu",
-      (unsigned long) *((gid_t *) cmd->argv[0]));
-    gid_name = gidstr;
+    const char *gidstr = NULL;
+
+    gidstr = pr_gid2str(cmd->pool, *((gid_t *) cmd->argv[0]));
+    gid_name = (char *) gidstr;
   }
 
   return mod_create_data(cmd, gid_name);
@@ -5325,7 +5622,7 @@ MODRET cmd_getstats(cmd_rec *cmd) {
 		  cmap.sql_fretr, ", ", cmap.sql_bstor, ", ",
 		  cmap.sql_bretr, NULL);
   
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
     cmap.usrtable, query, where), "sql_select");
   if (check_response(mr, 0) < 0) {
     return mr;
@@ -5363,7 +5660,7 @@ MODRET cmd_getratio(cmd_rec *cmd) {
 		  cmap.sql_fcred, ", ", cmap.sql_brate, ", ",
 		  cmap.sql_bcred, NULL);
   
-  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
+  mr = sql_dispatch(sql_make_cmd(cmd->tmp_pool, 4, MOD_SQL_DEF_CONN_NAME,
     cmap.usrtable, query, where), "sql_select");
   if (check_response(mr, 0) < 0)
     return mr;
@@ -5518,21 +5815,22 @@ MODRET set_sqluserinfo(cmd_rec *cmd) {
 
   if (cmd->argc-1 == 1) {
     char *user = NULL, *userbyid = NULL, *userset = NULL, *usersetfast = NULL;
-    char *ptr = NULL;
+    char *param, *ptr = NULL;
 
     /* If only one paramter is used, it must be of the "custom:/" form. */
-    if (strncmp("custom:/", cmd->argv[1], 8) != 0) {
+    param = cmd->argv[1];
+    if (strncmp("custom:/", param, 8) != 0) {
       CONF_ERROR(cmd, "badly formatted parameter");
     }
 
-    ptr = strchr(cmd->argv[1] + 8, '/');
+    ptr = strchr(param + 8, '/');
     if (ptr == NULL) {
-      add_config_param_str("SQLCustomUserInfoByName", 1, cmd->argv[1] + 8);
+      add_config_param_str("SQLCustomUserInfoByName", 1, param + 8);
       return PR_HANDLED(cmd);
     }
 
     *ptr = '\0';
-    user = cmd->argv[1] + 8;
+    user = param + 8;
     userbyid = ptr + 1;
 
     add_config_param_str("SQLCustomUserInfoByName", 1, user);
@@ -5608,20 +5906,21 @@ MODRET set_sqlgroupinfo(cmd_rec *cmd) {
   if (cmd->argc-1 == 1) {
     char *groupbyname = NULL, *groupbyid = NULL, *groupmembers = NULL,
       *groupset = NULL, *groupsetfast = NULL;
-    char *ptr = NULL;
+    char *param, *ptr = NULL;
 
     /* If only one paramter is used, it must be of the "custom:/" form. */
-    if (strncmp("custom:/", cmd->argv[1], 8) != 0) {
+    param = cmd->argv[1];
+    if (strncmp("custom:/", param, 8) != 0) {
       CONF_ERROR(cmd, "badly formatted parameter");
     }
 
-    ptr = strchr(cmd->argv[1] + 8, '/');
+    ptr = strchr(param + 8, '/');
     if (ptr == NULL) {
       CONF_ERROR(cmd, "badly formatted parameter");
     }
 
     *ptr = '\0';
-    groupbyname = cmd->argv[1] + 8;
+    groupbyname = param + 8;
     groupbyid = ptr + 1;
 
     add_config_param_str("SQLCustomGroupInfoByName", 1, groupbyname);
@@ -5835,23 +6134,28 @@ MODRET set_sqllogonevent(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: SQLNamedConnectInfo name backend info [user [pass [ttl]]] */
+/* usage: SQLNamedConnectInfo name backend info [user [pass [ttl]]]
+ *          [ssl-cert:<path>] [ssl-key:<path>] [ssl-ca:/path] [ssl-ciphers:str]
+ */
 MODRET set_sqlnamedconnectinfo(cmd_rec *cmd) {
+  register unsigned int i;
+  int argc = 0;
   char *conn_name = NULL;
   char *backend = NULL;
-  char *info = NULL;
-  char *user = "";
-  char *pass = "";
-  char *ttl = NULL;
+  char **argv = NULL, *info = NULL, *user = "", *pass = "", *ttl = NULL;
+  char *ssl_cert_file = NULL, *ssl_key_file = NULL, *ssl_ca_file = NULL;
+  char *ssl_ca_dir = NULL, *ssl_ciphers = NULL;
   struct sql_backend *sb;
+  array_header *params;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
   if (cmd->argc-1 < 3 ||
-      cmd->argc-1 > 6) {
-    CONF_ERROR(cmd, "requires 3 to 6 arguments. Check the mod_sql docs.");
+      cmd->argc-1 > 10) {
+    CONF_ERROR(cmd, "requires 3 to 10 parameters; check the mod_sql docs");
   }
 
+  /* First, deal with any required parameters. */
   conn_name = cmd->argv[1];
 
   backend = cmd->argv[2];
@@ -5861,14 +6165,98 @@ MODRET set_sqlnamedconnectinfo(cmd_rec *cmd) {
       "' not supported", NULL));
   }
 
-  if (cmd->argc >= 4)
-    info = cmd->argv[3];
+  /* Next, search for/process any optional named parameters. */
+  params = make_array(cmd->tmp_pool, 0, sizeof(char *));
+
+  for (i = 3; i < cmd->argc; i++) {
+    if (strncmp(cmd->argv[i], "ssl-cert:", 9) == 0) {
+      char *path;
+
+      path = cmd->argv[i];
+
+      /* Advance past the "ssl-cert:" prefix. */
+      path += 9;
+
+      /* Check the file exists! */
+      if (file_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_cert_file = path;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SQL_VERSION
+          ": %s: SSL certificate '%s': %s", (char *) cmd->argv[0], path,
+          strerror(ENOENT));
+      }
+
+    } else if (strncmp(cmd->argv[i], "ssl-key:", 8) == 0) {
+      char *path;
+
+      path = cmd->argv[i];
 
-  if (cmd->argc >= 5)
-    user = cmd->argv[4];
+      /* Advance past the "ssl-key:" prefix. */
+      path += 8;
 
-  if (cmd->argc >= 6)
-    pass = cmd->argv[5];
+      /* Check the file exists! */
+      if (file_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_key_file = path;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SQL_VERSION
+          ": %s: SSL certificate key '%s': %s", (char *) cmd->argv[0], path,
+          strerror(ENOENT));
+      }
+
+    } else if (strncmp(cmd->argv[i], "ssl-ca:", 7) == 0) {
+      char *path;
+
+      path = cmd->argv[i];
+
+      /* Advance past the "ssl-ca:" prefix. */
+      path += 7;
+
+      /* Check the file exists! */
+      if (file_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_ca_file = path;
+
+      } else if (dir_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_ca_dir = path;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SQL_VERSION
+          ": %s: SSL CA '%s': %s", (char *) cmd->argv[0], path,
+          strerror(ENOENT));
+      }
+
+    } else if (strncmp(cmd->argv[i], "ssl-ciphers:", 12) == 0) {
+      char *ciphers;
+
+      ciphers = cmd->argv[i];
+
+      /* Advance past the "ssl-ciphers:" prefix. */
+      ciphers += 12;
+
+      ssl_ciphers = ciphers;
+
+    } else {
+      *((char **) push_array(params)) = cmd->argv[i];
+    }
+  }
+
+  /* Last, handle any optional positional parameters. */
+
+  argc = params->nelts;
+  argv = params->elts;
+
+  if (argc >= 1) {
+    info = argv[0];
+  }
+
+  if (argc >= 2) {
+    user = argv[1];
+  }
+
+  if (argc >= 3) {
+    pass = argv[2];
+  }
 
   /* Note: The only connection policy which is honored for NamedConnInfos
    * is the TTL policy, i.e. for setting a timer on this connect.  Other
@@ -5876,16 +6264,16 @@ MODRET set_sqlnamedconnectinfo(cmd_rec *cmd) {
    * SQLConnectInfo.
    */
 
-  if (cmd->argc >= 7) {
-    ttl = cmd->argv[6];
+  if (argc >= 4) {
+    ttl = argv[3];
 
   } else {
     ttl = "0";
   }
 
-  (void) add_config_param_str(cmd->argv[0], 6, conn_name, backend, info, user,
-    pass, ttl);
-
+  (void) add_config_param_str(cmd->argv[0], 11, conn_name, backend, info, user,
+    pass, ttl, ssl_cert_file, ssl_key_file, ssl_ca_file, ssl_ca_dir,
+    ssl_ciphers);
   return PR_HANDLED(cmd);
 }
 
@@ -5897,7 +6285,7 @@ MODRET set_sqlnamedquery(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
   if (cmd->argc < 4) {
-    CONF_ERROR(cmd, "requires at least 3 arguments");
+    CONF_ERROR(cmd, "requires at least 3 parameters");
   }
 
   name = pstrcat(cmd->tmp_pool, "SQLNamedQuery_", cmd->argv[1], NULL);
@@ -6002,18 +6390,17 @@ MODRET set_sqlauthenticate(cmd_rec *cmd) {
   config_rec *c = NULL;
   char *arg = NULL;
   int authmask = 0;
-  int cnt = 0;
-
-  int groupset_flag = 0;
-  int userset_flag = 0;
-  int groups_flag = 0;
-  int users_flag = 0;
+  unsigned long cnt = 0;
+  int groupset_flag, userset_flag, groups_flag, users_flag;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
   if (cmd->argc < 2 ||
-      cmd->argc > 5)
-    CONF_ERROR(cmd, "requires 1 to 4 arguments. Check the mod_sql docs");
+      cmd->argc > 5) {
+    CONF_ERROR(cmd, "requires 1 to 4 parameters; check the mod_sql docs");
+  }
+
+  groupset_flag = userset_flag = groups_flag = users_flag = FALSE;
 
   /* We're setting our authmask here -- we have a bunch of checks needed to
    * make sure users aren't trying to screw around with us.
@@ -6027,10 +6414,11 @@ MODRET set_sqlauthenticate(cmd_rec *cmd) {
   } else if (!((cmd->argc == 2) && !strcasecmp(cmd->argv[1], "off"))) {
     for (cnt = 1; cnt < cmd->argc; cnt++) {
       arg = cmd->argv[cnt];
-      
+
       if (strncasecmp("groupset", arg, 8) == 0) {
-        if (groupset_flag)
+        if (groupset_flag) {
           CONF_ERROR(cmd, "groupset already set");
+        }
 
         if (strcasecmp("groupsetfast", arg) == 0) {
           authmask |= SQL_FAST_GROUPSET;
@@ -6040,11 +6428,12 @@ MODRET set_sqlauthenticate(cmd_rec *cmd) {
         }
 
         authmask |= SQL_AUTH_GROUPSET;
-        groupset_flag = 1;
+        groupset_flag = TRUE;
 
       } else if (strncasecmp("userset", arg, 7) == 0) {
-        if (userset_flag)
+        if (userset_flag) {
           CONF_ERROR(cmd, "userset already set");
+        }
 
         if (strcasecmp("usersetfast", arg) == 0) {
           authmask |= SQL_FAST_USERSET;
@@ -6054,35 +6443,43 @@ MODRET set_sqlauthenticate(cmd_rec *cmd) {
         }
 
         authmask |= SQL_AUTH_USERSET;
-        userset_flag = 1;
+        userset_flag = TRUE;
 
       } else if (strncasecmp("groups", arg, 6) == 0) {
-        if (groups_flag)
+        if (groups_flag) {
           CONF_ERROR(cmd, "groups already set");
-	
+        }
+
         if (strcasecmp("groups*", arg) == 0) {
-          pr_log_debug(DEBUG1, "%s: use of '*' in SQLAuthenticate has been deprecated.  Use AuthOrder for setting authoritativeness", cmd->argv[0]);
+          pr_log_debug(DEBUG1,
+            "%s: use of '*' in SQLAuthenticate has been deprecated. "
+            "Use AuthOrder for setting authoritativeness",
+            (char *) cmd->argv[0]);
 
         } else if (strlen(arg) > 6) {
           CONF_ERROR(cmd, "unknown argument");
         }
 
         authmask |= SQL_AUTH_GROUPS;
-        groups_flag = 1;
+        groups_flag = TRUE;
 
       } else if (strncasecmp("users", arg, 5) == 0) {
-        if (users_flag)
+        if (users_flag) {
           CONF_ERROR(cmd, "users already set");
+        }
 
         if (strcasecmp("users*", arg) == 0) {
-          pr_log_debug(DEBUG1, "%s: use of '*' in SQLAuthenticate has been deprecated.  Use AuthOrder for setting authoritativeness", cmd->argv[0]);
+          pr_log_debug(DEBUG1,
+            "%s: use of '*' in SQLAuthenticate has been deprecated. "
+            "Use AuthOrder for setting authoritativeness",
+            (char *) cmd->argv[0]);
 
         } else if (strlen(arg) > 5) {
           CONF_ERROR(cmd, "unknown argument");
         }
 
         authmask |= SQL_AUTH_USERS;
-        users_flag = 1;
+        users_flag = TRUE;
 
       } else {
         CONF_ERROR(cmd, "unknown argument");
@@ -6114,12 +6511,13 @@ static int sql_logfd = -1;
 static int sql_closelog(void) {
 
   /* sanity check */
-  if (sql_logfd != -1) {
-    close(sql_logfd);
-    sql_logfd = -1;
-    sql_logfile = NULL;
+  if (sql_logfd >= 0) {
+    (void) close(sql_logfd);
   }
 
+  sql_logfd = -1;
+  sql_logfile = NULL;
+
   return 0;
 }
 
@@ -6162,38 +6560,125 @@ static int sql_openlog(void) {
   return res;
 }
 
-/* usage: SQLConnectInfo info [user [pass [policy]]] */
+/* usage: SQLConnectInfo info [user [pass [policy]]]
+ *          [ssl-cert:<path>] [ssl-key:<path>] [ssl-ca:/path] [ssl-ciphers:str]
+ */
 MODRET set_sqlconnectinfo(cmd_rec *cmd) {
-  char *info = NULL;
-  char *user = "";
-  char *pass = "";
-  char *ttl = NULL;
+  register unsigned int i;
+  int argc = 0;
+  char **argv = NULL, *info = NULL, *user = "", *pass = "", *ttl = NULL;
+  char *ssl_cert_file = NULL, *ssl_key_file = NULL, *ssl_ca_file = NULL;
+  char *ssl_ca_dir = NULL, *ssl_ciphers = NULL;
+  array_header *params;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
   if (cmd->argc < 2 ||
-      cmd->argc > 5) {
-    CONF_ERROR(cmd, "requires 1 to 4 arguments.  Check the mod_sql docs");
+      cmd->argc > 9) {
+    CONF_ERROR(cmd, "requires 1 to 8 parameters; check the mod_sql docs");
   }
 
-  if (cmd->argc > 1)
-    info = cmd->argv[1];
+  /* First, deal with any required parameters. */
+  info = cmd->argv[1];
+
+  /* Next, search for/process any optional named parameters. */
+  params = make_array(cmd->tmp_pool, 0, sizeof(char *));
 
-  if (cmd->argc > 2)
-    user = cmd->argv[2];
+  for (i = 2; i < cmd->argc; i++) {
+    if (strncmp(cmd->argv[i], "ssl-cert:", 9) == 0) {
+      char *path;
+
+      path = cmd->argv[i];
+
+      /* Advance past the "ssl-cert:" prefix. */
+      path += 9;
+
+      /* Check the file exists! */
+      if (file_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_cert_file = path;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SQL_VERSION
+          ": %s: SSL certificate '%s': %s", (char *) cmd->argv[0], path,
+          strerror(ENOENT));
+      }
 
-  if (cmd->argc > 3)
-    pass = cmd->argv[3];
+    } else if (strncmp(cmd->argv[i], "ssl-key:", 8) == 0) {
+      char *path;
 
-  if (cmd->argc > 4) {
-    ttl = cmd->argv[4];
+      path = cmd->argv[i];
+
+      /* Advance past the "ssl-key:" prefix. */
+      path += 8;
+
+      /* Check the file exists! */
+      if (file_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_key_file = path;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SQL_VERSION
+          ": %s: SSL certificate key '%s': %s", (char *) cmd->argv[0], path,
+          strerror(ENOENT));
+      }
+
+    } else if (strncmp(cmd->argv[i], "ssl-ca:", 7) == 0) {
+      char *path;
+
+      path = cmd->argv[i];
+
+      /* Advance past the "ssl-ca:" prefix. */
+      path += 7;
+
+      /* Check the file exists! */
+      if (file_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_ca_file = path;
+
+      } else if (dir_exists2(cmd->tmp_pool, path) == TRUE) {
+        ssl_ca_dir = path;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_SQL_VERSION
+          ": %s: SSL CA '%s': %s", (char *) cmd->argv[0], path,
+          strerror(ENOENT));
+      }
+
+    } else if (strncmp(cmd->argv[i], "ssl-ciphers:", 12) == 0) {
+      char *ciphers;
+
+      ciphers = cmd->argv[i];
+
+      /* Advance past the "ssl-ciphers:" prefix. */
+      ciphers += 12;
+
+      ssl_ciphers = ciphers;
+
+    } else {
+      *((char **) push_array(params)) = cmd->argv[i];
+    }
+  }
+
+  /* Last, handle any optional positional parameters. */
+
+  argc = params->nelts;
+  argv = params->elts;
+
+  if (argc >= 1) {
+    user = argv[0];
+  }
+
+  if (argc >= 2) {
+    pass = argv[1];
+  }
+
+  if (argc >= 3) {
+    ttl = argv[2];
 
   } else {
     ttl = "0";
   }
 
-  (void) add_config_param_str(cmd->argv[0], 4, info, user, pass, ttl);
-
+  (void) add_config_param_str(cmd->argv[0], 9, info, user, pass, ttl,
+    ssl_cert_file, ssl_key_file, ssl_ca_file, ssl_ca_dir, ssl_ciphers);
   return PR_HANDLED(cmd);
 }
 
@@ -6258,7 +6743,13 @@ MODRET set_sqlauthtypes(cmd_rec *cmd) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown SQLAuthType '",
         cmd->argv[i], "'", NULL));
     }
- 
+
+    if (strcasecmp(sah->name, "Plaintext") == 0) {
+      pr_log_pri(PR_LOG_WARNING, MOD_SQL_VERSION
+        ": WARNING: Use of Plaintext SQLAuthType is insecure, as it allows "
+        "storage of passwords *in the clear* in your database tables");
+    }
+
     *((struct sql_authtype_handler **) push_array(auth_list)) = sah;
   }
 
@@ -6277,134 +6768,103 @@ MODRET set_sqlbackend(cmd_rec *cmd) {
 
 MODRET set_sqlminid(cmd_rec *cmd) {
   config_rec *c;
-  unsigned long val;
-  char *endptr = NULL;
+  uid_t uid;
+  gid_t gid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
-  val = strtoul(cmd->argv[1], &endptr, 10);
-
-  if (*endptr != '\0')
-    CONF_ERROR(cmd, "requires a numeric argument");
+  if (pr_str2uid(cmd->argv[1], &uid) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid UID value '",
+      cmd->argv[1], "'", NULL));
+  }
 
-  /* Whee! need to check if in the legal range for uid_t and gid_t. */
-  if (val == ULONG_MAX &&
-      errno == ERANGE) {
-    CONF_ERROR(cmd, "the value given is outside the legal range");
+  if (pr_str2gid(cmd->argv[1], &gid) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid GID value '",
+      cmd->argv[1], "'", NULL));
   }
 
-  c = add_config_param(cmd->argv[0], 1, NULL);
-  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
-  *((unsigned long *) c->argv[0]) = val;
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(uid_t));
+  *((uid_t *) c->argv[0]) = uid;
+  c->argv[1] = pcalloc(c->pool, sizeof(gid_t));
+  *((gid_t *) c->argv[1]) = gid;
 
   return PR_HANDLED(cmd);
 }
 
 MODRET set_sqlminuseruid(cmd_rec *cmd) {
   config_rec *c = NULL;
-  unsigned long val;
-  char *endptr = NULL;
+  uid_t uid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
-  val = strtoul(cmd->argv[1], &endptr, 10);
-
-  if (*endptr != '\0')
-    CONF_ERROR(cmd, "requires a numeric argument");
-
-  /* Whee! need to check if in the legal range for uid_t. */
-  if (val == ULONG_MAX &&
-      errno == ERANGE) {
-    CONF_ERROR(cmd, "the value given is outside the legal range");
+  if (pr_str2uid(cmd->argv[1], &uid) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid UID value '",
+      cmd->argv[1], "'", NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(uid_t));
-  *((uid_t *) c->argv[0]) = val;
+  *((uid_t *) c->argv[0]) = uid;
 
   return PR_HANDLED(cmd);
 }
 
 MODRET set_sqlminusergid(cmd_rec *cmd) {
   config_rec *c = NULL;
-  unsigned long val;
-  char *endptr = NULL;
+  gid_t gid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
-  val = strtoul(cmd->argv[1], &endptr, 10);
-
-  if (*endptr != '\0')
-    CONF_ERROR(cmd, "requires a numeric argument");
-
-  /* Whee! need to check if in the legal range for gid_t. */
-  if (val == ULONG_MAX &&
-      errno == ERANGE) {
-    CONF_ERROR(cmd, "the value given is outside the legal range");
+  if (pr_str2gid(cmd->argv[1], &gid) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid GID value '",
+      cmd->argv[1], "'", NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(gid_t));
-  *((gid_t *) c->argv[0]) = val;
+  *((gid_t *) c->argv[0]) = gid;
 
   return PR_HANDLED(cmd);
 }
 
 MODRET set_sqldefaultuid(cmd_rec *cmd) {
-  int xerrno;
   config_rec *c;
-  uid_t val;
-  char *endptr = NULL;
+  uid_t uid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
-  errno = 0;
-  val = strtoul(cmd->argv[1], &endptr, 10);
-  xerrno = errno;
-
-  if (*endptr != '\0')
-    CONF_ERROR(cmd, "requires a numeric argument");
-
-  /* Whee! need to check is in the legal range for uid_t. */
-  if (xerrno == ERANGE) {
-    CONF_ERROR(cmd, "the value given is outside the legal range");
+  if (pr_str2uid(cmd->argv[1], &uid) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid UID value '",
+      cmd->argv[1], "'", NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(uid_t));
-  *((uid_t *) c->argv[0]) = val;
+  *((uid_t *) c->argv[0]) = uid;
 
   return PR_HANDLED(cmd);
 }
 
 MODRET set_sqldefaultgid(cmd_rec *cmd) {
-  int xerrno;
   config_rec *c;
-  gid_t val;
-  char *endptr = NULL;
+  gid_t gid;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
 
-  errno = 0;
-  val = strtoul(cmd->argv[1], &endptr, 10);
-  xerrno = errno;
-
-  if (*endptr != '\0')
-    CONF_ERROR(cmd, "requires a numeric argument");
-
-  /* Whee! need to check is in the legal range for gid_t. */
-  if (xerrno == ERANGE) {
-    CONF_ERROR(cmd, "the value given is outside the legal range");
+  if (pr_str2gid(cmd->argv[1], &gid) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid GID value '",
+      cmd->argv[1], "'", NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(gid_t));
-  *((gid_t *) c->argv[0]) = val;
+  *((gid_t *) c->argv[0]) = gid;
 
   return PR_HANDLED(cmd);
 }
@@ -6429,8 +6889,8 @@ static void sql_chroot_ev(const void *event_data, void *user_data) {
         cmd_rec *cmd;
         modret_t *mr; 
 
-        cmd = _sql_make_cmd(tmp_pool, 1, snc->conn_name);
-        mr = _sql_dispatch(cmd, "sql_open");
+        cmd = sql_make_cmd(tmp_pool, 1, snc->conn_name);
+        mr = sql_dispatch(cmd, "sql_open");
         (void) check_response(mr, 0);
         SQL_FREE_CMD(cmd);
       }
@@ -6451,14 +6911,13 @@ static void sql_exit_ev(const void *event_data, void *user_data) {
 
   /* handle EXIT queries */
   c = find_config(main_server->conf, CONF_PARAM, "SQLLog_EXIT", FALSE);
-
-  while (c) {
+  while (c != NULL) {
     pr_signals_handle();
 
     /* Since we're exiting the process here (or soon, anyway), we can
      * get away with using the config_rec's pool.
      */
-    cmd = _sql_make_cmd(c->pool, 1, "EXIT");
+    cmd = sql_make_cmd(c->pool, 1, "EXIT");
 
     /* Ignore errors; we're exiting anyway. */
     (void) process_sqllog(cmd, c, "exit_listener", SQL_LOG_FL_IGNORE_ERRORS);
@@ -6466,8 +6925,8 @@ static void sql_exit_ev(const void *event_data, void *user_data) {
     c = find_config_next(c, c->next, CONF_PARAM, "SQLLog_EXIT", FALSE);
   }
 
-  cmd = _sql_make_cmd(session.pool, 0);
-  mr = _sql_dispatch(cmd, "sql_exit");
+  cmd = sql_make_cmd(session.pool, 0);
+  mr = sql_dispatch(cmd, "sql_exit");
   (void) check_response(mr, SQL_LOG_FL_IGNORE_ERRORS);
 
   sql_closelog();
@@ -6495,6 +6954,7 @@ static void sql_mod_unload_ev(const void *event_data, void *user_data) {
 
     close(sql_logfd);
     sql_logfd = -1;
+    sql_logfile = NULL;
   }
 }
 
@@ -6521,6 +6981,49 @@ static void sql_eventlog_ev(const void *event_data, void *user_data) {
   }
 }
 
+static void sql_sess_reinit_ev(const void *event_data, void *user_data) {
+  config_rec *c;
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&sql_module, "core.chroot", sql_chroot_ev);
+  pr_event_unregister(&sql_module, "core.exit", sql_exit_ev);
+  pr_event_unregister(&sql_module, "core.session-reinit", sql_sess_reinit_ev);
+
+  c = find_config(session.prev_server->conf, CONF_PARAM, "SQLLogOnEvent",
+    FALSE);
+  while (c != NULL) {
+    char *event_name;
+
+    pr_signals_handle();
+
+    event_name = c->argv[0];
+
+    pr_event_unregister(&sql_module, event_name, sql_eventlog_ev);
+    c = find_config_next(c, c->next, CONF_PARAM, "SQLLogOnEvent", FALSE);
+  }
+
+  pr_sql_opts = 0UL;
+  pr_sql_conn_policy = 0;
+
+  if (sql_logfd >= 0) {
+    (void) close(sql_logfd);
+    sql_logfd = -1;
+    sql_logfile = NULL;
+  }
+
+  memset(&cmap, 0, sizeof(cmap));
+  sql_cmdtable = NULL;
+  sql_default_cmdtable = NULL;
+
+  res = sql_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&sql_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED,
+      NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -6556,6 +7059,9 @@ static int sql_sess_init(void) {
   char *fieldset = NULL;
   pool *tmp_pool = NULL;
 
+  pr_event_register(&sql_module, "core.session-reinit", sql_sess_reinit_ev,
+    NULL);
+
   /* Build a temporary pool */
   tmp_pool = make_sub_pool(session.pool);
 
@@ -6591,6 +7097,10 @@ static int sql_sess_init(void) {
     return -1;
   }
 
+  if (ptr != NULL) {
+    pr_trace_msg(trace_channel, 9, "loaded '%s' SQL backend", (char *) ptr);
+  }
+
   /* Construct our internal cache structure for this session. */
   memset(&cmap, 0, sizeof(cmap));
 
@@ -6602,9 +7112,14 @@ static int sql_sess_init(void) {
     cmap.engine = engine = (SQL_ENGINE_FL_AUTH|SQL_ENGINE_FL_LOG);
   }
 
+  if (cmap.engine == 0) {
+    destroy_pool(tmp_pool);
+    return 0;
+  }
+
   /* Get our backend info and toss it up */
-  cmd = _sql_make_cmd(tmp_pool, 1, "foo");
-  mr = _sql_dispatch(cmd, "sql_identify");
+  cmd = sql_make_cmd(tmp_pool, 1, "foo");
+  mr = sql_dispatch(cmd, "sql_identify");
   if (check_response(mr, 0) < 0) {
     destroy_pool(tmp_pool);
     return -1;
@@ -6619,7 +7134,7 @@ static int sql_sess_init(void) {
 
   sql_log(DEBUG_FUNC, "%s", ">>> sql_sess_init");
 
-  if (!sql_pool) {
+  if (sql_pool == NULL) {
     sql_pool = make_sub_pool(session.pool);
     pr_pool_tag(sql_pool, MOD_SQL_VERSION);
   }
@@ -6895,9 +7410,10 @@ static int sql_sess_init(void) {
     sql_log(DEBUG_INFO, "%s", "error: no SQLAuthTypes configured");
   }
 
-  ptr = get_param_ptr(main_server->conf, "SQLMinID", FALSE);
-  if (ptr != NULL) {
-    cmap.minuseruid = cmap.minusergid = *((unsigned long *) ptr);
+  c = find_config(main_server->conf, CONF_PARAM, "SQLMinID", FALSE);
+  if (c != NULL) {
+    cmap.minuseruid = *((uid_t *) c->argv[0]);
+    cmap.minusergid = *((gid_t *) c->argv[1]);
 
   } else {
     ptr = get_param_ptr(main_server->conf, "SQLMinUserUID", FALSE);
@@ -6972,7 +7488,8 @@ static int sql_sess_init(void) {
     }
 
     if (sql_define_conn(tmp_pool, MOD_SQL_DEF_CONN_NAME, c->argv[1], c->argv[2],
-      c->argv[0], c->argv[3]) < 0) {
+        c->argv[0], c->argv[3], c->argv[4], c->argv[5], c->argv[6], c->argv[7],
+        c->argv[8]) < 0) {
       return -1;
     }
 
@@ -6986,9 +7503,9 @@ static int sql_sess_init(void) {
 
     c = find_config(main_server->conf, CONF_PARAM, "SQLNamedConnectInfo",
       FALSE);
-    while (c) {
+    while (c != NULL) {
       struct sql_named_conn *snc;
-      char *conn_name;
+      const char *conn_name;
 
       pr_signals_handle();
 
@@ -7007,7 +7524,8 @@ static int sql_sess_init(void) {
         }
 
         if (sql_define_conn(tmp_pool, c->argv[0], c->argv[3], c->argv[4],
-            c->argv[2], c->argv[5]) < 0) {
+            c->argv[2], c->argv[5], c->argv[6], c->argv[7], c->argv[8],
+            c->argv[9], c->argv[10]) < 0) {
           /* Restore the default connection policy. */
           pr_sql_conn_policy = default_conn_policy;
 
@@ -7048,7 +7566,7 @@ static int sql_sess_init(void) {
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "SQLLogOnEvent", FALSE);
-  while (c) {
+  while (c != NULL) {
     char *event_name;
 
     pr_signals_handle();
@@ -7141,10 +7659,8 @@ static int sql_sess_init(void) {
     sql_log(DEBUG_INFO, "SQLMinUserGID      : %u", cmap.minusergid);
   }
    
-  if (SQL_GROUPS) {
-    sql_log(DEBUG_INFO, "SQLDefaultUID      : %u", cmap.defaultuid);
-    sql_log(DEBUG_INFO, "SQLDefaultGID      : %u", cmap.defaultgid);
-  }
+  sql_log(DEBUG_INFO, "SQLDefaultUID      : %u", cmap.defaultuid);
+  sql_log(DEBUG_INFO, "SQLDefaultGID      : %u", cmap.defaultgid);
 
   if (cmap.sql_fstor) {
     sql_log(DEBUG_INFO, "sql_fstor          : %s", cmap.sql_fstor);
@@ -7177,42 +7693,35 @@ static int sql_sess_init(void) {
  *****************************************************************/
 
 static conftable sql_conftab[] = {
-  { "SQLConnectInfo",	 set_sqlconnectinfo,	NULL },
-  { "SQLNamedConnectInfo",set_sqlnamedconnectinfo, NULL },
-
-  { "SQLAuthenticate",	set_sqlauthenticate,	NULL },
-  { "SQLAuthTypes",	set_sqlauthtypes,	NULL },
-  { "SQLBackend",	set_sqlbackend,		NULL },
-  { "SQLEngine",	set_sqlengine,		NULL },
-  { "SQLOptions",	set_sqloptions,		NULL },
-
-  { "SQLUserInfo", set_sqluserinfo, NULL},
-  { "SQLUserPrimaryKey", set_sqluserprimarykey, NULL },
-  { "SQLUserWhereClause", set_sqluserwhereclause, NULL },
-
-  { "SQLGroupInfo", set_sqlgroupinfo, NULL },
-  { "SQLGroupPrimaryKey", set_sqlgroupprimarykey, NULL },
-  { "SQLGroupWhereClause", set_sqlgroupwhereclause, NULL },
-
-  { "SQLMinID", set_sqlminid, NULL },
-  { "SQLMinUserUID", set_sqlminuseruid, NULL },
-  { "SQLMinUserGID", set_sqlminusergid, NULL },
-  { "SQLDefaultUID", set_sqldefaultuid, NULL },
-  { "SQLDefaultGID", set_sqldefaultgid, NULL },
-
-  { "SQLNegativeCache", set_sqlnegativecache, NULL },
+  { "SQLAuthenticate",		set_sqlauthenticate,		NULL },
+  { "SQLAuthTypes",		set_sqlauthtypes,		NULL },
+  { "SQLBackend",		set_sqlbackend,			NULL },
+  { "SQLConnectInfo",	 	set_sqlconnectinfo,		NULL },
+  { "SQLDefaultGID",		set_sqldefaultgid,		NULL },
+  { "SQLDefaultHomedir",	set_sqldefaulthomedir,		NULL },
+  { "SQLDefaultUID",		set_sqldefaultuid,		NULL },
+  { "SQLEngine",		set_sqlengine,			NULL },
+  { "SQLGroupInfo",		set_sqlgroupinfo,		NULL },
+  { "SQLGroupPrimaryKey",	set_sqlgroupprimarykey,		NULL },
+  { "SQLGroupWhereClause",	set_sqlgroupwhereclause,	NULL },
+  { "SQLLog",			set_sqllog,			NULL },
+  { "SQLLogFile",		set_sqllogfile,			NULL },
+  { "SQLLogOnEvent",		set_sqllogonevent,		NULL },
+  { "SQLMinID",			set_sqlminid,			NULL },
+  { "SQLMinUserGID",		set_sqlminusergid,		NULL },
+  { "SQLMinUserUID",		set_sqlminuseruid,		NULL },
+  { "SQLNamedConnectInfo",	set_sqlnamedconnectinfo,	NULL },
+  { "SQLNamedQuery",		set_sqlnamedquery,		NULL },
+  { "SQLNegativeCache",		set_sqlnegativecache,		NULL },
+  { "SQLOptions",		set_sqloptions,			NULL },
+  { "SQLShowInfo",		set_sqlshowinfo,		NULL },
+  { "SQLUserInfo",		set_sqluserinfo,		NULL },
+  { "SQLUserPrimaryKey",	set_sqluserprimarykey,		NULL },
+  { "SQLUserWhereClause",	set_sqluserwhereclause,		NULL },
 
   { "SQLRatios", set_sqlratios, NULL },
   { "SQLRatioStats", set_sqlratiostats, NULL },
 
-  { "SQLDefaultHomedir", set_sqldefaulthomedir, NULL },
-
-  { "SQLLog", set_sqllog, NULL },
-  { "SQLLogFile", set_sqllogfile, NULL },
-  { "SQLLogOnEvent", set_sqllogonevent, NULL },
-  { "SQLNamedQuery", set_sqlnamedquery, NULL },
-  { "SQLShowInfo", set_sqlshowinfo, NULL },
-
   { NULL, NULL, NULL }
 };
 
diff --git a/contrib/mod_sql.h b/contrib/mod_sql.h
index 7a2534a..ff853c3 100644
--- a/contrib/mod_sql.h
+++ b/contrib/mod_sql.h
@@ -3,7 +3,7 @@
  * Time-stamp: <1999-10-04 03:21:21 root>
  * Copyright (c) 1998-1999 Johnie Ingram.
  * Copyright (c) 2001 Andrew Houghton
- * Copyright (c) 2002-2013 The ProFTPD Project
+ * Copyright (c) 2002-2015 The ProFTPD Project
  *  
  * 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
@@ -23,8 +23,6 @@
  * 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.
- *
- * $Id: mod_sql.h,v 1.12 2013-09-04 20:32:25 castaglia Exp $
  */
 
 #ifndef MOD_SQL_H
@@ -32,7 +30,7 @@
 
 /* mod_sql helper functions */
 int sql_log(int, const char *, ...);
-cmd_rec *_sql_make_cmd(pool * cp, int argc, ...);
+cmd_rec *sql_make_cmd(pool *p, int argc, ...);
 int sql_register_backend(const char *, cmdtable *);
 int sql_unregister_backend(const char *);
 
@@ -73,18 +71,16 @@ typedef struct sql_data_struct sql_data_t;
 
 /* API versions */
 
-/* MOD_SQL_API_V2: guarantees to correctly implement cmd_open, cmd_close,
+/* MOD_SQL_API_V1: guarantees to correctly implement cmd_open, cmd_close,
  *  cmd_defineconnection, cmd_select, cmd_insert, cmd_update, cmd_escapestring,
  *  cmd_query, cmd_checkauth, and cmd_identify.  Also guarantees to
  *  perform proper registration of the cmdtable.
  */
-
 #define MOD_SQL_API_V1 "mod_sql_api_v1"
 
 /* MOD_SQL_API_V2: MOD_SQL_API_V1 && guarantees to correctly implement 
  *  cmd_procedure.
  */
-
 #define MOD_SQL_API_V2 "mod_sql_api_v2"
 
 /* SQLOption values */
@@ -104,5 +100,3 @@ extern unsigned int pr_sql_conn_policy;
 #define SQL_CONN_POLICY_PERCONN		4
 
 #endif /* MOD_SQL_H */
-
-
diff --git a/contrib/mod_sql_mysql.c b/contrib/mod_sql_mysql.c
index ba37fd9..21cf0b5 100644
--- a/contrib/mod_sql_mysql.c
+++ b/contrib/mod_sql_mysql.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD: mod_sql_mysql -- Support for connecting to MySQL databases.
  * Copyright (c) 2001 Andrew Houghton
- * Copyright (c) 2004-2016 TJ Saunders
+ * Copyright (c) 2004-2017 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
@@ -23,7 +23,7 @@
  * the source distribution.
  *
  * -----DO NOT EDIT-----
- * $Libraries: -lm -lmysqlclient -lz $
+ * $Libraries: -lm -lmysqlclient -lz$
  */
 
 /* INTRO:
@@ -121,11 +121,10 @@
  * file.  If anything is unclear, please contact the author.  
  */
 
-/* 
- * Internal define used for debug and logging.  All backends are encouraged
+/* Internal define used for debug and logging.  All backends are encouraged
  * to use the same format.
  */
-#define MOD_SQL_MYSQL_VERSION		"mod_sql_mysql/4.0.8"
+#define MOD_SQL_MYSQL_VERSION		"mod_sql_mysql/4.0.9"
 
 #define _MYSQL_PORT "3306"
 
@@ -161,16 +160,21 @@ module sql_mysql_module;
 struct db_conn_struct {
 
   /* MySQL-specific members */
-
-  char *host;
-  char *user;
-  char *pass;
-  char *db;
-  char *port;
-  char *unix_sock;
+  const char *host;
+  const char *user;
+  const char *pass;
+  const char *db;
+  const char *port;
+  const char *unix_sock;
+
+  /* For configuring the SSL/TLS session to the MySQL server. */
+  const char *ssl_cert_file;
+  const char *ssl_key_file;
+  const char *ssl_ca_file;
+  const char *ssl_ca_dir;
+  const char *ssl_ciphers;
 
   MYSQL *mysql;
-
 };
 
 typedef struct db_conn_struct db_conn_t;
@@ -183,16 +187,14 @@ typedef struct db_conn_struct db_conn_t;
  */
 
 struct conn_entry_struct {
-  char *name;
+  const char *name;
   void *data;
 
-  /* timer handling */
-
+  /* Timer handling */
   int timer;
   int ttl;
 
-  /* connection handling */
-
+  /* Connection handling */
   unsigned int connections;
 };
 
@@ -203,24 +205,26 @@ typedef struct conn_entry_struct conn_entry_t;
 static pool *conn_pool = NULL;
 static array_header *conn_cache = NULL;
 
-/*
- *  _sql_get_connection: walks the connection cache looking for the named
+static const char *trace_channel = "sql.mysql";
+
+/*  sql_get_connection: walks the connection cache looking for the named
  *   connection.  Returns NULL if unsuccessful, a pointer to the conn_entry_t
  *   if successful.
  */
-static conn_entry_t *_sql_get_connection(char *name) {
+static conn_entry_t *sql_get_connection(const char *conn_name) {
   register unsigned int i;
 
-  if (name == NULL) {
+  if (conn_name == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
   /* walk the array looking for our entry */
   for (i = 0; i < conn_cache->nelts; i++) {
-    conn_entry_t *entry = ((conn_entry_t **) conn_cache->elts)[i];
+    conn_entry_t *entry;
 
-    if (strcmp(name, entry->name) == 0) {
+    entry = ((conn_entry_t **) conn_cache->elts)[i];
+    if (strcmp(conn_name, entry->name) == 0) {
       return entry;
     }
   }
@@ -229,16 +233,15 @@ static conn_entry_t *_sql_get_connection(char *name) {
   return NULL;
 }
 
-/* 
- * _sql_add_connection: internal helper function to maintain a cache of 
- *  connections.  Since we expect the number of named connections to
- *  be small, simply use an array header to hold them.  We don't allow 
- *  duplicate connection names.
+/* sql_add_connection: internal helper function to maintain a cache of
+ *  connections.  Since we expect the number of named connections to be small,
+ *  simply use an array header to hold them.  We don't allow duplicate
+ *  connection names.
  *
  * Returns: NULL if the insertion was unsuccessful, a pointer to the 
  *  conn_entry_t that was created if successful.
  */
-static void *_sql_add_connection(pool *p, char *name, db_conn_t *conn) {
+static void *sql_add_connection(pool *p, const char *name, db_conn_t *conn) {
   conn_entry_t *entry = NULL;
 
   if (name == NULL ||
@@ -248,28 +251,25 @@ static void *_sql_add_connection(pool *p, char *name, db_conn_t *conn) {
     return NULL;
   }
 
-  if (_sql_get_connection(name)) {
-    /* duplicated name */
+  if (sql_get_connection(name) != NULL) {
     errno = EEXIST;
     return NULL;
   }
 
   entry = (conn_entry_t *) pcalloc(p, sizeof(conn_entry_t));
-  entry->name = name;
+  entry->name = pstrdup(p, name);
   entry->data = conn;
 
   *((conn_entry_t **) push_array(conn_cache)) = entry;
-
   return entry;
 }
 
-/* _sql_check_cmd: tests to make sure the cmd_rec is valid and is 
- *  properly filled in.  If not, it's grounds for the daemon to
- *  shutdown.
+/* sql_check_cmd: tests to make sure the cmd_rec is valid and is properly
+ *  filled in.  If not, it's grounds for the daemon to shutdown.
  */
-static void _sql_check_cmd(cmd_rec *cmd, char *msg) {
-  if (!cmd || 
-      !cmd->tmp_pool) {
+static void sql_check_cmd(cmd_rec *cmd, char *msg) {
+  if (cmd == NULL ||
+      cmd->tmp_pool == NULL) {
     pr_log_pri(PR_LOG_ERR, MOD_SQL_MYSQL_VERSION
       ": '%s' was passed an invalid cmd_rec (internal bug); shutting down",
       msg);
@@ -281,22 +281,22 @@ static void _sql_check_cmd(cmd_rec *cmd, char *msg) {
   return;
 }
 
-/*
- * sql_timer_cb: when a timer goes off, this is the function that gets called.
+/* sql_timer_cb: when a timer goes off, this is the function that gets called.
  * This function makes assumptions about the db_conn_t members.
  */
 static int sql_timer_cb(CALLBACK_FRAME) {
-  conn_entry_t *entry = NULL;
-  int i = 0;
-  cmd_rec *cmd = NULL;
+  register unsigned int i;
  
   for (i = 0; i < conn_cache->nelts; i++) {
+    conn_entry_t *entry = NULL;
+
     entry = ((conn_entry_t **) conn_cache->elts)[i];
+    if ((unsigned long) entry->timer == p2) {
+      cmd_rec *cmd = NULL;
 
-    if (entry->timer == p2) {
       sql_log(DEBUG_INFO, "timer expired for connection '%s'", entry->name);
-      cmd = _sql_make_cmd( conn_pool, 2, entry->name, "1" );
-      cmd_close( cmd );
+      cmd = sql_make_cmd(conn_pool, 2, entry->name, "1");
+      cmd_close(cmd);
       SQL_FREE_CMD(cmd);
       entry->timer = 0;
     }
@@ -305,29 +305,29 @@ static int sql_timer_cb(CALLBACK_FRAME) {
   return 0;
 }
 
-/* _build_error: constructs a modret_t filled with error information;
+/* build_error: constructs a modret_t filled with error information;
  *  mod_sql_mysql calls this function and returns the resulting mod_ret_t
  *  whenever a call to the database results in an error.  Other backends
  *  may want to use a different method to return error information.
  */
-static modret_t *_build_error(cmd_rec *cmd, db_conn_t *conn) {
+static modret_t *build_error(cmd_rec *cmd, db_conn_t *conn) {
   char num[20] = {'\0'};
 
-  if (!conn)
+  if (conn == NULL) {
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
+  }
 
   snprintf(num, 20, "%u", mysql_errno(conn->mysql));
-
-  return PR_ERROR_MSG(cmd, num, (char *) mysql_error(conn->mysql));
+  return PR_ERROR_MSG(cmd, pstrdup(cmd->pool, num),
+    pstrdup(cmd->pool, (char *) mysql_error(conn->mysql)));
 }
 
-/*
- * _build_data: both cmd_select and cmd_procedure potentially
+/* build_data: both cmd_select and cmd_procedure potentially
  *  return data to mod_sql; this function builds a modret to return
  *  that data.  This is MySQL specific; other backends may choose 
  *  to do things differently.
  */
-static modret_t *_build_data(cmd_rec *cmd, db_conn_t *conn) {
+static modret_t *build_data(cmd_rec *cmd, db_conn_t *conn) {
   modret_t *mr = NULL;
   MYSQL *mysql = NULL;
   MYSQL_RES *result = NULL;
@@ -337,8 +337,9 @@ static modret_t *_build_data(cmd_rec *cmd, db_conn_t *conn) {
   unsigned long cnt = 0;
   unsigned long i = 0;
 
-  if (!conn) 
+  if (conn == NULL) {
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
+  }
 
   mysql = conn->mysql;
 
@@ -348,7 +349,7 @@ static modret_t *_build_data(cmd_rec *cmd, db_conn_t *conn) {
 
   result = mysql_store_result(mysql);
   if (!result) {
-    return _build_error(cmd, conn);
+    return build_error(cmd, conn);
   }
   
   sd = (sql_data_t *) pcalloc(cmd->tmp_pool, sizeof(sql_data_t));
@@ -366,8 +367,8 @@ static modret_t *_build_data(cmd_rec *cmd, db_conn_t *conn) {
   /* At this point either we finished correctly or an error occurred in the
    * fetch.  Do the right thing.
    */
-  if (mysql_errno(mysql)) {
-    mr = _build_error(cmd, conn);
+  if (mysql_errno(mysql) != 0) {
+    mr = build_error(cmd, conn);
     mysql_free_result(result);
     return mr;
   }
@@ -417,19 +418,20 @@ MODRET cmd_open(cmd_rec *cmd) {
 #ifdef PR_USE_NLS
   const char *encoding = NULL;
 #endif
+#ifdef HAVE_MYSQL_MYSQL_GET_SSL_CIPHER
+  const char *ssl_cipher = NULL;
+#endif
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_open");
 
-  _sql_check_cmd(cmd, "cmd_open");
+  sql_check_cmd(cmd, "cmd_open");
 
   if (cmd->argc < 1) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_open");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }    
 
-  /* get the named connection */
-
-  entry = _sql_get_connection(cmd->argv[0]);
+  entry = sql_get_connection(cmd->argv[0]);
   if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_open");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
@@ -505,20 +507,89 @@ MODRET cmd_open(cmd_rec *cmd) {
   client_flags |= CLIENT_MULTI_RESULTS;
 #endif
 
+#if defined(HAVE_MYSQL_MYSQL_SSL_SET)
+  /* Per the MySQL docs, this function always returns success.  Errors are
+   * reported when we actually attempt to connect.
+   *
+   * Note: There are some other TLS-related options, in newer versions of
+   * MySQL, which might be interest (although they require the use of the
+   * mysql_options() function, not mysql_ssl_set()):
+   *
+   *  MYSQL_OPT_SSL_ENFORCE (boolean, defaults to 'false')
+   *  MYSQL_OPT_SSL_VERIFY_SERVER_CERT (boolean, defaults to 'false')
+   *  MYSQL_OPT_TLS_VERSION (char *, for configuring the protocol versions)
+   */
+  (void) mysql_ssl_set(conn->mysql, conn->ssl_key_file, conn->ssl_cert_file,
+    conn->ssl_ca_file, conn->ssl_ca_dir, conn->ssl_ciphers);
+#endif
+
   if (!mysql_real_connect(conn->mysql, conn->host, conn->user, conn->pass,
       conn->db, (int) strtol(conn->port, (char **) NULL, 10),
       conn->unix_sock, client_flags)) {
+    modret_t *mr = NULL;
 
     /* If it didn't work, return an error. */
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_open");
-    return _build_error(cmd, conn);
+    mr = build_error(cmd, conn);
+
+    /* Since we failed to connect here, avoid a memory leak by freeing up the
+     * mysql conn struct.
+     */
+    mysql_close(conn->mysql);
+    conn->mysql = NULL;
+
+    return mr;
   }
 
   sql_log(DEBUG_FUNC, "MySQL client version: %s", mysql_get_client_info());
   sql_log(DEBUG_FUNC, "MySQL server version: %s",
     mysql_get_server_info(conn->mysql));
 
-#ifdef PR_USE_NLS
+# if MYSQL_VERSION_ID >= 50703 && defined(HAVE_MYSQL_GET_OPTION)
+  /* Log the configured authentication plugin, if any.  For example, it
+   * might be set in the my.cnf file using:
+   *
+   *   [client]
+   *   default-auth = mysql_native_password
+   *
+   * Note: the mysql_get_option() function appeared in MySQL 5.7.3, as per:
+   *
+   *  https://dev.mysql.com/doc/refman/5.7/en/mysql-get-option.html
+   *
+   * The MYSQL_DEFAULT_AUTH value is an enum, not a #define, so we cannot
+   * use a simple #ifdef here.
+   */
+  {
+    const char *auth_plugin = NULL;
+
+    if (mysql_get_option(conn->mysql, MYSQL_DEFAULT_AUTH, &auth_plugin) == 0) {
+      /* There may not have been a default auth plugin explicitly configured,
+       * and the MySQL internals themselves may not set one.  So it is not
+       * surprising if the pointer remains null.
+       */
+      if (auth_plugin != NULL) {
+        sql_log(DEBUG_FUNC, "MySQL client default authentication plugin: %s",
+          auth_plugin);
+      }
+    }
+  }
+#endif /* MySQL 5.7.3 and later */
+
+#if defined(HAVE_MYSQL_MYSQL_GET_SSL_CIPHER)
+  ssl_cipher = mysql_get_ssl_cipher(conn->mysql);
+  /* XXX Should we fail the connection here, if we expect an SSL session to
+   * have been successfully completed/required?
+   */
+  if (ssl_cipher != NULL) {
+    sql_log(DEBUG_FUNC, "%s", "MySQL SSL connection: true");
+    sql_log(DEBUG_FUNC, "MySQL SSL cipher: %s", ssl_cipher);
+
+  } else {
+    sql_log(DEBUG_FUNC, "%s", "MySQL SSL connection: false");
+  }
+#endif
+
+#if defined(PR_USE_NLS)
   encoding = pr_encode_get_encoding();
   if (encoding != NULL) {
 
@@ -618,9 +689,9 @@ MODRET cmd_open(cmd_rec *cmd) {
 
   /* return HANDLED */
   sql_log(DEBUG_INFO, "connection '%s' opened", entry->name);
-
   sql_log(DEBUG_INFO, "connection '%s' count is now %d", entry->name,
     entry->connections);
+  pr_event_generate("mod_sql.db.connection-opened", &sql_mysql_module);
 
   sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_open");
   return PR_HANDLED(cmd);
@@ -656,15 +727,15 @@ MODRET cmd_close(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_close");
 
-  _sql_check_cmd(cmd, "cmd_close");
+  sql_check_cmd(cmd, "cmd_close");
 
   if ((cmd->argc < 1) || (cmd->argc > 2)) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_close");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  if (!(entry = _sql_get_connection( cmd->argv[0] ))) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_close");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
   }
@@ -684,9 +755,11 @@ MODRET cmd_close(cmd_rec *cmd) {
    * close the connection, explicitly set the counter to 0, and remove any
    * timers.
    */
-  if (((--entry->connections) == 0 ) || ((cmd->argc == 2) && (cmd->argv[1]))) {
-    mysql_close(conn->mysql);
-    conn->mysql = NULL;
+  if (((--entry->connections) == 0) || ((cmd->argc == 2) && (cmd->argv[1]))) {
+    if (conn->mysql != NULL) {
+      mysql_close(conn->mysql);
+      conn->mysql = NULL;
+    }
     entry->connections = 0;
 
     if (entry->timer) {
@@ -696,6 +769,7 @@ MODRET cmd_close(cmd_rec *cmd) {
     }
 
     sql_log(DEBUG_INFO, "connection '%s' closed", entry->name);
+    pr_event_generate("mod_sql.db.connection-closed", &sql_mysql_module);
   }
 
   sql_log(DEBUG_INFO, "connection '%s' count is now %d", entry->name,
@@ -705,8 +779,7 @@ MODRET cmd_close(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/*
- * cmd_defineconnection: takes all information about a database
+/* cmd_defineconnection: takes all information about a database
  *  connection and stores it for later use.
  *
  * Inputs:
@@ -714,8 +787,14 @@ MODRET cmd_close(cmd_rec *cmd) {
  *  cmd->argv[1]: username portion of the SQLConnectInfo directive
  *  cmd->argv[2]: password portion of the SQLConnectInfo directive
  *  cmd->argv[3]: info portion of the SQLConnectInfo directive
+ *
  * Optional:
  *  cmd->argv[4]: time-to-live in seconds
+ *  cmd->argv[5]: SSL client cert file
+ *  cmd->argv[6]: SSL client key file
+ *  cmd->argv[7]: SSL CA file
+ *  cmd->argv[8]: SSL CA directory
+ *  cmd->argv[9]: SSL ciphers
  *
  * Returns:
  *  either a properly filled error modret_t if the connection could not
@@ -729,32 +808,26 @@ MODRET cmd_close(cmd_rec *cmd) {
  *  associated timer.
  */
 MODRET cmd_defineconnection(cmd_rec *cmd) {
-  char *info = NULL;
-  char *name = NULL;
-
-  char *db = NULL;
-  char *host = NULL;
-  char *port = NULL;
-
-  char *havehost = NULL;
-  char *haveport = NULL;
-
+  char *have_host = NULL, *have_port = NULL, *info = NULL, *name = NULL;
+  const char *db = NULL, *host = NULL, *port = NULL;
+  const char *ssl_cert_file = NULL, *ssl_key_file = NULL, *ssl_ca_file = NULL;
+  const char *ssl_ca_dir = NULL, *ssl_ciphers = NULL;
   conn_entry_t *entry = NULL;
   db_conn_t *conn = NULL; 
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_defineconnection");
 
-  _sql_check_cmd(cmd, "cmd_defineconnection");
+  sql_check_cmd(cmd, "cmd_defineconnection");
 
   if (cmd->argc < 4 ||
-      cmd->argc > 5 ||
+      cmd->argc > 10 ||
       !cmd->argv[0]) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_defineconnection");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  if (!conn_pool) {
-    pr_log_pri(PR_LOG_WARNING, "warning: the mod_sql_mysql module has not been "
+  if (conn_pool == NULL) {
+    pr_log_pri(PR_LOG_WARNING, "WARNING: the mod_sql_mysql module has not been "
       "properly initialized.  Please make sure your --with-modules configure "
       "option lists mod_sql *before* mod_sql_mysql, and recompile.");
 
@@ -776,27 +849,27 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
 
   db = pstrdup(cmd->tmp_pool, info);
 
-  havehost = strchr(db, '@');
-  haveport = strchr(db, ':');
+  have_host = strchr(db, '@');
+  have_port = strchr(db, ':');
 
-  /* If haveport, parse it, otherwise default it. 
-   * If haveport, set it to '\0'.
+  /* If have_port, parse it, otherwise default it.
+   * If have_port, set it to '\0'.
    *
-   * If havehost, parse it, otherwise default it.
-   * If havehost, set it to '\0'.
+   * If have_host, parse it, otherwise default it.
+   * If have_host, set it to '\0'.
    */
 
-  if (haveport) {
-    port = haveport + 1;
-    *haveport = '\0';
+  if (have_port != NULL) {
+    port = have_port + 1;
+    *have_port = '\0';
 
   } else {
     port = _MYSQL_PORT;
   }
 
-  if (havehost) {
-    host = havehost + 1;
-    *havehost = '\0';
+  if (have_host != NULL) {
+    host = have_host + 1;
+    *have_host = '\0';
 
   } else {
     host = "localhost";
@@ -813,18 +886,53 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
     conn->host = pstrdup(conn_pool, host);
   }
 
-  conn->db   = pstrdup(conn_pool, db);
+  conn->db = pstrdup(conn_pool, db);
   conn->port = pstrdup(conn_pool, port);
 
-  /* Insert the new conn_info into the connection hash */
-  entry = _sql_add_connection(conn_pool, name, (void *) conn);
-  if (!entry) {
+  /* SSL parameters, if configured. */
+  if (cmd->argc >= 6) {
+    ssl_cert_file = cmd->argv[5];
+    if (ssl_cert_file != NULL) {
+      conn->ssl_cert_file = pstrdup(conn_pool, ssl_cert_file);
+    }
+  }
+
+  if (cmd->argc >= 7) {
+    ssl_key_file = cmd->argv[6];
+    if (ssl_key_file != NULL) {
+      conn->ssl_key_file = pstrdup(conn_pool, ssl_key_file);
+    }
+  }
+
+  if (cmd->argc >= 8) {
+    ssl_ca_file = cmd->argv[7];
+    if (ssl_ca_file != NULL) {
+      conn->ssl_ca_file = pstrdup(conn_pool, ssl_ca_file);
+    }
+  }
+
+  if (cmd->argc >= 9) {
+    ssl_ca_dir = cmd->argv[8];
+    if (ssl_ca_dir != NULL) {
+      conn->ssl_ca_dir = pstrdup(conn_pool, ssl_ca_dir);
+    }
+  }
+
+  if (cmd->argc >= 10) {
+    ssl_ciphers = cmd->argv[9];
+    if (ssl_ciphers != NULL) {
+      conn->ssl_ciphers = pstrdup(conn_pool, ssl_ciphers);
+    }
+  }
+
+  entry = sql_add_connection(conn_pool, name, (void *) conn);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_defineconnection");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION,
       "named connection already exists");
   }
 
-  if (cmd->argc == 5) { 
+  if (cmd->argc >= 5) {
     entry->ttl = (int) strtol(cmd->argv[4], (char **) NULL, 10);
     if (entry->ttl >= 1) {
       pr_sql_conn_policy = SQL_CONN_POLICY_TIMER;
@@ -840,10 +948,10 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
   sql_log(DEBUG_INFO, "  name: '%s'", entry->name);
   sql_log(DEBUG_INFO, "  user: '%s'", conn->user);
 
-  if (conn->host) {
+  if (conn->host != NULL) {
     sql_log(DEBUG_INFO, "  host: '%s'", conn->host);
 
-  } else if (conn->unix_sock) {
+  } else if (conn->unix_sock != NULL) {
     sql_log(DEBUG_INFO, "socket: '%s'", conn->unix_sock);
   }
 
@@ -851,6 +959,26 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
   sql_log(DEBUG_INFO, "  port: '%s'", conn->port);
   sql_log(DEBUG_INFO, "   ttl: '%d'", entry->ttl);
 
+  if (conn->ssl_cert_file != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: client cert = '%s'", conn->ssl_cert_file);
+  }
+
+  if (conn->ssl_key_file != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: client key = '%s'", conn->ssl_key_file);
+  }
+
+  if (conn->ssl_ca_file != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: CA file = '%s'", conn->ssl_ca_file);
+  }
+
+  if (conn->ssl_ca_dir != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: CA dir = '%s'", conn->ssl_ca_dir);
+  }
+
+  if (conn->ssl_ciphers != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: ciphers = '%s'", conn->ssl_ciphers);
+  }
+
   sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_defineconnection");
   return PR_HANDLED(cmd);
 }
@@ -870,17 +998,19 @@ static modret_t *cmd_exit(cmd_rec *cmd) {
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_exit");
 
   for (i = 0; i < conn_cache->nelts; i++) {
-    conn_entry_t *entry = ((conn_entry_t **) conn_cache->elts)[i];
+    conn_entry_t *entry;
 
+    entry = ((conn_entry_t **) conn_cache->elts)[i];
     if (entry->connections > 0) {
-      cmd_rec *close_cmd = _sql_make_cmd(conn_pool, 2, entry->name, "1");
+      cmd_rec *close_cmd;
+
+      close_cmd = sql_make_cmd(conn_pool, 2, entry->name, "1");
       cmd_close(close_cmd);
       destroy_pool(close_cmd->pool);
     }
   }
 
   sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_exit");
-
   return PR_HANDLED(cmd);
 }
 
@@ -936,21 +1066,20 @@ MODRET cmd_select(cmd_rec *cmd) {
   modret_t *cmr = NULL;
   modret_t *dmr = NULL;
   char *query = NULL;
-  int cnt = 0;
+  unsigned long cnt = 0;
   cmd_rec *close_cmd;
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_select");
 
-  _sql_check_cmd(cmd, "cmd_select");
+  sql_check_cmd(cmd, "cmd_select");
 
   if (cmd->argc < 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_select");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_select");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
   }
@@ -971,21 +1100,21 @@ MODRET cmd_select(cmd_rec *cmd) {
     query = pstrcat(cmd->tmp_pool, cmd->argv[2], " FROM ", cmd->argv[1], NULL);
 
     if (cmd->argc > 3 &&
-        cmd->argv[3])
+        cmd->argv[3]) {
       query = pstrcat(cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL);
+    }
 
     if (cmd->argc > 4 &&
-        cmd->argv[4])
+        cmd->argv[4]) {
       query = pstrcat(cmd->tmp_pool, query, " LIMIT ", cmd->argv[4], NULL);
+    }
 
     if (cmd->argc > 5) {
-
       /* Handle the optional arguments -- they're rare, so in this case
        * we'll play with the already constructed query string, but in 
        * general we should probably take optional arguments into account 
        * and put the query string together later once we know what they are.
        */
-    
       for (cnt = 5; cnt < cmd->argc; cnt++) {
 	if (cmd->argv[cnt] &&
             strcasecmp("DISTINCT", cmd->argv[cnt]) == 0) {
@@ -1003,10 +1132,10 @@ MODRET cmd_select(cmd_rec *cmd) {
   /* Perform the query.  if it doesn't work, log the error, close the
    * connection then return the error from the query processing.
    */
-  if (mysql_real_query(conn->mysql, query, strlen(query))) {
-    dmr = _build_error(cmd, conn);
+  if (mysql_real_query(conn->mysql, query, strlen(query)) != 0) {
+    dmr = build_error(cmd, conn);
 
-    close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -1017,19 +1146,19 @@ MODRET cmd_select(cmd_rec *cmd) {
   /* Get the data. if it doesn't work, log the error, close the
    * connection then return the error from the data processing.
    */
-  dmr = _build_data(cmd, conn);
+  dmr = build_data(cmd, conn);
   if (MODRET_ERROR(dmr)) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_select");
 
-    close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
     return dmr;
-  }    
+  }
 
   /* close the connection, return the data. */
-  close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
  
@@ -1078,16 +1207,15 @@ MODRET cmd_insert(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_insert");
 
-  _sql_check_cmd(cmd, "cmd_insert");
+  sql_check_cmd(cmd, "cmd_insert");
 
   if ((cmd->argc != 2) && (cmd->argc != 4)) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_insert");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_insert");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
   }
@@ -1103,23 +1231,22 @@ MODRET cmd_insert(cmd_rec *cmd) {
   /* construct the query string */
   if (cmd->argc == 2) {
     query = pstrcat(cmd->tmp_pool, "INSERT ", cmd->argv[1], NULL);
+
   } else {
-    query = pstrcat( cmd->tmp_pool, "INSERT INTO ", cmd->argv[1], " (",
-		     cmd->argv[2], ") VALUES (", cmd->argv[3], ")",
-		     NULL );
+    query = pstrcat(cmd->tmp_pool, "INSERT INTO ", cmd->argv[1], " (",
+      cmd->argv[2], ") VALUES (", cmd->argv[3], ")", NULL);
   }
 
-  /* log the query string */
   sql_log(DEBUG_INFO, "query \"%s\"", query);
 
   /* perform the query.  if it doesn't work, log the error, close the
    * connection (and log any errors there, too) then return the error
    * from the query processing.
    */
-  if (mysql_real_query(conn->mysql, query, strlen(query))) {
-    dmr = _build_error(cmd, conn);
+  if (mysql_real_query(conn->mysql, query, strlen(query)) != 0) {
+    dmr = build_error(cmd, conn);
 
-    close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -1128,7 +1255,7 @@ MODRET cmd_insert(cmd_rec *cmd) {
   }
 
   /* close the connection and return HANDLED. */
-  close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1176,16 +1303,15 @@ MODRET cmd_update(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_update");
 
-  _sql_check_cmd(cmd, "cmd_update");
+  sql_check_cmd(cmd, "cmd_update");
 
   if ((cmd->argc < 2) || (cmd->argc > 4)) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_update");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_update");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
   }
@@ -1200,12 +1326,14 @@ MODRET cmd_update(cmd_rec *cmd) {
 
   if (cmd->argc == 2) {
     query = pstrcat(cmd->tmp_pool, "UPDATE ", cmd->argv[1], NULL);
+
   } else {
-    /* construct the query string */
-    query = pstrcat( cmd->tmp_pool, "UPDATE ", cmd->argv[1], " SET ",
-		     cmd->argv[2], NULL );
-    if ((cmd->argc > 3) && (cmd->argv[3]))
-      query = pstrcat( cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL );
+    query = pstrcat(cmd->tmp_pool, "UPDATE ", cmd->argv[1], " SET ",
+      cmd->argv[2], NULL);
+    if (cmd->argc > 3 &&
+        cmd->argv[3]) {
+      query = pstrcat(cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL);
+    }
   }
 
   /* Log the query string */
@@ -1214,10 +1342,10 @@ MODRET cmd_update(cmd_rec *cmd) {
   /* Perform the query.  if it doesn't work close the connection, then
    * return the error from the query processing.
    */
-  if (mysql_real_query(conn->mysql, query, strlen(query))) {
-    dmr = _build_error(cmd, conn);
+  if (mysql_real_query(conn->mysql, query, strlen(query)) != 0) {
+    dmr = build_error(cmd, conn);
 
-    close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -1226,7 +1354,7 @@ MODRET cmd_update(cmd_rec *cmd) {
   }
 
   /* Close the connection, return HANDLED.  */
-  close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1255,7 +1383,7 @@ MODRET cmd_update(cmd_rec *cmd) {
 MODRET cmd_procedure(cmd_rec *cmd) {
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_procedure");
 
-  _sql_check_cmd(cmd, "cmd_procedure");
+  sql_check_cmd(cmd, "cmd_procedure");
 
   if (cmd->argc != 3) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_procedure");
@@ -1299,16 +1427,15 @@ MODRET cmd_query(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_query");
 
-  _sql_check_cmd(cmd, "cmd_query");
+  sql_check_cmd(cmd, "cmd_query");
 
   if (cmd->argc != 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_query");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_query");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
   }
@@ -1329,10 +1456,10 @@ MODRET cmd_query(cmd_rec *cmd) {
   /* Perform the query.  if it doesn't work close the connection, then
    * return the error from the query processing.
    */
-  if (mysql_real_query(conn->mysql, query, strlen(query))) {
-    dmr = _build_error(cmd, conn);
+  if (mysql_real_query(conn->mysql, query, strlen(query)) != 0) {
+    dmr = build_error(cmd, conn);
     
-    close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
     
@@ -1344,8 +1471,8 @@ MODRET cmd_query(cmd_rec *cmd) {
    * connection then return the error from the data processing.
    */
 
-  if (mysql_field_count(conn->mysql)) {
-    dmr = _build_data(cmd, conn);
+  if (mysql_field_count(conn->mysql) > 0) {
+    dmr = build_data(cmd, conn);
     if (MODRET_ERROR(dmr)) {
       sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_query");
     }
@@ -1355,7 +1482,7 @@ MODRET cmd_query(cmd_rec *cmd) {
   }
   
   /* close the connection, return the data. */
-  close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1399,16 +1526,15 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_escapestring");
 
-  _sql_check_cmd(cmd, "cmd_escapestring");
+  sql_check_cmd(cmd, "cmd_escapestring");
 
   if (cmd->argc != 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_escapestring");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_escapestring");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
   }
@@ -1436,7 +1562,7 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
   mysql_escape_string(escaped, unescaped, strlen(unescaped));
 #endif
 
-  close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1446,12 +1572,12 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
 
 /*
  * cmd_checkauth: some backend databases may provide backend-specific
- *  methods to check passwords.  This function takes a cleartext password
+ *  methods to check passwords.  This function takes a plaintext password
  *  and a hashed password and checks to see if they are the same.
  *
  * Inputs:
  *  cmd->argv[0]: connection name
- *  cmd->argv[1]: cleartext string
+ *  cmd->argv[1]: plaintext string
  *  cmd->argv[2]: hashed string
  *
  * Returns:
@@ -1466,16 +1592,151 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
  *  If this backend does not provide this functionality, this cmd *must*
  *  return ERROR.
  */
+
+/* Per the MySQL docs for the PASSWORD function, MySQL pre-4.1 passwords
+ * are always 16 bytes; MySQL 4.1 passwords are 41 bytes AND start with '*'.
+ * See:
+ *   http://dev.mysql.com/doc/refman/5.7/en/encryption-functions.html#function_password
+ */
+
+#define MYSQL_PASSWD_FMT_UNKNOWN	-1
+#define MYSQL_PASSWD_FMT_PRE41		1
+#define MYSQL_PASSWD_FMT_41		2
+#define MYSQL_PASSWD_FMT_SHA256		3
+
+static int get_mysql_passwd_fmt(const char *txt, size_t txt_len) {
+  if (txt_len == 16) {
+    return MYSQL_PASSWD_FMT_PRE41;
+  }
+
+  if (txt_len == 41 &&
+      txt[0] == '*') {
+    return MYSQL_PASSWD_FMT_41;
+  }
+
+  if (txt_len > 3 &&
+      txt[0] == '$' &&
+      txt[1] == '5' &&
+      txt[2] == '$') {
+    return MYSQL_PASSWD_FMT_SHA256;
+  }
+
+  return MYSQL_PASSWD_FMT_UNKNOWN;
+}
+
+static int match_mysql_passwds(const char *hashed, size_t hashed_len,
+    const char *scrambled, size_t scrambled_len, const char *scramble_func) {
+  int hashed_fmt = 0, scrambled_fmt = 0, matched = FALSE;
+
+  if (pr_trace_get_level(trace_channel) >= 7) {
+    const char *hashed_fmt_name, *scrambled_fmt_name;
+
+    hashed_fmt = get_mysql_passwd_fmt(hashed, hashed_len);
+    scrambled_fmt = get_mysql_passwd_fmt(scrambled, scrambled_len);
+
+    switch (hashed_fmt) {
+      case MYSQL_PASSWD_FMT_PRE41:
+        hashed_fmt_name = "pre-4.1";
+        break;
+
+      case MYSQL_PASSWD_FMT_41:
+        hashed_fmt_name = "4.1";
+        break;
+
+      case MYSQL_PASSWD_FMT_SHA256:
+        hashed_fmt_name = "SHA256";
+        break;
+
+      default:
+        hashed_fmt_name = "unknown";
+        break;
+    }
+
+    switch (scrambled_fmt) {
+      case MYSQL_PASSWD_FMT_PRE41:
+        scrambled_fmt_name = "pre-4.1";
+        break;
+
+      case MYSQL_PASSWD_FMT_41:
+        scrambled_fmt_name = "4.1";
+        break;
+
+      case MYSQL_PASSWD_FMT_SHA256:
+        scrambled_fmt_name = "SHA256";
+        break;
+
+      default:
+        scrambled_fmt_name = "unknown";
+        break;
+    }
+
+    pr_trace_msg(trace_channel, 7,
+      "SQLAuthType Backend: database password format = %s, "
+      "client library password format = %s (using %s())", hashed_fmt_name,
+      scrambled_fmt_name, scramble_func);
+  }
+
+  /* Note here that if the scrambled value has a different length than our
+   * expected hash, it might be a completely different format (i.e. not the
+   * 4.1 or whatever format provided by the db).  Log if this the case!
+   *
+   * Consider that using PASSWORD() on the server might make a 4.1 format
+   * value, but the client lib might make a SHA256 format value.  Or
+   * vice versa.
+   */
+  if (scrambled_len == hashed_len) {
+    matched = (strncmp(scrambled, hashed, hashed_len) == 0);
+  }
+
+  if (matched == FALSE) {
+    if (hashed_fmt == 0) {
+      hashed_fmt = get_mysql_passwd_fmt(hashed, hashed_len);
+    }
+
+    if (scrambled_fmt == 0) {
+      scrambled_fmt = get_mysql_passwd_fmt(scrambled, scrambled_len);
+    }
+
+    if (hashed_fmt != scrambled_fmt) {
+      if (scrambled_fmt == MYSQL_PASSWD_FMT_SHA256) {
+        sql_log(DEBUG_FUNC, "MySQL client library used MySQL SHA256 password format, and Backend SQLAuthType cannot succeed; consider using MD5/SHA1/SHA256 SQLAuthType using mod_sql_passwd");
+        switch (hashed_fmt) {
+          case MYSQL_PASSWD_FMT_PRE41:
+            sql_log(DEBUG_FUNC, "MySQL server used MySQL pre-4.1 password format for PASSWORD() value");
+            break;
+
+          case MYSQL_PASSWD_FMT_41:
+            sql_log(DEBUG_FUNC, "MySQL server used MySQL 4.1 password format for PASSWORD() value");
+            break;
+
+          default:
+            pr_trace_msg(trace_channel, 19,
+              "unknown MySQL PASSWORD() format used on server");
+            break;
+        }
+      }
+    }
+
+    pr_trace_msg(trace_channel, 9,
+      "expected '%.*s' (%lu), got '%.*s' (%lu) using MySQL %s()",
+      (int) hashed_len, hashed, (unsigned long) hashed_len,
+      (int) scrambled_len, scrambled, (unsigned long) scrambled_len,
+      scramble_func);
+  }
+
+  return matched;
+}
+
 MODRET cmd_checkauth(cmd_rec *cmd) {
   conn_entry_t *entry = NULL;
-  char scrambled[256]={'\0'};
-  char *c_clear = NULL;
-  char *c_hash = NULL;
+  char scrambled[256] = {'\0'};
+  char *plaintxt = NULL, *hashed = NULL;
+  size_t plaintxt_len = 0, hashed_len = 0, scrambled_len = 0;
   int success = 0;
 
   sql_log(DEBUG_FUNC, "%s", "entering \tmysql cmd_checkauth");
 
-  _sql_check_cmd(cmd, "cmd_checkauth");
+  sql_check_cmd(cmd, "cmd_checkauth");
 
   if (cmd->argc != 3) {
     sql_log(DEBUG_FUNC, "exiting \tmysql cmd_checkauth");
@@ -1483,7 +1744,7 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
   }
 
   /* get the named connection -- not used in this case, but for consistency */
-  entry = _sql_get_connection(cmd->argv[0]);
+  entry = sql_get_connection(cmd->argv[0]);
   if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tmysql cmd_checkauth");
     return PR_ERROR_MSG(cmd, MOD_SQL_MYSQL_VERSION, "unknown named connection");
@@ -1494,8 +1755,10 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
     return PR_ERROR_INT(cmd, PR_AUTH_NOPWD);
   }
 
-  c_clear = cmd->argv[1];
-  c_hash = cmd->argv[2];
+  plaintxt = cmd->argv[1];
+  plaintxt_len = strlen(plaintxt);
+  hashed = cmd->argv[2];
+  hashed_len = strlen(hashed);
 
   /* Checking order (damn MySQL API changes):
    *
@@ -1504,12 +1767,16 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
    *  make_scrambled_password (if available)
    *  make_scrammbed_password_323 (if available)
    */
+
 #if defined(HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD)
   if (success == FALSE) {
     memset(scrambled, '\0', sizeof(scrambled));
 
-    my_make_scrambled_password(scrambled, c_clear, strlen(c_clear));
-    success = (strcmp(scrambled, c_hash) == 0);
+    my_make_scrambled_password(scrambled, plaintxt, plaintxt_len);
+    scrambled_len = strlen(scrambled);
+
+    success = match_mysql_passwds(hashed, hashed_len, scrambled, scrambled_len,
+      "my_make_scrambled_password");
   }
 #endif /* HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD */
 
@@ -1522,8 +1789,11 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
     sql_log(DEBUG_FUNC, "%s",
       "warning: support for this legacy MySQ-3.xL password algorithm will be dropped from MySQL in the future");
 
-    my_make_scrambled_password_323(scrambled, c_clear, strlen(c_clear));
-    success = (strcmp(scrambled, c_hash) == 0);
+    my_make_scrambled_password_323(scrambled, plaintxt, plaintxt_len);
+    scrambled_len = strlen(scrambled);
+
+    success = match_mysql_passwds(hashed, hashed_len, scrambled, scrambled_len,
+      "my_make_scrambled_password_323");
   }
 #endif /* HAVE_MYSQL_MY_MAKE_SCRAMBLED_PASSWORD_323 */
 
@@ -1532,11 +1802,14 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
     memset(scrambled, '\0', sizeof(scrambled));
 
 # if MYSQL_VERSION_ID >= 40100 && MYSQL_VERSION_ID < 40101
-    make_scrambled_password(scrambled, c_clear, 1, NULL);
+    make_scrambled_password(scrambled, plaintxt, 1, NULL);
 # else
-    make_scrambled_password(scrambled, c_clear);
+    make_scrambled_password(scrambled, plaintxt);
 # endif
-    success = (strcmp(scrambled, c_hash) == 0);
+    scrambled_len = strlen(scrambled);
+
+    success = match_mysql_passwds(hashed, hashed_len, scrambled, scrambled_len,
+      "make_scrambled_password");
   }
 #endif /* HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD */
 
@@ -1549,8 +1822,11 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
     sql_log(DEBUG_FUNC, "%s",
       "warning: support for this legacy MySQ-3.xL password algorithm will be dropped from MySQL in the future");
 
-    make_scrambled_password_323(scrambled, c_clear);
-    success = (strcmp(scrambled, c_hash) == 0);
+    make_scrambled_password_323(scrambled, plaintxt);
+    scrambled_len = strlen(scrambled);
+
+    success = match_mysql_passwds(hashed, hashed_len, scrambled, scrambled_len,
+      "make_scrambled_password_323");
   }
 #endif /* HAVE_MYSQL_MAKE_SCRAMBLED_PASSWORD_323 */
 
@@ -1585,10 +1861,10 @@ MODRET cmd_checkauth(cmd_rec *cmd) {
 MODRET cmd_identify(cmd_rec * cmd) {
   sql_data_t *sd = NULL;
 
-  _sql_check_cmd(cmd, "cmd_identify");
+  sql_check_cmd(cmd, "cmd_identify");
 
-  sd = (sql_data_t *) pcalloc( cmd->tmp_pool, sizeof(sql_data_t));
-  sd->data = (char **) pcalloc( cmd->tmp_pool, sizeof(char *) * 2);
+  sd = (sql_data_t *) pcalloc(cmd->tmp_pool, sizeof(sql_data_t));
+  sd->data = (char **) pcalloc(cmd->tmp_pool, sizeof(char *) * 2);
 
   sd->rnum = 1;
   sd->fnum = 2;
diff --git a/contrib/mod_sql_odbc.c b/contrib/mod_sql_odbc.c
index 396d31b..db1ed34 100644
--- a/contrib/mod_sql_odbc.c
+++ b/contrib/mod_sql_odbc.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_sql_odbc -- Support for connecting to databases via ODBC
- *
- * Copyright (c) 2003-2013 TJ Saunders
+ * Copyright (c) 2003-2017 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
@@ -20,18 +19,16 @@
  * As a special exemption, TJ Saunders gives permission to link this program
  * with OpenSSL, and distribute the resulting executable, without including
  * the source code for OpenSSL in the source distribution.
- *
- * $Id: mod_sql_odbc.c,v 1.15 2013-09-25 04:25:54 castaglia Exp $
  */
 
 #include "conf.h"
 #include "mod_sql.h"
 
-#define MOD_SQL_ODBC_VERSION    "mod_sql_odbc/0.3.3"
+#define MOD_SQL_ODBC_VERSION    "mod_sql_odbc/0.3.4"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030001
-# error "ProFTPD 1.3.0rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 #include "sql.h"
@@ -74,6 +71,8 @@ typedef struct conn_entry_struct {
 
 static pool *conn_pool = NULL;
 static array_header *conn_cache = NULL;
+static int odbc_version = SQL_OV_ODBC3;
+static const char *odbc_version_str = "ODBCv3";
 
 /* Default to using the LIMIT clause.  Some database drivers prefer
  * ROWNUM (e.g. Oracle), and others prefer TOP (e.g. TDS).
@@ -198,7 +197,7 @@ static void sqlodbc_escape_string(char *to, const char *from, size_t fromlen) {
         break;
 
       case '\'':
-        *to++ = '\\';
+        *to++ = '\'';
         *to++ = '\'';
         break;
 
@@ -832,10 +831,10 @@ MODRET sqlodbc_open(cmd_rec *cmd) {
     }
 
     res = SQLSetEnvAttr(conn->envh, SQL_ATTR_ODBC_VERSION,
-      (SQLPOINTER) SQL_OV_ODBC3, 0);
+      (SQLPOINTER) odbc_version, 0);
     if (res != SQL_SUCCESS) {
-      sql_log(DEBUG_WARN, "error setting SQL_ATTR_ODBC_VERSION ODBC3: %s",
-        sqlodbc_strerror(res));
+      sql_log(DEBUG_WARN, "error setting SQL_ATTR_ODBC_VERSION %s: %s",
+        odbc_version_str, sqlodbc_strerror(res));
       sql_log(DEBUG_FUNC, "%s", "exiting \todbc cmd_open");
       return sqlodbc_get_error(cmd, SQL_HANDLE_ENV, conn->envh);
     }
@@ -989,6 +988,7 @@ MODRET sqlodbc_open(cmd_rec *cmd) {
   sql_log(DEBUG_INFO, "'%s' connection opened", entry->name);
   sql_log(DEBUG_INFO, "'%s' connection count is now %u", entry->name,
     entry->nconn);
+  pr_event_generate("mod_sql.db.connection-opened", &sql_odbc_module);
 
   sql_log(DEBUG_FUNC, "%s", "exiting \todbc cmd_open");
   return PR_HANDLED(cmd);
@@ -1059,6 +1059,7 @@ MODRET sqlodbc_close(cmd_rec *cmd) {
     }
 
     sql_log(DEBUG_INFO, "'%s' connection closed", entry->name);
+    pr_event_generate("mod_sql.db.connection-closed", &sql_odbc_module);
   }
 
   sql_log(DEBUG_INFO, "'%s' connection count is now %u", entry->name,
@@ -1075,7 +1076,9 @@ MODRET sqlodbc_def_conn(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \todbc cmd_defineconnection");
 
-  if (cmd->argc < 4 || cmd->argc > 5 || !cmd->argv[0]) {
+  if (cmd->argc < 4 ||
+      cmd->argc > 10 ||
+      !cmd->argv[0]) {
     sql_log(DEBUG_FUNC, "%s", "exiting \todbc cmd_defineconnection");
     return PR_ERROR_MSG(cmd, MOD_SQL_ODBC_VERSION, "badly formed request");
   }
@@ -1095,7 +1098,7 @@ MODRET sqlodbc_def_conn(cmd_rec *cmd) {
       "named connection already exists");
   }
 
-  if (cmd->argc == 5) { 
+  if (cmd->argc >= 5) {
     entry->ttl = (int) strtol(cmd->argv[4], (char **) NULL, 10);
     if (entry->ttl >= 1) {
       pr_sql_conn_policy = SQL_CONN_POLICY_TIMER;
@@ -1568,7 +1571,6 @@ MODRET sqlodbc_query(cmd_rec *cmd) {
 
 MODRET sqlodbc_quote(cmd_rec *cmd) {
   conn_entry_t *entry = NULL;
-  db_conn_t *conn = NULL;
   modret_t *mr = NULL;
   char *unescaped = NULL;
   char *escaped = NULL;
@@ -1595,8 +1597,6 @@ MODRET sqlodbc_quote(cmd_rec *cmd) {
     return mr;
   }
 
-  conn = (db_conn_t *) entry->data;
-
   unescaped = cmd->argv[1];
   escaped = (char *) pcalloc(cmd->tmp_pool, sizeof(char) * 
 			      (strlen(unescaped) * 2) + 1);
@@ -1646,7 +1646,7 @@ MODRET sqlodbc_checkauth(cmd_rec *cmd) {
   return PR_ERROR(cmd);
 }
 
-MODRET sqlodbc_identify(cmd_rec * cmd) {
+MODRET sqlodbc_identify(cmd_rec *cmd) {
   sql_data_t *sd = NULL;
 
   sd = (sql_data_t *) pcalloc(cmd->tmp_pool, sizeof(sql_data_t));
@@ -1679,6 +1679,53 @@ static cmdtable sqlodbc_cmdtable[] = {
   { 0, NULL }
 };
 
+/* Configuration handlers
+ */
+
+/* usage: SQLODBCVersion version */
+MODRET set_sqlodbcversion(cmd_rec *cmd) {
+  int version = -1;
+  const char *version_str;
+  config_rec *c;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
+
+  if (strcasecmp(cmd->argv[1], "2") == 0 ||
+      strcasecmp(cmd->argv[1], "odbcv2") == 0) {
+#if defined(SQL_OV_ODBC2)
+    version = SQL_OV_ODBC2;
+    version_str = "ODBCv2";
+#endif /* ODBCv2 */
+
+  } else if (strcasecmp(cmd->argv[1], "3") == 0 ||
+             strcasecmp(cmd->argv[1], "odbcv3") == 0) {
+#if defined(SQL_OV_ODBC3)
+    version = SQL_OV_ODBC3;
+    version_str = "ODBCv3";
+#endif /* ODBCv3 */
+
+  } else if (strcasecmp(cmd->argv[1], "3.80") == 0 ||
+             strcasecmp(cmd->argv[1], "odbcv3.80") == 0) {
+#if defined(SQL_OV_ODBC3_80)
+    version = SQL_OV_ODBC3_80;
+    version_str = "ODBCv3.80";
+#endif /* ODBCv3.80 */
+  }
+
+  if (version < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+      "unknown/supported ODBC API version: ", cmd->argv[1], NULL));
+  }
+
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = version;
+  c->argv[1] = pstrdup(c->pool, version_str);
+
+  return PR_HANDLED(cmd);
+}
+
 /* Event handlers
  */
 
@@ -1718,6 +1765,8 @@ static int sqlodbc_init(void) {
 }
 
 static int sqlodbc_sess_init(void) {
+  config_rec *c;
+
   if (conn_pool == NULL) {
     conn_pool = make_sub_pool(session.pool);
     pr_pool_tag(conn_pool, "ODBC connection pool");
@@ -1757,12 +1806,23 @@ static int sqlodbc_sess_init(void) {
 
   pr_proctitle_set("[accepting connections]");
 
+  c = find_config(main_server->conf, CONF_PARAM, "SQLODBCVersion", FALSE);
+  if (c != NULL) {
+    odbc_version = *((int *) c->argv[0]);
+    odbc_version_str = c->argv[1];
+  }
+
   return 0;
 }
 
 /* Module API tables
  */
 
+static conftable sqlodbc_conftab[] = {
+  { "SQLODBCVersion",	set_sqlodbcversion,	NULL },
+  { NULL, NULL, NULL }
+};
+
 module sql_odbc_module = {
   NULL, NULL,
 
@@ -1773,7 +1833,7 @@ module sql_odbc_module = {
   "sql_odbc",
 
   /* Module configuration directive table */
-  NULL,
+  sqlodbc_conftab,
 
   /* Module command handler table */
   NULL,
diff --git a/contrib/mod_sql_passwd.c b/contrib/mod_sql_passwd.c
index 097341e..ea0132a 100644
--- a/contrib/mod_sql_passwd.c
+++ b/contrib/mod_sql_passwd.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD: mod_sql_passwd -- Various SQL password handlers
- * Copyright (c) 2009-2016 TJ Saunders
+ * Copyright (c) 2009-2017 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
@@ -26,7 +26,17 @@
 #include "privs.h"
 #include "mod_sql.h"
 
-#define MOD_SQL_PASSWD_VERSION		"mod_sql_passwd/0.7"
+#define MOD_SQL_PASSWD_VERSION		"mod_sql_passwd/1.1"
+
+#ifdef PR_USE_SODIUM
+# include <sodium.h>
+/* Use/support Argon2, if libsodium is new enough. */
+# if SODIUM_LIBRARY_VERSION_MAJOR > 9 || \
+     (SODIUM_LIBRARY_VERSION_MAJOR == 9 && \
+      SODIUM_LIBRARY_VERSION_MINOR >= 2)
+#  define USE_SODIUM_ARGON2
+# endif
+#endif /* PR_USE_SODIUM */
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030302 
@@ -45,17 +55,26 @@ module sql_passwd_module;
 
 static int sql_passwd_engine = FALSE;
 
-#define SQL_PASSWD_USE_BASE64		1
-#define SQL_PASSWD_USE_HEX_LC		2
-#define SQL_PASSWD_USE_HEX_UC		3
-static unsigned int sql_passwd_encoding = SQL_PASSWD_USE_HEX_LC;
+#define SQL_PASSWD_COST_INTERACTIVE		1
+#define SQL_PASSWD_COST_SENSITIVE		2
+static unsigned int sql_passwd_cost = SQL_PASSWD_COST_INTERACTIVE;
+
+#define SQL_PASSWD_ENC_USE_BASE64		1
+#define SQL_PASSWD_ENC_USE_HEX_LC		2
+#define SQL_PASSWD_ENC_USE_HEX_UC		3
+#define SQL_PASSWD_ENC_USE_NONE			4
+static unsigned int sql_passwd_encoding = SQL_PASSWD_ENC_USE_HEX_LC;
+static unsigned int sql_passwd_salt_encoding = SQL_PASSWD_ENC_USE_NONE;
 
-static char *sql_passwd_salt = NULL;
-static size_t sql_passwd_salt_len = 0;
+static unsigned char *sql_passwd_file_salt = NULL;
+static size_t sql_passwd_file_salt_len = 0;
+static unsigned char *sql_passwd_user_salt = NULL;
+static size_t sql_passwd_user_salt_len = 0;
 
 #define SQL_PASSWD_SALT_FL_APPEND	0x0001
 #define SQL_PASSWD_SALT_FL_PREPEND	0x0002
-static unsigned long sql_passwd_salt_flags = SQL_PASSWD_SALT_FL_APPEND;
+static unsigned long sql_passwd_file_salt_flags = SQL_PASSWD_SALT_FL_APPEND;
+static unsigned long sql_passwd_user_salt_flags = SQL_PASSWD_SALT_FL_APPEND;
 
 #define SQL_PASSWD_OPT_HASH_SALT		0x0001
 #define SQL_PASSWD_OPT_ENCODE_SALT		0x0002
@@ -64,7 +83,7 @@ static unsigned long sql_passwd_salt_flags = SQL_PASSWD_SALT_FL_APPEND;
 
 static unsigned long sql_passwd_opts = 0UL;
 
-static unsigned int sql_passwd_nrounds = 1;
+static unsigned long sql_passwd_nrounds = 1;
 
 /* For PBKDF2 */
 static const EVP_MD *sql_passwd_pbkdf2_digest = NULL;
@@ -75,12 +94,30 @@ static int sql_passwd_pbkdf2_len = -1;
 #define SQL_PASSWD_ERR_PBKDF2_BAD_ROUNDS		-3
 #define SQL_PASSWD_ERR_PBKDF2_BAD_LENGTH		-4
 
-static const char *trace_channel = "sql_passwd";
+#ifdef PR_USE_SODIUM
+/* For Scrypt */
+# define SQL_PASSWD_SCRYPT_DEFAULT_HASH_SIZE	32U
+# define SQL_PASSWD_SCRYPT_DEFAULT_SALT_SIZE	32U
+static unsigned int sql_passwd_scrypt_hash_len = SQL_PASSWD_SCRYPT_DEFAULT_HASH_SIZE;
+
+/* For Argon2 */
+# ifdef USE_SODIUM_ARGON2
+#  define SQL_PASSWD_ARGON2_DEFAULT_HASH_SIZE	32U
+#  define SQL_PASSWD_ARGON2_DEFAULT_SALT_SIZE	16U
+static unsigned int sql_passwd_argon2_hash_len = SQL_PASSWD_ARGON2_DEFAULT_HASH_SIZE;
+# endif /* USE_SODIUM_ARGON2 */
+#endif /* PR_USE_SODIUM */
 
-static cmd_rec *sql_passwd_cmd_create(pool *parent_pool, int argc, ...) {
+static const char *trace_channel = "sql.passwd";
+
+/* Necessary prototypes */
+static int sql_passwd_sess_init(void);
+
+static cmd_rec *sql_passwd_cmd_create(pool *parent_pool,
+    unsigned int argc, ...) {
+  register unsigned int i = 0;
   pool *cmd_pool = NULL;
   cmd_rec *cmd = NULL;
-  register unsigned int i = 0;
   va_list argp;
  
   cmd_pool = make_sub_pool(parent_pool);
@@ -88,29 +125,31 @@ static cmd_rec *sql_passwd_cmd_create(pool *parent_pool, int argc, ...) {
   cmd->pool = cmd_pool;
  
   cmd->argc = argc;
-  cmd->argv = (char **) pcalloc(cmd->pool, argc * sizeof(char *));
+  cmd->argv = pcalloc(cmd->pool, argc * sizeof(void *));
 
   /* Hmmm... */
   cmd->tmp_pool = cmd->pool;
 
   va_start(argp, argc);
-  for (i = 0; i < argc; i++)
+  for (i = 0; i < argc; i++) {
     cmd->argv[i] = va_arg(argp, char *);
+  } 
   va_end(argp);
 
   return cmd;
 }
 
-static char *sql_passwd_get_str(pool *p, char *str) {
+static const char *sql_passwd_get_str(pool *p, const char *str) {
   cmdtable *cmdtab;
   cmd_rec *cmd;
   modret_t *res;
 
-  if (strlen(str) == 0)
+  if (strlen(str) == 0) {
     return str;
+  }
 
   /* Find the cmdtable for the sql_escapestr command. */
-  cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_escapestr", NULL, NULL);
+  cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_escapestr", NULL, NULL, NULL);
   if (cmdtab == NULL) {
     pr_log_debug(DEBUG2, MOD_SQL_PASSWD_VERSION
       ": unable to find SQL hook symbol 'sql_escapestr'");
@@ -135,23 +174,37 @@ static char *sql_passwd_get_str(pool *p, char *str) {
 
 static const char *get_crypto_errors(void) {
   unsigned int count = 0;
-  unsigned long e = ERR_get_error();
+  unsigned long error_code;
   BIO *bio = NULL;
   char *data = NULL;
   long datalen;
-  const char *str = "(unknown)";
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
 
   /* Use ERR_print_errors() and a memory BIO to build up a string with
    * all of the error messages from the error queue.
    */
 
-  if (e)
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
     bio = BIO_new(BIO_s_mem());
+  }
 
-  while (e) {
+  while (error_code) {
     pr_signals_handle();
-    BIO_printf(bio, "\n  (%u) %s", ++count, ERR_error_string(e, NULL));
-    e = ERR_get_error();
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
   }
 
   datalen = BIO_get_mem_data(bio, &data);
@@ -160,8 +213,9 @@ static const char *get_crypto_errors(void) {
     str = pstrdup(session.pool, data);
   }
 
-  if (bio)
+  if (bio) {
     BIO_free(bio);
+  }
 
   return str;
 }
@@ -170,12 +224,12 @@ static int get_pbkdf2_config(char *algo, const EVP_MD **md,
     char *iter_str, int *iter, char *len_str, int *len) {
 
   *md = EVP_get_digestbyname(algo);
-  if (md == NULL) {
+  if (*md == NULL) {
     return SQL_PASSWD_ERR_PBKDF2_UNKNOWN_DIGEST;
   }
 
 #if OPENSSL_VERSION_NUMBER < 0x1000003f
-  /* The necesary OpenSSL support for non-SHA1 digests for PBKDF2 appeared in
+  /* The necessary OpenSSL support for non-SHA1 digests for PBKDF2 appeared in
    * 1.0.0c.
    */
   if (EVP_MD_type(*md) != EVP_MD_type(EVP_sha1())) {
@@ -196,43 +250,133 @@ static int get_pbkdf2_config(char *algo, const EVP_MD **md,
   return 0;
 }
 
-static char *sql_passwd_encode(pool *p, unsigned char *data, size_t data_len) {
-  char *buf;
+static unsigned char *sql_passwd_decode(pool *p, unsigned int encoding,
+    char *text, size_t text_len, size_t *data_len) {
+  unsigned char *data = NULL;
 
-  /* According to RATS, the output buffer for EVP_EncodeBlock() needs to be
-   * 4/3 the size of the input buffer (which is usually EVP_MAX_MD_SIZE).
-   * Let's make it easy, and use an output buffer that's twice the size of the
-   * input buffer.
-   */
-  buf = pcalloc(p, (2 * data_len) + 1);
+  switch (encoding) {
+    case SQL_PASSWD_ENC_USE_NONE:
+      *data_len = text_len;
+      data = (unsigned char *) pstrndup(p, text, text_len);
+      break;
 
-  switch (sql_passwd_encoding) {
-    case SQL_PASSWD_USE_BASE64:
-      EVP_EncodeBlock((unsigned char *) buf, data, (int) data_len);
+    case SQL_PASSWD_ENC_USE_BASE64: {
+      int have_padding = FALSE, res;
+
+      /* Due to Base64's padding, we need to detect if the last block was
+       * padded with zeros; we do this by looking for '=' characters at the
+       * end of the text being decoded.  If we see these characters, then we
+       * will "trim" off any trailing zero values in the decoded data, on the
+       * ASSUMPTION that they are the auto-added padding bytes.
+       */
+      if (text[text_len-1] == '=') {
+        have_padding = TRUE;
+      }
+
+      data = pcalloc(p, text_len);
+      res = EVP_DecodeBlock((unsigned char *) data, (unsigned char *) text,
+        (int) text_len);
+      if (res <= 0) {
+        /* Base64-decoding error. */
+        errno = EINVAL;
+        return NULL;
+      }
+
+      if (have_padding) {
+        /* Assume that only one or two zero bytes of padding were added. */
+        if (data[res-1] == '\0') {
+          res -= 1;
+
+          if (data[res-1] == '\0') {
+            res -= 1;
+          }
+        }
+      }
+
+      *data_len = (size_t) res;
       break;
+    }
+
+    case SQL_PASSWD_ENC_USE_HEX_LC: {
+      register unsigned int i, j;
+      unsigned int len = 0;
+
+      data = pcalloc(p, text_len);
+      for (i = 0, j = 0; i < text_len; i += 2) {
+        int res;
 
-    case SQL_PASSWD_USE_HEX_LC: {
-      register unsigned int i;
+        res = sscanf(text + i, "%02hhx", &(data[j++]));
+        if (res == 0) {
+          /* hex decoding error. */
+          errno = EINVAL;
+          return NULL;
+        }
 
-      for (i = 0; i < data_len; i++) {
-        sprintf((char *) &(buf[i*2]), "%02x", data[i]);
+        len += res;
       }
 
+      *data_len = len;
       break;
     }
 
-    case SQL_PASSWD_USE_HEX_UC: {
-      register unsigned int i;
+    case SQL_PASSWD_ENC_USE_HEX_UC: {
+      register unsigned int i, j;
+      unsigned int len = 0;
+
+      data = pcalloc(p, text_len);
+      for (i = 0, j = 0; i < text_len; i += 2) {
+        int res;
 
-      for (i = 0; i < data_len; i++) {
-        sprintf((char *) &(buf[i*2]), "%02X", data[i]);
+        res = sscanf(text + i, "%02hhX", &(data[j++]));
+        if (res == 0) {
+          /* hex decoding error. */
+          errno = EINVAL;
+          return NULL;
+        }
+
+        len += res;
       }
 
+      *data_len = len;
+      break;
+    }
+
+    default:
+      errno = EPERM;
+      return NULL;
+  }
+
+  return data;
+}
+
+static char *sql_passwd_encode(pool *p, unsigned int encoding,
+    unsigned char *data, size_t data_len) {
+  char *buf = NULL;
+
+  switch (encoding) {
+    case SQL_PASSWD_ENC_USE_BASE64: {
+      /* According to RATS, the output buffer for EVP_EncodeBlock() needs to be
+       * 4/3 the size of the input buffer (which is usually EVP_MAX_MD_SIZE).
+       * Let's make it easy, and use an output buffer that's twice the size of
+       * the input buffer.
+       */
+      buf = pcalloc(p, (2 * data_len) + 1);
+      EVP_EncodeBlock((unsigned char *) buf, data, (int) data_len);
+      break;
+    }
+
+    case SQL_PASSWD_ENC_USE_HEX_LC: {
+      buf = pr_str_bin2hex(p, data, data_len, PR_STR_FL_HEX_USE_LC);
+      break;
+    }
+
+    case SQL_PASSWD_ENC_USE_HEX_UC: {
+      buf = pr_str_bin2hex(p, data, data_len, PR_STR_FL_HEX_USE_UC);
       break;
     }
 
     default:
-      errno = EINVAL;
+      errno = EPERM;
       return NULL;
   }
 
@@ -364,34 +508,66 @@ static modret_t *sql_passwd_auth(cmd_rec *cmd, const char *plaintext,
    * suffix?
    */
 
-  if (sql_passwd_salt_len > 0 &&
-      (sql_passwd_salt_flags & SQL_PASSWD_SALT_FL_PREPEND)) {
+  if (sql_passwd_file_salt_len > 0 &&
+      (sql_passwd_file_salt_flags & SQL_PASSWD_SALT_FL_PREPEND)) {
 
     /* If we have salt data, add it to the mix. */
 
     if (!(sql_passwd_opts & SQL_PASSWD_OPT_HASH_SALT)) {
-      prefix = (unsigned char *) sql_passwd_salt;
-      prefix_len = sql_passwd_salt_len;
+      prefix = (unsigned char *) sql_passwd_file_salt;
+      prefix_len = sql_passwd_file_salt_len;
 
       pr_trace_msg(trace_channel, 9,
-        "prepending %lu bytes of salt data", (unsigned long) prefix_len);
+        "prepending %lu bytes of file salt data", (unsigned long) prefix_len);
 
     } else {
       unsigned int salt_hashlen = 0;
 
       prefix = sql_passwd_hash(cmd->tmp_pool, md,
-        (unsigned char *) sql_passwd_salt, sql_passwd_salt_len,
+        (unsigned char *) sql_passwd_file_salt, sql_passwd_file_salt_len,
         NULL, 0, NULL, 0, &salt_hashlen);
       prefix_len = salt_hashlen;
 
       if (sql_passwd_opts & SQL_PASSWD_OPT_ENCODE_SALT) {
         prefix = (unsigned char *) sql_passwd_encode(cmd->tmp_pool,
-          (unsigned char *) prefix, prefix_len);
+          sql_passwd_encoding, (unsigned char *) prefix, prefix_len);
         prefix_len = strlen((char *) prefix);
       }
 
       pr_trace_msg(trace_channel, 9,
-        "prepending %lu bytes of %s-hashed salt data (%s)",
+        "prepending %lu bytes of %s-hashed file salt data (%s)",
+        (unsigned long) prefix_len, digest, prefix);
+    }
+  }
+
+  if (sql_passwd_user_salt_len > 0 &&
+      (sql_passwd_user_salt_flags & SQL_PASSWD_SALT_FL_PREPEND)) {
+
+    /* If we have user salt data, add it to the mix. */
+
+    if (!(sql_passwd_opts & SQL_PASSWD_OPT_HASH_SALT)) {
+      prefix = (unsigned char *) sql_passwd_user_salt;
+      prefix_len = sql_passwd_user_salt_len;
+
+      pr_trace_msg(trace_channel, 9,
+        "prepending %lu bytes of user salt data", (unsigned long) prefix_len);
+
+    } else {
+      unsigned int salt_hashlen = 0;
+
+      prefix = sql_passwd_hash(cmd->tmp_pool, md,
+        (unsigned char *) sql_passwd_user_salt, sql_passwd_user_salt_len,
+        NULL, 0, NULL, 0, &salt_hashlen);
+      prefix_len = salt_hashlen;
+
+      if (sql_passwd_opts & SQL_PASSWD_OPT_ENCODE_SALT) {
+        prefix = (unsigned char *) sql_passwd_encode(cmd->tmp_pool,
+          sql_passwd_encoding, (unsigned char *) prefix, prefix_len);
+        prefix_len = strlen((char *) prefix);
+      }
+
+      pr_trace_msg(trace_channel, 9,
+        "prepending %lu bytes of %s-hashed user salt data (%s)",
         (unsigned long) prefix_len, digest, prefix);
     }
   }
@@ -405,7 +581,8 @@ static modret_t *sql_passwd_auth(cmd_rec *cmd, const char *plaintext,
      * also salt data present.  Otherwise, it is equivalent to another
      * round of processing, which defeats the principle of least surprise.
      */
-    if (sql_passwd_salt_len == 0 &&
+    if ((sql_passwd_file_salt_len == 0 &&
+         sql_passwd_user_salt_len == 0) &&
         (sql_passwd_opts & SQL_PASSWD_OPT_HASH_PASSWORD) &&
         (sql_passwd_opts & SQL_PASSWD_OPT_ENCODE_PASSWORD)) {
       pr_trace_msg(trace_channel, 4, "%s",
@@ -423,39 +600,70 @@ static modret_t *sql_passwd_auth(cmd_rec *cmd, const char *plaintext,
 
       if (sql_passwd_opts & SQL_PASSWD_OPT_ENCODE_PASSWORD) {
         data = (unsigned char *) sql_passwd_encode(cmd->tmp_pool,
-          (unsigned char *) data, data_len);
+          sql_passwd_encoding, (unsigned char *) data, data_len);
         data_len = strlen((char *) data);
       }
     }
   }
 
-  if (sql_passwd_salt_len > 0 &&
-      (sql_passwd_salt_flags & SQL_PASSWD_SALT_FL_APPEND)) {
-    /* If we have salt data, add it to the mix. */
+  if (sql_passwd_file_salt_len > 0 &&
+      (sql_passwd_file_salt_flags & SQL_PASSWD_SALT_FL_APPEND)) {
+    /* If we have file salt data, add it to the mix. */
 
     if (!(sql_passwd_opts & SQL_PASSWD_OPT_HASH_SALT)) {
-      suffix = (unsigned char *) sql_passwd_salt;
-      suffix_len = sql_passwd_salt_len;
+      suffix = (unsigned char *) sql_passwd_file_salt;
+      suffix_len = sql_passwd_file_salt_len;
 
       pr_trace_msg(trace_channel, 9,
-        "appending %lu bytes of salt data", (unsigned long) suffix_len);
+        "appending %lu bytes of file salt data", (unsigned long) suffix_len);
 
     } else {
       unsigned int salt_hashlen = 0;
 
       suffix = sql_passwd_hash(cmd->tmp_pool, md,
-        (unsigned char *) sql_passwd_salt, sql_passwd_salt_len,
+        (unsigned char *) sql_passwd_file_salt, sql_passwd_file_salt_len,
         NULL, 0, NULL, 0, &salt_hashlen);
       suffix_len = salt_hashlen;
 
       if (sql_passwd_opts & SQL_PASSWD_OPT_ENCODE_SALT) {
         suffix = (unsigned char *) sql_passwd_encode(cmd->tmp_pool,
-          (unsigned char *) suffix, suffix_len);
+          sql_passwd_encoding, (unsigned char *) suffix, suffix_len);
         suffix_len = strlen((char *) suffix);
       }
 
       pr_trace_msg(trace_channel, 9, 
-        "appending %lu bytes of %s-hashed salt data",
+        "appending %lu bytes of %s-hashed file salt data",
+        (unsigned long) suffix_len, digest);
+    }
+  }
+
+  if (sql_passwd_user_salt_len > 0 &&
+      (sql_passwd_user_salt_flags & SQL_PASSWD_SALT_FL_APPEND)) {
+    /* If we have user salt data, add it to the mix. */
+
+    if (!(sql_passwd_opts & SQL_PASSWD_OPT_HASH_SALT)) {
+      suffix = (unsigned char *) sql_passwd_user_salt;
+      suffix_len = sql_passwd_user_salt_len;
+
+      pr_trace_msg(trace_channel, 9,
+        "appending %lu bytes of user salt data", (unsigned long) suffix_len);
+
+    } else {
+      unsigned int salt_hashlen = 0;
+
+      suffix = sql_passwd_hash(cmd->tmp_pool, md,
+        (unsigned char *) sql_passwd_user_salt, sql_passwd_user_salt_len,
+        NULL, 0, NULL, 0, &salt_hashlen);
+      suffix_len = salt_hashlen;
+
+      if (sql_passwd_opts & SQL_PASSWD_OPT_ENCODE_SALT) {
+        suffix = (unsigned char *) sql_passwd_encode(cmd->tmp_pool,
+          sql_passwd_encoding, (unsigned char *) suffix, suffix_len);
+        suffix_len = strlen((char *) suffix);
+      }
+
+      pr_trace_msg(trace_channel, 9, 
+        "appending %lu bytes of %s-hashed user salt data",
         (unsigned long) suffix_len, digest);
     }
   }
@@ -468,7 +676,8 @@ static modret_t *sql_passwd_auth(cmd_rec *cmd, const char *plaintext,
     return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
   }
 
-  encodedtext = sql_passwd_encode(cmd->tmp_pool, hash, hash_len);
+  encodedtext = sql_passwd_encode(cmd->tmp_pool, sql_passwd_encoding, hash,
+    hash_len);
   if (encodedtext == NULL) {
     sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
       ": unsupported SQLPasswordEncoding configured");
@@ -481,10 +690,10 @@ static modret_t *sql_passwd_auth(cmd_rec *cmd, const char *plaintext,
    */
   if (sql_passwd_nrounds > 1) {
     register unsigned int i;
-    unsigned int nrounds = sql_passwd_nrounds - 1;
+    unsigned long nrounds = sql_passwd_nrounds - 1;
 
     pr_trace_msg(trace_channel, 9, 
-      "transforming the data for another %u %s", nrounds,
+      "transforming the data for another %lu %s", nrounds,
       nrounds != 1 ? "rounds" : "round");
 
     for (i = 0; i < nrounds; i++) {
@@ -492,7 +701,8 @@ static modret_t *sql_passwd_auth(cmd_rec *cmd, const char *plaintext,
 
       hash = sql_passwd_hash(cmd->tmp_pool, md, (unsigned char *) encodedtext,
         strlen(encodedtext), NULL, 0, NULL, 0, &hash_len);
-      encodedtext = sql_passwd_encode(cmd->tmp_pool, hash, hash_len);
+      encodedtext = sql_passwd_encode(cmd->tmp_pool, sql_passwd_encoding,
+        hash, hash_len);
 
       pr_trace_msg(trace_channel, 15, "data after round %u: '%s'", i + 1,
         encodedtext);
@@ -537,9 +747,13 @@ static modret_t *sql_passwd_pbkdf2(cmd_rec *cmd, const char *plaintext,
     const char *ciphertext) {
   unsigned char *derived_key;
   const char *encodedtext;
+  char *pbkdf2_salt = NULL;
+  size_t pbkdf2_salt_len = 0;
   int res;
 
-  if (!sql_passwd_engine) {
+  if (sql_passwd_engine == FALSE) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": SQLPasswordEngine disabled; unable to handle PBKDF2 SQLAuthType");
     return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
   }
 
@@ -550,7 +764,8 @@ static modret_t *sql_passwd_pbkdf2(cmd_rec *cmd, const char *plaintext,
   }
 
   /* PBKDF2 requires a salt; if no salt is configured, it is an error. */
-  if (sql_passwd_salt == NULL) {
+  if (sql_passwd_file_salt == NULL &&
+      sql_passwd_user_salt == NULL) {
     sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
       ": no salt configured (PBKDF2 requires salt)");
     return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
@@ -558,17 +773,27 @@ static modret_t *sql_passwd_pbkdf2(cmd_rec *cmd, const char *plaintext,
 
   derived_key = palloc(cmd->tmp_pool, sql_passwd_pbkdf2_len);
 
+  /* Prefer user salts over global salts. */
+  if (sql_passwd_user_salt_len > 0) {
+    pbkdf2_salt = (char *) sql_passwd_user_salt;
+    pbkdf2_salt_len = sql_passwd_user_salt_len;
+
+  } else {
+    pbkdf2_salt = (char *) sql_passwd_file_salt;
+    pbkdf2_salt_len = sql_passwd_file_salt_len;
+  }
+
 #if OPENSSL_VERSION_NUMBER >= 0x1000003f
-  /* For digests other than SHA1, the necesary OpenSSL support
+  /* For digests other than SHA1, the necessary OpenSSL support
    * (via PKCS5_PBKDF2_HMAC) appeared in 1.0.0c.
    */
   res = PKCS5_PBKDF2_HMAC(plaintext, -1,
-    (const unsigned char *) sql_passwd_salt, sql_passwd_salt_len,
+    (const unsigned char *) pbkdf2_salt, pbkdf2_salt_len,
     sql_passwd_pbkdf2_iter, sql_passwd_pbkdf2_digest, sql_passwd_pbkdf2_len,
     derived_key);
 #else
   res = PKCS5_PBKDF2_HMAC_SHA1(plaintext, -1,
-    (const unsigned char *) sql_passwd_salt, sql_passwd_salt_len,
+    (const unsigned char *) pbkdf2_salt, pbkdf2_salt_len,
     sql_passwd_pbkdf2_iter, sql_passwd_pbkdf2_len, derived_key);
 #endif /* OpenSSL-1.0.0b and earlier */
 
@@ -578,7 +803,8 @@ static modret_t *sql_passwd_pbkdf2(cmd_rec *cmd, const char *plaintext,
     return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
   }
 
-  encodedtext = sql_passwd_encode(cmd->tmp_pool, derived_key,
+  encodedtext = sql_passwd_encode(cmd->tmp_pool, sql_passwd_encoding,
+    derived_key,
     sql_passwd_pbkdf2_len);
   if (encodedtext == NULL) {
     sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
@@ -600,6 +826,197 @@ static modret_t *sql_passwd_pbkdf2(cmd_rec *cmd, const char *plaintext,
   return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
 }
 
+#ifdef PR_USE_SODIUM
+static modret_t *sql_passwd_scrypt(cmd_rec *cmd, const char *plaintext,
+    const char *ciphertext) {
+  int res;
+  unsigned char *hash = NULL;
+  unsigned int hash_len = 0;
+  const char *encodedtext;
+  const unsigned char *scrypt_salt;
+  size_t ops_limit, mem_limit, plaintext_len, scrypt_salt_len;
+
+  if (sql_passwd_engine == FALSE) {
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  /* scrypt requires a salt; if no salt is configured, it is an error. */
+  if (sql_passwd_file_salt == NULL &&
+      sql_passwd_user_salt == NULL) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": no salt configured (scrypt requires salt)");
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  /* Prefer user salts over global salts. */
+  if (sql_passwd_user_salt_len > 0) {
+    scrypt_salt = sql_passwd_user_salt;
+    scrypt_salt_len = sql_passwd_user_salt_len;
+
+  } else {
+    scrypt_salt = sql_passwd_file_salt;
+    scrypt_salt_len = sql_passwd_file_salt_len;
+  }
+
+  /* scrypt requires 32 bytes of salt */
+  if (scrypt_salt_len != SQL_PASSWD_SCRYPT_DEFAULT_SALT_SIZE) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": scrypt requires %u bytes of salt (%lu bytes of salt configured)",
+      SQL_PASSWD_SCRYPT_DEFAULT_SALT_SIZE, (unsigned long) scrypt_salt_len);
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  switch (sql_passwd_cost) {
+    case SQL_PASSWD_COST_INTERACTIVE:
+      ops_limit = crypto_pwhash_scryptsalsa208sha256_opslimit_interactive();
+      mem_limit = crypto_pwhash_scryptsalsa208sha256_memlimit_interactive();
+      break;
+
+    case SQL_PASSWD_COST_SENSITIVE:
+      ops_limit = crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive();
+      mem_limit = crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive();
+      break;
+
+    default:
+      sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+        ": unknown SQLPasswordCost value");
+      return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  hash_len = sql_passwd_scrypt_hash_len;
+  hash = palloc(cmd->tmp_pool, hash_len);
+
+  plaintext_len = strlen(plaintext);
+  res = crypto_pwhash_scryptsalsa208sha256(hash, hash_len, plaintext,
+    plaintext_len, scrypt_salt, ops_limit, mem_limit);
+  if (res < 0) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION ": scrypt error: %s",
+      strerror(errno));
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  encodedtext = sql_passwd_encode(cmd->tmp_pool, sql_passwd_encoding, hash,
+    hash_len);
+  if (encodedtext == NULL) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": unsupported SQLPasswordEncoding configured");
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  if (strcmp((char *) encodedtext, ciphertext) == 0) {
+    return PR_HANDLED(cmd);
+
+  } else {
+    pr_trace_msg(trace_channel, 9, "expected '%s', got '%s'", ciphertext,
+      encodedtext);
+
+    pr_log_debug(DEBUG9, MOD_SQL_PASSWD_VERSION ": expected '%s', got '%s'",
+      ciphertext, encodedtext);
+  }
+
+  return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+}
+
+static modret_t *sql_passwd_argon2(cmd_rec *cmd, const char *plaintext,
+    const char *ciphertext) {
+# if defined(USE_SODIUM_ARGON2)
+  int argon2_algo, res;
+  unsigned char *hash = NULL;
+  unsigned int hash_len = 0;
+  const char *encodedtext;
+  const unsigned char *argon2_salt;
+  size_t ops_limit, mem_limit, plaintext_len, argon2_salt_len;
+
+  if (sql_passwd_engine == FALSE) {
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  /* argon2 requires a salt; if no salt is configured, it is an error. */
+  if (sql_passwd_file_salt == NULL &&
+      sql_passwd_user_salt == NULL) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": no salt configured (argon2 requires salt)");
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  /* Prefer user salts over global salts. */
+  if (sql_passwd_user_salt_len > 0) {
+    argon2_salt = sql_passwd_user_salt;
+    argon2_salt_len = sql_passwd_user_salt_len;
+
+  } else {
+    argon2_salt = sql_passwd_file_salt;
+    argon2_salt_len = sql_passwd_file_salt_len;
+  }
+
+  /* argon2 requires 16 bytes of salt */
+  if (argon2_salt_len != SQL_PASSWD_ARGON2_DEFAULT_SALT_SIZE) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": argon2 requires %u bytes of salt (%lu bytes of salt configured)",
+      SQL_PASSWD_ARGON2_DEFAULT_SALT_SIZE, (unsigned long) argon2_salt_len);
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  argon2_algo = crypto_pwhash_argon2i_alg_argon2i13();
+
+  switch (sql_passwd_cost) {
+    case SQL_PASSWD_COST_INTERACTIVE:
+      ops_limit = crypto_pwhash_argon2i_opslimit_interactive();
+      mem_limit = crypto_pwhash_argon2i_memlimit_interactive();
+      break;
+
+    case SQL_PASSWD_COST_SENSITIVE:
+      ops_limit = crypto_pwhash_argon2i_opslimit_sensitive();
+      mem_limit = crypto_pwhash_argon2i_memlimit_sensitive();
+      break;
+
+    default:
+      sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+        ": unknown SQLPasswordCost value");
+      return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  hash_len = sql_passwd_argon2_hash_len;
+  hash = palloc(cmd->tmp_pool, hash_len);
+
+  plaintext_len = strlen(plaintext);
+  res = crypto_pwhash_argon2i(hash, hash_len, plaintext, plaintext_len,
+    argon2_salt, ops_limit, mem_limit, argon2_algo);
+  if (res < 0) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION ": argon2 error: %s",
+      strerror(errno));
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  encodedtext = sql_passwd_encode(cmd->tmp_pool, sql_passwd_encoding, hash,
+    hash_len);
+  if (encodedtext == NULL) {
+    sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+      ": unsupported SQLPasswordEncoding configured");
+    return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+  }
+
+  if (strcmp((char *) encodedtext, ciphertext) == 0) {
+    return PR_HANDLED(cmd);
+
+  } else {
+    pr_trace_msg(trace_channel, 9, "expected '%s', got '%s'", ciphertext,
+      encodedtext);
+
+    pr_log_debug(DEBUG9, MOD_SQL_PASSWD_VERSION ": expected '%s', got '%s'",
+      ciphertext, encodedtext);
+  }
+
+  return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+# else
+  sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
+    ": argon2 not supported on this system (requires libsodium-1.0.9 or "
+    "later)");
+  return PR_ERROR_INT(cmd, PR_AUTH_ERROR);
+# endif /* USE_SODIUM_ARGON2 */
+}
+#endif /* PR_USE_SODIUM */
+
 /* Event handlers
  */
 
@@ -611,6 +1028,10 @@ static void sql_passwd_mod_unload_ev(const void *event_data, void *user_data) {
     sql_unregister_authtype("sha256");
     sql_unregister_authtype("sha512");
     sql_unregister_authtype("pbkdf2");
+# ifdef PR_USE_SODIUM
+    sql_unregister_authtype("argon2");
+    sql_unregister_authtype("scrypt");
+# endif /* PR_USE_SODIUM */
 
     pr_event_unregister(&sql_passwd_module, NULL, NULL);
   }
@@ -623,13 +1044,13 @@ static void sql_passwd_mod_unload_ev(const void *event_data, void *user_data) {
 MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
   config_rec *c;
 
-  if (!sql_passwd_engine) {
+  if (sql_passwd_engine == FALSE) {
     return PR_DECLINED(cmd);
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordRounds", FALSE);
   if (c != NULL) {
-    sql_passwd_nrounds = *((unsigned int *) c->argv[0]);
+    sql_passwd_nrounds = *((unsigned long *) c->argv[0]);
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordPBKDF2", FALSE);
@@ -640,8 +1061,8 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
       sql_passwd_pbkdf2_len = *((int *) c->argv[2]);
 
     } else {
-      char *key;
-      char *named_query, *ptr, *user;
+      const char *user;
+      char *key, *named_query, *ptr;
       cmdtable *sql_cmdtab;
       cmd_rec *sql_cmd;
       modret_t *sql_res;
@@ -659,7 +1080,8 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
         return PR_DECLINED(cmd);
       }
 
-      sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+      sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+        NULL);
       if (sql_cmdtab == NULL) {
         sql_log(DEBUG_WARN, MOD_SQL_PASSWD_VERSION
           ": unable to find SQL hook symbol 'sql_lookup'");
@@ -732,7 +1154,7 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordUserSalt", FALSE);
-  if (c) {
+  if (c != NULL) {
     char *key;
     unsigned long salt_flags;
 
@@ -740,18 +1162,26 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
     salt_flags = *((unsigned long *) c->argv[1]);
 
     if (strcasecmp(key, "name") == 0) {
-      char *user;
+      const char *user;
 
       user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-      sql_passwd_salt = user;
-      sql_passwd_salt_len = strlen(user);
+      if (user == NULL) {
+        pr_log_debug(DEBUG3, MOD_SQL_PASSWD_VERSION
+          ": unable to determine original USER name");
+        return PR_DECLINED(cmd);
+      }
+
+      sql_passwd_user_salt = (unsigned char *) user;
+      sql_passwd_user_salt_len = strlen(user);
 
     } else if (strncasecmp(key, "sql:/", 5) == 0) {
-      char *named_query, *ptr, *user, **values;
+      const char *user;
+      char *named_query, *ptr, **values;
       cmdtable *sql_cmdtab;
       cmd_rec *sql_cmd;
       modret_t *sql_res;
       array_header *sql_data;
+      size_t value_len;
 
       ptr = key + 5;
       named_query = pstrcat(cmd->tmp_pool, "SQLNamedQuery_", ptr, NULL);
@@ -763,7 +1193,8 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
         return PR_DECLINED(cmd);
       }
 
-      sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+      sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+        NULL);
       if (sql_cmdtab == NULL) {
         pr_log_debug(DEBUG3, MOD_SQL_PASSWD_VERSION
           ": unable to find SQL hook symbol 'sql_lookup'");
@@ -771,6 +1202,11 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
       }
 
       user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
+      if (user == NULL) {
+        pr_log_debug(DEBUG3, MOD_SQL_PASSWD_VERSION
+          ": unable to determine original USER name");
+        return PR_DECLINED(cmd);
+      }
 
       sql_cmd = sql_passwd_cmd_create(cmd->tmp_pool, 3, "sql_lookup", ptr,
         sql_passwd_get_str(cmd->tmp_pool, user));
@@ -794,14 +1230,27 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
       }
 
       values = sql_data->elts;
-      sql_passwd_salt = pstrdup(session.pool, values[0]);
-      sql_passwd_salt_len = strlen(values[0]);
+
+      /* Note: this ASSUMES that the value coming from the database is a 
+       * string.
+       */ 
+      value_len = strlen(values[0]);
+
+      sql_passwd_user_salt = sql_passwd_decode(session.pool,
+        sql_passwd_salt_encoding, values[0], value_len,
+        &sql_passwd_user_salt_len);
+      if (sql_passwd_user_salt == NULL) {
+        pr_log_debug(DEBUG0, MOD_SQL_PASSWD_VERSION
+          ": error decoding salt from SQLNamedQuery '%s': %s", ptr,
+          strerror(errno));
+        return PR_DECLINED(cmd);
+      }
 
     } else {
       return PR_DECLINED(cmd);
     }
 
-    sql_passwd_salt_flags = salt_flags;
+    sql_passwd_user_salt_flags = salt_flags;
   }
 
   return PR_DECLINED(cmd);
@@ -810,6 +1259,56 @@ MODRET sql_passwd_pre_pass(cmd_rec *cmd) {
 /* Configuration handlers
  */
 
+/* usage: SQLPasswordArgon2 len */
+MODRET set_sqlpasswdargon2(cmd_rec *cmd) {
+#ifdef USE_SODIUM_ARGON2
+  config_rec *c;
+  int len;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  len = atoi(cmd->argv[1]);
+  if (len <= 0) {
+    CONF_ERROR(cmd, "length must be greater than 0");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[0]) = len;
+
+  return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, "requires libsodium Argon2 support");
+#endif /* No libsodium Argon2 support */
+}
+
+/* usage: SQLPasswordCost "interactive"|"sensitive" */
+MODRET set_sqlpasswdcost(cmd_rec *cmd) {
+  unsigned int cost;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  if (strcasecmp(cmd->argv[1], "interactive") == 0) {
+    cost = SQL_PASSWD_COST_INTERACTIVE;
+
+  } else if (strcasecmp(cmd->argv[1], "sensitive") == 0) {
+    cost = SQL_PASSWD_COST_SENSITIVE;
+
+  } else {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown/unsupported cost: '",
+      cmd->argv[1], "'", NULL));
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[0]) = cost;
+
+  return PR_HANDLED(cmd);
+}
+
 /* usage: SQLPasswordEncoding "base64"|"hex"|"HEX" */
 MODRET set_sqlpasswdencoding(cmd_rec *cmd) {
   unsigned int encoding;
@@ -818,14 +1317,17 @@ MODRET set_sqlpasswdencoding(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (strcmp(cmd->argv[1], "base64") == 0) {
-    encoding = SQL_PASSWD_USE_BASE64;
+  if (strcasecmp(cmd->argv[1], "none") == 0) {
+    encoding = SQL_PASSWD_ENC_USE_NONE;
+
+  } else if (strcasecmp(cmd->argv[1], "base64") == 0) {
+    encoding = SQL_PASSWD_ENC_USE_BASE64;
 
   } else if (strcmp(cmd->argv[1], "hex") == 0) {
-    encoding = SQL_PASSWD_USE_HEX_LC;
+    encoding = SQL_PASSWD_ENC_USE_HEX_LC;
 
   } else if (strcmp(cmd->argv[1], "HEX") == 0) {
-    encoding = SQL_PASSWD_USE_HEX_UC;
+    encoding = SQL_PASSWD_ENC_USE_HEX_UC;
 
   } else {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unsupported encoding '",
@@ -962,12 +1464,12 @@ MODRET set_sqlpasswdpbkdf2(cmd_rec *cmd) {
 /* usage: SQLPasswordRounds count */
 MODRET set_sqlpasswdrounds(cmd_rec *cmd) {
   config_rec *c;
-  int nrounds;
+  long nrounds;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  nrounds = atoi(cmd->argv[1]);
+  nrounds = atol(cmd->argv[1]);
   if (nrounds < 1) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "insufficient number of rounds (",
       cmd->argv[1], ")", NULL));
@@ -975,11 +1477,17 @@ MODRET set_sqlpasswdrounds(cmd_rec *cmd) {
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = palloc(c->pool, sizeof(unsigned int));
-  *((unsigned int *) c->argv[0]) = nrounds;
+  *((unsigned long *) c->argv[0]) = nrounds;
 
   return PR_HANDLED(cmd);
 }
 
+/* usage: SQLPasswordSaltEncoding "base64"|"hex"|"HEX"|"none" */
+MODRET set_sqlpasswdsaltencoding(cmd_rec *cmd) {
+  /* Reuse the parsing code for the SQLPasswordEncoding directive. */
+  return set_sqlpasswdencoding(cmd);
+}
+
 /* usage: SQLPasswordSaltFile path|"none" [flags] */
 MODRET set_sqlpasswdsaltfile(cmd_rec *cmd) {
   config_rec *c;
@@ -1015,8 +1523,31 @@ MODRET set_sqlpasswdsaltfile(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: SQLPasswordUserSalt "name"|"sql:/named-query" [flags]
- */
+/* usage: SQLPasswordScrypt len */
+MODRET set_sqlpasswdscrypt(cmd_rec *cmd) {
+#ifdef PR_USE_SODIUM
+  config_rec *c;
+  int len;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  len = atoi(cmd->argv[1]);
+  if (len <= 0) {
+    CONF_ERROR(cmd, "length must be greater than 0");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[0]) = len;
+
+  return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, "requires libsodium support");
+#endif /* No libsodium support */
+}
+
+/* usage: SQLPasswordUserSalt "name"|"sql:/named-query" [flags] */
 MODRET set_sqlpasswdusersalt(cmd_rec *cmd) {
   config_rec *c;
   register unsigned int i;
@@ -1057,6 +1588,43 @@ MODRET set_sqlpasswdusersalt(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void sql_passwd_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&sql_passwd_module, "core.session-reinit",
+    sql_passwd_sess_reinit_ev);
+
+  sql_passwd_engine = FALSE;
+  sql_passwd_encoding = SQL_PASSWD_ENC_USE_HEX_LC;
+  sql_passwd_salt_encoding = SQL_PASSWD_ENC_USE_NONE;
+  sql_passwd_file_salt = NULL;
+  sql_passwd_file_salt_len = 0;
+  sql_passwd_user_salt = NULL;
+  sql_passwd_user_salt_len = 0;
+  sql_passwd_file_salt_flags = SQL_PASSWD_SALT_FL_APPEND;
+  sql_passwd_user_salt_flags = SQL_PASSWD_SALT_FL_APPEND;
+  sql_passwd_opts = 0UL;
+  sql_passwd_nrounds = 1;
+
+#ifdef PR_USE_SODIUM
+  sql_passwd_scrypt_hash_len = SQL_PASSWD_SCRYPT_DEFAULT_HASH_SIZE;
+# ifdef USE_SODIUM_ARGON2
+  sql_passwd_argon2_hash_len = SQL_PASSWD_ARGON2_DEFAULT_HASH_SIZE;
+# endif /* USE_SODIUM_ARGON2 */
+#endif /* PR_USE_SODIUM */
+
+  res = sql_passwd_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&sql_passwd_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -1068,6 +1636,20 @@ static int sql_passwd_init(void) {
     sql_passwd_mod_unload_ev, NULL);
 #endif /* PR_SHARED_MODULE */
 
+#ifdef PR_USE_SODIUM
+  if (sodium_init() < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_SQL_PASSWD_VERSION
+      ": error initializing libsodium");
+
+  } else {
+    const char *sodium_version;
+
+    sodium_version = sodium_version_string();
+    pr_log_debug(DEBUG2, MOD_SQL_PASSWD_VERSION ": using libsodium-%s",
+      sodium_version);
+  }
+#endif /* PR_USE_SODIUM */
+
   if (sql_register_authtype("md5", sql_passwd_md5) < 0) {
     pr_log_pri(PR_LOG_WARNING, MOD_SQL_PASSWD_VERSION
       ": unable to register 'md5' SQLAuthType handler: %s", strerror(errno));
@@ -1113,12 +1695,35 @@ static int sql_passwd_init(void) {
       ": registered 'pbkdf2' SQLAuthType handler");
   }
 
+#ifdef PR_USE_SODIUM
+  if (sql_register_authtype("scrypt", sql_passwd_scrypt) < 0) {
+    pr_log_pri(PR_LOG_WARNING, MOD_SQL_PASSWD_VERSION
+      ": unable to register 'scrypt' SQLAuthType handler: %s", strerror(errno));
+
+  } else {
+    pr_log_debug(DEBUG6, MOD_SQL_PASSWD_VERSION
+      ": registered 'scrypt' SQLAuthType handler");
+  }
+
+  if (sql_register_authtype("argon2", sql_passwd_argon2) < 0) {
+    pr_log_pri(PR_LOG_WARNING, MOD_SQL_PASSWD_VERSION
+      ": unable to register 'argon2' SQLAuthType handler: %s", strerror(errno));
+
+  } else {
+    pr_log_debug(DEBUG6, MOD_SQL_PASSWD_VERSION
+      ": registered 'argon2' SQLAuthType handler");
+  }
+#endif /* PR_USE_SODIUM */
+
   return 0;
 }
 
 static int sql_passwd_sess_init(void) {
   config_rec *c;
 
+  pr_event_register(&sql_passwd_module, "core.session-reinit",
+    sql_passwd_sess_reinit_ev, NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordEngine", FALSE);
   if (c != NULL) {
     sql_passwd_engine = *((int *) c->argv[0]);
@@ -1128,6 +1733,11 @@ static int sql_passwd_sess_init(void) {
     return 0;
   }
 
+  c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordCost", FALSE);
+  if (c != NULL) {
+    sql_passwd_cost = *((unsigned int *) c->argv[0]);
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordEncoding", FALSE);
   if (c != NULL) {
     sql_passwd_encoding = *((unsigned int *) c->argv[0]);
@@ -1145,8 +1755,14 @@ static int sql_passwd_sess_init(void) {
     c = find_config_next(c, c->next, CONF_PARAM, "SQLPasswordOptions", FALSE);
   }
 
+  c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordSaltEncoding",
+    FALSE);
+  if (c != NULL) {
+    sql_passwd_salt_encoding = *((unsigned int *) c->argv[0]);
+  }
+
   c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordSaltFile", FALSE);
-  if (c) {
+  if (c != NULL) {
     char *path;
     unsigned long salt_flags;
 
@@ -1154,7 +1770,7 @@ static int sql_passwd_sess_init(void) {
     salt_flags = *((unsigned long *) c->argv[1]);
 
     if (strcasecmp(path, "none") != 0) {
-      int fd, xerrno = 0;;
+      int fd, xerrno = 0;
 
       PRIVS_ROOT
       fd = open(path, O_RDONLY|O_NONBLOCK);
@@ -1164,7 +1780,13 @@ static int sql_passwd_sess_init(void) {
       PRIVS_RELINQUISH
 
       if (fd >= 0) {
+        char *file_salt = NULL;
+        size_t file_salt_len = 0;
         int flags;
+
+        /* XXX Rather than using a fixed size, this should be a dynamically
+         * allocated buffer, of st.st_blksize bytes, for optimal disk IO.
+         */
         char buf[512];
         ssize_t nread;
   
@@ -1180,34 +1802,28 @@ static int sql_passwd_sess_init(void) {
         while (nread > 0) {
           pr_signals_handle();
 
-          if (sql_passwd_salt == NULL) {
-
+          if (file_salt == NULL) {
             /* If the very last byte in the buffer is a newline, trim it. */
             if (buf[nread-1] == '\n') {
               buf[nread-1] = '\0';
               nread--;
             }
 
-            sql_passwd_salt_len = nread;
-            sql_passwd_salt = palloc(session.pool, sql_passwd_salt_len);
-            memcpy(sql_passwd_salt, buf, nread);
+            file_salt_len = nread;
+            file_salt = palloc(session.pool, file_salt_len);
+            memcpy(file_salt, buf, nread);
 
           } else {
             char *ptr, *tmp;
 
             /* Allocate a larger buffer for the salt. */
-            ptr = tmp = palloc(session.pool, sql_passwd_salt_len + nread);
-            memcpy(tmp, sql_passwd_salt, sql_passwd_salt_len);
-            tmp += sql_passwd_salt_len;
+            ptr = tmp = palloc(session.pool, file_salt_len + nread);
+            memcpy(tmp, file_salt, file_salt_len);
+            tmp += file_salt_len;
 
             memcpy(tmp, buf, nread);
-            sql_passwd_salt_len += nread;
-
-            /* XXX Yes, this is a minor memory leak; we are overwriting the
-             * previously allocated memory for the salt.  But it's per-session,
-             * so it's not a great concern at this point.
-             */
-            sql_passwd_salt = ptr;
+            file_salt_len += nread;
+            file_salt = ptr;
           }
 
           nread = read(fd, buf, sizeof(buf));
@@ -1217,21 +1833,34 @@ static int sql_passwd_sess_init(void) {
           pr_log_debug(DEBUG1, MOD_SQL_PASSWD_VERSION
             ": error reading salt data from SQLPasswordSaltFile '%s': %s",
             path, strerror(errno));
-          sql_passwd_salt = NULL;
+          file_salt = NULL;
         }
 
         (void) close(fd);
 
-        /* If the very last byte in the buffer is a newline, trim it.  This
-         * is to deal with cases where the SaltFile may have been written
-         * with an editor (e.g. vi) which automatically adds a trailing newline.
-         */
-        if (sql_passwd_salt[sql_passwd_salt_len-1] == '\n') {
-          sql_passwd_salt[sql_passwd_salt_len-1] = '\0';
-          sql_passwd_salt_len--;
-        }
+        if (file_salt != NULL) {
+          /* If the very last byte in the buffer is a newline, trim it.  This
+           * is to deal with cases where the SaltFile may have been written
+           * with an editor (e.g. vi) which automatically adds a trailing
+           * newline.
+           */
+          if (file_salt[file_salt_len-1] == '\n') {
+            file_salt[file_salt_len-1] = '\0';
+            file_salt_len--;
+          }
+
+          sql_passwd_file_salt = sql_passwd_decode(session.pool,
+            sql_passwd_salt_encoding, file_salt, file_salt_len,
+            &sql_passwd_file_salt_len);
+          if (sql_passwd_file_salt == NULL) {
+            pr_log_debug(DEBUG0, MOD_SQL_PASSWD_VERSION
+              ": error decoding salt from SQLPasswordSaltFile '%s': %s", path,
+              strerror(errno));
 
-        sql_passwd_salt_flags = salt_flags;
+          } else {
+            sql_passwd_file_salt_flags = salt_flags;
+          }
+        }
 
       } else {
         pr_log_debug(DEBUG1, MOD_SQL_PASSWD_VERSION
@@ -1241,6 +1870,20 @@ static int sql_passwd_sess_init(void) {
     }
   }
 
+#ifdef PR_USE_SODIUM
+  c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordScrypt", FALSE);
+  if (c != NULL) {
+    sql_passwd_scrypt_hash_len = *((unsigned int *) c->argv[0]);
+  }
+
+# ifdef USE_SODIUM_ARGON2
+  c = find_config(main_server->conf, CONF_PARAM, "SQLPasswordArgon2", FALSE);
+  if (c != NULL) {
+    sql_passwd_argon2_hash_len = *((unsigned int *) c->argv[0]);
+  }
+# endif /* USE_SODIUM_ARGON2 */
+#endif /* PR_USE_SODIUM */
+
   return 0;
 }
 
@@ -1248,13 +1891,17 @@ static int sql_passwd_sess_init(void) {
  */
 
 static conftable sql_passwd_conftab[] = {
-  { "SQLPasswordEncoding",	set_sqlpasswdencoding,	NULL },
-  { "SQLPasswordEngine",	set_sqlpasswdengine,	NULL },
-  { "SQLPasswordOptions",	set_sqlpasswdoptions,	NULL },
-  { "SQLPasswordPBKDF2",	set_sqlpasswdpbkdf2,	NULL },
-  { "SQLPasswordRounds",	set_sqlpasswdrounds,	NULL },
-  { "SQLPasswordSaltFile",	set_sqlpasswdsaltfile,	NULL },
-  { "SQLPasswordUserSalt",	set_sqlpasswdusersalt,	NULL },
+  { "SQLPasswordArgon2",	set_sqlpasswdargon2,		NULL },
+  { "SQLPasswordCost",		set_sqlpasswdcost,		NULL },
+  { "SQLPasswordEncoding",	set_sqlpasswdencoding,		NULL },
+  { "SQLPasswordEngine",	set_sqlpasswdengine,		NULL },
+  { "SQLPasswordOptions",	set_sqlpasswdoptions,		NULL },
+  { "SQLPasswordPBKDF2",	set_sqlpasswdpbkdf2,		NULL },
+  { "SQLPasswordRounds",	set_sqlpasswdrounds,		NULL },
+  { "SQLPasswordSaltEncoding",	set_sqlpasswdsaltencoding,	NULL },
+  { "SQLPasswordSaltFile",	set_sqlpasswdsaltfile,		NULL },
+  { "SQLPasswordScrypt",	set_sqlpasswdscrypt,		NULL },
+  { "SQLPasswordUserSalt",	set_sqlpasswdusersalt,		NULL },
 
   { NULL, NULL, NULL }
 };
@@ -1294,4 +1941,3 @@ module sql_passwd_module = {
   /* Module version */
   MOD_SQL_PASSWD_VERSION
 };
-
diff --git a/contrib/mod_sql_postgres.c b/contrib/mod_sql_postgres.c
index 129a076..86fa483 100644
--- a/contrib/mod_sql_postgres.c
+++ b/contrib/mod_sql_postgres.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_sql_postgres -- Support for connecting to Postgres databases.
  * Time-stamp: <1999-10-04 03:21:21 root>
  * Copyright (c) 2001 Andrew Houghton
- * Copyright (c) 2004-2014 TJ Saunders
+ * Copyright (c) 2004-2017 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
@@ -23,8 +23,7 @@
  * the resulting executable, without including the source code for OpenSSL in
  * the source distribution.
  *
- * $Id: mod_sql_postgres.c,v 1.56 2013-10-09 04:57:13 castaglia Exp $
- * $Libraries: -lm -lpq $
+ * $Libraries: -lm -lpq$
  */
 
 /* Internal define used for debug and logging.  All backends are encouraged
@@ -38,6 +37,14 @@
 #include "../contrib/mod_sql.h"
 
 #include <libpq-fe.h>
+#if defined(HAVE_POSTGRES_PQGETSSL) && defined(PR_USE_OPENSSL)
+# include <openssl/ssl.h>
+
+/* Define if you have the LibreSSL library.  */
+# if defined(LIBRESSL_VERSION_NUMBER)
+#   define HAVE_LIBRESSL	1
+# endif
+#endif /* HAVE_POSTGRES_PQGETSSL and PR_USE_OPENSSL */
 
 /* For the pg_encoding_to_char() function, used for NLS support, we need
  * to include the <mb/pg_wchar.h> file.  It's OK; the function has been
@@ -59,6 +66,8 @@ extern const char *pg_encoding_to_char(int encoding);
 static const char *get_postgres_encoding(const char *encoding);
 #endif
 
+static const char *trace_channel = "sql.postgres";
+
 /* 
  * timer-handling code adds the need for a couple of forward declarations
  */
@@ -73,14 +82,18 @@ module sql_postgres_module;
 struct db_conn_struct {
 
   /* Postgres-specific members */
+  const char *host;
+  const char *user;
+  const char *pass;
+  const char *db;
+  const char *port;
 
-  char *host;
-  char *user;
-  char *pass;
-  char *db;
-  char *port;
+  /* For configuring the SSL/TLS session to the Postgres server. */
+  const char *ssl_cert_file;
+  const char *ssl_key_file;
+  const char *ssl_ca_file;
 
-  char *connectstring;
+  const char *connect_string;
 
   PGconn *postgres;
   PGresult *result;
@@ -88,23 +101,19 @@ struct db_conn_struct {
 
 typedef struct db_conn_struct db_conn_t;
 
-/*
- * This struct is a wrapper for whatever backend data is needed to access 
+/* This struct is a wrapper for whatever backend data is needed to access
  * the database, and supports named connections, connection counting, and 
  * timer handling.  
  */
-
 struct conn_entry_struct {
-  char *name;
+  const char *name;
   void *data;
 
-  /* timer handling */
-
+  /* Iimer handling */
   int timer;
   int ttl;
 
-  /* connection handling */
-
+  /* Connection handling */
   unsigned int connections;
 };
 
@@ -115,63 +124,66 @@ typedef struct conn_entry_struct conn_entry_t;
 static pool *conn_pool = NULL;
 static array_header *conn_cache = NULL;
 
-/*
- *  _sql_get_connection: walks the connection cache looking for the named
+/*  sql_get_connection: walks the connection cache looking for the named
  *   connection.  Returns NULL if unsuccessful, a pointer to the conn_entry_t
  *   if successful.
  */
-static conn_entry_t *_sql_get_connection(char *name)
-{
-  conn_entry_t *entry = NULL;
-  int cnt;
+static conn_entry_t *sql_get_connection(const char *conn_name) {
+  register unsigned int i;
 
-  if (name == NULL) return NULL;
+  if (conn_name == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
 
-  /* walk the array looking for our entry */
-  for (cnt=0; cnt < conn_cache->nelts; cnt++) {
-    entry = ((conn_entry_t **) conn_cache->elts)[cnt];
-    if (!strcmp(name, entry->name)) {
+  for (i = 0; i < conn_cache->nelts; i++) {
+    conn_entry_t *entry;
+
+    entry = ((conn_entry_t **) conn_cache->elts)[i];
+    if (strcmp(conn_name, entry->name) == 0) {
       return entry;
     }
   }
 
+  errno = ENOENT;
   return NULL;
 }
 
-/* 
- * _sql_add_connection: internal helper function to maintain a cache of 
- *  connections.  Since we expect the number of named connections to
- *  be small, simply use an array header to hold them.  We don't allow 
- *  duplicate connection names.
+/* sql_add_connection: internal helper function to maintain a cache of
+ *  connections.  Since we expect the number of named connections to be small,
+ *  simply use an array header to hold them.  We don't allow duplicate
+ *  connection names.
  *
  * Returns: NULL if the insertion was unsuccessful, a pointer to the 
  *  conn_entry_t that was created if successful.
  */
-static void *_sql_add_connection(pool *p, char *name, db_conn_t *conn)
-{
+static void *sql_add_connection(pool *p, const char *name, db_conn_t *conn) {
   conn_entry_t *entry = NULL;
 
-  if ((!name) || (!conn) || (!p)) return NULL;
+  if (p == NULL ||
+      name == NULL ||
+      conn == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
   
-  if (_sql_get_connection(name)) {
-    /* duplicated name */
+  if (sql_get_connection(name) != NULL) {
+    errno = EEXIST;
     return NULL;
   }
 
   entry = (conn_entry_t *) pcalloc(p, sizeof(conn_entry_t));
-  entry->name = name;
+  entry->name = pstrdup(p, name);
   entry->data = conn;
 
   *((conn_entry_t **) push_array(conn_cache)) = entry;
-
   return entry;
 }
 
-/* _sql_check_cmd: tests to make sure the cmd_rec is valid and is 
- *  properly filled in.  If not, it's grounds for the daemon to
- *  shutdown.
+/* sql_check_cmd: tests to make sure the cmd_rec is valid and is properly
+ *   filled in.  If not, it's grounds for the daemon to shutdown.
  */
-static void _sql_check_cmd(cmd_rec *cmd, char *msg) {
+static void sql_check_cmd(cmd_rec *cmd, char *msg) {
   if (cmd == NULL ||
       cmd->tmp_pool == NULL) {
     pr_log_pri(PR_LOG_ERR, MOD_SQL_POSTGRES_VERSION
@@ -190,17 +202,18 @@ static void _sql_check_cmd(cmd_rec *cmd, char *msg) {
  * This function makes assumptions about the db_conn_t members.
  */
 static int sql_timer_cb(CALLBACK_FRAME) {
-  conn_entry_t *entry = NULL;
-  int i = 0;
-  cmd_rec *cmd = NULL;
- 
+  register unsigned int i;
+
   for (i = 0; i < conn_cache->nelts; i++) {
+    conn_entry_t *entry = NULL;
+
     entry = ((conn_entry_t **) conn_cache->elts)[i];
+    if ((unsigned long) entry->timer == p2) {
+      cmd_rec *cmd = NULL;
 
-    if (entry->timer == p2) {
       sql_log(DEBUG_INFO, "timer expired for connection '%s'", entry->name);
-      cmd = _sql_make_cmd( conn_pool, 2, entry->name, "1" );
-      cmd_close( cmd );
+      cmd = sql_make_cmd(conn_pool, 2, entry->name, "1");
+      cmd_close(cmd);
       SQL_FREE_CMD(cmd);
       entry->timer = 0;
     }
@@ -209,35 +222,34 @@ static int sql_timer_cb(CALLBACK_FRAME) {
   return 0;
 }
 
-/* 
- * _build_error: constructs a modret_t filled with error information;
- *  mod_sql_postgres calls this function and returns the resulting mod_ret_t
+/* build_error: constructs a modret_t filled with error information;
+ *  mod_sql_postgres calls this function and returns the resulting modret_t
  *  whenever a call to the database results in an error.
  */
-static modret_t *_build_error(cmd_rec *cmd, db_conn_t *conn) {
-  if (!conn)
+static modret_t *build_error(cmd_rec *cmd, db_conn_t *conn) {
+  if (conn == NULL) {
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
+  }
 
   return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
-    PQerrorMessage(conn->postgres));
+    pstrdup(cmd->pool, PQerrorMessage(conn->postgres)));
 }
 
-/*
- * _build_data: both cmd_select and cmd_procedure potentially
+/* build_data: both cmd_select and cmd_procedure potentially
  *  return data to mod_sql; this function builds a modret to return
  *  that data.  This is Postgres specific; other backends may choose 
  *  to do things differently.
  */
-static modret_t *_build_data(cmd_rec *cmd, db_conn_t *conn) {
+static modret_t *build_data(cmd_rec *cmd, db_conn_t *conn) {
   PGresult *result = NULL;
   sql_data_t *sd = NULL;
   char **data = NULL;
-  int index = 0;
-  int field = 0;
-  int row =0;
+  int idx = 0;
+  unsigned long row;
 
-  if (!conn) 
+  if (conn == NULL) {
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
+  }
 
   result = conn->result;
 
@@ -245,19 +257,21 @@ static modret_t *_build_data(cmd_rec *cmd, db_conn_t *conn) {
   sd->rnum = (unsigned long) PQntuples(result);
   sd->fnum = (unsigned long) PQnfields(result);
 
-  data = (char **) pcalloc(cmd->tmp_pool, sizeof(char *) * 
-			    ((sd->rnum * sd->fnum) + 1));
-  
+  data = (char **) pcalloc(cmd->tmp_pool, sizeof(char *) *
+    ((sd->rnum * sd->fnum) + 1));
+
   for (row = 0; row < sd->rnum; row++) {
+    unsigned long field;
+
     for (field = 0; field < sd->fnum; field++) {
-      data[index++] = pstrdup(cmd->tmp_pool, PQgetvalue(result, row, field));
+      data[idx++] = pstrdup(cmd->tmp_pool, PQgetvalue(result, row, field));
     }
   }
-  data[index] = NULL;
+  data[idx] = NULL;
 
   sd->data = data;
 
-  return mod_create_data( cmd, (void *) sd );
+  return mod_create_data(cmd, (void *) sd);
 }
 
 #ifdef PR_USE_NLS
@@ -364,16 +378,15 @@ MODRET cmd_open(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_open");
 
-  _sql_check_cmd(cmd, "cmd_open" );
+  sql_check_cmd(cmd, "cmd_open");
 
   if (cmd->argc < 1) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_open");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }    
 
-  /* get the named connection */
-
-  if (!(entry = _sql_get_connection(cmd->argv[0]))) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_open");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -458,12 +471,20 @@ MODRET cmd_open(cmd_rec *cmd) {
   }
 
   /* make sure we have a new conn struct */
-  conn->postgres = PQconnectdb(conn->connectstring);
-  
+  conn->postgres = PQconnectdb(conn->connect_string);
   if (PQstatus(conn->postgres) == CONNECTION_BAD) {
-    /* if it didn't work, return an error */
+    modret_t *mr = NULL;
+
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_open");
-    return _build_error( cmd, conn );
+    mr = build_error(cmd, conn);
+
+    /* Since we failed to connect here, avoid a memory leak by freeing up the
+     * postgres conn struct.
+     */
+    PQfinish(conn->postgres);
+    conn->postgres = NULL;
+
+    return mr;
   }
 
 #if defined(PG_VERSION_STR)
@@ -475,7 +496,7 @@ MODRET cmd_open(cmd_rec *cmd) {
     sql_log(DEBUG_FUNC, "Postgres server version: %s", server_version);
   }
 
-#ifdef PR_USE_NLS
+#if defined(PR_USE_NLS)
   if (pr_encode_get_encoding() != NULL) {
     const char *encoding;
 
@@ -483,9 +504,8 @@ MODRET cmd_open(cmd_rec *cmd) {
 
     /* Configure the connection for the current local character set. */
     if (PQsetClientEncoding(conn->postgres, encoding) < 0) {
-      /* if it didn't work, return an error */
       sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_open");
-      return _build_error(cmd, conn);
+      return build_error(cmd, conn);
     }
 
     sql_log(DEBUG_FUNC, "Postgres connection character set now '%s' "
@@ -494,6 +514,23 @@ MODRET cmd_open(cmd_rec *cmd) {
   }
 #endif /* !PR_USE_NLS */
 
+#if defined(HAVE_POSTGRES_PQGETSSL)
+  if (PQgetssl(conn->postgres) != NULL) {
+# if defined(PR_USE_OPENSSL)
+    SSL *ssl;
+
+    ssl = PQgetssl(conn->postgres);
+    sql_log(DEBUG_FUNC, "%s", "Postgres SSL connection: true");
+    sql_log(DEBUG_FUNC, "%s", "Postgres SSL cipher: %s",
+      SSL_get_cipher_name(ssl));
+# else
+    sql_log(DEBUG_FUNC, "%s", "Postgres SSL connection: true");
+# endif /* PR_USE_OPENSSL */
+  } else {
+    sql_log(DEBUG_FUNC, "%s", "Postgres SSL connection: false");
+  }
+#endif /* HAVE_POSTGRES_PQGETSSL */
+
   /* bump connections */
   entry->connections++;
 
@@ -523,9 +560,9 @@ MODRET cmd_open(cmd_rec *cmd) {
 
   /* return HANDLED */
   sql_log(DEBUG_INFO, "connection '%s' opened", entry->name);
-
   sql_log(DEBUG_INFO, "connection '%s' count is now %d", entry->name,
     entry->connections);
+  pr_event_generate("mod_sql.db.connection-opened", &sql_postgres_module);
 
   sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_open");
   return PR_HANDLED(cmd);
@@ -561,15 +598,16 @@ MODRET cmd_close(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_close");
 
-  _sql_check_cmd(cmd, "cmd_close");
+  sql_check_cmd(cmd, "cmd_close");
 
-  if ((cmd->argc < 1) || (cmd->argc > 2)) {
+  if (cmd->argc < 1 ||
+      cmd->argc > 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_close");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  if (!(entry = _sql_get_connection(cmd->argv[0]))) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_close");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -590,9 +628,11 @@ MODRET cmd_close(cmd_rec *cmd) {
    * close the connection, explicitly set the counter to 0, and remove any
    * timers.
    */
-  if (((--entry->connections) == 0 ) || ((cmd->argc == 2) && (cmd->argv[1]))) {
-    PQfinish(conn->postgres);
-    conn->postgres = NULL;
+  if (((--entry->connections) == 0) || ((cmd->argc == 2) && (cmd->argv[1]))) {
+    if (conn->postgres != NULL) {
+      PQfinish(conn->postgres);
+      conn->postgres = NULL;
+    }
     entry->connections = 0;
 
     if (entry->timer) {
@@ -602,6 +642,7 @@ MODRET cmd_close(cmd_rec *cmd) {
     }
 
     sql_log(DEBUG_INFO, "connection '%s' closed", entry->name);
+    pr_event_generate("mod_sql.db.connection-closed", &sql_postgres_module);
   }
 
   sql_log(DEBUG_INFO, "connection '%s' count is now %d", entry->name,
@@ -611,8 +652,7 @@ MODRET cmd_close(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/*
- * cmd_defineconnection: takes all information about a database
+/* cmd_defineconnection: takes all information about a database
  *  connection and stores it for later use.
  *
  * Inputs:
@@ -620,8 +660,14 @@ MODRET cmd_close(cmd_rec *cmd) {
  *  cmd->argv[1]: username portion of the SQLConnectInfo directive
  *  cmd->argv[2]: password portion of the SQLConnectInfo directive
  *  cmd->argv[3]: info portion of the SQLConnectInfo directive
+ *
  * Optional:
  *  cmd->argv[4]: time-to-live in seconds
+ *  cmd->argv[5]: SSL client cert file
+ *  cmd->argv[6]: SSL client key file
+ *  cmd->argv[7]: SSL CA file
+ *  cmd->argv[8]: SSL CA directory
+ *  cmd->argv[9]: SSL ciphers
  *
  * Returns:
  *  either a properly filled error modret_t if the connection could not
@@ -635,31 +681,26 @@ MODRET cmd_close(cmd_rec *cmd) {
  *  associated timer.
  */
 MODRET cmd_defineconnection(cmd_rec *cmd) {
-  char *info = NULL;
-  char *name = NULL;
-
-  char *db = NULL;
-  char *host = NULL;
-  char *port = NULL;
-
-  char *havehost = NULL;
-  char *haveport = NULL;
-  
-  char *connectstring = NULL;
+  char *have_host = NULL, *have_port = NULL, *info = NULL, *name = NULL;
+  const char *db = NULL, *host = NULL, *port = NULL, *connect_string = NULL;
+  const char *ssl_cert_file = NULL, *ssl_key_file = NULL, *ssl_ca_file = NULL;
+  const char *ssl_ciphers = NULL;
   conn_entry_t *entry = NULL;
   db_conn_t *conn = NULL; 
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_defineconnection");
 
-  _sql_check_cmd(cmd, "cmd_defineconnection");
+  sql_check_cmd(cmd, "cmd_defineconnection");
 
-  if ((cmd->argc < 4) || (cmd->argc > 5) || (!cmd->argv[0])) {
+  if (cmd->argc < 4 ||
+      cmd->argc > 10 ||
+      !cmd->argv[0]) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_defineconnection");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  if (!conn_pool) {
-    pr_log_pri(PR_LOG_WARNING, "warning: the mod_sql_postgres module has not "
+  if (conn_pool == NULL) {
+    pr_log_pri(PR_LOG_WARNING, "WARNING: the mod_sql_postgres module has not "
       "been properly initialized.  Please make sure your --with-modules "
       "configure option lists mod_sql *before* mod_sql_postgres, and "
       "recompile.");
@@ -673,7 +714,6 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
   }
 
   conn = (db_conn_t *) pcalloc(conn_pool, sizeof(db_conn_t));
-
   name = pstrdup(conn_pool, cmd->argv[0]);
   conn->user = pstrdup(conn_pool, cmd->argv[1]);
   conn->pass = pstrdup(conn_pool, cmd->argv[2]);
@@ -682,50 +722,101 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
 
   db = pstrdup(cmd->tmp_pool, info);
 
-  havehost = strchr(db, '@');
-  haveport = strchr(db, ':');
+  have_host = strchr(db, '@');
+  have_port = strchr(db, ':');
 
-  /*
-   * if haveport, parse it, otherwise default it. 
-   * if haveport, set it to '\0'
+  /* If have_port, parse it, otherwise default it.
+   * If have_port, set it to '\0'
    *
-   * if havehost, parse it, otherwise default it.
-   * if havehost, set it to '\0'
+   * If have_host, parse it, otherwise default it.
+   * If have_host, set it to '\0'
    */
 
-  if (haveport) {
-    port = haveport + 1;
-    *haveport = '\0';
+  if (have_port != NULL) {
+    port = have_port + 1;
+    *have_port = '\0';
+
   } else {
     port = _POSTGRES_PORT;
   }
 
-  if (havehost) {
-    host = havehost + 1;
-    *havehost = '\0';
+  if (have_host) {
+    host = have_host + 1;
+    *have_host = '\0';
+
   } else {
     host = "localhost";
   }
 
+  /* SSL parameters, if configured. */
+  if (cmd->argc >= 6) {
+    ssl_cert_file = cmd->argv[5];
+  }
+
+  if (cmd->argc >= 7) {
+    ssl_key_file = cmd->argv[6];
+  }
+
+  if (cmd->argc >= 8) {
+    ssl_ca_file = cmd->argv[7];
+  }
+
+  /* Ignore the ssl_ca_dir parameter, for now. */
+  if (cmd->argc >= 10) {
+    ssl_ciphers = cmd->argv[9];
+  }
+
   conn->host = pstrdup(conn_pool, host);
-  conn->db   = pstrdup(conn_pool, db);
+  conn->db = pstrdup(conn_pool, db);
   conn->port = pstrdup(conn_pool, port);
+  conn->ssl_cert_file = pstrdup(conn_pool, ssl_cert_file);
+  conn->ssl_key_file = pstrdup(conn_pool, ssl_key_file);
+  conn->ssl_ca_file = pstrdup(conn_pool, ssl_ca_file);
 
-  /* setup the connect string the way postgres likes it */
-  connectstring = pstrcat(cmd->tmp_pool, "host='", conn->host, "' port='",
-			  conn->port,"' dbname='", conn->db, "' user='",
-			  conn->user,"' password='", conn->pass, "'", NULL);
-  conn->connectstring = pstrdup(conn_pool, connectstring);
+  /* Set up the connect string the way postgres likes it */
+  connect_string = pstrcat(cmd->tmp_pool, "host='", conn->host, "' port='",
+    conn->port,"' dbname='", conn->db, "' user='", conn->user,"' password='",
+    conn->pass, "'", NULL);
 
+  /* XXX Should we set the sslmode keyword to "prefer" explicitly, or
+   * "require", when SSL parameters have been set?
+   */
+
+  if (ssl_ciphers != NULL ||
+      ssl_cert_file != NULL ||
+      ssl_key_file != NULL ||
+      ssl_ca_file != NULL) {
+    connect_string = pstrcat(cmd->tmp_pool, connect_string,
+      " sslmode='prefer'", NULL);
+  }
+
+  if (conn->ssl_cert_file != NULL) {
+    connect_string = pstrcat(cmd->tmp_pool, connect_string, " sslcert='",
+      conn->ssl_cert_file, "'", NULL);
+  }
+
+  if (conn->ssl_key_file != NULL) {
+    connect_string = pstrcat(cmd->tmp_pool, connect_string, " sslkey='",
+      conn->ssl_key_file, "'", NULL);
+  }
+
+  if (conn->ssl_ca_file != NULL) {
+    connect_string = pstrcat(cmd->tmp_pool, connect_string, " sslrootcert='",
+      conn->ssl_ca_file, "'", NULL);
+  }
+
+  pr_trace_msg(trace_channel, 17, "using connect string '%s'", connect_string);
+  conn->connect_string = pstrdup(conn_pool, connect_string);
 
   /* insert the new conn_info into the connection hash */
-  if (!(entry = _sql_add_connection(conn_pool, name, (void *) conn))) {
+  entry = sql_add_connection(conn_pool, name, (void *) conn);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_defineconnection");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "named connection already exists");
   }
 
-  if (cmd->argc == 5) { 
+  if (cmd->argc >= 5) {
     entry->ttl = (int) strtol(cmd->argv[4], (char **) NULL, 10);
     if (entry->ttl >= 1) {
       pr_sql_conn_policy = SQL_CONN_POLICY_TIMER;
@@ -745,6 +836,18 @@ MODRET cmd_defineconnection(cmd_rec *cmd) {
   sql_log(DEBUG_INFO, " port: '%s'", conn->port);
   sql_log(DEBUG_INFO, "  ttl: '%d'", entry->ttl);
 
+  if (conn->ssl_cert_file != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: client cert = '%s'", conn->ssl_cert_file);
+  }
+
+  if (conn->ssl_key_file != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: client key = '%s'", conn->ssl_key_file);
+  }
+
+  if (conn->ssl_ca_file != NULL) {
+    sql_log(DEBUG_INFO, "   ssl: CA file = '%s'", conn->ssl_ca_file);
+  }
+
   sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_defineconnection");
   return PR_HANDLED(cmd);
 }
@@ -764,10 +867,13 @@ static modret_t *cmd_exit(cmd_rec *cmd) {
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_exit");
 
   for (i = 0; i < conn_cache->nelts; i++) {
-    conn_entry_t *entry = ((conn_entry_t **) conn_cache->elts)[i];
+    conn_entry_t *entry;
 
+    entry = ((conn_entry_t **) conn_cache->elts)[i];
     if (entry->connections > 0) {
-      cmd_rec *close_cmd = _sql_make_cmd(conn_pool, 2, entry->name, "1");
+      cmd_rec *close_cmd;
+
+      close_cmd = sql_make_cmd(conn_pool, 2, entry->name, "1");
       cmd_close(close_cmd);
       destroy_pool(close_cmd->pool);
     }
@@ -830,21 +936,20 @@ MODRET cmd_select(cmd_rec *cmd) {
   modret_t *cmr = NULL;
   modret_t *dmr = NULL;
   char *query = NULL;
-  int cnt = 0;
+  unsigned long cnt = 0;
   cmd_rec *close_cmd;
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_select");
 
-  _sql_check_cmd(cmd, "cmd_select");
+  sql_check_cmd(cmd, "cmd_select");
 
   if (cmd->argc < 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_select");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_select");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -861,29 +966,34 @@ MODRET cmd_select(cmd_rec *cmd) {
   /* construct the query string */
   if (cmd->argc == 2) {
     query = pstrcat(cmd->tmp_pool, "SELECT ", cmd->argv[1], NULL);
+
   } else {
-    query = pstrcat( cmd->tmp_pool, cmd->argv[2], " FROM ", 
-		     cmd->argv[1], NULL );
-    if ((cmd->argc > 3) && (cmd->argv[3]))
-      query = pstrcat( cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL );
-    if ((cmd->argc > 4) && (cmd->argv[4]))
-      query = pstrcat( cmd->tmp_pool, query, " LIMIT ", cmd->argv[4], NULL );
-    if (cmd->argc > 5) {
+    query = pstrcat(cmd->tmp_pool, cmd->argv[2], " FROM ", cmd->argv[1], NULL);
+    if (cmd->argc > 3 &&
+        cmd->argv[3]) {
+      query = pstrcat(cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL);
+    }
+
+    if (cmd->argc > 4 &&
+        cmd->argv[4]) {
+      query = pstrcat(cmd->tmp_pool, query, " LIMIT ", cmd->argv[4], NULL);
+    }
 
-      /* handle the optional arguments -- they're rare, so in this case
+    if (cmd->argc > 5) {
+      /* Handle the optional arguments -- they're rare, so in this case
        * we'll play with the already constructed query string, but in 
        * general we should probably take optional arguments into account 
        * and put the query string together later once we know what they are.
        */
     
-      for (cnt=5; cnt < cmd->argc; cnt++) {
-	if ((cmd->argv[cnt]) && !strcasecmp("DISTINCT",cmd->argv[cnt])) {
-	  query = pstrcat( cmd->tmp_pool, "DISTINCT ", query, NULL);
+      for (cnt = 5; cnt < cmd->argc; cnt++) {
+	if ((cmd->argv[cnt]) && !strcasecmp("DISTINCT", cmd->argv[cnt])) {
+	  query = pstrcat(cmd->tmp_pool, "DISTINCT ", query, NULL);
 	}
       }
     }
 
-    query = pstrcat( cmd->tmp_pool, "SELECT ", query, NULL);    
+    query = pstrcat(cmd->tmp_pool, "SELECT ", query, NULL);
   }
 
   /* log the query string */
@@ -894,11 +1004,13 @@ MODRET cmd_select(cmd_rec *cmd) {
    */
   if (!(conn->result = PQexec(conn->postgres, query)) ||
       (PQresultStatus(conn->result) != PGRES_TUPLES_OK)) {
-    dmr = _build_error( cmd, conn );
+    dmr = build_error(cmd, conn);
 
-    if (conn->result) PQclear(conn->result);
+    if (conn->result != NULL) {
+      PQclear(conn->result);
+    }
 
-    close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -909,14 +1021,14 @@ MODRET cmd_select(cmd_rec *cmd) {
   /* get the data. if it doesn't work, log the error, close the
    * connection then return the error from the data processing.
    */
-  dmr = _build_data( cmd, conn );
+  dmr = build_data(cmd, conn);
 
   PQclear(conn->result);
 
   if (MODRET_ERROR(dmr)) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_select");
 
-    close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -924,7 +1036,7 @@ MODRET cmd_select(cmd_rec *cmd) {
   }    
 
   /* close the connection, return the data. */
-  close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -973,16 +1085,16 @@ MODRET cmd_insert(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_insert");
 
-  _sql_check_cmd(cmd, "cmd_insert");
+  sql_check_cmd(cmd, "cmd_insert");
 
-  if ((cmd->argc != 2) && (cmd->argc != 4)) {
+  if (cmd->argc != 2 &&
+      cmd->argc != 4) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_insert");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_insert");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -999,13 +1111,12 @@ MODRET cmd_insert(cmd_rec *cmd) {
   /* construct the query string */
   if (cmd->argc == 2) {
     query = pstrcat(cmd->tmp_pool, "INSERT ", cmd->argv[1], NULL);
+
   } else {
-    query = pstrcat( cmd->tmp_pool, "INSERT INTO ", cmd->argv[1], " (",
-		     cmd->argv[2], ") VALUES (", cmd->argv[3], ")",
-		     NULL );
+    query = pstrcat(cmd->tmp_pool, "INSERT INTO ", cmd->argv[1], " (",
+      cmd->argv[2], ") VALUES (", cmd->argv[3], ")", NULL);
   }
 
-  /* log the query string */
   sql_log(DEBUG_INFO, "query \"%s\"", query);
 
   /* perform the query.  if it doesn't work, log the error, close the
@@ -1013,11 +1124,13 @@ MODRET cmd_insert(cmd_rec *cmd) {
    */
   if (!(conn->result = PQexec(conn->postgres, query)) ||
       (PQresultStatus(conn->result) != PGRES_COMMAND_OK)) {
-    dmr = _build_error( cmd, conn );
+    dmr = build_error(cmd, conn);
 
-    if (conn->result) PQclear(conn->result);
+    if (conn->result != NULL) {
+      PQclear(conn->result);
+    }
 
-    close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -1028,7 +1141,7 @@ MODRET cmd_insert(cmd_rec *cmd) {
   PQclear(conn->result);
 
   /* close the connection and return HANDLED. */
-  close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1076,16 +1189,16 @@ MODRET cmd_update(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_update");
 
-  _sql_check_cmd(cmd, "cmd_update");
+  sql_check_cmd(cmd, "cmd_update");
 
-  if ((cmd->argc < 2) || (cmd->argc > 4)) {
+  if (cmd->argc < 2 ||
+      cmd->argc > 4) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_update");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_update");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -1101,15 +1214,16 @@ MODRET cmd_update(cmd_rec *cmd) {
 
   if (cmd->argc == 2) {
     query = pstrcat(cmd->tmp_pool, "UPDATE ", cmd->argv[1], NULL);
+
   } else {
-    /* construct the query string */
-    query = pstrcat( cmd->tmp_pool, "UPDATE ", cmd->argv[1], " SET ",
-		     cmd->argv[2], NULL );
-    if ((cmd->argc > 3) && (cmd->argv[3]))
-      query = pstrcat( cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL );
+    query = pstrcat(cmd->tmp_pool, "UPDATE ", cmd->argv[1], " SET ",
+      cmd->argv[2], NULL);
+    if (cmd->argc > 3 &&
+        cmd->argv[3]) {
+      query = pstrcat(cmd->tmp_pool, query, " WHERE ", cmd->argv[3], NULL);
+    }
   }
 
-  /* log the query string */
   sql_log(DEBUG_INFO, "query \"%s\"", query);
 
   /* perform the query.  if it doesn't work, log the error, close the
@@ -1117,11 +1231,13 @@ MODRET cmd_update(cmd_rec *cmd) {
    */
   if (!(conn->result = PQexec(conn->postgres, query)) ||
       (PQresultStatus(conn->result) != PGRES_COMMAND_OK)) {
-    dmr = _build_error( cmd, conn );
+    dmr = build_error(cmd, conn);
 
-    if (conn->result) PQclear(conn->result);
+    if (conn->result != NULL) {
+      PQclear(conn->result);
+    }
 
-    close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -1132,7 +1248,7 @@ MODRET cmd_update(cmd_rec *cmd) {
   PQclear(conn->result);
 
   /* close the connection, return HANDLED.  */
-  close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1161,7 +1277,7 @@ MODRET cmd_update(cmd_rec *cmd) {
 MODRET cmd_procedure(cmd_rec *cmd) {
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_procedure");
 
-  _sql_check_cmd(cmd, "cmd_procedure");
+  sql_check_cmd(cmd, "cmd_procedure");
 
   if (cmd->argc != 3) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_procedure");
@@ -1205,16 +1321,15 @@ MODRET cmd_query(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_query");
 
-  _sql_check_cmd(cmd, "cmd_query");
+  sql_check_cmd(cmd, "cmd_query");
 
   if (cmd->argc != 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_query");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_query");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -1230,8 +1345,7 @@ MODRET cmd_query(cmd_rec *cmd) {
 
   query = pstrcat(cmd->tmp_pool, cmd->argv[1], NULL);
 
-  /* log the query string */
-  sql_log( DEBUG_INFO, "query \"%s\"", query); 
+  sql_log(DEBUG_INFO, "query \"%s\"", query);
 
   /* perform the query.  if it doesn't work, log the error, close the
    * connection then return the error from the query processing.
@@ -1239,11 +1353,13 @@ MODRET cmd_query(cmd_rec *cmd) {
   if (!(conn->result = PQexec(conn->postgres, query)) ||
       ((PQresultStatus(conn->result) != PGRES_TUPLES_OK) &&
        (PQresultStatus(conn->result) != PGRES_COMMAND_OK))) {
-    dmr = _build_error( cmd, conn );
+    dmr = build_error(cmd, conn);
 
-    if (conn->result) PQclear(conn->result);
+    if (conn->result != NULL) {
+      PQclear(conn->result);
+    }
 
-    close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+    close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
     cmd_close(close_cmd);
     SQL_FREE_CMD(close_cmd);
 
@@ -1255,20 +1371,20 @@ MODRET cmd_query(cmd_rec *cmd) {
    * connection then return the error from the data processing.
    */
 
-  if ( PQresultStatus( conn->result ) == PGRES_TUPLES_OK ) {
-    dmr = _build_data( cmd, conn );
+  if (PQresultStatus(conn->result) == PGRES_TUPLES_OK) {
+    dmr = build_data(cmd, conn);
 
     PQclear(conn->result);
 
     if (MODRET_ERROR(dmr)) {
       sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_query");
     }
+
   } else {
     dmr = PR_HANDLED(cmd);
   }
 
-  /* close the connection, return the data. */
-  close_cmd = _sql_make_cmd( cmd->tmp_pool, 1, entry->name );
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1302,7 +1418,7 @@ MODRET cmd_query(cmd_rec *cmd) {
  *  copying the data from argv[0] into the data field of the modret allows
  *  for possible SQL injection attacks when this backend is used.
  */
-MODRET cmd_escapestring(cmd_rec * cmd) {
+MODRET cmd_escapestring(cmd_rec *cmd) {
   conn_entry_t *entry = NULL;
   db_conn_t *conn = NULL;
   modret_t *cmr = NULL;
@@ -1316,16 +1432,15 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_escapestring");
 
-  _sql_check_cmd(cmd, "cmd_escapestring");
+  sql_check_cmd(cmd, "cmd_escapestring");
 
   if (cmd->argc != 2) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_escapestring");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION, "badly formed request");
   }
 
-  /* get the named connection */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_escapestring");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -1349,13 +1464,13 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
   PQescapeStringConn(conn->postgres, escaped, unescaped, unescaped_len, &pgerr);
   if (pgerr != 0) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_escapestring");
-    return _build_error(cmd, conn);
+    return build_error(cmd, conn);
   }
 #else
   PQescapeString(escaped, unescaped, unescaped_len);
 #endif
 
-  close_cmd = _sql_make_cmd(cmd->tmp_pool, 1, entry->name);
+  close_cmd = sql_make_cmd(cmd->tmp_pool, 1, entry->name);
   cmd_close(close_cmd);
   SQL_FREE_CMD(close_cmd);
 
@@ -1385,12 +1500,12 @@ MODRET cmd_escapestring(cmd_rec * cmd) {
  *  If this backend does not provide this functionality, this cmd *must*
  *  return ERROR.
  */
-MODRET cmd_checkauth(cmd_rec * cmd) {
+MODRET cmd_checkauth(cmd_rec *cmd) {
   conn_entry_t *entry = NULL;
 
   sql_log(DEBUG_FUNC, "%s", "entering \tpostgres cmd_checkauth");
 
-  _sql_check_cmd(cmd, "cmd_checkauth");
+  sql_check_cmd(cmd, "cmd_checkauth");
 
   if (cmd->argc != 3) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_checkauth");
@@ -1398,8 +1513,8 @@ MODRET cmd_checkauth(cmd_rec * cmd) {
   }
 
   /* get the named connection -- not used in this case, but for consistency */
-  entry = _sql_get_connection(cmd->argv[0]);
-  if (!entry) {
+  entry = sql_get_connection(cmd->argv[0]);
+  if (entry == NULL) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tpostgres cmd_checkauth");
     return PR_ERROR_MSG(cmd, MOD_SQL_POSTGRES_VERSION,
       "unknown named connection");
@@ -1435,10 +1550,10 @@ MODRET cmd_checkauth(cmd_rec * cmd) {
  * Notes:
  *  See mod_sql.h for currently accepted APIs.
  */
-MODRET cmd_identify(cmd_rec * cmd) {
+MODRET cmd_identify(cmd_rec *cmd) {
   sql_data_t *sd = NULL;
 
-  _sql_check_cmd(cmd, "cmd_identify");
+  sql_check_cmd(cmd, "cmd_identify");
 
   sd = (sql_data_t *) pcalloc(cmd->tmp_pool, sizeof(sql_data_t));
   sd->data = (char **) pcalloc(cmd->tmp_pool, sizeof(char *) * 2);
@@ -1534,14 +1649,49 @@ static void sql_postgres_mod_unload_ev(const void *event_data,
 /* Initialization routines
  */
 
-static int sql_postgres_init(void) {
+static void sql_postgres_ssl_init(void) {
+#ifdef HAVE_POSTGRES_PQINITOPENSSL
+  int init_ssl = TRUE, init_crypto = TRUE;
+
+  /* If any of the OpenSSL-using modules are loaded, tell Postgres to NOT
+   * initialize OpenSSL itself.  Note that there are nuances to this; some
+   * of the other modules may only use the crypto libs.
+   */
+  if (pr_module_exists("mod_auth_otp.c") == TRUE ||
+      pr_module_exists("mod_digest.c") == TRUE ||
+      pr_module_exists("mod_sftp.c") == TRUE ||
+      pr_module_exists("mod_sql_passwd.c") == TRUE) {
+    init_crypto = FALSE;
+  }
 
+  if (pr_module_exists("mod_tls.c") == TRUE) {
+    init_ssl = FALSE;
+    init_crypto = FALSE;
+  }
+
+# if defined(HAVE_LIBRESSL)
+  /* However, if we are using LibreSSL, then the above modules will NOT be
+   * properly initializing OpenSSL.  Thus Postgres should do such
+   * initializations itself.
+   */
+  init_ssl = init_crypto = TRUE;
+# endif
+
+  pr_trace_msg(trace_channel, 18,
+    "telling Postgres about OpenSSL initialization: ssl = %s, crypto = %s",
+    init_ssl ? "yes" : "no", init_crypto ? "yes" : "no");
+  PQinitOpenSSL(init_ssl, init_crypto);
+#endif /* HAVE_POSTGRES_PQINITOPENSSL */
+}
+
+static int sql_postgres_init(void) {
   /* Register listeners for the load and unload events. */
   pr_event_register(&sql_postgres_module, "core.module-load",
     sql_postgres_mod_load_ev, NULL);
   pr_event_register(&sql_postgres_module, "core.module-unload",
     sql_postgres_mod_unload_ev, NULL);
 
+  sql_postgres_ssl_init();
   return 0;
 }
 
diff --git a/contrib/mod_sql_sqlite.c b/contrib/mod_sql_sqlite.c
index d3c3de4..555174b 100644
--- a/contrib/mod_sql_sqlite.c
+++ b/contrib/mod_sql_sqlite.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_sql_sqlite -- Support for connecting to SQLite databases
- *
- * Copyright (c) 2004-2014 TJ Saunders
+ * Copyright (c) 2004-2017 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
@@ -21,8 +20,7 @@
  * with OpenSSL, and distribute the resulting executable, without including
  * the source code for OpenSSL in the source distribution.
  *
- * $Id: mod_sql_sqlite.c,v 1.24 2013-10-07 05:51:29 castaglia Exp $
- * $Libraries: -lsqlite3 $
+ * $Libraries: -lsqlite3$
  */
 
 #define MOD_SQL_SQLITE_VERSION		"mod_sql_sqlite/0.4"
@@ -34,8 +32,8 @@
 #include <sqlite3.h>
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030001
-# error "ProFTPD 1.3.0rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 module sql_sqlite_module;
@@ -121,9 +119,10 @@ static int sql_sqlite_timer_cb(CALLBACK_FRAME) {
   register unsigned int i = 0;
  
   for (i = 0; i < conn_cache->nelts; i++) {
-    conn_entry_t *entry = ((conn_entry_t **) conn_cache->elts)[i];
+    conn_entry_t *entry;
 
-    if (entry->timer == p2) {
+    entry = ((conn_entry_t **) conn_cache->elts)[i];
+    if ((unsigned long) entry->timer == p2) {
       cmd_rec *cmd = NULL;
 
       sql_log(DEBUG_INFO, "timer expired for connection '%s'", entry->name);
@@ -147,7 +146,7 @@ static array_header *result_list = NULL;
 
 static int exec_cb(void *n, int ncols, char **cols,
     char **colnames) {
-  register unsigned int i;
+  register int i;
   char ***row;
   cmd_rec *cmd = n;
 
@@ -252,8 +251,9 @@ static modret_t *sql_sqlite_get_data(cmd_rec *cmd) {
   char **data;
   sql_data_t *sd = pcalloc(cmd->tmp_pool, sizeof(sql_data_t));
 
-  if (result_list == NULL)
+  if (result_list == NULL) {
     return mod_create_data(cmd, sd);
+  }
 
   sd->rnum = result_list->nelts;
   sd->fnum = result_ncols;
@@ -261,11 +261,13 @@ static modret_t *sql_sqlite_get_data(cmd_rec *cmd) {
   data = pcalloc(cmd->tmp_pool, sizeof(char *) * (count + 1));
 
   for (i = 0; i < result_list->nelts; i++) {
-    register unsigned int j;
-    char **row = ((char ***) result_list->elts)[i];
+    register int j;
+    char **row;
 
-    for (j = 0; j < result_ncols; j++)
+    row = ((char ***) result_list->elts)[i];
+    for (j = 0; j < result_ncols; j++) {
       data[k++] = pstrdup(cmd->tmp_pool, row[j]);
+    }
   }
 
   data[k] = NULL;
@@ -310,8 +312,9 @@ MODRET sql_sqlite_open(cmd_rec *cmd) {
   if (entry->nconn > 0) {
     entry->nconn++;
 
-    if (entry->timer)
+    if (entry->timer) {
       pr_timer_reset(entry->timer, &sql_sqlite_module);
+    }
 
     sql_log(DEBUG_INFO, "'%s' connection count is now %u", entry->name,
       entry->nconn);
@@ -409,6 +412,7 @@ MODRET sql_sqlite_open(cmd_rec *cmd) {
   sql_log(DEBUG_INFO, "'%s' connection opened", entry->name);
   sql_log(DEBUG_INFO, "'%s' connection count is now %u", entry->name,
     entry->nconn);
+  pr_event_generate("mod_sql.db.connection-opened", &sql_sqlite_module);
 
   sql_log(DEBUG_FUNC, "%s", "exiting \tsqlite cmd_open");
   return PR_HANDLED(cmd);
@@ -467,6 +471,7 @@ MODRET sql_sqlite_close(cmd_rec *cmd) {
     }
 
     sql_log(DEBUG_INFO, "'%s' connection closed", entry->name);
+    pr_event_generate("mod_sql.db.connection-closed", &sql_sqlite_module);
   }
 
   sql_log(DEBUG_INFO, "'%s' connection count is now %u", entry->name,
@@ -491,13 +496,15 @@ MODRET sql_sqlite_def_conn(cmd_rec *cmd) {
 
   sql_log(DEBUG_FUNC, "%s", "entering \tsqlite cmd_defineconnection");
 
-  if (cmd->argc < 4 || cmd->argc > 5 || !cmd->argv[0]) {
+  if (cmd->argc < 4 ||
+      cmd->argc > 10 ||
+      !cmd->argv[0]) {
     sql_log(DEBUG_FUNC, "%s", "exiting \tsqlite cmd_defineconnection");
     return PR_ERROR_MSG(cmd, MOD_SQL_SQLITE_VERSION, "badly formed request");
   }
 
-  if (!conn_pool) {
-    pr_log_pri(PR_LOG_WARNING, "warning: the mod_sql_sqlite module has not "
+  if (conn_pool == NULL) {
+    pr_log_pri(PR_LOG_WARNING, "WARNING: the mod_sql_sqlite module has not "
       "been properly intialized.  Please make sure your --with-modules "
       "configure option lists mod_sql *before* mod_sql_sqlite, and recompile.");
 
@@ -524,7 +531,7 @@ MODRET sql_sqlite_def_conn(cmd_rec *cmd) {
       "named connection already exists");
   }
 
-  if (cmd->argc == 5) {
+  if (cmd->argc >= 5) {
     entry->ttl = (int) strtol(cmd->argv[4], (char **) NULL, 10);
     if (entry->ttl >= 1) {
       pr_sql_conn_policy = SQL_CONN_POLICY_TIMER;
@@ -1020,7 +1027,7 @@ MODRET sql_sqlite_checkauth(cmd_rec *cmd) {
     "SQLite does not support the 'Backend' SQLAuthType");
 }
 
-MODRET sql_sqlite_identify(cmd_rec * cmd) {
+MODRET sql_sqlite_identify(cmd_rec *cmd) {
   sql_data_t *sd = NULL;
 
   sd = (sql_data_t *) pcalloc(cmd->tmp_pool, sizeof(sql_data_t));
@@ -1105,7 +1112,7 @@ static int sql_sqlite_init(void) {
    * For now, we only log if there is a difference.
    */
   if (strcmp(sqlite3_libversion(), SQLITE_VERSION) != 0) {
-    pr_log_pri(PR_LOG_WARNING, MOD_SQL_SQLITE_VERSION
+    pr_log_pri(PR_LOG_INFO, MOD_SQL_SQLITE_VERSION
       ": compiled using SQLite version '%s' headers, but linked to "
       "SQLite version '%s' library", SQLITE_VERSION, sqlite3_libversion());
   }
diff --git a/contrib/mod_statcache.c b/contrib/mod_statcache.c
new file mode 100644
index 0000000..17b9df7
--- /dev/null
+++ b/contrib/mod_statcache.c
@@ -0,0 +1,2579 @@
+/*
+ * ProFTPD: mod_statcache -- a module implementing caching of stat(2),
+ *                           fstat(2), and lstat(2) calls
+ * Copyright (c) 2013-2017 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_statcache, contrib software for proftpd 1.3.x.
+ * For more information contact TJ Saunders <tj at castaglia.org>.
+ */
+
+#include "conf.h"
+#include "privs.h"
+#ifdef PR_USE_CTRLS
+# include "mod_ctrls.h"
+#endif /* PR_USE_CTRLS */
+
+#include <signal.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+
+#if HAVE_SYS_MMAN_H
+# include <sys/mman.h>
+#endif
+
+#if HAVE_SYS_UIO_H
+# include <sys/uio.h>
+#endif
+
+#define MOD_STATCACHE_VERSION			"mod_statcache/0.2"
+
+/* Make sure the version of proftpd is as necessary. */
+#if PROFTPD_VERSION_NUMBER < 0x0001030402
+# error "ProFTPD 1.3.4rc2 or later required"
+#endif
+
+/* On some platforms, this may not be defined.  On AIX, for example, this
+ * symbol is only defined when _NO_PROTO is defined, and _XOPEN_SOURCE is 500.
+ * How annoying.
+ */
+#ifndef MAP_FAILED
+# define MAP_FAILED     ((void *) -1)
+#endif
+
+#define STATCACHE_DEFAULT_CAPACITY	5000
+#define STATCACHE_DEFAULT_MAX_AGE	5
+
+/* A path is hashed, and that hash % ncols indicates the row index.  For
+ * each row, there can be N columns.  This value indicates the number of
+ * columns for a row; it controls how many collisions can be handled.
+ */
+#define STATCACHE_COLS_PER_ROW		10
+
+/* Max number of lock attempts */
+#define STATCACHE_MAX_LOCK_ATTEMPTS	10
+
+/* Subpool size */
+#define STATCACHE_POOL_SIZE		256
+
+/* From src/main.c */
+extern pid_t mpid;
+
+module statcache_module;
+
+#ifdef PR_USE_CTRLS
+static ctrls_acttab_t statcache_acttab[];
+#endif
+
+/* Pool for this module's use */
+static pool *statcache_pool = NULL;
+
+/* Copied from src/fsio.c. */
+struct statcache_entry {
+  uint32_t sce_hash;
+  char sce_path[PR_TUNABLE_PATH_MAX+1];
+  size_t sce_pathlen;
+  struct stat sce_stat;
+  int sce_errno;
+  unsigned char sce_op;
+  time_t sce_ts;
+};
+
+/*  Storage structure:
+ *
+ *    Header (stats):
+ *      uint32_t count
+ *      uint32_t highest
+ *      uint32_t hits
+ *      uint32_t misses
+ *      uint32_t expires
+ *      uint32_t rejects
+ *
+ *  Data (entries):
+ *    nrows = capacity / STATCACHE_COLS_PER_ROW
+ *    row_len = sizeof(struct statcache_entry) * STATCACHE_COLS_PER_ROW
+ *    row_start = ((hash % nrows) * row_len) + data_start
+ */
+
+static int statcache_engine = FALSE;
+static unsigned int statcache_max_positive_age = STATCACHE_DEFAULT_MAX_AGE;
+static unsigned int statcache_max_negative_age = 1;
+static unsigned int statcache_capacity = STATCACHE_DEFAULT_CAPACITY;
+static unsigned int statcache_nrows = 0;
+static size_t statcache_rowlen = 0;
+
+static char *statcache_table_path = NULL;
+static pr_fh_t *statcache_tabfh = NULL;
+
+static void *statcache_table = NULL;
+static size_t statcache_tablesz = 0;
+static void *statcache_table_stats = NULL;
+static struct statcache_entry *statcache_table_data = NULL;
+
+static const char *trace_channel = "statcache";
+
+static int statcache_wlock_row(int fd, uint32_t hash);
+static int statcache_unlock_row(int fd, uint32_t hash);
+
+#ifdef PR_USE_CTRLS
+static int statcache_rlock_stats(int fd);
+static int statcache_rlock_table(int fd);
+static int statcache_unlock_table(int fd);
+#endif /* PR_USE_CTRLS */
+static int statcache_wlock_stats(int fd);
+static int statcache_unlock_stats(int fd);
+
+static void statcache_fs_statcache_clear_ev(const void *event_data,
+  void *user_data);
+static int statcache_sess_init(void);
+
+/* Functions for marshalling key/value data to/from local cache (SysV shm). */
+static void *statcache_get_shm(pr_fh_t *tabfh, size_t datasz) {
+  void *data;
+  int fd, mmap_flags, res, xerrno;
+#if defined(MADV_RANDOM) || defined(MADV_ACCESS_MANY)
+  int advice = 0;
+#endif
+
+  fd = tabfh->fh_fd;
+
+  /* Truncate the table first; any existing data should be deleted. */
+  res = ftruncate(fd, 0);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_log_debug(DEBUG0, MOD_STATCACHE_VERSION
+      ": error truncating StatCacheTable '%s' to size 0: %s", tabfh->fh_path,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  /* Seek to the desired table size (actually, one byte less than the desired
+   * size) and write a single byte, so that there's enough allocated backing
+   * store on the filesystem to support the ensuing mmap() call.
+   */
+  if (lseek(fd, datasz, SEEK_SET) == (off_t) -1) {
+    xerrno = errno;
+
+    pr_log_debug(DEBUG0, MOD_STATCACHE_VERSION
+      ": error seeking to offset %lu in StatCacheTable '%s': %s",
+      (unsigned long) datasz-1, tabfh->fh_path, strerror(xerrno));
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  res = write(fd, "", 1);
+  if (res != 1) {
+    xerrno = errno;
+
+    pr_log_debug(DEBUG0, MOD_STATCACHE_VERSION
+      ": error writing single byte to StatCacheTable '%s': %s",
+      tabfh->fh_path, strerror(xerrno));
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  mmap_flags = MAP_SHARED;
+
+  /* Make sure to set the fd to -1 if MAP_ANON(YMOUS) is used.  By definition,
+   * anonymous mapped memory does not need (or want) a valid file backing
+   * store; some implementations will not do what is expected when anonymous
+   * memory is requested AND a valid fd is passed in.
+   *
+   * However, we want to keep a valid fd open anyway, for later use by
+   * fcntl(2) for byte range locking; we simply don't use the valid fd for
+   * the mmap(2) call.
+   */
+
+#if defined(MAP_ANONYMOUS)
+  /* Linux */
+  mmap_flags |= MAP_ANONYMOUS;
+  fd = -1;
+
+#elif defined(MAP_ANON)
+  /* FreeBSD, MacOSX, Solaris, others? */
+  mmap_flags |= MAP_ANON;
+  fd = -1;
+
+#else
+  pr_log_debug(DEBUG8, MOD_STATCACHE_VERSION
+    ": mmap(2) MAP_ANONYMOUS and MAP_ANON flags not defined");
+#endif
+
+  data = mmap(NULL, datasz, PROT_READ|PROT_WRITE, mmap_flags, fd, 0);
+  if (data == MAP_FAILED) {
+    xerrno = errno;
+
+    pr_log_debug(DEBUG0, MOD_STATCACHE_VERSION
+      ": error mapping StatCacheTable '%s' fd %d size %lu into memory: %s",
+      tabfh->fh_path, fd, (unsigned long) datasz, strerror(xerrno));
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  /* Make sure the data are zeroed. */
+  memset(data, 0, datasz);
+
+#if defined(MADV_RANDOM) || defined(MADV_ACCESS_MANY)
+  /* Provide some hints to the kernel, for hopefully better handling of
+   * this buffer.
+   */
+# if defined(MADV_RANDOM)
+  advice = MADV_RANDOM;
+# elif defined(MADV_ACCESS_MANY)
+  /* Oracle-ism? */
+  advice = MADV_ACCESS_MANY;
+# endif /* Random access pattern memory advice */
+
+  res = madvise(data, datasz, advice);
+  if (res < 0) {
+    pr_log_debug(DEBUG5, MOD_STATCACHE_VERSION
+      ": madvise(2) error with MADV_RANDOM: %s", strerror(errno));
+  }
+#endif
+
+  return data;
+}
+
+static const char *get_lock_type(struct flock *lock) {
+  const char *lock_type;
+
+  switch (lock->l_type) {
+    case F_RDLCK:
+      lock_type = "read";
+      break;
+
+    case F_WRLCK:
+      lock_type = "write";
+      break;
+
+    case F_UNLCK:
+      lock_type = "unlock";
+      break;
+
+    default:
+      lock_type = "[UNKNOWN]";
+  }
+
+  return lock_type;
+}
+
+/* Header locking routines */
+static int lock_table(int fd, int lock_type, off_t lock_len) {
+  struct flock lock;
+  unsigned int nattempts = 1;
+
+  lock.l_type = lock_type;
+  lock.l_whence = 0;
+  lock.l_start = 0;
+  lock.l_len = (6 * sizeof(uint32_t));
+
+  pr_trace_msg(trace_channel, 15,
+    "attempt #%u to acquire %s lock on StatCacheTable fd %d (off %lu, len %lu)",
+    nattempts, get_lock_type(&lock), fd, (unsigned long) lock.l_start,
+    (unsigned long) lock.l_len);
+
+  while (fcntl(fd, F_SETLK, &lock) < 0) {
+    int xerrno = errno;
+
+    if (xerrno == EINTR) {
+      pr_signals_handle();
+      continue;
+    }
+
+    pr_trace_msg(trace_channel, 3,
+      "%s lock (attempt #%u) of StatCacheTable fd %d failed: %s",
+      get_lock_type(&lock), nattempts, fd, strerror(xerrno));
+    if (xerrno == EACCES) {
+      struct flock locker;
+
+      /* Get the PID of the process blocking this lock. */
+      if (fcntl(fd, F_GETLK, &locker) == 0) {
+        pr_trace_msg(trace_channel, 3, "process ID %lu has blocking %s lock on "
+          "StatCacheTable fd %d", (unsigned long) locker.l_pid,
+          get_lock_type(&locker), fd);
+      }
+    }
+
+    if (xerrno == EAGAIN ||
+        xerrno == EACCES) {
+      /* Treat this as an interrupted call, call pr_signals_handle() (which
+       * will delay for a few msecs because of EINTR), and try again.
+       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
+       */
+
+      nattempts++;
+      if (nattempts <= STATCACHE_MAX_LOCK_ATTEMPTS) {
+        errno = EINTR;
+
+        pr_signals_handle();
+
+        errno = 0;
+        pr_trace_msg(trace_channel, 15,
+          "attempt #%u to acquire %s lock on StatCacheTable fd %d", nattempts,
+          get_lock_type(&lock), fd);
+        continue;
+      }
+
+      pr_trace_msg(trace_channel, 15, "unable to acquire %s lock on "
+        "StatCacheTable fd %d after %u attempts: %s", get_lock_type(&lock),
+        nattempts, fd, strerror(xerrno));
+    }
+
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 15,
+    "acquired %s lock of StatCacheTable fd %d successfully",
+    get_lock_type(&lock), fd);
+  return 0;
+}
+
+#ifdef PR_USE_CTRLS
+static int statcache_rlock_stats(int fd) {
+  return lock_table(fd, F_RDLCK, (6 * sizeof(uint32_t)));
+}
+
+static int statcache_rlock_table(int fd) {
+  return lock_table(fd, F_RDLCK, 0);
+}
+
+static int statcache_unlock_table(int fd) {
+  return lock_table(fd, F_RDLCK, 0);
+}
+#endif /* PR_USE_CTRLS */
+
+static int statcache_wlock_stats(int fd) {
+  return lock_table(fd, F_WRLCK, (6 * sizeof(uint32_t)));
+}
+
+static int statcache_unlock_stats(int fd) {
+  return lock_table(fd, F_UNLCK, (6 * sizeof(uint32_t)));
+}
+
+#ifdef PR_USE_CTRLS
+static uint32_t statcache_stats_get_count(void) {
+  uint32_t count = 0;
+
+  /* count = statcache_table_stats + (0 * sizeof(uint32_t)) */
+  count = *((uint32_t *) statcache_table_stats);
+  return count;
+}
+
+static uint32_t statcache_stats_get_highest(void) {
+  uint32_t highest = 0;
+
+  /* highest = statcache_table_stats + (1 * sizeof(uint32_t)) */
+  highest = *((uint32_t *) ((char *) statcache_table_stats +
+    (1 * sizeof(uint32_t))));
+  return highest;
+}
+
+static uint32_t statcache_stats_get_hits(void) {
+  uint32_t hits = 0;
+
+  /* hits = statcache_table_stats + (2 * sizeof(uint32_t)) */
+  hits = *((uint32_t *) ((char *) statcache_table_stats +
+    (2 * sizeof(uint32_t))));
+  return hits;
+}
+
+static uint32_t statcache_stats_get_misses(void) {
+  uint32_t misses = 0;
+
+  /* misses = statcache_table_stats + (3 * sizeof(uint32_t)) */
+  misses = *((uint32_t *) ((char *) statcache_table_stats +
+    (3 * sizeof(uint32_t))));
+  return misses;
+}
+
+static uint32_t statcache_stats_get_expires(void) {
+  uint32_t expires = 0;
+
+  /* expires = statcache_table_stats + (4 * sizeof(uint32_t)) */
+  expires = *((uint32_t *) ((char *) statcache_table_stats +
+    (4 * sizeof(uint32_t))));
+  return expires;
+}
+
+static uint32_t statcache_stats_get_rejects(void) {
+  uint32_t rejects = 0;
+
+  /* rejects = statcache_table_stats + (5 * sizeof(uint32_t)) */
+  rejects = *((uint32_t *) ((char *) statcache_table_stats +
+    (5 * sizeof(uint32_t))));
+  return rejects;
+}
+#endif /* PR_USE_CTRLS */
+
+static int statcache_stats_incr_count(int32_t incr) {
+  uint32_t *count = NULL, *highest = NULL;
+
+  if (incr == 0) {
+    return 0;
+  }
+
+  /* count = statcache_table_stats + (0 * sizeof(uint32_t)) */
+  count = ((uint32_t *) statcache_table_stats);
+
+  /* highest = statcache_table_stats + (1 * sizeof(uint32_t)) */
+  highest = ((uint32_t *) ((char *) statcache_table_stats) +
+    (1 * sizeof(uint32_t)));
+
+  if (incr < 0) {
+    /* Prevent underflow. */
+    if (*count <= incr) {
+      *count = 0;
+
+    } else {
+      *count += incr;
+    }
+
+  } else {
+    *count += incr;
+
+    if (*count > *highest) {
+      *highest = *count;
+    }
+  }
+
+  return 0;
+}
+
+static int statcache_stats_incr_hits(int32_t incr) {
+  uint32_t *hits = NULL;
+
+  if (incr == 0) {
+    return 0;
+  }
+
+  /* hits = statcache_table_stats + (2 * sizeof(uint32_t)) */
+  hits = ((uint32_t *) ((char *) statcache_table_stats) +
+    (2 * sizeof(uint32_t)));
+
+  /* Prevent underflow. */
+  if (incr < 0 &&
+      *hits <= incr) {
+    *hits = 0;
+
+  } else {
+    *hits += incr;
+  }
+
+  return 0;
+} 
+
+static int statcache_stats_incr_misses(int32_t incr) {
+  uint32_t *misses = NULL;
+ 
+  if (incr == 0) {
+    return 0;
+  }
+ 
+  /* misses = statcache_table_stats + (3 * sizeof(uint32_t)) */
+  misses = ((uint32_t *) ((char *) statcache_table_stats) +
+    (3 * sizeof(uint32_t)));
+
+  /* Prevent underflow. */
+  if (incr < 0 &&
+      *misses <= incr) {
+    *misses = 0;
+
+  } else {
+    *misses += incr;
+  }
+
+  return 0;
+} 
+
+static int statcache_stats_incr_expires(int32_t incr) {
+  uint32_t *expires = NULL;
+ 
+  if (incr == 0) {
+    return 0;
+  }
+ 
+  /* expires = statcache_table_stats + (4 * sizeof(uint32_t)) */
+  expires = ((uint32_t *) ((char *) statcache_table_stats) +
+    (4 * sizeof(uint32_t)));
+
+  /* Prevent underflow. */
+  if (incr < 0 &&
+      *expires <= incr) {
+    *expires = 0;
+
+  } else {
+    *expires += incr;
+  }
+
+  return 0;
+} 
+
+static int statcache_stats_incr_rejects(int32_t incr) {
+  uint32_t *rejects = NULL;
+
+  if (incr == 0) {
+    return 0;
+  }
+
+  /* rejects = statcache_table_stats + (5 * sizeof(uint32_t)) */
+  rejects = ((uint32_t *) ((char *) statcache_table_stats) +
+    (5 * sizeof(uint32_t)));
+
+  /* Prevent underflow. */
+  if (incr < 0 &&
+      *rejects <= incr) {
+    *rejects = 0;
+
+  } else {
+    *rejects += incr;
+  }
+
+  return 0;
+}
+
+/* Data locking routines */
+
+static int get_row_range(uint32_t hash, off_t *row_start, off_t *row_len) {
+  uint32_t row_idx;
+
+  row_idx = hash % statcache_nrows;
+  *row_start = (row_idx * statcache_rowlen);
+  *row_len = statcache_rowlen;
+
+  return 0;
+}
+
+static int lock_row(int fd, int lock_type, uint32_t hash) {
+  struct flock lock;
+  unsigned int nattempts = 1;
+
+  lock.l_type = lock_type;
+  lock.l_whence = 0;
+  get_row_range(hash, &lock.l_start, &lock.l_len);
+
+  pr_trace_msg(trace_channel, 15,
+    "attempt #%u to acquire row %s lock on StatCacheTable fd %d "
+    "(off %lu, len %lu)", nattempts, get_lock_type(&lock), fd,
+    (unsigned long) lock.l_start,
+    (unsigned long) lock.l_len);
+
+  while (fcntl(fd, F_SETLK, &lock) < 0) {
+    int xerrno = errno;
+
+    if (xerrno == EINTR) {
+      pr_signals_handle();
+      continue;
+    }
+
+    pr_trace_msg(trace_channel, 3,
+      "%s lock (attempt #%u) of StatCacheTable fd %d failed: %s",
+      get_lock_type(&lock), nattempts, fd, strerror(xerrno));
+    if (xerrno == EACCES) {
+      struct flock locker;
+
+      /* Get the PID of the process blocking this lock. */
+      if (fcntl(fd, F_GETLK, &locker) == 0) {
+        pr_trace_msg(trace_channel, 3, "process ID %lu has blocking %s lock on "
+          "StatCacheTable fd %d", (unsigned long) locker.l_pid,
+          get_lock_type(&locker), fd);
+      }
+    }
+
+    if (xerrno == EAGAIN ||
+        xerrno == EACCES) {
+      /* Treat this as an interrupted call, call pr_signals_handle() (which
+       * will delay for a few msecs because of EINTR), and try again.
+       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
+       */
+
+      nattempts++;
+      if (nattempts <= STATCACHE_MAX_LOCK_ATTEMPTS) {
+        errno = EINTR;
+
+        pr_signals_handle();
+
+        errno = 0;
+        pr_trace_msg(trace_channel, 15,
+          "attempt #%u to acquire %s row lock on StatCacheTable fd %d",
+          nattempts, get_lock_type(&lock), fd);
+        continue;
+      }
+
+      pr_trace_msg(trace_channel, 15, "unable to acquire %s row lock on "
+        "StatCacheTable fd %d after %u attempts: %s", get_lock_type(&lock),
+        nattempts, fd, strerror(xerrno));
+    }
+
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 15,
+    "acquired %s row lock of StatCacheTable fd %d successfully",
+    get_lock_type(&lock), fd);
+  return 0;
+}
+
+static int statcache_wlock_row(int fd, uint32_t hash) {
+  return lock_row(fd, F_WRLCK, hash);
+}
+
+static int statcache_unlock_row(int fd, uint32_t hash) {
+  return lock_row(fd, F_UNLCK, hash);
+}
+
+/* Table manipulation routines */
+
+/* See http://www.cse.yorku.ca/~oz/hash.html */
+static uint32_t statcache_hash(const char *path, size_t pathlen) {
+  register unsigned int i;
+  uint32_t h = 5381;
+
+  for (i = 0; i < pathlen; i++) {
+    h = ((h << 5) + h) + path[i];
+  }
+
+  /* Strip off the high bit. */
+  h &= ~(1 << 31);
+
+  return h;
+}
+
+/* Add an entry to the table. */
+static int statcache_table_add(int fd, const char *path, size_t pathlen,
+    struct stat *st, int xerrno, uint32_t hash, unsigned char op) {
+  register unsigned int i;
+  uint32_t row_idx, row_start;
+  int found_slot = FALSE, expired_entries = 0;
+  time_t now;
+  struct statcache_entry *sce = NULL;
+
+  if (statcache_table == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  /* Find an open slot in the list for this new entry. */
+  now = time(NULL);
+
+  row_idx = hash % statcache_nrows;
+  row_start = (row_idx * statcache_rowlen);
+
+  for (i = 0; i < STATCACHE_COLS_PER_ROW; i++) {
+    uint32_t col_start;
+
+    pr_signals_handle();
+
+    col_start = (row_start + (i * sizeof(struct statcache_entry)));
+    sce = (((char *) statcache_table_data) + col_start);
+    if (sce->sce_ts == 0) {
+      /* Empty slot */
+      found_slot = TRUE;
+      break;
+    }
+
+    /* If existing item is too old, use this slot.  Note that there
+     * are different expiry rules for negative cache entries (i.e.
+     * errors) than for positive cache entries.
+     */
+    if (sce->sce_errno == 0) {
+      if (now > (sce->sce_ts + statcache_max_positive_age)) {
+        found_slot = TRUE;
+        expired_entries++;
+        break;
+      }
+
+    } else {
+      if (now > (sce->sce_ts + statcache_max_negative_age)) {
+        found_slot = TRUE;
+        expired_entries++;
+        break;
+      }
+    }
+  }
+
+  if (found_slot == FALSE) {
+    if (statcache_wlock_stats(fd) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+
+    statcache_stats_incr_rejects(1);
+
+    if (statcache_unlock_stats(fd) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error un-locking shared memory: %s", strerror(errno));
+    }
+
+    errno = ENOSPC;
+    return -1;
+  }
+
+  if (st != NULL) {
+    pr_trace_msg(trace_channel, 9,
+      "adding entry for path '%s' (hash %lu) at row %lu, col %u "
+      "(op %s, type %s)", path,
+      (unsigned long) hash, (unsigned long) row_idx + 1, i + 1,
+      op == FSIO_FILE_LSTAT ? "LSTAT" : "STAT",
+      S_ISLNK(st->st_mode) ? "symlink" :
+        S_ISDIR(st->st_mode) ? "dir" : "file");
+
+  } else {
+    pr_trace_msg(trace_channel, 9,
+      "adding entry for path '%s' (hash %lu) at row %lu, col %u "
+      "(op %s, errno %d)", path,
+      (unsigned long) hash, (unsigned long) row_idx + 1, i + 1,
+      op == FSIO_FILE_LSTAT ? "LSTAT" : "STAT", xerrno);
+  }
+
+  sce->sce_hash = hash;
+  sce->sce_pathlen = pathlen;
+
+  /* Include trailing NUL. */
+  memcpy(sce->sce_path, path, pathlen + 1);
+  if (st != NULL) {
+    memcpy(&(sce->sce_stat), st, sizeof(struct stat));
+  }
+  sce->sce_errno = xerrno;
+  sce->sce_ts = now;
+  sce->sce_op = op;
+
+  if (statcache_wlock_stats(fd) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  statcache_stats_incr_count(1);
+  if (expired_entries > 0) {
+    statcache_stats_incr_count(-expired_entries);
+    statcache_stats_incr_expires(expired_entries);
+  }
+
+  if (statcache_unlock_stats(fd) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error un-locking shared memory: %s", strerror(errno));
+  }
+
+  return 0;
+}
+
+static int statcache_table_get(int fd, const char *path, size_t pathlen,
+    struct stat *st, int *xerrno, uint32_t hash, unsigned char op) {
+  register unsigned int i;
+  int expired_entries = 0, res = -1;
+  uint32_t row_idx, row_start;
+
+  if (statcache_table == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  row_idx = hash % statcache_nrows;
+  row_start = (row_idx * statcache_rowlen);
+
+  /* Find the matching entry for this path. */
+  for (i = 0; i < STATCACHE_COLS_PER_ROW; i++) {
+    uint32_t col_start;
+    struct statcache_entry *sce;
+
+    pr_signals_handle();
+
+    col_start = (row_start + (i * sizeof(struct statcache_entry)));
+    sce = (((char *) statcache_table_data) + col_start);
+    if (sce->sce_ts > 0) {
+      if (sce->sce_hash == hash) {
+        /* Possible collision; check paths. */
+        if (sce->sce_pathlen == pathlen) {
+
+          /* Include the trailing NUL in the comparison... */
+          if (strncmp(sce->sce_path, path, pathlen + 1) == 0) {
+            time_t now;
+
+            now = time(NULL);
+
+            /* Check the age.  If it's aged out, clear it now, for later use. */
+            if (sce->sce_errno == 0) {
+              if (now > (sce->sce_ts + statcache_max_positive_age)) {
+                pr_trace_msg(trace_channel, 17,
+                  "clearing expired cache entry for path '%s' (hash %lu) "
+                  "at row %lu, col %u: aged %lu secs",
+                  sce->sce_path, (unsigned long) hash,
+                  (unsigned long) row_idx + 1, i + 1,
+                  (unsigned long) (now - sce->sce_ts));
+                sce->sce_ts = 0;
+                expired_entries++;
+                continue;
+              }
+
+            } else {
+              if (now > (sce->sce_ts + statcache_max_negative_age)) {
+                pr_trace_msg(trace_channel, 17,
+                  "clearing expired negative cache entry for path '%s' "
+                  "(hash %lu) at row %lu, col %u: aged %lu secs",
+                  sce->sce_path, (unsigned long) hash,
+                  (unsigned long) row_idx + 1, i + 1,
+                  (unsigned long) (now - sce->sce_ts));
+                sce->sce_ts = 0;
+                expired_entries++;
+                continue;
+              }
+            }
+
+            /* If the ops match, OR if the entry is from a LSTAT AND the entry
+             * is NOT a symlink, we can use it.
+             */
+            if (sce->sce_op == op ||
+                (sce->sce_op == FSIO_FILE_LSTAT &&
+                 S_ISLNK(sce->sce_stat.st_mode) == FALSE)) {
+              /* Found matching entry. */
+              pr_trace_msg(trace_channel, 9,
+                "found entry for path '%s' (hash %lu) at row %lu, col %u",
+                path, (unsigned long) hash, (unsigned long) row_idx + 1, i + 1);
+
+              *xerrno = sce->sce_errno;
+              if (sce->sce_errno == 0) {
+                memcpy(st, &(sce->sce_stat), sizeof(struct stat));
+              }
+
+              res = 0;
+              break;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  if (statcache_wlock_stats(fd) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  if (res == 0) {
+    statcache_stats_incr_hits(1);
+
+  } else {
+    statcache_stats_incr_misses(1);
+  }
+
+  if (expired_entries > 0) {
+    statcache_stats_incr_count(-expired_entries);
+    statcache_stats_incr_expires(expired_entries);
+  }
+
+  if (statcache_unlock_stats(fd) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error un-locking shared memory: %s", strerror(errno));
+  }
+
+  if (res < 0) {
+    errno = ENOENT;
+  }
+
+  return res;
+}
+
+static int statcache_table_remove(int fd, const char *path, size_t pathlen,
+    uint32_t hash) {
+  register unsigned int i;
+  uint32_t row_idx, row_start;
+  int removed_entries = 0, res = -1;
+
+  if (statcache_table == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  row_idx = hash % statcache_nrows;
+  row_start = (row_idx * statcache_rowlen);
+
+  /* Find the matching entry for this path. */
+  for (i = 0; i < STATCACHE_COLS_PER_ROW; i++) {
+    uint32_t col_start;
+    struct statcache_entry *sce;
+
+    pr_signals_handle();
+
+    col_start = (row_start + (i * sizeof(struct statcache_entry)));
+    sce = (((char *) statcache_table_data) + col_start);
+    if (sce->sce_ts > 0) {
+      if (sce->sce_hash == hash) {
+        /* Possible collision; check paths. */
+        if (sce->sce_pathlen == pathlen) {
+
+          /* Include the trailing NUL in the comparison... */
+          if (strncmp(sce->sce_path, path, pathlen + 1) == 0) {
+            /* Found matching entry.  Clear it by zeroing timestamp field. */
+
+            pr_trace_msg(trace_channel, 9,
+              "removing entry for path '%s' (hash %lu) at row %lu, col %u",
+              path, (unsigned long) hash, (unsigned long) row_idx + 1, i + 1);
+
+            sce->sce_ts = 0;
+            removed_entries++;
+            res = 0;
+
+            /* Rather than returning now, we finish iterating through
+             * the bucket, in order to clear out multiple entries for
+             * the same path (e.g. one for LSTAT, and another for STAT).
+             */
+          }
+        }
+      }
+    }
+  }
+
+  if (res == 0) {
+    if (statcache_wlock_stats(fd) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+
+    if (removed_entries > 0) {
+      statcache_stats_incr_count(-removed_entries);
+    }
+
+    if (statcache_unlock_stats(fd) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error un-locking shared memory: %s", strerror(errno));
+    }
+
+  } else {
+    errno = ENOENT;
+  }
+
+  return res;
+}
+
+static const char *statcache_get_canon_path(pool *p, const char *path,
+    size_t *pathlen) {
+  int res;
+  char *canon_path = NULL, *interp_path = NULL;
+  size_t canon_pathlen = PR_TUNABLE_PATH_MAX + 1;
+
+  /* Handle any '~' interpolation needed. */
+  interp_path = dir_interpolate(p, path);
+  if (interp_path == NULL) {
+    /* This happens when the '~' was just that, and did NOT refer to
+     * any known user.
+     */
+    interp_path = (char *) path;
+  }
+
+  canon_path = palloc(p, canon_pathlen);
+  res = pr_fs_dircat(canon_path, canon_pathlen, pr_fs_getcwd(), interp_path);
+  if (res < 0) {
+    errno = ENOMEM;
+    return NULL;
+  }
+
+  *pathlen = strlen(canon_path);
+  return canon_path;
+}
+
+/* FSIO callbacks
+ */
+
+static int statcache_fsio_stat(pr_fs_t *fs, const char *path,
+    struct stat *st) {
+  int res, tab_fd, xerrno = 0;
+  const char *canon_path = NULL;
+  size_t canon_pathlen = 0;
+  pool *p;
+  uint32_t hash;
+
+  p = make_sub_pool(statcache_pool);
+  pr_pool_tag(p, "statcache_fsio_stat sub-pool");
+  canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+  if (canon_path == NULL) {
+    xerrno = errno;
+
+    destroy_pool(p);
+    errno = xerrno;
+    return -1;
+  }
+
+  hash = statcache_hash(canon_path, canon_pathlen);
+  tab_fd = statcache_tabfh->fh_fd;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  res = statcache_table_get(tab_fd, canon_path, canon_pathlen, st, &xerrno,
+    hash, FSIO_FILE_STAT);
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  if (res == 0) {
+    if (xerrno != 0) {
+      res = -1;
+
+    } else {
+      pr_trace_msg(trace_channel, 11,
+        "using cached stat for path '%s'", canon_path);
+    }
+
+    destroy_pool(p);
+    errno = xerrno;
+    return res;
+  }
+
+  res = stat(path, st);
+  xerrno = errno;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  if (res < 0) {
+    if (statcache_max_negative_age > 0) {
+      /* Negatively cache the failed stat(2). */
+      if (statcache_table_add(tab_fd, canon_path, canon_pathlen, NULL, xerrno,
+          hash, FSIO_FILE_STAT) < 0) {
+        pr_trace_msg(trace_channel, 3, "error adding entry for path '%s': %s",
+          canon_path, strerror(errno));
+      }
+    }
+
+  } else {
+    if (statcache_table_add(tab_fd, canon_path, canon_pathlen, st, 0, hash,
+        FSIO_FILE_STAT) < 0) {
+      pr_trace_msg(trace_channel, 3, "error adding entry for path '%s': %s",
+        canon_path, strerror(errno));
+    }
+  }
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  destroy_pool(p);
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_fstat(pr_fh_t *fh, int fd, struct stat *st) {
+  int res, tab_fd, xerrno = 0;
+  size_t pathlen = 0;
+  uint32_t hash;
+
+  /* XXX Core FSIO API should have an fh_pathlen member.
+   *
+   * XXX Core FSIO API should have an fh_notes table, so that e.g.
+   * mod_statcache could generate its hash for this handle only once, and
+   * stash it in the table.
+   */
+
+  pathlen = strlen(fh->fh_path);
+  hash = statcache_hash(fh->fh_path, pathlen);
+  tab_fd = statcache_tabfh->fh_fd;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  res = statcache_table_get(tab_fd, fh->fh_path, pathlen, st, &xerrno, hash,
+    FSIO_FILE_STAT);
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  if (res == 0) {
+    if (xerrno != 0) {
+      res = -1;
+
+    } else {
+      pr_trace_msg(trace_channel, 11,
+        "using cached stat for path '%s'", fh->fh_path);
+    }
+
+    errno = xerrno;
+    return res;
+  }
+
+  res = fstat(fd, st);
+  xerrno = errno;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  if (res < 0) {
+    if (statcache_max_negative_age > 0) {
+      /* Negatively cache the failed fstat(2). */
+      if (statcache_table_add(tab_fd, fh->fh_path, pathlen, NULL, xerrno,
+          hash, FSIO_FILE_STAT) < 0) {
+        pr_trace_msg(trace_channel, 3, "error adding entry for path '%s': %s",
+          fh->fh_path, strerror(errno));
+      }
+    }
+
+  } else {
+    if (statcache_table_add(tab_fd, fh->fh_path, pathlen, st, 0, hash,
+        FSIO_FILE_STAT) < 0) {
+      pr_trace_msg(trace_channel, 3, "error adding entry for path '%s': %s",
+        fh->fh_path, strerror(errno));
+    }
+  }
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_lstat(pr_fs_t *fs, const char *path,
+    struct stat *st) {
+  int res, tab_fd, xerrno = 0;
+  const char *canon_path = NULL;
+  size_t canon_pathlen = 0;
+  pool *p;
+  uint32_t hash;
+
+  p = make_sub_pool(statcache_pool);
+  pr_pool_tag(p, "statcache_fsio_lstat sub-pool");
+  canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+  if (canon_path == NULL) {
+    xerrno = errno;
+    
+    destroy_pool(p);
+    errno = xerrno; 
+    return -1;
+  }
+
+  hash = statcache_hash(canon_path, canon_pathlen);
+  tab_fd = statcache_tabfh->fh_fd;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  res = statcache_table_get(tab_fd, canon_path, canon_pathlen, st, &xerrno,
+    hash, FSIO_FILE_LSTAT);
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  if (res == 0) {
+    if (xerrno != 0) {
+      res = -1;
+
+    } else {
+      pr_trace_msg(trace_channel, 11,
+        "using cached lstat for path '%s'", canon_path);
+    }
+
+    destroy_pool(p);
+    errno = xerrno;
+    return res;
+  }
+
+  res = lstat(path, st);
+  xerrno = errno;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  if (res < 0) {
+    if (statcache_max_negative_age > 0) {
+      /* Negatively cache the failed lstat(2). */
+      if (statcache_table_add(tab_fd, canon_path, canon_pathlen, NULL, xerrno,
+          hash, FSIO_FILE_LSTAT) < 0) {
+        pr_trace_msg(trace_channel, 3, "error adding entry for path '%s': %s",
+          canon_path, strerror(errno));
+      }
+    }
+
+  } else {
+    if (statcache_table_add(tab_fd, canon_path, canon_pathlen, st, 0, hash,
+        FSIO_FILE_LSTAT) < 0) {
+      pr_trace_msg(trace_channel, 3, "error adding entry for path '%s': %s",
+        canon_path, strerror(errno));
+    }
+  }
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  destroy_pool(p);
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_rename(pr_fs_t *fs, const char *rnfm,
+    const char *rnto) {
+  int res, xerrno;
+
+  res = rename(rnfm, rnto);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_rnfm = NULL, *canon_rnto = NULL;
+    size_t canon_rnfmlen = 0, canon_rntolen = 0;
+    pool *p;
+    uint32_t hash_rnfm, hash_rnto;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_rename sub-pool");
+
+    canon_rnfm = statcache_get_canon_path(p, rnfm, &canon_rnfmlen);
+    if (canon_rnfm == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    canon_rnto = statcache_get_canon_path(p, rnto, &canon_rntolen);
+    if (canon_rnto == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash_rnfm = statcache_hash(canon_rnfm, canon_rnfmlen);
+    hash_rnto = statcache_hash(canon_rnto, canon_rntolen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash_rnfm) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }   
+
+    (void) statcache_table_remove(tab_fd, canon_rnfm, canon_rnfmlen, hash_rnfm);
+
+    if (statcache_unlock_row(tab_fd, hash_rnfm) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    if (statcache_wlock_row(tab_fd, hash_rnto) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+
+    (void) statcache_table_remove(tab_fd, canon_rnto, canon_rntolen, hash_rnto);
+
+    if (statcache_unlock_row(tab_fd, hash_rnto) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_unlink(pr_fs_t *fs, const char *path) {
+  int res, xerrno;
+
+  res = unlink(path);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_path = NULL;
+    size_t canon_pathlen = 0;
+    pool *p;
+    uint32_t hash;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_unlink sub-pool");
+    canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+    if (canon_path == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash = statcache_hash(canon_path, canon_pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+
+    (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_open(pr_fh_t *fh, const char *path, int flags) {
+  int res, xerrno;
+
+  res = open(path, flags);
+  xerrno = errno;
+
+  if (res >= 0) {
+    /* Clear the cache for this patch, but only if O_CREAT or O_TRUNC are
+     * present.
+     */
+    if ((flags & O_CREAT) ||
+        (flags & O_TRUNC)) {
+      int tab_fd;
+      const char *canon_path = NULL;
+      size_t canon_pathlen = 0;
+      pool *p;
+      uint32_t hash;
+
+      p = make_sub_pool(statcache_pool);
+      pr_pool_tag(p, "statcache_fsio_open sub-pool");
+      canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+      if (canon_path == NULL) {
+        xerrno = errno;
+
+        destroy_pool(p);
+        errno = xerrno;
+        return res;
+      }
+
+      hash = statcache_hash(canon_path, canon_pathlen);
+      tab_fd = statcache_tabfh->fh_fd;
+
+      if (statcache_wlock_row(tab_fd, hash) < 0) {
+        pr_trace_msg(trace_channel, 3,
+          "error write-locking shared memory: %s", strerror(errno));
+      } 
+
+      pr_trace_msg(trace_channel, 14,
+        "removing entry for path '%s' due to open(2) flags", canon_path);
+      (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+      if (statcache_unlock_row(tab_fd, hash) < 0) {
+        pr_trace_msg(trace_channel, 3,
+          "error unlocking shared memory: %s", strerror(errno));
+      }
+
+      destroy_pool(p);
+    }
+  } 
+
+  errno = xerrno;  
+  return res;
+}
+
+static int statcache_fsio_write(pr_fh_t *fh, int fd, const char *buf,
+    size_t buflen) {
+  int res, xerrno;
+
+  res = write(fd, buf, buflen);
+  xerrno = errno;
+
+  if (res > 0) {
+    int tab_fd;
+    size_t pathlen = 0;
+    uint32_t hash;
+
+    pathlen = strlen(fh->fh_path);
+    hash = statcache_hash(fh->fh_path, pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+ 
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+  
+    (void) statcache_table_remove(tab_fd, fh->fh_path, pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    } 
+  }
+
+  errno = xerrno; 
+  return res;
+}
+
+static int statcache_fsio_truncate(pr_fs_t *fs, const char *path, off_t len) {
+  int res, xerrno;
+
+  res = truncate(path, len);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_path = NULL;
+    size_t canon_pathlen = 0;
+    pool *p;
+    uint32_t hash;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_truncate sub-pool");
+    canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+    if (canon_path == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash = statcache_hash(canon_path, canon_pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;  
+  return res;
+}
+
+static int statcache_fsio_ftruncate(pr_fh_t *fh, int fd, off_t len) {
+  int res, xerrno;
+
+  res = ftruncate(fd, len);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    size_t pathlen = 0;
+    uint32_t hash;
+
+    pathlen = strlen(fh->fh_path);
+    hash = statcache_hash(fh->fh_path, pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, fh->fh_path, pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    } 
+  }
+
+  errno = xerrno; 
+  return res;
+}
+
+static int statcache_fsio_chmod(pr_fs_t *fs, const char *path, mode_t mode) {
+  int res, xerrno;
+
+  res = chmod(path, mode);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_path = NULL;
+    size_t canon_pathlen = 0;
+    pool *p;
+    uint32_t hash;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_chmod sub-pool");
+    canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+    if (canon_path == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash = statcache_hash(canon_path, canon_pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_fchmod(pr_fh_t *fh, int fd, mode_t mode) {
+  int res, xerrno;
+
+  res = fchmod(fd, mode);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    size_t pathlen = 0;
+    uint32_t hash;
+
+    pathlen = strlen(fh->fh_path);
+    hash = statcache_hash(fh->fh_path, pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, fh->fh_path, pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    } 
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_chown(pr_fs_t *fs, const char *path, uid_t uid,
+    gid_t gid) {
+  int res, xerrno;
+
+  res = chown(path, uid, gid);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_path = NULL;
+    size_t canon_pathlen = 0;
+    pool *p;
+    uint32_t hash;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_chown sub-pool");
+    canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+    if (canon_path == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash = statcache_hash(canon_path, canon_pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_fchown(pr_fh_t *fh, int fd, uid_t uid, gid_t gid) {
+  int res, xerrno;
+
+  res = fchown(fd, uid, gid);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    size_t pathlen = 0;
+    uint32_t hash;
+
+    pathlen = strlen(fh->fh_path);
+    hash = statcache_hash(fh->fh_path, pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, fh->fh_path, pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    } 
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+#if PROFTPD_VERSION_NUMBER >= 0x0001030407
+static int statcache_fsio_lchown(pr_fs_t *fs, const char *path, uid_t uid,
+    gid_t gid) {
+  int res, xerrno;
+
+  res = lchown(path, uid, gid);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_path = NULL;
+    size_t canon_pathlen = 0;
+    pool *p;
+    uint32_t hash;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_lchown sub-pool");
+    canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+    if (canon_path == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash = statcache_hash(canon_path, canon_pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;
+  return res;
+}
+#endif /* ProFTPD 1.3.4c or later */
+
+static int statcache_fsio_utimes(pr_fs_t *fs, const char *path,
+    struct timeval *tvs) {
+  int res, xerrno;
+
+  res = utimes(path, tvs);
+  xerrno = errno;
+
+  if (res == 0) {
+    int tab_fd;
+    const char *canon_path = NULL;
+    size_t canon_pathlen = 0;
+    pool *p;
+    uint32_t hash;
+
+    p = make_sub_pool(statcache_pool);
+    pr_pool_tag(p, "statcache_fsio_utimes sub-pool");
+    canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+    if (canon_path == NULL) {
+      xerrno = errno;
+
+      destroy_pool(p);
+      errno = xerrno;
+      return res;
+    }
+
+    hash = statcache_hash(canon_path, canon_pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    }
+
+    destroy_pool(p);
+  }
+
+  errno = xerrno;
+  return res;
+}
+
+static int statcache_fsio_futimes(pr_fh_t *fh, int fd, struct timeval *tvs) {
+#ifdef HAVE_FUTIMES
+  int res, xerrno;
+
+  /* Check for an ENOSYS errno; if so, fallback to using fsio_utimes.  Some
+   * platforms will provide a futimes(2) stub which does not actually do
+   * anything.
+   */
+  res = futimes(fd, tvs);
+  xerrno = errno;
+
+  if (res < 0 &&
+      xerrno == ENOSYS) {
+    return statcache_fsio_utimes(fh->fh_fs, fh->fh_path, tvs);
+  }
+
+  if (res == 0) {
+    int tab_fd;
+    size_t pathlen = 0;
+    uint32_t hash;
+
+    pathlen = strlen(fh->fh_path);
+    hash = statcache_hash(fh->fh_path, pathlen);
+    tab_fd = statcache_tabfh->fh_fd;
+
+    if (statcache_wlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error write-locking shared memory: %s", strerror(errno));
+    }
+ 
+    (void) statcache_table_remove(tab_fd, fh->fh_path, pathlen, hash);
+
+    if (statcache_unlock_row(tab_fd, hash) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking shared memory: %s", strerror(errno));
+    } 
+  }
+
+  errno = xerrno;
+  return res;
+#else
+  return statcache_fsio_utimes(fh->fh_fs, fh->fh_path, tvs);
+#endif /* HAVE_FUTIMES */
+}
+
+#ifdef PR_USE_CTRLS
+/* Controls handlers
+ */
+
+static int statcache_handle_statcache(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+  /* Check the ban ACL */
+  if (!pr_ctrls_check_acl(ctrl, statcache_acttab, "statcache")) {
+
+    /* Access denied */
+    pr_ctrls_add_response(ctrl, "access denied");
+    return -1;
+  }
+
+  /* Sanity check */
+  if (reqargv == NULL) {
+    pr_ctrls_add_response(ctrl, "missing parameters");
+    return -1;
+  }
+
+  if (statcache_engine != TRUE) {
+    pr_ctrls_add_response(ctrl, MOD_STATCACHE_VERSION " not enabled");
+    return -1;
+  }
+
+  /* Check for options. */
+  pr_getopt_reset();
+
+  if (strcmp(reqargv[0], "info") == 0) {
+    uint32_t count, highest, hits, misses, expires, rejects;
+    float current_usage = 0.0, highest_usage = 0.0, hit_rate = 0.0;
+
+    if (statcache_rlock_stats(statcache_tabfh->fh_fd) < 0) {
+      pr_ctrls_add_response(ctrl, "error locking shared memory: %s",
+        strerror(errno));
+      return -1;
+    }
+
+    count = statcache_stats_get_count();
+    highest = statcache_stats_get_highest();
+    hits = statcache_stats_get_hits();
+    misses = statcache_stats_get_misses();
+    expires = statcache_stats_get_expires();
+    rejects = statcache_stats_get_rejects();
+
+    if (statcache_unlock_stats(statcache_tabfh->fh_fd) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error un-locking shared memory: %s", strerror(errno));
+    }
+
+    current_usage = (((float) count / (float) statcache_capacity) * 100.0);
+    highest_usage = (((float) highest / (float) statcache_capacity) * 100.0);
+    if ((hits + misses) > 0) {
+      hit_rate = (((float) hits / (float) (hits + misses)) * 100.0);
+    }
+
+    pr_log_debug(DEBUG7, MOD_STATCACHE_VERSION
+      ": showing statcache statistics");
+
+    pr_ctrls_add_response(ctrl,
+      " hits %lu, misses %lu: %02.1f%% hit rate",
+      (unsigned long) hits, (unsigned long) misses, hit_rate);
+    pr_ctrls_add_response(ctrl,
+      "   expires %lu, rejects %lu", (unsigned long) expires,
+      (unsigned long) rejects);
+    pr_ctrls_add_response(ctrl, " current count: %lu (of %lu) (%02.1f%% usage)",
+      (unsigned long) count, (unsigned long) statcache_capacity, current_usage);
+    pr_ctrls_add_response(ctrl, " highest count: %lu (of %lu) (%02.1f%% usage)",
+      (unsigned long) highest, (unsigned long) statcache_capacity,
+      highest_usage);
+
+  } else if (strcmp(reqargv[0], "dump") == 0) {
+    register unsigned int i;
+    time_t now;
+
+    if (statcache_rlock_table(statcache_tabfh->fh_fd) < 0) {
+      pr_ctrls_add_response(ctrl, "error locking shared memory: %s",
+        strerror(errno));
+      return -1;
+    }
+
+    pr_log_debug(DEBUG7, MOD_STATCACHE_VERSION ": dumping statcache");
+
+    pr_ctrls_add_response(ctrl, "StatCache Contents:");
+    now = time(NULL);
+
+    for (i = 0; i < statcache_nrows; i++) {
+      register unsigned int j;
+      unsigned long row_start;
+
+      pr_ctrls_add_response(ctrl, "  Row %u:", i + 1);
+      row_start = (i * statcache_rowlen);
+
+      for (j = 0; j < STATCACHE_COLS_PER_ROW; j++) {
+        unsigned long col_start;
+        struct statcache_entry *sce;
+
+        pr_signals_handle();
+
+        col_start = (row_start + (j * sizeof(struct statcache_entry)));
+        sce = (((char *) statcache_table_data) + col_start);
+        if (sce->sce_ts > 0) {
+          if (sce->sce_errno == 0) {
+            pr_ctrls_add_response(ctrl, "    Col %u: '%s' (%u secs old)",
+              j + 1, sce->sce_path, (unsigned int) (now - sce->sce_ts));
+
+          } else {
+            pr_ctrls_add_response(ctrl, "    Col %u: '%s' (error: %s)",
+              j + 1, sce->sce_path, strerror(sce->sce_errno));
+          }
+
+        } else {
+          pr_ctrls_add_response(ctrl, "    Col %u: <empty>", j + 1);
+        }
+      }
+    }
+
+    statcache_unlock_table(statcache_tabfh->fh_fd);
+
+  } else {
+    pr_ctrls_add_response(ctrl, "unknown statcache action requested: '%s'",
+      reqargv[0]);
+    return -1;
+  }
+
+  return 0;
+}
+
+#endif /* PR_USE_CTRLS */
+
+/* Configuration handlers
+ */
+
+/* usage: StatCacheCapacity count */
+MODRET set_statcachecapacity(cmd_rec *cmd) {
+  int capacity;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  capacity = atoi(cmd->argv[1]);
+  if (capacity < STATCACHE_COLS_PER_ROW) {
+    char str[32];
+
+    memset(str, '\0', sizeof(str));
+    snprintf(str, sizeof(str), "%d", (int) STATCACHE_COLS_PER_ROW);
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "parameter must be ", str,
+      " or greater", NULL));
+  }
+
+  /* Always round UP to the nearest multiple of STATCACHE_COLS_PER_ROW. */
+  if (capacity % STATCACHE_COLS_PER_ROW != 0) {
+    int factor;
+
+    factor = (capacity / (int) STATCACHE_COLS_PER_ROW);
+    capacity = ((factor * (int) STATCACHE_COLS_PER_ROW) +
+      (int) STATCACHE_COLS_PER_ROW);
+  }
+
+  statcache_capacity = capacity;
+  return PR_HANDLED(cmd);
+}
+
+/* usage: StatCacheControlsACLs actions|all allow|deny user|group list */
+MODRET set_statcachectrlsacls(cmd_rec *cmd) {
+#ifdef PR_USE_CTRLS
+  char *bad_action = NULL, **actions = NULL;
+
+  CHECK_ARGS(cmd, 4);
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  /* We can cheat here, and use the ctrls_parse_acl() routine to
+   * separate the given string...
+   */
+  actions = ctrls_parse_acl(cmd->tmp_pool, cmd->argv[1]);
+
+  /* Check the second parameter to make sure it is "allow" or "deny" */
+  if (strcmp(cmd->argv[2], "allow") != 0 &&
+      strcmp(cmd->argv[2], "deny") != 0) {
+    CONF_ERROR(cmd, "second parameter must be 'allow' or 'deny'");
+  }
+
+  /* Check the third parameter to make sure it is "user" or "group" */
+  if (strcmp(cmd->argv[3], "user") != 0 &&
+      strcmp(cmd->argv[3], "group") != 0) {
+    CONF_ERROR(cmd, "third parameter must be 'user' or 'group'");
+  }
+
+  bad_action = pr_ctrls_set_module_acls(statcache_acttab, statcache_pool,
+    actions, cmd->argv[2], cmd->argv[3], cmd->argv[4]);
+  if (bad_action != NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown action: '",
+      bad_action, "'", NULL));
+  }
+
+  return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, "requires Controls support (use --enable-ctrls)");
+#endif /* PR_USE_CTRLS */
+}
+
+/* usage: StatCacheEngine on|off */
+MODRET set_statcacheengine(cmd_rec *cmd) {
+  int engine = -1;
+  config_rec *c;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  engine = get_boolean(cmd, 1);
+  if (engine == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  statcache_engine = engine;
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = engine;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: StatCacheMaxAge secs */
+MODRET set_statcachemaxage(cmd_rec *cmd) {
+  int positive_age;
+
+  if (cmd->argc < 2 ||
+      cmd->argc > 3) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  positive_age = atoi(cmd->argv[1]);
+  if (positive_age <= 0) {
+    CONF_ERROR(cmd, "positive-age parameter must be 1 or greater");
+  }
+
+  if (cmd->argc == 2) {
+    statcache_max_positive_age = statcache_max_negative_age = positive_age;
+
+  } else {
+    int negative_age;
+
+    negative_age = atoi(cmd->argv[2]);
+    if (negative_age < 0) {
+      negative_age = 0;
+    }
+
+    statcache_max_positive_age = positive_age;
+    statcache_max_negative_age = negative_age;
+  }
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: StatCacheTable path */
+MODRET set_statcachetable(cmd_rec *cmd) {
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  if (pr_fs_valid_path(cmd->argv[1]) < 0) {
+    CONF_ERROR(cmd, "must be an absolute path");
+  }
+
+  statcache_table_path = pstrdup(statcache_pool, cmd->argv[1]);
+  return PR_HANDLED(cmd);
+}
+
+/* Command handlers
+ */
+
+MODRET statcache_post_pass(cmd_rec *cmd) {
+  pr_fs_t *fs;
+  const char *proto;
+
+  if (statcache_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  /* Unmount the default/system FS, so that our FS is used for relative
+   * paths, too.
+   */
+  (void) pr_unmount_fs("/", NULL);
+
+  fs = pr_register_fs(statcache_pool, "statcache", "/");
+  if (fs == NULL) {
+    pr_log_debug(DEBUG3, MOD_STATCACHE_VERSION
+      ": error registering 'statcache' fs: %s", strerror(errno));
+    statcache_engine = FALSE;
+    return PR_DECLINED(cmd);
+  }
+
+  /* Add the module's custom FS callbacks here. */
+  fs->stat = statcache_fsio_stat;
+  fs->fstat = statcache_fsio_fstat;
+  fs->lstat = statcache_fsio_lstat;
+  fs->rename = statcache_fsio_rename;
+  fs->unlink = statcache_fsio_unlink;
+  fs->open = statcache_fsio_open;;
+  fs->truncate = statcache_fsio_truncate;
+  fs->ftruncate = statcache_fsio_ftruncate;
+  fs->write = statcache_fsio_write;
+  fs->chmod = statcache_fsio_chmod;
+  fs->fchmod = statcache_fsio_fchmod;
+  fs->chown = statcache_fsio_chown;
+  fs->fchown = statcache_fsio_fchown;
+#if PROFTPD_VERSION_NUMBER >= 0x0001030407
+  fs->lchown = statcache_fsio_lchown;
+#endif /* ProFTPD 1.3.4c or later */
+  fs->utimes = statcache_fsio_utimes;
+  fs->futimes = statcache_fsio_futimes;
+
+  pr_fs_setcwd(pr_fs_getvwd());
+  pr_fs_clear_cache();
+
+  pr_event_register(&statcache_module, "fs.statcache.clear",
+    statcache_fs_statcache_clear_ev, NULL);
+
+  /* If we are handling an SSH2 session, then we need to disable all
+   * negative caching; something about ProFTPD's stat caching interacting
+   * with mod_statcache's caching, AND mod_sftp's dispatching through
+   * the main FTP handlers, causes unexpected behavior.
+   */
+
+  proto = pr_session_get_protocol(0);
+  if (strncmp(proto, "ssh2", 5) == 0 ||
+      strncmp(proto, "sftp", 5) == 0 ||
+      strncmp(proto, "scp", 4) == 0) {
+    pr_trace_msg(trace_channel, 9,
+      "disabling negative caching for %s protocol", proto);
+    statcache_max_negative_age = 0;
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+#ifdef MADV_WILLNEED
+MODRET statcache_pre_list(cmd_rec *cmd) {
+  int res;
+
+  if (statcache_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  res = madvise(statcache_table, statcache_tablesz, MADV_WILLNEED);
+  if (res < 0) {
+    pr_log_debug(DEBUG5, MOD_STATCACHE_VERSION
+      ": madvise(2) error with MADV_WILLNEED: %s", strerror(errno));
+  }
+
+  return PR_DECLINED(cmd);
+}
+#endif /* MADV_WILLNEED */
+
+/* Event handlers
+ */
+
+static void statcache_fs_statcache_clear_ev(const void *event_data,
+    void *user_data) {
+  int tab_fd;
+  const char *canon_path = NULL, *path;
+  size_t canon_pathlen = 0;
+  pool *p;
+  uint32_t hash;
+
+  path = event_data;
+  if (path == NULL) {
+    return;
+  }
+
+  p = make_sub_pool(statcache_pool);
+  pr_pool_tag(p, "statcache_clear_ev sub-pool");
+  canon_path = statcache_get_canon_path(p, path, &canon_pathlen);
+  if (canon_path == NULL) {
+    destroy_pool(p);
+    return;
+  }
+
+  hash = statcache_hash(canon_path, canon_pathlen);
+  tab_fd = statcache_tabfh->fh_fd;
+
+  if (statcache_wlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error write-locking shared memory: %s", strerror(errno));
+  }
+
+  pr_trace_msg(trace_channel, 14,
+    "removing entry for path '%s' due to event", canon_path);
+  (void) statcache_table_remove(tab_fd, canon_path, canon_pathlen, hash);
+
+  if (statcache_unlock_row(tab_fd, hash) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking shared memory: %s", strerror(errno));
+  }
+
+  destroy_pool(p);
+}
+
+static void statcache_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&statcache_module, "core.session-reinit",
+    statcache_sess_reinit_ev);
+
+  /* Restore defaults */
+  statcache_engine = FALSE;
+
+  res = statcache_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&statcache_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
+static void statcache_shutdown_ev(const void *event_data, void *user_data) {
+
+  /* Remove the mmap from the system.  We can only do this reliably
+   * when the standalone daemon process exits; if it's an inetd process,
+   * there many be other proftpd processes still running.
+   */
+
+  if (getpid() == mpid &&
+      ServerType == SERVER_STANDALONE &&
+      (statcache_table != NULL && statcache_tabfh->fh_fd >= 0)) {
+    int res;
+
+    res = munmap(statcache_table, statcache_tablesz);
+    if (res < 0) {
+      pr_log_debug(DEBUG1, MOD_STATCACHE_VERSION
+        ": error detaching shared memory: %s", strerror(errno));
+
+    } else {
+      pr_log_debug(DEBUG7, MOD_STATCACHE_VERSION
+        ": detached %lu bytes of shared memory for StatCacheTable '%s'",
+        (unsigned long) statcache_tablesz, statcache_table_path);
+    }
+
+    res = pr_fsio_close(statcache_tabfh);
+    if (res < 0) {
+      pr_log_debug(DEBUG1, MOD_STATCACHE_VERSION
+        ": error closing StatCacheTable '%s': %s", statcache_table_path,
+        strerror(errno));
+    }
+  }
+}
+
+#if defined(PR_SHARED_MODULE)
+static void statcache_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_statcache.c", (const char *) event_data) == 0) {
+#ifdef PR_USE_CTRLS
+    register unsigned int i;
+
+    for (i = 0; statcache_acttab[i].act_action; i++) {
+      (void) pr_ctrls_unregister(&statcache_module,
+        statcache_acttab[i].act_action);
+    }
+#endif /* PR_USE_CTRLS */
+
+    pr_event_unregister(&statcache_module, NULL, NULL);
+
+    if (statcache_tabfh) {
+      (void) pr_fsio_close(statcache_tabfh);
+      statcache_tabfh = NULL;
+    }
+
+    if (statcache_pool) {
+      destroy_pool(statcache_pool);
+      statcache_pool = NULL;
+    }
+
+    statcache_engine = FALSE;
+  }
+}
+#endif /* PR_SHARED_MODULE */
+
+static void statcache_postparse_ev(const void *event_data, void *user_data) {
+  size_t tablesz;
+  void *table;
+  int xerrno;
+  struct stat st;
+
+  if (statcache_engine == FALSE) {
+    return;
+  }
+
+  /* Make sure the StatCacheTable exists. */
+  if (statcache_table_path == NULL) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_STATCACHE_VERSION
+      ": missing required StatCacheTable configuration");
+    pr_session_disconnect(&statcache_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+      NULL);
+  }
+
+  PRIVS_ROOT
+  statcache_tabfh = pr_fsio_open(statcache_table_path, O_RDWR|O_CREAT);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (statcache_tabfh == NULL) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_STATCACHE_VERSION
+      ": unable to open StatCacheTable '%s': %s", statcache_table_path,
+      strerror(xerrno));
+    pr_session_disconnect(&statcache_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+      NULL);
+  }
+
+  if (pr_fsio_fstat(statcache_tabfh, &st) < 0) {
+    xerrno = errno;
+
+    pr_log_pri(PR_LOG_NOTICE, MOD_STATCACHE_VERSION
+      ": unable to stat StatCacheTable '%s': %s", statcache_table_path,
+      strerror(xerrno));
+    pr_fsio_close(statcache_tabfh);
+    statcache_tabfh = NULL;
+    pr_session_disconnect(&statcache_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+      NULL);
+  }
+
+  if (S_ISDIR(st.st_mode)) {
+    xerrno = EISDIR;
+
+    pr_log_pri(PR_LOG_NOTICE, MOD_STATCACHE_VERSION
+      ": unable to stat StatCacheTable '%s': %s", statcache_table_path,
+      strerror(xerrno));
+    pr_fsio_close(statcache_tabfh);
+    statcache_tabfh = NULL;
+    pr_session_disconnect(&statcache_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+      NULL);
+  }
+
+  if (statcache_tabfh->fh_fd <= STDERR_FILENO) {
+    int usable_fd;
+
+    usable_fd = pr_fs_get_usable_fd(statcache_tabfh->fh_fd);
+    if (usable_fd < 0) {
+      pr_log_debug(DEBUG0, MOD_STATCACHE_VERSION
+        "warning: unable to find good fd for StatCacheTable %s: %s",
+        statcache_table_path, strerror(errno));
+
+    } else {
+      close(statcache_tabfh->fh_fd);
+      statcache_tabfh->fh_fd = usable_fd;
+    }
+  } 
+
+  /* The size of the table, in bytes, is:
+   *
+   *  sizeof(header) + sizeof(data)
+   *
+   * thus:
+   *
+   *  header = 6 * sizeof(uint32_t)
+   *  data = capacity * sizeof(struct statcache_entry)
+   */
+
+  tablesz = (6 * sizeof(uint32_t)) +
+    (statcache_capacity * sizeof(struct statcache_entry));
+
+  /* Get the shm for storing all of our stat info. */
+  table = statcache_get_shm(statcache_tabfh, tablesz);
+  if (table == NULL) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_STATCACHE_VERSION
+      ": unable to get shared memory for StatCacheTable '%s': %s",
+      statcache_table_path, strerror(errno));
+    pr_session_disconnect(&statcache_module, PR_SESS_DISCONNECT_BAD_CONFIG,
+      NULL);
+  }
+
+  pr_trace_msg(trace_channel, 9,
+    "allocated %lu bytes of shared memory for %u cache entries",
+    (unsigned long) tablesz, statcache_capacity);
+
+  statcache_table = table;
+  statcache_tablesz = tablesz;
+  statcache_table_stats = statcache_table;
+  statcache_table_data = ((struct statcache_entry *) statcache_table) +
+    (6 * sizeof(uint32_t));
+
+  statcache_nrows = (statcache_capacity / STATCACHE_COLS_PER_ROW);
+  statcache_rowlen = (STATCACHE_COLS_PER_ROW * sizeof(struct statcache_entry));
+
+  return;
+}
+
+static void statcache_restart_ev(const void *event_data, void *user_data) {
+#ifdef PR_USE_CTRLS
+  register unsigned int i;
+#endif /* PR_USE_CTRLS */
+
+  if (statcache_pool) {
+    destroy_pool(statcache_pool);
+    statcache_pool = NULL;
+  }
+
+  statcache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(statcache_pool, MOD_STATCACHE_VERSION);
+
+#ifdef PR_USE_CTRLS
+  /* Register the control handlers */
+  for (i = 0; statcache_acttab[i].act_action; i++) {
+
+    /* Allocate and initialize the ACL for this control. */
+    statcache_acttab[i].act_acl = pcalloc(statcache_pool, sizeof(ctrls_acl_t));
+    pr_ctrls_init_acl(statcache_acttab[i].act_acl);
+  }
+#endif /* PR_USE_CTRLS */
+
+  /* Close the StatCacheTable file descriptor; it will be reopened by the
+   * postparse event listener.
+   */
+  if (statcache_tabfh != NULL) {
+    pr_fsio_close(statcache_tabfh);
+    statcache_tabfh = NULL;
+  }
+
+  return;
+}
+
+/* Initialization routines
+ */
+
+static int statcache_init(void) {
+#ifdef PR_USE_CTRLS
+  register unsigned int i = 0;
+#endif /* PR_USE_CTRLS */
+
+  /* Allocate the pool for this module's use. */
+  statcache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(statcache_pool, MOD_STATCACHE_VERSION);
+
+#ifdef PR_USE_CTRLS
+  /* Register the control handlers */
+  for (i = 0; statcache_acttab[i].act_action; i++) {
+
+    /* Allocate and initialize the ACL for this control. */
+    statcache_acttab[i].act_acl = pcalloc(statcache_pool, sizeof(ctrls_acl_t));
+    pr_ctrls_init_acl(statcache_acttab[i].act_acl);
+
+    if (pr_ctrls_register(&statcache_module, statcache_acttab[i].act_action,
+        statcache_acttab[i].act_desc, statcache_acttab[i].act_cb) < 0) {
+      pr_log_pri(PR_LOG_INFO, MOD_STATCACHE_VERSION
+        ": error registering '%s' control: %s",
+        statcache_acttab[i].act_action, strerror(errno));
+    }
+  }
+#endif /* PR_USE_CTRLS */
+
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&statcache_module, "core.module-unload",
+    statcache_mod_unload_ev, NULL);
+#endif /* PR_SHARED_MODULE */
+  pr_event_register(&statcache_module, "core.postparse",
+    statcache_postparse_ev, NULL);
+  pr_event_register(&statcache_module, "core.restart",
+    statcache_restart_ev, NULL);
+  pr_event_register(&statcache_module, "core.shutdown",
+    statcache_shutdown_ev, NULL);
+
+  return 0;
+}
+
+static int statcache_sess_init(void) {
+  config_rec *c;
+
+  pr_event_register(&statcache_module, "core.session-reinit",
+    statcache_sess_reinit_ev, NULL);
+
+  /* Check to see if the BanEngine directive is set to 'off'. */
+  c = find_config(main_server->conf, CONF_PARAM, "StatCacheEngine", FALSE);
+  if (c != NULL) {
+    statcache_engine = *((int *) c->argv[0]);
+  }
+
+  return 0;
+}
+
+#ifdef PR_USE_CTRLS
+
+/* Controls table
+ */
+static ctrls_acttab_t statcache_acttab[] = {
+  { "statcache",	"display cache stats", NULL,
+    statcache_handle_statcache },
+
+  { NULL, NULL, NULL, NULL }
+};
+#endif /* PR_USE_CTRLS */
+
+/* Module API tables
+ */
+
+static conftable statcache_conftab[] = {
+  { "StatCacheCapacity",	set_statcachecapacity,	NULL },
+  { "StatCacheControlsACLs",	set_statcachectrlsacls,	NULL },
+  { "StatCacheEngine",		set_statcacheengine,	NULL },
+  { "StatCacheMaxAge",		set_statcachemaxage,	NULL },
+  { "StatCacheTable",		set_statcachetable,	NULL },
+  { NULL }
+};
+
+static cmdtable statcache_cmdtab[] = {
+  { POST_CMD,   C_PASS, G_NONE, statcache_post_pass,	FALSE,	FALSE },
+
+#ifdef MADV_WILLNEED
+  /* If the necessary madvise(2) flag is present, register a PRE_CMD
+   * handler for directory listings, to suggest to the kernel that
+   * it read in some pages of the mmap()'d region.
+   */
+  { PRE_CMD,	C_LIST,	G_NONE,	statcache_pre_list,	FALSE,	FALSE },
+  { PRE_CMD,	C_MLSD,	G_NONE,	statcache_pre_list,	FALSE,	FALSE },
+  { PRE_CMD,	C_NLST,	G_NONE,	statcache_pre_list,	FALSE,	FALSE },
+#endif /* MADV_WILLNEED */
+
+  { 0, NULL }
+};
+
+module statcache_module = {
+  NULL, NULL,
+
+  /* Module API version 2.0 */
+  0x20,
+
+  /* Module name */
+  "statcache",
+
+  /* Module configuration handler table */
+  statcache_conftab,
+
+  /* Module command handler table */
+  statcache_cmdtab,
+
+  /* Module authentication handler table */
+  NULL,
+
+  /* Module initialization function */
+  statcache_init,
+
+  /* Session initialization function */
+  statcache_sess_init,
+
+  /* Module version */
+  MOD_STATCACHE_VERSION
+};
diff --git a/contrib/mod_tls.c b/contrib/mod_tls.c
index 4ba80a7..a9855ed 100644
--- a/contrib/mod_tls.c
+++ b/contrib/mod_tls.c
@@ -42,6 +42,11 @@
 # include "mod_ctrls.h"
 #endif
 
+/* Define if you have the LibreSSL library.  */
+#if defined(LIBRESSL_VERSION_NUMBER)
+# define HAVE_LIBRESSL	1
+#endif
+
 /* Note that the openssl/ssl.h header is already included in mod_tls.h, so
  * we don't need to include it here.
 */
@@ -50,33 +55,36 @@
 #include <openssl/conf.h>
 #include <openssl/crypto.h>
 #include <openssl/evp.h>
+#include <openssl/ssl.h>
 #include <openssl/ssl3.h>
 #include <openssl/x509v3.h>
 #include <openssl/pkcs12.h>
 #include <openssl/rand.h>
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
 # include <openssl/engine.h>
-# include <openssl/ocsp.h>
+# ifdef PR_USE_OPENSSL_OCSP
+#  include <openssl/ocsp.h>
+# endif /* PR_USE_OPENSSL_OCSP */
 #endif
 #ifdef PR_USE_OPENSSL_ECC
 # include <openssl/ec.h>
 # include <openssl/ecdh.h>
 #endif /* PR_USE_OPENSSL_ECC */
 
-
 #ifdef HAVE_MLOCK
 # include <sys/mman.h>
 #endif
 
-#define MOD_TLS_VERSION		"mod_tls/2.6"
+#define MOD_TLS_VERSION		"mod_tls/2.7"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030504 
-# error "ProFTPD 1.3.5rc4 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 extern session_t session;
 extern xaset_t *server_list;
+extern int ServerUseReverseDNS;
 
 /* DH parameters.  These are generated using:
  *
@@ -96,7 +104,8 @@ static DH *get_dh(BIGNUM *p, BIGNUM *g) {
     return NULL;
   }
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   if (DH_set0_pqg(dh, p, NULL, g) != 1) {
     pr_trace_msg(trace_channel, 3, "error setting DH p/q parameters: %s",
       ERR_error_string(ERR_get_error(), NULL));
@@ -114,7 +123,8 @@ static DH *get_dh(BIGNUM *p, BIGNUM *g) {
 static X509 *read_cert(FILE *fh, SSL_CTX *ssl_ctx) {
   X509 *cert;
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   cert = PEM_read_X509(fh, NULL, SSL_CTX_get_default_passwd_cb(ssl_ctx),
     SSL_CTX_get_default_passwd_cb_userdata(ssl_ctx));
 #else
@@ -349,24 +359,35 @@ static DH *get_dh2048(void) {
 # define M_ASN1_BIT_STRING_cmp ASN1_BIT_STRING_cmp
 #endif
 
-/* From src/dirtree.c */
-extern int ServerUseReverseDNS;
+#if defined(SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB)
+# define TLS_USE_SESSION_TICKETS
+#endif
 
 module tls_module;
 
+struct tls_next_proto {
+  const char *proto;
+  unsigned char *encoded_proto;
+  unsigned int encoded_protolen;
+};
+
 typedef struct tls_pkey_obj {
   struct tls_pkey_obj *next;
+  pool *pool;
 
   size_t pkeysz;
 
   char *rsa_pkey;
+  int rsa_passlen;
   void *rsa_pkey_ptr;
 
   char *dsa_pkey;
+  int dsa_passlen;
   void *dsa_pkey_ptr;
 
 #ifdef PR_USE_OPENSSL_ECC
   char *ec_pkey;
+  int ec_passlen;
   void *ec_pkey_ptr;
 #endif /* PR_USE_OPENSSL_ECC */
 
@@ -375,11 +396,12 @@ typedef struct tls_pkey_obj {
    * certificate should be in one of the above RSA/DSA buffers.
    */
   char *pkcs12_passwd;
+  int pkcs12_passlen;
   void *pkcs12_passwd_ptr;
 
   unsigned int flags;
-
-  server_rec *server;
+  unsigned int sid;
+  const char *path;
 
 } tls_pkey_t;
 
@@ -391,6 +413,19 @@ static tls_pkey_t *tls_pkey_list = NULL;
 static unsigned int tls_npkeys = 0;
 
 #define TLS_DEFAULT_CIPHER_SUITE	"DEFAULT:!ADH:!EXPORT:!DES"
+#define TLS_DEFAULT_NEXT_PROTO		"ftp"
+
+/* SSL record/buffer sizes */
+#define TLS_HANDSHAKE_WRITE_BUFFER_SIZE			1400
+
+/* SSL adaptive buffer sizes/values */
+#define TLS_DATA_ADAPTIVE_WRITE_MIN_BUFFER_SIZE		(4 * 1024)
+#define TLS_DATA_ADAPTIVE_WRITE_MAX_BUFFER_SIZE		(16 * 1024)
+#define TLS_DATA_ADAPTIVE_WRITE_BOOST_THRESHOLD		(1024 * 1024)
+#define TLS_DATA_ADAPTIVE_WRITE_BOOST_INTERVAL_MS	1000
+
+static uint64_t tls_data_adaptive_bytes_written_ms = 0L;
+static off_t tls_data_adaptive_bytes_written_count = 0;
 
 /* Module variables */
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
@@ -398,9 +433,17 @@ static const char *tls_crypto_device = NULL;
 #endif
 static unsigned char tls_engine = FALSE;
 static unsigned long tls_flags = 0UL, tls_opts = 0UL;
+static pool *tls_pool = NULL;
 static tls_pkey_t *tls_pkey = NULL;
 static int tls_logfd = -1;
-static char *tls_logname = NULL;
+#if defined(PR_USE_OPENSSL_OCSP)
+static int tls_stapling = FALSE;
+static unsigned long tls_stapling_opts = 0UL;
+# define TLS_STAPLING_OPT_NO_NONCE	0x0001
+# define TLS_STAPLING_OPT_NO_VERIFY	0x0002
+static const char *tls_stapling_responder = NULL;
+static unsigned int tls_stapling_timeout = 10;
+#endif
 
 static char *tls_passphrase_provider = NULL;
 #define TLS_PASSPHRASE_TIMEOUT		10
@@ -420,6 +463,9 @@ static char *tls_passphrase_provider = NULL;
 # define TLS_PROTO_DEFAULT		(TLS_PROTO_TLS_V1)
 #endif /* OpenSSL 1.0.1 or later */
 
+/* This is used for e.g. "TLSProtocol ALL -SSLv3 ...". */
+#define TLS_PROTO_ALL			(TLS_PROTO_SSL_V3|TLS_PROTO_TLS_V1|TLS_PROTO_TLS_V1_1|TLS_PROTO_TLS_V1_2)
+
 #ifdef SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS
 static int tls_ssl_opts = (SSL_OP_ALL|SSL_OP_NO_SSLv2|SSL_OP_SINGLE_DH_USE)^SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS;
 #else
@@ -446,7 +492,7 @@ static unsigned char *tls_authenticated = NULL;
 #define TLS_SESS_ON_DATA			0x0002
 #define TLS_SESS_PBSZ_OK			0x0004
 #define TLS_SESS_TLS_REQUIRED			0x0010
-#define TLS_SESS_VERIFY_CLIENT			0x0020
+#define TLS_SESS_VERIFY_CLIENT_REQUIRED		0x0020
 #define TLS_SESS_NO_PASSWD_NEEDED		0x0040
 #define TLS_SESS_NEED_DATA_PROT			0x0100
 #define TLS_SESS_CTRL_RENEGOTIATING		0x0200
@@ -454,9 +500,9 @@ static unsigned char *tls_authenticated = NULL;
 #define TLS_SESS_HAVE_CCC			0x0800
 #define TLS_SESS_VERIFY_SERVER			0x1000
 #define TLS_SESS_VERIFY_SERVER_NO_DNS		0x2000
+#define TLS_SESS_VERIFY_CLIENT_OPTIONAL		0x4000
 
 /* mod_tls option flags */
-#define TLS_OPT_NO_CERT_REQUEST				0x0001
 #define TLS_OPT_VERIFY_CERT_FQDN			0x0002
 #define TLS_OPT_VERIFY_CERT_IP_ADDR			0x0004
 #define TLS_OPT_ALLOW_DOT_LOGIN				0x0008
@@ -468,7 +514,8 @@ static unsigned char *tls_authenticated = NULL;
 #define TLS_OPT_USE_IMPLICIT_SSL			0x0200
 #define TLS_OPT_ALLOW_CLIENT_RENEGOTIATIONS		0x0400
 #define TLS_OPT_VERIFY_CERT_CN				0x0800
-#define TLS_OPT_ALLOW_WEAK_DH				0x1000
+#define TLS_OPT_NO_AUTO_ECDH				0x1000
+#define TLS_OPT_ALLOW_WEAK_DH				0x2000
 
 /* mod_tls SSCN modes */
 #define TLS_SSCN_MODE_SERVER				0
@@ -483,13 +530,17 @@ static unsigned int tls_sscn_mode = TLS_SSCN_MODE_SERVER;
 
 static char *tls_cipher_suite = NULL;
 static char *tls_crl_file = NULL, *tls_crl_path = NULL;
-static char *tls_dhparam_file = NULL;
 static char *tls_ec_cert_file = NULL, *tls_ec_key_file = NULL;
 static char *tls_dsa_cert_file = NULL, *tls_dsa_key_file = NULL;
 static char *tls_pkcs12_file = NULL;
 static char *tls_rsa_cert_file = NULL, *tls_rsa_key_file = NULL;
 static char *tls_rand_file = NULL;
 
+#if defined(PSK_MAX_PSK_LEN)
+static pr_table_t *tls_psks = NULL;
+# define TLS_MIN_PSK_LEN	20
+#endif /* PSK support */
+
 /* Timeout given for TLS handshakes.  The default is 5 minutes. */
 static unsigned int tls_handshake_timeout = 300;
 static unsigned char tls_handshake_timed_out = FALSE;
@@ -525,6 +576,7 @@ static pr_netio_stream_t *tls_data_rd_nstrm = NULL;
 static pr_netio_stream_t *tls_data_wr_nstrm = NULL;
 
 static tls_sess_cache_t *tls_sess_cache = NULL;
+static tls_ocsp_cache_t *tls_ocsp_cache = NULL;
 
 /* OpenSSL variables */
 static SSL *ctrl_ssl = NULL;
@@ -533,12 +585,13 @@ static X509_STORE *tls_crl_store = NULL;
 static array_header *tls_tmp_dhs = NULL;
 static RSA *tls_tmp_rsa = NULL;
 
+static void tls_exit_ev(const void *, void *);
 static int tls_sess_init(void);
 
 /* SSL/TLS support functions */
 static void tls_closelog(void);
-static void tls_end_sess(SSL *, int, int);
-#define TLS_SHUTDOWN_BIDIRECTIONAL	0x0001
+static void tls_end_sess(SSL *, conn_t *, int);
+#define TLS_SHUTDOWN_FL_BIDIRECTIONAL		0x0001
 
 static void tls_fatal_error(long, int);
 static const char *tls_get_errors(void);
@@ -550,9 +603,9 @@ static int tls_get_passphrase(server_rec *, const char *, const char *,
 static char *tls_get_subj_name(SSL *);
 
 static int tls_openlog(void);
-static RSA *tls_rsa_cb(SSL *, int, int);
 static int tls_seed_prng(void);
-static void tls_setup_environ(SSL *);
+static int tls_sess_init(void);
+static void tls_setup_environ(pool *, SSL *);
 static int tls_verify_cb(int, X509_STORE_CTX *);
 static int tls_verify_crl(int, X509_STORE_CTX *);
 static int tls_verify_ocsp(int, X509_STORE_CTX *);
@@ -572,10 +625,54 @@ static int tls_sess_cache_remove(void);
 static int tls_sess_cache_status(pr_ctrls_t *, int);
 #endif /* PR_USE_CTRLS */
 static int tls_sess_cache_add_sess_cb(SSL *, SSL_SESSION *);
-static SSL_SESSION *tls_sess_cache_get_sess_cb(SSL *, const unsigned char *,
-  int, int *);
+static SSL_SESSION *tls_sess_cache_get_sess_cb(SSL *, unsigned char *, int,
+  int *);
 static void tls_sess_cache_delete_sess_cb(SSL_CTX *, SSL_SESSION *);
 
+/* OCSP response cache API */
+static tls_ocsp_cache_t *tls_ocsp_cache_get_cache(const char *);
+static int tls_ocsp_cache_open(char *);
+static int tls_ocsp_cache_close(void);
+#ifdef PR_USE_CTRLS
+static int tls_ocsp_cache_clear(void);
+static int tls_ocsp_cache_remove(void);
+static int tls_ocsp_cache_status(pr_ctrls_t *, int);
+#endif /* PR_USE_CTRLS */
+
+#if defined(TLS_USE_SESSION_TICKETS)
+/* Default maximum ticket key age: 12 hours */
+static unsigned int tls_ticket_key_max_age = 43200;
+
+/* Maximum number of session ticket keys: 25 (1 per hour, plus leeway) */
+static unsigned int tls_ticket_key_max_count = 25;
+static unsigned int tls_ticket_key_curr_count = 0;
+
+struct tls_ticket_key {
+  struct tls_ticket_key *next, *prev;
+
+  /* Memory page pointer and size, for locking. */
+  void *page_ptr;
+  size_t pagesz;
+  int locked;
+  time_t created;
+
+  /* 16 bytes for the key name, per OpenSSL implementation. */
+  unsigned char key_name[16];
+  unsigned char cipher_key[32];
+  unsigned char hmac_key[32];
+};
+
+/* In-memory list of session ticket keys, newest key first.  Note that the
+ * memory pages used for a ticket key will be mlock(2)'d into memory, where
+ * possible.
+ *
+ * Ticket keys will be generated randomly, based on the timeout.  Expired
+ * ticket keys will be destroyed when a new key is generated.  Tickets
+ * encrypted with older keys will be renewed using the newest key.
+ */
+static xaset_t *tls_ticket_keys = NULL;
+#endif
+
 #ifdef PR_USE_CTRLS
 static pool *tls_act_pool = NULL;
 static ctrls_acttab_t tls_acttab[];
@@ -584,7 +681,136 @@ static ctrls_acttab_t tls_acttab[];
 static int tls_ctrl_need_init_handshake = TRUE;
 static int tls_data_need_init_handshake = TRUE;
 
-static void tls_diags_cb(const SSL *ssl, int where, int ret) {
+static const char *timing_channel = "timing";
+
+static const char *tls_get_fingerprint(pool *p, X509 *cert) {
+  const EVP_MD *md = EVP_sha1();
+  unsigned char fp[EVP_MAX_MD_SIZE];
+  unsigned int fp_len = 0;
+  char *fp_hex = NULL;
+
+  if (X509_digest(cert, md, fp, &fp_len) != 1) {
+    pr_trace_msg(trace_channel, 1,
+      "error obtaining %s digest of X509 cert: %s", OBJ_nid2sn(EVP_MD_type(md)),
+      tls_get_errors());
+    errno = EINVAL;
+    return NULL;
+  }
+
+  fp_hex = pr_str_bin2hex(p, fp, fp_len, 0);
+
+  pr_trace_msg(trace_channel, 8,
+    "%s fingerprint: %s", OBJ_nid2sn(EVP_MD_type(md)), fp_hex);
+  return fp_hex;
+}
+
+static const char *tls_get_fingerprint_from_file(pool *p, const char *path) {
+  FILE *fh;
+  X509 *cert = NULL;
+  const char *fingerprint;
+
+  fh = fopen(path, "rb");
+  if (fh == NULL) {
+    return NULL;
+  }
+
+  /* As the file may contain sensitive data, we do not want it lingering
+   * around in stdio buffers.
+   */
+  (void) setvbuf(fh, NULL, _IONBF, 0);
+
+  cert = PEM_read_X509(fh, &cert, NULL, NULL);
+  (void) fclose(fh);
+
+  if (cert == NULL) {
+    pr_trace_msg(trace_channel, 1, "error obtaining X509 cert from '%s': %s",
+      path, tls_get_errors());
+    errno = ENOENT;
+    return NULL;
+  }
+
+  fingerprint = tls_get_fingerprint(p, cert);
+  X509_free(cert);
+
+  return fingerprint;
+}
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static const char *ocsp_get_responder_url(pool *p, X509 *cert) {
+  STACK_OF(OPENSSL_STRING) *strs;
+  char *ocsp_url = NULL;
+
+  strs = X509_get1_ocsp(cert);
+  if (strs != NULL) {
+# if OPENSSL_VERSION_NUMBER >= 0x10000001L
+    if (sk_OPENSSL_STRING_num(strs) > 0) {
+      ocsp_url = pstrdup(p, sk_OPENSSL_STRING_value(strs, 0));
+    }
+# endif /* OpenSSL-1.0.0 and later */
+
+    /* Yes, this says "email", but it Does The Right Thing(tm) for our needs. */
+    X509_email_free(strs);
+  }
+
+  return ocsp_url;
+}
+#endif /* PR_USE_OPENSSL_OCSP */
+
+static void tls_reset_state(void) {
+  if (ssl_ctx != NULL) {
+    SSL_CTX_set_options(ssl_ctx, SSL_CTX_get_options(ssl_ctx));
+  }
+
+  tls_engine = FALSE;
+  tls_flags = 0UL;
+  tls_opts = 0UL;
+
+  if (tls_logfd >= 0) {
+    (void) close(tls_logfd);
+    tls_logfd = -1;
+  }
+
+  tls_cipher_suite = NULL;
+  tls_crl_file = NULL;
+  tls_crl_path = NULL;
+  tls_ec_cert_file = NULL;
+  tls_ec_key_file = NULL;
+  tls_dsa_cert_file = NULL;
+  tls_dsa_key_file = NULL;
+  tls_pkcs12_file = NULL;
+  tls_rsa_cert_file = NULL;
+  tls_rsa_key_file = NULL;
+  tls_rand_file = NULL;
+
+  tls_handshake_timeout = 300;
+  tls_handshake_timed_out = FALSE;
+  tls_handshake_timer_id = -1;
+
+  tls_verify_depth = 9;
+
+  tls_ctrl_netio = NULL;
+  tls_ctrl_rd_nstrm = NULL;
+  tls_ctrl_wr_nstrm = NULL;
+
+  tls_data_netio = NULL;
+  tls_data_rd_nstrm = NULL;
+  tls_data_wr_nstrm = NULL;
+
+  tls_sess_cache = NULL;
+
+  tls_crl_store = NULL;
+  tls_tmp_dhs = NULL;
+  tls_tmp_rsa = NULL;
+
+  tls_ctrl_need_init_handshake = TRUE;
+  tls_data_need_init_handshake = TRUE;
+
+  tls_required_on_auth = 0;
+  tls_required_on_ctrl = 0;
+  tls_required_on_data = 0;
+}
+
+static void tls_info_cb(const SSL *ssl, int where, int ret) {
   const char *str = "(unknown)";
   int w;
 
@@ -609,7 +835,8 @@ static void tls_diags_cb(const SSL *ssl, int where, int ret) {
         break;
 #endif
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       case TLS_ST_OK:
 #else
       case SSL_ST_OK:
@@ -633,7 +860,8 @@ static void tls_diags_cb(const SSL *ssl, int where, int ret) {
 
     ssl_state = SSL_get_state(ssl);
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     if (ssl_state == TLS_ST_SR_CLNT_HELLO) {
 #else
     if (ssl_state == SSL3_ST_SR_CLNT_HELLO_A ||
@@ -663,7 +891,7 @@ static void tls_diags_cb(const SSL *ssl, int where, int ret) {
               ": client-initiated session renegotiation detected, "
               "aborting connection");
 
-            tls_end_sess(ctrl_ssl, PR_NETIO_STRM_CTRL, 0);
+            tls_end_sess(ctrl_ssl, session.c, 0);
             pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
             pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
             ctrl_ssl = NULL;
@@ -700,7 +928,7 @@ static void tls_diags_cb(const SSL *ssl, int where, int ret) {
               ": client-initiated session renegotiation detected, "
               "aborting connection");
 
-            tls_end_sess(ctrl_ssl, PR_NETIO_STRM_CTRL, 0);
+            tls_end_sess(ctrl_ssl, session.c, 0);
             pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
             pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
             ctrl_ssl = NULL;
@@ -796,10 +1024,502 @@ static void tls_diags_cb(const SSL *ssl, int where, int ret) {
 }
 
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
+/* Tables needed for describing bits of the ClientHello. */
+
+struct tls_label {
+  int labelno;
+  const char *label_name;
+};
+
+/* SSL versions */
+static struct tls_label tls_version_labels[] = {
+  { 0x0002, "SSL 2.0" },
+  { 0x0300, "SSL 3.0" },
+  { 0x0301, "TLS 1.0" },
+  { 0x0302, "TLS 1.1" },
+  { 0x0303, "TLS 1.2" },
+
+  { 0, NULL }
+};
+
+/* Cipher suites.  These values come from:
+ *   http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4
+ */
+static struct tls_label tls_ciphersuite_labels[] = {
+  { 0x0000, "SSL_NULL_WITH_NULL_NULL" },
+  { 0x0001, "SSL_RSA_WITH_NULL_MD5" },
+  { 0x0002, "SSL_RSA_WITH_NULL_SHA" },
+  { 0x0003, "SSL_RSA_EXPORT_WITH_RC4_40_MD5" },
+  { 0x0004, "SSL_RSA_WITH_RC4_128_MD5" },
+  { 0x0005, "SSL_RSA_WITH_RC4_128_SHA" },
+  { 0x0006, "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5" },
+  { 0x0007, "SSL_RSA_WITH_IDEA_CBC_SHA" },
+  { 0x0008, "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA" },
+  { 0x0009, "SSL_RSA_WITH_DES_CBC_SHA" },
+  { 0x000A, "SSL_RSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0x000B, "SSL_DH_DSS_EXPORT_WITH_DES40_CBC_SHA" },
+  { 0x000C, "SSL_DH_DSS_WITH_DES_CBC_SHA" },
+  { 0x000D, "SSL_DH_DSS_WITH_3DES_EDE_CBC_SHA" },
+  { 0x000E, "SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA" },
+  { 0x000F, "SSL_DH_RSA_WITH_DES_CBC_SHA" },
+  { 0x0010, "SSL_DH_RSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0x0011, "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA" },
+  { 0x0012, "SSL_DHE_DSS_WITH_DES_CBC_SHA" },
+  { 0x0013, "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA" },
+  { 0x0014, "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA" },
+  { 0x0015, "SSL_DHE_RSA_WITH_DES_CBC_SHA" },
+  { 0x0016, "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0x0017, "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5" },
+  { 0x0018, "SSL_DH_anon_WITH_RC4_128_MD5" },
+  { 0x0019, "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA" },
+  { 0x001A, "SSL_DH_anon_WITH_DES_CBC_SHA" },
+  { 0x001B, "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA" },
+  { 0x001D, "SSL_FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA" },
+  { 0x001E, "SSL_FORTEZZA_KEA_WITH_RC4_128_SHA" },
+  { 0x001F, "TLS_KRB5_WITH_3DES_EDE_CBC_SHA" },
+  { 0x0020, "TLS_KRB5_WITH_RC4_128_SHA" },
+  { 0x0021, "TLS_KRB5_WITH_IDEA_CBC_SHA" },
+  { 0x0022, "TLS_KRB5_WITH_DES_CBC_MD5" },
+  { 0x0023, "TLS_KRB5_WITH_3DES_EDE_CBC_MD5" },
+  { 0x0024, "TLS_KRB5_WITH_RC4_128_MD5" },
+  { 0x0025, "TLS_KRB5_WITH_IDEA_CBC_MD5" },
+  { 0x0026, "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA" },
+  { 0x0027, "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA" },
+  { 0x0028, "TLS_KRB5_EXPORT_WITH_RC4_40_SHA" },
+  { 0x0029, "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5" },
+  { 0x002A, "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5" },
+  { 0x002B, "TLS_KRB5_EXPORT_WITH_RC4_40_MD5" },
+  { 0x002F, "TLS_RSA_WITH_AES_128_CBC_SHA" },
+  { 0x0030, "TLS_DH_DSS_WITH_AES_128_CBC_SHA" },
+  { 0x0031, "TLS_DH_RSA_WITH_AES_128_CBC_SHA" },
+  { 0x0032, "TLS_DHE_DSS_WITH_AES_128_CBC_SHA" },
+  { 0x0033, "TLS_DHE_RSA_WITH_AES_128_CBC_SHA" },
+  { 0x0034, "TLS_DH_anon_WITH_AES_128_CBC_SHA" },
+  { 0x0035, "TLS_RSA_WITH_AES_256_CBC_SHA" },
+  { 0x0036, "TLS_DH_DSS_WITH_AES_256_CBC_SHA" },
+  { 0x0037, "TLS_DH_RSA_WITH_AES_256_CBC_SHA" },
+  { 0x0038, "TLS_DHE_DSS_WITH_AES_256_CBC_SHA" },
+  { 0x0039, "TLS_DHE_RSA_WITH_AES_256_CBC_SHA" },
+  { 0x003A, "TLS_DH_anon_WITH_AES_256_CBC_SHA" },
+  { 0x003B, "TLS_RSA_WITH_NULL_SHA256" },
+  { 0x003C, "TLS_RSA_WITH_AES_128_CBC_SHA256" },
+  { 0x003D, "TLS_RSA_WITH_AES_256_CBC_SHA256" },
+  { 0x003E, "TLS_DH_DSS_WITH_AES_128_CBC_SHA256" },
+  { 0x003F, "TLS_DH_RSA_WITH_AES_128_CBC_SHA256" },
+  { 0x0040, "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256" },
+  { 0x0041, "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA" },
+  { 0x0042, "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA" },
+  { 0x0043, "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA" },
+  { 0x0044, "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA" },
+  { 0x0045, "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA" },
+  { 0x0046, "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA" },
+  { 0x0067, "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256" },
+  { 0x0068, "TLS_DH_DSS_WITH_AES_256_CBC_SHA256" },
+  { 0x0069, "TLS_DH_RSA_WITH_AES_256_CBC_SHA256" },
+  { 0x006A, "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" },
+  { 0x006B, "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256" },
+  { 0x006C, "TLS_DH_anon_WITH_AES_128_CBC_SHA256" },
+  { 0x006D, "TLS_DH_anon_WITH_AES_256_CBC_SHA256" },
+  { 0x0084, "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA" },
+  { 0x0085, "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA" },
+  { 0x0086, "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA" },
+  { 0x0087, "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA" },
+  { 0x0088, "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA" },
+  { 0x0089, "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA" },
+  { 0x008A, "TLS_PSK_WITH_RC4_128_SHA" },
+  { 0x008B, "TLS_PSK_WITH_3DES_EDE_CBC_SHA" },
+  { 0x008C, "TLS_PSK_WITH_AES_128_CBC_SHA" },
+  { 0x008D, "TLS_PSK_WITH_AES_256_CBC_SHA" },
+  { 0x008E, "TLS_DHE_PSK_WITH_RC4_128_SHA" },
+  { 0x008F, "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA" },
+  { 0x0090, "TLS_DHE_PSK_WITH_AES_128_CBC_SHA" },
+  { 0x0091, "TLS_DHE_PSK_WITH_AES_256_CBC_SHA" },
+  { 0x0092, "TLS_RSA_PSK_WITH_RC4_128_SHA" },
+  { 0x0093, "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA" },
+  { 0x0094, "TLS_RSA_PSK_WITH_AES_128_CBC_SHA" },
+  { 0x0095, "TLS_RSA_PSK_WITH_AES_256_CBC_SHA" },
+  { 0x0096, "TLS_RSA_WITH_SEED_CBC_SHA" },
+  { 0x0097, "TLS_DH_DSS_WITH_SEED_CBC_SHA" },
+  { 0x0098, "TLS_DH_RSA_WITH_SEED_CBC_SHA" },
+  { 0x0099, "TLS_DHE_DSS_WITH_SEED_CBC_SHA" },
+  { 0x009A, "TLS_DHE_RSA_WITH_SEED_CBC_SHA" },
+  { 0x009B, "TLS_DH_anon_WITH_SEED_CBC_SHA" },
+  { 0x009C, "TLS_RSA_WITH_AES_128_GCM_SHA256" },
+  { 0x009D, "TLS_RSA_WITH_AES_256_GCM_SHA384" },
+  { 0x009E, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256" },
+  { 0x009F, "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384" },
+  { 0x00A0, "TLS_DH_RSA_WITH_AES_128_GCM_SHA256" },
+  { 0x00A1, "TLS_DH_RSA_WITH_AES_256_GCM_SHA384" },
+  { 0x00A2, "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256" },
+  { 0x00A3, "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384" },
+  { 0x00A4, "TLS_DH_DSS_WITH_AES_128_GCM_SHA256" },
+  { 0x00A5, "TLS_DH_DSS_WITH_AES_256_GCM_SHA384" },
+  { 0x00A6, "TLS_DH_anon_WITH_AES_128_GCM_SHA256" },
+  { 0x00A7, "TLS_DH_anon_WITH_AES_256_GCM_SHA384" },
+  { 0x00A8, "TLS_PSK_WITH_AES_128_GCM_SHA256" },
+  { 0x00A9, "TLS_PSK_WITH_AES_256_GCM_SHA384" },
+  { 0x00AA, "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256" },
+  { 0x00AB, "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384" },
+  { 0x00AC, "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256" },
+  { 0x00AD, "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384" },
+  { 0x00AE, "TLS_PSK_WITH_AES_128_CBC_SHA256" },
+  { 0x00AF, "TLS_PSK_WITH_AES_256_CBC_SHA384" },
+  { 0x00B0, "TLS_PSK_WITH_NULL_SHA256" },
+  { 0x00B1, "TLS_PSK_WITH_NULL_SHA384" },
+  { 0x00B2, "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256" },
+  { 0x00B3, "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384"},
+  { 0x00B4, "TLS_DHE_PSK_WITH_NULL_SHA256" },
+  { 0x00B5, "TLS_DHE_PSK_WITH_NULL_SHA384" },
+  { 0x00B6, "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256" },
+  { 0x00B7, "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384" },
+  { 0x00B8, "TLS_RSA_PSK_WITH_NULL_SHA256" },
+  { 0x00B9, "TLS_RSA_PSK_WITH_NULL_SHA384" },
+  { 0x00BA, "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256" },
+  { 0x00BB, "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256" },
+  { 0x00BC, "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256" },
+  { 0x00BD, "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256" },
+  { 0x00BE, "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256" },
+  { 0x00BF, "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256" },
+  { 0x00C0, "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256" },
+  { 0x00C1, "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256" },
+  { 0x00C2, "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256" },
+  { 0x00C3, "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256" },
+  { 0x00C4, "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256" },
+  { 0x00C5, "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256" },
+  { 0x00FF, "TLS_EMPTY_RENEGOTIATION_INFO_SCSV" },
+  { 0xC001, "TLS_ECDH_ECDSA_WITH_NULL_SHA" },
+  { 0xC002, "TLS_ECDH_ECDSA_WITH_RC4_128_SHA" },
+  { 0xC003, "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC004, "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA" },
+  { 0xC005, "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA" },
+  { 0xC006, "TLS_ECDHE_ECDSA_WITH_NULL_SHA" },
+  { 0xC007, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA" },
+  { 0xC008, "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC009, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
+  { 0xC00A, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
+  { 0xC00B, "TLS_ECDH_RSA_WITH_NULL_SHA" },
+  { 0xC00C, "TLS_ECDH_RSA_WITH_RC4_128_SHA" },
+  { 0xC00D, "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC00E, "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA" },
+  { 0xC00F, "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA" },
+  { 0xC010, "TLS_ECDHE_RSA_WITH_NULL_SHA" },
+  { 0xC011, "TLS_ECDHE_RSA_WITH_RC4_128_SHA" },
+  { 0xC012, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC013, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
+  { 0xC014, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
+  { 0xC015, "TLS_ECDH_anon_WITH_NULL_SHA" },
+  { 0xC016, "TLS_ECDH_anon_WITH_RC4_128_SHA" },
+  { 0xC017, "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC018, "TLS_ECDH_anon_WITH_AES_128_CBC_SHA" },
+  { 0xC019, "TLS_ECDH_anon_WITH_AES_256_CBC_SHA" },
+  { 0xC01A, "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC01B, "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC01C, "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA" },
+  { 0xC01D, "TLS_SRP_SHA_WITH_AES_128_CBC_SHA" },
+  { 0xC01E, "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA" },
+  { 0xC01F, "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA" },
+  { 0xC020, "TLS_SRP_SHA_WITH_AES_256_CBC_SHA" },
+  { 0xC021, "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA" },
+  { 0xC022, "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA" },
+  { 0xC023, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" },
+  { 0xC024, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384" },
+  { 0xC025, "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256" },
+  { 0xC026, "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384" },
+  { 0xC027, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" },
+  { 0xC028, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384" },
+  { 0xC029, "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256" },
+  { 0xC02A, "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384" },
+  { 0xC02B, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
+  { 0xC02C, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
+  { 0xC02D, "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256" },
+  { 0xC02E, "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384" },
+  { 0xC02F, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
+  { 0xC030, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
+  { 0xC031, "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256" },
+  { 0xC032, "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384" },
+  { 0xFEFE, "SSL_RSA_FIPS_WITH_DES_CBC_SHA" },
+  { 0xFEFF, "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA" },
+
+  { 0, NULL }
+};
+
+/* Compressions */
+static struct tls_label tls_compression_labels[] = {
+  { 0x0000, "None" },
+  { 0x0001, "Zlib" },
+
+  { 0, NULL }
+};
+
+/* Extensions */
+static struct tls_label tls_extension_labels[] = {
+  { 0, "server_name" },
+  { 1, "max_fragment_length" },
+  { 2, "client_certificate_url" },
+  { 3, "trusted_ca_keys" },
+  { 4, "truncated_hmac" },
+  { 5, "status_request" },
+  { 6, "user_mapping" },
+  { 7, "client_authz" },
+  { 8, "server_authz" },
+  { 9, "cert_type" },
+  { 10, "elliptic_curves" },
+  { 11, "ec_point_formats" },
+  { 12, "srp" },
+  { 13, "signature_algorithms" },
+  { 14, "use_srtp" },
+  { 15, "heartbeat" },
+  { 16, "application_layer_protocol_negotiation" },
+  { 21, "padding" },
+  { 35, "session_ticket" },
+  { 0xFF01, "renegotiate" },
+  { 13172, "next_proto_neg" },
+
+  { 0, NULL }
+};
+
+static const char *tls_get_label(int labelno, struct tls_label *labels) {
+  register unsigned int i;
+
+  for (i = 0; labels[i].label_name != NULL; i++) {
+    if (labels[i].labelno == labelno) {
+      return labels[i].label_name;
+    }
+  }
+
+  return "[unknown/unsupported]";
+}
+
+static void tls_print_ssl_version(BIO *bio, const char *name,
+    const unsigned char **msg, size_t *msglen) {
+  int version;
+
+  if (*msglen < 2) {
+    return;
+  }
+
+  version = ((*msg)[0] << 8) | (*msg)[1];
+  BIO_printf(bio, "  %s = %s\n", name,
+    tls_get_label(version, tls_version_labels));
+  *msg += 2;
+  *msglen -= 2;
+}
+
+static void tls_print_hex(BIO *bio, const char *indent, const char *name,
+    const unsigned char *msg, size_t msglen) {
+
+  BIO_printf(bio, "%s (%lu %s)\n", name, (unsigned long) msglen,
+    msglen != 1 ? "bytes" : "byte");
+
+  if (msglen > 0) {
+    register unsigned int i;
+
+    BIO_puts(bio, indent);
+    for (i = 0; i < msglen; i++) {
+      BIO_printf(bio, "%02x", msg[i]);
+    }
+    BIO_puts(bio, "\n");
+  }
+}
+
+static void tls_print_hexbuf(BIO *bio, const char *indent, const char *name,
+    size_t namelen, const unsigned char **msg, size_t *msglen) {
+  size_t buflen;
+  const unsigned char *ptr;
+
+  if (*msglen < namelen) {
+    return;
+  }
+
+  ptr = *msg;
+  buflen = ptr[0];
+
+  if (namelen > 1) {
+    buflen = (buflen << 8) | ptr[1];
+  }
+
+  if (*msglen < namelen + buflen) {
+    return;
+  }
+
+  ptr += namelen;
+  tls_print_hex(bio, indent, name, ptr, buflen);
+  *msg += (buflen + namelen);
+  *msglen -= (buflen + namelen);
+}
+
+static void tls_print_random(BIO *bio, const unsigned char **msg,
+    size_t *msglen) {
+  time_t ts;
+  const unsigned char *ptr;
+
+  if (*msglen < 32) {
+    return;
+  }
+
+  ptr = *msg;
+
+  ts = ((ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3]);
+  ptr += 4;
+
+  BIO_puts(bio, "  random:\n");
+  BIO_printf(bio, "    gmt_unix_time = %s (not guaranteed to be accurate)\n",
+    pr_strtime2(ts, TRUE));
+  tls_print_hex(bio, "      ", "    random_bytes", ptr, 28);
+  *msg += 32;
+  *msglen -= 32;
+}
+
+static void tls_print_session_id(BIO *bio, const unsigned char **msg,
+    size_t *msglen) {
+  tls_print_hexbuf(bio, "    ", "  session_id", 1, msg, msglen);
+}
+
+static void tls_print_ciphersuites(BIO *bio, const char *name,
+    const unsigned char **msg, size_t *msglen) {
+  size_t len;
+
+  len = ((*msg[0]) << 8) | (*msg)[1];
+  *msg += 2;
+  *msglen -= 2;
+  BIO_printf(bio, "  %s (%lu %s)\n", name, (unsigned long) len,
+    len != 1 ? "bytes" : "byte");
+  if (*msglen < len ||
+      (len & 1)) {
+    return;
+  }
+
+  while (len > 0) {
+    unsigned int suiteno;
+
+    pr_signals_handle();
+
+    suiteno = ((*msg[0]) << 8) | (*msg)[1];
+    BIO_printf(bio, "    %s\n", tls_get_label(suiteno, tls_ciphersuite_labels));
+
+    *msg += 2;
+    *msglen -= 2;
+    len -= 2;
+  }
+}
+
+static void tls_print_compressions(BIO *bio, const char *name,
+    const unsigned char **msg, size_t *msglen) {
+  size_t len;
+
+  len = (*msg)[0];
+  *msg += 1;
+  *msglen -= 1;
+
+  if (*msglen < len) {
+    return;
+  }
+
+  BIO_printf(bio, "  %s (%lu %s)\n", name, (unsigned long) len,
+    len != 1 ? "bytes" : "byte");
+  while (len > 0) {
+    int comp_type;
+
+    pr_signals_handle();
+
+    comp_type = (*msg)[0];
+    BIO_printf(bio, "    %s\n",
+      tls_get_label(comp_type, tls_compression_labels));
+
+    *msg += 1;
+    *msglen -= 1;
+    len -= 1;
+  }
+}
+
+static void tls_print_extension(BIO *bio, const char *indent, int server,
+    int ext_type, const unsigned char *ext, size_t extlen) {
+
+  BIO_printf(bio, "%sextension_type = %s (%lu %s)\n", indent,
+    tls_get_label(ext_type, tls_extension_labels), (unsigned long) extlen,
+    extlen != 1 ? "bytes" : "byte");
+
+  /* There might be additional extension information to be displayed. */
+}
+
+static void tls_print_extensions(BIO *bio, const char *name, int server,
+    const unsigned char **msg, size_t *msglen) {
+  size_t len;
+
+  if (*msglen == 0) {
+    BIO_printf(bio, "%s: None\n", name);
+    return;
+  }
+
+  len = ((*msg)[0] << 8) | (*msg)[1];
+  if (len != (*msglen - 2)) {
+    return;
+  }
+
+  *msg += 2;
+  *msglen -= 2;
+
+  BIO_printf(bio, "  %s (%lu %s)\n", name, (unsigned long) len,
+    len != 1 ? "bytes" : "byte");
+  while (len > 0) {
+    int ext_type;
+    size_t ext_len;
+
+    pr_signals_handle();
+
+    if (*msglen < 4) {
+      break;
+    }
+
+    ext_type = ((*msg)[0] << 8) | (*msg)[1];
+    ext_len = ((*msg)[2] << 8) | (*msg)[3];
+
+    if (*msglen < (ext_len + 4)) {
+      break;
+    }
+
+    *msg += 4;
+    tls_print_extension(bio, "    ", server, ext_type, *msg, ext_len);
+    *msg += ext_len;
+    *msglen -= (ext_len + 4);
+  }
+}
+
+/* XXX Consider doing same for tls_print_server_hello? */
+static void tls_print_client_hello(int io_flag, int version, int content_type,
+    const unsigned char *buf, size_t buflen, SSL *ssl, void *arg) {
+  BIO *bio;
+  char *data = NULL;
+  long datalen;
+
+  bio = BIO_new(BIO_s_mem());
+
+  BIO_puts(bio, "\nClientHello:\n");
+  tls_print_ssl_version(bio, "client_version", &buf, &buflen);
+  tls_print_random(bio, &buf, &buflen);
+  tls_print_session_id(bio, &buf, &buflen);
+  if (buflen < 2) {
+    return;
+  }
+  tls_print_ciphersuites(bio, "cipher_suites", &buf, &buflen);
+  if (buflen < 1) {
+    return;
+  }
+  tls_print_compressions(bio, "compression_methods", &buf, &buflen);
+  tls_print_extensions(bio, "extensions", FALSE, &buf, &buflen);
+
+  datalen = BIO_get_mem_data(bio, &data);
+  if (data != NULL) {
+    data[datalen] = '\0';
+    tls_log("[msg] %.*s", (int) datalen, data);
+  }
+
+  BIO_free(bio);
+}
+
 static void tls_msg_cb(int io_flag, int version, int content_type,
     const void *buf, size_t buflen, SSL *ssl, void *arg) {
-  char *action_str = NULL;
-  char *version_str = NULL;
+  char *action_str = NULL, *version_str = NULL;
   char *bytes_str = buflen != 1 ? "bytes" : "byte";
 
   if (io_flag == 0) {
@@ -856,42 +1576,42 @@ static void tls_msg_cb(int io_flag, int version, int content_type,
       version == TLS1_VERSION) {
 
     switch (content_type) {
-      case 20:
+      case SSL3_RT_CHANGE_CIPHER_SPEC:
         /* ChangeCipherSpec message */
         tls_log("[msg] %s %s ChangeCipherSpec message (%u %s)",
           action_str, version_str, (unsigned int) buflen, bytes_str);
         break;
 
-      case 21: {
+      case SSL3_RT_ALERT: {
         /* Alert messages */
         if (buflen == 2) {
           char *severity_str = NULL;
 
           /* Peek naughtily into the buffer. */
           switch (((const unsigned char *) buf)[0]) {
-            case 1:
+            case SSL3_AL_WARNING:
               severity_str = "warning";
               break;
 
-            case 2:
+            case SSL3_AL_FATAL:
               severity_str = "fatal";
               break;
           }
 
           switch (((const unsigned char *) buf)[1]) {
-            case 0:
+            case SSL3_AD_CLOSE_NOTIFY:
               tls_log("[msg] %s %s %s 'close_notify' Alert message (%u %s)",
                 action_str, version_str, severity_str, (unsigned int) buflen,
                 bytes_str);
               break;
 
-            case 10:
+            case SSL3_AD_UNEXPECTED_MESSAGE:
               tls_log("[msg] %s %s %s 'unexpected_message' Alert message "
                 "(%u %s)", action_str, version_str, severity_str,
                 (unsigned int) buflen, bytes_str);
               break;
 
-            case 20:
+            case SSL3_AD_BAD_RECORD_MAC:
               tls_log("[msg] %s %s %s 'bad_record_mac' Alert message (%u %s)",
                 action_str, version_str, severity_str, (unsigned int) buflen,
                 bytes_str);
@@ -909,17 +1629,73 @@ static void tls_msg_cb(int io_flag, int version, int content_type,
                 bytes_str);
               break;
 
-            case 30:
+            case SSL3_AD_DECOMPRESSION_FAILURE:
               tls_log("[msg] %s %s %s 'decompression_failure' Alert message "
                 "(%u %s)", action_str, version_str, severity_str,
                 (unsigned int) buflen, bytes_str);
               break;
 
-            case 40:
+            case SSL3_AD_HANDSHAKE_FAILURE:
               tls_log("[msg] %s %s %s 'handshake_failure' Alert message "
                 "(%u %s)", action_str, version_str, severity_str,
                 (unsigned int) buflen, bytes_str);
               break;
+
+#ifdef SSL3_AD_NO_CERTIFICATE
+            case SSL3_AD_NO_CERTIFICATE:
+              tls_log("[msg] %s %s %s 'no_certificate' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_NO_CERTIFICATE */
+
+#ifdef SSL3_AD_BAD_CERTIFICATE
+            case SSL3_AD_BAD_CERTIFICATE:
+              tls_log("[msg] %s %s %s 'bad_certificate' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_BAD_CERTIFICATE */
+
+#ifdef SSL3_AD_UNSUPPORTED_CERTIFICATE
+            case SSL3_AD_UNSUPPORTED_CERTIFICATE:
+              tls_log("[msg] %s %s %s 'unsupported_certificate' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_UNSUPPORTED_CERTIFICATE */
+
+#ifdef SSL3_AD_CERTIFICATE_REVOKED
+            case SSL3_AD_CERTIFICATE_REVOKED:
+              tls_log("[msg] %s %s %s 'certificate_revoked' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_CERTIFICATE_REVOKED */
+
+#ifdef SSL3_AD_CERTIFICATE_EXPIRED
+            case SSL3_AD_CERTIFICATE_EXPIRED:
+              tls_log("[msg] %s %s %s 'certificate_expired' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_CERTIFICATE_EXPIRED */
+
+#ifdef SSL3_AD_CERTIFICATE_UNKNOWN
+            case SSL3_AD_CERTIFICATE_UNKNOWN:
+              tls_log("[msg] %s %s %s 'certificate_unknown' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_CERTIFICATE_UNKNOWN */
+
+#ifdef SSL3_AD_ILLEGAL_PARAMETER
+            case SSL3_AD_ILLEGAL_PARAMETER:
+              tls_log("[msg] %s %s %s 'illegal_parameter' Alert message "
+                "(%u %s)", action_str, version_str, severity_str,
+                (unsigned int) buflen, bytes_str);
+              break;
+#endif /* SSL3_AD_ILLEGAL_PARAMETER */
           }
 
         } else {
@@ -930,69 +1706,94 @@ static void tls_msg_cb(int io_flag, int version, int content_type,
         break;
       }
 
-      case 22: {
+      case SSL3_RT_HANDSHAKE: {
         /* Handshake messages */
         if (buflen > 0) {
           /* Peek naughtily into the buffer. */
           switch (((const unsigned char *) buf)[0]) {
-            case 0:
+            case SSL3_MT_HELLO_REQUEST:
               tls_log("[msg] %s %s 'HelloRequest' Handshake message (%u %s)",
                 action_str, version_str, (unsigned int) buflen, bytes_str);
               break;
 
-            case 1:
+            case SSL3_MT_CLIENT_HELLO: {
+              const unsigned char *msg;
+              size_t msglen;
+
+              msg = buf;
+              msglen = buflen;
+
               tls_log("[msg] %s %s 'ClientHello' Handshake message (%u %s)",
-                action_str, version_str, (unsigned int) buflen, bytes_str);
+                action_str, version_str, (unsigned int) msglen, bytes_str);
+
+              tls_print_client_hello(io_flag, version, content_type, msg + 4,
+                msglen - 4, ssl, arg);
               break;
+            }
 
-            case 2:
+            case SSL3_MT_SERVER_HELLO:
               tls_log("[msg] %s %s 'ServerHello' Handshake message (%u %s)",
                 action_str, version_str, (unsigned int) buflen, bytes_str);
               break;
 
-            case 11:
+            case SSL3_MT_NEWSESSION_TICKET:
+              tls_log("[msg] %s %s 'NewSessionTicket' Handshake message "
+                "(%u %s)", action_str, version_str, (unsigned int) buflen,
+                bytes_str);
+              break;
+
+            case SSL3_MT_CERTIFICATE:
               tls_log("[msg] %s %s 'Certificate' Handshake message (%u %s)",
                 action_str, version_str, (unsigned int) buflen, bytes_str);
               break;
 
-            case 12:
+            case SSL3_MT_SERVER_KEY_EXCHANGE:
               tls_log("[msg] %s %s 'ServerKeyExchange' Handshake message "
                 "(%u %s)", action_str, version_str, (unsigned int) buflen,
                 bytes_str);
               break;
 
-            case 13:
+            case SSL3_MT_CERTIFICATE_REQUEST:
               tls_log("[msg] %s %s 'CertificateRequest' Handshake message "
                 "(%u %s)", action_str, version_str, (unsigned int) buflen,
                 bytes_str);
               break;
 
-            case 14:
+            case SSL3_MT_SERVER_DONE:
               tls_log("[msg] %s %s 'ServerHelloDone' Handshake message (%u %s)",
                 action_str, version_str, (unsigned int) buflen, bytes_str);
               break;
 
-            case 15:
+            case SSL3_MT_CERTIFICATE_VERIFY:
               tls_log("[msg] %s %s 'CertificateVerify' Handshake message "
                 "(%u %s)", action_str, version_str, (unsigned int) buflen,
                 bytes_str);
               break;
 
-            case 16:
+            case SSL3_MT_CLIENT_KEY_EXCHANGE:
               tls_log("[msg] %s %s 'ClientKeyExchange' Handshake message "
                 "(%u %s)", action_str, version_str, (unsigned int) buflen,
                 bytes_str);
               break;
 
-            case 20:
+            case SSL3_MT_FINISHED:
               tls_log("[msg] %s %s 'Finished' Handshake message (%u %s)",
                 action_str, version_str, (unsigned int) buflen, bytes_str);
               break;
+
+#ifdef SSL3_MT_CERTIFICATE_STATUS
+            case SSL3_MT_CERTIFICATE_STATUS:
+              tls_log("[msg] %s %s 'CertificateStatus' Handshake message "
+                "(%u %s)", action_str, version_str, (unsigned int) buflen,
+                bytes_str);
+              break;
+#endif /* SSL3_MT_CERTIFICATE_STATUS */
           }
 
         } else {
-          tls_log("[msg] %s %s Handshake message, unknown type (%u %s)",
-            action_str, version_str, (unsigned int) buflen, bytes_str);
+          tls_log("[msg] %s %s Handshake message, unknown type %d (%u %s)",
+            action_str, version_str, content_type, (unsigned int) buflen,
+            bytes_str);
         }
 
         break;
@@ -1040,8 +1841,9 @@ static void tls_msg_cb(int io_flag, int version, int content_type,
             }
 
           } else {
-            tls_log("[msg] %s %s Error message, unknown type (%u %s)",
-              action_str, version_str, (unsigned int) buflen, bytes_str);
+            tls_log("[msg] %s %s Error message, unknown type %d (%u %s)",
+              action_str, version_str, content_type, (unsigned int) buflen,
+              bytes_str);
           }
           break;
         }
@@ -1092,14 +1894,22 @@ static void tls_msg_cb(int io_flag, int version, int content_type,
         (unsigned int) buflen, bytes_str);
     }
 
+#ifdef SSL3_RT_HEADER
+  } else if (version == 0 &&
+             content_type == SSL3_RT_HEADER &&
+             buflen == SSL3_RT_HEADER_LENGTH) {
+    tls_log("[msg] %s protocol record message (%u %s)", action_str,
+      (unsigned int) buflen, bytes_str);
+#endif
+
   } else {
     /* This case might indicate an issue with OpenSSL itself; the version
      * given to the msg_callback function was not initialized, or not set to
      * one of the recognized SSL/TLS versions.  Weird.
      */
 
-    tls_log("[msg] %s message of unknown version (%d) (%u %s)", action_str,
-      version, (unsigned int) buflen, bytes_str);
+    tls_log("[msg] %s message of unknown version %d, type %d (%u %s)",
+      action_str, version, content_type, (unsigned int) buflen, bytes_str);
   }
 
 }
@@ -1169,7 +1979,7 @@ static int tls_cert_match_dns_san(pool *p, X509 *cert, const char *dns_name) {
          * only checks "www.goodguy.com".
          */
 
-        if (ASN1_STRING_length(alt_name->d.ia5) != dns_sanlen) {
+        if ((size_t) ASN1_STRING_length(alt_name->d.ia5) != dns_sanlen) {
           tls_log("%s", "cert dNSName SAN contains embedded NULs, "
             "rejecting as possible spoof attempt");
           tls_log("suspicious dNSName SAN value: '%s'",
@@ -1364,7 +2174,7 @@ static int tls_cert_match_cn(pool *p, X509 *cert, const char *name,
 
   cn_len = strlen(cn_str);
 
-  if (ASN1_STRING_length(cn_asn1) != cn_len) {
+  if ((size_t) ASN1_STRING_length(cn_asn1) != cn_len) {
     tls_log("%s", "cert CommonName contains embedded NULs, rejecting as "
       "possible spoof attempt");
     tls_log("suspicious CommonName value: '%s'",
@@ -1397,7 +2207,7 @@ static int tls_check_client_cert(SSL *ssl, conn_t *conn) {
   int ok = -1;
 
   /* Only perform these more stringent checks if asked to verify clients. */
-  if (!(tls_flags & TLS_SESS_VERIFY_CLIENT)) {
+  if (!(tls_flags & TLS_SESS_VERIFY_CLIENT_REQUIRED)) {
     return 0;
   }
 
@@ -1533,18 +2343,18 @@ static int tls_check_server_cert(SSL *ssl, conn_t *conn) {
   }
 
   if (ok == 0 &&
-      !(tls_opts & TLS_SESS_VERIFY_SERVER_NO_DNS)) {
+      !(tls_flags & TLS_SESS_VERIFY_SERVER_NO_DNS)) {
     int reverse_dns;
     const char *remote_name;
+    pr_netaddr_t *remote_addr;
 
     reverse_dns = pr_netaddr_set_reverse_dns(TRUE);
 
-    /* XXX Should clear the Netaddr cache here, but just for our single
-     * name.  Should be an API for that.
-     */
-    pr_netaddr_clear_cache();
+    pr_netaddr_clear_ipcache(pr_netaddr_get_ipstr(conn->remote_addr));
+
+    remote_addr = (pr_netaddr_t *) conn->remote_addr;
+    remote_addr->na_have_dnsstr = FALSE;
 
-    conn->remote_addr->na_have_dnsstr = FALSE;
     remote_name = pr_netaddr_get_dnsstr(conn->remote_addr);
     pr_netaddr_set_reverse_dns(reverse_dns);
 
@@ -1555,8 +2365,6 @@ static int tls_check_server_cert(SSL *ssl, conn_t *conn) {
   }
 
   X509_free(cert);
-
-  ok = 0;
   return ok;
 }
 
@@ -1600,7 +2408,11 @@ static void tls_prepare_provider_fds(int stdout_fd, int stderr_fd) {
 # elif defined(RLIMIT_OFILE)
   if (getrlimit(RLIMIT_OFILE, &rlim) < 0) {
 # endif
-    tls_log("getrlimit error: %s", strerror(errno));
+    /* Ignore ENOSYS (and EPERM, since some libc's use this as ENOSYS). */
+    if (errno != ENOSYS &&
+        errno != EPERM) {
+      tls_log("getrlimit error: %s", strerror(errno));
+    }
 
     /* Pick some arbitrary high number. */
     nfiles = 255;
@@ -1611,45 +2423,57 @@ static void tls_prepare_provider_fds(int stdout_fd, int stderr_fd) {
    nfiles = 255;
 #endif
 
-  if (nfiles > 255)
+  if (nfiles > 255) {
     nfiles = 255;
+  }
 
   /* Close the "non-standard" file descriptors. */
-  for (i = 3; i < nfiles; i++)
+  for (i = 3; i < nfiles; i++) {
     (void) close(i);
+  }
 
   return;
 }
 
 static void tls_prepare_provider_pipes(int *stdout_pipe, int *stderr_pipe) {
   if (pipe(stdout_pipe) < 0) {
-    tls_log("error opening stdout pipe: %s", strerror(errno));
+    pr_trace_msg(trace_channel, 2, "error opening stdout pipe: %s",
+      strerror(errno));
     stdout_pipe[0] = -1;
     stdout_pipe[1] = STDOUT_FILENO;
 
   } else {
-    if (fcntl(stdout_pipe[0], F_SETFD, FD_CLOEXEC) < 0)
-      tls_log("error setting close-on-exec flag on stdout pipe read fd: %s",
+    if (fcntl(stdout_pipe[0], F_SETFD, FD_CLOEXEC) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error setting close-on-exec flag on stdout pipe read fd: %s",
         strerror(errno));
+    }
 
-    if (fcntl(stdout_pipe[1], F_SETFD, 0) < 0)
-      tls_log("error setting close-on-exec flag on stdout pipe write fd: %s",
+    if (fcntl(stdout_pipe[1], F_SETFD, 0) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error setting close-on-exec flag on stdout pipe write fd: %s",
         strerror(errno));
+    }
   }
 
   if (pipe(stderr_pipe) < 0) {
-    tls_log("error opening stderr pipe: %s", strerror(errno));
+    pr_trace_msg(trace_channel, 2, "error opening stderr pipe: %s",
+      strerror(errno));
     stderr_pipe[0] = -1;
     stderr_pipe[1] = STDERR_FILENO;
 
   } else {
-    if (fcntl(stderr_pipe[0], F_SETFD, FD_CLOEXEC) < 0)
-      tls_log("error setting close-on-exec flag on stderr pipe read fd: %s",
+    if (fcntl(stderr_pipe[0], F_SETFD, FD_CLOEXEC) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error setting close-on-exec flag on stderr pipe read fd: %s",
         strerror(errno));
+    }
 
-    if (fcntl(stderr_pipe[1], F_SETFD, 0) < 0)
-      tls_log("error setting close-on-exec flag on stderr pipe write fd: %s",
+    if (fcntl(stderr_pipe[1], F_SETFD, 0) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error setting close-on-exec flag on stderr pipe write fd: %s",
         strerror(errno));
+    }
   }
 }
 
@@ -1667,17 +2491,20 @@ static int tls_exec_passphrase_provider(server_rec *s, char *buf, int buflen,
   sigemptyset(&sa_ignore.sa_mask);
   sa_ignore.sa_flags = 0;
 
-  if (sigaction(SIGINT, &sa_ignore, &sa_intr) < 0)
+  if (sigaction(SIGINT, &sa_ignore, &sa_intr) < 0) {
     return -1;
+  }
 
-  if (sigaction(SIGQUIT, &sa_ignore, &sa_quit) < 0)
+  if (sigaction(SIGQUIT, &sa_ignore, &sa_quit) < 0) {
     return -1;
+  }
 
   sigemptyset(&set_chldmask);
   sigaddset(&set_chldmask, SIGCHLD);
 
-  if (sigprocmask(SIG_BLOCK, &set_chldmask, &set_save) < 0)
+  if (sigprocmask(SIG_BLOCK, &set_chldmask, &set_save) < 0) {
     return -1;
+  }
 
   tls_prepare_provider_pipes(stdout_pipe, stderr_pipe);
 
@@ -1785,8 +2612,8 @@ static int tls_exec_passphrase_provider(server_rec *s, char *buf, int buflen,
     while (res <= 0) {
       if (res < 0) {
         if (errno != EINTR) {
-          pr_log_debug(DEBUG2, MOD_TLS_VERSION
-            ": passphrase provider error: unable to wait for pid %u: %s",
+          pr_trace_msg(trace_channel, 2,
+            "passphrase provider error: unable to wait for pid %u: %s",
             (unsigned int) pid, strerror(errno));
           status = -1;
           break;
@@ -1833,8 +2660,9 @@ static int tls_exec_passphrase_provider(server_rec *s, char *buf, int buflen,
       fds = select(maxfd + 1, &readfds, NULL, NULL, &tv);
 
       if (fds == -1 &&
-          errno == EINTR)
+          errno == EINTR) {
         pr_signals_handle();
+      }
 
       if (fds > 0) {
         /* The child sent us something.  How thoughtful. */
@@ -1842,12 +2670,18 @@ static int tls_exec_passphrase_provider(server_rec *s, char *buf, int buflen,
         if (FD_ISSET(stdout_pipe[0], &readfds)) {
           res = read(stdout_pipe[0], buf, buflen);
           if (res > 0) {
-              while (res && (buf[res-1] == '\r' || buf[res-1] == '\n'))
-                res--;
-              buf[res] = '\0';
+            buf[buflen-1] = '\0';
+
+            while (res &&
+                   (buf[res-1] == '\r' ||
+                    buf[res-1] == '\n')) {
+              pr_signals_handle();
+              res--;
+            }
+            buf[res] = '\0';
 
-              pr_trace_msg(trace_channel, 18,
-                "read passphrase from '%s'", tls_passphrase_provider);
+            pr_trace_msg(trace_channel, 18, "read passphrase from '%s'",
+              tls_passphrase_provider);
 
           } else if (res < 0) {
             int xerrno = errno;
@@ -1863,27 +2697,44 @@ static int tls_exec_passphrase_provider(server_rec *s, char *buf, int buflen,
         }
 
         if (FD_ISSET(stderr_pipe[0], &readfds)) {
-          int stderrlen;
-          char stderrbuf[PIPE_BUF];
+          long stderrlen, stderrsz;
+          char *stderrbuf;
+          pool *tmp_pool = make_sub_pool(s->pool);
+
+          stderrbuf = pr_fsio_getpipebuf(tmp_pool, stderr_pipe[0], &stderrsz);
+          memset(stderrbuf, '\0', stderrsz);
 
-          memset(stderrbuf, '\0', sizeof(stderrbuf));
-          stderrlen = read(stderr_pipe[0], stderrbuf, sizeof(stderrbuf)-1);
+          stderrlen = read(stderr_pipe[0], stderrbuf, stderrsz-1);
           if (stderrlen > 0) {
             while (stderrlen &&
                    (stderrbuf[stderrlen-1] == '\r' ||
-                    stderrbuf[stderrlen-1] == '\n'))
+                    stderrbuf[stderrlen-1] == '\n')) {
               stderrlen--;
+            }
             stderrbuf[stderrlen] = '\0';
 
+            pr_trace_msg(trace_channel, 5,
+              "stderr from '%s': %s", tls_passphrase_provider,
+              stderrbuf);
+
             pr_log_debug(DEBUG5, MOD_TLS_VERSION
               ": stderr from '%s': %s", tls_passphrase_provider,
               stderrbuf);
 
           } else if (res < 0) {
+            int xerrno = errno;
+
+            pr_trace_msg(trace_channel, 2,
+              "error reading stderr from '%s': %s",
+              tls_passphrase_provider, strerror(xerrno));
+
             pr_log_debug(DEBUG2, MOD_TLS_VERSION
               ": error reading stderr from '%s': %s",
-              tls_passphrase_provider, strerror(errno));
+              tls_passphrase_provider, strerror(xerrno));
           }
+
+          destroy_pool(tmp_pool);
+          tmp_pool = NULL;
         }
       }
 
@@ -1892,14 +2743,17 @@ static int tls_exec_passphrase_provider(server_rec *s, char *buf, int buflen,
   }
 
   /* Restore the previous signal actions. */
-  if (sigaction(SIGINT, &sa_intr, NULL) < 0)
+  if (sigaction(SIGINT, &sa_intr, NULL) < 0) {
     return -1;
+  }
 
-  if (sigaction(SIGQUIT, &sa_quit, NULL) < 0)
+  if (sigaction(SIGQUIT, &sa_quit, NULL) < 0) {
     return -1; 
+  }
 
-  if (sigprocmask(SIG_SETMASK, &set_save, NULL) < 0)
+  if (sigprocmask(SIG_SETMASK, &set_save, NULL) < 0) {
     return -1;
+  }
 
   if (WIFSIGNALED(status)) {
     pr_log_debug(DEBUG2, MOD_TLS_VERSION
@@ -1916,7 +2770,7 @@ static int tls_passphrase_cb(char *buf, int buflen, int rwflag, void *d) {
   static int need_banner = TRUE;
   struct tls_pkey_data *pdata = d;
 
-  if (!tls_passphrase_provider) {
+  if (tls_passphrase_provider == NULL) {
     register unsigned int attempt;
     int pwlen = 0;
 
@@ -1945,10 +2799,12 @@ static int tls_passphrase_cb(char *buf, int buflen, int rwflag, void *d) {
        * means a system error occurred, and 1 means user interaction problems.
        */
       if (res != 0) {
-         fprintf(stderr, "\nPassphrases do not match.  Please try again.\n");
-         continue;
+        fprintf(stderr, "\nPassphrases do not match.  Please try again.\n");
+        continue;
       }
 
+      /* Ensure that the buffer is NUL-terminated. */
+      buf[buflen-1] = '\0';
       pwlen = strlen(buf);
       if (pwlen < 1) {
         fprintf(stderr, "Error: passphrase must be at least one character\n");
@@ -1969,6 +2825,9 @@ static int tls_passphrase_cb(char *buf, int buflen, int rwflag, void *d) {
         tls_passphrase_provider, strerror(errno));
 
     } else {
+      /* Ensure that the buffer is NUL-terminated. */
+      buf[buflen-1] = '\0';
+
       sstrncpy(pdata->buf, buf, pdata->bufsz);
       pdata->buflen = strlen(buf);
 
@@ -1994,9 +2853,10 @@ static void set_prompt_fds(void) {
    * to the general stderr logfile.
    */
   prompt_fd = open("/dev/null", O_WRONLY);
-  if (prompt_fd == -1)
+  if (prompt_fd == -1) {
     /* This is an arbitrary, meaningless placeholder number. */
     prompt_fd = 76;
+  }
 
   dup2(STDERR_FILENO, prompt_fd);
   dup2(STDOUT_FILENO, STDERR_FILENO);
@@ -2166,6 +3026,11 @@ static int tls_get_passphrase(server_rec *s, const char *path,
       SYSerr(SYS_F_FOPEN, xerrno);
       return -1;
     }
+
+    /* As the file contains sensitive data, we do not want it lingering
+     * around in stdio buffers.
+     */
+    (void) setvbuf(keyf, NULL, _IONBF, 0);
   }
 
   pdata.s = s;
@@ -2182,7 +3047,7 @@ static int tls_get_passphrase(server_rec *s, const char *path,
 
     res = tls_get_pkcs12_passwd(s, keyf, prompt, buf, bufsz, flags, &pdata);
 
-    if (keyf) {
+    if (keyf != NULL) {
       fclose(keyf);
       keyf = NULL;
     }
@@ -2203,17 +3068,18 @@ static int tls_get_passphrase(server_rec *s, const char *path,
     ERR_clear_error();
 
     pkey = PEM_read_PrivateKey(keyf, NULL, tls_passphrase_cb, &pdata);
-    if (pkey)
+    if (pkey != NULL) {
       break;
+    }
 
-    if (keyf) {
+    if (keyf != NULL) {
       fseek(keyf, 0, SEEK_SET);
     }
 
     fprintf(stderr, "\nWrong passphrase for this key.  Please try again.\n");
   }
 
-  if (keyf) {
+  if (keyf != NULL) {
     fclose(keyf);
     keyf = NULL;
   }
@@ -2221,34 +3087,37 @@ static int tls_get_passphrase(server_rec *s, const char *path,
   /* Restore the normal stderr logging. */
   restore_prompt_fds();
 
-  if (pkey == NULL)
+  if (pkey == NULL) {
     return -1;
+  }
 
   EVP_PKEY_free(pkey);
 
+  if (pdata.buflen > 0) {
 #if OPENSSL_VERSION_NUMBER >= 0x000905000L
-  /* Use the obtained passphrase as additional entropy, ostensibly
-   * unknown to attackers who may be watching the network, for
-   * OpenSSL's PRNG.
-   *
-   * Human language gives about 2-3 bits of entropy per byte (RFC1750).
-   */
-  RAND_add(buf, pdata.buflen, pdata.buflen * 0.25);
+    /* Use the obtained passphrase as additional entropy, ostensibly
+     * unknown to attackers who may be watching the network, for
+     * OpenSSL's PRNG.
+     *
+     * Human language gives about 2-3 bits of entropy per byte (RFC1750).
+     */
+    RAND_add(buf, pdata.buflen, pdata.buflen * 0.25);
 #endif
 
 #ifdef HAVE_MLOCK
-   PRIVS_ROOT
-   if (mlock(buf, bufsz) < 0) {
-     pr_log_debug(DEBUG1, MOD_TLS_VERSION
-       ": error locking passphrase into memory: %s", strerror(errno));
-
-   } else {
-     pr_log_debug(DEBUG1, MOD_TLS_VERSION ": passphrase locked into memory");
-   }
-   PRIVS_RELINQUISH
+    PRIVS_ROOT
+    if (mlock(buf, bufsz) < 0) {
+      pr_log_debug(DEBUG1, MOD_TLS_VERSION
+        ": error locking passphrase into memory: %s", strerror(errno));
+
+    } else {
+      pr_log_debug(DEBUG1, MOD_TLS_VERSION ": passphrase locked into memory");
+    }
+    PRIVS_RELINQUISH
 #endif
+  }
 
-  return 0;
+  return pdata.buflen;
 }
 
 static int tls_handshake_timeout_cb(CALLBACK_FRAME) {
@@ -2256,40 +3125,87 @@ static int tls_handshake_timeout_cb(CALLBACK_FRAME) {
   return 0;
 }
 
+static void tls_scrub_pkey(tls_pkey_t *k) {
+  if (k->rsa_pkey != NULL) {
+    pr_memscrub(k->rsa_pkey, k->pkeysz);
+    free(k->rsa_pkey_ptr);
+    k->rsa_pkey = k->rsa_pkey_ptr = NULL;
+    k->rsa_passlen = 0;
+  }
+
+  if (k->dsa_pkey != NULL) {
+    pr_memscrub(k->dsa_pkey, k->pkeysz);
+    free(k->dsa_pkey_ptr);
+    k->dsa_pkey = k->dsa_pkey_ptr = NULL;
+    k->dsa_passlen = 0;
+  }
+
+#ifdef PR_USE_OPENSSL_ECC
+  if (k->ec_pkey != NULL) {
+    pr_memscrub(k->ec_pkey, k->pkeysz);
+    free(k->ec_pkey_ptr);
+    k->ec_pkey = k->ec_pkey_ptr = NULL;
+    k->ec_passlen = 0;
+  }
+#endif /* PR_USE_OPENSSL_ECC */
+
+  if (k->pkcs12_passwd != NULL) {
+    pr_memscrub(k->pkcs12_passwd, k->pkeysz);
+    free(k->pkcs12_passwd_ptr);
+    k->pkcs12_passwd = k->pkcs12_passwd_ptr = NULL;
+    k->pkcs12_passlen = 0;
+  }
+
+  if (k->path != NULL) {
+    free((void *) k->path);
+    k->path = NULL;
+  }
+
+  k->next = NULL;
+  k->sid = 0;
+}
+
 static tls_pkey_t *tls_lookup_pkey(void) {
-  tls_pkey_t *k, *pkey = NULL;
+  tls_pkey_t *k, *knext, *pkey = NULL;
 
-  for (k = tls_pkey_list; k; k = k->next) {
+  for (k = tls_pkey_list; k; k = knext) {
+    pr_signals_handle();
+
+    knext = k->next;
 
     /* If this pkey matches the current server_rec, mark it and move on. */
-    if (k->server == main_server) {
+    if (k->sid == main_server->sid) {
 
 #ifdef HAVE_MLOCK
       /* mlock() the passphrase memory areas again; page locks are not
        * inherited across forks.
        */
       PRIVS_ROOT
-      if (k->rsa_pkey) {
+      if (k->rsa_pkey != NULL &&
+          k->rsa_passlen > 0) {
         if (mlock(k->rsa_pkey, k->pkeysz) < 0) {
           tls_log("error locking passphrase into memory: %s", strerror(errno));
         }
       }
 
-      if (k->dsa_pkey) {
+      if (k->dsa_pkey != NULL &&
+          k->dsa_passlen > 0) {
         if (mlock(k->dsa_pkey, k->pkeysz) < 0) {
           tls_log("error locking passphrase into memory: %s", strerror(errno));
         }
       }
 
 # ifdef PR_USE_OPENSSL_ECC
-      if (k->ec_pkey) {
+      if (k->ec_pkey != NULL &&
+          k->ec_passlen > 0) {
         if (mlock(k->ec_pkey, k->pkeysz) < 0) {
           tls_log("error locking passphrase into memory: %s", strerror(errno));
         }
       }
 # endif /* PR_USE_OPENSSL_ECC */
 
-      if (k->pkcs12_passwd) {
+      if (k->pkcs12_passwd != NULL &&
+          k->pkcs12_passlen > 0) {
         if (mlock(k->pkcs12_passwd, k->pkeysz) < 0) {
           tls_log("error locking password into memory: %s", strerror(errno));
         }
@@ -2298,35 +3214,11 @@ static tls_pkey_t *tls_lookup_pkey(void) {
 #endif /* HAVE_MLOCK */
 
       pkey = k;
-      continue;
+      break;
     }
 
     /* Otherwise, scrub the passphrase's memory areas. */
-    if (k->rsa_pkey) {
-      pr_memscrub(k->rsa_pkey, k->pkeysz);
-      free(k->rsa_pkey_ptr);
-      k->rsa_pkey = k->rsa_pkey_ptr = NULL;
-    }
-
-    if (k->dsa_pkey) {
-      pr_memscrub(k->dsa_pkey, k->pkeysz);
-      free(k->dsa_pkey_ptr);
-      k->dsa_pkey = k->dsa_pkey_ptr = NULL;
-    }
-
-# ifdef PR_USE_OPENSSL_ECC
-    if (k->ec_pkey) {
-      pr_memscrub(k->ec_pkey, k->pkeysz);
-      free(k->ec_pkey_ptr);
-      k->ec_pkey = k->ec_pkey_ptr = NULL;
-    }
-# endif /* PR_USE_OPENSSL_ECC */
-
-    if (k->pkcs12_passwd) {
-      pr_memscrub(k->pkcs12_passwd, k->pkeysz);
-      free(k->pkcs12_passwd_ptr);
-      k->pkcs12_passwd = k->pkcs12_passwd_ptr = NULL;
-    }
+    tls_scrub_pkey(k);
   }
 
   return pkey;
@@ -2335,8 +3227,9 @@ static tls_pkey_t *tls_lookup_pkey(void) {
 static int tls_pkey_cb(char *buf, int buflen, int rwflag, void *data) {
   tls_pkey_t *k;
 
-  if (!data)
+  if (data == NULL) {
     return 0;
+  }
 
   k = (tls_pkey_t *) data;
 
@@ -2363,51 +3256,140 @@ static int tls_pkey_cb(char *buf, int buflen, int rwflag, void *data) {
   return 0;
 }
 
-static void tls_scrub_pkeys(void) {
-  tls_pkey_t *k;
+static void tls_remove_pkey(tls_pkey_t *k) {
+  if (tls_pkey_list != k) {
+    tls_pkey_t *ki, *prev;
 
-  /* Scrub and free all passphrases in memory. */
-  if (tls_pkey_list) {
-    pr_log_debug(DEBUG5, MOD_TLS_VERSION
-      ": scrubbing %u %s from memory",
-      tls_npkeys, tls_npkeys != 1 ? "passphrases" : "passphrase");
+    prev = tls_pkey_list;
+    for (ki = tls_pkey_list->next; ki; ki = ki->next) {
+      if (ki == k) {
+        prev->next = k->next;
+        break;
+      }
+
+      prev = ki;
+    }
 
   } else {
+    /* We are the head of the list. */
+    tls_pkey_list = k->next;
+  }
+
+  if (tls_npkeys > 0) {
+    tls_npkeys--;
+  }
+}
+
+static void tls_scrub_pkeys(void) {
+  tls_pkey_t *k, *knext;
+  unsigned int passphrase_count = 0;
+
+  if (tls_pkey_list == NULL) {
     return;
   }
 
+  /* Scrub and free all passphrases in memory. */
   for (k = tls_pkey_list; k; k = k->next) {
-    if (k->rsa_pkey) {
-      pr_memscrub(k->rsa_pkey, k->pkeysz);
-      free(k->rsa_pkey_ptr);
-      k->rsa_pkey = k->rsa_pkey_ptr = NULL;
+    if (k->rsa_pkey != NULL &&
+        k->rsa_passlen > 0) {
+      passphrase_count++;
     }
 
-    if (k->dsa_pkey) {
-      pr_memscrub(k->dsa_pkey, k->pkeysz);
-      free(k->dsa_pkey_ptr);
-      k->dsa_pkey = k->dsa_pkey_ptr = NULL;
+    if (k->dsa_pkey != NULL &&
+        k->dsa_passlen > 0) {
+      passphrase_count++;
     }
 
 #ifdef PR_USE_OPENSSL_ECC
-    if (k->ec_pkey) {
-      pr_memscrub(k->ec_pkey, k->pkeysz);
-      free(k->ec_pkey_ptr);
-      k->ec_pkey = k->ec_pkey_ptr = NULL;
+    if (k->ec_pkey != NULL &&
+        k->ec_passlen > 0) {
+      passphrase_count++;
     }
 #endif /* PR_USE_OPENSSL_ECC */
 
-    if (k->pkcs12_passwd) {
-      pr_memscrub(k->pkcs12_passwd, k->pkeysz);
-      free(k->pkcs12_passwd_ptr);
-      k->pkcs12_passwd = k->pkcs12_passwd_ptr = NULL;
+    if (k->pkcs12_passwd != NULL &&
+        k->pkcs12_passlen > 0) {
+      passphrase_count++;
     }
   }
 
+  if (passphrase_count == 0) {
+    tls_pkey_list = NULL;
+    tls_npkeys = 0;
+    return;
+  }
+
+  pr_log_debug(DEBUG5, MOD_TLS_VERSION
+    ": scrubbing %u %s from memory", passphrase_count,
+    passphrase_count != 1 ? "passphrases" : "passphrase");
+
+  for (k = tls_pkey_list; k; k = knext) {
+    knext = k->next;
+
+    pr_signals_handle();
+    tls_scrub_pkey(k);
+  }
+
   tls_pkey_list = NULL;
   tls_npkeys = 0;
 }
 
+static void tls_clean_pkeys(void) {
+  register unsigned int i;
+  tls_pkey_t *k;
+  pool *tmp_pool;
+  array_header *dead_keys, *valid_sids;
+  server_rec *s;
+
+  /* We scan the tls_pkey_list for any keys belonging to vhosts (by SID) which
+   * no longer appear in our configuration.
+   */
+
+  if (tls_pkey_list == NULL) {
+    return;
+  }
+
+  tmp_pool = make_sub_pool(tls_pool);
+  pr_pool_tag(tmp_pool, "TLS Passphrase Cleaning");
+
+  dead_keys = make_array(tmp_pool, 0, sizeof(tls_pkey_t *));
+  valid_sids = make_array(tmp_pool, 0, sizeof(unsigned int));
+
+  for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
+    *((unsigned int *) push_array(valid_sids)) = s->sid;
+  }
+
+  for (k = tls_pkey_list; k; k = k->next) {
+    int dead_key = TRUE;
+
+    for (i = 0; i < valid_sids->nelts; i++) {
+      unsigned int sid;
+
+      sid = ((unsigned int *) valid_sids->elts)[i];
+      if (k->sid == sid) {
+        dead_key = FALSE;
+        break;
+      }
+    }
+
+    if (dead_key) {
+      *((tls_pkey_t **) push_array(dead_keys)) = k;
+    }
+  }
+
+  for (i = 0; i < dead_keys->nelts; i++) {
+    pr_signals_handle();
+
+    k = ((tls_pkey_t **) dead_keys->elts)[i];
+    tls_remove_pkey(k);
+    tls_scrub_pkey(k);
+    destroy_pool(k->pool);
+  }
+
+  destroy_pool(tmp_pool);
+  return;
+}
+
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
 static int tls_renegotiate_timeout_cb(CALLBACK_FRAME) {
   if ((tls_flags & TLS_SESS_ON_CTRL) &&
@@ -2420,7 +3402,7 @@ static int tls_renegotiate_timeout_cb(CALLBACK_FRAME) {
     } else if (tls_renegotiate_required) {
       tls_log("%s", "requested TLS renegotiation timed out on control channel");
       tls_log("%s", "shutting down control channel TLS session");
-      tls_end_sess(ctrl_ssl, PR_NETIO_STRM_CTRL, 0);
+      tls_end_sess(ctrl_ssl, session.c, 0);
       pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
       pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
       ctrl_ssl = NULL;
@@ -2431,7 +3413,7 @@ static int tls_renegotiate_timeout_cb(CALLBACK_FRAME) {
       (tls_flags & TLS_SESS_DATA_RENEGOTIATING)) {
     SSL *ssl;
 
-    ssl = pr_table_get(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
+    ssl = (SSL *) pr_table_get(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
     if (!SSL_renegotiate_pending(ssl)) {
       tls_log("%s", "data channel TLS session renegotiated");
       tls_flags &= ~TLS_SESS_DATA_RENEGOTIATING;
@@ -2439,7 +3421,7 @@ static int tls_renegotiate_timeout_cb(CALLBACK_FRAME) {
     } else if (tls_renegotiate_required) {
       tls_log("%s", "requested TLS renegotiation timed out on data channel");
       tls_log("%s", "shutting down data channel TLS session");
-      tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+      tls_end_sess(ssl, session.d, 0);
       pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
       pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
     }
@@ -2660,110 +3642,1690 @@ static DH *tls_dh_cb(SSL *ssl, int is_export, int keylen) {
   return dh;
 }
 
-#ifdef PR_USE_OPENSSL_ECC
-static EC_KEY *tls_ecdh_cb(SSL *ssl, int is_export, int keylen) {
-  static EC_KEY *ecdh = NULL;
-  static int init = 0;
+#if !defined(OPENSSL_NO_TLSEXT)
+# if defined(TLSEXT_MAXLEN_host_name)
+static int tls_sni_cb(SSL *ssl, int *alert_desc, void *user_data) {
+  const char *server_name = NULL;
 
-  /* XXX Uses 256-bit key for now. TODO: support other sizes. */
+  server_name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+  if (server_name != NULL) {
+    const char *host = NULL, *sni;
 
-  if (init == 0) {
-    ecdh = EC_KEY_new();
+    pr_trace_msg(trace_channel, 5, "received SNI '%s'", server_name);
 
-    if (ecdh != NULL) {
-      /* ecdh->group = EC_GROUP_new_by_nid(NID_secp160r2); */
-      EC_KEY_set_group(ecdh,
-        EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
+    /* If we have already received a HOST command, then we need to
+     * check that the SNI value matches that of the HOST.  Otherwise,
+     * we stash the SNI, so that if/when a HOST command is received,
+     * we can check the HOST name against the SNI.
+     *
+     * RFC 7151, Section 3.2.2, does not mandate whether HOST must be
+     * sent before e.g. AUTH TLS or not; the only example the RFC provides
+     * shows AUTH TLS being used before HOST.  Section 4 of that RFC goes
+     * on to recommend using HOST before AUTH, in general, unless the SNI
+     * extension will be used, in which case, clients should use AUTH TLS
+     * before HOST.  We need to be ready for either case.
+     *
+     * Note that this SNI/HOST check can only really happen for control
+     * connections, not data connections.  FTPS clients do not receive/use
+     * DNS hostnames for data connections, only IP addresses.
+     */
+
+    host = pr_table_get(session.notes, "mod_core.host", NULL);
+
+    /* If we have already stashed an SNI, it means this is probably a data
+     * connection.
+     *
+     * For data connections where an SNI is provided, we MIGHT be able to
+     * validate that SNI (assuming it is an IP address) against our IP address,
+     * at least.
+     */
+    sni = pr_table_get(session.notes, "mod_tls.sni", NULL);
+
+    if (host != NULL &&
+        sni == NULL) {
+      /* If the requested HOST does not match the SNI, it's a fatal error.
+       *
+       * Bear in mind, however, that the HOST command might have used an
+       * IPv4/IPv6 address, NOT a name.  If that is the case, we do NOT want
+       * compare that with SNI.  Do we?
+       */
+
+      if (pr_netaddr_is_v4(host) != TRUE &&
+          pr_netaddr_is_v6(host) != TRUE) {
+        if (strcasecmp(host, server_name) != 0) {
+          tls_log("warning: SNI '%s' does not match HOST '%s', rejecting "
+            "SSL/TLS connection", server_name, host);
+          pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION
+            ": SNI '%s' does not match HOST '%s', rejecting SSL/TLS connection",
+            server_name, host);
+          *alert_desc = SSL_AD_ACCESS_DENIED;
+          return SSL_TLSEXT_ERR_ALERT_FATAL;
+        }
+      }
     }
 
-    init = 1;
+    if (pr_table_add_dup(session.notes, "mod_tls.sni",
+        (char *) server_name, 0) < 0) {
+
+      /* The session.notes may already have the SNI from the ctrl channel;
+       * no need to overwrite that.
+       */
+      if (errno != EEXIST) {
+        pr_trace_msg(trace_channel, 3,
+          "error stashing 'mod_tls.sni' in session.notes: %s", strerror(errno));
+      }
+    }
   }
 
-  return ecdh;
+  return SSL_TLSEXT_ERR_OK;
 }
-#endif /* PR_USE_OPENSSL_ECC */
+# endif /* !TLSEXT_MAXLEN_host_name */
 
-/* Post 0.9.7a, RSA blinding is turned on by default, so there is no need to
- * do this manually.
- */
-#if OPENSSL_VERSION_NUMBER < 0x0090702fL
-static void tls_blinding_on(SSL *ssl) {
-  EVP_PKEY *pkey = NULL;
-  RSA *rsa = NULL;
+static void tls_tlsext_cb(SSL *ssl, int client_server, int type,
+    unsigned char *tlsext_data, int tlsext_datalen, void *data) {
+  char *extension_name = "(unknown)";
 
-  /* RSA keys are subject to timing attacks.  To attempt to make such
-   * attacks harder, use RSA blinding.
+  /* Note: OpenSSL does not implement all possible extensions.  For the
+   * "(unknown)" extensions, see:
+   *
+   *  http://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#tls-extensiontype-values-1
    */
+  switch (type) {
+    case TLSEXT_TYPE_server_name:
+        extension_name = "server name";
+        break;
 
-  pkey = SSL_get_privatekey(ssl);
-
-  if (pkey)
-    rsa = EVP_PKEY_get1_RSA(pkey);
+    case TLSEXT_TYPE_max_fragment_length:
+        extension_name = "max fragment length";
+        break;
 
-  if (rsa) {
-    if (RSA_blinding_on(rsa, NULL) != 1) {
-      tls_log("error setting RSA blinding: %s",
-        ERR_error_string(ERR_get_error(), NULL));
+    case TLSEXT_TYPE_client_certificate_url:
+        extension_name = "client certificate URL";
+        break;
 
-    } else {
-      tls_log("set RSA blinding on");
-    }
+    case TLSEXT_TYPE_trusted_ca_keys:
+        extension_name = "trusted CA keys";
+        break;
 
-    /* Now, "free" the RSA pointer, to properly decrement the reference
-     * counter.
-     */
-    RSA_free(rsa);
+    case TLSEXT_TYPE_truncated_hmac:
+        extension_name = "truncated HMAC";
+        break;
 
-  } else {
+    case TLSEXT_TYPE_status_request:
+        extension_name = "status request";
+        break;
 
-    /* The administrator may have configured DSA keys rather than RSA keys.
-     * In this case, there is nothing to do.
-     */
-  }
+# ifdef TLSEXT_TYPE_user_mapping
+    case TLSEXT_TYPE_user_mapping:
+        extension_name = "user mapping";
+        break;
+# endif
 
-  return;
-}
-#endif
+# ifdef TLSEXT_TYPE_client_authz
+    case TLSEXT_TYPE_client_authz:
+        extension_name = "client authz";
+        break;
+# endif
 
-static int tls_init_ctx(void) {
-  config_rec *c;
-  int ssl_opts = tls_ssl_opts;
-  long ssl_mode = 0;
+# ifdef TLSEXT_TYPE_server_authz
+    case TLSEXT_TYPE_server_authz:
+        extension_name = "server authz";
+        break;
+# endif
 
-  if (pr_define_exists("TLS_USE_FIPS") &&
-      ServerType == SERVER_INETD) {
-#ifdef OPENSSL_FIPS
-    if (!FIPS_mode()) {
-      /* Make sure OpenSSL is set to use the default RNG, as per an email
-       * discussion on the OpenSSL developer list:
-       *
-       *  "The internal FIPS logic uses the default RNG to see the FIPS RNG
-       *   as part of the self test process..."
-       */
-      RAND_set_rand_method(NULL);
+# ifdef TLSEXT_TYPE_cert_type
+    case TLSEXT_TYPE_cert_type:
+        extension_name = "cert type";
+        break;
+# endif
 
-      if (!FIPS_mode_set(1)) {
-        const char *errstr;
+# ifdef TLSEXT_TYPE_elliptic_curves
+    case TLSEXT_TYPE_elliptic_curves:
+        extension_name = "elliptic curves";
+        break;
+# endif
 
-        errstr = tls_get_errors();
-        tls_log("unable to use FIPS mode: %s", errstr);
-        pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION
-          ": unable to use FIPS mode: %s", errstr);
+# ifdef TLSEXT_TYPE_ec_point_formats
+    case TLSEXT_TYPE_ec_point_formats:
+        extension_name = "EC point formats";
+        break;
+# endif
 
-        errno = EPERM;
-        return -1;
+# ifdef TLSEXT_TYPE_srp
+    case TLSEXT_TYPE_srp:
+        extension_name = "SRP";
+        break;
+# endif
 
-      } else {
-        pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION ": FIPS mode enabled");
-      }
+# ifdef TLSEXT_TYPE_signature_algorithms
+    case TLSEXT_TYPE_signature_algorithms:
+        extension_name = "signature algorithms";
+        break;
+# endif
 
-    } else {
-      pr_log_pri(PR_LOG_DEBUG, MOD_TLS_VERSION ": FIPS mode already enabled");
-    }
-#else
-    pr_log_pri(PR_LOG_WARNING, MOD_TLS_VERSION ": FIPS mode requested, but " OPENSSL_VERSION_TEXT " not built with FIPS support");
-#endif /* OPENSSL_FIPS */
-  }
+# ifdef TLSEXT_TYPE_use_srtp
+    case TLSEXT_TYPE_use_srtp:
+        extension_name = "use SRTP";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_heartbeat
+    case TLSEXT_TYPE_heartbeat:
+        extension_name = "heartbeat";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_session_ticket
+    case TLSEXT_TYPE_session_ticket:
+        extension_name = "session ticket";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_renegotiate
+    case TLSEXT_TYPE_renegotiate:
+        extension_name = "renegotiation info";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_opaque_prf_input
+    case TLSEXT_TYPE_opaque_prf_input:
+        extension_name = "opaque PRF input";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_next_proto_neg
+    case TLSEXT_TYPE_next_proto_neg:
+        extension_name = "next protocol";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
+    case TLSEXT_TYPE_application_layer_protocol_negotiation:
+        extension_name = "application layer protocol";
+        break;
+# endif
+
+# ifdef TLSEXT_TYPE_padding
+    case TLSEXT_TYPE_padding:
+        extension_name = "TLS padding";
+        break;
+# endif
+
+    default:
+      break;
+  }
+
+  pr_trace_msg(trace_channel, 6,
+    "[tls.tlsext] TLS %s extension \"%s\" (ID %d, %d %s)",
+    client_server ? "server" : "client", extension_name, type, tlsext_datalen,
+    tlsext_datalen != 1 ? "bytes" : "byte");
+}
+#endif /* !OPENSSL_NO_TLSEXT */
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static OCSP_RESPONSE *ocsp_send_request(pool *p, BIO *bio, const char *host,
+    const char *uri, OCSP_REQUEST *req, unsigned int request_timeout) {
+  int fd, res;
+  OCSP_RESPONSE *resp = NULL;
+  OCSP_REQ_CTX *ctx = NULL;
+  const char *header_name, *header_value;
+
+  res = BIO_get_fd(bio, &fd);
+  if (res <= 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error obtaining OCSP responder socket fd: %s", tls_get_errors());
+    return NULL;
+  }
+
+  ctx = OCSP_sendreq_new(bio, (char *) uri, NULL, -1);
+  if (ctx == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "error allocating OCSP request context: %s", tls_get_errors());
+    return NULL;
+  }
+
+# if OPENSSL_VERSION_NUMBER >= 0x10000001L
+  header_name = "Host";
+  header_value = host;
+  res = OCSP_REQ_CTX_add1_header(ctx, header_name, header_value);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 4,
+      "error adding '%s: %s' header to OCSP request context: %s", header_name,
+      header_value, tls_get_errors());
+    OCSP_REQ_CTX_free(ctx);
+    return NULL;
+  }
+
+  header_name = "Accept";
+  header_value = "application/ocsp-response";
+  res = OCSP_REQ_CTX_add1_header(ctx, header_name, header_value);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 4,
+      "error adding '%s: %s' header to OCSP request context: %s", header_name,
+      header_value, tls_get_errors());
+    OCSP_REQ_CTX_free(ctx);
+    return NULL;
+  }
+
+  header_name = "User-Agent";
+  header_value = "proftpd+" MOD_TLS_VERSION;
+  res = OCSP_REQ_CTX_add1_header(ctx, header_name, header_value);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 4,
+      "error adding '%s: %s' header to OCSP request context: %s", header_name,
+      header_value, tls_get_errors());
+    OCSP_REQ_CTX_free(ctx);
+    return NULL;
+  }
+
+  /* If we are using nonces, then we need to explicitly request that no
+   * caches along the way interfere.
+   */
+  if (!(tls_stapling_opts & TLS_STAPLING_OPT_NO_NONCE)) {
+    header_name = "Pragma";
+    header_value = "no-cache";
+    res = OCSP_REQ_CTX_add1_header(ctx, header_name, header_value);
+    if (res != 1) {
+      pr_trace_msg(trace_channel, 4,
+        "error adding '%s: %s' header to OCSP request context: %s", header_name,
+        header_value, tls_get_errors());
+      OCSP_REQ_CTX_free(ctx);
+      return NULL;
+    }
+
+    header_name = "Cache-Control";
+    header_value = "no-cache, no-store";
+    res = OCSP_REQ_CTX_add1_header(ctx, header_name, header_value);
+    if (res != 1) {
+      pr_trace_msg(trace_channel, 4,
+        "error adding '%s: %s' header to OCSP request context: %s", header_name,
+        header_value, tls_get_errors());
+      OCSP_REQ_CTX_free(ctx);
+      return NULL;
+    }
+  }
+
+  /* Only add the request after we've added our headers. */
+  res = OCSP_REQ_CTX_set1_req(ctx, req);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 4,
+      "error adding OCSP request to context: %s", tls_get_errors());
+    OCSP_REQ_CTX_free(ctx);
+    return NULL;
+  }
+# endif /* OpenSSL-1.0.0 and later */
+
+  while (TRUE) {
+    fd_set fds;
+    struct timeval tv;
+
+    res = OCSP_sendreq_nbio(&resp, ctx);
+    if (res != -1) {
+      break;
+    }
+
+    if (request_timeout == 0) {
+      break;
+    }
+
+    FD_ZERO(&fds);
+    FD_SET(fd, &fds);
+    tv.tv_usec = 0;
+    tv.tv_sec = request_timeout;
+
+    if (BIO_should_read(bio)) {
+      res = select(fd + 1, (void *) &fds, NULL, NULL, &tv);
+
+    } else if (BIO_should_write(bio)) {
+      res = select(fd + 1, NULL, (void *) &fds, NULL, &tv);
+
+    } else {
+      pr_trace_msg(trace_channel, 3,
+        "unexpected retry condition when talking to OCSP responder '%s%s'",
+        host, uri);
+      res = -1;
+      break;
+    }
+
+    if (res == 0) {
+      pr_trace_msg(trace_channel, 3,
+         "timed out talking to OCSP responder '%s%s'", host, uri);
+      errno = ETIMEDOUT;
+      res = -1;
+      break;
+    }
+  }
+
+  OCSP_REQ_CTX_free(ctx);
+
+  if (res) {
+    if (tls_opts & TLS_OPT_ENABLE_DIAGS) {
+      BIO *diags_bio;
+
+      diags_bio = BIO_new(BIO_s_mem());
+      if (diags_bio != NULL) {
+        if (OCSP_RESPONSE_print(diags_bio, resp, 0) == 1) {
+          char *data = NULL;
+          long datalen = 0;
+
+          datalen = BIO_get_mem_data(diags_bio, &data);
+          if (data != NULL) {
+            data[datalen] = '\0';
+            tls_log("received OCSP response (%ld bytes):\n%s", datalen, data);
+          }
+        }
+      }
+
+      BIO_free(diags_bio);
+    }
+
+    return resp;
+  }
+
+  pr_trace_msg(trace_channel, 4,
+    "error obtaining OCSP response from responder: %s", tls_get_errors());
+  return NULL;
+}
+
+static X509 *ocsp_get_issuing_cert(pool *p, X509 *cert, SSL *ssl) {
+  int res;
+  X509 *issuer = NULL;
+  SSL_CTX *ctx;
+  X509_STORE *store;
+  X509_STORE_CTX *store_ctx;
+
+  if (ssl == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  ctx = SSL_get_SSL_CTX(ssl);
+  if (ctx == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "no SSL_CTX found for SSL session: %s", tls_get_errors());
+    errno = EINVAL;
+    return NULL;
+  }
+
+  store = SSL_CTX_get_cert_store(ctx);
+  if (store == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "no certificate store found for SSL_CTX: %s", tls_get_errors());
+    errno = EINVAL;
+    return NULL;
+  }
+
+  store_ctx = X509_STORE_CTX_new();
+  if (store_ctx == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "error allocating certificate store context: %s", tls_get_errors());
+    errno = ENOMEM;
+    return NULL;
+  }
+
+  res = X509_STORE_CTX_init(store_ctx, store, NULL, NULL);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 4,
+      "error initializing certificate store context: %s", tls_get_errors());
+    X509_STORE_CTX_free(store_ctx);
+    errno = ENOMEM;
+    return NULL;
+  }
+
+  res = X509_STORE_CTX_get1_issuer(&issuer, store_ctx, cert);
+  if (res == -1) {
+    pr_trace_msg(trace_channel, 4,
+      "error finding issuing certificate: %s", tls_get_errors());
+    X509_STORE_CTX_free(store_ctx);
+
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 4,
+      "no issuing certificate found: %s", tls_get_errors());
+    X509_STORE_CTX_free(store_ctx);
+
+    errno = ENOENT;
+    return NULL;
+  }
+
+  X509_STORE_CTX_free(store_ctx);
+  pr_trace_msg(trace_channel, 14, "found issuer %p for certificate", issuer);
+  return issuer;
+}
+
+static OCSP_REQUEST *ocsp_get_request(pool *p, X509 *cert, X509 *issuer) {
+  OCSP_REQUEST *req = NULL;
+  OCSP_CERTID *cert_id = NULL;
+
+  req = OCSP_REQUEST_new();
+  if (req == NULL) {
+    pr_trace_msg(trace_channel, 4, "error allocating OCSP request: %s",
+      tls_get_errors());
+    return NULL;
+  }
+
+  cert_id = OCSP_cert_to_id(NULL, cert, issuer);
+  if (cert_id == NULL) {
+    pr_trace_msg(trace_channel, 4, "error obtaining ID for cert: %s",
+      tls_get_errors());
+    OCSP_REQUEST_free(req);
+    return NULL;
+  }
+
+  if (OCSP_request_add0_id(req, cert_id) == NULL) {
+    pr_trace_msg(trace_channel, 4, "error adding ID to OCSP request: %s",
+      tls_get_errors());
+    OCSP_CERTID_free(cert_id);
+    OCSP_REQUEST_free(req);
+    return NULL;
+  }
+
+  if (!(tls_stapling_opts & TLS_STAPLING_OPT_NO_NONCE)) {
+    OCSP_request_add1_nonce(req, NULL, -1);
+  }
+
+  if (tls_opts & TLS_OPT_ENABLE_DIAGS) {
+    BIO *diags_bio;
+
+    diags_bio = BIO_new(BIO_s_mem());
+    if (diags_bio != NULL) {
+      if (OCSP_REQUEST_print(diags_bio, req, 0) == 1) {
+        char *data = NULL;
+        long datalen = 0;
+
+        datalen = BIO_get_mem_data(diags_bio, &data);
+        if (data != NULL) {
+          data[datalen] = '\0';
+          tls_log("sending OCSP request (%ld bytes):\n%s", datalen, data);
+        }
+      }
+    }
+
+    BIO_free(diags_bio);
+  }
+
+  return req;
+}
+
+static int ocsp_check_cert_status(pool *p, X509 *cert, X509 *issuer,
+    OCSP_BASICRESP *basic_resp, int *ocsp_status, int *ocsp_reason) {
+  int res, status, reason;
+  OCSP_CERTID *cert_id = NULL;
+  ASN1_GENERALIZEDTIME *this_update = NULL, *next_update = NULL,
+    *revoked_at = NULL;
+
+  cert_id = OCSP_cert_to_id(NULL, cert, issuer);
+  if (cert_id == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error obtaining cert ID from basic OCSP response: %s", tls_get_errors());
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = OCSP_resp_find_status(basic_resp, cert_id, &status, &reason,
+    &revoked_at, &this_update, &next_update);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "error locating certificate status in OCSP response: %s",
+      tls_get_errors());
+
+    OCSP_CERTID_free(cert_id);
+    errno = ENOENT;
+    return -1;
+  }
+
+  OCSP_CERTID_free(cert_id);
+
+  res = OCSP_check_validity(this_update, next_update,
+    TLS_OCSP_RESP_MAX_AGE_SECS, -1);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "failed time-based validity check of OCSP response: %s",
+      tls_get_errors());
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Valid or not, we still want to cache this response, AND communicate
+   * the certificate status, as is, backed to the client via the stapled
+   * response.
+   */
+
+  pr_trace_msg(trace_channel, 8,
+    "found certificate status '%s' in OCSP response",
+    OCSP_cert_status_str(status));
+  if (status == V_OCSP_CERTSTATUS_REVOKED) {
+    if (reason != -1) {
+      pr_trace_msg(trace_channel, 8, "revocation reason: %s",
+        OCSP_crl_reason_str(reason));
+    }
+  }
+
+  if (ocsp_status != NULL) {
+    *ocsp_status = status;
+  }
+
+  if (ocsp_reason != NULL) {
+    *ocsp_reason = reason;
+  }
+
+  return 0;
+}
+
+static int ocsp_check_response(pool *p, X509 *cert, X509 *issuer, SSL *ssl,
+    OCSP_REQUEST *req, OCSP_RESPONSE *resp) {
+  int flags = 0, res = 0, resp_status;
+  OCSP_BASICRESP *basic_resp = NULL;
+  SSL_CTX *ctx = NULL;
+  X509_STORE *store = NULL;
+  STACK_OF(X509) *chain = NULL;
+
+  ctx = SSL_get_SSL_CTX(ssl);
+  if (ctx == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "no SSL_CTX found for SSL session: %s", tls_get_errors());
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  store = SSL_CTX_get_cert_store(ctx);
+  if (store == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "no certificate store found for SSL_CTX: %s", tls_get_errors());
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  basic_resp = OCSP_response_get1_basic(resp);
+  if (basic_resp == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error getting basic OCSP response: %s", tls_get_errors());
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (!(tls_stapling_opts & TLS_STAPLING_OPT_NO_NONCE)) {
+    res = OCSP_check_nonce(req, basic_resp);
+    if (res < 0) {
+      pr_trace_msg(trace_channel, 1,
+        "WARNING: OCSP response is missing request nonce");
+
+    } else if (res == 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error verifying OCSP response nonce: %s", tls_get_errors());
+
+      OCSP_BASICRESP_free(basic_resp);
+      errno = EINVAL;
+      return -1;
+    }
+  }
+
+  chain = sk_X509_new_null();
+  if (chain != NULL) {
+    STACK_OF(X509) *extra_certs = NULL;
+
+    sk_X509_push(chain, issuer);
+
+# if OPENSSL_VERSION_NUMBER >= 0x10001000L
+    SSL_CTX_get_extra_chain_certs(ctx, &extra_certs);
+# else
+    extra_certs = ctx->extra_certs;
+# endif
+
+    if (extra_certs != NULL) {
+      register int i;
+
+      for (i = 0; i < sk_X509_num(extra_certs); i++) {
+        sk_X509_push(chain, sk_X509_value(extra_certs, i));
+      }
+    }
+  }
+
+  flags = OCSP_TRUSTOTHER;
+  if (tls_stapling_opts & TLS_STAPLING_OPT_NO_VERIFY) {
+    flags = OCSP_NOVERIFY;
+  }
+
+  res = OCSP_basic_verify(basic_resp, chain, store, flags);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 3,
+      "error verifying basic OCSP response data: %s", tls_get_errors());
+
+    OCSP_BASICRESP_free(basic_resp);
+
+    if (chain != NULL) {
+      sk_X509_free(chain);
+    }
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (chain != NULL) {
+    sk_X509_free(chain);
+  }
+
+  /* Now that we have verified the response, we can check the response status.
+   * If we only looked at the status first, then a malicious responder
+   * could be tricking us, e.g.:
+   *
+   *  http://www.thoughtcrime.org/papers/ocsp-attack.pdf
+   */
+
+  resp_status = OCSP_response_status(resp);
+  if (resp_status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
+    pr_trace_msg(trace_channel, 3,
+      "OCSP response not successful: %s (%d)",
+      OCSP_response_status_str(resp_status), resp_status);
+
+    OCSP_BASICRESP_free(basic_resp);
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = ocsp_check_cert_status(p, cert, issuer, basic_resp, NULL, NULL);
+  OCSP_BASICRESP_free(basic_resp);
+
+  return res;
+}
+
+static int ocsp_connect(pool *p, BIO *bio, unsigned int request_timeout) {
+  int fd, res;
+
+  if (request_timeout > 0) {
+    BIO_set_nbio(bio, 1);
+  }
+
+  res = BIO_do_connect(bio);
+  if (res <= 0 &&
+      (request_timeout == 0 || !BIO_should_retry(bio))) {
+    pr_trace_msg(trace_channel, 4,
+      "error connecting to OCSP responder: %s", tls_get_errors());
+    return -1;
+  }
+
+  if (BIO_get_fd(bio, &fd) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error obtaining OCSP responder socket fd: %s", tls_get_errors());
+    return -1;
+  }
+
+  if (request_timeout > 0 &&
+      res <= 0) {
+    struct timeval tv;
+    fd_set fds;
+
+    FD_ZERO(&fds);
+    FD_SET(fd, &fds);
+    tv.tv_usec = 0;
+    tv.tv_sec = request_timeout;
+    res = select(fd + 1, NULL, (void *) &fds, NULL, &tv);
+    if (res == 0) {
+      errno = ETIMEDOUT;
+      return -1;
+    }
+  }
+
+  return 0;
+}
+
+static OCSP_RESPONSE *ocsp_request_response(pool *p, X509 *cert, SSL *ssl,
+    const char *url, unsigned int request_timeout) {
+  BIO *bio;
+  SSL_CTX *ctx = NULL;
+  X509 *issuer = NULL;
+  char *host = NULL, *port = NULL, *uri = NULL;
+  int res, use_ssl = FALSE;
+  OCSP_REQUEST *req = NULL;
+  OCSP_RESPONSE *resp = NULL;
+
+  issuer = ocsp_get_issuing_cert(p, cert, ssl);
+  if (issuer == NULL) {
+    return NULL;
+  }
+
+  /* Current OpenSSL implementation of OCSP_parse_url() guarantees that
+   * host, port, and uri will never be NULL.  Nice.
+   */
+  res = OCSP_parse_url((char *) url, &host, &port, &uri, &use_ssl);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 4, "error parsing OCSP URL '%s': %s", url,
+      tls_get_errors());
+    return NULL;
+  }
+
+  bio = BIO_new_connect(host);
+  if (bio == NULL) {
+    pr_trace_msg(trace_channel, 4, "error allocating connect BIO: %s",
+      tls_get_errors());
+
+    OPENSSL_free(host);
+    OPENSSL_free(port);
+    OPENSSL_free(uri);
+    return NULL;
+  }
+
+  BIO_set_conn_port(bio, port);
+
+  if (use_ssl) {
+    BIO *ssl_bio;
+
+    ctx = SSL_CTX_new(SSLv23_client_method());
+    if (ctx == NULL) {
+      pr_trace_msg(trace_channel, 4, "error allocating SSL context: %s",
+        tls_get_errors());
+
+      BIO_free_all(bio);
+      OPENSSL_free(host);
+      OPENSSL_free(port);
+      OPENSSL_free(uri);
+      return NULL;
+    }
+
+    SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
+    ssl_bio = BIO_new_ssl(ctx, 1);
+    bio = BIO_push(ssl_bio, bio);
+  }
+
+  res = ocsp_connect(p, bio, request_timeout);
+  if (res < 0) {
+    BIO_free_all(bio);
+    OPENSSL_free(host);
+    OPENSSL_free(port);
+    OPENSSL_free(uri);
+    return NULL;
+  }
+
+  req = ocsp_get_request(p, cert, issuer);
+  if (req == NULL) {
+    BIO_free_all(bio);
+    OPENSSL_free(host);
+    OPENSSL_free(port);
+    OPENSSL_free(uri);
+    return NULL;
+  }
+
+  resp = ocsp_send_request(p, bio, host, uri, req, request_timeout);
+
+  OPENSSL_free(host);
+  OPENSSL_free(port);
+  OPENSSL_free(uri);
+
+  if (ctx != NULL) {
+    SSL_CTX_free(ctx);
+  }
+
+  if (bio != NULL) {
+    BIO_free_all(bio);
+  }
+
+  if (resp == NULL) {
+    return NULL;
+  }
+
+  if (ocsp_check_response(p, cert, issuer, ssl, req, resp) < 0) {
+    if (errno != ENOSYS) {
+      OCSP_REQUEST_free(req);
+      OCSP_RESPONSE_free(resp);
+      errno = EINVAL;
+      return NULL;
+    }
+  }
+
+  OCSP_REQUEST_free(req);
+  return resp;
+}
+
+static int ocsp_expired_cached_response(pool *p, OCSP_RESPONSE *resp,
+    time_t age) {
+  int res = -1, status;
+  time_t expired = 0;
+
+  status = OCSP_response_status(resp);
+
+  /* If we received a SUCCESSFUL response from the OCSP responder, then
+   * we expire the response after 1 hour (hardcoded).  Otherwise, we expire
+   * the cached entry after 5 minutes (hardcoded).
+   */
+
+  if (status == OCSP_RESPONSE_STATUS_SUCCESSFUL) {
+    if (age > 3600) {
+      expired = age - 3600;
+      res = 0;
+    }
+
+  } else {
+    if (age > 300) {
+      expired = age - 300;
+      res = 0;
+    }
+  }
+
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 8,
+      "cached %s OCSP response expired %lu %s ago",
+      OCSP_response_status_str(status), (unsigned long) expired,
+      expired != 1 ? "secs" : "sec");
+  }
+
+  return res;
+}
+
+static OCSP_RESPONSE *ocsp_get_cached_response(pool *p,
+    const char *fingerprint) {
+  OCSP_RESPONSE *resp = NULL;
+  time_t resp_age = 0;
+
+  if (tls_ocsp_cache == NULL) {
+    errno = ENOSYS;
+    return NULL;
+  }
+
+  resp = (tls_ocsp_cache->get)(tls_ocsp_cache, fingerprint, &resp_age);
+  if (resp != NULL) {
+    time_t now = 0, age = 0;
+    int res;
+
+    time(&now);
+    age = now - resp_age;
+    pr_trace_msg(trace_channel, 9,
+      "found cached OCSP response for fingerprint '%s': %lu %s old",
+      fingerprint, (unsigned long) age, age != 1 ? "secs" : "sec");
+
+    res = ocsp_expired_cached_response(p, resp, age);
+    if (res == 0) {
+      /* Cached response has expired; request a new one. */
+      res = (tls_ocsp_cache->delete)(tls_ocsp_cache, fingerprint);
+      if (res < 0) {
+        pr_trace_msg(trace_channel, 3,
+          "error deleting OCSP response from '%s' cache for "
+          "fingerprint '%s': %s", tls_ocsp_cache->cache_name, fingerprint,
+          strerror(errno));
+      }
+
+      OCSP_RESPONSE_free(resp);
+      resp = NULL;
+    }
+
+  } else {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error retrieving OCSP response from '%s' cache for "
+      "fingerprint '%s': %s", tls_ocsp_cache->cache_name, fingerprint,
+      strerror(xerrno));
+
+    errno = xerrno;
+  }
+
+  return resp;
+}
+
+static int ocsp_add_cached_response(pool *p, const char *fingerprint,
+    OCSP_RESPONSE *resp) {
+  int res;
+  time_t resp_age = 0;
+
+  if (fingerprint == NULL ||
+      tls_ocsp_cache == NULL) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  /* Cache this fake response, so that we don't have to keep redoing this
+   * for a short amount of time (e.g. 5 minutes).
+   */
+  time(&resp_age);
+  res = (tls_ocsp_cache->add)(tls_ocsp_cache, fingerprint, resp, resp_age);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error adding OCSP response to '%s' cache for fingerprint '%s': %s",
+      tls_ocsp_cache->cache_name, fingerprint, strerror(xerrno));
+
+    errno = xerrno;
+
+  } else {
+    pr_trace_msg(trace_channel, 15,
+      "added OCSP response to '%s' cache for fingerprint '%s'",
+      tls_ocsp_cache->cache_name, fingerprint);
+  }
+
+  return res;
+}
+
+static OCSP_RESPONSE *ocsp_get_response(pool *p, SSL *ssl) {
+  X509 *cert;
+  const char *fingerprint = NULL;
+  OCSP_RESPONSE *resp = NULL, *cached_resp = NULL;
+
+  /* We need to find a cached OCSP response for the server cert in question,
+   * thus we need to find out which server cert is used for this session.
+   */
+  cert = SSL_get_certificate(ssl);
+  if (cert != NULL) {
+    fingerprint = tls_get_fingerprint(p, cert);
+    if (fingerprint != NULL) {
+
+      pr_trace_msg(trace_channel, 3,
+        "using fingerprint '%s' for server cert", fingerprint);
+      if (tls_ocsp_cache != NULL) {
+        OCSP_RESPONSE *fresh_resp = NULL;
+
+        cached_resp = ocsp_get_cached_response(p, fingerprint);
+        if (cached_resp != NULL) {
+          if (tls_opts & TLS_OPT_ENABLE_DIAGS) {
+            BIO *diags_bio;
+
+            diags_bio = BIO_new(BIO_s_mem());
+            if (diags_bio != NULL) {
+              if (OCSP_RESPONSE_print(diags_bio, cached_resp, 0) == 1) {
+                char *data = NULL;
+                long datalen = 0;
+
+                datalen = BIO_get_mem_data(diags_bio, &data);
+                if (data != NULL) {
+                  data[datalen] = '\0';
+                  tls_log("cached OCSP response (%ld bytes):\n%s", datalen,
+                    data);
+                }
+              }
+            }
+
+            BIO_free(diags_bio);
+          }
+
+          resp = cached_resp;
+        }
+
+        if (cached_resp == NULL) {
+          int xerrno = errno;
+
+          if (xerrno == ENOENT) {
+            const char *ocsp_url;
+
+            if (tls_stapling_responder == NULL) {
+              ocsp_url = ocsp_get_responder_url(p, cert);
+              if (ocsp_url != NULL) {
+                pr_trace_msg(trace_channel, 8,
+                  "found OCSP responder URL '%s' in certificate "
+                  "(fingerprint '%s')", ocsp_url, fingerprint);
+              }
+
+            } else {
+              ocsp_url = tls_stapling_responder;
+              pr_trace_msg(trace_channel, 8,
+                "using configured OCSP responder URL '%s'", ocsp_url);
+            }
+
+            if (ocsp_url != NULL) {
+              fresh_resp = ocsp_request_response(p, cert, ssl, ocsp_url,
+                tls_stapling_timeout);
+              if (fresh_resp != NULL) {
+                resp = fresh_resp;
+              }
+
+            } else {
+              pr_trace_msg(trace_channel, 5,
+                "no OCSP responder URL found in certificate (fingerprint '%s')",
+                fingerprint);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  if (resp == NULL) {
+    pr_trace_msg(trace_channel, 5, "returning fake tryLater OCSP response");
+
+    /* If we have not found an OCSP response, then fall back to using
+     * a fake "tryLater" response.
+     */
+    resp = OCSP_response_create(OCSP_RESPONSE_STATUS_TRYLATER, NULL);
+    if (resp == NULL) {
+      pr_trace_msg(trace_channel, 1,
+        "error allocating fake 'tryLater' OCSP response: %s", tls_get_errors());
+      return NULL;
+    }
+  }
+
+  /* If this response is not the one we just pulled from the cache, then
+   * add it.
+   */
+  if (resp != cached_resp) {
+    if (ocsp_add_cached_response(p, fingerprint, resp) < 0) {
+      if (errno != ENOSYS) {
+        pr_trace_msg(trace_channel, 3,
+          "error caching OCSP response: %s", strerror(errno));
+      }
+    }
+  }
+
+  return resp;
+}
+
+static int tls_ocsp_cb(SSL *ssl, void *user_data) {
+  OCSP_RESPONSE *resp;
+  int resp_derlen;
+  unsigned char *resp_der = NULL;
+  pool *ocsp_pool;
+
+  if (tls_stapling == FALSE) {
+    /* OCSP stapling disabled; do nothing. */
+    return SSL_TLSEXT_ERR_NOACK;
+  }
+
+  ocsp_pool = make_sub_pool(session.pool);
+  pr_pool_tag(ocsp_pool, "Session OCSP response pool");
+
+  resp = ocsp_get_response(ocsp_pool, ssl);
+  resp_derlen = i2d_OCSP_RESPONSE(resp, &resp_der);
+  destroy_pool(ocsp_pool);
+
+  /* Success or failure, we're done with the OCSP response. */
+  OCSP_RESPONSE_free(resp);
+
+  if (resp_derlen <= 0) {
+    tls_log("error determining OCSP response length: %s", tls_get_errors());
+    return SSL_TLSEXT_ERR_NOACK;
+  }
+
+  SSL_set_tlsext_status_ocsp_resp(ssl, resp_der, resp_derlen);
+  return SSL_TLSEXT_ERR_OK;
+}
+#endif /* PR_USE_OPENSSL_OCSP */
+
+#if defined(TLS_USE_SESSION_TICKETS)
+static int tls_ticket_key_cmp(xasetmember_t *a, xasetmember_t *b) {
+  struct tls_ticket_key *k1, *k2;
+
+  k1 = (struct tls_ticket_key *) a;
+  k2 = (struct tls_ticket_key *) b;
+
+  if (k1->created == k2->created) {
+    return 0;
+  }
+
+  if (k1->created < k2->created) {
+    return -1;
+  }
+
+  return 1;
+}
+
+static struct tls_ticket_key *create_ticket_key(void) {
+  struct tls_ticket_key *k;
+  void *page_ptr = NULL;
+  size_t pagesz;
+  char *ptr;
+# ifdef HAVE_MLOCK
+  int res, xerrno = 0;
+# endif /* HAVE_MLOCK */
+
+  pagesz = sizeof(struct tls_ticket_key);
+  ptr = tls_get_page(pagesz, &page_ptr);
+  if (ptr == NULL) {
+    if (page_ptr != NULL) {
+      free(page_ptr);
+    }
+    return NULL;
+  }
+
+  k = (void *) ptr;
+  time(&(k->created));
+
+  if (RAND_bytes(k->key_name, 16) != 1) {
+    pr_log_debug(DEBUG1, MOD_TLS_VERSION
+      ": error generating random bytes: %s", tls_get_errors());
+    free(page_ptr);
+    errno = EPERM;
+    return NULL;
+  }
+
+  if (RAND_bytes(k->cipher_key, 32) != 1) {
+    pr_log_debug(DEBUG1, MOD_TLS_VERSION
+      ": error generating random bytes: %s", tls_get_errors());
+    free(page_ptr);
+    errno = EPERM;
+    return NULL;
+  }
+
+  if (RAND_bytes(k->hmac_key, 32) != 1) {
+    pr_log_debug(DEBUG1, MOD_TLS_VERSION
+      ": error generating random bytes: %s", tls_get_errors());
+    free(page_ptr);
+    errno = EPERM;
+    return NULL;
+  }
+
+# ifdef HAVE_MLOCK
+  PRIVS_ROOT
+  res = mlock(page_ptr, pagesz);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (res < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_VERSION
+      ": error locking session ticket key into memory: %s", strerror(xerrno));
+    free(page_ptr);
+    errno = xerrno;
+    return NULL;
+  }
+# endif /* HAVE_MLOCK */
+
+  k->page_ptr = page_ptr;
+  k->pagesz = pagesz;
+  return k;
+}
+
+static void destroy_ticket_key(struct tls_ticket_key *k) {
+  void *page_ptr;
+  size_t pagesz;
+# ifdef HAVE_MLOCK
+  int res, xerrno = 0;
+# endif /* HAVE_MLOCK */
+
+  if (k == NULL) {
+    return;
+  }
+
+  page_ptr = k->page_ptr;
+  pagesz = k->pagesz;
+
+  pr_memscrub(k->page_ptr, k->pagesz);
+
+# ifdef HAVE_MLOCK
+  PRIVS_ROOT
+  res = munlock(page_ptr, pagesz);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (res < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_VERSION
+      ": error unlocking session ticket key memory: %s", strerror(xerrno));
+  }
+# endif /* HAVE_MLOCK */
+
+  free(page_ptr);
+}
+
+static int remove_expired_ticket_keys(void) {
+  struct tls_ticket_key *k = NULL;
+  int expired_count = 0;
+  time_t now;
+
+  if (tls_ticket_key_curr_count < 2) {
+    /* Always keep at least one key. */
+    return 0;
+  }
+
+  time(&now);
+
+  for (k = (struct tls_ticket_key *) tls_ticket_keys->xas_list;
+       k;
+       k = k->next) {
+    time_t key_age;
+
+    key_age = now - k->created;
+    if (key_age > tls_ticket_key_max_age) {
+      if (xaset_remove(tls_ticket_keys, (xasetmember_t *) k) == 0) {
+        expired_count++;
+        tls_ticket_key_curr_count--;
+      }
+    }
+  }
+
+  return expired_count;
+}
+
+static int remove_oldest_ticket_key(void) {
+  struct tls_ticket_key *k = NULL;
+  int res;
+
+  if (tls_ticket_key_curr_count < 2) {
+    /* Always keep at least one key. */
+    return 0;
+  }
+
+  /* Remove the last ticket key in the set. */
+  for (k = (struct tls_ticket_key *) tls_ticket_keys->xas_list;
+       k && k->next != NULL;
+       k = k->next);
+
+  res = xaset_remove(tls_ticket_keys, (xasetmember_t *) k);
+  if (res == 0) {
+    tls_ticket_key_curr_count--;
+  }
+
+  return res;
+}
+
+static int add_ticket_key(struct tls_ticket_key *k) {
+  int res;
+
+  res = remove_expired_ticket_keys();
+  if (res > 0) {
+    pr_trace_msg(trace_channel, 9, "removed %d expired %s", res,
+      res != 1 ? "keys" : "key");
+  }
+
+  if (tls_ticket_key_curr_count == tls_ticket_key_max_count) {
+    res = remove_oldest_ticket_key();
+    if (res < 0) {
+      return -1;
+    }
+  }
+
+  res = xaset_insert_sort(tls_ticket_keys, (xasetmember_t *) k, FALSE);
+  if (res == 0) {
+    tls_ticket_key_curr_count++;
+  }
+
+  return res;
+}
+
+/* Note: This lookup routine is where we might look in external storage,
+ * e.g. Redis/memcache, for clustered/shared pool of ticket keys generated by
+ * other servers.
+ */
+static struct tls_ticket_key *get_ticket_key(unsigned char *key_name,
+    size_t key_namelen) {
+  struct tls_ticket_key *k = NULL;
+
+  if (tls_ticket_keys == NULL) {
+    return NULL;
+  }
+
+  for (k = (struct tls_ticket_key *) tls_ticket_keys->xas_list;
+       k;
+       k = k->next) {
+    if (memcmp(key_name, k->key_name, key_namelen) == 0) {
+      break;
+    }
+  }
+
+  return k;
+}
+
+static int new_ticket_key_timer_cb(CALLBACK_FRAME) {
+  struct tls_ticket_key *k;
+
+  pr_log_debug(DEBUG9, MOD_TLS_VERSION
+    ": generating new TLS session ticket key");
+
+  k = create_ticket_key();
+  if (k == NULL) {
+    pr_log_debug(DEBUG0, MOD_TLS_VERSION
+      ": unable to generate new session ticket key: %s", strerror(errno));
+
+  } else {
+    add_ticket_key(k);
+  }
+
+  /* Always restart this timer. */
+  return 1;
+}
+
+/* Remember that mlock(2) locks are not inherited across forks, thus
+ * we want to renew those locks for session processes.
+ */
+static void lock_ticket_keys(void) {
+# ifdef HAVE_MLOCK
+  struct tls_ticket_key *k;
+
+  if (tls_ticket_keys == NULL) {
+    return;
+  }
+
+  for (k = (struct tls_ticket_key *) tls_ticket_keys->xas_list;
+       k;
+       k = k->next) {
+    if (k->locked == FALSE) {
+      int res, xerrno = 0;
+
+      PRIVS_ROOT
+      res = mlock(k->page_ptr, k->pagesz);
+      xerrno = errno;
+      PRIVS_RELINQUISH
+
+      if (res < 0) {
+        pr_log_debug(DEBUG1, MOD_TLS_VERSION
+          ": error locking session ticket key into memory: %s",
+          strerror(xerrno));
+
+      } else {
+        k->locked = TRUE;
+      }
+    }
+  }
+# endif /* HAVE_MLOCK */
+}
+
+static void scrub_ticket_keys(void) {
+  struct tls_ticket_key *k, *next_k;
+
+  if (tls_ticket_keys == NULL) {
+    return;
+  }
+
+  for (k = (struct tls_ticket_key *) tls_ticket_keys->xas_list; k; k = next_k) {
+    next_k = k->next;
+    destroy_ticket_key(k);
+  }
+
+  tls_ticket_keys = NULL;
+}
+
+static int tls_ticket_key_cb(SSL *ssl, unsigned char *key_name,
+    unsigned char *iv, EVP_CIPHER_CTX *cipher_ctx, HMAC_CTX *hmac_ctx,
+    int mode) {
+  register unsigned int i;
+  struct tls_ticket_key *k;
+  char key_name_str[33];
+
+  /* Note: should we have a list of ciphers from which we randomly choose,
+   * when creating a key?  I.e. should the keys themselves hold references
+   * to their ciphers, digests?
+   */
+  const EVP_CIPHER *cipher = EVP_aes_256_cbc();
+# ifdef OPENSSL_NO_SHA256
+  const EVP_MD *md = EVP_sha1();
+# else
+  const EVP_MD *md = EVP_sha256();
+# endif
+
+  if (mode == 1) {
+    int ticket_key_len, sess_key_len;
+
+    if (tls_ticket_keys == NULL) {
+      return -1;
+    }
+
+    /* Creating a new session ticket.  Always use the first key in the set. */
+    k = (struct tls_ticket_key *) tls_ticket_keys->xas_list;
+
+    for (i = 0; i < 16; i++) {
+      sprintf((char *) &(key_name_str[i*2]), "%02x", k->key_name[i]);
+    }
+    key_name_str[sizeof(key_name_str)-1] = '\0';
+
+    pr_trace_msg(trace_channel, 3,
+      "TLS session ticket: encrypting using key '%s' for %s session",
+      key_name_str, SSL_session_reused(ssl) ? "reused" : "new");
+
+    /* Warn loudly if the ticket key we are using is not as strong (based on
+     * cipher key length) as the one negotiated for the session.
+     */
+    ticket_key_len = EVP_CIPHER_key_length(cipher) * 8;
+    sess_key_len = SSL_get_cipher_bits(ssl, NULL);
+    if (ticket_key_len < sess_key_len) {
+      pr_log_pri(PR_LOG_INFO, MOD_TLS_VERSION
+        ": WARNING: TLS session tickets encrypted with weaker key than "
+        "session: ticket key = %s (%d bytes), session key = %s (%d bytes)",
+        OBJ_nid2sn(EVP_CIPHER_type(cipher)), ticket_key_len,
+        SSL_get_cipher_name(ssl), sess_key_len);
+    }
+
+    if (RAND_bytes(iv, 16) != 1) {
+      pr_trace_msg(trace_channel, 3,
+        "unable to initialize session ticket key IV: %s", tls_get_errors());
+      return -1;
+    }
+
+    if (EVP_EncryptInit_ex(cipher_ctx, cipher, NULL, k->cipher_key, iv) != 1) {
+      pr_trace_msg(trace_channel, 3,
+        "unable to initialize session ticket key cipher: %s", tls_get_errors());
+      return -1;
+    }
+
+# if OPENSSL_VERSION_NUMBER >= 0x10000001L
+    if (HMAC_Init_ex(hmac_ctx, k->hmac_key, 32, md, NULL) != 1) {
+      pr_trace_msg(trace_channel, 3,
+        "unable to initialize session ticket key HMAC: %s", tls_get_errors());
+      return -1;
+    }
+# else
+    HMAC_Init_ex(hmac_ctx, k->hmac_key, 32, md, NULL);
+# endif /* OpenSSL-1.0.0 and later */
+
+    memcpy(key_name, k->key_name, 16);
+    return 0;
+  }
+
+  if (mode == 0) {
+    struct tls_ticket_key *newest_key;
+    time_t key_age, now;
+
+    for (i = 0; i < 16; i++) {
+      sprintf((char *) &(key_name_str[i*2]), "%02x", key_name[i]);
+    }
+    key_name_str[sizeof(key_name_str)-1] = '\0';
+
+    k = get_ticket_key(key_name, 16);
+    if (k == NULL) {
+      /* No matching key found. */
+      pr_trace_msg(trace_channel, 3,
+        "TLS session ticket: decrypting ticket using key '%s': key not found",
+        key_name_str);
+      return 0;
+    }
+
+    pr_trace_msg(trace_channel, 3,
+      "TLS session ticket: decrypting ticket using key '%s'", key_name_str);
+
+# if OPENSSL_VERSION_NUMBER >= 0x10000001L
+    if (HMAC_Init_ex(hmac_ctx, k->hmac_key, 32, md, NULL) != 1) {
+      pr_trace_msg(trace_channel, 3,
+        "unable to initialize session ticket key HMAC: %s", tls_get_errors());
+      return 0;
+    }
+# else
+    HMAC_Init_ex(hmac_ctx, k->hmac_key, 32, md, NULL);
+# endif /* OpenSSL-1.0.0 and later */
+
+    if (EVP_DecryptInit_ex(cipher_ctx, cipher, NULL, k->cipher_key, iv) != 1) {
+      pr_trace_msg(trace_channel, 3,
+        "unable to initialize session ticket key cipher: %s", tls_get_errors());
+      return 0;
+    }
+
+    /* If the key we found is older than the newest key, tell the client to
+     * get a new ticket.  This helps to reduce the window of time a given
+     * ticket key is used.
+     */
+    time(&now);
+    key_age = now - k->created;
+
+    newest_key = (struct tls_ticket_key *) tls_ticket_keys->xas_list;
+    if (k != newest_key) {
+      time_t newest_age;
+
+      newest_age = now - newest_key->created;
+
+      pr_trace_msg(trace_channel, 3,
+        "key '%s' age (%lu %s) older than newest key (%lu %s), requesting "
+        "ticket renewal", key_name_str, (unsigned long) key_age,
+        key_age != 1 ? "secs" : "sec", (unsigned long) newest_age,
+        newest_age != 1 ? "secs" : "sec");
+      return 2;
+    }
+
+    return 1;
+  }
+
+  pr_trace_msg(trace_channel, 3, "TLS session ticket: unknown mode (%d)", mode);
+  return -1;
+}
+#endif /* TLS_USE_SESSION_TICKETS */
+
+#if defined(PR_USE_OPENSSL_ECC) && OPENSSL_VERSION_NUMBER < 0x10100000L
+static EC_KEY *tls_ecdh_cb(SSL *ssl, int is_export, int keylen) {
+  static EC_KEY *ecdh = NULL;
+  static int init = 0;
+
+  if (init == 0) {
+    ecdh = EC_KEY_new();
+
+    if (ecdh != NULL) {
+      EC_KEY_set_group(ecdh,
+        EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
+    }
+
+    init = 1;
+  }
+
+  return ecdh;
+}
+#endif /* PR_USE_OPENSSL_ECC and before OpenSSL-1.1.x */
+
+#if defined(PR_USE_OPENSSL_ALPN)
+static int tls_alpn_select_cb(SSL *ssl,
+    const unsigned char **selected_proto, unsigned char *selected_protolen, 
+    const unsigned char *advertised_proto, unsigned int advertised_protolen,
+    void *user_data) {
+  register unsigned int i;
+  struct tls_next_proto *next_proto;
+  char *selected_alpn;
+
+  pr_trace_msg(trace_channel, 9, "%s",
+    "ALPN protocols advertised by client:");
+  for (i = 0; i < advertised_protolen; i++) {
+    pr_trace_msg(trace_channel, 9,
+      " %*s", advertised_proto[i], &(advertised_proto[i+1])); 
+    i += advertised_proto[i] + 1;
+  }
+
+  next_proto = user_data;
+
+  if (SSL_select_next_proto(
+      (unsigned char **) selected_proto, selected_protolen,
+      next_proto->encoded_proto, next_proto->encoded_protolen,
+      advertised_proto, advertised_protolen) != OPENSSL_NPN_NEGOTIATED) {
+    pr_trace_msg(trace_channel, 9,
+      "no common ALPN protocols found (no '%s' in ALPN protocols)",
+      next_proto->proto);
+    return SSL_TLSEXT_ERR_NOACK;
+  }
+
+  selected_alpn = pstrndup(session.pool, (char *) *selected_proto,
+    *selected_protolen);
+  pr_trace_msg(trace_channel, 9,
+    "selected ALPN protocol '%s'", selected_alpn);
+  return SSL_TLSEXT_ERR_OK;
+}
+#endif /* ALPN */
+
+#if defined(PR_USE_OPENSSL_NPN)
+static int tls_npn_advertised_cb(SSL *ssl,
+    const unsigned char **advertise_proto, unsigned int *advertise_protolen,
+    void *user_data) {
+  struct tls_next_proto *next_proto;
+
+  next_proto = user_data;
+
+  pr_trace_msg(trace_channel, 9,
+    "advertising NPN protocol '%s'", next_proto->proto);
+  *advertise_proto = next_proto->encoded_proto;
+  *advertise_protolen = next_proto->encoded_protolen;
+
+  return SSL_TLSEXT_ERR_OK;
+}
+#endif /* NPN */
+
+/* Post 0.9.7a, RSA blinding is turned on by default, so there is no need to
+ * do this manually.
+ */
+#if OPENSSL_VERSION_NUMBER < 0x0090702fL
+static void tls_blinding_on(SSL *ssl) {
+  EVP_PKEY *pkey = NULL;
+  RSA *rsa = NULL;
+
+  /* RSA keys are subject to timing attacks.  To attempt to make such
+   * attacks harder, use RSA blinding.
+   */
+
+  pkey = SSL_get_privatekey(ssl);
+
+  if (pkey)
+    rsa = EVP_PKEY_get1_RSA(pkey);
+
+  if (rsa) {
+    if (RSA_blinding_on(rsa, NULL) != 1) {
+      tls_log("error setting RSA blinding: %s",
+        ERR_error_string(ERR_get_error(), NULL));
+
+    } else {
+      tls_log("set RSA blinding on");
+    }
+
+    /* Now, "free" the RSA pointer, to properly decrement the reference
+     * counter.
+     */
+    RSA_free(rsa);
+
+  } else {
+
+    /* The administrator may have configured DSA keys rather than RSA keys.
+     * In this case, there is nothing to do.
+     */
+  }
+
+  return;
+}
+#endif
+
+static int tls_init_ctx(void) {
+  config_rec *c;
+  int ssl_opts = tls_ssl_opts;
+  long ssl_mode = 0;
+
+  if (pr_define_exists("TLS_USE_FIPS") &&
+      ServerType == SERVER_INETD) {
+#ifdef OPENSSL_FIPS
+    if (!FIPS_mode()) {
+      /* Make sure OpenSSL is set to use the default RNG, as per an email
+       * discussion on the OpenSSL developer list:
+       *
+       *  "The internal FIPS logic uses the default RNG to see the FIPS RNG
+       *   as part of the self test process..."
+       */
+      RAND_set_rand_method(NULL);
+
+      if (!FIPS_mode_set(1)) {
+        const char *errstr;
+
+        errstr = tls_get_errors();
+        tls_log("unable to use FIPS mode: %s", errstr);
+        pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION
+          ": unable to use FIPS mode: %s", errstr);
+
+        errno = EPERM;
+        return -1;
+
+      } else {
+        pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION ": FIPS mode enabled");
+      }
+
+    } else {
+      pr_log_pri(PR_LOG_DEBUG, MOD_TLS_VERSION ": FIPS mode already enabled");
+    }
+#else
+    pr_log_pri(PR_LOG_WARNING, MOD_TLS_VERSION ": FIPS mode requested, but " OPENSSL_VERSION_TEXT " not built with FIPS support");
+#endif /* OPENSSL_FIPS */
+  }
 
   if (ssl_ctx != NULL) {
     SSL_CTX_free(ssl_ctx);
@@ -2798,16 +5360,20 @@ static int tls_init_ctx(void) {
   ssl_opts |= SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
 #endif
 
-  /* Disable SSL tickets, for now. */
-#ifdef SSL_OP_NO_TICKET
-  ssl_opts |= SSL_OP_NO_TICKET;
-#endif
-
   /* Disable SSL compression. */
 #ifdef SSL_OP_NO_COMPRESSION
   ssl_opts |= SSL_OP_NO_COMPRESSION;
 #endif /* SSL_OP_NO_COMPRESSION */
 
+#if defined(PR_USE_OPENSSL_ECC)
+# if defined(SSL_OP_SINGLE_ECDH_USE)
+  ssl_opts |= SSL_OP_SINGLE_ECDH_USE;
+# endif
+# if defined(SSL_OP_SAFARI_ECDHE_ECDSA_BUG)
+  ssl_opts |= SSL_OP_SAFARI_ECDHE_ECDSA_BUG;
+# endif
+#endif /* ECC support */
+
 #ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
   c = find_config(main_server->conf, CONF_PARAM, "TLSServerCipherPreference",
     FALSE);
@@ -2931,15 +5497,112 @@ static int tls_init_ctx(void) {
     SSL_CTX_set_timeout(ssl_ctx, timeout);
   }
 
+  /* Set up OCSP response caching */
+  c = find_config(main_server->conf, CONF_PARAM, "TLSStaplingCache", FALSE);
+  if (c != NULL) {
+    const char *provider;
+
+    /* Look up and initialize the configured OCSP cache provider. */
+    provider = c->argv[0];
+
+    if (provider != NULL) {
+      tls_ocsp_cache = tls_ocsp_cache_get_cache(provider);
+
+      pr_log_debug(DEBUG8, MOD_TLS_VERSION ": opening '%s' TLSStaplingCache",
+        provider);
+
+      if (tls_ocsp_cache_open(c->argv[1]) < 0 &&
+          errno != ENOSYS) {
+        pr_log_debug(DEBUG1, MOD_TLS_VERSION
+          ": error opening '%s' TLSStaplingCache: %s", provider,
+          strerror(errno));
+        tls_ocsp_cache = NULL;
+      }
+    }
+  }
+
+#if defined(TLS_USE_SESSION_TICKETS)
+  c = find_config(main_server->conf, CONF_PARAM, "TLSSessionTicketKeys", FALSE);
+  if (c != NULL) {
+    tls_ticket_key_max_age = *((unsigned int *) c->argv[0]);
+    tls_ticket_key_max_count = *((unsigned int *) c->argv[1]);
+  }
+
+  /* Generate a random session ticket key, if necessary.  Maybe this list
+   * of keys could be stored as ex/app data in the SSL_CTX?
+   */
+  if (tls_ticket_keys == NULL) {
+    struct tls_ticket_key *k;
+    unsigned int new_ticket_key_intvl;
+
+    pr_log_debug(DEBUG9, MOD_TLS_VERSION
+      ": generating initial TLS session ticket key");
+
+    k = create_ticket_key();
+    if (k == NULL) {
+      pr_log_debug(DEBUG0, MOD_TLS_VERSION
+        ": unable to generate initial session ticket key: %s",
+        strerror(errno));
+
+    } else {
+      tls_ticket_keys = xaset_create(permanent_pool, tls_ticket_key_cmp);
+      add_ticket_key(k);
+    }
+
+    /* Also register a timer, to generate new keys every hour (or just under
+     * the max age of a key, whichever is smaller).
+     */
+
+    new_ticket_key_intvl = 3600;
+    if (tls_ticket_key_max_age < new_ticket_key_intvl) {
+      /* Try to get a new ticket a little before one expires. */
+      new_ticket_key_intvl = tls_ticket_key_max_age - 1;
+    }
+
+    pr_log_debug(DEBUG9, MOD_TLS_VERSION
+      ": scheduling new TLS session ticket key every %d %s",
+      new_ticket_key_intvl, new_ticket_key_intvl != 1 ? "secs" : "sec");
+
+    pr_timer_add(new_ticket_key_intvl, -1, NULL, new_ticket_key_timer_cb,
+      "New TLS Session Ticket Key");
+
+  } else {
+    struct tls_ticket_key *k;
+
+    /* Generate a new key on restart, as part of a good cryptographic
+     * hygiene.
+     */
+    pr_log_debug(DEBUG9, MOD_TLS_VERSION ": generating TLS session ticket key");
+
+    k = create_ticket_key();
+    if (k == NULL) {
+      pr_log_debug(DEBUG0, MOD_TLS_VERSION
+        ": unable to generate new session ticket key: %s", strerror(errno));
+
+    } else {
+      add_ticket_key(k);
+    }
+  }
+#endif /* TLS_USE_SESSION_TICKETS */
+
   SSL_CTX_set_tmp_dh_callback(ssl_ctx, tls_dh_cb);
-#if defined(PR_USE_OPENSSL_ECC) && OPENSSL_VERSION_NUMBER < 0x10100000L
-  SSL_CTX_set_tmp_ecdh_callback(ssl_ctx, tls_ecdh_cb);
+
+#ifdef PR_USE_OPENSSL_ECC
+  /* If using OpenSSL 1.0.2 or later, let it automatically choose the
+   * correct/best curve, rather than having to hardcode a fallback.
+   */
+# if defined(SSL_CTX_set_ecdh_auto)
+  SSL_CTX_set_ecdh_auto(ssl_ctx, 1);
+# endif
 #endif /* PR_USE_OPENSSL_ECC */
 
   if (tls_seed_prng() < 0) {
     pr_log_debug(DEBUG1, MOD_TLS_VERSION ": unable to properly seed PRNG");
   }
 
+  tls_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(tls_pool, MOD_TLS_VERSION);
+
   return 0;
 }
 
@@ -2976,8 +5639,7 @@ static const char *tls_get_proto_str(pool *p, unsigned int protos,
   return proto_str;
 }
 
-/* Construct the options value that disables all unsupported protocols.  
- */
+/* Construct the options value that disables all unsupported protocols. */
 static int get_disabled_protocols(unsigned int supported_protocols) {
   int disabled_protocols;
 
@@ -3020,7 +5682,7 @@ static int tls_init_server(void) {
   config_rec *c = NULL;
   char *tls_ca_cert = NULL, *tls_ca_path = NULL, *tls_ca_chain = NULL;
   X509 *server_ec_cert = NULL, *server_dsa_cert = NULL, *server_rsa_cert = NULL;
-  int verify_mode = SSL_VERIFY_PEER;
+  int verify_mode = 0;
   unsigned int enabled_proto_count = 0, tls_protocol = TLS_PROTO_DEFAULT;
   int disabled_proto;
   const char *enabled_proto_str = NULL;
@@ -3073,22 +5735,16 @@ static int tls_init_server(void) {
     }
   }
 
-  if (tls_flags & TLS_SESS_VERIFY_CLIENT) {
+  if (tls_flags & TLS_SESS_VERIFY_CLIENT_OPTIONAL) {
+    verify_mode = SSL_VERIFY_PEER;
+  }
+
+  if (tls_flags & TLS_SESS_VERIFY_CLIENT_REQUIRED) {
     /* If we are verifying clients, make sure the client sends a cert;
      * the protocol allows for the client to disregard a request for
      * its cert by the server.
      */
-    verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
-
-    if (tls_opts & TLS_OPT_NO_CERT_REQUEST) {
-      /* Warn about the incompatibility of using "TLSVerifyClient on" and
-       * "TLSOption NoCertRequest" at the same time.
-       */
-      tls_log("TLSVerifyClient in effect, ignoring NoCertRequest TLSOption");
-    }
-
-  } else if (tls_opts & TLS_OPT_NO_CERT_REQUEST) {
-    verify_mode = 0;
+    verify_mode |= (SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
   }
 
   if (verify_mode != 0) {
@@ -3210,16 +5866,23 @@ static int tls_init_server(void) {
     xerrno = errno;
     if (fh == NULL) {
       PRIVS_RELINQUISH
-      tls_log("error reading TLSRSACertificateFile '%s': %s", tls_rsa_cert_file,
+      pr_log_pri(PR_LOG_DEBUG, MOD_TLS_VERSION
+        ": error reading TLSRSACertificateFile '%s': %s", tls_rsa_cert_file,
         strerror(xerrno));
       errno = xerrno;
       return -1;
     }
 
+    /* As the file may contain sensitive data, we do not want it lingering
+     * around in stdio buffers.
+     */
+    (void) setvbuf(fh, NULL, _IONBF, 0);
+
     cert = read_cert(fh, ssl_ctx);
     if (cert == NULL) {
       PRIVS_RELINQUISH
-      tls_log("error reading TLSRSACertificateFile '%s': %s", tls_rsa_cert_file,
+      pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION
+        ": error reading TLSRSACertificateFile '%s': %s", tls_rsa_cert_file,
         tls_get_errors());
       fclose(fh);
       return -1;
@@ -3235,12 +5898,12 @@ static int tls_init_server(void) {
     if (res <= 0) {
       PRIVS_RELINQUISH
 
-      tls_log("error loading TLSRSACertificateFile '%s': %s", tls_rsa_cert_file,
+      pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION
+        ": error loading TLSRSACertificateFile '%s': %s", tls_rsa_cert_file,
         tls_get_errors());
       return -1;
     }
 
-    SSL_CTX_set_tmp_rsa_callback(ssl_ctx, tls_rsa_cb);
     server_rsa_cert = cert;
   }
 
@@ -3256,19 +5919,34 @@ static int tls_init_server(void) {
       X509_FILETYPE_PEM);
 
     if (res <= 0) {
+      const char *errors;
+
       PRIVS_RELINQUISH
 
-      tls_log("error loading TLSRSACertificateKeyFile '%s': %s",
-        tls_rsa_key_file, tls_get_errors());
+      errors = tls_get_errors();
+
+      pr_trace_msg(trace_channel, 3,
+        "error loading TLSRSACertificateKeyFile '%s': %s",
+        tls_rsa_key_file, errors);
+      pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION
+        ": error loading TLSRSACertificateKeyFile '%s': %s",
+        tls_rsa_key_file, errors);
       return -1;
     }
 
     res = SSL_CTX_check_private_key(ssl_ctx);
     if (res != 1) {
+      const char *errors;
+
       PRIVS_RELINQUISH 
 
-      tls_log("error checking key from TLSRSACertificateKeyFile '%s': %s",
-        tls_rsa_key_file, tls_get_errors());
+      errors = tls_get_errors();
+      pr_trace_msg(trace_channel, 3,
+        "error checking key from TLSRSACertificateKeyFile '%s': %s",
+        tls_rsa_key_file, errors);
+      pr_log_pri(PR_LOG_NOTICE, MOD_TLS_VERSION
+        ": error checking key from TLSRSACertificateKeyFile '%s': %s",
+        tls_rsa_key_file, errors);
       return -1;
     }
   }
@@ -3288,6 +5966,11 @@ static int tls_init_server(void) {
       return -1;
     }
 
+    /* As the file may contain sensitive data, we do not want it lingering
+     * around in stdio buffers.
+     */
+    (void) setvbuf(fh, NULL, _IONBF, 0);
+
     cert = read_cert(fh, ssl_ctx);
     if (cert == NULL) {
       PRIVS_RELINQUISH
@@ -3360,6 +6043,11 @@ static int tls_init_server(void) {
       return -1;
     }
 
+    /* As the file may contain sensitive data, we do not want it lingering
+     * around in stdio buffers.
+     */
+    (void) setvbuf(fh, NULL, _IONBF, 0);
+
     cert = read_cert(fh, ssl_ctx);
     if (cert == NULL) {
       PRIVS_RELINQUISH
@@ -3439,6 +6127,11 @@ static int tls_init_server(void) {
       return -1;
     }
 
+    /* As the file may contain sensitive data, we do not want it lingering
+     * around in stdio buffers.
+     */
+    (void) setvbuf(fp, NULL, _IONBF, 0);
+
     /* Note that this should NOT fail; we will have already parsed the
      * PKCS12 file already, in order to get the password and key passphrases.
      */
@@ -3771,15 +6464,53 @@ static int tls_get_block(conn_t *conn) {
   return TRUE;
 }
 
+static int tls_compare_session_ids(SSL_SESSION *ctrl_sess,
+    SSL_SESSION *data_sess) {
+  int res = -1;
+
+#if OPENSSL_VERSION_NUMBER < 0x000907000L
+  /* In the OpenSSL source code, SSL_SESSION_cmp() ultimately uses memcmp(3)
+   * to check, and thus returns memcmp(3)'s return value.
+   */
+  res = SSL_SESSION_cmp(ctrl_sess, data_sess);
+#else
+  const unsigned char *sess_id;
+  unsigned int sess_id_len;
+
+# if OPENSSL_VERSION_NUMBER > 0x000908000L
+  sess_id = (const unsigned char *) SSL_SESSION_get_id(data_sess, &sess_id_len);
+# else
+  /* XXX Directly accessing these fields cannot be a Good Thing. */
+  sess_id = data_sess->session_id;
+  sess_id_len = data_sess->session_id_length;
+# endif
+
+  res = SSL_has_matching_session_id(ctrl_ssl, sess_id, sess_id_len);
+
+  /* SSL_has_matchin_session_id() returns 1 for true, 0 for false.  Thus to
+   * emulate the memcmp(3) type interface, we return 0 for true, and -1 for
+   * false.
+   */
+  if (res == 1) {
+    res = 0;
+
+  } else {
+    res = -1;
+  }
+#endif
+
+  return res;
+}
+
 static int tls_accept(conn_t *conn, unsigned char on_data) {
+  static unsigned char logged_data = FALSE;
   int blocking, res = 0, xerrno = 0;
   long cache_mode = 0;
   char *subj = NULL;
-  static unsigned char logged_data = FALSE;
   SSL *ssl = NULL;
   BIO *rbio = NULL, *wbio = NULL;
 
-  if (!ssl_ctx) {
+  if (ssl_ctx == NULL) {
     tls_log("%s", "unable to start session: null SSL_CTX");
     return -1;
   }
@@ -3794,8 +6525,26 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
   /* This works with either rfd or wfd (I hope). */
   rbio = BIO_new_socket(conn->rfd, FALSE);
   wbio = BIO_new_socket(conn->wfd, FALSE);
+
+  /* During handshakes, set the write buffer size smaller, so that we do not
+   * overflow the (new) connection's TCP CWND size and force another round
+   * trip.
+   *
+   * Then, later, we set a larger buffer size, but ONLY if we are doing a data
+   * transfer.  For the control connection, the interactions/messages are
+   * assumed to be small, thus there's no need for the larger buffer size.
+   * Right?
+   */
+  (void) BIO_set_write_buf_size(wbio, TLS_HANDSHAKE_WRITE_BUFFER_SIZE);
+
   SSL_set_bio(ssl, rbio, wbio);
 
+#if !defined(OPENSSL_NO_TLSEXT)
+  if (tls_opts & TLS_OPT_ENABLE_DIAGS) {
+    SSL_set_tlsext_debug_callback(ssl, tls_tlsext_cb);
+  }
+#endif /* !OPENSSL_NO_TLSEXT */
+
   /* If configured, set a timer for the handshake. */
   if (tls_handshake_timeout) {
     tls_handshake_timer_id = pr_timer_add(tls_handshake_timeout, -1,
@@ -3803,18 +6552,23 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
   }
 
   if (on_data) {
-
     /* Make sure that TCP_NODELAY is enabled for the handshake. */
-    pr_inet_set_proto_nodelay(conn->pool, conn, 1);
+    if (pr_inet_set_proto_nodelay(conn->pool, conn, 1) < 0) {
+      pr_trace_msg(trace_channel, 9,
+        "error enabling TCP_NODELAY on data conn: %s", strerror(errno));
+    }
 
     /* Make sure that TCP_CORK (aka TCP_NOPUSH) is DISABLED for the handshake.
      * This socket option is set via the pr_inet_set_proto_opts() call made
      * in mod_core, upon handling the PASV/EPSV command.
      */
-    (void) pr_inet_set_proto_cork(conn->wfd, 0);
+    if (pr_inet_set_proto_cork(conn->wfd, 0) < 0) {
+      pr_trace_msg(trace_channel, 9,
+        "error disabling TCP_CORK on data conn: %s", strerror(errno));
+    }
 
     cache_mode = SSL_CTX_get_session_cache_mode(ssl_ctx);
-    if (!(cache_mode & SSL_SESS_CACHE_OFF)) {
+    if (cache_mode != SSL_SESS_CACHE_OFF) {
       /* Disable STORING of any new session IDs in the session cache. We DO
        * want to allow LOOKUP of session IDs in the session cache, however.
        */
@@ -3832,18 +6586,31 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
      * SSL handshake.  This lets us handle EAGAIN/retries better (i.e.
      * without spinning in a tight loop and consuming the CPU).
      */
-    pr_inet_set_nonblock(conn->pool, conn);
+    if (pr_inet_set_nonblock(conn->pool, conn) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error making %s connection nonblocking: %s",
+        on_data ? "data" : "ctrl", strerror(errno));
+    }
   }
 
   pr_signals_handle();
+
+  pr_trace_msg(trace_channel, 17, "calling SSL_accept() on %s conn fd %d",
+    on_data ? "data" : "ctrl", conn->rfd);
   res = SSL_accept(ssl);
   if (res == -1) {
     xerrno = errno;
   }
+  pr_trace_msg(trace_channel, 17, "SSL_accept() returned %d for %s conn fd %d",
+    res, on_data ? "data" : "ctrl", conn->rfd);
 
   if (blocking) {
     /* Return the connection to blocking mode. */
-    pr_inet_set_block(conn->pool, conn);
+    if (pr_inet_set_block(conn->pool, conn) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error making %s connection blocking: %s",
+        on_data ? "data" : "ctrl", strerror(errno));
+    }
   }
 
   if (res < 1) {
@@ -3854,22 +6621,22 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
 
     if (tls_handshake_timed_out) {
       tls_log("TLS negotiation timed out (%u seconds)", tls_handshake_timeout);
-      tls_end_sess(ssl, on_data ? PR_NETIO_STRM_DATA : PR_NETIO_STRM_CTRL, 0);
+      tls_end_sess(ssl, on_data ? session.d : session.c, 0);
       return -4;
     }
 
     switch (errcode) {
       case SSL_ERROR_WANT_READ:
         pr_trace_msg(trace_channel, 17,
-          "SSL_accept() returned WANT_READ, waiting for more to "
-          "read on fd %d", conn->rfd);
+          "WANT_READ encountered while accepting %s conn on fd %d, "
+          "waiting to read data", on_data ? "data" : "ctrl", conn->rfd);
         tls_readmore(conn->rfd);
         goto retry;
 
       case SSL_ERROR_WANT_WRITE:
         pr_trace_msg(trace_channel, 17,
-          "SSL_accept() returned WANT_WRITE, waiting for more to "
-          "write on fd %d", conn->rfd);
+          "WANT_WRITE encountered while accepting %s conn on fd %d, "
+          "waiting to send data", on_data ? "data" : "ctrl", conn->rfd);
         tls_writemore(conn->rfd);
         goto retry;
 
@@ -3893,6 +6660,8 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
           if (res == 0) {
             /* EOF */
             tls_log("%s: received EOF that violates protocol", msg);
+            tls_log("%s: usually this indicates an FTP-aware router, NAT, or "
+              "firewall interfering with the TLS handshake", msg);
 
           } else if (res == -1) {
             /* Check errno */
@@ -3907,9 +6676,86 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
         break;
       }
 
-      case SSL_ERROR_SSL:
+      case SSL_ERROR_SSL: {
+        pool *tmp_pool;
+        unsigned long ssl_errcode = ERR_peek_error();
+
         tls_log("%s: protocol error: %s", msg, tls_get_errors());
+
+        tmp_pool = make_sub_pool(conn->pool);
+
+        /* The error codes in the OpenSSL error queue are "packed"; we need
+         * to unpack them to get the reason value.
+         *
+         * Try to provide more context for the most commonly ocurring/reported
+         * handshake errors here.
+         */
+
+        switch (ERR_GET_REASON(ssl_errcode)) {
+          case SSL_R_UNKNOWN_PROTOCOL: {
+            long ssl_opts;
+            char *proto_str = "";
+
+            ssl_opts = SSL_get_options(ssl);
+
+#ifdef SSL_OP_NO_SSLv2
+            if (ssl_opts & SSL_OP_NO_SSLv2) {
+              proto_str = pstrcat(tmp_pool, proto_str, *proto_str ? ", " : "",
+                "SSLv2", NULL);
+            }
+#endif /* SSLv2 */
+
+            if (ssl_opts & SSL_OP_NO_SSLv3) {
+              proto_str = pstrcat(tmp_pool, proto_str, *proto_str ? ", " : "",
+                "SSLv3", NULL);
+            }
+
+            if (ssl_opts & SSL_OP_NO_TLSv1) {
+              proto_str = pstrcat(tmp_pool, proto_str, *proto_str ? ", " : "",
+                "TLSv1", NULL);
+            }
+
+#ifdef SSL_OP_NO_TLSv1_1
+            if (ssl_opts & SSL_OP_NO_TLSv1_1) {
+              proto_str = pstrcat(tmp_pool, proto_str, *proto_str ? ", " : "",
+                "TLSv1.1", NULL);
+            }
+#endif /* TLSv1.1 */
+
+#ifdef SSL_OP_NO_TLSv1_2
+            if (ssl_opts & SSL_OP_NO_TLSv1_2) {
+              proto_str = pstrcat(tmp_pool, proto_str, *proto_str ? ", " : "",
+                "TLSv1.2", NULL);
+            }
+#endif /* TLSv1.2 */
+
+            tls_log("%s: perhaps client requested disabled TLS protocol "
+              "version: %s", msg, proto_str);
+            break;
+          }
+
+          case SSL_R_NO_SHARED_CIPHER: {
+            tls_log("%s: client does not support any cipher from "
+              "'TLSCipherSuite %s' (see `openssl ciphers %s` for full list)",
+              msg, tls_cipher_suite, tls_cipher_suite);
+            break;
+          }
+
+          case SSL_R_PEER_DID_NOT_RETURN_A_CERTIFICATE: {
+            if (tls_flags & TLS_SESS_VERIFY_CLIENT_REQUIRED) {
+              tls_log("%s: client did not provide certificate, but one is "
+                "required via 'TLSVerifyClient on'", msg);
+            }
+            break;
+          }
+
+          default:
+            break;
+        }
+
+        destroy_pool(tmp_pool);
         break;
+      }
     }
 
     if (on_data) {
@@ -3919,26 +6765,76 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
       pr_event_generate("mod_tls.ctrl-handshake-failed", &errcode);
     }
 
-    tls_end_sess(ssl, on_data ? PR_NETIO_STRM_DATA : PR_NETIO_STRM_CTRL, 0);
+    tls_end_sess(ssl, on_data ? session.d : session.c, 0);
     return -3;
   }
 
+  pr_trace_msg(trace_channel, 17,
+    "TLS handshake on %s conn fd %d COMPLETED", on_data ? "data" : "ctrl",
+    conn->rfd);
+
   if (on_data) {
     /* Disable TCP_NODELAY, now that the handshake is done. */
-    pr_inet_set_proto_nodelay(conn->pool, conn, 0);
+    if (pr_inet_set_proto_nodelay(conn->pool, conn, 0) < 0) {
+      pr_trace_msg(trace_channel, 9,
+        "error disabling TCP_NODELAY on data conn: %s", strerror(errno));
+    }
 
     /* Reenable TCP_CORK (aka TCP_NOPUSH), now that the handshake is done. */
-    (void) pr_inet_set_proto_cork(conn->wfd, 1);
+    if (pr_inet_set_proto_cork(conn->wfd, 1) < 0) {
+      pr_trace_msg(trace_channel, 9,
+        "error re-enabling TCP_CORK on data conn: %s", strerror(errno));
+    }
 
-    if (!(cache_mode & SSL_SESS_CACHE_OFF)) {
+    if (cache_mode != SSL_SESS_CACHE_OFF) {
       /* Restore the previous session cache mode. */
       SSL_CTX_set_session_cache_mode(ssl_ctx, cache_mode);
     }
+
+    (void) BIO_set_write_buf_size(wbio,
+      TLS_DATA_ADAPTIVE_WRITE_MIN_BUFFER_SIZE);
+    tls_data_adaptive_bytes_written_ms = 0L;
+    tls_data_adaptive_bytes_written_count = 0;
   }
  
   /* Disable the handshake timer. */
   pr_timer_remove(tls_handshake_timer_id, &tls_module);
 
+#if defined(PR_USE_OPENSSL_NPN)
+  /* Which NPN protocol was selected, if any? */
+  {
+    const unsigned char *npn = NULL;
+    unsigned int npn_len = 0;
+
+    SSL_get0_next_proto_negotiated(ssl, &npn, &npn_len);
+    if (npn != NULL &&
+        npn_len > 0) {
+      pr_trace_msg(trace_channel, 9,
+        "negotiated NPN '%*s'", npn_len, npn);
+
+    } else {
+      pr_trace_msg(trace_channel, 9, "%s", "no NPN negotiated");
+    }
+  }
+#endif /* NPN */
+
+#if defined(PR_USE_OPENSSL_ALPN)
+  /* Which ALPN protocol was selected, if any? */
+  {
+    const unsigned char *alpn = NULL;
+    unsigned int alpn_len = 0;
+
+    SSL_get0_alpn_selected(ssl, &alpn, &alpn_len);
+    if (alpn != NULL &&
+        alpn_len > 0) {
+      pr_trace_msg(trace_channel, 9,
+        "selected ALPN '%*s'", alpn_len, alpn);
+    } else {
+      pr_trace_msg(trace_channel, 9, "%s", "no ALPN selected");
+    }
+  }
+#endif /* ALPN */
+
   /* Manually update the raw bytes counters with the network IO from the
    * SSL handshake.
    */
@@ -3953,12 +6849,23 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
 
     ctrl_ssl = ssl;
 
-    pr_table_add(tls_ctrl_rd_nstrm->notes,
-      pstrdup(tls_ctrl_rd_nstrm->strm_pool, TLS_NETIO_NOTE),
-      ssl, sizeof(SSL *));
-    pr_table_add(tls_ctrl_wr_nstrm->notes,
-      pstrdup(tls_ctrl_wr_nstrm->strm_pool, TLS_NETIO_NOTE),
-      ssl, sizeof(SSL *));
+    if (pr_table_add(tls_ctrl_rd_nstrm->notes,
+        pstrdup(tls_ctrl_rd_nstrm->strm_pool, TLS_NETIO_NOTE),
+        ssl, sizeof(SSL *)) < 0) {
+      if (errno != EEXIST) {
+        tls_log("error stashing '%s' note on ctrl read stream: %s",
+          TLS_NETIO_NOTE, strerror(errno));
+      }
+    }
+
+    if (pr_table_add(tls_ctrl_wr_nstrm->notes,
+        pstrdup(tls_ctrl_wr_nstrm->strm_pool, TLS_NETIO_NOTE),
+        ssl, sizeof(SSL *)) < 0) {
+      if (errno != EEXIST) {
+        tls_log("error stashing '%s' note on ctrl write stream: %s",
+          TLS_NETIO_NOTE, strerror(errno));
+      }
+    }
 
 #if OPENSSL_VERSION_NUMBER >= 0x009080dfL
     if (SSL_get_secure_renegotiation_support(ssl) == 1) {
@@ -3990,12 +6897,23 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
   } else if (conn == session.d) {
     pr_buffer_t *strm_buf;
 
-    pr_table_add(tls_data_rd_nstrm->notes,
-      pstrdup(tls_data_rd_nstrm->strm_pool, TLS_NETIO_NOTE),
-      ssl, sizeof(SSL *));
-    pr_table_add(tls_data_wr_nstrm->notes,
-      pstrdup(tls_data_wr_nstrm->strm_pool, TLS_NETIO_NOTE),
-      ssl, sizeof(SSL *));
+    if (pr_table_add(tls_data_rd_nstrm->notes,
+        pstrdup(tls_data_rd_nstrm->strm_pool, TLS_NETIO_NOTE),
+        ssl, sizeof(SSL *)) < 0) {
+      if (errno != EEXIST) {
+        tls_log("error stashing '%s' note on data read stream: %s",
+          TLS_NETIO_NOTE, strerror(errno));
+      }
+    }
+
+    if (pr_table_add(tls_data_wr_nstrm->notes,
+        pstrdup(tls_data_wr_nstrm->strm_pool, TLS_NETIO_NOTE),
+        ssl, sizeof(SSL *)) < 0) {
+      if (errno != EEXIST) {
+        tls_log("error stashing '%s' note on data write stream: %s",
+          TLS_NETIO_NOTE, strerror(errno));
+      }
+    }
 
     /* Clear any data from the NetIO stream buffers which may have been read
      * in before the SSL/TLS handshake occurred (Bug#3624).
@@ -4023,16 +6941,16 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
     int reused;
 
     subj = tls_get_subj_name(ctrl_ssl);
-    if (subj)
+    if (subj) {
       tls_log("Client: %s", subj);
+    }
 
-    if (!(tls_opts & TLS_OPT_NO_CERT_REQUEST)) {
-
+    if (tls_flags & TLS_SESS_VERIFY_CLIENT_REQUIRED) {
       /* Now we can go on with our post-handshake, application level
        * requirement checks.
        */
       if (tls_check_client_cert(ssl, conn) < 0) {
-        tls_end_sess(ssl, PR_NETIO_STRM_CTRL, 0);
+        tls_end_sess(ssl, session.c, 0);
         ctrl_ssl = NULL;
         return -1;
       }
@@ -4046,7 +6964,7 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
       reused > 0 ? ", resumed session" : "");
 
     /* Setup the TLS environment variables, if requested. */
-    tls_setup_environ(ssl);
+    tls_setup_environ(session.pool, ssl);
 
     if (reused > 0) {
       pr_log_writefile(tls_logfd, MOD_TLS_VERSION, "%s",
@@ -4080,52 +6998,30 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
 
       reused = SSL_session_reused(ssl);
       if (reused != 1) {
-        tls_log("client did not reuse SSL session, rejecting data connection "
-          "(see the NoSessionReuseRequired TLSOptions parameter)");
-        tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+        tls_log("%s", "client did not reuse SSL session, rejecting data "
+          "connection (see the NoSessionReuseRequired TLSOptions parameter)");
+        tls_end_sess(ssl, session.d, 0);
         pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
         pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
         return -1;
-
-      } else {
-        tls_log("%s", "client reused SSL session for data connection");
       }
 
+      tls_log("%s", "client reused SSL session for data connection");
+
       ctrl_sess = SSL_get_session(ctrl_ssl);
       if (ctrl_sess != NULL) {
         SSL_SESSION *data_sess;
 
         data_sess = SSL_get_session(ssl);
         if (data_sess != NULL) {
-          int matching_sess_id = -1;
-
-#if OPENSSL_VERSION_NUMBER < 0x000907000L
-          /* In the OpenSSL source code, SSL_SESSION_cmp() ultimately uses
-           * memcmp(3) to check, and thus returns memcmp(3)'s return value.
-           */
-          matching_sess_id = SSL_SESSION_cmp(ctrl_sess, data_sess);
-          if (matching_sess_id != 0) {
-#else
-          unsigned char *sess_id;
-          unsigned int sess_id_len;
+          int matching_sess = -1;
 
-# if OPENSSL_VERSION_NUMBER > 0x000908000L
-          sess_id = (unsigned char *) SSL_SESSION_get_id(data_sess,
-            &sess_id_len);
-# else
-          /* XXX Directly accessing these fields cannot be a Good Thing. */
-          sess_id = data_sess->session_id;
-          sess_id_len = data_sess->session_id_length;
-# endif
- 
-          matching_sess_id = SSL_has_matching_session_id(ctrl_ssl, sess_id,
-            sess_id_len);
-          if (matching_sess_id == 0) {
-#endif
+          matching_sess = tls_compare_session_ids(ctrl_sess, data_sess);
+          if (matching_sess != 0) {
             tls_log("Client did not reuse SSL session from control channel, "
               "rejecting data connection (see the NoSessionReuseRequired "
               "TLSOptions parameter)");
-            tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+            tls_end_sess(ssl, session.d, 0);
             pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
             pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
             return -1;
@@ -4159,17 +7055,21 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
               remaining = (unsigned long) ((sess_created + sess_expires) - now);
 
               if (remaining <= 60) {
-                tls_log("control channel SSL session expires in %lu secs (%lu session cache expiration)", remaining, sess_expires);
-                tls_log("%s","Consider using 'TLSSessionCache internal:' to increase the session cache expiration if necessary, or renegotiate the control channel SSL session");
+                tls_log("control channel SSL session expires in %lu secs "
+                  "(%lu session cache expiration)", remaining, sess_expires);
+                tls_log("%s", "Consider using 'TLSSessionCache internal:' to "
+                  "increase the session cache expiration if necessary, or "
+                  "renegotiate the control channel SSL session");
               }
             }
           }
 
         } else {
           /* This should never happen, so log if it does. */
-          tls_log("BUG: unable to determine whether client reused SSL session: SSL_get_session() for control connection return NULL");
-          tls_log("rejecting data connection (see TLSOption NoSessionReuseRequired)");
-          tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+          tls_log("%s", "BUG: unable to determine whether client reused SSL "
+            "session: SSL_get_session() for data connection returned NULL");
+          tls_log("%s", "rejecting data connection (see TLSOption NoSessionReuseRequired)");
+          tls_end_sess(ssl, session.d, 0);
           pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
           pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
           return -1;
@@ -4177,9 +7077,10 @@ static int tls_accept(conn_t *conn, unsigned char on_data) {
 
       } else {
         /* This should never happen, so log if it does. */
-        tls_log("BUG: unable to determine whether client reused SSL session: SSL_get_session() for control connection return NULL");
-        tls_log("rejecting data connection (see TLSOption NoSessionReuseRequired)");
-        tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+        tls_log("%s", "BUG: unable to determine whether client reused SSL "
+          "session: SSL_get_session() for control connection returned NULL!");
+        tls_log("%s", "rejecting data connection (see TLSOption NoSessionReuseRequired)");
+        tls_end_sess(ssl, session.d, 0);
         pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
         pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
         return -1;
@@ -4235,7 +7136,7 @@ static int tls_connect(conn_t *conn) {
   }
 
   /* Make sure that TCP_NODELAY is enabled for the handshake. */
-  pr_inet_set_proto_nodelay(conn->pool, conn, 1);
+  (void) pr_inet_set_proto_nodelay(conn->pool, conn, 1);
 
   retry:
 
@@ -4245,7 +7146,10 @@ static int tls_connect(conn_t *conn) {
      * SSL handshake.  This lets us handle EAGAIN/retries better (i.e.
      * without spinning in a tight loop and consuming the CPU).
      */
-    pr_inet_set_nonblock(conn->pool, conn);
+    if (pr_inet_set_nonblock(conn->pool, conn) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error making connection nonblocking: %s", strerror(errno));
+    }
   }
 
   pr_signals_handle();
@@ -4256,7 +7160,10 @@ static int tls_connect(conn_t *conn) {
 
   if (blocking) {
     /* Return the connection to blocking mode. */
-    pr_inet_set_block(conn->pool, conn);
+    if (pr_inet_set_block(conn->pool, conn) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error making connection blocking: %s", strerror(errno));
+    }
   }
 
   if (res < 1) {
@@ -4267,22 +7174,22 @@ static int tls_connect(conn_t *conn) {
 
     if (tls_handshake_timed_out) {
       tls_log("TLS negotiation timed out (%u seconds)", tls_handshake_timeout);
-      tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+      tls_end_sess(ssl, conn, 0);
       return -4;
     }
 
     switch (errcode) {
       case SSL_ERROR_WANT_READ:
         pr_trace_msg(trace_channel, 17,
-          "SSL_connect() returned WANT_READ, waiting for more to "
-          "read on fd %d", conn->rfd);
+          "WANT_READ encountered while connecting on fd %d, "
+          "waiting to read data", conn->rfd);
         tls_readmore(conn->rfd);
         goto retry;
 
       case SSL_ERROR_WANT_WRITE:
         pr_trace_msg(trace_channel, 17,
-          "SSL_connect() returned WANT_READ, waiting for more to "
-          "read on fd %d", conn->rfd);
+          "WANT_WRITE encountered while connecting on fd %d, "
+          "waiting to read data", conn->rfd);
         tls_writemore(conn->rfd);
         goto retry;
 
@@ -4327,12 +7234,12 @@ static int tls_connect(conn_t *conn) {
 
     pr_event_generate("mod_tls.data-handshake-failed", &errcode);
 
-    tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+    tls_end_sess(ssl, conn, 0);
     return -3;
   }
 
   /* Disable TCP_NODELAY, now that the handshake is done. */
-  pr_inet_set_proto_nodelay(conn->pool, conn, 0);
+  (void) pr_inet_set_proto_nodelay(conn->pool, conn, 0);
  
   /* Disable the handshake timer. */
   pr_timer_remove(tls_handshake_timer_id, &tls_module);
@@ -4349,12 +7256,23 @@ static int tls_connect(conn_t *conn) {
   if (conn == session.d) {
     pr_buffer_t *strm_buf;
 
-    pr_table_add(tls_data_rd_nstrm->notes,
-      pstrdup(tls_data_rd_nstrm->strm_pool, TLS_NETIO_NOTE),
-      ssl, sizeof(SSL *));
-    pr_table_add(tls_data_wr_nstrm->notes,
-      pstrdup(tls_data_wr_nstrm->strm_pool, TLS_NETIO_NOTE),
-      ssl, sizeof(SSL *));
+    if (pr_table_add(tls_data_rd_nstrm->notes,
+        pstrdup(tls_data_rd_nstrm->strm_pool, TLS_NETIO_NOTE),
+        ssl, sizeof(SSL *)) < 0) {
+      if (errno != EEXIST) {
+        tls_log("error stashing '%s' note on data read stream: %s",
+          TLS_NETIO_NOTE, strerror(errno));
+      }
+    }
+
+    if (pr_table_add(tls_data_wr_nstrm->notes,
+        pstrdup(tls_data_wr_nstrm->strm_pool, TLS_NETIO_NOTE),
+        ssl, sizeof(SSL *)) < 0) {
+      if (errno != EEXIST) {
+        tls_log("error stashing '%s' note on data write stream: %s",
+          TLS_NETIO_NOTE, strerror(errno));
+      }
+    }
 
     /* Clear any data from the NetIO stream buffers which may have been read
      * in before the SSL/TLS handshake occurred (Bug#3624).
@@ -4383,7 +7301,7 @@ static int tls_connect(conn_t *conn) {
   }
 
   if (tls_check_server_cert(ssl, conn) < 0) {
-    tls_end_sess(ssl, PR_NETIO_STRM_DATA, 0);
+    tls_end_sess(ssl, conn, 0);
     return -1;
   }
 
@@ -4397,6 +7315,7 @@ static int tls_connect(conn_t *conn) {
 static void tls_cleanup(int flags) {
 
   tls_sess_cache_close();
+  tls_ocsp_cache_close();
 
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
   if (tls_crypto_device) {
@@ -4454,10 +7373,11 @@ static void tls_cleanup(int flags) {
      * initialization, and other modules want to use OpenSSL, we may
      * be depriving those modules of OpenSSL functionality.
      *
-     * At the moment, the modules known to use OpenSSL are mod_ldap,
+     * At the moment, the modules known to use OpenSSL are mod_ldap, mod_proxy,
      * mod_sftp, mod_sql, and mod_sql_passwd.
      */
     if (pr_module_get("mod_ldap.c") == NULL &&
+        pr_module_get("mod_proxy.c") == NULL &&
         pr_module_get("mod_sftp.c") == NULL &&
         pr_module_get("mod_sql.c") == NULL &&
         pr_module_get("mod_sql_passwd.c") == NULL) {
@@ -4479,15 +7399,16 @@ static void tls_cleanup(int flags) {
   }
 }
 
-static void tls_end_sess(SSL *ssl, int strms, int flags) {
+static void tls_end_sess(SSL *ssl, conn_t *conn, int flags) {
   int res = 0;
   int shutdown_state;
   BIO *rbio, *wbio;
   int bread, bwritten;
   unsigned long rbio_rbytes, rbio_wbytes, wbio_rbytes, wbio_wbytes; 
 
-  if (!ssl)
+  if (ssl == NULL) {
     return;
+  }
 
   rbio = SSL_get_rbio(ssl);
   rbio_rbytes = BIO_number_read(rbio);
@@ -4505,13 +7426,28 @@ static void tls_end_sess(SSL *ssl, int strms, int flags) {
   if (!(shutdown_state & SSL_SENT_SHUTDOWN)) {
     errno = 0;
 
+    if (conn != NULL) {
+      /* Disable any socket buffering (Nagle, TCP_CORK), so that the alert
+       * is sent in a timely manner (avoiding TLS shutdown latency).
+       */
+      if (pr_inet_set_proto_nodelay(conn->pool, conn, 1) < 0) {
+        pr_trace_msg(trace_channel, 9,
+          "error enabling TCP_NODELAY on conn: %s", strerror(errno));
+      }
+
+      if (pr_inet_set_proto_cork(conn->wfd, 0) < 0) {
+        pr_trace_msg(trace_channel, 9,
+          "error disabling TCP_CORK on fd %d: %s", conn->wfd, strerror(errno));
+      }
+    }
+
     /* 'close_notify' not already sent; send it now. */
     res = SSL_shutdown(ssl);
   }
 
   if (res == 0) {
     /* Now call SSL_shutdown() again, but only if necessary. */
-    if (flags & TLS_SHUTDOWN_BIDIRECTIONAL) {
+    if (flags & TLS_SHUTDOWN_FL_BIDIRECTIONAL) {
       shutdown_state = SSL_get_shutdown(ssl);
 
       res = 1;
@@ -4537,9 +7473,28 @@ static void tls_end_sess(SSL *ssl, int strms, int flags) {
           tls_log("SSL_shutdown error: WANT_WRITE");
           break;
 
-        case SSL_ERROR_SSL:
+        case SSL_ERROR_SSL: {
+          unsigned long ssl_errcode = ERR_peek_error();
+
+          /* The error codes in the OpenSSL error queue are "packed"; we need
+           * to unpack them to get the reason value.
+           */
+
+#ifdef SSL_R_SHUTDOWN_WHILE_IN_INIT
+          if (ERR_GET_REASON(ssl_errcode) != SSL_R_SHUTDOWN_WHILE_IN_INIT) {
+            /* This SHUTDOWN_WHILE_IN_INIT can happen if the TLS handshake
+             * failed before being completed.  As such, logging this shutdown
+             * error on an incomplete session is spurious and not helpful.
+             */
+            tls_log("SSL_shutdown error: SSL: %s", tls_get_errors());
+          }
+#else
+          (void) ssl_errcode;
           tls_log("SSL_shutdown error: SSL: %s", tls_get_errors());
+#endif /* No SSL_R_SHUTDOWN_WHILE_IN_INIT */
+
           break;
+        }
 
         case SSL_ERROR_ZERO_RETURN:
           /* Clean shutdown, nothing we need to do. */
@@ -4580,9 +7535,27 @@ static void tls_end_sess(SSL *ssl, int strms, int flags) {
          */
         break;
 
-      case SSL_ERROR_SSL:
+      case SSL_ERROR_SSL: {
+        unsigned long ssl_errcode = ERR_peek_error();
+
+        /* The error codes in the OpenSSL error queue are "packed"; we need
+         * to unpack them to get the reason value.
+         */
+
+#ifdef SSL_R_SHUTDOWN_WHILE_IN_INIT
+        if (ERR_GET_REASON(ssl_errcode) != SSL_R_SHUTDOWN_WHILE_IN_INIT) {
+          /* This SHUTDOWN_WHILE_IN_INIT can happen if the TLS handshake
+           * failed before being completed.  As such, logging this shutdown
+           * error on an incomplete session is spurious and not helpful.
+           */
+          tls_log("SSL_shutdown error: SSL: %s", tls_get_errors());
+        }
+#else
+        (void) ssl_errcode;
         tls_log("SSL_shutdown error: SSL: %s", tls_get_errors());
+#endif /* No SSL_R_SHUTDOWN_WHILE_IN_INIT */
         break;
+      }
 
       case SSL_ERROR_SYSCALL:
         if (errno != 0 &&
@@ -4622,23 +7595,37 @@ static void tls_end_sess(SSL *ssl, int strms, int flags) {
 
 static const char *tls_get_errors(void) {
   unsigned int count = 0;
-  unsigned long e = ERR_get_error();
+  unsigned long error_code;
   BIO *bio = NULL;
   char *data = NULL;
   long datalen;
-  const char *str = "(unknown)";
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
 
   /* Use ERR_print_errors() and a memory BIO to build up a string with
    * all of the error messages from the error queue.
    */
 
-  if (e)
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
     bio = BIO_new(BIO_s_mem());
+  }
 
-  while (e) {
+  while (error_code) {
     pr_signals_handle();
-    BIO_printf(bio, "\n  (%u) %s", ++count, ERR_error_string(e, NULL));
-    e = ERR_get_error(); 
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
   }
 
   datalen = BIO_get_mem_data(bio, &data);
@@ -4647,14 +7634,14 @@ static const char *tls_get_errors(void) {
     str = pstrdup(session.pool, data);
   }
 
-  if (bio)
+  if (bio) {
     BIO_free(bio);
+  }
 
   return str;
 }
 
-/* Return a page-aligned pointer to memory of at least the given size.
- */
+/* Return a page-aligned pointer to memory of at least the given size. */
 static char *tls_get_page(size_t sz, void **ptr) {
   void *d;
   long pagesz = tls_get_pagesz(), p;
@@ -4662,7 +7649,7 @@ static char *tls_get_page(size_t sz, void **ptr) {
   d = calloc(1, sz + (pagesz-1));
   if (d == NULL) {
     pr_log_pri(PR_LOG_ALERT, MOD_TLS_VERSION ": Out of memory!");
-    pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_NOMEM, NULL);
+    exit(1);
   }
 
   *ptr = d;
@@ -4672,8 +7659,7 @@ static char *tls_get_page(size_t sz, void **ptr) {
   return ((char *) p);
 }
 
-/* Return the size of a page on this architecture.
- */
+/* Return the size of a page on this architecture. */
 static size_t tls_get_pagesz(void) {
   long pagesz;
 
@@ -4794,6 +7780,8 @@ static int tls_dotlogin_allow(const char *user) {
   /* If the client did not provide a cert, we cannot do the .tlslogin check. */
   client_cert = SSL_get_peer_certificate(ctrl_ssl);
   if (client_cert == NULL) {
+    pr_trace_msg(trace_channel, 9, "%s",
+      "client did not provide certificate, skipping AllowDotLogin check");
     return FALSE;
   }
 
@@ -4832,12 +7820,18 @@ static int tls_dotlogin_allow(const char *user) {
     return FALSE;
   }
 
+  /* As the file may contain sensitive data, we do not want it lingering
+   * around in stdio buffers.
+   */
+  (void) setvbuf(fp, NULL, _IONBF, 0);
+
   while ((file_cert = PEM_read_X509(fp, NULL, NULL, NULL))) {
     const ASN1_BIT_STRING *client_sig = NULL, *file_sig = NULL;
 
     pr_signals_handle();
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     X509_get0_signature(&client_sig, NULL, client_cert);
     X509_get0_signature(&file_sig, NULL, file_cert);
 #else
@@ -4845,7 +7839,8 @@ static int tls_dotlogin_allow(const char *user) {
     file_sig = file_cert->signature;
 #endif /* OpenSSL-1.1.x and later */
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     if (!ASN1_STRING_cmp(client_sig, file_sig)) {
 #else
     if (!M_ASN1_BIT_STRING_cmp(client_sig, file_sig)) {
@@ -4912,7 +7907,7 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
       /* Watch for any embedded NULs, which can cause verification
        * problems via spoofing.
        */
-      if (data_len != strlen((char *) data_str)) {
+      if ((size_t) data_len != strlen((char *) data_str)) {
         tls_log("%s", "client cert CommonName contains embedded NULs, "
           "ignoring as possible spoof attempt");
         tls_log("suspicious CommonName value: '%s'", data_str);
@@ -4937,11 +7932,15 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
     sk_alt_names = X509_get_ext_d2i(client_cert, NID_subject_alt_name, NULL,
       NULL);
     if (sk_alt_names != NULL) {
-      register unsigned int i;
+      register int i;
       int nnames = sk_GENERAL_NAME_num(sk_alt_names);
 
       for (i = 0; i < nnames; i++) {
-        GENERAL_NAME *name = sk_GENERAL_NAME_value(sk_alt_names, i);
+        GENERAL_NAME *name;
+
+        pr_signals_handle();
+
+        name = sk_GENERAL_NAME_value(sk_alt_names, i);
 
         /* We're only looking for the Email type. */
         if (name->type == GEN_EMAIL) {
@@ -4954,7 +7953,7 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
           /* Watch for any embedded NULs, which can cause verification
            * problems via spoofing.
            */
-          if (data_len != strlen((char *) data_str)) {
+          if ((size_t) data_len != strlen((char *) data_str)) {
             tls_log("%s", "client cert Email SAN contains embedded NULs, "
               "ignoring as possible spoof attempt");
             tls_log("suspicious Email SubjAltName value: '%s'", data_str);
@@ -4986,13 +7985,15 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
 
     nexts = X509_get_ext_count(client_cert);
     if (nexts > 0) {
-      register unsigned int i;
+      register int i;
 
       for (i = 0; i < nexts; i++) {
         X509_EXTENSION *ext = NULL;
         ASN1_OBJECT *asn_object = NULL;
         char oid[PR_TUNABLE_PATH_MAX];
 
+        pr_signals_handle();
+
         ext = X509_get_ext(client_cert, i);
         asn_object = X509_EXTENSION_get_object(ext);
 
@@ -5011,7 +8012,7 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
             /* Watch for any embedded NULs, which can cause verification
              * problems via spoofing.
              */
-            if (asn_datalen != strlen((char *) asn_datastr)) {
+            if ((size_t) asn_datalen != strlen((char *) asn_datastr)) {
               tls_log("client cert %s extension contains embedded NULs, "
                 "ignoring as possible spoof attempt", field_name);
               tls_log("suspicious %s extension value: '%s'", field_name,
@@ -5089,8 +8090,8 @@ static ssize_t tls_read(SSL *ssl, void *buf, size_t len) {
          * so we wait a little while for it.
          */
         pr_trace_msg(trace_channel, 17,
-          "SSL_read() returned WANT_READ, waiting for more to "
-          "read on fd %d", fd);
+          "WANT_READ encountered while reading SSL data on fd %d, "
+          "waiting to read data", fd);
         err = tls_readmore(fd);
         if (err > 0) {
           goto retry;
@@ -5112,8 +8113,8 @@ static ssize_t tls_read(SSL *ssl, void *buf, size_t len) {
          * block, so we wait a little while for it.
          */
         pr_trace_msg(trace_channel, 17,
-          "SSL_read() returned WANT_WRITE, waiting for more to "
-          "write on fd %d", fd);
+          "WANT_WRITE encountered while writing SSL data on fd %d, "
+          "waiting to send data", fd);
         err = tls_writemore(fd);
         if (err > 0) {
           goto retry;
@@ -5143,40 +8144,6 @@ static ssize_t tls_read(SSL *ssl, void *buf, size_t len) {
   return count;
 }
 
-static RSA *tls_rsa_cb(SSL *ssl, int is_export, int keylen) {
-  BIGNUM *e = NULL;
-
-  if (tls_tmp_rsa) {
-    return tls_tmp_rsa;
-  }
-
-#if OPENSSL_VERSION_NUMBER > 0x000908000L
-  e = BN_new();
-  if (e == NULL) {
-    return NULL;
-  }
-
-  if (BN_set_word(e, RSA_F4) != 1) {
-    BN_free(e);
-    return NULL;
-  }
-
-  if (RSA_generate_key_ex(tls_tmp_rsa, keylen, e, NULL) != 1) {
-    BN_free(e);
-    return NULL;
-  }
-
-#else
-  tls_tmp_rsa = RSA_generate_key(keylen, RSA_F4, NULL, NULL);
-#endif /* OpenSSL version 0.9.8 and later */
-
-  if (e != NULL) {
-    BN_free(e);
-  }
-
-  return tls_tmp_rsa;
-}
-
 static int tls_seed_prng(void) {
   char *heapdata, stackdata[1024];
   static char rand_file[300];
@@ -5281,12 +8248,14 @@ static void tls_setup_cert_ext_environ(const char *env_prefix, X509 *cert) {
 
   nexts = X509_get_ext_count(cert);
   if (nexts > 0) {
-    register unsigned int i = 0;
+    register int i;
 
     for (i = 0; i < nexts; i++) {
-      X509_EXTENSION *ext = X509_get_ext(cert, i);
-      const char *extstr = OBJ_nid2sn(OBJ_obj2nid(
-        X509_EXTENSION_get_object(ext)));
+      X509_EXTENSION *ext;
+      const char *extstr;
+
+      ext = X509_get_ext(cert, i);
+      extstr = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext)));
     }
   }
 #endif
@@ -5316,7 +8285,7 @@ static void tls_setup_cert_ext_environ(const char *env_prefix, X509 *cert) {
  */
 
 static void tls_setup_cert_dn_environ(const char *env_prefix, X509_NAME *name) {
-  register unsigned int i = 0;
+  register int i;
   int nentries;
   char *k, *v;
 
@@ -5434,7 +8403,8 @@ static void tls_setup_cert_dn_environ(const char *env_prefix, X509_NAME *name) {
   }
 }
 
-static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
+static void tls_setup_cert_environ(pool *p, const char *env_prefix,
+    X509 *cert) {
   char *data = NULL, *k, *v;
   long datalen = 0;
   BIO *bio = NULL;
@@ -5449,18 +8419,18 @@ static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
     snprintf(buf, sizeof(buf) - 1, "%lu", X509_get_version(cert) + 1);
     buf[sizeof(buf)-1] = '\0';
 
-    k = pstrcat(main_server->pool, env_prefix, "M_VERSION", NULL);
-    v = pstrdup(main_server->pool, buf);
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "M_VERSION", NULL);
+    v = pstrdup(p, buf);
+    pr_env_set(p, k, v);
 
     if (serial->length < 4) {
       memset(buf, '\0', sizeof(buf));
       snprintf(buf, sizeof(buf) - 1, "%lu", ASN1_INTEGER_get(serial));
       buf[sizeof(buf)-1] = '\0';
 
-      k = pstrcat(main_server->pool, env_prefix, "M_SERIAL", NULL);
-      v = pstrdup(main_server->pool, buf);
-      pr_env_set(main_server->pool, k, v);
+      k = pstrcat(p, env_prefix, "M_SERIAL", NULL);
+      v = pstrdup(p, buf);
+      pr_env_set(p, k, v);
 
     } else {
 
@@ -5470,33 +8440,30 @@ static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
       tls_log("%s", "certificate serial number not printable");
     }
 
-    k = pstrcat(main_server->pool, env_prefix, "S_DN", NULL);
-    v = pstrdup(main_server->pool,
-      tls_x509_name_oneline(X509_get_subject_name(cert)));
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "S_DN", NULL);
+    v = pstrdup(p, tls_x509_name_oneline(X509_get_subject_name(cert)));
+    pr_env_set(p, k, v);
 
-    tls_setup_cert_dn_environ(pstrcat(main_server->pool, env_prefix, "S_DN_",
+    tls_setup_cert_dn_environ(pstrcat(p, env_prefix, "S_DN_",
       NULL), X509_get_subject_name(cert));
 
-    k = pstrcat(main_server->pool, env_prefix, "I_DN", NULL);
-    v = pstrdup(main_server->pool,
-      tls_x509_name_oneline(X509_get_issuer_name(cert)));
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "I_DN", NULL);
+    v = pstrdup(p, tls_x509_name_oneline(X509_get_issuer_name(cert)));
+    pr_env_set(p, k, v);
 
-    tls_setup_cert_dn_environ(pstrcat(main_server->pool, env_prefix, "I_DN_",
-      NULL), X509_get_issuer_name(cert));
+    tls_setup_cert_dn_environ(pstrcat(p, env_prefix, "I_DN_", NULL),
+      X509_get_issuer_name(cert));
 
-    tls_setup_cert_ext_environ(pstrcat(main_server->pool, env_prefix, "EXT_",
-      NULL), cert);
+    tls_setup_cert_ext_environ(pstrcat(p, env_prefix, "EXT_", NULL), cert);
 
     bio = BIO_new(BIO_s_mem());
     ASN1_TIME_print(bio, X509_get_notBefore(cert));
     datalen = BIO_get_mem_data(bio, &data);
     data[datalen] = '\0';
 
-    k = pstrcat(main_server->pool, env_prefix, "V_START", NULL);
-    v = pstrdup(main_server->pool, data);
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "V_START", NULL);
+    v = pstrdup(p, data);
+    pr_env_set(p, k, v);
 
     BIO_free(bio);
 
@@ -5505,14 +8472,15 @@ static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
     datalen = BIO_get_mem_data(bio, &data);
     data[datalen] = '\0';
 
-    k = pstrcat(main_server->pool, env_prefix, "V_END", NULL);
-    v = pstrdup(main_server->pool, data);
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "V_END", NULL);
+    v = pstrdup(p, data);
+    pr_env_set(p, k, v);
 
     BIO_free(bio);
 
     bio = BIO_new(BIO_s_mem());
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
     X509_get0_signature(NULL, &algo, cert);
 #else
     algo = cert->cert_info->signature;
@@ -5521,9 +8489,9 @@ static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
     datalen = BIO_get_mem_data(bio, &data);
     data[datalen] = '\0';
 
-    k = pstrcat(main_server->pool, env_prefix, "A_SIG", NULL);
-    v = pstrdup(main_server->pool, data);
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "A_SIG", NULL);
+    v = pstrdup(p, data);
+    pr_env_set(p, k, v);
 
     BIO_free(bio);
 
@@ -5539,9 +8507,9 @@ static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
     datalen = BIO_get_mem_data(bio, &data);
     data[datalen] = '\0';
 
-    k = pstrcat(main_server->pool, env_prefix, "A_KEY", NULL);
-    v = pstrdup(main_server->pool, data);
-    pr_env_set(main_server->pool, k, v);
+    k = pstrcat(p, env_prefix, "A_KEY", NULL);
+    v = pstrdup(p, data);
+    pr_env_set(p, k, v);
 
     BIO_free(bio);
   }
@@ -5551,41 +8519,42 @@ static void tls_setup_cert_environ(const char *env_prefix, X509 *cert) {
   datalen = BIO_get_mem_data(bio, &data);
   data[datalen] = '\0';
 
-  k = pstrcat(main_server->pool, env_prefix, "CERT", NULL);
-  v = pstrdup(main_server->pool, data);
-  pr_env_set(main_server->pool, k, v);
+  k = pstrcat(p, env_prefix, "CERT", NULL);
+  v = pstrdup(p, data);
+  pr_env_set(p, k, v);
 
   BIO_free(bio);
 }
 
-static void tls_setup_environ(SSL *ssl) {
+static void tls_setup_environ(pool *p, SSL *ssl) {
   X509 *cert = NULL;
   STACK_OF(X509) *sk_cert_chain = NULL;
   char *k, *v;
 
   if (!(tls_opts & TLS_OPT_EXPORT_CERT_DATA) &&
-      !(tls_opts & TLS_OPT_STD_ENV_VARS))
+      !(tls_opts & TLS_OPT_STD_ENV_VARS)) {
     return;
+  }
 
   if (tls_opts & TLS_OPT_STD_ENV_VARS) {
     SSL_CIPHER *cipher = NULL;
     SSL_SESSION *ssl_session = NULL;
+    const char *sni = NULL;
 
-    k = pstrdup(main_server->pool, "FTPS");
-    v = pstrdup(main_server->pool, "1");
-    pr_env_set(main_server->pool, k, v);
+    k = pstrdup(p, "FTPS");
+    v = pstrdup(p, "1");
+    pr_env_set(p, k, v);
 
-    k = pstrdup(main_server->pool, "TLS_PROTOCOL");
-    v = pstrdup(main_server->pool, SSL_get_version(ssl));
-    pr_env_set(main_server->pool, k, v);
+    k = pstrdup(p, "TLS_PROTOCOL");
+    v = pstrdup(p, SSL_get_version(ssl));
+    pr_env_set(p, k, v);
 
     /* Process the SSL session-related environ variable. */
     ssl_session = SSL_get_session(ssl);
     if (ssl_session) {
-      char buf[SSL_MAX_SSL_SESSION_ID_LENGTH*2+1];
-      register unsigned int i = 0;
       const unsigned char *sess_data;
       unsigned int sess_datalen;
+      char *sess_id;
 
 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
       sess_data = SSL_SESSION_get_id(ssl_session, &sess_datalen);
@@ -5594,16 +8563,11 @@ static void tls_setup_environ(SSL *ssl) {
       sess_data = ssl_session->session_id;
 #endif /* OpenSSL-1.1.x and later */
 
-      /* Have to obtain a stringified session ID the hard way. */
-      memset(buf, '\0', sizeof(buf));
-      for (i = 0; i < sess_datalen; i++) {
-        snprintf(&(buf[i*2]), sizeof(buf) - (i*2) - 1, "%02X", sess_data[i]);
-      }
-      buf[sizeof(buf)-1] = '\0';
+      sess_id = pr_str_bin2hex(p, sess_data, sess_datalen,
+        PR_STR_FL_HEX_USE_UC);
 
-      k = pstrdup(main_server->pool, "TLS_SESSION_ID");
-      v = pstrdup(main_server->pool, buf);
-      pr_env_set(main_server->pool, k, v);
+      k = pstrdup(p, "TLS_SESSION_ID");
+      pr_env_set(p, k, sess_id);
     }
 
     /* Process the SSL cipher-related environ variables. */
@@ -5612,51 +8576,61 @@ static void tls_setup_environ(SSL *ssl) {
       char buf[10] = {'\0'};
       int cipher_bits_used = 0, cipher_bits_possible = 0;
 
-      k = pstrdup(main_server->pool, "TLS_CIPHER");
-      v = pstrdup(main_server->pool, SSL_CIPHER_get_name(cipher));
-      pr_env_set(main_server->pool, k, v);
+      k = pstrdup(p, "TLS_CIPHER");
+      v = pstrdup(p, SSL_CIPHER_get_name(cipher));
+      pr_env_set(p, k, v);
 
       cipher_bits_used = SSL_CIPHER_get_bits(cipher, &cipher_bits_possible);
 
       if (cipher_bits_used < 56) {
-        k = pstrdup(main_server->pool, "TLS_CIPHER_EXPORT");
-        v = pstrdup(main_server->pool, "1");
-        pr_env_set(main_server->pool, k, v);
+        k = pstrdup(p, "TLS_CIPHER_EXPORT");
+        v = pstrdup(p, "1");
+        pr_env_set(p, k, v);
       }
 
       memset(buf, '\0', sizeof(buf));
       snprintf(buf, sizeof(buf), "%d", cipher_bits_possible);
       buf[sizeof(buf)-1] = '\0';
 
-      k = pstrdup(main_server->pool, "TLS_CIPHER_KEYSIZE_POSSIBLE");
-      v = pstrdup(main_server->pool, buf);
-      pr_env_set(main_server->pool, k, v);
+      k = pstrdup(p, "TLS_CIPHER_KEYSIZE_POSSIBLE");
+      v = pstrdup(p, buf);
+      pr_env_set(p, k, v);
 
       memset(buf, '\0', sizeof(buf));
       snprintf(buf, sizeof(buf), "%d", cipher_bits_used);
       buf[sizeof(buf)-1] = '\0';
 
-      k = pstrdup(main_server->pool, "TLS_CIPHER_KEYSIZE_USED");
-      v = pstrdup(main_server->pool, buf);
-      pr_env_set(main_server->pool, k, v);
+      k = pstrdup(p, "TLS_CIPHER_KEYSIZE_USED");
+      v = pstrdup(p, buf);
+      pr_env_set(p, k, v);
+    }
+
+    sni = pr_table_get(session.notes, "mod_tls.sni", NULL);
+    if (sni != NULL) {
+      k = pstrdup(p, "TLS_SERVER_NAME");
+      v = pstrdup(p, sni);
+      pr_env_set(p, k, v);
     }
 
-    k = pstrdup(main_server->pool, "TLS_LIBRARY_VERSION");
-    v = pstrdup(main_server->pool, OPENSSL_VERSION_TEXT);
-    pr_env_set(main_server->pool, k, v);
+    k = pstrdup(p, "TLS_LIBRARY_VERSION");
+    v = pstrdup(p, OPENSSL_VERSION_TEXT);
+    pr_env_set(p, k, v);
   }
 
   sk_cert_chain = SSL_get_peer_cert_chain(ssl);
   if (sk_cert_chain) {
+    register int i;
     char *data = NULL;
     long datalen = 0;
-    register unsigned int i = 0;
     BIO *bio = NULL;
 
     /* Adding TLS_CLIENT_CERT_CHAIN environ variables. */
     for (i = 0; i < sk_X509_num(sk_cert_chain); i++) {
       size_t klen = 256;
-      k = pcalloc(main_server->pool, klen);
+
+      pr_signals_handle();
+
+      k = pcalloc(p, klen);
       snprintf(k, klen - 1, "%s%u", "TLS_CLIENT_CERT_CHAIN", i + 1);
 
       bio = BIO_new(BIO_s_mem());
@@ -5664,9 +8638,8 @@ static void tls_setup_environ(SSL *ssl) {
       datalen = BIO_get_mem_data(bio, &data);
       data[datalen] = '\0';
 
-      v = pstrdup(main_server->pool, data);
-
-      pr_env_set(main_server->pool, k, v);
+      v = pstrdup(p, data);
+      pr_env_set(p, k, v);
 
       BIO_free(bio);
     } 
@@ -5676,8 +8649,8 @@ static void tls_setup_environ(SSL *ssl) {
    * so we do not call X509_free() on it.
    */
   cert = SSL_get_certificate(ssl);
-  if (cert) {
-    tls_setup_cert_environ("TLS_SERVER_", cert);
+  if (cert != NULL) {
+    tls_setup_cert_environ(p, "TLS_SERVER_", cert);
 
   } else {
     tls_log("unable to set server certificate environ variables: "
@@ -5685,8 +8658,8 @@ static void tls_setup_environ(SSL *ssl) {
   }
 
   cert = SSL_get_peer_certificate(ssl);
-  if (cert) {
-    tls_setup_cert_environ("TLS_CLIENT_", cert);
+  if (cert != NULL) {
+    tls_setup_cert_environ(p, "TLS_CLIENT_", cert);
     X509_free(cert);
 
   } else {
@@ -5702,8 +8675,10 @@ static int tls_verify_cb(int ok, X509_STORE_CTX *ctx) {
   int verify_err = 0;
 
   /* We can configure the server to skip the peer's cert verification */
-  if (!(tls_flags & TLS_SESS_VERIFY_CLIENT))
-     return 1;
+  if (!(tls_flags & TLS_SESS_VERIFY_CLIENT_REQUIRED) &&
+      !(tls_flags & TLS_SESS_VERIFY_CLIENT_OPTIONAL)) {
+    return 1;
+  }
 
   c = find_config(main_server->conf, CONF_PARAM, "TLSVerifyOrder", FALSE);
   if (c) {
@@ -5735,8 +8710,11 @@ static int tls_verify_cb(int ok, X509_STORE_CTX *ctx) {
   }
 
   if (!ok) {
-    X509 *cert = X509_STORE_CTX_get_current_cert(ctx);
-    int ctx_error, depth = X509_STORE_CTX_get_error_depth(ctx);
+    X509 *cert;
+    int ctx_error, depth;
+
+    cert = X509_STORE_CTX_get_current_cert(ctx);
+    depth = X509_STORE_CTX_get_error_depth(ctx);
 
 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
     verify_err = X509_STORE_CTX_get_error(ctx);
@@ -5776,14 +8754,17 @@ static int tls_verify_cb(int ok, X509_STORE_CTX *ctx) {
         break;
 
       case X509_V_ERR_INVALID_PURPOSE: {
-        register unsigned int i;
-        int count = X509_PURPOSE_get_count();
+        register int i;
+        int count;
 
         tls_log("client certificate failed verification: %s",
           X509_verify_cert_error_string(ctx_error));
 
+        count = X509_PURPOSE_get_count();
         for (i = 0; i < count; i++) {
-          X509_PURPOSE *purp = X509_PURPOSE_get0(i);
+          X509_PURPOSE *purp;
+
+          purp = X509_PURPOSE_get0(i);
           tls_log("  purpose #%d: %s", i+1, X509_PURPOSE_get0_name(purp));
         }
 
@@ -5813,12 +8794,12 @@ static int tls_verify_cb(int ok, X509_STORE_CTX *ctx) {
  * <rse at engelshall.com>.  Comments by Ralf.
  */
 static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
+  register int i = 0;
   X509_NAME *subject = NULL, *issuer = NULL;
   X509 *xs = NULL;
   STACK_OF(X509_CRL) *crls = NULL;
   X509_STORE_CTX *store_ctx = NULL;
   int n, res;
-  register int i = 0;
 
   /* Unless a revocation store for CRLs was created we cannot do any
    * CRL-based verification, of course.
@@ -5827,11 +8808,10 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
     return ok;
   }
 
-  tls_log("CRL store present, checking client certificate against configured "
-    "CRLs");
+  tls_log("%s",
+    "CRL store present, checking client certificate against configured CRLs");
 
-  /* Determine certificate ingredients in advance.
-   */
+  /* Determine certificate ingredients in advance. */
   xs = X509_STORE_CTX_get_current_cert(ctx);
 
   subject = X509_get_subject_name(xs);
@@ -5886,9 +8866,11 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
   X509_STORE_CTX_init(store_ctx, tls_crl_store, NULL, NULL);
 #endif
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   crls = X509_STORE_CTX_get1_crls(store_ctx, subject);
-#elif OPENSSL_VERSION_NUMBER >= 0x10000000L
+#elif OPENSSL_VERSION_NUMBER >= 0x10000000L && \
+      !defined(HAVE_LIBRESSL)
   crls = X509_STORE_get1_crls(store_ctx, subject);
 #else
   /* Your OpenSSL is before 1.0.0.  You really need to upgrade. */
@@ -5902,6 +8884,7 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
       int len;
       BIO *b = BIO_new(BIO_s_mem());
 
+      crl = sk_X509_CRL_value(crls, i);
       BIO_printf(b, "CA CRL: Issuer: ");
       X509_NAME_print(b, issuer, 0);
 
@@ -5920,7 +8903,7 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
 #endif /* OpenSSL-1.1.x and later */
 
       len = BIO_read(b, buf, sizeof(buf) - 1);
-      if (len >= sizeof(buf)) {
+      if ((size_t) len >= sizeof(buf)) {
         len = sizeof(buf)-1;
       }
       buf[len] = '\0';
@@ -5983,9 +8966,11 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
    * the current certificate in order to check for revocation.
    */
 
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   crls = X509_STORE_CTX_get1_crls(store_ctx, subject);
-#elif OPENSSL_VERSION_NUMBER >= 0x10000000L
+#elif OPENSSL_VERSION_NUMBER >= 0x10000000L && \
+      !defined(HAVE_LIBRESSL)
   crls = X509_STORE_get1_crls(store_ctx, subject);
 #else
   /* Your OpenSSL is before 1.0.0.  You really need to upgrade. */
@@ -6004,8 +8989,9 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
         X509_REVOKED *revoked;
         ASN1_INTEGER *sn;
 
-        revoked = sk_X509_REVOKED_value(X509_CRL_get_REVOKED(crl), i);
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+        revoked = sk_X509_REVOKED_value(X509_CRL_get_REVOKED(crl), j);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
         sn = X509_REVOKED_get0_serialNumber(revoked);
 #else
         sn = revoked->serialNumber;
@@ -6035,23 +9021,21 @@ static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
   return ok;
 }
 
-#if OPENSSL_VERSION_NUMBER > 0x000907000L
+#if OPENSSL_VERSION_NUMBER > 0x000907000L && defined(PR_USE_OPENSSL_OCSP)
 static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
     const char *url) {
   BIO *conn;
   X509 *issuing_cert = NULL;
-  X509_STORE *store = NULL;
   X509_NAME *subj = NULL;
+  X509_STORE *store = NULL;
   const char *subj_name;
   char *host = NULL, *port = NULL, *uri = NULL;
-  int ok = FALSE, res = 0 , use_ssl = 0, ocsp_status, ocsp_cert_status,
-    ocsp_reason;
+  int ok = FALSE, res = 0 , use_ssl = 0;
+  int ocsp_status, ocsp_cert_status, ocsp_reason;
   OCSP_REQUEST *req = NULL;
-  OCSP_CERTID *cert_id = NULL;
   OCSP_RESPONSE *resp = NULL;
   OCSP_BASICRESP *basic_resp = NULL;
   SSL_CTX *ocsp_ssl_ctx = NULL;
-  ASN1_GENERALIZEDTIME *revtime, *thisupd, *nextupd;
 
   if (cert == NULL ||
       url == NULL) {
@@ -6063,32 +9047,14 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
 
   tls_log("checking OCSP URL '%s' for client cert '%s'", url, subj_name);
 
+  /* Current OpenSSL implementation of OCSP_parse_url() guarantees that
+   * host, port, and uri will never be NULL.  Nice.
+   */
   if (OCSP_parse_url((char *) url, &host, &port, &uri, &use_ssl) != 1) {
     tls_log("error parsing OCSP URL '%s': %s", url, tls_get_errors());
     return FALSE;
   }
 
-  /* XXX Need to check for NULL host, uri. */
-
-  if (port == NULL) {
-    if (use_ssl == 1) {
-#ifdef OPENSSL_strdup
-      port = OPENSSL_strdup("443");
-#else
-      port = OPENSSL_malloc(4);
-      sstrncpy(port, "443", 3);
-#endif /* OPENSSL_strdup */
-
-    } else {
-#ifdef OPENSSL_strdup
-      port = OPENSSL_strdup("80");
-#else
-      port = OPENSSL_malloc(3);
-      sstrncpy(port, "80", 2);
-#endif /* OPENSSL_strdup */
-    }
-  }
-
   tls_log("connecting to OCSP responder at host '%s', port '%s', URI '%s'%s",
     host, port, uri, use_ssl ? ", using SSL/TLS" : "");
 
@@ -6127,8 +9093,8 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
     }
   }
 
-  res = BIO_do_connect(conn);
-  if (res != 1) {
+  res = ocsp_connect(session.pool, conn, 0);
+  if (res < 0) {
     tls_log("error connecting to OCSP URL '%s': %s", url, tls_get_errors());
 
     if (ocsp_ssl_ctx != NULL) {
@@ -6170,53 +9136,12 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
     return FALSE;
   }
 
-  /* Note that the cert_id value will be freed when the request is freed. */
-  cert_id = OCSP_cert_to_id(NULL, cert, issuing_cert);
-  if (cert_id == NULL) {
-    const char *issuer_subj_name = tls_x509_name_oneline(
-      X509_get_subject_name(issuing_cert));
-
-    tls_log("error converting client cert '%s' and its issuing cert '%s' "
-      "to an OCSP cert ID: %s", subj_name, issuer_subj_name, tls_get_errors());
-
-    if (ocsp_ssl_ctx != NULL) {
-      SSL_CTX_free(ocsp_ssl_ctx);
-    }
-
-    X509_free(issuing_cert);
-    BIO_free_all(conn);
-    OPENSSL_free(host);
-    OPENSSL_free(port);
-    OPENSSL_free(uri);
-
-    return FALSE;
-  }
-
-  req = OCSP_REQUEST_new();
+  req = ocsp_get_request(session.pool, cert, issuing_cert);
   if (req == NULL) {
-    tls_log("unable to allocate OCSP request: %s", tls_get_errors());
-
-    if (ocsp_ssl_ctx != NULL) {
-      SSL_CTX_free(ocsp_ssl_ctx);
-    }
-
-    X509_free(issuing_cert);
-    BIO_free_all(conn);
-    OPENSSL_free(host);
-    OPENSSL_free(port);
-    OPENSSL_free(uri);
-
-    return FALSE;
-  }
-
-  if (OCSP_request_add0_id(req, cert_id) == NULL) {
-    tls_log("error adding cert ID to OCSP request: %s", tls_get_errors());
-
     if (ocsp_ssl_ctx != NULL) {
       SSL_CTX_free(ocsp_ssl_ctx);
     }
 
-    OCSP_REQUEST_free(req);
     X509_free(issuing_cert);
     BIO_free_all(conn);
     OPENSSL_free(host);
@@ -6249,24 +9174,6 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
   }
 # endif
 
-  res = OCSP_request_add1_nonce(req, NULL, 0);
-  if (res != 1) {
-    tls_log("error adding nonce to OCSP request: %s", tls_get_errors());
-
-    if (ocsp_ssl_ctx != NULL) {
-      SSL_CTX_free(ocsp_ssl_ctx);
-    }
-
-    OCSP_REQUEST_free(req);
-    X509_free(issuing_cert);
-    BIO_free_all(conn);
-    OPENSSL_free(host);
-    OPENSSL_free(port);
-    OPENSSL_free(uri);
-
-    return FALSE;
-  }
-
   if (tls_opts & TLS_OPT_ENABLE_DIAGS) {
     BIO *bio;
 
@@ -6287,8 +9194,7 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
     }
   }
 
-  resp = OCSP_sendreq_bio(conn, uri, req);
-
+  resp = ocsp_send_request(session.pool, conn, host, uri, req, 0);
   if (resp == NULL) {
     tls_log("error receiving response from OCSP responder at '%s': %s", url,
       tls_get_errors());
@@ -6350,28 +9256,8 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
     return FALSE;
   }
 
-  res = OCSP_check_nonce(req, basic_resp);
-  if (res != 1) {
-    tls_log("unable to use response from OCSP responder at '%s': bad nonce",
-      url);
-
-    if (ocsp_ssl_ctx != NULL) {
-      SSL_CTX_free(ocsp_ssl_ctx);
-    }
-
-    OCSP_BASICRESP_free(basic_resp);
-    OCSP_RESPONSE_free(resp);
-    OCSP_REQUEST_free(req);
-    X509_free(issuing_cert);
-    BIO_free_all(conn);
-    OPENSSL_free(host);
-    OPENSSL_free(port);
-    OPENSSL_free(uri);
-
-    return FALSE;
-  }
-
-#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
   store = X509_STORE_CTX_get0_store(ctx);
 #else
   store = ctx->ctx;
@@ -6436,41 +9322,18 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
          * implementation (e.g. SIGREQUIRED, UNAUTHORIZED).
          */
         ok = TRUE;
-        break;
-
-      default:
-        ok = FALSE;
-    }
-
-    return ok;
-  }
-
-  res = OCSP_resp_find_status(basic_resp, cert_id, &ocsp_cert_status,
-    &ocsp_reason, &revtime, &thisupd, &nextupd);
-  if (res != 1) {
-    tls_log("unable to retrieve cert status from OCSP response: %s",
-      tls_get_errors());
-
-    if (ocsp_ssl_ctx != NULL) {
-      SSL_CTX_free(ocsp_ssl_ctx);
-    }
-
-    OCSP_REQUEST_free(req);
-    OCSP_BASICRESP_free(basic_resp);
-    OCSP_RESPONSE_free(resp);
-    X509_free(issuing_cert);
-    BIO_free_all(conn);
-    OPENSSL_free(host);
-    OPENSSL_free(port);
-    OPENSSL_free(uri);
+        break;
 
-    return FALSE;
+      default:
+        ok = FALSE;
+    }
+
+    return ok;
   }
 
-  /* Check the validity of the timestamps on the response. */
-  res = OCSP_check_validity(thisupd, nextupd, TLS_OCSP_RESP_MAX_AGE_SECS, -1);
-  if (res != 1) {
-    tls_log("unable validate OCSP response timestamps: %s",
+  if (ocsp_check_cert_status(session.pool, cert, issuing_cert, basic_resp,
+      &ocsp_cert_status, &ocsp_reason) < 0) {
+    tls_log("unable to retrieve cert status from OCSP response: %s",
       tls_get_errors());
 
     if (ocsp_ssl_ctx != NULL) {
@@ -6536,8 +9399,8 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
 #endif
 
 static int tls_verify_ocsp(int ok, X509_STORE_CTX *ctx) {
-#if OPENSSL_VERSION_NUMBER > 0x000907000L
-  register unsigned int i;
+#if OPENSSL_VERSION_NUMBER > 0x000907000L && defined(PR_USE_OPENSSL_OCSP)
+  register int i;
   X509 *cert;
   const char *subj;
   STACK_OF(ACCESS_DESCRIPTION) *descs;
@@ -6564,8 +9427,9 @@ static int tls_verify_ocsp(int ok, X509_STORE_CTX *ctx) {
   }
 
   for (i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) {
-    ACCESS_DESCRIPTION *desc = sk_ACCESS_DESCRIPTION_value(descs, i);
+    ACCESS_DESCRIPTION *desc;
 
+    desc = sk_ACCESS_DESCRIPTION_value(descs, i);
     if (OBJ_obj2nid(desc->method) == NID_ad_OCSP) {
       /* Found an OCSP AuthorityInfoAccess attribute */
 
@@ -6596,7 +9460,7 @@ static int tls_verify_ocsp(int ok, X509_STORE_CTX *ctx) {
     "'%s'", ocsp_urls->nelts, ocsp_urls->nelts != 1 ? "URLs" : "URL", subj);
 
   /* Check each of the URLs. */
-  for (i = 0; i < ocsp_urls->nelts; i++) {
+  for (i = 0; i < (int) ocsp_urls->nelts; i++) {
     char *url = ((char **) ocsp_urls->elts)[i];
 
     ok = tls_verify_ocsp_url(ctx, cert, url);
@@ -6617,7 +9481,6 @@ static ssize_t tls_write(SSL *ssl, const void *buf, size_t len) {
   ssize_t count;
 
   count = SSL_write(ssl, buf, len);
-
   if (count < 0) {
     long err = SSL_get_error(ssl, count);
 
@@ -6637,6 +9500,35 @@ static ssize_t tls_write(SSL *ssl, const void *buf, size_t len) {
     }
   }
 
+  if (ssl != ctrl_ssl) {
+    BIO *wbio;
+    uint64_t now;
+
+    (void) pr_gettimeofday_millis(&now);
+    tls_data_adaptive_bytes_written_count += count;
+    wbio = SSL_get_wbio(ssl);
+
+    if (tls_data_adaptive_bytes_written_count >= TLS_DATA_ADAPTIVE_WRITE_BOOST_THRESHOLD) {
+      /* Boost the buffer size if we've written more than the "boost"
+       * threshold.
+       */
+      (void) BIO_set_write_buf_size(wbio,
+        TLS_DATA_ADAPTIVE_WRITE_MAX_BUFFER_SIZE);
+    }
+
+    if (now > (tls_data_adaptive_bytes_written_ms + TLS_DATA_ADAPTIVE_WRITE_BOOST_INTERVAL_MS)) {
+      /* If it's been longer than the boost interval since our last write,
+       * then reset the buffer size to the smaller version, assuming
+       * congestion (and thus closing of the TCP congestion window).
+       */
+      tls_data_adaptive_bytes_written_count = 0;
+      (void) BIO_set_write_buf_size(wbio,
+        TLS_DATA_ADAPTIVE_WRITE_MIN_BUFFER_SIZE);
+    }
+
+    tls_data_adaptive_bytes_written_ms = now;
+  }
+
   return count;
 }
 
@@ -6665,7 +9557,7 @@ static char *tls_x509_name_oneline(X509_NAME *x509_name) {
     if (data) {
       memset(&buf, '\0', sizeof(buf));
 
-      if (datalen >= sizeof(buf)) {
+      if ((size_t) datalen >= sizeof(buf)) {
         datalen = sizeof(buf)-1;
       }
 
@@ -6884,8 +9776,252 @@ static int tls_sess_cache_status(pr_ctrls_t *ctrl, int flags) {
   pr_ctrls_add_response(ctrl, "No TLSSessionCache configured");
   return res;
 }
+#endif /* PR_USE_CTRLS */
+
+/* OCSP response cache API */
+
+struct tls_ocache {
+  struct tls_ocache *next, *prev;
+
+  const char *name;
+  tls_ocsp_cache_t *cache;
+};
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static pool *tls_ocsp_cache_pool = NULL;
+static struct tls_ocache *tls_ocsp_caches = NULL;
+static unsigned int tls_ocsp_ncaches = 0;
+#endif /* PR_USE_OPENSSL_OCSP */
+
+int tls_ocsp_cache_register(const char *name, tls_ocsp_cache_t *cache) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  struct tls_ocache *oc;
+
+  if (name == NULL ||
+      cache == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (tls_ocsp_cache_pool == NULL) {
+    tls_ocsp_cache_pool = make_sub_pool(permanent_pool);
+    pr_pool_tag(tls_ocsp_cache_pool, "TLS OCSP Response Cache API Pool");
+  }
+
+  /* Make sure this cache has not already been registered. */
+  if (tls_ocsp_cache_get_cache(name) != NULL) {
+    errno = EEXIST;
+    return -1;
+  }
+
+  oc = pcalloc(tls_ocsp_cache_pool, sizeof(struct tls_ocache));
+
+  /* XXX Should this name string be dup'd from the tls_ocsp_cache_pool? */
+  oc->name = name;
+  cache->cache_name = pstrdup(tls_ocsp_cache_pool, name);
+  oc->cache = cache;
+
+  if (tls_ocsp_caches != NULL) {
+    oc->next = tls_ocsp_caches;
+
+  } else {
+    oc->next = NULL;
+  }
+
+  tls_ocsp_caches = oc;
+  tls_ocsp_ncaches++;
+
+  return 0;
+#else
+  errno = ENOSYS;
+  return -1;
+#endif /* PR_USE_OPENSSL_OCSP */
+}
+
+int tls_ocsp_cache_unregister(const char *name) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  struct tls_ocache *oc;
+
+  if (name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  for (oc = tls_ocsp_caches; oc; oc = oc->next) {
+    if (strcmp(oc->name, name) == 0) {
+
+      if (oc->prev) {
+        oc->prev->next = oc->next;
+
+      } else {
+        /* If prev is NULL, this is the head of the list. */
+        tls_ocsp_caches = oc->next;
+      }
+
+      if (oc->next) {
+        oc->next->prev = oc->prev;
+      }
+
+      oc->next = oc->prev = NULL;
+      tls_ocsp_ncaches--;
+
+      /* If the OCSP response cache being unregistered is in use, update the
+       * ocsp-cache-in-use pointer.
+       */
+      if (oc->cache == tls_ocsp_cache) {
+        tls_ocsp_cache_close();
+        tls_ocsp_cache = NULL;
+      }
+
+      /* NOTE: a counter should be kept of the number of unregistrations,
+       * as the memory for a registration is not freed on unregistration.
+       */
+
+      return 0;
+    }
+  }
+
+  errno = ENOENT;
+  return -1;
+#else
+  errno = ENOSYS;
+  return -1;
+#endif /* PR_USE_OPENSSL_OCSP */
+}
+
+static tls_ocsp_cache_t *tls_ocsp_cache_get_cache(const char *name) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  struct tls_ocache *oc;
+
+  if (name == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  for (oc = tls_ocsp_caches; oc; oc = oc->next) {
+    if (strcmp(oc->name, name) == 0) {
+      return oc->cache;
+    }
+  }
+
+  errno = ENOENT;
+  return NULL;
+#else
+  errno = ENOSYS;
+  return NULL;
+#endif /* PR_USE_OPENSSL_OCSP */
+}
+
+static int tls_ocsp_cache_open(char *info) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  int res;
+
+  if (tls_ocsp_cache == NULL) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  res = (tls_ocsp_cache->open)(tls_ocsp_cache, info);
+  return res;
+#else
+  errno = ENOSYS;
+  return -1;
+#endif /* PR_USE_OPENSSL_OCSP */
+}
+
+static int tls_ocsp_cache_close(void) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  int res;
+
+  if (tls_ocsp_cache == NULL) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  res = (tls_ocsp_cache->close)(tls_ocsp_cache);
+  return res;
+#else
+  errno = ENOSYS;
+  return -1;
+#endif /* PR_USE_OPENSSL_OCSP */
+}
+
+#ifdef PR_USE_CTRLS
+static int tls_ocsp_cache_clear(void) {
+# if defined(PR_USE_OPENSSL_OCSP)
+  int res;
+
+  if (tls_ocsp_cache == NULL) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  res = (tls_ocsp_cache->clear)(tls_ocsp_cache);
+  return res;
+# else
+  errno = ENOSYS;
+  return -1;
+# endif /* PR_USE_OPENSSL_OCSP */
+}
+
+static int tls_ocsp_cache_remove(void) {
+# if defined(PR_USE_OPENSSL_OCSP)
+  int res;
+
+  if (tls_ocsp_cache == NULL) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  res = (tls_ocsp_cache->remove)(tls_ocsp_cache);
+  return res;
+# else
+  errno = ENOSYS;
+  return -1;
+# endif /* PR_USE_OPENSSL_OCSP */
+}
+
+# if defined(PR_USE_OPENSSL_OCSP)
+static void ocsp_cache_printf(void *ctrl, const char *fmt, ...) {
+  char buf[PR_TUNABLE_BUFFER_SIZE];
+  va_list msg;
+
+  memset(buf, '\0', sizeof(buf));
+
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf), fmt, msg);
+  va_end(msg);
+
+  buf[sizeof(buf)-1] = '\0';
+  pr_ctrls_add_response(ctrl, "%s", buf);
+}
+# endif /* PR_USE_OPENSSL_OCSP */
+
+static int tls_ocsp_cache_status(pr_ctrls_t *ctrl, int flags) {
+# if defined(PR_USE_OPENSSL_OCSP)
+  int res = 0;
+
+  if (tls_ocsp_cache != NULL) {
+    res = (tls_ocsp_cache->status)(tls_ocsp_cache, ocsp_cache_printf, ctrl,
+      flags);
+    return res;
+  }
+
+  pr_ctrls_add_response(ctrl, "No TLSStaplingCache configured");
+  return res;
+# else
+  errno = ENOSYS;
+  return -1;
+# endif /* PR_USE_OPENSSL_OCSP */
+}
+#endif /* PR_USE_CTRLS */
+
+/* Controls
+ */
 
-static int tls_handle_clear(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
+#ifdef PR_USE_CTRLS
+static int tls_handle_sesscache_clear(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
   int res;
 
   res = tls_sess_cache_clear();
@@ -6903,7 +10039,8 @@ static int tls_handle_clear(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
   return res;
 }
 
-static int tls_handle_info(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
+static int tls_handle_sesscache_info(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
   int flags = 0, optc, res;
   const char *opts = "v";
 
@@ -6925,7 +10062,96 @@ static int tls_handle_info(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
   res = tls_sess_cache_status(ctrl, flags);
   if (res < 0) {
     pr_ctrls_add_response(ctrl,
-      "tls sesscache: error obtaining session cache status: %s",
+      "tls sesscache: error obtaining session cache status: %s",
+      strerror(errno));
+
+  } else {
+    res = 0;
+  }
+
+  return res;
+}
+
+static int tls_handle_sesscache_remove(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+  int res;
+
+  res = tls_sess_cache_remove();
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl,
+      "tls sesscache: error removing session cache: %s", strerror(errno));
+
+  } else {
+    pr_ctrls_add_response(ctrl, "tls sesscache: removed '%s' session cache",
+      tls_sess_cache->cache_name);
+    res = 0;
+  }
+
+  return res;
+}
+
+static int tls_handle_sesscache(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
+
+  /* Sanity check */
+  if (reqargc == 0 ||
+      reqargv == NULL) {
+    pr_ctrls_add_response(ctrl, "tls sesscache: missing required parameters");
+    return -1;
+  }
+
+  if (strncmp(reqargv[0], "info", 5) == 0) {
+    /* Check the ACLs. */
+    if (!pr_ctrls_check_acl(ctrl, tls_acttab, "info")) {
+      pr_ctrls_add_response(ctrl, "access denied");
+      return -1;
+    }
+
+    return tls_handle_sesscache_info(ctrl, reqargc, reqargv);
+
+  } else if (strncmp(reqargv[0], "clear", 6) == 0) {
+    /* Check the ACLs. */
+    if (!pr_ctrls_check_acl(ctrl, tls_acttab, "clear")) {
+      pr_ctrls_add_response(ctrl, "access denied");
+      return -1;
+    }
+
+    return tls_handle_sesscache_clear(ctrl, reqargc, reqargv);
+
+  } else if (strncmp(reqargv[0], "remove", 7) == 0) {
+    /* Check the ACLs. */
+    if (!pr_ctrls_check_acl(ctrl, tls_acttab, "remove")) {
+      pr_ctrls_add_response(ctrl, "access denied");
+      return -1;
+    }
+
+    return tls_handle_sesscache_remove(ctrl, reqargc, reqargv);
+  }
+
+  pr_ctrls_add_response(ctrl, "tls sesscache: unknown sesscache action: '%s'",
+    reqargv[0]);
+  return -1;
+}
+
+static int tls_handle_ocspcache_info(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+  int flags = 0, optc, res;
+  const char *opts = "v";
+
+  pr_getopt_reset();
+
+  while ((optc = getopt(reqargc, reqargv, opts)) != -1) {
+    switch (optc) {
+      case '?':
+        pr_ctrls_add_response(ctrl,
+          "tls ocspcache: unsupported parameter: '%s'", reqargv[1]);
+        return -1;
+    }
+  }
+
+  res = tls_ocsp_cache_status(ctrl, flags);
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl,
+      "tls ocspcache: error obtaining OCSP cache status: %s",
       strerror(errno));
 
   } else {
@@ -6935,64 +10161,80 @@ static int tls_handle_info(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
   return res;
 }
 
-static int tls_handle_remove(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
+static int tls_handle_ocspcache_clear(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
   int res;
 
-  res = tls_sess_cache_remove();
+  res = tls_ocsp_cache_clear();
   if (res < 0) {
     pr_ctrls_add_response(ctrl,
-      "tls sesscache: error removing session cache: %s", strerror(errno));
+      "tls ocspcache: error clearing OCSP cache: %s", strerror(errno));
 
   } else {
-    pr_ctrls_add_response(ctrl, "tls sesscache: removed '%s' session cache",
-      tls_sess_cache->cache_name);
+    pr_ctrls_add_response(ctrl, "tls ocspcache: cleared %d %s from '%s' "
+      "OCSP cache", res, res != 1 ? "responses" : "response",
+      tls_ocsp_cache->cache_name);
     res = 0;
   }
 
   return res;
 }
 
-static int tls_handle_sesscache(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
+static int tls_handle_ocspcache_remove(pr_ctrls_t *ctrl, int reqargc,
+    char **reqargv) {
+  int res;
+
+  res = tls_ocsp_cache_remove();
+  if (res < 0) {
+    pr_ctrls_add_response(ctrl,
+      "tls ocspcache: error removing OCSP cache: %s", strerror(errno));
+
+  } else {
+    pr_ctrls_add_response(ctrl, "tls sesscache: removed '%s' OCSP cache",
+      tls_ocsp_cache->cache_name);
+    res = 0;
+  }
+
+  return res;
+}
 
+static int tls_handle_ocspcache(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
   /* Sanity check */
   if (reqargc == 0 ||
       reqargv == NULL) {
-    pr_ctrls_add_response(ctrl, "tls sesscache: missing required parameters");
+    pr_ctrls_add_response(ctrl, "tls ocspcache: missing required parameters");
     return -1;
   }
 
   if (strncmp(reqargv[0], "info", 5) == 0) {
-
     /* Check the ACLs. */
     if (!pr_ctrls_check_acl(ctrl, tls_acttab, "info")) {
       pr_ctrls_add_response(ctrl, "access denied");
       return -1;
     }
 
-    return tls_handle_info(ctrl, reqargc, reqargv);
+    return tls_handle_ocspcache_info(ctrl, reqargc, reqargv);
 
   } else if (strncmp(reqargv[0], "clear", 6) == 0) {
-
     /* Check the ACLs. */
     if (!pr_ctrls_check_acl(ctrl, tls_acttab, "clear")) {
       pr_ctrls_add_response(ctrl, "access denied");
       return -1;
     }
 
-    return tls_handle_clear(ctrl, reqargc, reqargv);
+    return tls_handle_ocspcache_clear(ctrl, reqargc, reqargv);
 
   } else if (strncmp(reqargv[0], "remove", 7) == 0) {
-
     /* Check the ACLs. */
     if (!pr_ctrls_check_acl(ctrl, tls_acttab, "remove")) {
       pr_ctrls_add_response(ctrl, "access denied");
       return -1;
     }
 
-    return tls_handle_remove(ctrl, reqargc, reqargv);
+    return tls_handle_ocspcache_remove(ctrl, reqargc, reqargv);
   }
 
-  pr_ctrls_add_response(ctrl, "tls sesscache: unknown sesscache action: '%s'",
+  pr_ctrls_add_response(ctrl, "tls ocspcache: unknown ocspcache action: '%s'",
     reqargv[0]);
   return -1;
 }
@@ -7008,7 +10250,6 @@ static int tls_handle_tls(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
   }
 
   if (strncmp(reqargv[0], "sesscache", 10) == 0) {
-
     /* Check the ACLs. */
     if (!pr_ctrls_check_acl(ctrl, tls_acttab, "sesscache")) {
       pr_ctrls_add_response(ctrl, "access denied");
@@ -7018,11 +10259,24 @@ static int tls_handle_tls(pr_ctrls_t *ctrl, int reqargc, char **reqargv) {
     return tls_handle_sesscache(ctrl, --reqargc, ++reqargv);
   }
 
+  if (strncmp(reqargv[0], "ocspcache", 10) == 0) {
+    /* Check the ACLs. */
+    if (!pr_ctrls_check_acl(ctrl, tls_acttab, "ocspcache")) {
+      pr_ctrls_add_response(ctrl, "access denied");
+      return -1;
+    }
+
+    return tls_handle_ocspcache(ctrl, --reqargc, ++reqargv);
+  }
+
   pr_ctrls_add_response(ctrl, "tls: unknown tls action: '%s'", reqargv[0]);
   return -1;
 }
 #endif
 
+/* TLSSessionCache callbacks
+ */
+
 static int tls_sess_cache_add_sess_cb(SSL *ssl, SSL_SESSION *sess) {
   unsigned char *sess_id;
   unsigned int sess_id_len;
@@ -7078,9 +10332,12 @@ static int tls_sess_cache_add_sess_cb(SSL *ssl, SSL_SESSION *sess) {
   return 0;
 }
 
-static SSL_SESSION *tls_sess_cache_get_sess_cb(SSL *ssl,
-    const unsigned char *sess_id, int sess_id_len, int *do_copy) {
+static SSL_SESSION *tls_sess_cache_get_sess_cb(SSL *ssl, unsigned char *id,
+    int sess_id_len, int *do_copy) {
   SSL_SESSION *sess;
+  const unsigned char *sess_id;
+
+  sess_id = id;
 
   /* Indicate to OpenSSL that the ref count should not be incremented
    * by setting the do_copy pointer to zero.
@@ -7141,6 +10398,92 @@ static void tls_sess_cache_delete_sess_cb(SSL_CTX *ctx, SSL_SESSION *sess) {
   return;
 }
 
+/* Ideally we would use the OPENSSL_NO_PSK macro.  However, to use this, we
+ * would need to say "if !defined(OPENSSL_NO_PSK)".  And that does not work
+ * as well for older OpenSSL installations, where that macro would not be
+ * defined anyway.  So instead, we use the presence of another PSK-related
+ * macro as a more reliable sentinel.
+ */
+
+#if defined(PSK_MAX_PSK_LEN)
+/* PSK callbacks */
+
+static int set_random_bn(unsigned char *psk, unsigned int max_psklen) {
+  BIGNUM *bn = NULL;
+  int res = 0;
+
+  bn = BN_new();
+  if (BN_pseudo_rand(bn, max_psklen, 0, 0) != 1) {
+    tls_log("error generating pseudo-random number: %s",
+      ERR_error_string(ERR_get_error(), NULL));
+  }
+
+  res = BN_bn2bin(bn, psk);
+  BN_free(bn);
+
+  return res;
+}
+
+static unsigned int tls_lookup_psk(SSL *ssl, const char *identity,
+    unsigned char *psk, unsigned int max_psklen) {
+  const void *v = NULL;
+  BIGNUM *bn = NULL;
+  int bn_len = -1, res;
+
+  if (identity == NULL) {
+    tls_log("%s", "error: client did not provide PSK identity name, providing "
+      "random fake PSK");
+
+    res = set_random_bn(psk, max_psklen);
+    return res;
+  }
+
+  pr_trace_msg(trace_channel, 5,
+    "PSK lookup: identity '%s' requested", identity);
+
+  if (tls_psks == NULL) {
+    tls_log("warning: no pre-shared keys configured, providing random fake "
+      "PSK for identity '%s'", identity);
+
+    res = set_random_bn(psk, max_psklen);
+    return res;
+  }
+
+  v = pr_table_get(tls_psks, identity, NULL);
+  if (v == NULL) {
+    tls_log("warning: requested PSK identity '%s' not configured, providing "
+      "random fake PSK", identity);
+
+    res = set_random_bn(psk, max_psklen);
+    return res;
+  }
+
+  bn = (BIGNUM *) v;
+  bn_len = BN_num_bytes(bn);
+
+  if (bn_len > (int) max_psklen) {
+    tls_log("warning: unable to use '%s' PSK: max buffer size (%u bytes) "
+      "too small for key (%d bytes), providing random fake PSK", identity,
+      max_psklen, bn_len);
+
+    res = set_random_bn(psk, max_psklen);
+    return res;
+  }
+
+  res = BN_bn2bin(bn, psk); 
+  if (res == 0) {
+    tls_log("error converting PSK for identity '%s' to binary: %s",
+      identity, tls_get_errors());
+    return 0;
+  }
+
+  pr_trace_msg(trace_channel, 5,
+    "found PSK (%d bytes) for identity '%s'", res, identity);
+  return res;
+}
+
+#endif /* PSK_MAX_PSK_LEN */
+
 /* NetIO callbacks
  */
 
@@ -7152,11 +10495,11 @@ static int tls_netio_close_cb(pr_netio_stream_t *nstrm) {
   int res = 0;
   SSL *ssl = NULL;
 
-  ssl = pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
+  ssl = (SSL *) pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
   if (ssl != NULL) {
     if (nstrm->strm_type == PR_NETIO_STRM_CTRL &&
         nstrm->strm_mode == PR_NETIO_IO_WR) {
-      tls_end_sess(ssl, nstrm->strm_type, 0);
+      tls_end_sess(ssl, session.c, 0);
       pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
       pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
       tls_ctrl_netio = NULL;
@@ -7165,7 +10508,7 @@ static int tls_netio_close_cb(pr_netio_stream_t *nstrm) {
 
     if (nstrm->strm_type == PR_NETIO_STRM_DATA &&
         nstrm->strm_mode == PR_NETIO_IO_WR) {
-      tls_end_sess(ssl, nstrm->strm_type, 0);
+      tls_end_sess(ssl, session.d, 0);
       pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
       pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
       tls_data_netio = NULL;
@@ -7274,8 +10617,6 @@ static int tls_netio_postopen_cb(pr_netio_stream_t *nstrm) {
         (tls_flags & TLS_SESS_NEED_DATA_PROT)) {
       SSL *ssl = NULL;
 
-      ssl = pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
-
       /* XXX How to force 421 response code for failed secure FXP/SSCN? */
 
       /* Directory listings (LIST, MLSD, NLST) are ALWAYS handled in server
@@ -7286,16 +10627,32 @@ static int tls_netio_postopen_cb(pr_netio_stream_t *nstrm) {
           session.curr_cmd_id == PR_CMD_NLST_ID ||
           tls_sscn_mode == TLS_SSCN_MODE_SERVER) {
         X509 *ctrl_cert = NULL, *data_cert = NULL;
+        uint64_t start_ms;
+
+        pr_gettimeofday_millis(&start_ms);
 
         tls_log("%s", "starting TLS negotiation on data connection");
         tls_data_need_init_handshake = TRUE;
         if (tls_accept(session.d, TRUE) < 0) {
           tls_log("%s",
             "unable to open data connection: TLS negotiation failed");
-          session.d->xerrno = EPERM;
+          session.d->xerrno = errno = EPERM;
           return -1;
         }
 
+        if (pr_trace_get_level(timing_channel) >= 4) {
+          unsigned long elapsed_ms;
+          uint64_t finish_ms;
+
+          pr_gettimeofday_millis(&finish_ms);
+          elapsed_ms = (unsigned long) (finish_ms - start_ms);
+
+          pr_trace_msg(timing_channel, 4,
+            "TLS data handshake duration: %lu ms", elapsed_ms);
+        } 
+
+        ssl = (SSL *) pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
+
         /* Make sure that the certificate used, if any, for this data channel
          * handshake is the same as that used for the control channel handshake.
          * This may be too strict of a requirement, though.
@@ -7310,14 +10667,14 @@ static int tls_netio_postopen_cb(pr_netio_stream_t *nstrm) {
             X509_free(data_cert);
 
             /* Properly shutdown the SSL session. */
-            tls_end_sess(ssl, nstrm->strm_type, 0);
+            tls_end_sess(ssl, session.d, 0);
             pr_table_remove(tls_data_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
             pr_table_remove(tls_data_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
 
             tls_log("%s", "unable to open data connection: control/data "
               "certificate mismatch");
 
-            session.d->xerrno = EPERM;
+            session.d->xerrno = errno = EPERM;
             return -1;
           }
 
@@ -7335,7 +10692,7 @@ static int tls_netio_postopen_cb(pr_netio_stream_t *nstrm) {
         if (tls_connect(session.d) < 0) {
           tls_log("%s",
             "unable to open data connection: TLS connection failed");
-          session.d->xerrno = EPERM;
+          session.d->xerrno = errno = EPERM;
           return -1;
         }
      }
@@ -7358,7 +10715,7 @@ static int tls_netio_read_cb(pr_netio_stream_t *nstrm, char *buf,
     size_t buflen) {
   SSL *ssl;
 
-  ssl = pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
+  ssl = (SSL *) pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
   if (ssl != NULL) {
     BIO *rbio, *wbio;
     int bread = 0, bwritten = 0;
@@ -7384,7 +10741,9 @@ static int tls_netio_read_cb(pr_netio_stream_t *nstrm, char *buf,
      * the raw bytes read in versus the non-SSL bytes read in, in order to
      * have %I be accurately represented for the raw traffic.
      */
-    session.total_raw_in += (bread - res);
+    if (res > 0) {
+      session.total_raw_in += (bread - res);
+    }
 
     /* Manually update session.total_raw_out, in order to have %O be
      * accurately represented for the raw traffic.
@@ -7427,11 +10786,12 @@ static int tls_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
          nstrm->strm_type == PR_NETIO_STRM_DATA)) {
       SSL *ssl;
 
-      ssl = pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
+      ssl = (SSL *) pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
       if (ssl != NULL) {
         BIO *rbio, *wbio;
         int bread = 0, bwritten = 0;
         unsigned long rbio_rbytes, rbio_wbytes, wbio_rbytes, wbio_wbytes;
+        conn_t *conn;
 
         rbio = SSL_get_rbio(ssl);
         rbio_rbytes = BIO_number_read(rbio);
@@ -7442,6 +10802,25 @@ static int tls_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
         wbio_wbytes = BIO_number_written(wbio);
 
         if (!(SSL_get_shutdown(ssl) & SSL_SENT_SHUTDOWN)) {
+
+          /* Disable any socket buffering (Nagle, TCP_CORK), so that the alert
+           * is sent in a timely manner (avoiding TLS shutdown latency).
+           */
+          conn = (nstrm->strm_type == PR_NETIO_STRM_DATA) ? session.d :
+            session.c;
+          if (conn != NULL) {
+            if (pr_inet_set_proto_nodelay(conn->pool, conn, 1) < 0) {
+              pr_trace_msg(trace_channel, 9,
+                "error enabling TCP_NODELAY on conn: %s", strerror(errno));
+            }
+
+            if (pr_inet_set_proto_cork(conn->wfd, 0) < 0) {
+              pr_trace_msg(trace_channel, 9,
+                "error disabling TCP_CORK on fd %d: %s", conn->wfd,
+                strerror(errno));
+            }
+          }
+
           /* We haven't sent a 'close_notify' alert yet; do so now. */
           SSL_shutdown(ssl);
         }
@@ -7461,6 +10840,10 @@ static int tls_netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
         if (bwritten > 0) {
           session.total_raw_out += bwritten;
         }
+
+      } else {
+        pr_trace_msg(trace_channel, 3,
+          "no SSL found in stream notes for '%s'", TLS_NETIO_NOTE);
       }
     }
   }
@@ -7472,7 +10855,7 @@ static int tls_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
     size_t buflen) {
   SSL *ssl;
 
-  ssl = pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
+  ssl = (SSL *) pr_table_get(nstrm->notes, TLS_NETIO_NOTE, NULL);
   if (ssl != NULL) {
     BIO *rbio, *wbio;
     int bread = 0, bwritten = 0;
@@ -7533,7 +10916,9 @@ static int tls_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
      * the raw bytes written out versus the non-SSL bytes written out,
      * in order to have %) be accurately represented for the raw traffic.
      */
-    session.total_raw_out += (bwritten - res);
+    if (res > 0) {
+      session.total_raw_out += (bwritten - res);
+    }
 
     return res;
   }
@@ -7551,7 +10936,7 @@ static void tls_netio_install_ctrl(void) {
     return;
   }
 
-  tls_ctrl_netio = netio = pr_alloc_netio2(permanent_pool, &tls_module);
+  tls_ctrl_netio = netio = pr_alloc_netio2(permanent_pool, &tls_module, NULL);
 
   netio->abort = tls_netio_abort_cb;
   netio->close = tls_netio_close_cb;
@@ -7574,7 +10959,7 @@ static void tls_netio_install_ctrl(void) {
 static void tls_netio_install_data(void) {
   pr_netio_t *netio = tls_data_netio ? tls_data_netio :
     (tls_data_netio = pr_alloc_netio2(session.pool ? session.pool :
-    permanent_pool, &tls_module));
+    permanent_pool, &tls_module, NULL));
 
   netio->abort = tls_netio_abort_cb;
   netio->close = tls_netio_close_cb;
@@ -7603,7 +10988,6 @@ static void tls_closelog(void) {
   if (tls_logfd != -1) {
     close(tls_logfd);
     tls_logfd = -1;
-    tls_logname = NULL;
   }
 
   return;
@@ -7614,8 +10998,9 @@ int tls_log(const char *fmt, ...) {
   int res;
 
   /* Sanity check */
-  if (!tls_logname)
+  if (tls_logfd < 0) {
     return 0;
+  }
 
   va_start(msg, fmt);
   res = pr_log_vwritefile(tls_logfd, MOD_TLS_VERSION, fmt, msg);
@@ -7626,20 +11011,18 @@ int tls_log(const char *fmt, ...) {
 
 static int tls_openlog(void) {
   int res = 0, xerrno;
+  char *path;
 
   /* Sanity checks */
-  tls_logname = get_param_ptr(main_server->conf, "TLSLog", FALSE);
-  if (tls_logname == NULL)
-    return 0;
-
-  if (strncasecmp(tls_logname, "none", 5) == 0) {
-    tls_logname = NULL;
+  path = get_param_ptr(main_server->conf, "TLSLog", FALSE);
+  if (path == NULL ||
+      strncasecmp(path, "none", 5) == 0) {
     return 0;
   }
 
   pr_signals_block();
   PRIVS_ROOT
-  res = pr_log_openfile(tls_logname, &tls_logfd, PR_LOG_SYSTEM_MODE);
+  res = pr_log_openfile(path, &tls_logfd, PR_LOG_SYSTEM_MODE);
   xerrno = errno;
   PRIVS_RELINQUISH
   pr_signals_unblock();
@@ -7674,14 +11057,15 @@ MODRET tls_authenticate(cmd_rec *cmd) {
     if (tls_opts & TLS_OPT_ALLOW_DOT_LOGIN) {
       if (tls_dotlogin_allow(cmd->argv[0])) {
         tls_log("TLS/X509 .tlslogin check successful for user '%s'",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         pr_log_auth(PR_LOG_NOTICE, "USER %s: TLS/X509 .tlslogin authentication "
-          "successful", cmd->argv[0]);
+          "successful", (char *) cmd->argv[0]);
         session.auth_mech = "mod_tls.c";
         return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
 
       } else {
-        tls_log("TLS/X509 .tlslogin check failed for user '%s'", cmd->argv[0]);
+        tls_log("TLS/X509 .tlslogin check failed for user '%s'",
+          (char *) cmd->argv[0]);
       }
     }
 
@@ -7692,7 +11076,7 @@ MODRET tls_authenticate(cmd_rec *cmd) {
           (char *) c->argv[0], (char *) cmd->argv[0]);
         pr_log_auth(PR_LOG_NOTICE,
           "USER %s: TLS/X509 TLSUserName authentication successful",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         session.auth_mech = "mod_tls.c";
         return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
 
@@ -7731,7 +11115,7 @@ MODRET tls_auth_check(cmd_rec *cmd) {
         tls_log("TLS/X509 .tlslogin check successful for user '%s'",
           (char *) cmd->argv[0]);
         pr_log_auth(PR_LOG_NOTICE, "USER %s: TLS/X509 .tlslogin authentication "
-          "successful", cmd->argv[1]);
+          "successful", (char *) cmd->argv[1]);
         session.auth_mech = "mod_tls.c";
         return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
 
@@ -7748,7 +11132,7 @@ MODRET tls_auth_check(cmd_rec *cmd) {
           (char *) c->argv[0], (char *) cmd->argv[0]);
         pr_log_auth(PR_LOG_NOTICE,
           "USER %s: TLS/X509 TLSUserName authentication successful",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         session.auth_mech = "mod_tls.c";
         return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
 
@@ -7773,6 +11157,7 @@ MODRET tls_any(cmd_rec *cmd) {
   if (pr_cmd_cmp(cmd, PR_CMD_SYST_ID) == 0 ||
       pr_cmd_cmp(cmd, PR_CMD_AUTH_ID) == 0 ||
       pr_cmd_cmp(cmd, PR_CMD_FEAT_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_HOST_ID) == 0 ||
       pr_cmd_cmp(cmd, PR_CMD_QUIT_ID) == 0) {
     return PR_DECLINED(cmd);
   }
@@ -7785,9 +11170,12 @@ MODRET tls_any(cmd_rec *cmd) {
           pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_ACCT_ID) == 0) {
         tls_log("SSL/TLS required but absent for authentication, "
-          "denying %s command", cmd->argv[0]);
+          "denying %s command", (char *) cmd->argv[0]);
         pr_response_add_err(R_550,
           _("SSL/TLS required on the control channel"));
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
     }
@@ -7798,8 +11186,11 @@ MODRET tls_any(cmd_rec *cmd) {
 
     if (!(tls_opts & TLS_OPT_ALLOW_PER_USER)) {
       tls_log("SSL/TLS required but absent on control channel, "
-        "denying %s command", cmd->argv[0]);
+        "denying %s command", (char *) cmd->argv[0]);
       pr_response_add_err(R_550, _("SSL/TLS required on the control channel"));
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
 
     } else {
@@ -7807,9 +11198,12 @@ MODRET tls_any(cmd_rec *cmd) {
       if (tls_authenticated &&
           *tls_authenticated == TRUE) {
         tls_log("SSL/TLS required but absent on control channel, "
-          "denying %s command", cmd->argv[0]);
+          "denying %s command", (char *) cmd->argv[0]);
         pr_response_add_err(R_550,
           _("SSL/TLS required on the control channel"));
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
     }
@@ -7832,8 +11226,11 @@ MODRET tls_any(cmd_rec *cmd) {
           pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_STOU_ID) == 0) {
         tls_log("SSL/TLS required but absent on data channel, "
-          "denying %s command", cmd->argv[0]);
+          "denying %s command", (char *) cmd->argv[0]);
         pr_response_add_err(R_522, _("SSL/TLS required on the data channel"));
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
     }
@@ -7867,10 +11264,14 @@ MODRET tls_any(cmd_rec *cmd) {
         if (tls_required == TRUE &&
             !(tls_flags & TLS_SESS_NEED_DATA_PROT)) {
           tls_log("%s command denied by TLSRequired in directory '%s'",
-            cmd->argv[0], session.dir_config ? session.dir_config->name :
-            session.anon_config ? session.anon_config->name :
-            main_server->ServerName);
+            (char *) cmd->argv[0],
+            session.dir_config ? session.dir_config->name :
+              session.anon_config ? session.anon_config->name :
+              main_server->ServerName);
           pr_response_add_err(R_522, _("SSL/TLS required on the data channel"));
+
+          pr_cmd_set_errno(cmd, EPERM);
+          errno = EPERM;
           return PR_ERROR(cmd);
         }
       }
@@ -7882,9 +11283,11 @@ MODRET tls_any(cmd_rec *cmd) {
 
 MODRET tls_auth(cmd_rec *cmd) {
   register unsigned int i = 0;
+  char *mode;
 
-  if (!tls_engine)
+  if (!tls_engine) {
     return PR_DECLINED(cmd);
+  }
 
   /* If we already have protection on the control channel (i.e. AUTH has
    * already been sent by the client and handled), then reject this second
@@ -7894,29 +11297,61 @@ MODRET tls_auth(cmd_rec *cmd) {
   if (tls_flags & TLS_SESS_ON_CTRL) {
     tls_log("Unwilling to accept AUTH after AUTH for this session");
     pr_response_add_err(R_503, _("Unwilling to accept second AUTH"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   if (cmd->argc < 2) {
     pr_response_add_err(R_504, _("AUTH requires at least one argument"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   if (tls_flags & TLS_SESS_HAVE_CCC) {
     tls_log("Unwilling to accept AUTH after CCC for this session");
     pr_response_add_err(R_534, _("Unwilling to accept security parameters"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  /* CAN we even handle an AUTH command?  If we do not have the necessary
+   * certificates, then we should indicate that we cannot.
+   */
+  if (tls_rsa_cert_file == NULL &&
+      tls_dsa_cert_file == NULL &&
+      tls_ec_cert_file == NULL &&
+      tls_pkcs12_file == NULL) {
+    tls_log("Unable to accept AUTH %s due to lack of certificates", cmd->arg);
+    pr_response_add_err(R_431, _("Necessary security resource unavailable"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   /* Convert the parameter to upper case */
-  for (i = 0; i < strlen(cmd->argv[1]); i++)
-    (cmd->argv[1])[i] = toupper((cmd->argv[1])[i]);
+  mode = cmd->argv[1];
+  for (i = 0; i < strlen(mode); i++) {
+    mode[i] = toupper(mode[i]);
+  }
 
-  if (strncmp(cmd->argv[1], "TLS", 4) == 0 ||
-      strncmp(cmd->argv[1], "TLS-C", 6) == 0) {
-    pr_response_send(R_234, _("AUTH %s successful"), cmd->argv[1]);
+  if (strncmp(mode, "TLS", 4) == 0 ||
+      strncmp(mode, "TLS-C", 6) == 0) {
+    uint64_t start_ms;
 
+    pr_response_send(R_234, _("AUTH %s successful"), (char *) cmd->argv[1]);
     tls_log("%s", "TLS/TLS-C requested, starting TLS handshake");
+
+    if (pr_trace_get_level(timing_channel) > 0) {
+      pr_gettimeofday_millis(&start_ms);
+    }
+
     pr_event_generate("mod_tls.ctrl-handshake", session.c);
     if (tls_accept(session.c, FALSE) < 0) {
       tls_log("%s", "TLS/TLS-C negotiation failed on control channel");
@@ -7945,11 +11380,32 @@ MODRET tls_auth(cmd_rec *cmd) {
 
     tls_flags |= TLS_SESS_ON_CTRL;
 
-  } else if (strncmp(cmd->argv[1], "SSL", 4) == 0 ||
-             strncmp(cmd->argv[1], "TLS-P", 6) == 0) {
-    pr_response_send(R_234, _("AUTH %s successful"), cmd->argv[1]);
+    if (pr_trace_get_level(timing_channel) >= 4) {
+      unsigned long elapsed_ms;
+      uint64_t finish_ms;
+
+      pr_gettimeofday_millis(&finish_ms);
+
+      elapsed_ms = (unsigned long) (finish_ms - session.connect_time_ms);
+      pr_trace_msg(timing_channel, 4,
+        "Time before TLS ctrl handshake: %lu ms", elapsed_ms);
 
+      elapsed_ms = (unsigned long) (finish_ms - start_ms);
+      pr_trace_msg(timing_channel, 4,
+        "TLS ctrl handshake duration: %lu ms", elapsed_ms);
+    }
+
+  } else if (strncmp(mode, "SSL", 4) == 0 ||
+             strncmp(mode, "TLS-P", 6) == 0) {
+    uint64_t start_ms;
+
+    pr_response_send(R_234, _("AUTH %s successful"), (char *) cmd->argv[1]);
     tls_log("%s", "SSL/TLS-P requested, starting TLS handshake");
+
+    if (pr_trace_get_level(timing_channel) > 0) {
+      pr_gettimeofday_millis(&start_ms);
+    }
+
     if (tls_accept(session.c, FALSE) < 0) {
       tls_log("%s", "SSL/TLS-P negotiation failed on control channel");
 
@@ -7978,8 +11434,23 @@ MODRET tls_auth(cmd_rec *cmd) {
     tls_flags |= TLS_SESS_ON_CTRL;
     tls_flags |= TLS_SESS_NEED_DATA_PROT;
 
+    if (pr_trace_get_level(timing_channel) >= 4) {
+      unsigned long elapsed_ms;
+      uint64_t finish_ms;
+
+      pr_gettimeofday_millis(&finish_ms);
+
+      elapsed_ms = (unsigned long) (finish_ms - session.connect_time_ms);
+      pr_trace_msg(timing_channel, 4,
+        "Time before TLS ctrl handshake: %lu ms", elapsed_ms);
+
+      elapsed_ms = (unsigned long) (finish_ms - start_ms);
+      pr_trace_msg(timing_channel, 4,
+        "TLS ctrl handshake duration: %lu ms", elapsed_ms);
+    }
+
   } else {
-    tls_log("AUTH %s unsupported, declining", cmd->argv[1]);
+    tls_log("AUTH %s unsupported, declining", (char *) cmd->argv[1]);
 
     /* Allow other RFC2228 modules a chance a handling this command. */
     return PR_DECLINED(cmd);
@@ -7995,12 +11466,16 @@ MODRET tls_ccc(cmd_rec *cmd) {
 
   if (!tls_engine ||
       !session.rfc2228_mech ||
-      strncmp(session.rfc2228_mech, "TLS", 4) != 0)
+      strncmp(session.rfc2228_mech, "TLS", 4) != 0) {
     return PR_DECLINED(cmd);
+  }
 
   if (!(tls_flags & TLS_SESS_ON_CTRL)) {
     pr_response_add_err(R_533,
       _("CCC not allowed on insecure control connection"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -8008,14 +11483,21 @@ MODRET tls_ccc(cmd_rec *cmd) {
     pr_response_add_err(R_534, _("Unwilling to accept security parameters"));
     tls_log("%s: unwilling to accept security parameters: "
       "TLSRequired setting does not allow for unprotected control channel",
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   /* Check for <Limit> restrictions. */
   if (!dir_check(cmd->tmp_pool, cmd, G_NONE, session.cwd, NULL)) {
     pr_response_add_err(R_534, _("Unwilling to accept security parameters"));
-    tls_log("%s: unwilling to accept security parameters", cmd->argv[0]);
+    tls_log("%s: unwilling to accept security parameters",
+      (char *) cmd->argv[0]);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -8030,7 +11512,7 @@ MODRET tls_ccc(cmd_rec *cmd) {
    * The data channel, if protected, should remain so.
    */
 
-  tls_end_sess(ctrl_ssl, PR_NETIO_STRM_CTRL, TLS_SHUTDOWN_BIDIRECTIONAL);
+  tls_end_sess(ctrl_ssl, session.c, TLS_SHUTDOWN_FL_BIDIRECTIONAL);
   pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
   pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
   ctrl_ssl = NULL;
@@ -8056,6 +11538,9 @@ MODRET tls_pbsz(cmd_rec *cmd) {
   if (!(tls_flags & TLS_SESS_ON_CTRL)) {
     pr_response_add_err(R_503,
       _("PBSZ not allowed on insecure control connection"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -8071,34 +11556,6 @@ MODRET tls_pbsz(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-MODRET tls_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-
-    /* HOST after AUTH?  Make the SNI check, close the connection if failed. */
-
-    /* HOST before AUTH?  Re-init mod_tls. */
-
-    /* XXX Has a TLS handshake already been performed?  If so, do some stuff. */
-    /* XXX Perform SNI check */
-
-#if 0
-    int res;
-
-    res = tls_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&tls_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
-#endif
-  }
-
-  return PR_DECLINED(cmd);
-}
-
 MODRET tls_post_pass(cmd_rec *cmd) {
   config_rec *protocols_config;
 
@@ -8190,11 +11647,13 @@ MODRET tls_post_pass(cmd_rec *cmd) {
 }
 
 MODRET tls_prot(cmd_rec *cmd) {
+  char *prot;
 
   if (!tls_engine ||
       !session.rfc2228_mech ||
-      strncmp(session.rfc2228_mech, "TLS", 4) != 0)
+      strncmp(session.rfc2228_mech, "TLS", 4) != 0) {
     return PR_DECLINED(cmd);
+  }
 
   CHECK_CMD_ARGS(cmd, 2);
 
@@ -8202,24 +11661,37 @@ MODRET tls_prot(cmd_rec *cmd) {
       !(tls_flags & TLS_SESS_HAVE_CCC)) {
     pr_response_add_err(R_503,
       _("PROT not allowed on insecure control connection"));
-    return PR_ERROR(cmd);
-  }
 
-  if (!(tls_flags & TLS_SESS_PBSZ_OK)) {
-    pr_response_add_err(R_503,
-      _("You must issue the PBSZ command prior to PROT"));
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
+  /* In theory, we could enforce the RFC 4217 semantics, which require
+   * that PBSZ be sent before the PROT command.
+   *
+   * However, some broken FTPS clients do not send PBSZ before PROT.  And,
+   * in practice, since the PBSZ value for FTPS is ALWAYS zero, there is little
+   * value in punishing users of these broken clients by refusing to work
+   * with their client.
+   *
+   * Thus we've relaxed our PBSZ requirements, by acting as if PBSZ has been
+   * sent already, even if it has not.  For now.
+   */
+
   /* Check for <Limit> restrictions. */
   if (!dir_check(cmd->tmp_pool, cmd, G_NONE, session.cwd, NULL)) {
     pr_response_add_err(R_534, _("Unwilling to accept security parameters"));
-    tls_log("%s: denied by <Limit> configuration", cmd->argv[0]);
+    tls_log("%s: denied by <Limit> configuration", (char *) cmd->argv[0]);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   /* Only PROT C or PROT P is valid with respect to SSL/TLS. */
-  if (strncmp(cmd->argv[1], "C", 2) == 0) {
+  prot = cmd->argv[1];
+  if (strncmp(prot, "C", 2) == 0) {
     char *mesg = "Protection set to Clear";
 
     if (tls_required_on_data != 1) {
@@ -8233,13 +11705,16 @@ MODRET tls_prot(cmd_rec *cmd) {
     } else {
       pr_response_add_err(R_534, _("Unwilling to accept security parameters"));
       tls_log("%s: TLSRequired requires protection for data transfers",
-        cmd->argv[0]);
-      tls_log("%s: unwilling to accept security parameter (%s)", cmd->argv[0],
-        cmd->argv[1]);
+        (char *) cmd->argv[0]);
+      tls_log("%s: unwilling to accept security parameter (%s)",
+        (char *) cmd->argv[0], prot);
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
-  } else if (strncmp(cmd->argv[1], "P", 2) == 0) {
+  } else if (strncmp(prot, "P", 2) == 0) {
     char *mesg = "Protection set to Private";
 
     if (tls_required_on_data != -1) {
@@ -8253,15 +11728,18 @@ MODRET tls_prot(cmd_rec *cmd) {
     } else {
       pr_response_add_err(R_534, _("Unwilling to accept security parameters"));
       tls_log("%s: TLSRequired does not allow protection for data transfers",
-        cmd->argv[0]);
-      tls_log("%s: unwilling to accept security parameter (%s)", cmd->argv[0],
-        cmd->argv[1]);
+        (char *) cmd->argv[0]);
+      tls_log("%s: unwilling to accept security parameter (%s)",
+        (char *) cmd->argv[0], prot);
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
-  } else if (strncmp(cmd->argv[1], "S", 2) == 0 ||
-             strncmp(cmd->argv[1], "E", 2) == 0) {
-    pr_response_add_err(R_536, _("PROT %s unsupported"), cmd->argv[1]);
+  } else if (strncmp(prot, "S", 2) == 0 ||
+             strncmp(prot, "E", 2) == 0) {
+    pr_response_add_err(R_536, _("PROT %s unsupported"), prot);
 
     /* By the time the logic reaches this point, there must have been
      * an SSL/TLS session negotiated; other AUTH mechanisms will handle
@@ -8271,13 +11749,20 @@ MODRET tls_prot(cmd_rec *cmd) {
      * is handling the security mechanism, and that this module does not
      * allow for the unsupported PROT levels.
      */
+
+    pr_cmd_set_errno(cmd, ENOSYS);
+    errno = ENOSYS;
     return PR_ERROR(cmd);
 
   } else {
-    pr_response_add_err(R_504, _("PROT %s unsupported"), cmd->argv[1]);
+    pr_response_add_err(R_504, _("PROT %s unsupported"), prot);
+
+    pr_cmd_set_errno(cmd, ENOSYS);
+    errno = ENOSYS;
     return PR_ERROR(cmd);
   }
 
+  tls_flags |= TLS_SESS_PBSZ_OK;
   return PR_HANDLED(cmd);
 }
 
@@ -8292,9 +11777,12 @@ MODRET tls_sscn(cmd_rec *cmd) {
   if (cmd->argc > 2) {
     int xerrno = EINVAL;
 
-    tls_log("denying malformed SSCN command: '%s %s'", cmd->argv[0], cmd->arg);
-    pr_response_add_err(R_504, _("%s: %s"), cmd->argv[0], strerror(xerrno));
+    tls_log("denying malformed SSCN command: '%s %s'", (char *) cmd->argv[0],
+      cmd->arg);
+    pr_response_add_err(R_504, _("%s: %s"), (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -8302,36 +11790,41 @@ MODRET tls_sscn(cmd_rec *cmd) {
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.cwd, NULL)) {
     int xerrno = EPERM;
 
-    pr_log_debug(DEBUG8, "%s denied by <Limit> configuration", cmd->argv[0]);
-    tls_log("%s denied by <Limit> configuration", cmd->argv[0]);
-    pr_response_add_err(R_550, _("%s: %s"), cmd->argv[0], strerror(xerrno));
+    pr_log_debug(DEBUG8, "%s denied by <Limit> configuration",
+      (char *) cmd->argv[0]);
+    tls_log("%s denied by <Limit> configuration", (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, _("%s: %s"), (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   if (cmd->argc == 1) {
     /* Client is querying our SSCN mode. */
-    pr_response_add(R_200, "%s:%s METHOD", cmd->argv[0],
+    pr_response_add(R_200, "%s:%s METHOD", (char *) cmd->argv[0],
       tls_sscn_mode == TLS_SSCN_MODE_SERVER ? "SERVER" : "CLIENT");
 
   } else {
     /* Parameter MUST be one of: "ON", "OFF. */
     if (strncmp(cmd->argv[1], "ON", 3) == 0) {
       tls_sscn_mode = TLS_SSCN_MODE_CLIENT;
-      pr_response_add(R_200, "%s:CLIENT METHOD", cmd->argv[0]);
+      pr_response_add(R_200, "%s:CLIENT METHOD", (char *) cmd->argv[0]);
 
     } else if (strncmp(cmd->argv[1], "OFF", 4) == 0) {
       tls_sscn_mode = TLS_SSCN_MODE_SERVER;
-      pr_response_add(R_200, "%s:SERVER METHOD", cmd->argv[0]);
+      pr_response_add(R_200, "%s:SERVER METHOD", (char *) cmd->argv[0]);
 
     } else {
       int xerrno = EINVAL;
 
-      tls_log("denying unsupported SSCN command: '%s %s'", cmd->argv[0],
-        cmd->argv[1]);
-      pr_response_add_err(R_501, _("%s: %s"), cmd->argv[0], strerror(xerrno));
+      tls_log("denying unsupported SSCN command: '%s %s'",
+        (char *) cmd->argv[0], (char *) cmd->argv[1]);
+      pr_response_add_err(R_501, _("%s: %s"), (char *) cmd->argv[0],
+        strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -8346,127 +11839,150 @@ MODRET tls_sscn(cmd_rec *cmd) {
 /* usage: TLSCACertificateFile file */
 MODRET set_tlscacertfile(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSCACertificatePath path */
 MODRET set_tlscacertpath(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = dir_exists(cmd->argv[1]);
+  res = dir_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
     CONF_ERROR(cmd, "parameter must be a directory path");
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
  
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); 
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSCARevocationFile file */
 MODRET set_tlscacrlfile(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSCARevocationPath path */
 MODRET set_tlscacrlpath(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = dir_exists(cmd->argv[1]);
+  res = dir_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
     CONF_ERROR(cmd, "parameter must be a directory path");
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSCertificateChainFile file */
 MODRET set_tlscertchain(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSCipherSuite string */
 MODRET set_tlsciphersuite(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  char *ciphersuite = NULL;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  ciphersuite = cmd->argv[1];
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  /* Make sure that EXPORT ciphers cannot be used, per Bug#4163. */
+  c->argv[0] = pstrcat(c->pool, "!EXPORT:", ciphersuite, NULL);
+
   return PR_HANDLED(cmd);
 }
 
@@ -8485,19 +12001,22 @@ MODRET set_tlsctrlsacls(cmd_rec *cmd) {
 
   /* Check the second parameter to make sure it is "allow" or "deny" */
   if (strncmp(cmd->argv[2], "allow", 6) != 0 &&
-      strncmp(cmd->argv[2], "deny", 5) != 0)
+      strncmp(cmd->argv[2], "deny", 5) != 0) {
     CONF_ERROR(cmd, "second parameter must be 'allow' or 'deny'");
+  }
 
   /* Check the third parameter to make sure it is "user" or "group" */
   if (strncmp(cmd->argv[3], "user", 5) != 0 &&
-      strncmp(cmd->argv[3], "group", 6) != 0)
+      strncmp(cmd->argv[3], "group", 6) != 0) {
     CONF_ERROR(cmd, "third parameter must be 'user' or 'group'");
+  }
 
   bad_action = pr_ctrls_set_module_acls(tls_acttab, tls_act_pool, actions,
     cmd->argv[2], cmd->argv[3], cmd->argv[4]);
-  if (bad_action != NULL)
+  if (bad_action != NULL) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown action: '",
       bad_action, "'", NULL));
+  }
 
   return PR_HANDLED(cmd);
 #else
@@ -8526,150 +12045,212 @@ MODRET set_tlscryptodevice(cmd_rec *cmd) {
 /* usage: TLSDHParamFile file */
 MODRET set_tlsdhparamfile(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSDSACertificateFile file */
 MODRET set_tlsdsacertfile(cmd_rec *cmd) {
-  int res;
+  char *path;
+  const char *fingerprint;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+  if (*path != '/') {
+    CONF_ERROR(cmd, "parameter must be an absolute path");
+  }
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  fingerprint = tls_get_fingerprint_from_file(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
-  if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
-  }
-
-  if (*cmd->argv[1] != '/') {
-    CONF_ERROR(cmd, "parameter must be an absolute path");
+  if (fingerprint == NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path,
+      "' does not exist or does not contain a certificate", NULL));
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 2, path, fingerprint);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSDSACertificateKeyFile file */
 MODRET set_tlsdsakeyfile(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSECCertificateFile file */
 MODRET set_tlseccertfile(cmd_rec *cmd) {
 #ifdef PR_USE_OPENSSL_ECC
+  char *path;
+  const char *fingerprint;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  path = cmd->argv[1];
+  if (*path != '/') {
+    CONF_ERROR(cmd, "parameter must be an absolute path");
+  }
+
+  PRIVS_ROOT
+  fingerprint = tls_get_fingerprint_from_file(cmd->tmp_pool, path);
+  PRIVS_RELINQUISH
+
+  if (fingerprint == NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path,
+      "' does not exist or does not contain a certificate", NULL));
+  }
+
+  add_config_param_str(cmd->argv[0], 2, path, fingerprint);
+  return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", (char *) cmd->argv[0],
+    " directive cannot be used on this system, as your OpenSSL version "
+    "does not have EC support", NULL));
+#endif /* PR_USE_OPENSSL_ECC */
+}
+
+/* usage: TLSECCertificateKeyFile file */
+MODRET set_tlseckeyfile(cmd_rec *cmd) {
+#ifdef PR_USE_OPENSSL_ECC
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
   return PR_HANDLED(cmd);
 #else
-  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0],
+  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", (char *) cmd->argv[0],
     " directive cannot be used on this system, as your OpenSSL version "
-    "does have EC support", NULL));
-#endif /* PR_USE_OPENSSL_ ECC */
+    "does not have EC support", NULL));
+#endif /* PR_USE_OPENSSL_ECC */
 }
 
-/* usage: TLSECCertificateKeyFile file */
-MODRET set_tlseckeyfile(cmd_rec *cmd) {
+/* usage: TLSECDHCurve name */
+MODRET set_tlsecdhcurve(cmd_rec *cmd) {
 #ifdef PR_USE_OPENSSL_ECC
-  int res;
-
+  char *curve_name = NULL;
+  int curve_nid = -1;
+  EC_KEY *ec_key = NULL;
+  
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
-  PRIVS_RELINQUISH
+  curve_name = cmd->argv[1];
+
+  /* The special-case handling of these curve names is copied from OpenSSL's
+   * apps/ecparam.c code.
+   */
+
+  if (strcmp(curve_name, "secp192r1") == 0) {
+    curve_nid = NID_X9_62_prime192v1;
 
-  if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+  } else if (strcmp(curve_name, "secp256r1") == 0) {
+    curve_nid = NID_X9_62_prime256v1;
+
+  } else {
+    curve_nid = OBJ_sn2nid(curve_name);
   }
 
-  if (*cmd->argv[1] != '/') {
-    CONF_ERROR(cmd, "parameter must be an absolute path");
+  ec_key = EC_KEY_new_by_curve_name(curve_nid);
+  if (ec_key == NULL) {
+    char *err_str = "unknown/unsupported curve";
+
+    if (curve_nid > 0) {
+      err_str = ERR_error_string(ERR_get_error(), NULL);
+    }
+
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to create '", curve_name,
+      "' EC curve: ", err_str, NULL));
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  (void) add_config_param(cmd->argv[0], 1, ec_key);
   return PR_HANDLED(cmd);
+
 #else
   CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0],
     " directive cannot be used on this system, as your OpenSSL version "
-    "does have EC support", NULL));
-#endif /* PR_USE_OPENSSL_ ECC */
+    "does not have EC support", NULL));
+#endif /* PR_USE_OPENSSL_ECC */
 }
 
 /* usage: TLSEngine on|off */
 MODRET set_tlsengine(cmd_rec *cmd) {
-  int bool = -1;
+  int engine = -1;
   config_rec *c = NULL;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  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 char));
-  *((unsigned char *) c->argv[0]) = bool;
+  *((unsigned char *) c->argv[0]) = engine;
 
   return PR_HANDLED(cmd);
 }
@@ -8686,7 +12267,7 @@ MODRET set_tlslog(cmd_rec *cmd) {
 /* usage: TLSMasqueradeAddress ip-addr|dns-name */
 MODRET set_tlsmasqaddr(cmd_rec *cmd) {
   config_rec *c = NULL;
-  pr_netaddr_t *masq_addr = NULL;
+  const pr_netaddr_t *masq_addr = NULL;
   unsigned int addr_flags = PR_NETADDR_GET_ADDR_FL_INCL_DEVICE;
 
   CHECK_ARGS(cmd, 1);
@@ -8708,14 +12289,41 @@ MODRET set_tlsmasqaddr(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: TLSNextProtocol on|off */
+MODRET set_tlsnextprotocol(cmd_rec *cmd) {
+#if !defined(OPENSSL_NO_TLSEXT)
+  config_rec *c;
+  int use_next_protocol = FALSE;
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+  CHECK_ARGS(cmd, 1);
+
+  use_next_protocol = get_boolean(cmd, 1);
+  if (use_next_protocol == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = use_next_protocol;
+  return PR_HANDLED(cmd);
+
+#else
+  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0],
+    " directive cannot be used on this system, as your OpenSSL version "
+    "does not have NPN/ALPN support", NULL));
+#endif /* !OPENSSL_NO_TLSEXT */
+}
+
 /* usage: TLSOptions opt1 opt2 ... */
 MODRET set_tlsoptions(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
   unsigned long opts = 0UL;
 
-  if (cmd->argc-1 == 0)
+  if (cmd->argc-1 == 0) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -8728,13 +12336,13 @@ MODRET set_tlsoptions(cmd_rec *cmd) {
     } else if (strcmp(cmd->argv[i], "AllowPerUser") == 0) {
       opts |= TLS_OPT_ALLOW_PER_USER;
 
+    } else if (strcmp(cmd->argv[i], "AllowWeakDH") == 0) {
+      opts |= TLS_OPT_ALLOW_WEAK_DH;
+
     } else if (strcmp(cmd->argv[i], "AllowClientRenegotiation") == 0 ||
                strcmp(cmd->argv[i], "AllowClientRenegotiations") == 0) {
       opts |= TLS_OPT_ALLOW_CLIENT_RENEGOTIATIONS;
 
-    } else if (strcmp(cmd->argv[i], "AllowWeakDH") == 0) {
-      opts |= TLS_OPT_ALLOW_WEAK_DH;
-
     } else if (strcmp(cmd->argv[i], "EnableDiags") == 0) {
       opts |= TLS_OPT_ENABLE_DIAGS;
 
@@ -8742,7 +12350,8 @@ MODRET set_tlsoptions(cmd_rec *cmd) {
       opts |= TLS_OPT_EXPORT_CERT_DATA;
 
     } else if (strcmp(cmd->argv[i], "NoCertRequest") == 0) {
-      opts |= TLS_OPT_NO_CERT_REQUEST;
+      pr_log_debug(DEBUG0, MOD_TLS_VERSION
+        ": NoCertRequest TLSOption is deprecated");
 
 #ifdef SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS
     } else if (strcmp(cmd->argv[i], "NoEmptyFragments") == 0) {
@@ -8764,7 +12373,7 @@ MODRET set_tlsoptions(cmd_rec *cmd) {
 
     } else if (strcmp(cmd->argv[i], "dNSNameRequired") == 0) {
       opts |= TLS_OPT_VERIFY_CERT_FQDN;
- 
+
     } else if (strcmp(cmd->argv[i], "iPAddressRequired") == 0) {
       opts |= TLS_OPT_VERIFY_CERT_IP_ADDR;
 
@@ -8774,6 +12383,9 @@ MODRET set_tlsoptions(cmd_rec *cmd) {
     } else if (strcmp(cmd->argv[i], "CommonNameRequired") == 0) {
       opts |= TLS_OPT_VERIFY_CERT_CN;
 
+    } else if (strcmp(cmd->argv[i], "NoAutoECDH") == 0) {
+      opts |= TLS_OPT_NO_AUTO_ECDH;
+
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown TLSOption '",
         cmd->argv[i], "'", NULL));
@@ -8789,47 +12401,101 @@ MODRET set_tlsoptions(cmd_rec *cmd) {
 /* usage: TLSPassPhraseProvider path */
 MODRET set_tlspassphraseprovider(cmd_rec *cmd) {
   struct stat st;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  if (*cmd->argv[1] != '/')
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be a full path: '",
-      cmd->argv[1], "'", NULL));
+  path = cmd->argv[1];
 
-  if (stat(cmd->argv[1], &st) < 0)
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '",
-      cmd->argv[1], "': ", strerror(errno), NULL));
+  if (*path != '/') {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "must be a full path: '", path, "'",
+      NULL));
+  }
+
+  if (stat(path, &st) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '", path, "': ",
+      strerror(errno), NULL));
+  }
 
-  if (!S_ISREG(st.st_mode))
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '",
-      cmd->argv[1], ": Not a regular file", NULL));
+  if (!S_ISREG(st.st_mode)) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", path,
+      ": Not a regular file", NULL));
+  }
 
-  tls_passphrase_provider = pstrdup(permanent_pool, cmd->argv[1]);
+  tls_passphrase_provider = pstrdup(permanent_pool, path);
   return PR_HANDLED(cmd);
 }
 
 /* usage: TLSPKCS12File file */
 MODRET set_tlspkcs12file(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
   }
 
-  if (*cmd->argv[1] != '/') {
+  if (*path != '/') {
     CONF_ERROR(cmd, "parameter must be an absolute path");
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  add_config_param_str(cmd->argv[0], 1, path);
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSPreSharedKey name path */
+MODRET set_tlspresharedkey(cmd_rec *cmd) {
+#if defined(PSK_MAX_PSK_LEN)
+  char *identity, *path;
+  size_t identity_len, path_len;
+
+  CHECK_ARGS(cmd, 2);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  identity = cmd->argv[1]; 
+  path = cmd->argv[2];
+
+  identity_len = strlen(identity);
+  if (identity_len > PSK_MAX_IDENTITY_LEN) {
+    char buf[32];
+
+    memset(buf, '\0', sizeof(buf));
+    snprintf(buf, sizeof(buf)-1, "%u", (unsigned int) PSK_MAX_IDENTITY_LEN);
+
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+      "TLSPreSharedKey identity '", identity, "' exceed maximum length ",
+      buf, path, NULL))
+  }
+
+  /* Ensure that the given path starts with "hex:", denoting the
+   * format of the key at the given path.  Support for other formats, e.g.
+   * bcrypt or somesuch, will be added later.
+   */
+  path_len = strlen(path);
+  if (path_len < 5 ||
+      strncmp(path, "hex:", 4) != 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+      "unsupported TLSPreSharedKey format: ", path, NULL))
+  }
+
+  (void) add_config_param_str(cmd->argv[0], 2, identity, path);
+#else
+  pr_log_debug(DEBUG0,
+    "%s is not supported by this build/version of OpenSSL, ignoring",
+    (char *) cmd->argv[0]);
+#endif /* PSK_MAX_PSK_LEN */
+
   return PR_HANDLED(cmd);
 }
 
@@ -8845,34 +12511,103 @@ MODRET set_tlsprotocol(cmd_rec *cmd) {
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  for (i = 1; i < cmd->argc; i++) {
-    if (strncasecmp(cmd->argv[i], "SSLv23", 7) == 0) {
-      tls_protocol |= TLS_PROTO_SSL_V3;
-      tls_protocol |= TLS_PROTO_TLS_V1;
+  if (strcasecmp(cmd->argv[1], "all") == 0) {
+    /* We're in an additive/subtractive type of configuration. */
+    tls_protocol = TLS_PROTO_ALL;
+
+    for (i = 2; i < cmd->argc; i++) {
+      int disable = FALSE;
+      char *proto_name;
+
+      proto_name = cmd->argv[i];
+
+      if (*proto_name == '+') {
+        proto_name++;
+
+      } else if (*proto_name == '-') {
+        disable = TRUE;
+        proto_name++;
+
+      } else {
+        /* Using the additive/subtractive approach requires a +/- prefix;
+         * it's malformed without such prefaces.
+         */
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "missing required +/- prefix: ",
+          proto_name, NULL));
+      }
+
+      if (strncasecmp(proto_name, "SSLv3", 6) == 0) {
+        if (disable) {
+          tls_protocol &= ~TLS_PROTO_SSL_V3;
+        } else {
+          tls_protocol |= TLS_PROTO_SSL_V3;
+        }
 
-    } else if (strncasecmp(cmd->argv[i], "SSLv3", 6) == 0) {
-      tls_protocol |= TLS_PROTO_SSL_V3;
+      } else if (strncasecmp(proto_name, "TLSv1", 6) == 0) {
+        if (disable) {
+          tls_protocol &= ~TLS_PROTO_TLS_V1;
+        } else {
+          tls_protocol |= TLS_PROTO_TLS_V1;
+        }
 
-    } else if (strncasecmp(cmd->argv[i], "TLSv1", 6) == 0) {
-      tls_protocol |= TLS_PROTO_TLS_V1;
+      } else if (strncasecmp(proto_name, "TLSv1.1", 8) == 0) {
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+        if (disable) {
+          tls_protocol &= ~TLS_PROTO_TLS_V1_1;
+        } else {
+          tls_protocol |= TLS_PROTO_TLS_V1_1;
+        }
+#else
+        CONF_ERROR(cmd, "Your OpenSSL installation does not support TLSv1.1");
+#endif /* OpenSSL 1.0.1 or later */
 
-    } else if (strncasecmp(cmd->argv[i], "TLSv1.1", 8) == 0) {
+      } else if (strncasecmp(proto_name, "TLSv1.2", 8) == 0) {
 #if OPENSSL_VERSION_NUMBER >= 0x10001000L
-      tls_protocol |= TLS_PROTO_TLS_V1_1;
+        if (disable) {
+          tls_protocol &= ~TLS_PROTO_TLS_V1_2;
+        } else {
+          tls_protocol |= TLS_PROTO_TLS_V1_2;
+        }
 #else
-      CONF_ERROR(cmd, "Your OpenSSL installation does not support TLSv1.1");
+        CONF_ERROR(cmd, "Your OpenSSL installation does not support TLSv1.2");
 #endif /* OpenSSL 1.0.1 or later */
 
-    } else if (strncasecmp(cmd->argv[i], "TLSv1.2", 8) == 0) {
+      } else {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown protocol: '",
+          cmd->argv[i], "'", NULL));
+      }
+    }
+
+  } else {
+    for (i = 1; i < cmd->argc; i++) {
+      if (strncasecmp(cmd->argv[i], "SSLv23", 7) == 0) {
+        tls_protocol |= TLS_PROTO_SSL_V3;
+        tls_protocol |= TLS_PROTO_TLS_V1;
+
+      } else if (strncasecmp(cmd->argv[i], "SSLv3", 6) == 0) {
+        tls_protocol |= TLS_PROTO_SSL_V3;
+
+      } else if (strncasecmp(cmd->argv[i], "TLSv1", 6) == 0) {
+        tls_protocol |= TLS_PROTO_TLS_V1;
+
+      } else if (strncasecmp(cmd->argv[i], "TLSv1.1", 8) == 0) {
 #if OPENSSL_VERSION_NUMBER >= 0x10001000L
-      tls_protocol |= TLS_PROTO_TLS_V1_2;
+        tls_protocol |= TLS_PROTO_TLS_V1_1;
 #else
-      CONF_ERROR(cmd, "Your OpenSSL installation does not support TLSv1.2");
+        CONF_ERROR(cmd, "Your OpenSSL installation does not support TLSv1.1");
 #endif /* OpenSSL 1.0.1 or later */
 
-    } else {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown protocol: '",
-        cmd->argv[i], "'", NULL));
+      } else if (strncasecmp(cmd->argv[i], "TLSv1.2", 8) == 0) {
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+        tls_protocol |= TLS_PROTO_TLS_V1_2;
+#else
+        CONF_ERROR(cmd, "Your OpenSSL installation does not support TLSv1.2");
+#endif /* OpenSSL 1.0.1 or later */
+
+      } else {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown protocol: '",
+          cmd->argv[i], "'", NULL));
+      }
     }
   }
 
@@ -8947,10 +12682,11 @@ MODRET set_tlsrenegotiate(cmd_rec *cmd) {
       i += 2;
 
     } else if (strcmp(cmd->argv[i], "required") == 0) {
-      int bool = get_boolean(cmd, i+1);
+      int required;
 
-      if (bool != -1) {
-        *((unsigned char *) c->argv[3]) = bool;
+      required = get_boolean(cmd, i+1);
+      if (required != -1) {
+        *((unsigned char *) c->argv[3]) = required;
 
       } else {
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, cmd->argv[i],
@@ -8987,7 +12723,7 @@ MODRET set_tlsrenegotiate(cmd_rec *cmd) {
 
 /* usage: TLSRequired on|off|both|control|ctrl|[!]data|auth|auth+data */
 MODRET set_tlsrequired(cmd_rec *cmd) {
-  int bool = -1;
+  int required = -1;
   int on_auth = 0, on_ctrl = 0, on_data = 0;
   config_rec *c = NULL;
 
@@ -8995,8 +12731,8 @@ MODRET set_tlsrequired(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR|
     CONF_DYNDIR);
 
-  bool = get_boolean(cmd, 1);
-  if (bool == -1) {
+  required = get_boolean(cmd, 1);
+  if (required == -1) {
     if (strcmp(cmd->argv[1], "control") == 0 ||
         strcmp(cmd->argv[1], "ctrl") == 0) {
       on_auth = 1;
@@ -9034,7 +12770,7 @@ MODRET set_tlsrequired(cmd_rec *cmd) {
       CONF_ERROR(cmd, "bad parameter");
 
   } else {
-    if (bool == TRUE) {
+    if (required == TRUE) {
       on_auth = 1;
       on_ctrl = 1;
       on_data = 1;
@@ -9056,96 +12792,308 @@ MODRET set_tlsrequired(cmd_rec *cmd) {
 
 /* usage: TLSRSACertificateFile file */
 MODRET set_tlsrsacertfile(cmd_rec *cmd) {
+  char *path;
+  const char *fingerprint;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  path = cmd->argv[1];
+  if (*path != '/') {
+    CONF_ERROR(cmd, "parameter must be an absolute path");
+  }
+
+  PRIVS_ROOT
+  fingerprint = tls_get_fingerprint_from_file(cmd->tmp_pool, path);
+  PRIVS_RELINQUISH
+
+  if (fingerprint == NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path,
+      "' does not exist or does not contain a certificate", NULL));
+  }
+
+  add_config_param_str(cmd->argv[0], 2, path, fingerprint);
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSRSACertificateKeyFile file */
+MODRET set_tlsrsakeyfile(cmd_rec *cmd) {
   int res;
+  char *path;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
+  path = cmd->argv[1];
+
   PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
+  res = file_exists2(cmd->tmp_pool, path);
   PRIVS_RELINQUISH
 
   if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' does not exist",
+      NULL));
+  }
+
+  if (*path != '/') {
+    CONF_ERROR(cmd, "parameter must be an absolute path");
+  }
+
+  add_config_param_str(cmd->argv[0], 1, path);
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSServerCipherPreference on|off */
+MODRET set_tlsservercipherpreference(cmd_rec *cmd) {
+  int use_server_prefs = -1;
+#ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
+  config_rec *c = NULL;
+#endif
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  use_server_prefs = get_boolean(cmd, 1);
+  if (use_server_prefs == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+#ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = use_server_prefs;
+
+#else
+  pr_log_debug(DEBUG0,
+    "%s is not supported by this version of OpenSSL, ignoring",
+    (char *) cmd->argv[0]);
+#endif /* SSL_OP_CIPHER_SERVER_PREFERENCE */
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSServerInfoFile path */
+MODRET set_tlsserverinfofile(cmd_rec *cmd) {
+#if !defined(OPENSSL_NO_TLSEXT) && OPENSSL_VERSION_NUMBER >= 0x10002000L
+  char *path;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  path = cmd->argv[1];
+  if (*path != '/') {
+    CONF_ERROR(cmd, "parameter must be an absolute path");
+  }
+
+  add_config_param_str(cmd->argv[0], 1, path);
+#else
+  pr_log_debug(DEBUG0,
+    "%s is not supported by this version of OpenSSL, ignoring",
+    (char *) cmd->argv[0]);
+#endif /* OPENSSL_NO_TLSEXT */
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSSessionCache "off"|type:/info [timeout] */
+MODRET set_tlssessioncache(cmd_rec *cmd) {
+  char *provider = NULL, *info = NULL;
+  config_rec *c;
+  long timeout = -1;
+  int enabled = -1;
+
+  if (cmd->argc < 2 ||
+      cmd->argc > 3) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  /* Has session caching been explicitly turned off? */
+  enabled = get_boolean(cmd, 1);
+  if (enabled != FALSE) {
+    char *ptr;
+
+    /* Separate the type/info parameter into pieces. */
+    ptr = strchr(cmd->argv[1], ':');
+    if (ptr == NULL) {
+      CONF_ERROR(cmd, "badly formatted parameter");
+    }
+
+    *ptr = '\0';
+    provider = cmd->argv[1];
+    info = ptr + 1;
+
+    /* Verify that the requested cache type has been registered. */
+    if (strncmp(provider, "internal", 9) != 0) {
+      if (tls_sess_cache_get_cache(provider) == NULL) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "session cache type '",
+        provider, "' not available", NULL));
+      }
+    }
+  }
+
+  if (cmd->argc == 3) {
+    char *ptr = NULL;
+   
+    timeout = strtol(cmd->argv[2], &ptr, 10);
+    if (ptr && *ptr) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[2],
+        "' is not a valid timeout value", NULL));
+    }
+
+    if (timeout < 1) {
+      CONF_ERROR(cmd, "timeout be greater than 1");
+    }
+
+  } else {
+    /* Default timeout is 30 min (1800 secs). */
+    timeout = 1800;
+  }
+
+  c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+  if (provider != NULL) {
+    c->argv[0] = pstrdup(c->pool, provider);
+  }
+
+  if (info != NULL) {
+    c->argv[1] = pstrdup(c->pool, info);
+  }
+
+  c->argv[2] = palloc(c->pool, sizeof(long));
+  *((long *) c->argv[2]) = timeout;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSSessionTicketKeys [age secs] [count num] */
+MODRET set_tlssessionticketkeys(cmd_rec *cmd) {
+#if defined(TLS_USE_SESSION_TICKETS)
+  register unsigned int i;
+  int max_age = -1, max_nkeys = -1;
+  config_rec *c = NULL;
+
+  if (cmd->argc != 3 &&
+      cmd->argc != 5) {
+    CONF_ERROR(cmd, "wrong number of parameters");
   }
 
-  if (*cmd->argv[1] != '/') {
-    CONF_ERROR(cmd, "parameter must be an absolute path");
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcasecmp(cmd->argv[i], "age") == 0) {
+      if (pr_str_get_duration(cmd->argv[i+1], &max_age) < 0) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing age value '",
+          cmd->argv[i+1], "': ", strerror(errno), NULL));
+      }
+
+      /* Note that we do not allow ticket keys to age out faster than 1
+       * minute.  Less than that is a bit ridiculous, no?
+       */
+      if (max_age < 60) {
+        CONF_ERROR(cmd, "max key age must be at least 60sec");
+      }
+
+      i++;
+
+    } else if (strcasecmp(cmd->argv[i], "count") == 0) {
+      max_nkeys = atoi(cmd->argv[i+1]);
+      if (max_nkeys < 0) {
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing count value '",
+          cmd->argv[i+1], "': ", strerror(EINVAL), NULL));
+      }
+
+      /* Note that we need at least ONE ticket key for session tickets to
+       * even work.
+       */
+      if (max_nkeys < 2) {
+        CONF_ERROR(cmd, "max key count must be at least 1");
+      }
+
+      i++;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown parameter: ",
+        (char *) cmd->argv[i], NULL));
+    }
   }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[0]) = max_age;
+  c->argv[1] = pcalloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[1]) = max_nkeys;
+
   return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0],
+    " directive cannot be used on this system, as your OpenSSL version "
+    "does not have session ticket support", NULL));
+#endif /* TLS_USE_SESSION_TICKETS */
 }
 
-/* usage: TLSRSACertificateKeyFile file */
-MODRET set_tlsrsakeyfile(cmd_rec *cmd) {
-  int res;
+/* usage; TLSSessionTickets on|off */
+MODRET set_tlssessiontickets(cmd_rec *cmd) {
+#if defined(TLS_USE_SESSION_TICKETS)
+  int session_tickets = -1;
+  config_rec *c = NULL;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  PRIVS_ROOT
-  res = file_exists(cmd->argv[1]);
-  PRIVS_RELINQUISH
-
-  if (!res) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
-      "' does not exist", NULL));
+  session_tickets = get_boolean(cmd, 1);
+  if (session_tickets == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
   }
 
-  if (*cmd->argv[1] != '/') {
-    CONF_ERROR(cmd, "parameter must be an absolute path");
-  }
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = session_tickets;
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
   return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0],
+    " directive cannot be used on this system, as your OpenSSL version "
+    "does not have session ticket support", NULL));
+#endif /* TLS_USE_SESSION_TICKETS */
 }
 
-/* usage: TLSServerCipherPreference on|off */
-MODRET set_tlsservercipherpreference(cmd_rec *cmd) {
-  int bool = -1;
-#ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
+/* usage: TLSStapling on|off */
+MODRET set_tlsstapling(cmd_rec *cmd) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  int stapling = -1;
   config_rec *c = NULL;
-#endif
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  bool = get_boolean(cmd, 1);
-  if (bool == -1) {
+  stapling = get_boolean(cmd, 1);
+  if (stapling == -1) {
     CONF_ERROR(cmd, "expected Boolean parameter");
   }
 
-#ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(int));
-  *((int *) c->argv[0]) = bool;
-
-#else
-  pr_log_debug(DEBUG0,
-    "%s is not supported by this version of OpenSSL, ignoring",
-    cmd->argv[0]);
-#endif /* SSL_OP_CIPHER_SERVER_PREFERENCE */
+  *((int *) c->argv[0]) = stapling;
 
   return PR_HANDLED(cmd);
+#else
+  CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0],
+    " directive cannot be used on this system, as your OpenSSL version "
+    "does not have OCSP support", NULL));
+#endif /* PR_USE_OPENSSL_OCSP */
 }
 
-/* usage: TLSSessionCache "off"|type:/info [timeout] */
-MODRET set_tlssessioncache(cmd_rec *cmd) {
+/* usage: TLSStaplingCache "off"|type:/info */
+MODRET set_tlsstaplingcache(cmd_rec *cmd) {
   char *provider = NULL, *info = NULL;
   config_rec *c;
-  long timeout = -1;
   int enabled = -1;
 
-  if (cmd->argc < 2 ||
-      cmd->argc > 3) {
-    CONF_ERROR(cmd, "wrong number of parameters");
-  }
-
+  CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  /* Has session caching been explicitly turned off? */
+  /* Has OCSP response caching been explicitly turned off? */
   enabled = get_boolean(cmd, 1);
   if (enabled != FALSE) {
     char *ptr;
@@ -9161,33 +13109,13 @@ MODRET set_tlssessioncache(cmd_rec *cmd) {
     info = ptr + 1;
 
     /* Verify that the requested cache type has been registered. */
-    if (strncmp(provider, "internal", 9) != 0) {
-      if (tls_sess_cache_get_cache(provider) == NULL) {
-        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "session cache type '",
+    if (tls_ocsp_cache_get_cache(provider) == NULL) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "OCSP stapling cache type '",
         provider, "' not available", NULL));
-      }
-    }
-  }
-
-  if (cmd->argc == 3) {
-    char *ptr = NULL;
-   
-    timeout = strtol(cmd->argv[2], &ptr, 10);
-    if (ptr && *ptr) {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[2],
-        "' is not a valid timeout value", NULL));
-    }
-
-    if (timeout < 1) {
-      CONF_ERROR(cmd, "timeout be greater than 1");
     }
-
-  } else {
-    /* Default timeout is 30 min (1800 secs). */
-    timeout = 1800;
   }
 
-  c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
   if (provider != NULL) {
     c->argv[0] = pstrdup(c->pool, provider);
   }
@@ -9196,13 +13124,90 @@ MODRET set_tlssessioncache(cmd_rec *cmd) {
     c->argv[1] = pstrdup(c->pool, info);
   }
 
-  c->argv[2] = palloc(c->pool, sizeof(long));
-  *((long *) c->argv[2]) = timeout;
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSStaplingOptions opt1 opt2 ... */
+MODRET set_tlsstaplingoptions(cmd_rec *cmd) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "NoNonce") == 0) {
+      opts |= TLS_STAPLING_OPT_NO_NONCE;
+
+    } else if (strcmp(cmd->argv[i], "NoVerify") == 0) {
+      opts |= TLS_STAPLING_OPT_NO_VERIFY;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown TLSStaplingOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+#endif /* PR_USE_OPENSSL_OCSP */
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSStaplingResponder url */
+MODRET set_tlsstaplingresponder(cmd_rec *cmd) {
+#if defined(PR_USE_OPENSSL_OCSP)
+  char *host = NULL, *port = NULL, *uri = NULL, *url;
+  int use_ssl = 0;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  url = cmd->argv[1];
+  if (OCSP_parse_url(url, &host, &port, &uri, &use_ssl) != 1) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing URL '", url, "': ",
+      tls_get_errors(), NULL));
+  }
+
+  OPENSSL_free(host);
+  OPENSSL_free(port);
+  OPENSSL_free(uri);
+
+  add_config_param_str(cmd->argv[0], 1, url);
+#endif /* PR_USE_OPENSSL_OCSP */
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TLSStaplingTimeout secs */
+MODRET set_tlsstaplingtimeout(cmd_rec *cmd) {
+  int timeout = -1;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  if (pr_str_get_duration(cmd->argv[1], &timeout) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing timeout value '",
+      cmd->argv[1], "': ", strerror(errno), NULL));
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[0]) = timeout;
 
   return PR_HANDLED(cmd);
 }
 
-/* usage: TLSTimeoutHandshake <secs> */
+/* usage: TLSTimeoutHandshake secs */
 MODRET set_tlstimeouthandshake(cmd_rec *cmd) {
   int timeout = -1;
   config_rec *c = NULL;
@@ -9250,21 +13255,26 @@ MODRET set_tlsusername(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: TLSVerifyClient on|off */
+/* usage: TLSVerifyClient on|off|optional */
 MODRET set_tlsverifyclient(cmd_rec *cmd) {
-  int bool = -1;
+  int verify_client = -1;
   config_rec *c = NULL;
 
   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");
+  verify_client = get_boolean(cmd, 1);
+  if (verify_client == -1) {
+    if (strcasecmp(cmd->argv[1], "optional") != 0) {
+      CONF_ERROR(cmd, "expected Boolean parameter");
+    }
+
+    verify_client = 2;
+  }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned char));
-  *((unsigned char *) c->argv[0]) = bool;
+  *((unsigned char *) c->argv[0]) = verify_client;
 
   return PR_HANDLED(cmd);
 }
@@ -9367,6 +13377,11 @@ static void tls_mod_unload_ev(const void *event_data, void *user_data) {
     /* Unregister ourselves from all events. */
     pr_event_unregister(&tls_module, NULL, NULL);
 
+    pr_timer_remove(-1, &tls_module);
+# if defined(TLS_USE_SESSION_TICKETS)
+    scrub_ticket_keys();
+# endif /* TLS_USE_SESSION_TICKETS */
+
 # ifdef PR_USE_CTRLS
     /* Unregister any control actions. */
     pr_ctrls_unregister(&tls_module, "tls");
@@ -9397,12 +13412,41 @@ static void tls_mod_unload_ev(const void *event_data, void *user_data) {
 }
 #endif /* PR_SHARED_MODULE */
 
+static void tls_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* If the client has already established a TLS session, then do nothing;
+   * we cannot easily force a re-handshake using different credentials very
+   * easily.  (Right?)
+   */
+  if (session.rfc2228_mech != NULL) {
+    return;
+  }
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&tls_module, "core.exit", tls_exit_ev);
+  pr_event_unregister(&tls_module, "core.session-reinit", tls_sess_reinit_ev);
+
+  tls_reset_state();
+  res = tls_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED,
+      NULL);
+  }
+}
+
 /* Daemon PID */
 extern pid_t mpid;
 
 static void tls_shutdown_ev(const void *event_data, void *user_data) {
   if (mpid == getpid()) {
     tls_scrub_pkeys();
+#if defined(TLS_USE_SESSION_TICKETS)
+    scrub_ticket_keys();
+#endif /* TLS_USE_SESSION_TICKETS */
+    destroy_pool(tls_pool);
+    tls_pool = NULL;
   }
 
   /* Write out a new RandomSeed file, for use later. */
@@ -9434,10 +13478,18 @@ static void tls_restart_ev(const void *event_data, void *user_data) {
   register unsigned int i;
 #endif /* PR_USE_CTRLS */
 
-  tls_scrub_pkeys();
+  /* Note: We SHOULD scrub all of the mlock'd passphrases from memory here.
+   * However, doing so would require that some outside agency, such as
+   * a TLSPassPhraseProvider, provide those passphrases again.  And some
+   * sites deem such providers insecure, but do have servers restarted due
+   * to e.g. log rotation, without changing the passphrases/certs.  For
+   * such sites, then, we will opportunistically scrub the pkeys later,
+   * during the phase where such passphrases are obtained as needed;
+   * see Bug#4260.
+   */
 
 #ifdef PR_USE_CTRLS
-  if (tls_act_pool) {
+  if (tls_act_pool != NULL) {
     destroy_pool(tls_act_pool);
     tls_act_pool = NULL;
   }
@@ -9457,6 +13509,18 @@ static void tls_restart_ev(const void *event_data, void *user_data) {
 
 static void tls_exit_ev(const void *event_data, void *user_data) {
 
+  if (ssl_ctx != NULL) {
+    time_t now;
+
+    /* Help out with the SSL session cache grooming by flushing any
+     * expired sessions out right now.  The client is closing its
+     * connection to us anyway, so some additional latency here shouldn't
+     * be noticed.  Right?
+     */
+    now = time(NULL);
+    SSL_CTX_flush_sessions(ssl_ctx, (long) now);
+  }
+
   /* If diags are enabled, log some OpenSSL stats. */
   if (ssl_ctx != NULL && 
       (tls_opts & TLS_OPT_ENABLE_DIAGS)) {
@@ -9490,6 +13554,11 @@ static void tls_exit_ev(const void *event_data, void *user_data) {
     tls_log("[stat]: SSL session cache size exceeded: %ld", res);
   }
 
+  if (tls_pkey != NULL) {
+    tls_scrub_pkey(tls_pkey);
+    tls_pkey = NULL;
+  }
+
   /* OpenSSL cleanup */
   tls_cleanup(0);
 
@@ -9499,47 +13568,238 @@ static void tls_exit_ev(const void *event_data, void *user_data) {
    * and thus we have a read-only copy.
    */
 
-  if (tls_ctrl_netio) {
-    pr_unregister_netio(PR_NETIO_STRM_CTRL);
-    destroy_pool(tls_ctrl_netio->pool);
-    tls_ctrl_netio = NULL;
+  if (tls_ctrl_netio) {
+    pr_unregister_netio(PR_NETIO_STRM_CTRL);
+    destroy_pool(tls_ctrl_netio->pool);
+    tls_ctrl_netio = NULL;
+  }
+
+  if (tls_data_netio) {
+    pr_unregister_netio(PR_NETIO_STRM_DATA);
+    destroy_pool(tls_data_netio->pool);
+    tls_data_netio = NULL;
+  }
+
+  if (mpid != getpid()) {
+    tls_scrub_pkeys();
+  }
+
+  tls_closelog();
+  return;
+}
+
+static void tls_timeout_ev(const void *event_data, void *user_data) {
+
+  if (session.c &&
+      ctrl_ssl != NULL &&
+      (tls_flags & TLS_SESS_ON_CTRL)) {
+    /* Try to properly close the SSL session down on the control channel,
+     * if there is one.
+     */ 
+    tls_end_sess(ctrl_ssl, session.c, 0);
+    pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
+    pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
+    ctrl_ssl = NULL;
+  }
+}
+
+static tls_pkey_t *tls_find_pkey(server_rec *s, int flags) {
+  tls_pkey_t *k, *pkey = NULL;
+
+  for (k = tls_pkey_list; k; k = k->next) {
+    if (k->sid == s->sid) {
+      switch (flags) {
+        case TLS_PASSPHRASE_FL_RSA_KEY:
+          if (k->rsa_pkey != NULL) {
+            pkey = k;
+          }
+          break;
+
+        case TLS_PASSPHRASE_FL_DSA_KEY:
+          if (k->dsa_pkey != NULL) {
+            pkey = k;
+          }
+          break;
+
+        case TLS_PASSPHRASE_FL_EC_KEY:
+#ifdef PR_USE_OPENSSL_ECC
+          if (k->ec_pkey != NULL) {
+            pkey = k;
+          }
+#endif /* PR_USE_OPENSSL_ECC */
+          break;
+
+        case TLS_PASSPHRASE_FL_PKCS12_PASSWD:
+          if (k->pkcs12_passwd != NULL) {
+            pkey = k;
+          }
+          break;
+
+        default:
+          break;
+      }
+
+      if (pkey != NULL) {
+        break;
+      }
+    }
+  }
+
+  return pkey;
+}
+
+static tls_pkey_t *tls_get_key_passphrase(server_rec *s, const char *path,
+    int flags) {
+  int res, *pass_len;
+  tls_pkey_t *k = NULL;
+  const char *key_type = "unsupported";
+  char buf[256], **key_data = NULL;
+  void **key_ptr = NULL;
+
+  switch (flags) {
+    case TLS_PASSPHRASE_FL_RSA_KEY:
+      key_type = "RSA";
+      break;
+
+    case TLS_PASSPHRASE_FL_DSA_KEY:
+      key_type = "DSA";
+      break;
+
+    case TLS_PASSPHRASE_FL_EC_KEY:
+      key_type = "EC";
+      break;
+
+    case TLS_PASSPHRASE_FL_PKCS12_PASSWD:
+      key_type = "PKCS12";
+      break;
+
+    default:
+      errno = EINVAL;
+      return NULL;
+  }
+
+  pr_trace_msg(trace_channel, 14,
+    "obtaining passphrase/password for %s cert for path %s", key_type, path);
+
+  /* First see if we have an existing (and usable!) passphrase already
+   * stored for this server/key type/path.
+   */
+  k = tls_find_pkey(s, flags);
+  if (k != NULL) {
+    /* Remove the key from the list; we will be adding this key, or another
+     * one, back to the list in its place.
+     */
+    tls_remove_pkey(k);
+
+    pr_trace_msg(trace_channel, 19,
+      "FOUND existing %s pkey found for server ID %u (path %s)", key_type,
+      s->sid, k->path);
+
+    /* If this key is for the same path, consider it usable. */
+    if (strcmp(path, k->path) == 0) {
+      pr_trace_msg(trace_channel, 14,
+        "reusing stored %s for %s certificate from path '%s'",
+        flags != TLS_PASSPHRASE_FL_PKCS12_PASSWD ? "passphrase" : "password",
+        key_type, path);
+      return k;
+    }
+
+    /* Not for the same path?  Consider this key stale, and scrub it. */
+    tls_scrub_pkey(k);
+  }
+
+  if (k == NULL) {
+    pool *key_pool;
+
+    key_pool = make_sub_pool(tls_pool);
+    pr_pool_tag(key_pool, "Private Key Pool");
+
+    k = pcalloc(key_pool, sizeof(tls_pkey_t));
+    k->pool = key_pool;
+  }
+
+  k->pkeysz = PEM_BUFSIZE;
+
+  switch (flags) {
+    case TLS_PASSPHRASE_FL_RSA_KEY:
+      key_data = &(k->rsa_pkey);
+      key_ptr = &(k->rsa_pkey_ptr);
+      pass_len = &(k->rsa_passlen);
+      break;
+
+    case TLS_PASSPHRASE_FL_DSA_KEY:
+      key_data = &(k->dsa_pkey);
+      key_ptr = &(k->dsa_pkey_ptr);
+      pass_len = &(k->dsa_passlen);
+      break;
+
+    case TLS_PASSPHRASE_FL_EC_KEY:
+#ifdef PR_USE_OPENSSL_ECC
+      key_data = &(k->ec_pkey);
+      key_ptr = &(k->ec_pkey_ptr);
+      pass_len = &(k->ec_passlen);
+#endif /* PR_USE_OPENSSL_ECC */
+      break;
+
+    case TLS_PASSPHRASE_FL_PKCS12_PASSWD:
+      key_data = &(k->pkcs12_passwd);
+      key_ptr = &(k->pkcs12_passwd_ptr);
+      pass_len = &(k->pkcs12_passlen);
+      break;
+
+    default:
+      errno = EINVAL;
+      return NULL;
   }
 
-  if (tls_data_netio) {
-    pr_unregister_netio(PR_NETIO_STRM_DATA);
-    destroy_pool(tls_data_netio->pool);
-    tls_data_netio = NULL;
+  res = snprintf(buf, sizeof(buf)-1, "%s %s for the %s#%d (%s) server: ",
+    key_type, flags != TLS_PASSPHRASE_FL_PKCS12_PASSWD ? "key" : "password",
+    pr_netaddr_get_ipstr(s->addr), s->ServerPort, s->ServerName);
+  buf[res] = '\0';
+  buf[sizeof(buf)-1] = '\0';
+
+  *key_data = tls_get_page(PEM_BUFSIZE, key_ptr);
+  if (*key_data == NULL) {
+    pr_log_pri(PR_LOG_ALERT, MOD_TLS_VERSION ": Out of memory!");
+    pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_NOMEM, NULL);
   }
 
-  if (mpid != getpid())
-    tls_scrub_pkeys();
+  res = tls_get_passphrase(s, path, buf, *key_data, k->pkeysz-1, flags);
+  if (res < 0) {
+    const char *errors;
 
-  tls_closelog();
-  return;
-}
+    errors = tls_get_errors();
+    if (errors == NULL) {
+      errors = "Not provided";
+    }
 
-static void tls_timeout_ev(const void *event_data, void *user_data) {
+    pr_trace_msg(trace_channel, 1, "error reading %s %s: %s", key_type,
+      flags != TLS_PASSPHRASE_FL_PKCS12_PASSWD ? "passphrase" : "password",
+      errors);
+    pr_log_debug(DEBUG0, MOD_TLS_VERSION ": error reading %s %s: %s", key_type,
+      flags != TLS_PASSPHRASE_FL_PKCS12_PASSWD ? "passphrase" : "password",
+      errors);
 
-  if (session.c &&
-      ctrl_ssl != NULL &&
-      (tls_flags & TLS_SESS_ON_CTRL)) {
-    /* Try to properly close the SSL session down on the control channel,
-     * if there is one.
-     */ 
-    tls_end_sess(ctrl_ssl, PR_NETIO_STRM_CTRL, 0);
-    pr_table_remove(tls_ctrl_rd_nstrm->notes, TLS_NETIO_NOTE, NULL);
-    pr_table_remove(tls_ctrl_wr_nstrm->notes, TLS_NETIO_NOTE, NULL);
-    ctrl_ssl = NULL;
+    pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION
+      ": unable to use %s certificate %sin '%s', exiting", key_type,
+      flags != TLS_PASSPHRASE_FL_PKCS12_PASSWD ? "key " : "", path);
+    pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_BY_APPLICATION, NULL);
   }
+
+  *pass_len = res;
+  k->path = strdup(path);
+  k->sid = s->sid;
+
+  return k;
 }
 
 static void tls_get_passphrases(void) {
   server_rec *s = NULL;
-  char buf[256];
 
   for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
     config_rec *rsa = NULL, *dsa = NULL, *ec = NULL, *pkcs12 = NULL;
     tls_pkey_t *k = NULL;
+    const char *path;
 
     /* Find any TLS*CertificateKeyFile directives.  If they aren't present,
      * look for TLS*CertificateFile directives (when appropriate).
@@ -9568,103 +13828,26 @@ static void tls_get_passphrases(void) {
       continue;
     }
 
-    k = pcalloc(s->pool, sizeof(tls_pkey_t));
-    k->pkeysz = PEM_BUFSIZE;
-    k->server = s;
-
-    if (rsa) {
-      snprintf(buf, sizeof(buf)-1, "RSA key for the %s#%d (%s) server: ",
-        pr_netaddr_get_ipstr(s->addr), s->ServerPort, s->ServerName);
-      buf[sizeof(buf)-1] = '\0';
-
-      k->rsa_pkey = tls_get_page(PEM_BUFSIZE, &k->rsa_pkey_ptr);
-      if (k->rsa_pkey == NULL) {
-        pr_log_pri(PR_LOG_ALERT, MOD_TLS_VERSION ": Out of memory!");
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_NOMEM, NULL);
-      }
-
-      if (tls_get_passphrase(s, rsa->argv[0], buf, k->rsa_pkey,
-          k->pkeysz, TLS_PASSPHRASE_FL_RSA_KEY) < 0) {
-        pr_log_debug(DEBUG0, MOD_TLS_VERSION
-          ": error reading RSA passphrase: %s", tls_get_errors());
-
-        pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION ": unable to use "
-          "RSA certificate key in '%s', exiting", (char *) rsa->argv[0]);
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_BY_APPLICATION,
-          NULL);
-      }
+    if (rsa != NULL) {
+      path = rsa->argv[0];
+      k = tls_get_key_passphrase(s, path, TLS_PASSPHRASE_FL_RSA_KEY);
     }
 
-    if (dsa) {
-      snprintf(buf, sizeof(buf)-1, "DSA key for the %s#%d (%s) server: ",
-        pr_netaddr_get_ipstr(s->addr), s->ServerPort, s->ServerName);
-      buf[sizeof(buf)-1] = '\0';
-
-      k->dsa_pkey = tls_get_page(PEM_BUFSIZE, &k->dsa_pkey_ptr);
-      if (k->dsa_pkey == NULL) {
-        pr_log_pri(PR_LOG_ALERT, MOD_TLS_VERSION ": Out of memory!");
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_NOMEM, NULL);
-      }
-
-      if (tls_get_passphrase(s, dsa->argv[0], buf, k->dsa_pkey,
-          k->pkeysz, TLS_PASSPHRASE_FL_DSA_KEY) < 0) {
-        pr_log_debug(DEBUG0, MOD_TLS_VERSION
-          ": error reading DSA passphrase: %s", tls_get_errors());
-
-        pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION ": unable to use "
-          "DSA certificate key '%s', exiting", (char *) dsa->argv[0]);
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_BY_APPLICATION,
-          NULL);
-      }
+    if (dsa != NULL) {
+      path = dsa->argv[0];
+      k = tls_get_key_passphrase(s, path, TLS_PASSPHRASE_FL_DSA_KEY);
     }
 
 #ifdef PR_USE_OPENSSL_ECC
     if (ec != NULL) {
-      snprintf(buf, sizeof(buf)-1, "EC key for the %s#%d (%s) server: ",
-        pr_netaddr_get_ipstr(s->addr), s->ServerPort, s->ServerName);
-      buf[sizeof(buf)-1] = '\0';
-
-      k->ec_pkey = tls_get_page(PEM_BUFSIZE, &k->ec_pkey_ptr);
-      if (k->ec_pkey == NULL) {
-        pr_log_pri(PR_LOG_ALERT, MOD_TLS_VERSION ": Out of memory!");
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_NOMEM, NULL);
-      }
-
-      if (tls_get_passphrase(s, ec->argv[0], buf, k->ec_pkey,
-          k->pkeysz, TLS_PASSPHRASE_FL_EC_KEY) < 0) {
-        pr_log_debug(DEBUG0, MOD_TLS_VERSION
-          ": error reading EC passphrase: %s", tls_get_errors());
-
-        pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION ": unable to use "
-          "EC certificate key '%s', exiting", (char *) dsa->argv[0]);
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_BY_APPLICATION,
-          NULL);
-      }
+      path = ec->argv[0];
+      k = tls_get_key_passphrase(s, path, TLS_PASSPHRASE_FL_EC_KEY);
     }
 #endif /* PR_USE_OPENSSL_ECC */
 
-    if (pkcs12) {
-      snprintf(buf, sizeof(buf)-1,
-        "PKCS12 password for the %s#%d (%s) server: ",
-        pr_netaddr_get_ipstr(s->addr), s->ServerPort, s->ServerName);
-      buf[sizeof(buf)-1] = '\0';
-
-      k->pkcs12_passwd = tls_get_page(PEM_BUFSIZE, &k->pkcs12_passwd_ptr);
-      if (k->pkcs12_passwd == NULL) {
-        pr_log_pri(PR_LOG_ALERT, MOD_TLS_VERSION ": Out of memory!");
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_NOMEM, NULL);
-      }
-
-      if (tls_get_passphrase(s, pkcs12->argv[0], buf, k->pkcs12_passwd,
-          k->pkeysz, TLS_PASSPHRASE_FL_PKCS12_PASSWD) < 0) {
-        pr_log_debug(DEBUG0, MOD_TLS_VERSION
-          ": error reading PKCS12 password: %s", tls_get_errors());
-
-        pr_log_pri(PR_LOG_ERR, MOD_TLS_VERSION ": unable to use "
-          "PKCS12 certificate '%s', exiting", (char *) pkcs12->argv[0]);
-        pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_BY_APPLICATION,
-          NULL);
-      }
+    if (pkcs12 != NULL) {
+      path = pkcs12->argv[0];
+      k = tls_get_key_passphrase(s, path, TLS_PASSPHRASE_FL_PKCS12_PASSWD);
     }
 
     k->next = tls_pkey_list;
@@ -9807,11 +13990,14 @@ static void tls_postparse_ev(const void *event_data, void *user_data) {
     pr_session_disconnect(&tls_module, PR_SESS_DISCONNECT_BY_APPLICATION, NULL);
   }
 
-  /* We can only get the passphrases from certs once OpenSSL has been
+  /* We can only get the passphrases for certs once OpenSSL has been
    * initialized.
    */
   tls_get_passphrases();
 
+  /* Clean up the pkey list, scrubbing any stale/irrelevant keys. */
+  tls_clean_pkeys();
+
   /* Install our control channel NetIO handlers.  This is done here
    * specifically because we need to cache a pointer to the nstrm that
    * is passed to the open callback().  Ideally we'd only install our
@@ -9868,6 +14054,9 @@ static int tls_init(void) {
   pr_event_register(&tls_module, "core.restart", tls_restart_ev, NULL);
   pr_event_register(&tls_module, "core.shutdown", tls_shutdown_ev, NULL);
 
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+  OPENSSL_config(NULL);
+#endif /* prior to OpenSSL-1.1.x */
   SSL_load_error_strings();
   SSL_library_init();
 
@@ -9875,6 +14064,7 @@ static int tls_init(void) {
    * handling some algorithms (e.g. PKCS12 files) which are NOT added by
    * just calling SSL_library_init().
    */
+  ERR_load_crypto_strings();
   OpenSSL_add_all_algorithms();
 
 #ifdef PR_USE_CTRLS
@@ -9899,48 +14089,105 @@ static int tls_init(void) {
   return 0;
 }
 
+#if !defined(OPENSSL_NO_TLSEXT)
+static int set_next_protocol(void) {
+  register unsigned int i;
+  const char *proto = TLS_DEFAULT_NEXT_PROTO;
+  size_t encoded_protolen, proto_len;
+  unsigned char *encoded_proto;
+  struct tls_next_proto *next_proto;
+
+  proto_len = strlen(proto);
+  encoded_protolen = proto_len + 1;
+  encoded_proto = palloc(session.pool, encoded_protolen);
+  encoded_proto[0] = proto_len;
+  for (i = 0; i < proto_len; i++) {
+    encoded_proto[i+1] = proto[i];
+  }
+
+  next_proto = palloc(session.pool, sizeof(struct tls_next_proto));
+  next_proto->proto = pstrdup(session.pool, proto);
+  next_proto->encoded_proto = encoded_proto;
+  next_proto->encoded_protolen = encoded_protolen;
+
+# if defined(PR_USE_OPENSSL_NPN)
+  SSL_CTX_set_next_protos_advertised_cb(ssl_ctx, tls_npn_advertised_cb,
+    next_proto);
+# endif /* NPN */
+
+# if defined(PR_USE_OPENSSL_ALPN)
+  SSL_CTX_set_alpn_select_cb(ssl_ctx, tls_alpn_select_cb, next_proto);
+# endif /* ALPN */
+
+  return 0;
+}
+#endif /* !OPENSSL_NO_TLSEXT */
+
 static int tls_sess_init(void) {
   int res = 0;
   unsigned char *tmp = NULL;
   config_rec *c = NULL;
 
+#if defined(TLS_USE_SESSION_TICKETS)
+  lock_ticket_keys();
+#endif /* TLS_USE_SESSION_TICKETS */
+
+  pr_event_register(&tls_module, "core.session-reinit", tls_sess_reinit_ev,
+    NULL);
+
   /* First, check to see whether mod_tls is even enabled. */
   tmp = get_param_ptr(main_server->conf, "TLSEngine", FALSE);
   if (tmp != NULL &&
       *tmp == TRUE) {
     tls_engine = TRUE;
+  }
 
-  } else {
-
-    /* No need for this modules's control channel NetIO handlers
-     * anymore.
+  if (tls_engine == FALSE) {
+    /* If we have no ServerAlias vhosts at all, then it is OK to clean up
+     * all of the TLS/OpenSSL-related code from this process.  Otherwise,
+     * a client MIGHT send a HOST command for a TLS-enabled vhost; if we
+     * were to always cleanup OpenSSL here, that HOST command would inevitably
+     * lead to a problem.
      */
-    pr_unregister_netio(PR_NETIO_STRM_CTRL);
 
-    /* No need for all the OpenSSL stuff in this process space, either.
-     */
-    tls_cleanup(TLS_CLEANUP_FL_SESS_INIT);
-    tls_scrub_pkeys();
+    res = pr_namebind_count(main_server);
+    if (res == 0) {
+      /* No need for this modules's control channel NetIO handlers
+       * anymore.
+       */
+      pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+      /* No need for all the OpenSSL stuff in this process space, either.
+       */
+      tls_cleanup(TLS_CLEANUP_FL_SESS_INIT);
+      tls_scrub_pkeys();
+    }
 
     return 0;
   }
 
   tls_cipher_suite = get_param_ptr(main_server->conf, "TLSCipherSuite",
     FALSE);
-  if (tls_cipher_suite == NULL)
+  if (tls_cipher_suite == NULL) {
     tls_cipher_suite = TLS_DEFAULT_CIPHER_SUITE;
+  }
 
   tls_crl_file = get_param_ptr(main_server->conf, "TLSCARevocationFile", FALSE);
   tls_crl_path = get_param_ptr(main_server->conf, "TLSCARevocationPath", FALSE);
 
-  tls_dhparam_file = get_param_ptr(main_server->conf, "TLSDHParamFile", FALSE);
-  if (tls_dhparam_file != NULL) {
+  c = find_config(main_server->conf, CONF_PARAM, "TLSDHParamFile", FALSE);
+  while (c != NULL) {
+    const char *path;
     FILE *fp;
     int xerrno;
 
+    pr_signals_handle();
+
+    path = c->argv[0];
+
     /* Load the DH params from the file. */
     PRIVS_ROOT
-    fp = fopen(tls_dhparam_file, "r");
+    fp = fopen(path, "r");
     xerrno = errno;
     PRIVS_RELINQUISH
 
@@ -9949,7 +14196,9 @@ static int tls_sess_init(void) {
 
       dh = PEM_read_DHparams(fp, NULL, NULL, NULL);
       if (dh != NULL) {
-        tls_tmp_dhs = make_array(session.pool, 1, sizeof(DH *));
+        if (tls_tmp_dhs == NULL) {
+          tls_tmp_dhs = make_array(session.pool, 1, sizeof(DH *));
+        }
       }
 
       while (dh != NULL) {
@@ -9962,9 +14211,10 @@ static int tls_sess_init(void) {
 
     } else {
       pr_log_debug(DEBUG3, MOD_TLS_VERSION
-        ": unable to open TLSDHParamFile '%s': %s", tls_dhparam_file,
-          strerror(xerrno));
+        ": unable to open TLSDHParamFile '%s': %s", path, strerror(xerrno));
     }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "TLSDHParamFile", FALSE);
   }
 
   tls_dsa_cert_file = get_param_ptr(main_server->conf, "TLSDSACertificateFile",
@@ -9984,6 +14234,185 @@ static int tls_sess_init(void) {
   tls_rsa_key_file = get_param_ptr(main_server->conf,
     "TLSRSACertificateKeyFile", FALSE);
 
+#if defined(PSK_MAX_PSK_LEN)
+  c = find_config(main_server->conf, CONF_PARAM, "TLSPreSharedKey", FALSE);
+  while (c != NULL) {
+    register int i;
+    char key_buf[PR_TUNABLE_BUFFER_SIZE], *identity, *path;
+    int fd, key_len, valid_hex = TRUE, xerrno;
+    struct stat st;
+    BIGNUM *bn = NULL;
+
+    pr_signals_handle();
+
+    identity = c->argv[0];
+    path = c->argv[1];
+
+    /* Advance past the "hex:" format prefix. */
+    path += 4;
+
+    PRIVS_ROOT
+    fd = open(path, O_RDONLY); 
+    xerrno = errno;
+    PRIVS_RELINQUISH
+
+    if (fd < 0) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": error opening TLSPreSharedKey file '%s': %s", path,
+        strerror(xerrno));
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    if (fstat(fd, &st) < 0) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": error checking TLSPreSharedKey file '%s': %s", path,
+        strerror(errno));
+      (void) close(fd);
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    /* Check on the permissions of the file; skip it if the permissions
+     * are too permissive, e.g. file is world-read/writable.
+     */
+    if (st.st_mode & S_IROTH) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": unable to use TLSPreSharedKey file '%s': file is world-readable",
+        path);
+      (void) close(fd);
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    if (st.st_mode & S_IWOTH) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": unable to use TLSPreSharedKey file '%s': file is world-writable",
+        path);
+      (void) close(fd);
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    /* Read the entire key into memory. */
+    key_len = read(fd, key_buf, sizeof(key_buf)-1);
+    (void) close(fd);
+
+    if (key_len < 0) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": error reading TLSPreSharedKey file '%s': %s", path,
+        strerror(xerrno));
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+
+    } else if (key_len == 0) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": read zero bytes from TLSPreSharedKey file '%s', ignoring", path);
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+
+    } else if (key_len < TLS_MIN_PSK_LEN) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": read %d bytes from TLSPreSharedKey file '%s', need at least %d "
+        "bytes of key data, ignoring", key_len, path, TLS_MIN_PSK_LEN);
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    key_buf[key_len] = '\0';
+    key_buf[sizeof(key_buf)-1] = '\0';
+
+    /* Ignore any trailing newlines. */
+    if (key_buf[key_len-1] == '\n') {
+      key_buf[key_len-1] = '\0';
+      key_len--;
+    }
+
+    if (key_buf[key_len-1] == '\r') {
+      key_buf[key_len-1] = '\0';
+      key_len--;
+    }
+
+    /* Ensure that it is all hex encoded data */
+    for (i = 0; i < key_len; i++) {
+      if (PR_ISXDIGIT((int) key_buf[i]) == 0) {
+        valid_hex = FALSE;
+        break;
+      }
+    }
+ 
+    if (valid_hex == FALSE) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": unable to use '%s': not a hex number", key_buf);
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    res = BN_hex2bn(&bn, key_buf);
+    if (res == 0) {
+      pr_log_debug(DEBUG2, MOD_TLS_VERSION
+        ": failed to convert '%s' to BIGNUM: %s", key_buf,
+        tls_get_errors());
+
+      if (bn != NULL) {
+        BN_free(bn);
+      }
+
+      c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+      continue;
+    }
+
+    if (tls_psks == NULL) {
+      tls_psks = pr_table_nalloc(session.pool, 0, 2);
+    }
+
+    if (pr_table_add(tls_psks, identity, bn, sizeof(BIGNUM *)) < 0) {
+      pr_log_debug(DEBUG0, MOD_TLS_VERSION
+        ": error stashing key for identity '%s': %s", identity,
+        strerror(errno));
+      BN_free(bn);
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "TLSPreSharedKey", FALSE);
+  }
+
+  if (tls_psks != NULL &&
+      pr_table_count(tls_psks) > 0) {
+    pr_trace_msg(trace_channel, 9,
+      "enabling support for PSK identities (%d)", pr_table_count(tls_psks));
+    SSL_CTX_set_psk_server_callback(ssl_ctx, tls_lookup_psk);
+  }
+#endif /* PSK_MAX_PSK_LEN */
+
+#if !defined(OPENSSL_NO_TLSEXT)
+  c = find_config(main_server->conf, CONF_PARAM, "TLSNextProtocol", FALSE);
+  if (c != NULL) {
+    int use_next_protocol = TRUE;
+
+    use_next_protocol = *((int *) c->argv[0]);
+    if (use_next_protocol) {
+      set_next_protocol();
+    }
+
+  } else {
+    set_next_protocol();
+  }
+
+# if OPENSSL_VERSION_NUMBER >= 0x10002000L && \
+     !defined(HAVE_LIBRESSL)
+  c = find_config(main_server->conf, CONF_PARAM, "TLSServerInfoFile", FALSE);
+  if (c != NULL) {
+    const char *path;
+
+    path = c->argv[0];
+    if (SSL_CTX_use_serverinfo_file(ssl_ctx, path) != 1) {
+      tls_log("error setting server info using '%s': %s", path,
+        tls_get_errors());
+    }
+  }
+# endif /* OpenSSL-1.0.2 and later */
+#endif /* !OPENSSL_NO_TLSEXT */
+
   c = find_config(main_server->conf, CONF_PARAM, "TLSOptions", FALSE);
   while (c != NULL) {
     unsigned long opts = 0;
@@ -10011,10 +14440,110 @@ static int tls_sess_init(void) {
   }
 #endif
 
-  tmp = get_param_ptr(main_server->conf, "TLSVerifyClient", FALSE);
-  if (tmp != NULL &&
-      *tmp == TRUE) {
-    tls_flags |= TLS_SESS_VERIFY_CLIENT;
+#if !defined(OPENSSL_NO_TLSEXT) && defined(TLSEXT_MAXLEN_host_name)
+  SSL_CTX_set_tlsext_servername_callback(ssl_ctx, tls_sni_cb);
+  SSL_CTX_set_tlsext_servername_arg(ssl_ctx, NULL);
+#endif /* !OPENSSL_NO_TLSEXT */
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  c = find_config(main_server->conf, CONF_PARAM, "TLSStaplingOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    tls_stapling_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "TLSStaplingOptions", FALSE);
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "TLSStaplingResponder", FALSE);
+  if (c != NULL) {
+    tls_stapling_responder = c->argv[0];
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "TLSStaplingTimeout", FALSE);
+  if (c != NULL) {
+    tls_stapling_timeout = *((unsigned int *) c->argv[0]);
+  }
+
+  SSL_CTX_set_tlsext_status_cb(ssl_ctx, tls_ocsp_cb);
+  SSL_CTX_set_tlsext_status_arg(ssl_ctx, NULL);
+#endif /* PR_USE_OPENSSL_OCSP */
+
+#if defined(TLS_USE_SESSION_TICKETS)
+  c = find_config(main_server->conf, CONF_PARAM, "TLSSessionTickets", FALSE);
+  if (c != NULL) {
+    int session_tickets;
+
+    session_tickets = *((int *) c->argv[0]);
+
+# ifdef SSL_OP_NO_TICKET
+    if (session_tickets == TRUE) {
+      if (SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, tls_ticket_key_cb) == 0) {
+        pr_log_pri(PR_LOG_WARNING, MOD_TLS_VERSION
+          ": mod_tls compiled with Session Ticket support, but linked to "
+          "an OpenSSL library without tlsext support, therefore Session "
+          "Tickets are not available");
+      }
+
+    } else {
+      /* Disable session tickets. */
+      SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_TICKET);
+    }
+# endif
+
+  } else {
+    /* Disable session tickets. */
+# ifdef SSL_OP_NO_TICKET
+    SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_TICKET);
+# endif
+  }
+#endif /* TLS_USE_SESSION_TICKETS */
+
+#ifdef PR_USE_OPENSSL_ECC
+# if defined(SSL_CTX_set_ecdh_auto)
+  if (tls_opts & TLS_OPT_NO_AUTO_ECDH) {
+    SSL_CTX_set_ecdh_auto(ssl_ctx, 0);
+  }
+# endif
+
+  c = find_config(main_server->conf, CONF_PARAM, "TLSECDHCurve", FALSE);
+  if (c != NULL) {
+    const EC_KEY *ec_key;
+
+    ec_key = c->argv[0];
+
+    SSL_CTX_set_options(ssl_ctx, SSL_OP_SINGLE_ECDH_USE);
+    SSL_CTX_set_tmp_ecdh(ssl_ctx, ec_key);
+  } else {
+# if OPENSSL_VERSION_NUMBER < 0x10100000L
+    SSL_CTX_set_tmp_ecdh_callback(ssl_ctx, tls_ecdh_cb);
+# endif /* Before OpenSSL-1.1.x */
+  }
+#endif /* PR_USE_OPENSSL_ECC */
+
+  c = find_config(main_server->conf, CONF_PARAM, "TLSVerifyClient", FALSE);
+  if (c != NULL) {
+    unsigned char verify_client;
+
+    verify_client = *((unsigned char *) c->argv[0]);
+    switch (verify_client) {
+      case 0:
+        break;
+
+      case 1:
+        tls_flags |= TLS_SESS_VERIFY_CLIENT_REQUIRED;
+        break;
+
+      case 2:
+        tls_flags |= TLS_SESS_VERIFY_CLIENT_OPTIONAL;
+        break;
+
+      default:
+        break;
+    }
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "TLSVerifyServer", FALSE);
@@ -10037,7 +14566,7 @@ static int tls_sess_init(void) {
   }
 
   /* If TLSVerifyClient/Server is on, look up the verification depth. */
-  if (tls_flags & (TLS_SESS_VERIFY_CLIENT|TLS_SESS_VERIFY_SERVER|TLS_SESS_VERIFY_SERVER_NO_DNS)) {
+  if (tls_flags & (TLS_SESS_VERIFY_CLIENT_REQUIRED|TLS_SESS_VERIFY_SERVER|TLS_SESS_VERIFY_SERVER_NO_DNS)) {
     int *depth = NULL;
 
     depth = get_param_ptr(main_server->conf, "TLSVerifyDepth", FALSE);
@@ -10047,14 +14576,28 @@ static int tls_sess_init(void) {
   }
 
   c = find_config(main_server->conf, CONF_PARAM, "TLSRequired", FALSE);
-  if (c) {
+  if (c != NULL) {
     tls_required_on_ctrl = *((int *) c->argv[0]);
     tls_required_on_data = *((int *) c->argv[1]);
     tls_required_on_auth = *((int *) c->argv[2]);
   }
 
+#if defined(PR_USE_OPENSSL_OCSP)
+  /* If a TLSStaplingCache has been configured, then TLSStapling should
+   * be enabled by default.
+   */
+  if (tls_ocsp_cache != NULL) {
+    tls_stapling = TRUE;
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "TLSStapling", FALSE);
+  if (c != NULL) {
+    tls_stapling = *((int *) c->argv[0]);
+  }
+#endif /* PR_USE_OPENSSL_OCSP */
+
   c = find_config(main_server->conf, CONF_PARAM, "TLSTimeoutHandshake", FALSE);
-  if (c) {
+  if (c != NULL) {
     tls_handshake_timeout = *((unsigned int *) c->argv[0]);
   }
 
@@ -10168,7 +14711,7 @@ static int tls_sess_init(void) {
    * is enabled, that info callback will also log the OpenSSL diagnostic
    * information.
    */
-  SSL_CTX_set_info_callback(ssl_ctx, tls_diags_cb);
+  SSL_CTX_set_info_callback(ssl_ctx, tls_info_cb);
 
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
   /* Install a callback for logging OpenSSL message information,
@@ -10191,6 +14734,9 @@ static int tls_sess_init(void) {
       /* Load all ENGINE implementations bundled with OpenSSL. */
       ENGINE_load_builtin_engines();
       ENGINE_register_all_complete();
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+      OPENSSL_config(NULL);
+#endif /* prior to OpenSSL-1.1.x */
 
       tls_log("%s", "enabled all builtin crypto devices");
 
@@ -10199,6 +14745,9 @@ static int tls_sess_init(void) {
 
       /* Load all ENGINE implementations bundled with OpenSSL. */
       ENGINE_load_builtin_engines();
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+      OPENSSL_config(NULL);
+#endif /* prior to OpenSSL-1.1.x */
 
       e = ENGINE_by_id(tls_crypto_device);
       if (e) {
@@ -10263,9 +14812,15 @@ static int tls_sess_init(void) {
   pr_help_add(C_PROT, _("<sp> protection code"), TRUE);
 
   if (tls_opts & TLS_OPT_USE_IMPLICIT_SSL) {
+    uint64_t start_ms;
+
     tls_log("%s", "TLSOption UseImplicitSSL in effect, starting SSL/TLS "
       "handshake");
 
+    if (pr_trace_get_level(timing_channel) > 0) {
+      pr_gettimeofday_millis(&start_ms);
+    }
+
     if (tls_accept(session.c, FALSE) < 0) {
       tls_log("%s", "implicit SSL/TLS negotiation failed on control channel");
 
@@ -10279,6 +14834,21 @@ static int tls_sess_init(void) {
       tls_flags |= TLS_SESS_NEED_DATA_PROT;
     }
 
+    if (pr_trace_get_level(timing_channel) >= 4) {
+      unsigned long elapsed_ms;
+      uint64_t finish_ms;
+
+      pr_gettimeofday_millis(&finish_ms);
+
+      elapsed_ms = (unsigned long) (finish_ms - session.connect_time_ms);
+      pr_trace_msg(timing_channel, 4,
+        "Time before TLS ctrl handshake: %lu ms", elapsed_ms);
+
+      elapsed_ms = (unsigned long) (finish_ms - start_ms);
+      pr_trace_msg(timing_channel, 4,
+        "TLS ctrl handshake duration: %lu ms", elapsed_ms);
+    }
+
     pr_session_set_protocol("ftps");
     session.rfc2228_mech = "TLS";
   }
@@ -10290,7 +14860,7 @@ static int tls_sess_init(void) {
 static ctrls_acttab_t tls_acttab[] = {
   { "clear", NULL, NULL, NULL },
   { "info", NULL, NULL, NULL },
-  { "remove", NULL, NULL, NULL },
+  { "ocspcache", NULL, NULL, NULL },
   { "sesscache", NULL, NULL, NULL },
  
   { NULL, NULL, NULL, NULL }
@@ -10315,20 +14885,31 @@ static conftable tls_conftab[] = {
   { "TLSDSACertificateKeyFile",	set_tlsdsakeyfile,	NULL },
   { "TLSECCertificateFile",	set_tlseccertfile,	NULL },
   { "TLSECCertificateKeyFile",	set_tlseckeyfile,	NULL },
+  { "TLSECDHCurve",		set_tlsecdhcurve,	NULL },
   { "TLSEngine",		set_tlsengine,		NULL },
   { "TLSLog",			set_tlslog,		NULL },
   { "TLSMasqueradeAddress",	set_tlsmasqaddr,	NULL },
+  { "TLSNextProtocol",		set_tlsnextprotocol,	NULL },
   { "TLSOptions",		set_tlsoptions,		NULL },
   { "TLSPassPhraseProvider",	set_tlspassphraseprovider, NULL },
   { "TLSPKCS12File", 		set_tlspkcs12file,	NULL },
+  { "TLSPreSharedKey",		set_tlspresharedkey,	NULL },
   { "TLSProtocol",		set_tlsprotocol,	NULL },
   { "TLSRandomSeed",		set_tlsrandseed,	NULL },
   { "TLSRenegotiate",		set_tlsrenegotiate,	NULL },
   { "TLSRequired",		set_tlsrequired,	NULL },
   { "TLSRSACertificateFile",	set_tlsrsacertfile,	NULL },
   { "TLSRSACertificateKeyFile",	set_tlsrsakeyfile,	NULL },
-  { "TLSServerCipherPreference",set_tlsservercipherpreference,NULL },
+  { "TLSServerCipherPreference",set_tlsservercipherpreference, NULL },
+  { "TLSServerInfoFile",	set_tlsserverinfofile,	NULL },
   { "TLSSessionCache",		set_tlssessioncache,	NULL },
+  { "TLSSessionTicketKeys",	set_tlssessionticketkeys, NULL },
+  { "TLSSessionTickets",	set_tlssessiontickets,	NULL },
+  { "TLSStapling",		set_tlsstapling,	NULL },
+  { "TLSStaplingCache",		set_tlsstaplingcache,	NULL },
+  { "TLSStaplingOptions",	set_tlsstaplingoptions,	NULL },
+  { "TLSStaplingResponder",	set_tlsstaplingresponder, NULL },
+  { "TLSStaplingTimeout",	set_tlsstaplingtimeout,	NULL },
   { "TLSTimeoutHandshake",	set_tlstimeouthandshake,NULL },
   { "TLSUserName",		set_tlsusername,	NULL },
   { "TLSVerifyClient",		set_tlsverifyclient,	NULL },
@@ -10345,8 +14926,7 @@ static cmdtable tls_cmdtab[] = {
   { CMD,	C_PBSZ,	G_NONE,	tls_pbsz,	FALSE,	FALSE,	CL_SEC },
   { CMD,	C_PROT,	G_NONE,	tls_prot,	FALSE,	FALSE,	CL_SEC },
   { CMD,	"SSCN",	G_NONE,	tls_sscn,	TRUE,	FALSE,	CL_SEC },
-  { POST_CMD,	C_HOST,	G_NONE,	tls_post_host,	FALSE,	FALSE,	CL_SEC },
-  { POST_CMD,	C_PASS,	G_NONE,	tls_post_pass,	FALSE,	FALSE,	CL_SEC },
+  { POST_CMD,	C_PASS,	G_NONE,	tls_post_pass,	FALSE,	FALSE },
   { 0,	NULL }
 };
 
diff --git a/contrib/mod_tls.h b/contrib/mod_tls.h
index 32078d1..2f9b766 100644
--- a/contrib/mod_tls.h
+++ b/contrib/mod_tls.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_tls API
- * Copyright (c) 2002-2011 TJ Saunders
+ * Copyright (c) 2002-2016 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
@@ -20,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_tls.h,v 1.2 2011-05-23 20:44:41 castaglia Exp $
  */
 
 #ifndef MOD_TLS_H
@@ -32,6 +30,9 @@
 #include <openssl/err.h>
 #include <openssl/bio.h>
 #include <openssl/ssl.h>
+#if defined(PR_USE_OPENSSL_OCSP)
+# include <openssl/ocsp.h>
+#endif /* PR_USE_OPENSSL_OCSP */
 
 /* For mod_tls-related modules wishing to log info to the TLSLog file. */
 int tls_log(const char *, ...)
@@ -84,11 +85,11 @@ typedef struct sess_cache_st {
     unsigned int sess_id_len, time_t expires, SSL_SESSION *sess);
 
   /* Retrieve a session from the cache, using the provided sess_id key. */
-  SSL_SESSION *(*get)(struct sess_cache_st *cache, unsigned char *sess_id,
+  SSL_SESSION *(*get)(struct sess_cache_st *cache, const unsigned char *sess_id,
     unsigned int sess_id_len);
 
   /* Remove the specified session from the cache. */
-  int (*delete)(struct sess_cache_st *cache, unsigned char *sess_id,
+  int (*delete)(struct sess_cache_st *cache, const unsigned char *sess_id,
     unsigned int sess_id_len);
 
   /* Clear the cache of all sessions, regardless of their normal expiration
@@ -120,4 +121,62 @@ typedef struct sess_cache_st {
 int tls_sess_cache_register(const char *name, tls_sess_cache_t *handler);
 int tls_sess_cache_unregister(const char *name);
 
+/* API for modules that which to register OCSP response cache handlers. */
+
+typedef struct ocsp_cache_st {
+  const char *cache_name;
+
+  /* Memory pool for this cache. */
+  pool *cache_pool;
+
+  /* Arbitrary cache-specific data */
+  void *cache_data;
+
+  /* Initialize the cache handler. Returns zero on success, -1 otherwise (with
+   * errno set appropriately).
+   */
+  int (*open)(struct ocsp_cache_st *cache, char *info);
+
+  /* Destroy the cache handler, cleaning up any associated resources.  Returns
+   * zero on success, -1 otherwise (with errno set appropriately).
+   */
+  int (*close)(struct ocsp_cache_st *cache);
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  /* Add a new OCSP response to the cache.  The provided cert_fingerprint
+   * (a hex-encoded, NUL-terminated string) is effectively the cache lookup key.
+   */
+  int (*add)(struct ocsp_cache_st *cache, const char *cert_fingerprint,
+    OCSP_RESPONSE *resp, time_t age);
+
+  /* Retrieve an OCSP response from the cache, using the provided lookup key. */
+  OCSP_RESPONSE *(*get)(struct ocsp_cache_st *cache,
+    const char *cert_fingerprint, time_t *age);
+#endif /* PR_USE_OPENSSL_OCSP */
+
+  /* Remove the specified certificate's response from the cache. */
+  int (*delete)(struct ocsp_cache_st *cache, const char *cert_fingerprint);
+
+  /* Clear the cache of all OCSP responses, regardless of their normal
+   * expiration time.  Returns the number of cleared responses on success,
+   * -1 otherwise (with errno set appropriately).
+   */
+  int (*clear)(struct ocsp_cache_st *cache);
+
+  /* Remove the entire cache.  Returns zero on success, -1 otherwise (with
+   * errno set appropriately).
+   */
+  int (*remove)(struct ocsp_cache_st *cache);
+
+  /* Query the cache for information: count of responses currently cached,
+   * hits/misses/expirations, etc.  Returns zero on success, -1 otherwise
+   * (with errno set appropriately).
+   */
+  int (*status)(struct ocsp_cache_st *cache, void (*writef)(void *, const char *, ...), void *arg, int flags);
+
+} tls_ocsp_cache_t;
+
+int tls_ocsp_cache_register(const char *name, tls_ocsp_cache_t *handler);
+int tls_ocsp_cache_unregister(const char *name);
+
 #endif /* MOD_TLS_H */
diff --git a/contrib/mod_tls_fscache.c b/contrib/mod_tls_fscache.c
new file mode 100644
index 0000000..233d69b
--- /dev/null
+++ b/contrib/mod_tls_fscache.c
@@ -0,0 +1,714 @@
+/*
+ * ProFTPD: mod_tls_fscache -- a module which provides a shared OCSP response
+ *                              cache using the filesystem
+ * Copyright (c) 2015-2016 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_tls_fscache, contrib software for proftpd 1.3.x and above.
+ * For more information contact TJ Saunders <tj at castaglia.org>.
+ */
+
+#include "conf.h"
+#include "privs.h"
+#include "mod_tls.h"
+
+#define MOD_TLS_FSCACHE_VERSION			"mod_tls_fscache/0.0"
+
+/* Make sure the version of proftpd is as necessary. */
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
+#endif
+
+extern xaset_t *server_list;
+
+module tls_fscache_module;
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static tls_ocsp_cache_t ocsp_cache;
+#endif /* PR_USE_OPENSSL_OCSP */
+
+static const char *trace_channel = "tls.fscache";
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static const char *fscache_get_errors(void) {
+  unsigned int count = 0;
+  unsigned long error_code;
+  BIO *bio = NULL;
+  char *data = NULL;
+  long datalen;
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
+
+  /* Use ERR_print_errors() and a memory BIO to build up a string with
+   * all of the error messages from the error queue.
+   */
+
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
+    bio = BIO_new(BIO_s_mem());
+  }
+
+  while (error_code) {
+    pr_signals_handle();
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  }
+
+  datalen = BIO_get_mem_data(bio, &data);
+  if (data) {
+    data[datalen] = '\0';
+    str = pstrdup(permanent_pool, data);
+  }
+
+  if (bio != NULL) {
+    BIO_free(bio);
+  }
+
+  return str;
+}
+
+/* OCSP Cache implementation callbacks.
+ */
+
+static int ocsp_cache_open(tls_ocsp_cache_t *cache, char *info) {
+  int res, xerrno = 0;
+  struct stat st;
+
+  pr_trace_msg(trace_channel, 9, "opening fscache cache %p", cache);
+
+  /* The info string must be formatted like:
+   *
+   *  /path=%s
+   */
+
+  if (strncmp(info, "/path=", 6) != 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_FSCACHE_VERSION
+      ": badly formatted info '%s', unable to open fscache", info);
+    errno = EINVAL;
+    return -1;
+  }
+
+  info += 6;
+
+  if (pr_fs_valid_path(info) < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_FSCACHE_VERSION
+      ": path '%s' not an absolute path", info);
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = lstat(info, &st);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_FSCACHE_VERSION
+      ": unable to check '%s': %s", info, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (!S_ISDIR(st.st_mode)) {
+    xerrno = ENOTDIR;
+
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_FSCACHE_VERSION
+      ": unable to use '%s': %s", info, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  /* Make sure that the directory is not world-writable; we don't want
+   * any/all users on the system to be able to futz with these.
+   */
+  if (st.st_mode & S_IWOTH) {
+    xerrno = EPERM;
+
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_FSCACHE_VERSION
+      ": unable to use world-writable '%s' (perms %04o)", info,
+      st.st_mode & ~S_IFMT);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (cache->cache_pool != NULL) {
+    char *prev_cache_dir;
+
+    /* XXX Are we a restarted server if this happens?
+     *
+     * If so, AND if cache->cache_data is NOT NULL, AND points to a
+     * directory which is NOT 'path', then should we clean up that
+     * other directory?  (No; it could be used by another process on
+     * the machine, e.g. multiple different proftpd servers.)
+     *
+     * For now, we complain about this, and tell the admin to manually remove
+     * the old directory.
+     */
+
+    prev_cache_dir = cache->cache_data;
+    if (prev_cache_dir != NULL &&
+        strcmp(prev_cache_dir, info) != 0) {
+      pr_log_pri(PR_LOG_DEBUG, MOD_TLS_FSCACHE_VERSION
+        ": path '%s' does not match previously configured path '%s'",
+        info, prev_cache_dir);
+    }
+
+    destroy_pool(cache->cache_pool);
+  }
+
+  cache->cache_pool = make_sub_pool(session.pool);
+  pr_pool_tag(cache->cache_pool, MOD_TLS_FSCACHE_VERSION);
+
+  cache->cache_data = pstrdup(cache->cache_pool, info);
+  return 0;
+}
+
+static int ocsp_cache_close(tls_ocsp_cache_t *cache) {
+
+  if (cache != NULL) {
+    pr_trace_msg(trace_channel, 9, "closing fscache cache %p", cache);
+
+    if (cache->cache_pool != NULL) {
+      destroy_pool(cache->cache_pool);
+
+      /* XXX TODO */
+    }
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_add(tls_ocsp_cache_t *cache, const char *fingerprint,
+    OCSP_RESPONSE *resp, time_t resp_age) {
+  int fd, res, resp_derlen = -1, xerrno = 0;
+  unsigned char *resp_der = NULL;
+  const char *cache_dir;
+  char *path, *tmpl;
+  pool *tmp_pool;
+  struct timeval tvs[2];
+
+  pr_trace_msg(trace_channel, 9, "adding OCSP response to fscache cache %p",
+    cache);
+
+  resp_derlen = i2d_OCSP_RESPONSE(resp, &resp_der);
+  if (resp_derlen <= 0) {
+    pr_trace_msg(trace_channel, 1,
+      "error DER-encoding OCSP response: %s", fscache_get_errors());
+    errno = EINVAL;
+    return -1;
+  }
+
+  cache_dir = cache->cache_data;
+  tmp_pool = make_sub_pool(cache->cache_pool);
+  pr_pool_tag(tmp_pool, "OCSP fscache add pool");
+
+  tmpl = pdircat(tmp_pool, cache_dir, "XXXXXX", NULL);
+  fd = mkstemp(tmpl);
+  if (fd < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1,
+      "unable to obtain secure temporary file for OCSP response: %s",
+      strerror(xerrno));
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 15,
+    "writing OCSP response to temporary file '%s'", tmpl);
+
+  res = write(fd, resp_der, resp_derlen);
+  if (res != resp_derlen) {
+    if (res < 0) {
+      xerrno = errno;
+
+      pr_trace_msg(trace_channel, 1,
+        "error writing OCSP response to '%s' (fd %d): %s", tmpl, fd,
+        strerror(xerrno));
+      errno = xerrno;
+
+    } else {
+      /* XXX Deal with short writes? */
+
+      pr_trace_msg(trace_channel, 1,
+        "only wrote %d of %d bytes of OCSP response to '%s' (fd %d)", res,
+        resp_derlen, tmpl, fd);
+      xerrno = EIO;
+    }
+
+    (void) unlink(tmpl);
+    (void) close(fd);
+    destroy_pool(tmp_pool);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = close(fd);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1,
+      "error writing OCSP response to '%s': %s", tmpl, strerror(xerrno));
+
+    (void) unlink(tmpl);
+    destroy_pool(tmp_pool);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  /* We ASSUME that rename(2) does not modify the mtime of the file.  Ideally
+   * we would futimes(2) on the file descriptor, but calling close(2) on that
+   * fd might also change the mtime (due to close flushing out buffered data),
+   * thus we use tha path.
+   */
+  tvs[0].tv_sec = tvs[1].tv_sec = resp_age;
+  tvs[0].tv_usec = tvs[1].tv_usec = 0;
+  res = utimes(tmpl, tvs);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 9,
+      "error setting atime/mtime on '%s' to %lu secs: %s", tmpl,
+      (unsigned long) resp_age, strerror(errno));
+  }
+
+  /* Atomically rename the temporary file into place. */
+  path = pstrcat(tmp_pool, cache_dir, "/", fingerprint, ".der", NULL);
+  res = rename(tmpl, path);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1,
+      "error renaming '%s' to '%s': %s", tmpl, path, strerror(xerrno));
+
+    (void) unlink(tmpl);
+
+  } else {
+    pr_trace_msg(trace_channel, 15, "renamed '%s' to '%s' (%d bytes)", tmpl,
+      path, resp_derlen);
+  } 
+
+  destroy_pool(tmp_pool);
+  errno = xerrno;
+  return res;
+}
+
+static OCSP_RESPONSE *ocsp_cache_get(tls_ocsp_cache_t *cache,
+    const char *fingerprint, time_t *resp_age) {
+  int res, xerrno = 0;
+  const char *cache_dir, *path;
+  pool *tmp_pool;
+  BIO *bio = NULL;
+  OCSP_RESPONSE *resp = NULL;
+  struct stat st;
+  pr_fh_t *fh;
+
+  pr_trace_msg(trace_channel, 9, "getting OCSP response from fscache cache %p",
+    cache); 
+
+  cache_dir = cache->cache_data;
+  tmp_pool = make_sub_pool(cache->cache_pool);
+  pr_pool_tag(tmp_pool, "OCSP fscache retrieval pool");
+
+  path = pstrcat(tmp_pool, cache_dir, "/", fingerprint, ".der", NULL);
+  pr_trace_msg(trace_channel, 15, "getting OCSP response at path '%s'", path);
+
+  fh = pr_fsio_open(path, O_RDONLY);
+  if (fh == NULL) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3, "error opening '%s': %s", path,
+      strerror(xerrno));
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return NULL;
+  }
+
+  res = pr_fsio_fstat(fh, &st);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3, "error checking '%s': %s", path,
+      strerror(xerrno));
+
+    (void) pr_fsio_close(fh);
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return NULL;
+  }
+
+  /* No symlinks or directories, only regular files. */
+  if (!S_ISREG(st.st_mode)) {
+    pr_trace_msg(trace_channel, 3, "path '%s' is NOT a regular file", path);
+
+    /* If it's a symlink, remove it.  We cannot just do the same with
+     * a directory.
+     */
+    if (S_ISLNK(st.st_mode)) {
+      (void) unlink(path);
+    }
+
+    (void) pr_fsio_close(fh);
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  bio = BIO_new_file(path, "r");
+  if (bio == NULL) {
+    xerrno = errno;
+
+    tls_log(MOD_TLS_FSCACHE_VERSION ": BIO_new_file('%s') failed: %s", path,
+      fscache_get_errors());
+    (void) pr_fsio_close(fh);
+    destroy_pool(tmp_pool);
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  resp = d2i_OCSP_RESPONSE_bio(bio, NULL);
+  if (resp == NULL) {
+    pr_trace_msg(trace_channel, 3,
+      "error reading valid OCSP response from path '%s': %s", path,
+      fscache_get_errors());
+
+    /* If we can't read a valid OCSP response from this file, delete it. */
+    (void) unlink(path);
+
+    (void) pr_fsio_close(fh);
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  BIO_free(bio);
+
+  /* Use the mtime of the file to determine how old it is. */
+  *resp_age = st.st_mtime;
+
+  (void) pr_fsio_close(fh);
+  destroy_pool(tmp_pool);
+  errno = xerrno;
+  return resp;
+}
+
+static int ocsp_cache_delete(tls_ocsp_cache_t *cache, const char *fingerprint) {
+  int res, xerrno = 0;
+  const char *cache_dir, *path;
+  pool *tmp_pool;
+
+  pr_trace_msg(trace_channel, 9,
+    "removing OCSP response from fscache cache %p", cache);
+
+  cache_dir = cache->cache_data;
+  tmp_pool = make_sub_pool(cache->cache_pool);
+  pr_pool_tag(tmp_pool, "OCSP fscache delete pool");
+
+  path = pstrcat(tmp_pool, cache_dir, "/", fingerprint, ".der", NULL);
+  pr_trace_msg(trace_channel, 15, "deleting OCSP response at path '%s'", path);
+
+  /* XXX Do we need root privs here?  Should we use the FSIO API? */
+  res = unlink(path);
+  xerrno = errno;
+
+  destroy_pool(tmp_pool);
+  errno = xerrno;
+  return res;
+}
+
+static int ocsp_cache_clear(tls_ocsp_cache_t *cache) {
+  int res, xerrno = 0;
+  const char *cache_dir;
+  pool *tmp_pool;
+  DIR *dirh;
+  struct dirent *dent;
+
+  pr_trace_msg(trace_channel, 9, "clearing fscache cache %p", cache); 
+
+  cache_dir = cache->cache_data;
+
+  dirh = opendir(cache_dir);
+  if (dirh == NULL) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3, "unable to open directory '%s': %s",
+      cache_dir, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(cache->cache_pool);
+  pr_pool_tag(tmp_pool, "OCSP fscache clear pool");
+
+  dent = readdir(dirh);
+  while (dent != NULL) {
+    struct stat st;
+    size_t namelen;
+
+    pr_signals_handle();
+
+    namelen = strlen(dent->d_name);
+    /* Skip any path which does not end in ".der". */
+    if (pr_strnrstr(dent->d_name, namelen, ".der", 4, 0) == TRUE) {
+      pr_fh_t *fh;
+      char *path;
+
+      path = pstrcat(tmp_pool, cache_dir, "/", dent->d_name, ".der", NULL);
+
+      fh = pr_fsio_open(path, O_RDONLY);
+      if (fh != NULL) {
+        res = pr_fsio_fstat(fh, &st);
+        if (res < 0) {
+          pr_trace_msg(trace_channel, 3, "error checking path '%s': %s", path,
+            strerror(errno));
+
+        } else {
+          if (S_ISREG(st.st_mode) ||
+              S_ISLNK(st.st_mode)) {
+
+            pr_trace_msg(trace_channel, 15,
+              "deleting OCSP response at path '%s'", path);
+            res = unlink(path);
+            if (res < 0) {
+              pr_trace_msg(trace_channel, 3,
+                "error deleting path '%s': %s", path, strerror(errno));
+            }
+
+          } else {
+            pr_trace_msg(trace_channel, 3,
+              "ignoring non-file/symlink path '%s'", path);
+          }
+        }
+
+        (void) pr_fsio_close(fh);
+
+      } else {
+        pr_trace_msg(trace_channel, 3, "error opening path '%s': %s", path,
+          strerror(errno));
+      }
+    }
+
+    dent = readdir(dirh);
+  }
+
+  (void) closedir(dirh);
+  destroy_pool(tmp_pool);
+
+  return 0;
+}
+
+static int ocsp_cache_remove(tls_ocsp_cache_t *cache) {
+  int res;
+
+  if (cache == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9, "removing fscache cache %p", cache);
+  res = ocsp_cache_clear(cache);
+  return res;
+}
+
+static int ocsp_cache_status(tls_ocsp_cache_t *cache,
+    void (*statusf)(void *, const char *, ...), void *arg, int flags) {
+  int res, xerrno = 0;
+  unsigned int resp_count = 0;
+  const char *cache_dir;
+  pool *tmp_pool;
+  DIR *dirh;
+  struct dirent *dent;
+
+  pr_trace_msg(trace_channel, 9, "checking fscache cache %p", cache); 
+
+  /* XXX TODO:
+   *  If flags says "SHOW RESPONSES", print out the PEM versions of the
+   *  responses?
+   */
+
+  cache_dir = cache->cache_data;
+
+  dirh = opendir(cache_dir);
+  if (dirh == NULL) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3, "unable to open directory '%s': %s",
+      cache_dir, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(cache->cache_pool);
+  pr_pool_tag(tmp_pool, "OCSP fscache status pool");
+
+  dent = readdir(dirh);
+  while (dent != NULL) {
+    struct stat st;
+    size_t namelen;
+
+    pr_signals_handle();
+
+    /* Skip any path which does not end in ".der". */
+    namelen = strlen(dent->d_name);
+    if (pr_strnrstr(dent->d_name, namelen, ".der", 4, 0) == TRUE) {
+      pr_fh_t *fh;
+      char *path;
+
+      path = pstrcat(tmp_pool, cache_dir, "/", dent->d_name, ".der", NULL);
+
+      fh = pr_fsio_open(path, O_RDONLY);
+      if (fh != NULL) {
+        res = pr_fsio_fstat(fh, &st);
+        if (res < 0) {
+          pr_trace_msg(trace_channel, 3, "error checking path '%s': %s", path,
+            strerror(errno));
+
+        } else {
+          if (S_ISREG(st.st_mode) ||
+              S_ISLNK(st.st_mode)) {
+            resp_count++;
+
+          } else {
+            pr_trace_msg(trace_channel, 3,
+              "ignoring non-file/symlink path '%s'", path);
+          }
+        }
+
+        (void) pr_fsio_close(fh);
+
+      } else {
+        pr_trace_msg(trace_channel, 3, "error opening path '%s': %s", path,
+          strerror(errno));
+      }
+    }
+
+    dent = readdir(dirh);
+  }
+
+  (void) closedir(dirh);
+  destroy_pool(tmp_pool);
+
+  statusf(arg, "%s", "Filesystem (fs) OCSP response cache provided by "
+    MOD_TLS_FSCACHE_VERSION);
+  statusf(arg, "%s", "");
+  statusf(arg, "Current OCSP responses cached: %u", resp_count);
+
+  return 0;
+}
+#endif /* PR_USE_OPENSSL_OCSP */
+
+/* Event Handlers
+ */
+
+#if defined(PR_SHARED_MODULE)
+static void fscache_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_tls_fscache.c", (const char *) event_data) == 0) {
+    pr_event_unregister(&tls_fscache_module, NULL, NULL);
+    (void) tls_ocsp_cache_unregister("fs");
+  }
+}
+#endif /* !PR_SHARED_MODULE */
+
+/* Initialization functions
+ */
+
+static int tls_fscache_init(void) {
+#if defined(PR_USE_OPENSSL_OCSP)
+# if defined(PR_SHARED_MODULE)
+  pr_event_register(&tls_fscache_module, "core.module-unload",
+    fscache_mod_unload_ev, NULL);
+# endif /* !PR_SHARED_MODULE */
+
+  /* Prepare our cache handler. */
+  memset(&ocsp_cache, 0, sizeof(ocsp_cache));
+  ocsp_cache.open = ocsp_cache_open;
+  ocsp_cache.close = ocsp_cache_close;
+  ocsp_cache.add = ocsp_cache_add;
+  ocsp_cache.get = ocsp_cache_get;
+  ocsp_cache.delete = ocsp_cache_delete;
+  ocsp_cache.clear = ocsp_cache_clear;
+  ocsp_cache.remove = ocsp_cache_remove;
+  ocsp_cache.status = ocsp_cache_status;
+
+  /* Register ourselves with mod_tls. */
+  if (tls_ocsp_cache_register("fs", &ocsp_cache) < 0) {
+    int xerrno = errno;
+
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_FSCACHE_VERSION
+      ": notice: error registering 'fs' OCSP cache: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+#endif /* PR_USE_OPENSSL_OCSP */
+
+  return 0;
+}
+
+/* Module API tables
+ */
+
+module tls_fscache_module = {
+  NULL, NULL,
+
+  /* Module API version 2.0 */
+  0x20,
+
+  /* Module name */
+  "tls_fscache",
+
+  /* Module configuration handler table */
+  NULL,
+
+  /* Module command handler table */
+  NULL,
+
+  /* Module authentication handler table */
+  NULL,
+
+  /* Module initialization function */
+  tls_fscache_init,
+
+  /* Session initialization function */
+  NULL,
+
+  /* Module version */
+  MOD_TLS_FSCACHE_VERSION
+};
diff --git a/contrib/mod_tls_memcache.c b/contrib/mod_tls_memcache.c
index f667a1d..74bc727 100644
--- a/contrib/mod_tls_memcache.c
+++ b/contrib/mod_tls_memcache.c
@@ -1,8 +1,7 @@
 /*
- * ProFTPD: mod_tls_memcache -- a module which provides a shared SSL session
- *                              cache using memcached servers
- *
- * Copyright (c) 2011-2013 TJ Saunders
+ * ProFTPD: mod_tls_memcache -- a module which provides shared SSL session
+ *                              and OCSP response caches using memcached servers
+ * Copyright (c) 2011-2017 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
@@ -25,25 +24,26 @@
  *
  * This is mod_tls_memcache, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- *  --- DO NOT DELETE BELOW THIS LINE ----
- *  $Id: mod_tls_memcache.c,v 1.6 2013-11-05 21:37:16 castaglia Exp $
- *  $Libraries: -lssl -lcrypto$
  */
 
 #include "conf.h"
 #include "privs.h"
 #include "mod_tls.h"
+#include "json.h"
+#include "hanson-tpl.h"
 
-#define MOD_TLS_MEMCACHE_VERSION		"mod_tls_memcache/0.1"
+#define MOD_TLS_MEMCACHE_VERSION		"mod_tls_memcache/0.2"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030402
-# error "ProFTPD 1.3.4rc2 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 module tls_memcache_module;
 
+/* For communicating with memcached servers for shared data. */
+static pr_memcache_t *sess_mcache = NULL;
+
 /* Assume a maximum SSL session (serialized) length of 10K.  Note that this
  * is different from the SSL_MAX_SSL_SESSION_ID_LENGTH provided by OpenSSL.
  * There is no limit imposed on the length of the ASN1 description of the
@@ -53,41 +53,48 @@ module tls_memcache_module;
 # define TLS_MAX_SSL_SESSION_SIZE	1024 * 10
 #endif
 
-struct mcache_entry {
+static unsigned long sess_cache_opts = 0UL;
+#define SESS_CACHE_OPT_USE_JSON		0x0001
+
+struct sesscache_entry {
   uint32_t expires;
   unsigned int sess_datalen;
   unsigned char sess_data[TLS_MAX_SSL_SESSION_SIZE];
 };
 
 /* These are tpl format strings */
-#define TLS_MCACHE_KEY_FMT		"s"
-#define TLS_MCACHE_VALUE_FMT		"S(uic#)"
+#define SESS_CACHE_TPL_KEY_FMT			"s"
+#define SESS_CACHE_TPL_VALUE_FMT		"S(uic#)"
 
-/* The difference between mcache_entry and mcache_large_entry is that the
+/* These are the JSON format field names */
+#define SESS_CACHE_JSON_KEY_EXPIRES		"expires"
+#define SESS_CACHE_JSON_KEY_DATA		"data"
+#define SESS_CACHE_JSON_KEY_DATA_LENGTH		"data_len"
+
+/* The difference between sesscache_entry and sesscache_large_entry is that the
  * buffers in the latter are dynamically allocated from the heap, not
  * stored in memcached (given that memcached has limits on how much it can
  * store).  The large_entry struct is used for storing sessions which don't
  * fit into memcached; this also means that these large entries are NOT shared
  * across processes.
  */
-struct mcache_large_entry {
+struct sesscache_large_entry {
   time_t expires;
   unsigned int sess_id_len;
-  unsigned char *sess_id;
+  const unsigned char *sess_id;
   unsigned int sess_datalen;
-  unsigned char *sess_data;
+  const unsigned char *sess_data;
 };
 
 /* These stats are stored in memcached as well, so that the status command can
  * be run on _any_ proftpd in the cluster.
  */
-
-struct cache_key {
+struct sesscache_key {
   const char *key;
   const char *desc;
 };
 
-static struct cache_key cache_keys[] = {
+static struct sesscache_key sesscache_keys[] = {
   { "cache_hits", "Cache lifetime hits" },
   { "cache_misses", "Cache lifetime misses" },
   { "cache_stores", "Cache lifetime sessions stored" },
@@ -98,45 +105,127 @@ static struct cache_key cache_keys[] = {
   { NULL, NULL }
 };
 
-/* Indexes into the cache_keys array */
-#define CACHE_KEY_HITS		0
-#define CACHE_KEY_MISSES	1
-#define CACHE_KEY_STORES	2
-#define CACHE_KEY_DELETES	3
-#define CACHE_KEY_ERRORS	4
-#define CACHE_KEY_EXCEEDS	5
-#define CACHE_KEY_MAX_LEN	6
+/* Indexes into the sesscache_keys array */
+#define SESSCACHE_KEY_HITS	0
+#define SESSCACHE_KEY_MISSES	1
+#define SESSCACHE_KEY_STORES	2
+#define SESSCACHE_KEY_DELETES	3
+#define SESSCACHE_KEY_ERRORS	4
+#define SESSCACHE_KEY_EXCEEDS	5
+#define SESSCACHE_KEY_MAX_LEN	6
+
+static tls_sess_cache_t sess_cache;
+static array_header *sesscache_sess_list = NULL;
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static pr_memcache_t *ocsp_mcache = NULL;
+
+/* Assume a maximum OCSP response (serialized) length of 4K. */
+# ifndef TLS_MAX_OCSP_RESPONSE_SIZE
+#  define TLS_MAX_OCSP_RESPONSE_SIZE		1024 * 4
+# endif
+
+struct ocspcache_entry {
+  time_t age;
+  unsigned int fingerprint_len;
+  char fingerprint[EVP_MAX_MD_SIZE];
+  unsigned int resp_derlen;
+  unsigned char resp_der[TLS_MAX_OCSP_RESPONSE_SIZE];
+};
+
+/* These are the JSON format field names */
+#define OCSP_CACHE_JSON_KEY_AGE			"expires"
+#define OCSP_CACHE_JSON_KEY_RESPONSE		"response"
+#define OCSP_CACHE_JSON_KEY_RESPONSE_LENGTH	"response_len"
 
-static tls_sess_cache_t tls_mcache;
+/* The difference between ocspcache_entry and ocspcache_large_entry is that the
+ * buffers in the latter are dynamically allocated from the heap, not
+ * stored in memcached (given that memcached has limits on how much it can
+ * store).  The large_entry struct is used for storing responses which don't
+ * fit into memcached; this also means that these large entries are NOT shared
+ * across processes.
+ */
+struct ocspcache_large_entry {
+  time_t age;
+  unsigned int fingerprint_len;
+  char *fingerprint;
+  unsigned int resp_derlen;
+  unsigned char *resp_der;
+};
+
+/* These stats are stored in memcached as well, so that the status command can
+ * be run on _any_ proftpd in the cluster.
+ */
+struct ocspcache_key {
+  const char *key;
+  const char *desc;
+};
 
-static array_header *tls_mcache_sess_list = NULL;
+static struct ocspcache_key ocspcache_keys[] = {
+  { "cache_hits", "Cache lifetime hits" },
+  { "cache_misses", "Cache lifetime misses" },
+  { "cache_stores", "Cache lifetime responses stored" },
+  { "cache_deletes", "Cache lifetime responses deleted" },
+  { "cache_errors", "Cache lifetime errors handling responses in cache" },
+  { "cache_exceeds", "Cache lifetime responses exceeding max entry size" },
+  { "cache_max_resp_len", "Largest response exceeding max entry size" },
+  { NULL, NULL }
+};
 
-/* For communicating with memcached servers for shared session data. */
-static pr_memcache_t *mcache = NULL;
+/* Indexes into the ocspcache_keys array */
+#define OCSPCACHE_KEY_HITS	0
+#define OCSPCACHE_KEY_MISSES	1
+#define OCSPCACHE_KEY_STORES	2
+#define OCSPCACHE_KEY_DELETES	3
+#define OCSPCACHE_KEY_ERRORS	4
+#define OCSPCACHE_KEY_EXCEEDS	5
+#define OCSPCACHE_KEY_MAX_LEN	6
+
+static tls_ocsp_cache_t ocsp_cache;
+static array_header *ocspcache_resp_list = NULL;
+#endif
 
-static const char *trace_channel = "tls_memcache";
+static const char *trace_channel = "tls.memcache";
 
-static int tls_mcache_close(tls_sess_cache_t *);
+static int sess_cache_close(tls_sess_cache_t *);
+#if defined(PR_USE_OPENSSL_OCSP)
+static int ocsp_cache_close(tls_ocsp_cache_t *);
+#endif /* PR_USE_OPENSSL_OCSP */
+static int tls_mcache_sess_init(void);
 
-static const char *tls_mcache_get_crypto_errors(void) {
+static const char *mcache_get_errors(void) {
   unsigned int count = 0;
-  unsigned long e = ERR_get_error();
+  unsigned long error_code;
   BIO *bio = NULL;
   char *data = NULL;
   long datalen;
-  const char *str = "(unknown)";
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
 
   /* Use ERR_print_errors() and a memory BIO to build up a string with
    * all of the error messages from the error queue.
    */
 
-  if (e)
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
     bio = BIO_new(BIO_s_mem());
+  }
 
-  while (e) {
+  while (error_code) {
     pr_signals_handle();
-    BIO_printf(bio, "\n  (%u) %s", ++count, ERR_error_string(e, NULL));
-    e = ERR_get_error();
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
   }
 
   datalen = BIO_get_mem_data(bio, &data);
@@ -145,33 +234,29 @@ static const char *tls_mcache_get_crypto_errors(void) {
     str = pstrdup(permanent_pool, data);
   }
 
-  if (bio)
+  if (bio != NULL) {
     BIO_free(bio);
+  }
 
   return str;
 }
 
+/* SSL session cache implementation callbacks.
+ */
+
 /* Functions for marshalling key/value data to/from memcached. */
 
-static int tls_mcache_key_get(pool *p, unsigned char *sess_id,
+static int sess_cache_get_tpl_key(pool *p, const unsigned char *sess_id,
     unsigned int sess_id_len, void **key, size_t *keysz) {
-  register unsigned int i;
   char *sess_id_hex;
   void *data = NULL;
-  size_t datasz = 0, sess_id_hexlen;
+  size_t datasz = 0;
   int res;
 
-  sess_id_hexlen = (sess_id_len * 2) + 1;
-  sess_id_hex = pcalloc(p, sess_id_hexlen);
+  sess_id_hex = pr_str_bin2hex(p, sess_id, sess_id_len, 0);
 
-  for (i = 0; i < sess_id_len; i++) {
-    sprintf((char *) &(sess_id_hex[i*2]), "%02X", sess_id[i]);
-  }
-
-  res = tpl_jot(TPL_MEM, &data, &datasz, TLS_MCACHE_KEY_FMT, &sess_id_hex);
+  res = tpl_jot(TPL_MEM, &data, &datasz, SESS_CACHE_TPL_KEY_FMT, &sess_id_hex);
   if (res < 0) {
-    pr_trace_msg(trace_channel, 3,
-      "error constructing cache lookup key for session ID '%s'", sess_id_hex);
     return -1;
   }
 
@@ -183,57 +268,77 @@ static int tls_mcache_key_get(pool *p, unsigned char *sess_id,
   return 0;
 }
 
-static int tls_mcache_entry_get(pool *p, unsigned char *sess_id,
-    unsigned int sess_id_len, struct mcache_entry *me) {
-  tpl_node *tn;
+static int sess_cache_get_json_key(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, void **key, size_t *keysz) {
+  char *sess_id_hex, *json_text;
+  pr_json_object_t *json;
+
+  sess_id_hex = pr_str_bin2hex(p, sess_id, sess_id_len, 0);
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_string(p, json, "id", sess_id_hex);
+
+  json_text = pr_json_object_to_text(p, json, "");
+
+  /* Include the terminating NUL in the key. */
+  *keysz = strlen(json_text) + 1;
+  *key = pstrndup(p, json_text, *keysz - 1);
+  (void) pr_json_object_free(json);
+
+  return 0;
+}
+
+static int sess_cache_get_key(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, void **key, size_t *keysz) {
   int res;
-  void *key = NULL, *value = NULL;
-  size_t keysz = 0, valuesz = 0;
-  uint32_t flags = 0;
+  const char *key_type = "unknown";
 
-  res = tls_mcache_key_get(p, sess_id, sess_id_len, &key, &keysz);
-  if (res < 0) {
-    pr_trace_msg(trace_channel, 1,
-      "unable to get cache entry: error getting cache key: %s",
-      strerror(errno));
+  if (sess_cache_opts & SESS_CACHE_OPT_USE_JSON) {
+    key_type = "JSON";
+    res = sess_cache_get_json_key(p, sess_id, sess_id_len, key, keysz);
 
-    return -1;
+  } else {
+    key_type = "TPL";
+    res = sess_cache_get_tpl_key(p, sess_id, sess_id_len, key, keysz);
   }
 
-  value = pr_memcache_kget(mcache, &tls_memcache_module, (const char *) key,
-    keysz, &valuesz, &flags);
-  if (value == NULL) {
+  if (res < 0) {
     pr_trace_msg(trace_channel, 3,
-      "no matching memcache entry found for session ID '%s'", (char *) key);
-    errno = ENOENT;
+      "error constructing cache %s lookup key for session ID (%lu bytes)",
+      key_type, (unsigned long) keysz);
     return -1;
   }
 
-  /* Unmarshal the session data. */
+  return 0;
+}
 
-  tn = tpl_map(TLS_MCACHE_VALUE_FMT, me, TLS_MAX_SSL_SESSION_SIZE);
+static int sess_cache_entry_decode_tpl(pool *p, void *value, size_t valuesz,
+    struct sesscache_entry *se) {
+  int res;
+  tpl_node *tn;
 
-  res = tpl_load(tn, TPL_MEM, value, valuesz);
-  if (res < 0) {
-    pr_trace_msg(trace_channel, 3, "%s",
-      "error loading marshalled memcache session data");
-    tpl_free(tn);
+  tn = tpl_map(SESS_CACHE_TPL_VALUE_FMT, se, TLS_MAX_SSL_SESSION_SIZE);
+  if (tn == NULL) {
+    tls_log(MOD_TLS_MEMCACHE_VERSION
+      ": error allocating tpl_map for format '%s'", SESS_CACHE_TPL_VALUE_FMT);
+    errno = ENOMEM;
     return -1;
   }
 
   res = tpl_load(tn, TPL_MEM, value, valuesz);
   if (res < 0) {
     pr_trace_msg(trace_channel, 3, "%s",
-      "error loading marshalled memcache session data");
+      "error loading TPL memcache session data");
     tpl_free(tn);
+    errno = EINVAL;
     return -1;
   }
 
   res = tpl_unpack(tn, 0);
   if (res < 0) {
     pr_trace_msg(trace_channel, 3, "%s",
-      "error unpacking marshalled memcache session data");
+      "error unpacking TPL memcache session data");
     tpl_free(tn);
+    errno = EINVAL;
     return -1;
   }
 
@@ -242,13 +347,191 @@ static int tls_mcache_entry_get(pool *p, unsigned char *sess_id,
   return 0;
 }
 
-static int tls_mcache_entry_remove(pool *p, unsigned char *sess_id,
+static int entry_get_json_number(pool *p, pr_json_object_t *json,
+    const char *key, double *val, const char *text) {
+  if (pr_json_object_get_number(p, json, key, val) < 0) {
+    if (errno == EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+       "ignoring non-number '%s' JSON field in '%s'", key, text);
+
+    } else {
+      tls_log(MOD_TLS_MEMCACHE_VERSION
+        ": missing required '%s' JSON field in '%s'", key, text);
+    }
+
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int entry_get_json_string(pool *p, pr_json_object_t *json,
+    const char *key, char **val, const char *text) {
+  if (pr_json_object_get_string(p, json, key, val) < 0) {
+    if (errno == EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+       "ignoring non-string '%s' JSON field in '%s'", key, text);
+
+    } else {
+      tls_log(MOD_TLS_MEMCACHE_VERSION
+        ": missing required '%s' JSON field in '%s'", key, text);
+    }
+
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int sess_cache_entry_decode_json(pool *p, void *value, size_t valuesz,
+    struct sesscache_entry *se) {
+  int res;
+  pr_json_object_t *json;
+  const char *key;
+  char *entry, *text;
+  double number;
+
+  entry = value;
+  if (pr_json_text_validate(p, entry) == FALSE) {
+    tls_log(MOD_TLS_MEMCACHE_VERSION
+      ": unable to decode invalid JSON session cache entry: '%s'", entry);
+    errno = EINVAL;
+    return -1;
+  }
+
+  json = pr_json_object_from_text(p, entry);
+
+  key = SESS_CACHE_JSON_KEY_EXPIRES;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  se->expires = (uint32_t) number;
+
+  key = SESS_CACHE_JSON_KEY_DATA;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res == 0) {
+    int have_padding = FALSE;
+    char *base64_data;
+    size_t base64_datalen;
+    unsigned char *data;
+
+    base64_data = text;
+    base64_datalen = strlen(base64_data);
+
+    /* Due to Base64's padding, we need to detect if the last block was
+     * padded with zeros; we do this by looking for '=' characters at the
+     * end of the text being decoded.  If we see these characters, then we
+     * will "trim" off any trailing zero values in the decoded data, on the
+     * ASSUMPTION that they are the auto-added padding bytes.
+     */
+    if (base64_data[base64_datalen-1] == '=') {
+      have_padding = TRUE;
+    }
+
+    data = se->sess_data;
+    res = EVP_DecodeBlock(data, (unsigned char *) base64_data,
+      (int) base64_datalen);
+    if (res <= 0) {
+      /* Base64-decoding error. */
+      pr_trace_msg(trace_channel, 5,
+        "error base64-decoding session data in '%s', rejecting", entry);
+      (void) pr_json_object_free(json);
+      errno = EINVAL;
+      return -1;
+    }
+
+    if (have_padding) {
+      /* Assume that only one or two zero bytes of padding were added. */
+      if (data[res-1] == '\0') {
+        res -= 1;
+
+        if (data[res-1] == '\0') {
+          res -= 1;
+        }
+      }
+    }
+  } else {
+    return -1;
+  }
+
+  key = SESS_CACHE_JSON_KEY_DATA_LENGTH;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  se->sess_datalen = (unsigned int) number;
+
+  (void) pr_json_object_free(json);
+  return 0;
+}
+
+static int sess_cache_mcache_entry_get(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, struct sesscache_entry *se) {
+  int res;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+  uint32_t flags = 0;
+
+  res = sess_cache_get_key(p, sess_id, sess_id_len, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to get cache entry: error getting cache key: %s",
+      strerror(errno));
+
+    return -1;
+  }
+
+  value = pr_memcache_kget(sess_mcache, &tls_memcache_module,
+    (const char *) key, keysz, &valuesz, &flags);
+  if (value == NULL) {
+    pr_trace_msg(trace_channel, 3,
+      "no matching memcache entry found for session ID (%lu bytes)",
+      (unsigned long) keysz);
+    errno = ENOENT;
+    return -1;
+  }
+
+  /* Decode the cached session data. */
+  if (sess_cache_opts & SESS_CACHE_OPT_USE_JSON) {
+    res = sess_cache_entry_decode_json(p, value, valuesz, se);
+
+  } else {
+    res = sess_cache_entry_decode_tpl(p, value, valuesz, se);
+  }
+
+  if (res == 0) {
+    time_t now;
+
+    /* Check for expired cache entries. */
+    time(&now);
+
+    if (se->expires <= now) {
+      pr_trace_msg(trace_channel, 4,
+        "ignoring expired cached session data (expires %lu <= now %lu)",
+        (unsigned long) se->expires, (unsigned long) now);
+      errno = EPERM;
+      return -1;
+    }
+
+    pr_trace_msg(trace_channel, 9, "retrieved session data from cache using %s",
+      sess_cache_opts & SESS_CACHE_OPT_USE_JSON ? "JSON" : "TPL");
+  }
+
+  return 0;
+}
+
+static int sess_cache_mcache_entry_delete(pool *p, const unsigned char *sess_id,
     unsigned int sess_id_len) {
   int res;
   void *key = NULL;
   size_t keysz = 0;
 
-  res = tls_mcache_key_get(p, sess_id, sess_id_len, &key, &keysz);
+  res = sess_cache_get_key(p, sess_id, sess_id_len, &key, &keysz);
   if (res < 0) {
     pr_trace_msg(trace_channel, 1,
       "unable to remove cache entry: error getting cache key: %s",
@@ -257,90 +540,171 @@ static int tls_mcache_entry_remove(pool *p, unsigned char *sess_id,
     return -1;
   }
 
-  res = pr_memcache_kremove(mcache, &tls_memcache_module, (const char *) key,
-    keysz, 0);
+  res = pr_memcache_kremove(sess_mcache, &tls_memcache_module,
+    (const char *) key, keysz, 0);
   if (res < 0) {
     int xerrno = errno;
 
     pr_trace_msg(trace_channel, 2,
-      "unable to remove memcache entry for session ID '%s': %s", (char *) key,
-      strerror(xerrno));
+      "unable to remove memcache entry for session ID (%lu bytes): %s",
+      (unsigned long) keysz, strerror(xerrno));
 
     errno = xerrno;
     return -1;
   }
  
-  return 0; 
+  return 0;
 }
 
-static int tls_mcache_entry_set(pool *p, unsigned char *sess_id,
-    unsigned int sess_id_len, struct mcache_entry *me) {
-  tpl_node *tn;
+static int sess_cache_entry_encode_tpl(pool *p, void **value, size_t *valuesz,
+    struct sesscache_entry *se) {
   int res;
-  void *key = NULL, *value = NULL;
-  size_t keysz = 0, valuesz = 0;
-  uint32_t flags = 0;
-
-  /* Marshal the SSL session data. */
+  tpl_node *tn;
+  void *ptr = NULL;
 
-  tn = tpl_map(TLS_MCACHE_VALUE_FMT, me, TLS_MAX_SSL_SESSION_SIZE);
+  tn = tpl_map(SESS_CACHE_TPL_VALUE_FMT, se, TLS_MAX_SSL_SESSION_SIZE);
   if (tn == NULL) {
     pr_trace_msg(trace_channel, 1,
-      "error allocating tpl_map for format '%s'", TLS_MCACHE_VALUE_FMT);
+      "error allocating tpl_map for format '%s'", SESS_CACHE_TPL_VALUE_FMT);
     return -1;
   }
 
   res = tpl_pack(tn, 0);
   if (res < 0) {
     pr_trace_msg(trace_channel, 1, "%s",
-      "error marshalling memcache session data");
+      "error marshalling TPL memcache session data");
     return -1;
   }
 
-  res = tpl_dump(tn, TPL_MEM, &value, &valuesz);
+  res = tpl_dump(tn, TPL_MEM, &ptr, valuesz);
   if (res < 0) {
     pr_trace_msg(trace_channel, 1, "%s",
-      "error dumping marshalled memcache session data");
+      "error dumping marshalled TPL memcache session data");
     return -1;
   }
 
+  /* Duplicate the value using the given pool, so that we can free up the
+   * memory allocated by tpl_dump().
+   */
+  *value = palloc(p, *valuesz);
+  memcpy(*value, ptr, *valuesz);
+
   tpl_free(tn);
+  free(ptr);
+
+  return 0;
+}
+
+static int sess_cache_entry_encode_json(pool *p, void **value, size_t *valuesz,
+    struct sesscache_entry *se) {
+  pr_json_object_t *json;
+  pool *tmp_pool;
+  char *base64_data = NULL, *json_text;
+
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_number(p, json, SESS_CACHE_JSON_KEY_EXPIRES,
+    (double) se->expires);
+
+  /* Base64-encode the session data.  Note that EVP_EncodeBlock does
+   * NUL-terminate the encoded data.
+   */
+  tmp_pool = make_sub_pool(p);
+  base64_data = pcalloc(tmp_pool, se->sess_datalen * 2);
+
+  EVP_EncodeBlock((unsigned char *) base64_data, se->sess_data,
+    (int) se->sess_datalen);
+  (void) pr_json_object_set_string(p, json, SESS_CACHE_JSON_KEY_DATA,
+    base64_data);
+  (void) pr_json_object_set_number(p, json, SESS_CACHE_JSON_KEY_DATA_LENGTH,
+    (double) se->sess_datalen);
+
+  destroy_pool(tmp_pool);
+
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
+
+  if (json_text == NULL) {
+    errno = ENOMEM;
+    return -1;
+  }
+
+  /* Safety check */
+  if (pr_json_text_validate(p, json_text) == FALSE) {
+    pr_trace_msg(trace_channel, 1, "invalid JSON emitted: '%s'", json_text);
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Include the terminating NUL in the value. */
+  *valuesz = strlen(json_text) + 1;
+  *value = pstrndup(p, json_text, *valuesz - 1);
+
+  return 0;
+}
+
+static int sess_cache_mcache_entry_set(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, struct sesscache_entry *se) {
+  int res, xerrno = 0;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+  uint32_t flags = 0;
+
+  /* Encode the SSL session data. */
+  if (sess_cache_opts & SESS_CACHE_OPT_USE_JSON) {
+    res = sess_cache_entry_encode_json(p, &value, &valuesz, se);
 
-  res = tls_mcache_key_get(p, sess_id, sess_id_len, &key, &keysz);
+  } else {
+    res = sess_cache_entry_encode_tpl(p, &value, &valuesz, se);
+  }
+
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 4, "error %s encoding session data: %s",
+      sess_cache_opts & SESS_CACHE_OPT_USE_JSON ? "JSON" : "TPL",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = sess_cache_get_key(p, sess_id, sess_id_len, &key, &keysz);
+  xerrno = errno;
   if (res < 0) {
     pr_trace_msg(trace_channel, 1,
       "unable to set cache entry: error getting cache key: %s",
-      strerror(errno));
+      strerror(xerrno));
 
-    free(value);
+    errno = xerrno;
     return -1;
   }
 
-  res = pr_memcache_kset(mcache, &tls_memcache_module, (const char *) key,
-    keysz, value, valuesz, me->expires, flags);
-  free(value);
+  res = pr_memcache_kset(sess_mcache, &tls_memcache_module, (const char *) key,
+    keysz, value, valuesz, se->expires, flags);
+  xerrno = errno;
 
   if (res < 0) {
-    int xerrno = errno;
-
     pr_trace_msg(trace_channel, 2,
-      "unable to add memcache entry for session ID '%s': %s", (char *) key,
-      strerror(xerrno));
+      "unable to add memcache entry for session ID (%lu bytes): %s",
+      (unsigned long) keysz, strerror(xerrno));
 
     errno = xerrno;
     return -1;
   }
 
+  pr_trace_msg(trace_channel, 9, "stored session data in cache using %s",
+    sess_cache_opts & SESS_CACHE_OPT_USE_JSON ? "JSON" : "TPL");
   return 0;
 }
 
-/* Cache implementation callbacks.
- */
-
-static int tls_mcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
+static int sess_cache_open(tls_sess_cache_t *cache, char *info, long timeout) {
   config_rec *c;
 
-  pr_trace_msg(trace_channel, 9, "opening memcache cache %p", cache);
+  cache->cache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(cache->cache_pool, MOD_TLS_MEMCACHE_VERSION);
+
+  pr_trace_msg(trace_channel, 9, "opening memcache cache %p (info '%s')",
+    cache, info ? info : "(none)");
 
   /* This is a little messy, but necessary. The mod_memcache module does
    * not set the configured list of memcached servers until a connection
@@ -361,8 +725,9 @@ static int tls_mcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
     }
   }
 
-  mcache = pr_memcache_conn_new(cache->cache_pool, &tls_memcache_module, 0, 0);
-  if (mcache == NULL) {
+  sess_mcache = pr_memcache_conn_new(cache->cache_pool,
+    &tls_memcache_module, 0, 0);
+  if (sess_mcache == NULL) {
     pr_trace_msg(trace_channel, 2,
       "error connecting to memcached: %s", strerror(errno));
     errno = EPERM;
@@ -370,21 +735,24 @@ static int tls_mcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
   }
 
   /* Configure a namespace prefix for our memcached keys. */
-  if (pr_memcache_conn_set_namespace(mcache, &tls_memcache_module,
-      "mod_tls_memcache") < 0) {
+  if (pr_memcache_conn_set_namespace(sess_mcache, &tls_memcache_module,
+      "mod_tls_memcache.sessions.") < 0) {
     pr_trace_msg(trace_channel, 2, 
       "error setting memcache namespace prefix: %s", strerror(errno));
   }
 
-  cache->cache_pool = make_sub_pool(session.pool);
-  pr_pool_tag(cache->cache_pool, MOD_TLS_MEMCACHE_VERSION);
-
   cache->cache_timeout = timeout;
+
+  if (info != NULL &&
+      strcasecmp(info, "/json") == 0) {
+    sess_cache_opts |= SESS_CACHE_OPT_USE_JSON;
+  }
+
   return 0;
 }
 
-static int tls_mcache_close(tls_sess_cache_t *cache) {
-  pr_trace_msg(trace_channel, 9, "closing memcache cache %p", cache);
+static int sess_cache_close(tls_sess_cache_t *cache) {
+  pr_trace_msg(trace_channel, 9, "closing memcache session cache %p", cache);
 
   if (cache != NULL &&
       cache->cache_pool != NULL) {
@@ -394,39 +762,39 @@ static int tls_mcache_close(tls_sess_cache_t *cache) {
      * the daemon lives.
      */
 
-    if (tls_mcache_sess_list != NULL) {
+    if (sesscache_sess_list != NULL) {
       register unsigned int i;
-      struct mcache_large_entry *entries;
+      struct sesscache_large_entry *entries;
 
-      entries = tls_mcache_sess_list->elts;
-      for (i = 0; i < tls_mcache_sess_list->nelts; i++) {
-        struct mcache_large_entry *entry;
+      entries = sesscache_sess_list->elts;
+      for (i = 0; i < sesscache_sess_list->nelts; i++) {
+        struct sesscache_large_entry *entry;
 
         entry = &(entries[i]);
         if (entry->expires > 0) {
-          pr_memscrub(entry->sess_data, entry->sess_datalen);
+          pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
         }
       }
 
-      tls_mcache_sess_list = NULL;
+      clear_array(sesscache_sess_list);
     }
   }
 
   return 0;
 }
 
-static int tls_mcache_add_large_sess(tls_sess_cache_t *cache,
-    unsigned char *sess_id, unsigned int sess_id_len, time_t expires,
+static int sess_cache_add_large_sess(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len, time_t expires,
     SSL_SESSION *sess, int sess_len) {
-  struct mcache_large_entry *entry = NULL;
+  struct sesscache_large_entry *entry = NULL;
 
   if (sess_len > TLS_MAX_SSL_SESSION_SIZE) {
-    const char *exceeds_key = cache_keys[CACHE_KEY_EXCEEDS].key,
-      *max_len_key = cache_keys[CACHE_KEY_MAX_LEN].key;
+    const char *exceeds_key = sesscache_keys[SESSCACHE_KEY_EXCEEDS].key,
+      *max_len_key = sesscache_keys[SESSCACHE_KEY_MAX_LEN].key;
     void *value = NULL;
     size_t valuesz = 0;
 
-    if (pr_memcache_incr(mcache, &tls_memcache_module, exceeds_key,
+    if (pr_memcache_incr(sess_mcache, &tls_memcache_module, exceeds_key,
         1, NULL) < 0) {
       pr_trace_msg(trace_channel, 2,
         "error incrementing '%s' value: %s", exceeds_key, strerror(errno));
@@ -436,15 +804,15 @@ static int tls_mcache_add_large_sess(tls_sess_cache_t *cache,
      * might also be modifying this value in memcached.  Oh well.
      */
 
-    value = pr_memcache_get(mcache, &tls_memcache_module, max_len_key,
+    value = pr_memcache_get(sess_mcache, &tls_memcache_module, max_len_key,
       &valuesz, NULL);
     if (value != NULL) {
       uint64_t max_len;
 
       memcpy(&max_len, value, valuesz);
-      if (sess_len > max_len) {
-        if (pr_memcache_set(mcache, &tls_memcache_module, max_len_key, &max_len,
-            sizeof(max_len), 0, 0) < 0) {
+      if ((uint64_t) sess_len > max_len) {
+        if (pr_memcache_set(sess_mcache, &tls_memcache_module, max_len_key,
+            &max_len, sizeof(max_len), 0, 0) < 0) {
           pr_trace_msg(trace_channel, 2,
             "error setting '%s' value: %s", max_len_key, strerror(errno));
         }
@@ -456,22 +824,22 @@ static int tls_mcache_add_large_sess(tls_sess_cache_t *cache,
     }
   }
 
-  if (tls_mcache_sess_list != NULL) {
+  if (sesscache_sess_list != NULL) {
     register unsigned int i;
-    struct mcache_large_entry *entries;
+    struct sesscache_large_entry *entries;
     time_t now;
     int ok = FALSE;
 
     /* Look for any expired sessions in the list to overwrite/reuse. */
-    entries = tls_mcache_sess_list->elts;
-    now = time(NULL);
-    for (i = 0; i < tls_mcache_sess_list->nelts; i++) {
+    entries = sesscache_sess_list->elts;
+    time(&now);
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
       entry = &(entries[i]);
 
-      if (entry->expires > now) {
+      if (entry->expires <= now) {
         /* This entry has expired; clear and reuse its slot. */
         entry->expires = 0;
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
 
         ok = TRUE;
         break;
@@ -480,33 +848,37 @@ static int tls_mcache_add_large_sess(tls_sess_cache_t *cache,
 
     if (!ok) {
       /* We didn't find an open slot in the list.  Need to add one. */
-      entry = push_array(tls_mcache_sess_list);
+      entry = push_array(sesscache_sess_list);
     }
 
   } else {
-    tls_mcache_sess_list = make_array(cache->cache_pool, 1,
-      sizeof(struct mcache_large_entry));
-    entry = push_array(tls_mcache_sess_list);
+    sesscache_sess_list = make_array(cache->cache_pool, 1,
+      sizeof(struct sesscache_large_entry));
+    entry = push_array(sesscache_sess_list);
   }
 
   entry->expires = expires;
   entry->sess_id_len = sess_id_len;
   entry->sess_id = palloc(cache->cache_pool, sess_id_len);
-  memcpy(entry->sess_id, sess_id, sess_id_len);
+  memcpy((unsigned char *) entry->sess_id, sess_id, sess_id_len);
   entry->sess_datalen = sess_len;
   entry->sess_data = palloc(cache->cache_pool, sess_len);
-  i2d_SSL_SESSION(sess, &(entry->sess_data));
+  i2d_SSL_SESSION(sess, (unsigned char **) &(entry->sess_data));
 
   return 0;
 }
 
-static int tls_mcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
+static int sess_cache_add(tls_sess_cache_t *cache, const unsigned char *sess_id,
     unsigned int sess_id_len, time_t expires, SSL_SESSION *sess) {
-  struct mcache_entry entry;
+  struct sesscache_entry entry;
   int sess_len;
   unsigned char *ptr;
+  time_t now;
 
-  pr_trace_msg(trace_channel, 9, "adding session to memcache cache %p", cache);
+  time(&now);
+  pr_trace_msg(trace_channel, 9,
+    "adding session to memcache cache %p (expires = %lu, now = %lu)", cache,
+    (unsigned long) expires, (unsigned long) now);
 
   /* First we need to find out how much space is needed for the serialized
    * session data.  There is no known maximum size for SSL session data;
@@ -524,7 +896,7 @@ static int tls_mcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
      * and will not be lost.
      */
 
-    return tls_mcache_add_large_sess(cache, sess_id, sess_id_len, expires,
+    return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
       sess, sess_len);
   }
 
@@ -533,19 +905,19 @@ static int tls_mcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
   ptr = entry.sess_data;
   i2d_SSL_SESSION(sess, &ptr);
 
-  if (tls_mcache_entry_set(cache->cache_pool, sess_id, sess_id_len,
+  if (sess_cache_mcache_entry_set(cache->cache_pool, sess_id, sess_id_len,
       &entry) < 0) {
     pr_trace_msg(trace_channel, 2,
       "error adding session to memcache: %s", strerror(errno));
 
     /* Add this session to the "large session" list instead as a fallback. */
-    return tls_mcache_add_large_sess(cache, sess_id, sess_id_len, expires,
+    return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
         sess, sess_len);
 
   } else {
-    const char *key = cache_keys[CACHE_KEY_STORES].key;
+    const char *key = sesscache_keys[SESSCACHE_KEY_STORES].key;
 
-    if (pr_memcache_incr(mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+    if (pr_memcache_incr(sess_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
       pr_trace_msg(trace_channel, 2,
         "error incrementing '%s' value: %s", key, strerror(errno));
     }
@@ -554,9 +926,9 @@ static int tls_mcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
   return 0;
 }
 
-static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
-    unsigned char *sess_id, unsigned int sess_id_len) {
-  struct mcache_entry entry;
+static SSL_SESSION *sess_cache_get(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len) {
+  struct sesscache_entry entry;
   time_t now;
   SSL_SESSION *sess = NULL;
 
@@ -564,13 +936,13 @@ static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
     cache); 
 
   /* Look for the requested session in the "large session" list first. */
-  if (tls_mcache_sess_list != NULL) {
+  if (sesscache_sess_list != NULL) {
     register unsigned int i;
-    struct mcache_large_entry *entries;
+    struct sesscache_large_entry *entries;
 
-    entries = tls_mcache_sess_list->elts;
-    for (i = 0; i < tls_mcache_sess_list->nelts; i++) {
-      struct mcache_large_entry *large_entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *large_entry;
 
       large_entry = &(entries[i]);
       if (large_entry->expires > 0 &&
@@ -579,15 +951,14 @@ static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
             large_entry->sess_id_len) == 0) {
 
         now = time(NULL);
-        if (large_entry->expires <= now) {
+        if (large_entry->expires > now) {
           TLS_D2I_SSL_SESSION_CONST unsigned char *ptr;
 
           ptr = large_entry->sess_data;
           sess = d2i_SSL_SESSION(NULL, &ptr, large_entry->sess_datalen);
           if (sess == NULL) {
             pr_trace_msg(trace_channel, 2,
-              "error retrieving session from cache: %s",
-              tls_mcache_get_crypto_errors());
+              "error retrieving session from cache: %s", mcache_get_errors());
 
           } else {
             break;
@@ -601,7 +972,7 @@ static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
     return sess;
   }
 
-  if (tls_mcache_entry_get(cache->cache_pool, sess_id, sess_id_len,
+  if (sess_cache_mcache_entry_get(cache->cache_pool, sess_id, sess_id_len,
       &entry) < 0) {
     return NULL;
   }
@@ -613,21 +984,22 @@ static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
     ptr = entry.sess_data;
     sess = d2i_SSL_SESSION(NULL, &ptr, entry.sess_datalen);
     if (sess != NULL) {
-      const char *key = cache_keys[CACHE_KEY_HITS].key;
+      const char *key = sesscache_keys[SESSCACHE_KEY_HITS].key;
 
-      if (pr_memcache_incr(mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+      if (pr_memcache_incr(sess_mcache, &tls_memcache_module, key, 1,
+          NULL) < 0) {
         pr_trace_msg(trace_channel, 2,
           "error incrementing '%s' value: %s", key, strerror(errno));
       }
 
     } else {
-      const char *key = cache_keys[CACHE_KEY_ERRORS].key;
+      const char *key = sesscache_keys[SESSCACHE_KEY_ERRORS].key;
 
       pr_trace_msg(trace_channel, 2,
-        "error retrieving session from cache: %s",
-        tls_mcache_get_crypto_errors());
+        "error retrieving session from cache: %s", mcache_get_errors());
 
-      if (pr_memcache_incr(mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+      if (pr_memcache_incr(sess_mcache, &tls_memcache_module, key, 1,
+          NULL) < 0) {
         pr_trace_msg(trace_channel, 2,
           "error incrementing '%s' value: %s", key, strerror(errno));
       }
@@ -635,9 +1007,9 @@ static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
   }
 
   if (sess == NULL) {
-    const char *key = cache_keys[CACHE_KEY_MISSES].key;
+    const char *key = sesscache_keys[SESSCACHE_KEY_MISSES].key;
 
-    if (pr_memcache_incr(mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+    if (pr_memcache_incr(sess_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
       pr_trace_msg(trace_channel, 2,
         "error incrementing '%s' value: %s", key, strerror(errno));
     }
@@ -648,42 +1020,42 @@ static SSL_SESSION *tls_mcache_get(tls_sess_cache_t *cache,
   return sess;
 }
 
-static int tls_mcache_delete(tls_sess_cache_t *cache,
-    unsigned char *sess_id, unsigned int sess_id_len) {
-  const char *key = cache_keys[CACHE_KEY_DELETES].key;
+static int sess_cache_delete(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len) {
+  const char *key = sesscache_keys[SESSCACHE_KEY_DELETES].key;
   int res;
 
   pr_trace_msg(trace_channel, 9, "removing session from memcache cache %p",
     cache);
 
   /* Look for the requested session in the "large session" list first. */
-  if (tls_mcache_sess_list != NULL) {
+  if (sesscache_sess_list != NULL) {
     register unsigned int i;
-    struct mcache_large_entry *entries;
+    struct sesscache_large_entry *entries;
 
-    entries = tls_mcache_sess_list->elts;
-    for (i = 0; i < tls_mcache_sess_list->nelts; i++) {
-      struct mcache_large_entry *entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
 
       entry = &(entries[i]);
       if (entry->sess_id_len == sess_id_len &&
           memcmp(entry->sess_id, sess_id, entry->sess_id_len) == 0) {
 
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
         entry->expires = 0;
         return 0;
       }
     }
   }
 
-  res = tls_mcache_entry_remove(cache->cache_pool, sess_id, sess_id_len);
+  res = sess_cache_mcache_entry_delete(cache->cache_pool, sess_id, sess_id_len);
   if (res < 0) {
     return -1;
   }
 
   /* Don't forget to update the stats. */
 
-  if (pr_memcache_incr(mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+  if (pr_memcache_incr(sess_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
     pr_trace_msg(trace_channel, 2,
       "error incrementing '%s' value: %s", key, strerror(errno));
   }
@@ -691,24 +1063,28 @@ static int tls_mcache_delete(tls_sess_cache_t *cache,
   return res;
 }
 
-static int tls_mcache_clear(tls_sess_cache_t *cache) {
+static int sess_cache_clear(tls_sess_cache_t *cache) {
   register unsigned int i;
   int res = 0;
 
-  pr_trace_msg(trace_channel, 9, "clearing memcache cache %p", cache); 
+  if (sess_mcache == NULL) {
+    pr_trace_msg(trace_channel, 9, "missing required memcached connection");
+    errno = EINVAL;
+    return -1;
+  }
 
-  /* XXX if mcache == NULL, return EINVAL */
+  pr_trace_msg(trace_channel, 9, "clearing memcache session cache %p", cache);
 
-  if (tls_mcache_sess_list != NULL) {
-    struct mcache_large_entry *entries;
+  if (sesscache_sess_list != NULL) {
+    struct sesscache_large_entry *entries;
     
-    entries = tls_mcache_sess_list->elts;
-    for (i = 0; i < tls_mcache_sess_list->nelts; i++) {
-      struct mcache_large_entry *entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
 
       entry = &(entries[i]);
       entry->expires = 0;
-      pr_memscrub(entry->sess_data, entry->sess_datalen);
+      pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
     }
   }
 
@@ -717,21 +1093,23 @@ static int tls_mcache_clear(tls_sess_cache_t *cache) {
   return res;
 }
 
-static int tls_mcache_remove(tls_sess_cache_t *cache) {
+static int sess_cache_remove(tls_sess_cache_t *cache) {
   int res;
 
-  res = tls_mcache_clear(cache);
+  pr_trace_msg(trace_channel, 9, "removing memcache session cache %p", cache);
+
+  res = sess_cache_clear(cache);
   /* XXX close memcache conn */
 
   return res;
 }
 
-static int tls_mcache_status(tls_sess_cache_t *cache,
+static int sess_cache_status(tls_sess_cache_t *cache,
     void (*statusf)(void *, const char *, ...), void *arg, int flags) {
   register unsigned int i;
   pool *tmp_pool;
 
-  pr_trace_msg(trace_channel, 9, "checking memcache cache %p", cache); 
+  pr_trace_msg(trace_channel, 9, "checking memcache session cache %p", cache);
 
   tmp_pool = make_sub_pool(permanent_pool);
 
@@ -740,16 +1118,16 @@ static int tls_mcache_status(tls_sess_cache_t *cache,
   statusf(arg, "%s", "");
   statusf(arg, "Memcache servers: ");
 
-  for (i = 0; cache_keys[i].key != NULL; i++) {
+  for (i = 0; sesscache_keys[i].key != NULL; i++) {
     const char *key, *desc;
     void *value = NULL;
     size_t valuesz = 0;
     uint32_t stat_flags = 0;
 
-    key = cache_keys[i].key;
-    desc = cache_keys[i].desc;
+    key = sesscache_keys[i].key;
+    desc = sesscache_keys[i].desc;
 
-    value = pr_memcache_get(mcache, &tls_memcache_module, key, &valuesz,
+    value = pr_memcache_get(sess_mcache, &tls_memcache_module, key, &valuesz,
       &stat_flags);
     if (value != NULL) {
       uint64_t num = 0;
@@ -778,7 +1156,7 @@ static int tls_mcache_status(tls_sess_cache_t *cache,
      */
 
     for (i = 0; i < 0; i++) {
-      struct mcache_entry *entry;
+      struct sesscache_entry *entry;
 
       pr_signals_handle();
 
@@ -792,8 +1170,7 @@ static int tls_mcache_status(tls_sess_cache_t *cache,
         sess = d2i_SSL_SESSION(NULL, &ptr, entry->sess_datalen); 
         if (sess == NULL) {
           pr_log_pri(PR_LOG_NOTICE, MOD_TLS_MEMCACHE_VERSION
-            ": error retrieving session from cache: %s",
-            tls_mcache_get_crypto_errors());
+            ": error retrieving session from cache: %s", mcache_get_errors());
           continue;
         }
 
@@ -801,27 +1178,19 @@ static int tls_mcache_status(tls_sess_cache_t *cache,
 
         /* XXX Directly accessing these fields cannot be a Good Thing. */
         if (sess->session_id_length > 0) {
-          register unsigned int j;
           char *sess_id_str;
 
-          sess_id_str = pcalloc(tmp_pool, (sess->session_id_length * 2) + 1);
-
-          for (j = 0; j < sess->session_id_length; j++) {
-            sprintf((char *) &(sess_id_str[j*2]), "%02X", sess->session_id[j]);
-          }
+          sess_id_str = pr_str2hex(tmp_pool, sess->session_id,
+            sess->session_id_length, PR_STR_FL_HEX_USE_UC);
 
           statusf(arg, "    Session ID: %s", sess_id_str);
         }
 
         if (sess->sid_ctx_length > 0) {
-          register unsigned int j;
           char *sid_ctx_str;
 
-          sid_ctx_str = pcalloc(tmp_pool, (sess->sid_ctx_length * 2) + 1);
-
-          for (j = 0; j < sess->sid_ctx_length; j++) {
-            sprintf((char *) &(sid_ctx_str[j*2]), "%02X", sess->sid_ctx[j]);
-          }
+          sid_ctx_str = pr_str2hex(tmp_pool, sess->sid_ctx,
+            sess->sid_ctx_length, PR_STR_FL_HEX_USE_UC);
 
           statusf(arg, "    Session ID Context: %s", sid_ctx_str);
         }
@@ -835,6 +1204,16 @@ static int tls_mcache_status(tls_sess_cache_t *cache,
             statusf(arg, "    Protocol: %s", "TLSv1");
             break;
 
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+          case TLS1_1_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1.1");
+            break;
+
+          case TLS1_2_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1.2");
+            break;
+#endif
+
           default:
             statusf(arg, "    Protocol: %s", "unknown");
         }
@@ -857,67 +1236,839 @@ static int tls_mcache_status(tls_sess_cache_t *cache,
   return 0;
 }
 
-/* Event Handlers
+#if defined(PR_USE_OPENSSL_OCSP)
+/* OCSP response cache implementation callbacks.
  */
 
-#if defined(PR_SHARED_MODULE)
-static void tls_mcache_mod_unload_ev(const void *event_data, void *user_data) {
-  if (strcmp("mod_tls_memcache.c", (const char *) event_data) == 0) {
-    pr_event_unregister(&tls_memcache_module, NULL, NULL);
-    tls_sess_cache_unregister("memcache");
-  }
-}
-#endif /* !PR_SHARED_MODULE */
+/* Functions for marshalling key/value data to/from memcached. */
 
-/* Initialization functions
- */
+static int ocsp_cache_get_json_key(pool *p, const char *fingerprint,
+    void **key, size_t *keysz) {
+  pr_json_object_t *json;
+  char *json_text;
 
-static int tls_mcache_init(void) {
-#if defined(PR_SHARED_MODULE)
-  pr_event_register(&tls_memcache_module, "core.module-unload",
-    tls_mcache_mod_unload_ev, NULL);
-#endif /* !PR_SHARED_MODULE */
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_string(p, json, "fingerprint", fingerprint);
 
-  /* Prepare our cache handler. */
-  memset(&tls_mcache, 0, sizeof(tls_mcache));
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
 
-  tls_mcache.cache_name = "memcache";
-  tls_mcache.cache_pool = pr_pool_create_sz(permanent_pool, 256);
-  pr_pool_tag(tls_mcache.cache_pool, MOD_TLS_MEMCACHE_VERSION);
+  /* Include the terminating NUL in the key. */
+  *keysz = strlen(json_text) + 1;
+  *key = pstrndup(p, json_text, *keysz - 1);
 
-  tls_mcache.open = tls_mcache_open;
-  tls_mcache.close = tls_mcache_close;
-  tls_mcache.add = tls_mcache_add;
-  tls_mcache.get = tls_mcache_get;
-  tls_mcache.delete = tls_mcache_delete;
-  tls_mcache.clear = tls_mcache_clear;
-  tls_mcache.remove = tls_mcache_remove;
-  tls_mcache.status = tls_mcache_status;
+  return 0;
+}
 
-#ifdef SSL_SESS_CACHE_NO_INTERNAL_LOOKUP
-  /* Take a chance, and inform OpenSSL that it does not need to use its own
-   * internal session cache lookups; using the external session cache (i.e. us)
-   * will be enough.
-   */
-  tls_mcache.cache_mode = SSL_SESS_CACHE_NO_INTERNAL_LOOKUP;
-#endif
+static int ocsp_cache_get_key(pool *p, const char *fingerprint, void **key,
+    size_t *keysz) {
+  int res;
 
-#ifdef PR_USE_MEMCACHE
-  /* Register ourselves with mod_tls. */
-  if (tls_sess_cache_register("memcache", &tls_mcache) < 0) {
-    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_MEMCACHE_VERSION
-      ": notice: error registering 'memcache' SSL session cache: %s",
+  res = ocsp_cache_get_json_key(p, fingerprint, key, keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error constructing ocsp cache JSON lookup key for fingerprint '%s'",
+      fingerprint);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_entry_decode_json(pool *p, void *value, size_t valuesz,
+    struct ocspcache_entry *oe) {
+  int res;
+  pr_json_object_t *json;
+  const char *key;
+  char *entry, *text;
+  double number;
+
+  entry = value;
+  if (pr_json_text_validate(p, entry) == FALSE) {
+    tls_log(MOD_TLS_MEMCACHE_VERSION
+      ": unable to decode invalid JSON ocsp cache entry: '%s'", entry);
+    errno = EINVAL;
+    return -1;
+  }
+
+  json = pr_json_object_from_text(p, entry);
+
+  key = OCSP_CACHE_JSON_KEY_AGE;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  oe->age = (uint32_t) number;
+
+  key = OCSP_CACHE_JSON_KEY_RESPONSE;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res == 0) {
+    int have_padding = FALSE;
+    char *base64_data;
+    size_t base64_datalen;
+    unsigned char *data;
+
+    base64_data = text;
+    base64_datalen = strlen(base64_data);
+
+    /* Due to Base64's padding, we need to detect if the last block was
+     * padded with zeros; we do this by looking for '=' characters at the
+     * end of the text being decoded.  If we see these characters, then we
+     * will "trim" off any trailing zero values in the decoded data, on the
+     * ASSUMPTION that they are the auto-added padding bytes.
+     */
+    if (base64_data[base64_datalen-1] == '=') {
+      have_padding = TRUE;
+    }
+
+    data = oe->resp_der;
+    res = EVP_DecodeBlock(data, (unsigned char *) base64_data,
+      (int) base64_datalen);
+    if (res <= 0) {
+      /* Base64-decoding error. */
+      pr_trace_msg(trace_channel, 5,
+        "error base64-decoding OCSP data in '%s', rejecting", entry);
+      pr_json_object_free(json);
+      errno = EINVAL;
+      return -1;
+    }
+
+    if (have_padding) {
+      /* Assume that only one or two zero bytes of padding were added. */
+      if (data[res-1] == '\0') {
+        res -= 1;
+
+        if (data[res-1] == '\0') {
+          res -= 1;
+        }
+      }
+    }
+
+  } else {
+    return -1;
+  }
+
+  key = OCSP_CACHE_JSON_KEY_RESPONSE_LENGTH;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  oe->resp_derlen = (unsigned int) number;
+
+  (void) pr_json_object_free(json);
+  return 0;
+}
+
+static int ocsp_cache_mcache_entry_get(pool *p, const char *fingerprint,
+    struct ocspcache_entry *oe) {
+  int res;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+  uint32_t flags = 0;
+
+  res = ocsp_cache_get_key(p, fingerprint, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to get ocsp cache entry: error getting cache key: %s",
       strerror(errno));
+
+    return -1;
+  }
+
+  value = pr_memcache_kget(ocsp_mcache, &tls_memcache_module,
+    (const char *) key, keysz, &valuesz, &flags);
+  if (value == NULL) {
+    pr_trace_msg(trace_channel, 3,
+      "no matching memcache entry found for fingerprint '%s'", fingerprint);
+    errno = ENOENT;
+    return -1;
+  }
+
+  /* Decode the cached response data. */
+  res = ocsp_cache_entry_decode_json(p, value, valuesz, oe);
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 9,
+     "retrieved response data from cache using JSON");
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_mcache_entry_delete(pool *p, const char *fingerprint) {
+  int res;
+  void *key = NULL;
+  size_t keysz = 0;
+
+  res = ocsp_cache_get_key(p, fingerprint, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to remove ocsp cache entry: error getting cache key: %s",
+      strerror(errno));
+
+    return -1;
+  }
+
+  res = pr_memcache_kremove(ocsp_mcache, &tls_memcache_module,
+    (const char *) key, keysz, 0);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "unable to remove memcache entry for fingerpring '%s': %s", fingerprint,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_entry_encode_json(pool *p, void **value, size_t *valuesz,
+    struct ocspcache_entry *oe) {
+  pr_json_object_t *json;
+  pool *tmp_pool;
+  char *base64_data = NULL, *json_text;
+
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_number(p, json, OCSP_CACHE_JSON_KEY_AGE,
+    (double) oe->age);
+
+  /* Base64-encode the response data.  Note that EVP_EncodeBlock does
+   * NUL-terminate the encoded data.
+   */
+  tmp_pool = make_sub_pool(p);
+  base64_data = pcalloc(tmp_pool, (oe->resp_derlen * 2) + 1);
+
+  EVP_EncodeBlock((unsigned char *) base64_data, oe->resp_der,
+    (int) oe->resp_derlen);
+  (void) pr_json_object_set_string(p, json, OCSP_CACHE_JSON_KEY_RESPONSE,
+    base64_data);
+  (void) pr_json_object_set_number(p, json, OCSP_CACHE_JSON_KEY_RESPONSE_LENGTH,
+    (double) oe->resp_derlen);
+  destroy_pool(tmp_pool);
+
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
+
+  /* Safety check */
+  if (pr_json_text_validate(p, json_text) == FALSE) {
+    pr_trace_msg(trace_channel, 1, "invalid JSON emitted: '%s'", json_text);
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Include the terminating NUL in the value. */
+  *valuesz = strlen(json_text) + 1;
+  *value = pstrndup(p, json_text, *valuesz - 1);
+
+  return 0;
+}
+
+static int ocsp_cache_mcache_entry_set(pool *p, const char *fingerprint,
+    struct ocspcache_entry *oe) {
+  int res, xerrno = 0;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+  uint32_t flags = 0;
+
+  /* Encode the OCSP response data. */
+  res = ocsp_cache_entry_encode_json(p, &value, &valuesz, oe);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 4, "error JSON encoding OCSP response data: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = ocsp_cache_get_key(p, fingerprint, &key, &keysz);
+  xerrno = errno;
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to set ocsp cache entry: error getting cache key: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
+
+  res = pr_memcache_kset(ocsp_mcache, &tls_memcache_module, (const char *) key,
+    keysz, value, valuesz, 0, flags);
+  xerrno = errno;
+
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "unable to add memcache entry for fingerprint '%s': %s", fingerprint,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9,
+    "stored OCSP response data in cache using JSON");
+  return 0;
+}
+
+static int ocsp_cache_open(tls_ocsp_cache_t *cache, char *info) {
+  config_rec *c;
+
+  pr_trace_msg(trace_channel, 9, "opening memcache cache %p (info '%s')",
+    cache, info ? info : "(none)");
+
+  cache->cache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(cache->cache_pool, MOD_TLS_MEMCACHE_VERSION);
+
+  /* This is a little messy, but necessary. The mod_memcache module does
+   * not set the configured list of memcached servers until a connection
+   * arrives.  But mod_tls opens its session cache prior to that, when the
+   * server is starting up.  Thus we need to set the configured list of
+   * memcached servers ourselves.
+   */
+  c = find_config(main_server->conf, CONF_PARAM, "MemcacheEngine", FALSE);
+  if (c != NULL) {
+    int engine;
+
+    engine = *((int *) c->argv[0]);
+    if (engine == FALSE) {
+      pr_trace_msg(trace_channel, 2, "%s",
+        "memcache support disabled (see MemcacheEngine directive)");
+      errno = EPERM;
+      return -1;
+    }
+  }
+
+  ocsp_mcache = pr_memcache_conn_new(cache->cache_pool,
+    &tls_memcache_module, 0, 0);
+  if (ocsp_mcache == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error connecting to memcached: %s", strerror(errno));
+    errno = EPERM;
+    return -1;
+  }
+
+  /* Configure a namespace prefix for our memcached keys. */
+  if (pr_memcache_conn_set_namespace(ocsp_mcache, &tls_memcache_module,
+      "mod_tls_memcache.ocsp.") < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting memcache namespace prefix: %s", strerror(errno));
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_close(tls_ocsp_cache_t *cache) {
+  pr_trace_msg(trace_channel, 9, "closing memcache ocsp cache %p", cache);
+
+  if (cache != NULL &&
+      cache->cache_pool != NULL) {
+
+    /* We do NOT destroy the cache_pool here or close the mcache connection;
+     * both were created at daemon startup, and should live as long as
+     * the daemon lives.
+     */
+
+    if (ocspcache_resp_list != NULL) {
+      register unsigned int i;
+      struct ocspcache_large_entry *entries;
+
+      entries = ocspcache_resp_list->elts;
+      for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+        struct ocspcache_large_entry *entry;
+
+        entry = &(entries[i]);
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+        entry->age = 0;
+      }
+
+      clear_array(ocspcache_resp_list);
+    }
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_add_large_resp(tls_ocsp_cache_t *cache,
+    const char *fingerprint, OCSP_RESPONSE *resp, time_t resp_age) {
+  struct ocspcache_large_entry *entry = NULL;
+  int resp_derlen;
+  unsigned char *ptr;
+
+  resp_derlen = i2d_OCSP_RESPONSE(resp, NULL);
+  if (resp_derlen > TLS_MAX_OCSP_RESPONSE_SIZE) {
+    const char *exceeds_key = ocspcache_keys[OCSPCACHE_KEY_EXCEEDS].key,
+      *max_len_key = ocspcache_keys[OCSPCACHE_KEY_MAX_LEN].key;
+    void *value = NULL;
+    size_t valuesz = 0;
+
+    if (pr_memcache_incr(ocsp_mcache, &tls_memcache_module, exceeds_key,
+        1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", exceeds_key, strerror(errno));
+    }
+
+    /* XXX Yes, this is subject to race conditions; other proftpd servers
+     * might also be modifying this value in memcached.  Oh well.
+     */
+
+    value = pr_memcache_get(ocsp_mcache, &tls_memcache_module, max_len_key,
+      &valuesz, NULL);
+    if (value != NULL) {
+      uint64_t max_len;
+
+      memcpy(&max_len, value, valuesz);
+      if ((uint64_t) resp_derlen > max_len) {
+        if (pr_memcache_set(ocsp_mcache, &tls_memcache_module, max_len_key,
+            &max_len, sizeof(max_len), 0, 0) < 0) {
+          pr_trace_msg(trace_channel, 2,
+            "error setting '%s' value: %s", max_len_key, strerror(errno));
+        }
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 2,
+        "error getting '%s' value: %s", max_len_key, strerror(errno));
+    }
+  }
+
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+    time_t now;
+    int ok = FALSE;
+
+    /* Look for any expired sessions in the list to overwrite/reuse. */
+    entries = ocspcache_resp_list->elts;
+    time(&now);
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      entry = &(entries[i]);
+
+      if (entry->age > (now - 3600)) {
+        /* This entry has expired; clear and reuse its slot. */
+        entry->age = 0;
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+
+        ok = TRUE;
+        break;
+      }
+    }
+
+    if (!ok) {
+      /* We didn't find an open slot in the list.  Need to add one. */
+      entry = push_array(ocspcache_resp_list);
+    }
+
+  } else {
+    ocspcache_resp_list = make_array(cache->cache_pool, 1,
+      sizeof(struct ocspcache_large_entry));
+    entry = push_array(ocspcache_resp_list);
+  }
+
+  entry->age = resp_age;
+  entry->fingerprint_len = strlen(fingerprint);
+  entry->fingerprint = pstrdup(cache->cache_pool, fingerprint);
+  entry->resp_derlen = resp_derlen;
+  entry->resp_der = ptr = palloc(cache->cache_pool, resp_derlen);
+  i2d_OCSP_RESPONSE(resp, &ptr);
+
+  return 0;
+}
+
+static int ocsp_cache_add(tls_ocsp_cache_t *cache, const char *fingerprint,
+    OCSP_RESPONSE *resp, time_t resp_age) {
+  struct ocspcache_entry entry;
+  int resp_derlen;
+  unsigned char *ptr;
+
+  pr_trace_msg(trace_channel, 9,
+    "adding response to memcache ocsp cache %p", cache);
+
+  /* First we need to find out how much space is needed for the serialized
+   * response data.  There is no known maximum size for OCSP response data;
+   * this module is currently designed to allow only up to a certain size.
+   */
+  resp_derlen = i2d_OCSP_RESPONSE(resp, NULL);
+  if (resp_derlen > TLS_MAX_OCSP_RESPONSE_SIZE) {
+    pr_trace_msg(trace_channel, 2,
+      "length of serialized OCSP response data (%d) exceeds maximum size (%u), "
+      "unable to add to shared memcache, adding to list", resp_derlen,
+      TLS_MAX_OCSP_RESPONSE_SIZE);
+
+    /* Instead of rejecting the add here, we add the response to a "large
+     * response" list.  Thus the large response would still be cached per
+     * process and will not be lost.
+     */
+
+    return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+  }
+
+  entry.age = resp_age;
+  entry.resp_derlen = resp_derlen;
+  ptr = entry.resp_der;
+  i2d_OCSP_RESPONSE(resp, &ptr);
+
+  if (ocsp_cache_mcache_entry_set(cache->cache_pool, fingerprint, &entry) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error adding response to memcache: %s", strerror(errno));
+
+    /* Add this response to the "large response" list instead as a fallback. */
+    return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+
+  } else {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_STORES].key;
+
+    if (pr_memcache_incr(ocsp_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+  }
+
+  return 0;
+}
+
+static OCSP_RESPONSE *ocsp_cache_get(tls_ocsp_cache_t *cache,
+    const char *fingerprint, time_t *resp_age) {
+  struct ocspcache_entry entry;
+  OCSP_RESPONSE *resp = NULL;
+  size_t fingerprint_len;
+  const unsigned char *ptr;
+
+  pr_trace_msg(trace_channel, 9, "getting response from memcache ocsp cache %p",
+    cache);
+
+  fingerprint_len = strlen(fingerprint);
+
+  /* Look for the requested response in the "large response" list first. */
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *large_entry;
+
+      large_entry = &(entries[i]);
+      if (large_entry->fingerprint_len > 0 &&
+          large_entry->fingerprint_len == fingerprint_len &&
+          memcmp(large_entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+        ptr = large_entry->resp_der;
+        resp = d2i_OCSP_RESPONSE(NULL, &ptr, large_entry->resp_derlen);
+        if (resp == NULL) {
+          pr_trace_msg(trace_channel, 2,
+            "error retrieving response from ocsp cache: %s",
+            mcache_get_errors());
+
+        } else {
+          *resp_age = large_entry->age;
+          break;
+        }
+      }
+    }
+  }
+
+  if (resp) {
+    return resp;
+  }
+
+  if (ocsp_cache_mcache_entry_get(cache->cache_pool, fingerprint, &entry) < 0) {
+    return NULL;
+  }
+
+  ptr = entry.resp_der;
+  resp = d2i_OCSP_RESPONSE(NULL, &ptr, entry.resp_derlen);
+  if (resp != NULL) {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_HITS].key;
+
+    *resp_age = entry.age;
+
+    if (pr_memcache_incr(ocsp_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+
+  } else {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_ERRORS].key;
+
+    pr_trace_msg(trace_channel, 2,
+      "error retrieving response from ocsp cache: %s", mcache_get_errors());
+
+    if (pr_memcache_incr(ocsp_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+  }
+
+  if (resp == NULL) {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_MISSES].key;
+
+    if (pr_memcache_incr(ocsp_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+
+    errno = ENOENT;
+  }
+
+  return resp;
+}
+
+static int ocsp_cache_delete(tls_ocsp_cache_t *cache,
+    const char *fingerprint) {
+  const char *key = ocspcache_keys[OCSPCACHE_KEY_DELETES].key;
+  int res;
+  size_t fingerprint_len;
+
+  pr_trace_msg(trace_channel, 9,
+    "deleting response from memcache ocsp cache %p", cache);
+
+  fingerprint_len = strlen(fingerprint);
+
+  /* Look for the requested response in the "large response" list first. */
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      if (entry->fingerprint_len == fingerprint_len &&
+          memcmp(entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+        entry->age = 0;
+
+        return 0;
+      }
+    }
+  }
+
+  res = ocsp_cache_mcache_entry_delete(cache->cache_pool, fingerprint);
+  if (res < 0) {
+    return -1;
+  }
+
+  /* Don't forget to update the stats. */
+
+  if (pr_memcache_incr(ocsp_mcache, &tls_memcache_module, key, 1, NULL) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing '%s' value: %s", key, strerror(errno));
+  }
+
+  return res;
+}
+
+static int ocsp_cache_clear(tls_ocsp_cache_t *cache) {
+  register unsigned int i;
+  int res = 0;
+
+  if (ocsp_mcache == NULL) {
+    pr_trace_msg(trace_channel, 9, "missing required memcached connection");
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9, "clearing memcache ocsp cache %p", cache);
+
+  if (ocspcache_resp_list != NULL) {
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      entry->age = 0;
+      pr_memscrub(entry->resp_der, entry->resp_derlen);
+      entry->resp_derlen = 0;
+      pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+      entry->fingerprint_len = 0;
+    }
+  }
+
+  /* XXX iterate through keys, kremoving any "mod_tls_memcache" prefixed keys */
+
+  return res;
+}
+
+static int ocsp_cache_remove(tls_ocsp_cache_t *cache) {
+  int res;
+
+  pr_trace_msg(trace_channel, 9, "removing memcache ocsp cache %p", cache);
+
+  res = ocsp_cache_clear(cache);
+  /* XXX close memcache conn */
+
+  return res;
+}
+
+static int ocsp_cache_status(tls_ocsp_cache_t *cache,
+    void (*statusf)(void *, const char *, ...), void *arg, int flags) {
+  register unsigned int i;
+  pool *tmp_pool;
+
+  pr_trace_msg(trace_channel, 9, "checking memcache ocsp cache %p", cache);
+
+  tmp_pool = make_sub_pool(permanent_pool);
+
+  statusf(arg, "%s", "Memcache OCSP response cache provided by "
+    MOD_TLS_MEMCACHE_VERSION);
+  statusf(arg, "%s", "");
+  statusf(arg, "Memcache servers: ");
+
+  for (i = 0; ocspcache_keys[i].key != NULL; i++) {
+    const char *key, *desc;
+    void *value = NULL;
+    size_t valuesz = 0;
+    uint32_t stat_flags = 0;
+
+    key = ocspcache_keys[i].key;
+    desc = ocspcache_keys[i].desc;
+
+    value = pr_memcache_get(ocsp_mcache, &tls_memcache_module, key, &valuesz,
+      &stat_flags);
+    if (value != NULL) {
+      uint64_t num = 0;
+      memcpy(&num, value, valuesz);
+      statusf(arg, "%s: %lu", desc, (unsigned long) num);
+    }
+  }
+
+  /* XXX run stats on memcached servers? */
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+#endif /* PR_USE_OPENSSL_OCSP */
+
+/* Event Handlers
+ */
+
+#if defined(PR_SHARED_MODULE)
+static void tls_mcache_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_tls_memcache.c", (const char *) event_data) == 0) {
+    pr_event_unregister(&tls_memcache_module, NULL, NULL);
+    tls_sess_cache_unregister("memcache");
+# if defined(PR_USE_OPENSSL_OCSP)
+    tls_ocsp_cache_unregister("memcache");
+# endif /* PR_USE_OPENSSL_OCSP */
+  }
+}
+#endif /* !PR_SHARED_MODULE */
+
+/* Initialization functions
+ */
+
+static int tls_mcache_init(void) {
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&tls_memcache_module, "core.module-unload",
+    tls_mcache_mod_unload_ev, NULL);
+#endif /* !PR_SHARED_MODULE */
+
+  /* Prepare our SSL session cache handler. */
+  memset(&sess_cache, 0, sizeof(sess_cache));
+
+  sess_cache.cache_name = "memcache";
+  pr_pool_tag(sess_cache.cache_pool, MOD_TLS_MEMCACHE_VERSION);
+
+  sess_cache.open = sess_cache_open;
+  sess_cache.close = sess_cache_close;
+  sess_cache.add = sess_cache_add;
+  sess_cache.get = sess_cache_get;
+  sess_cache.delete = sess_cache_delete;
+  sess_cache.clear = sess_cache_clear;
+  sess_cache.remove = sess_cache_remove;
+  sess_cache.status = sess_cache_status;
+
+#ifdef SSL_SESS_CACHE_NO_INTERNAL
+  /* Take a chance, and inform OpenSSL that it does not need to use its own
+   * internal session cache lookups/storage; using the external session cache
+   * (i.e. us) will be enough.
+   */
+  sess_cache.cache_mode = SSL_SESS_CACHE_NO_INTERNAL;
+#endif
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  /* Prepare our OCSP response cache handler. */
+  memset(&ocsp_cache, 0, sizeof(ocsp_cache));
+
+  ocsp_cache.cache_name = "memcache";
+  pr_pool_tag(ocsp_cache.cache_pool, MOD_TLS_MEMCACHE_VERSION);
+
+  ocsp_cache.open = ocsp_cache_open;
+  ocsp_cache.close = ocsp_cache_close;
+  ocsp_cache.add = ocsp_cache_add;
+  ocsp_cache.get = ocsp_cache_get;
+  ocsp_cache.delete = ocsp_cache_delete;
+  ocsp_cache.clear = ocsp_cache_clear;
+  ocsp_cache.remove = ocsp_cache_remove;
+  ocsp_cache.status = ocsp_cache_status;
+#endif /* PR_USE_OPENSSL_OCSP */
+
+#ifdef PR_USE_MEMCACHE
+  if (tls_sess_cache_register("memcache", &sess_cache) < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_MEMCACHE_VERSION
+      ": notice: error registering 'memcache' SSL session cache: %s",
+      strerror(errno));
+    return -1;
+  }
+
+# if defined(PR_USE_OPENSSL_OCSP)
+  if (tls_ocsp_cache_register("memcache", &ocsp_cache) < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_MEMCACHE_VERSION
+      ": notice: error registering 'memcache' OCSP response cache: %s",
+      strerror(errno));
+    return -1;
+  }
+# endif /* PR_USE_OPENSSL_OCSP */
+
 #else
-  pr_log_pri(PR_LOG_NOTICE, MOD_TLS_MEMCACHE_VERSION
-    ": notice: unable to register 'memcache' SSL session cache: Memcache support not enabled");
+  pr_log_debug(DEBUG1, MOD_TLS_MEMCACHE_VERSION
+    ": unable to register 'memcache' SSL session cache: Memcache support not enabled");
+# if defined(PR_USE_OPENSSL_OCSP)
+  pr_log_debug(DEBUG1, MOD_TLS_MEMCACHE_VERSION
+    ": unable to register 'memcache' OCSP response cache: Memcache support not enabled");
+# endif /* PR_USE_OPENSSL_OCSP */
 #endif /* PR_USE_MEMCACHE */
 
   return 0;
 }
 
+static int tls_mcache_sess_init(void) {
+  /* Reset our memcache handles. */
+
+  if (sess_mcache != NULL) {
+    if (pr_memcache_conn_clone(session.pool, sess_mcache) < 0) {
+      tls_log(MOD_TLS_MEMCACHE_VERSION
+        ": error resetting memcache handle: %s", strerror(errno));
+    }
+  }
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  if (ocsp_mcache != NULL) {
+    if (pr_memcache_conn_clone(session.pool, ocsp_mcache) < 0) {
+      tls_log(MOD_TLS_MEMCACHE_VERSION
+        ": error resetting memcache handle: %s", strerror(errno));
+    }
+  }
+#endif /* PR_USE_OPENSSL_OCSP */
+
+  return 0;
+}
+
 /* Module API tables
  */
 
@@ -943,7 +2094,7 @@ module tls_memcache_module = {
   tls_mcache_init,
 
   /* Session initialization function */
-  NULL,
+  tls_mcache_sess_init,
 
   /* Module version */
   MOD_TLS_MEMCACHE_VERSION
diff --git a/contrib/mod_tls_redis.c b/contrib/mod_tls_redis.c
new file mode 100644
index 0000000..0d9bd2a
--- /dev/null
+++ b/contrib/mod_tls_redis.c
@@ -0,0 +1,1966 @@
+/*
+ * ProFTPD: mod_tls_redis -- a module which provides shared SSL session
+ *                           and OCSP response caches using Redis servers
+ * Copyright (c) 2017 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_tls_redis, contrib software for proftpd 1.3.x and above.
+ * For more information contact TJ Saunders <tj at castaglia.org>.
+ */
+
+#include "conf.h"
+#include "privs.h"
+#include "mod_tls.h"
+#include "json.h"
+
+#define MOD_TLS_REDIS_VERSION		"mod_tls_redis/0.1"
+
+/* Make sure the version of proftpd is as necessary. */
+#if PROFTPD_VERSION_NUMBER < 0x0001030605
+# error "ProFTPD 1.3.6rc5 or later required"
+#endif
+
+module tls_redis_module;
+
+/* For communicating with Redis servers for shared data. */
+static pr_redis_t *sess_redis = NULL;
+
+/* Assume a maximum SSL session (serialized) length of 10K.  Note that this
+ * is different from the SSL_MAX_SSL_SESSION_ID_LENGTH provided by OpenSSL.
+ * There is no limit imposed on the length of the ASN1 description of the
+ * SSL session data.
+ */
+#ifndef TLS_MAX_SSL_SESSION_SIZE
+# define TLS_MAX_SSL_SESSION_SIZE	1024 * 10
+#endif
+
+struct sesscache_entry {
+  uint32_t expires;
+  unsigned int sess_datalen;
+  unsigned char sess_data[TLS_MAX_SSL_SESSION_SIZE];
+};
+
+/* These are the JSON format field names */
+#define SESS_CACHE_JSON_KEY_EXPIRES		"expires"
+#define SESS_CACHE_JSON_KEY_DATA		"data"
+#define SESS_CACHE_JSON_KEY_DATA_LENGTH		"data_len"
+
+/* The difference between sesscache_entry and sesscache_large_entry is that the
+ * buffers in the latter are dynamically allocated from the heap, not
+ * stored in Redis (given that Redis has limits on how much it can store).  The
+ * large_entry struct is used for storing sessions which don't fit into Redis;
+ * this also means that these large entries are NOT shared across processes.
+ */
+struct sesscache_large_entry {
+  time_t expires;
+  unsigned int sess_id_len;
+  const unsigned char *sess_id;
+  unsigned int sess_datalen;
+  const unsigned char *sess_data;
+};
+
+/* These stats are stored in Redis as well, so that the status command can
+ * be run on _any_ proftpd in the cluster.
+ */
+struct sesscache_key {
+  const char *key;
+  const char *desc;
+};
+
+static struct sesscache_key sesscache_keys[] = {
+  { "cache_hits", "Cache lifetime hits" },
+  { "cache_misses", "Cache lifetime misses" },
+  { "cache_stores", "Cache lifetime sessions stored" },
+  { "cache_deletes", "Cache lifetime sessions deleted" },
+  { "cache_errors", "Cache lifetime errors handling sessions in cache" },
+  { "cache_exceeds", "Cache lifetime sessions exceeding max entry size" },
+  { "cache_max_sess_len", "Largest session exceeding max entry size" },
+  { NULL, NULL }
+};
+
+/* Indexes into the sesscache_keys array */
+#define SESSCACHE_KEY_HITS	0
+#define SESSCACHE_KEY_MISSES	1
+#define SESSCACHE_KEY_STORES	2
+#define SESSCACHE_KEY_DELETES	3
+#define SESSCACHE_KEY_ERRORS	4
+#define SESSCACHE_KEY_EXCEEDS	5
+#define SESSCACHE_KEY_MAX_LEN	6
+
+static tls_sess_cache_t sess_cache;
+static array_header *sesscache_sess_list = NULL;
+
+#if defined(PR_USE_OPENSSL_OCSP)
+static pr_redis_t *ocsp_redis = NULL;
+
+/* Assume a maximum OCSP response (serialized) length of 4K. */
+# ifndef TLS_MAX_OCSP_RESPONSE_SIZE
+#  define TLS_MAX_OCSP_RESPONSE_SIZE		1024 * 4
+# endif
+
+struct ocspcache_entry {
+  time_t age;
+  unsigned int fingerprint_len;
+  char fingerprint[EVP_MAX_MD_SIZE];
+  unsigned int resp_derlen;
+  unsigned char resp_der[TLS_MAX_OCSP_RESPONSE_SIZE];
+};
+
+/* These are the JSON format field names */
+#define OCSP_CACHE_JSON_KEY_AGE			"expires"
+#define OCSP_CACHE_JSON_KEY_RESPONSE		"response"
+#define OCSP_CACHE_JSON_KEY_RESPONSE_LENGTH	"response_len"
+
+/* The difference between ocspcache_entry and ocspcache_large_entry is that the
+ * buffers in the latter are dynamically allocated from the heap, not
+ * stored in Redis (given that Redis has limits on how much it can store).  The
+ * large_entry struct is used for storing responses which don't fit into Redis;
+ * this also means that these large entries are NOT shared across processes.
+ */
+struct ocspcache_large_entry {
+  time_t age;
+  unsigned int fingerprint_len;
+  char *fingerprint;
+  unsigned int resp_derlen;
+  unsigned char *resp_der;
+};
+
+/* These stats are stored in Redis as well, so that the status command can be
+ * run on _any_ proftpd in the cluster.
+ */
+struct ocspcache_key {
+  const char *key;
+  const char *desc;
+};
+
+static struct ocspcache_key ocspcache_keys[] = {
+  { "cache_hits", "Cache lifetime hits" },
+  { "cache_misses", "Cache lifetime misses" },
+  { "cache_stores", "Cache lifetime responses stored" },
+  { "cache_deletes", "Cache lifetime responses deleted" },
+  { "cache_errors", "Cache lifetime errors handling responses in cache" },
+  { "cache_exceeds", "Cache lifetime responses exceeding max entry size" },
+  { "cache_max_resp_len", "Largest response exceeding max entry size" },
+  { NULL, NULL }
+};
+
+/* Indexes into the ocspcache_keys array */
+#define OCSPCACHE_KEY_HITS	0
+#define OCSPCACHE_KEY_MISSES	1
+#define OCSPCACHE_KEY_STORES	2
+#define OCSPCACHE_KEY_DELETES	3
+#define OCSPCACHE_KEY_ERRORS	4
+#define OCSPCACHE_KEY_EXCEEDS	5
+#define OCSPCACHE_KEY_MAX_LEN	6
+
+static tls_ocsp_cache_t ocsp_cache;
+static array_header *ocspcache_resp_list = NULL;
+#endif
+
+static const char *trace_channel = "tls.redis";
+
+static int sess_cache_close(tls_sess_cache_t *);
+#if defined(PR_USE_OPENSSL_OCSP)
+static int ocsp_cache_close(tls_ocsp_cache_t *);
+#endif /* PR_USE_OPENSSL_OCSP */
+static int tls_redis_sess_init(void);
+
+static const char *redis_get_errors(void) {
+  unsigned int count = 0;
+  unsigned long error_code;
+  BIO *bio = NULL;
+  char *data = NULL;
+  long datalen;
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
+
+  /* Use ERR_print_errors() and a memory BIO to build up a string with
+   * all of the error messages from the error queue.
+   */
+
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
+    bio = BIO_new(BIO_s_mem());
+  }
+
+  while (error_code) {
+    pr_signals_handle();
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  }
+
+  datalen = BIO_get_mem_data(bio, &data);
+  if (data) {
+    data[datalen] = '\0';
+    str = pstrdup(permanent_pool, data);
+  }
+
+  if (bio != NULL) {
+    BIO_free(bio);
+  }
+
+  return str;
+}
+
+/* SSL session cache implementation callbacks.
+ */
+
+/* Functions for marshalling key/value data to/from Redis. */
+
+static int sess_cache_get_json_key(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, void **key, size_t *keysz) {
+  pr_json_object_t *json;
+  char *sess_id_hex, *json_text;
+
+  sess_id_hex = pr_str_bin2hex(p, sess_id, sess_id_len, 0);
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_string(p, json, "id", sess_id_hex);
+
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
+
+  /* Include the terminating NUL in the key. */
+  *keysz = strlen(json_text) + 1;
+  *key = pstrndup(p, json_text, *keysz - 1);
+
+  return 0;
+}
+
+static int sess_cache_get_key(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, void **key, size_t *keysz) {
+  int res;
+
+  res = sess_cache_get_json_key(p, sess_id, sess_id_len, key, keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error constructing cache JSON lookup key for session ID (%lu bytes)",
+      (unsigned long) keysz);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int entry_get_json_number(pool *p, pr_json_object_t *json,
+    const char *key, double *val, const char *text) {
+  if (pr_json_object_get_number(p, json, key, val) < 0) {
+    if (errno == EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+       "ignoring non-number '%s' JSON field in '%s'", key, text);
+
+    } else {
+      tls_log(MOD_TLS_REDIS_VERSION
+        ": missing required '%s' JSON field in '%s'", key, text);
+    }
+
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int entry_get_json_string(pool *p, pr_json_object_t *json,
+    const char *key, char **val, const char *text) {
+  if (pr_json_object_get_string(p, json, key, val) < 0) {
+    if (errno == EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+       "ignoring non-string '%s' JSON field in '%s'", key, text);
+
+    } else {
+      tls_log(MOD_TLS_REDIS_VERSION
+        ": missing required '%s' JSON field in '%s'", key, text);
+    }
+
+    (void) pr_json_object_free(json);
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int sess_cache_entry_decode_json(pool *p, void *value, size_t valuesz,
+    struct sesscache_entry *se) {
+  int res;
+  pr_json_object_t *json;
+  const char *key;
+  char *entry, *text;
+  double number;
+
+  entry = value;
+  if (pr_json_text_validate(p, entry) == FALSE) {
+    tls_log(MOD_TLS_REDIS_VERSION
+      ": unable to decode invalid JSON session cache entry: '%s'", entry);
+    errno = EINVAL;
+    return -1;
+  }
+
+  json = pr_json_object_from_text(p, entry);
+
+  key = SESS_CACHE_JSON_KEY_EXPIRES;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  se->expires = (uint32_t) number;
+
+  key = SESS_CACHE_JSON_KEY_DATA;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res == 0) {
+    int have_padding = FALSE;
+    char *base64_data;
+    size_t base64_datalen;
+    unsigned char *data;
+
+    base64_data = text;
+    base64_datalen = strlen(base64_data);
+
+    /* Due to Base64's padding, we need to detect if the last block was
+     * padded with zeros; we do this by looking for '=' characters at the
+     * end of the text being decoded.  If we see these characters, then we
+     * will "trim" off any trailing zero values in the decoded data, on the
+     * ASSUMPTION that they are the auto-added padding bytes.
+     */
+    if (base64_data[base64_datalen-1] == '=') {
+      have_padding = TRUE;
+    }
+
+    data = se->sess_data;
+    res = EVP_DecodeBlock(data, (unsigned char *) base64_data,
+      (int) base64_datalen);
+    if (res <= 0) {
+      /* Base64-decoding error. */
+      pr_trace_msg(trace_channel, 5,
+        "error base64-decoding session data in '%s', rejecting", entry);
+      errno = EINVAL;
+      return -1;
+    }
+
+    if (have_padding) {
+      /* Assume that only one or two zero bytes of padding were added. */
+      if (data[res-1] == '\0') {
+        res -= 1;
+
+        if (data[res-1] == '\0') {
+          res -= 1;
+        }
+      }
+    }
+
+  } else {
+    return -1;
+  }
+
+  key = SESS_CACHE_JSON_KEY_DATA_LENGTH;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  se->sess_datalen = (unsigned int) number;
+
+  (void) pr_json_object_free(json);
+  return 0;
+}
+
+static int sess_cache_redis_entry_get(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, struct sesscache_entry *se) {
+  int res;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+
+  res = sess_cache_get_key(p, sess_id, sess_id_len, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to get cache entry: error getting cache key: %s",
+      strerror(errno));
+
+    return -1;
+  }
+
+  value = pr_redis_kget(p, sess_redis, &tls_redis_module, (const char *) key,
+    keysz, &valuesz);
+  if (value == NULL) {
+    pr_trace_msg(trace_channel, 3,
+      "no matching Redis entry found for session ID (%lu bytes)",
+      (unsigned long) keysz);
+    errno = ENOENT;
+    return -1;
+  }
+
+  /* Decode the cached session data. */
+  res = sess_cache_entry_decode_json(p, value, valuesz, se);
+  if (res == 0) {
+    time_t now;
+
+    /* Check for expired cache entries. */
+    time(&now);
+
+    if (se->expires <= now) {
+      pr_trace_msg(trace_channel, 4,
+        "ignoring expired cached session data (expires %lu <= now %lu)",
+        (unsigned long) se->expires, (unsigned long) now);
+      errno = EPERM;
+      return -1;
+    }
+
+    pr_trace_msg(trace_channel, 9, "retrieved JSON session data from cache");
+  }
+
+  return 0;
+}
+
+static int sess_cache_redis_entry_delete(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len) {
+  int res;
+  void *key = NULL;
+  size_t keysz = 0;
+
+  res = sess_cache_get_key(p, sess_id, sess_id_len, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to remove cache entry: error getting cache key: %s",
+      strerror(errno));
+
+    return -1;
+  }
+
+  res = pr_redis_kremove(sess_redis, &tls_redis_module, (const char *) key,
+    keysz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "unable to remove Redis entry for session ID (%lu bytes): %s",
+      (unsigned long) keysz, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+ 
+  return 0;
+}
+
+static int sess_cache_entry_encode_json(pool *p, void **value, size_t *valuesz,
+    struct sesscache_entry *se) {
+  pr_json_object_t *json;
+  pool *tmp_pool;
+  char *base64_data = NULL, *json_text;
+
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_number(p, json, SESS_CACHE_JSON_KEY_EXPIRES,
+    (double) se->expires);
+
+  /* Base64-encode the session data.  Note that EVP_EncodeBlock does
+   * NUL-terminate the encoded data.
+   */
+  tmp_pool = make_sub_pool(p);
+  base64_data = pcalloc(tmp_pool, se->sess_datalen * 2);
+
+  EVP_EncodeBlock((unsigned char *) base64_data, se->sess_data,
+    (int) se->sess_datalen);
+  (void) pr_json_object_set_string(p, json, SESS_CACHE_JSON_KEY_DATA,
+    base64_data);
+  (void) pr_json_object_set_number(p, json, SESS_CACHE_JSON_KEY_DATA_LENGTH,
+    (double) se->sess_datalen);
+  destroy_pool(tmp_pool);
+
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
+
+  /* Safety check */
+  if (pr_json_text_validate(p, json_text) == FALSE) {
+    pr_trace_msg(trace_channel, 1, "invalid JSON emitted: '%s'", json_text);
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Include the terminating NUL in the value. */
+  *valuesz = strlen(json_text) + 1;
+  *value = pstrndup(p, json_text, *valuesz - 1);
+
+  return 0;
+}
+
+static int sess_cache_redis_entry_set(pool *p, const unsigned char *sess_id,
+    unsigned int sess_id_len, struct sesscache_entry *se) {
+  int res, xerrno = 0;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+
+  /* Encode the SSL session data. */
+  res = sess_cache_entry_encode_json(p, &value, &valuesz, se);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 4, "error JSON encoding session data: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = sess_cache_get_key(p, sess_id, sess_id_len, &key, &keysz);
+  xerrno = errno;
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to set cache entry: error getting cache key: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = pr_redis_kset(sess_redis, &tls_redis_module, (const char *) key,
+    keysz, value, valuesz, se->expires);
+  xerrno = errno;
+
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "unable to add Redis entry for session ID (%lu bytes): %s",
+      (unsigned long) keysz, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9, "stored JSON session data in cache");
+  return 0;
+}
+
+static int sess_cache_open(tls_sess_cache_t *cache, char *info, long timeout) {
+  config_rec *c;
+
+  cache->cache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(cache->cache_pool, MOD_TLS_REDIS_VERSION);
+
+  pr_trace_msg(trace_channel, 9, "opening Redis cache %p (info '%s')",
+    cache, info ? info : "(none)");
+
+  /* This is a little messy, but necessary. The mod_redis module does not set
+   * the Redis server until a connection arrives.  But mod_tls opens its
+   * session cache prior to that, when the server is starting up.  Thus we need
+   * to set the configured Redis server ourselves.
+   */
+  c = find_config(main_server->conf, CONF_PARAM, "RedisEngine", FALSE);
+  if (c != NULL) {
+    int engine;
+
+    engine = *((int *) c->argv[0]);
+    if (engine == FALSE) {
+      pr_trace_msg(trace_channel, 2, "%s",
+        "Redis support disabled (see RedisEngine directive)");
+      errno = EPERM;
+      return -1;
+    }
+  }
+
+  sess_redis = pr_redis_conn_new(cache->cache_pool, &tls_redis_module, 0);
+  if (sess_redis == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error connecting to Redis: %s", strerror(errno));
+    errno = EPERM;
+    return -1;
+  }
+
+  /* Configure a namespace prefix for our Redis keys. */
+  if (pr_redis_conn_set_namespace(sess_redis, &tls_redis_module,
+      "mod_tls_redis.sessions.", 23) < 0) {
+    pr_trace_msg(trace_channel, 2, 
+      "error setting Redis namespace prefix: %s", strerror(errno));
+  }
+
+  cache->cache_timeout = timeout;
+  return 0;
+}
+
+static int sess_cache_close(tls_sess_cache_t *cache) {
+  pr_trace_msg(trace_channel, 9, "closing Redis session cache %p", cache);
+
+  if (cache != NULL &&
+      cache->cache_pool != NULL) {
+
+    /* We do NOT destroy the cache_pool here or close the Redis connection;
+     * both were created at daemon startup, and should live as long as
+     * the daemon lives.
+     */
+
+    if (sesscache_sess_list != NULL) {
+      register unsigned int i;
+      struct sesscache_large_entry *entries;
+
+      entries = sesscache_sess_list->elts;
+      for (i = 0; i < sesscache_sess_list->nelts; i++) {
+        struct sesscache_large_entry *entry;
+
+        entry = &(entries[i]);
+        if (entry->expires > 0) {
+          pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
+        }
+      }
+
+      clear_array(sesscache_sess_list);
+    }
+  }
+
+  return 0;
+}
+
+static int sess_cache_add_large_sess(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len, time_t expires,
+    SSL_SESSION *sess, int sess_len) {
+  struct sesscache_large_entry *entry = NULL;
+
+  if (sess_len > TLS_MAX_SSL_SESSION_SIZE) {
+    int res;
+    const char *exceeds_key = sesscache_keys[SESSCACHE_KEY_EXCEEDS].key,
+      *max_len_key = sesscache_keys[SESSCACHE_KEY_MAX_LEN].key;
+    void *value = NULL;
+    size_t valuesz = 0;
+    pool *tmp_pool;
+
+    res = pr_redis_incr(sess_redis, &tls_redis_module, exceeds_key, 1, NULL);
+    if (res < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", exceeds_key, strerror(errno));
+    }
+
+    /* XXX Yes, this is subject to race conditions; other proftpd servers
+     * might also be modifying this value in Redis.  Oh well.
+     */
+
+    tmp_pool = make_sub_pool(cache->cache_pool);
+    value = pr_redis_get(tmp_pool, sess_redis, &tls_redis_module, max_len_key,
+      &valuesz);
+    if (value != NULL) {
+      uint64_t max_len;
+
+      memcpy(&max_len, value, valuesz);
+      if ((uint64_t) sess_len > max_len) {
+        if (pr_redis_set(sess_redis, &tls_redis_module, max_len_key, &max_len,
+            sizeof(max_len), 0) < 0) {
+          pr_trace_msg(trace_channel, 2,
+            "error setting '%s' value: %s", max_len_key, strerror(errno));
+        }
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 2,
+        "error getting '%s' value: %s", max_len_key, strerror(errno));
+    }
+
+    destroy_pool(tmp_pool);
+  }
+
+  if (sesscache_sess_list != NULL) {
+    register unsigned int i;
+    struct sesscache_large_entry *entries;
+    time_t now;
+    int ok = FALSE;
+
+    /* Look for any expired sessions in the list to overwrite/reuse. */
+    entries = sesscache_sess_list->elts;
+    time(&now);
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      entry = &(entries[i]);
+
+      if (entry->expires <= now) {
+        /* This entry has expired; clear and reuse its slot. */
+        entry->expires = 0;
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
+
+        ok = TRUE;
+        break;
+      }
+    }
+
+    if (!ok) {
+      /* We didn't find an open slot in the list.  Need to add one. */
+      entry = push_array(sesscache_sess_list);
+    }
+
+  } else {
+    sesscache_sess_list = make_array(cache->cache_pool, 1,
+      sizeof(struct sesscache_large_entry));
+    entry = push_array(sesscache_sess_list);
+  }
+
+  entry->expires = expires;
+  entry->sess_id_len = sess_id_len;
+  entry->sess_id = palloc(cache->cache_pool, sess_id_len);
+  memcpy((unsigned char *) entry->sess_id, sess_id, sess_id_len);
+  entry->sess_datalen = sess_len;
+  entry->sess_data = palloc(cache->cache_pool, sess_len);
+  i2d_SSL_SESSION(sess, (unsigned char **) &(entry->sess_data));
+
+  return 0;
+}
+
+static int sess_cache_add(tls_sess_cache_t *cache, const unsigned char *sess_id,
+    unsigned int sess_id_len, time_t expires, SSL_SESSION *sess) {
+  struct sesscache_entry entry;
+  int sess_len;
+  unsigned char *ptr;
+  time_t now;
+
+  time(&now);
+  pr_trace_msg(trace_channel, 9,
+    "adding session to Redis cache %p (expires = %lu, now = %lu)", cache,
+    (unsigned long) expires, (unsigned long) now);
+
+  /* First we need to find out how much space is needed for the serialized
+   * session data.  There is no known maximum size for SSL session data;
+   * this module is currently designed to allow only up to a certain size.
+   */
+  sess_len = i2d_SSL_SESSION(sess, NULL);
+  if (sess_len > TLS_MAX_SSL_SESSION_SIZE) {
+    pr_trace_msg(trace_channel, 2,
+      "length of serialized SSL session data (%d) exceeds maximum size (%u), "
+      "unable to add to shared Redis, adding to list", sess_len,
+      TLS_MAX_SSL_SESSION_SIZE);
+
+    /* Instead of rejecting the add here, we add the session to a "large
+     * session" list.  Thus the large session would still be cached per process
+     * and will not be lost.
+     */
+
+    return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
+      sess, sess_len);
+  }
+
+  entry.expires = expires;
+  entry.sess_datalen = sess_len;
+  ptr = entry.sess_data;
+  i2d_SSL_SESSION(sess, &ptr);
+
+  if (sess_cache_redis_entry_set(cache->cache_pool, sess_id, sess_id_len,
+      &entry) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error adding session to Redis: %s", strerror(errno));
+
+    /* Add this session to the "large session" list instead as a fallback. */
+    return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
+        sess, sess_len);
+
+  } else {
+    const char *key = sesscache_keys[SESSCACHE_KEY_STORES].key;
+
+    if (pr_redis_incr(sess_redis, &tls_redis_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+  }
+
+  return 0;
+}
+
+static SSL_SESSION *sess_cache_get(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len) {
+  struct sesscache_entry entry;
+  time_t now;
+  SSL_SESSION *sess = NULL;
+
+  pr_trace_msg(trace_channel, 9, "getting session from Redis cache %p", cache); 
+
+  /* Look for the requested session in the "large session" list first. */
+  if (sesscache_sess_list != NULL) {
+    register unsigned int i;
+    struct sesscache_large_entry *entries;
+
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *large_entry;
+
+      large_entry = &(entries[i]);
+      if (large_entry->expires > 0 &&
+          large_entry->sess_id_len == sess_id_len &&
+          memcmp(large_entry->sess_id, sess_id,
+            large_entry->sess_id_len) == 0) {
+
+        now = time(NULL);
+        if (large_entry->expires > now) {
+          TLS_D2I_SSL_SESSION_CONST unsigned char *ptr;
+
+          ptr = large_entry->sess_data;
+          sess = d2i_SSL_SESSION(NULL, &ptr, large_entry->sess_datalen);
+          if (sess == NULL) {
+            pr_trace_msg(trace_channel, 2,
+              "error retrieving session from cache: %s", redis_get_errors());
+
+          } else {
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  if (sess != NULL) {
+    return sess;
+  }
+
+  if (sess_cache_redis_entry_get(cache->cache_pool, sess_id, sess_id_len,
+      &entry) < 0) {
+    return NULL;
+  }
+ 
+  now = time(NULL);
+  if (entry.expires > now) {
+    TLS_D2I_SSL_SESSION_CONST unsigned char *ptr;
+
+    ptr = entry.sess_data;
+    sess = d2i_SSL_SESSION(NULL, &ptr, entry.sess_datalen);
+    if (sess != NULL) {
+      const char *key = sesscache_keys[SESSCACHE_KEY_HITS].key;
+
+      if (pr_redis_incr(sess_redis, &tls_redis_module, key, 1, NULL) < 0) {
+        pr_trace_msg(trace_channel, 2,
+          "error incrementing '%s' value: %s", key, strerror(errno));
+      }
+
+    } else {
+      const char *key = sesscache_keys[SESSCACHE_KEY_ERRORS].key;
+
+      pr_trace_msg(trace_channel, 2,
+        "error retrieving session from cache: %s", redis_get_errors());
+
+      if (pr_redis_incr(sess_redis, &tls_redis_module, key, 1,
+          NULL) < 0) {
+        pr_trace_msg(trace_channel, 2,
+          "error incrementing '%s' value: %s", key, strerror(errno));
+      }
+    }
+  }
+
+  if (sess == NULL) {
+    const char *key = sesscache_keys[SESSCACHE_KEY_MISSES].key;
+
+    if (pr_redis_incr(sess_redis, &tls_redis_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+
+    errno = ENOENT;
+  }
+
+  return sess;
+}
+
+static int sess_cache_delete(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len) {
+  const char *key = sesscache_keys[SESSCACHE_KEY_DELETES].key;
+  int res;
+
+  pr_trace_msg(trace_channel, 9, "removing session from Redis cache %p", cache);
+
+  /* Look for the requested session in the "large session" list first. */
+  if (sesscache_sess_list != NULL) {
+    register unsigned int i;
+    struct sesscache_large_entry *entries;
+
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
+
+      entry = &(entries[i]);
+      if (entry->sess_id_len == sess_id_len &&
+          memcmp(entry->sess_id, sess_id, entry->sess_id_len) == 0) {
+
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
+        entry->expires = 0;
+        return 0;
+      }
+    }
+  }
+
+  res = sess_cache_redis_entry_delete(cache->cache_pool, sess_id, sess_id_len);
+  if (res < 0) {
+    return -1;
+  }
+
+  /* Don't forget to update the stats. */
+
+  if (pr_redis_incr(sess_redis, &tls_redis_module, key, 1, NULL) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing '%s' value: %s", key, strerror(errno));
+  }
+
+  return res;
+}
+
+static int sess_cache_clear(tls_sess_cache_t *cache) {
+  register unsigned int i;
+  int res = 0;
+
+  if (sess_redis == NULL) {
+    pr_trace_msg(trace_channel, 9, "missing required Redis connection");
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9, "clearing Redis session cache %p", cache);
+
+  if (sesscache_sess_list != NULL) {
+    struct sesscache_large_entry *entries;
+    
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
+
+      entry = &(entries[i]);
+      entry->expires = 0;
+      pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
+    }
+  }
+
+  /* XXX iterate through keys, kremoving any "mod_tls_redis" prefixed keys */
+
+  return res;
+}
+
+static int sess_cache_remove(tls_sess_cache_t *cache) {
+  int res;
+
+  pr_trace_msg(trace_channel, 9, "removing Redis session cache %p", cache);
+
+  res = sess_cache_clear(cache);
+  /* XXX close Redis conn */
+
+  return res;
+}
+
+static int sess_cache_status(tls_sess_cache_t *cache,
+    void (*statusf)(void *, const char *, ...), void *arg, int flags) {
+  register unsigned int i;
+  pool *tmp_pool;
+
+  pr_trace_msg(trace_channel, 9, "checking Redis session cache %p", cache);
+
+  tmp_pool = make_sub_pool(permanent_pool);
+
+  statusf(arg, "%s", "Redis SSL session cache provided by "
+    MOD_TLS_REDIS_VERSION);
+  statusf(arg, "%s", "");
+  statusf(arg, "Redis server: ");
+
+  for (i = 0; sesscache_keys[i].key != NULL; i++) {
+    const char *key, *desc;
+    void *value = NULL;
+    size_t valuesz = 0;
+
+    key = sesscache_keys[i].key;
+    desc = sesscache_keys[i].desc;
+
+    value = pr_redis_get(tmp_pool, sess_redis, &tls_redis_module, key,
+      &valuesz);
+    if (value != NULL) {
+      uint64_t num = 0;
+      memcpy(&num, value, valuesz);
+      statusf(arg, "%s: %lu", desc, (unsigned long) num);
+    }
+  }
+
+  /* XXX run stats on Redis servers? */
+
+#if 0
+  if (flags & TLS_SESS_CACHE_STATUS_FL_SHOW_SESSIONS) {
+    statusf(arg, "%s", "");
+    statusf(arg, "%s", "Cached sessions:");
+
+    /* XXX Get keys, looking for our namespace prefix, dump each one */
+
+    /* We _could_ use SSL_SESSION_print(), which is what the sess_id
+     * command-line tool does.  The problem is that SSL_SESSION_print() shows
+     * too much (particularly, it shows the master secret).  And
+     * SSL_SESSION_print() does not support a flags argument to use for
+     * specifying which bits of the session we want to print.
+     *
+     * Instead, we get to do the more dangerous (compatibility-wise) approach
+     * of rolling our own printing function.
+     */
+
+    for (i = 0; i < 0; i++) {
+      struct sesscache_entry *entry;
+
+      pr_signals_handle();
+
+      /* XXX Get entries */
+      if (entry->expires > 0) {
+        SSL_SESSION *sess;
+        TLS_D2I_SSL_SESSION_CONST unsigned char *ptr;
+        time_t ts;
+
+        ptr = entry->sess_data;
+        sess = d2i_SSL_SESSION(NULL, &ptr, entry->sess_datalen); 
+        if (sess == NULL) {
+          pr_log_pri(PR_LOG_NOTICE, MOD_TLS_REDIS_VERSION
+            ": error retrieving session from cache: %s", redis_get_errors());
+          continue;
+        }
+
+        statusf(arg, "%s", "  -----BEGIN SSL SESSION PARAMETERS-----");
+
+        /* XXX Directly accessing these fields cannot be a Good Thing. */
+        if (sess->session_id_length > 0) {
+          char *sess_id_str;
+
+          sess_id_str = pr_str2hex(tmp_pool, sess->session_id,
+            sess->session_id_length, PR_STR_FL_HEX_USE_UC);
+
+          statusf(arg, "    Session ID: %s", sess_id_str);
+        }
+
+        if (sess->sid_ctx_length > 0) {
+          char *sid_ctx_str;
+
+          sid_ctx_str = pr_str2hex(tmp_pool, sess->sid_ctx,
+            sess->sid_ctx_length, PR_STR_FL_HEX_USE_UC);
+
+          statusf(arg, "    Session ID Context: %s", sid_ctx_str);
+        }
+
+        switch (sess->ssl_version) {
+          case SSL3_VERSION:
+            statusf(arg, "    Protocol: %s", "SSLv3");
+            break;
+
+          case TLS1_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1");
+            break;
+
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+          case TLS1_1_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1.1");
+            break;
+
+          case TLS1_2_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1.2");
+            break;
+#endif
+
+          default:
+            statusf(arg, "    Protocol: %s", "unknown");
+        }
+
+        ts = SSL_SESSION_get_time(sess);
+        statusf(arg, "    Started: %s", pr_strtime(ts));
+        ts = entry->expires;
+        statusf(arg, "    Expires: %s (%u secs)", pr_strtime(ts),
+          SSL_SESSION_get_timeout(sess));
+
+        SSL_SESSION_free(sess);
+        statusf(arg, "%s", "  -----END SSL SESSION PARAMETERS-----");
+        statusf(arg, "%s", "");
+      }
+    }
+  }
+#endif
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+#if defined(PR_USE_OPENSSL_OCSP)
+/* OCSP response cache implementation callbacks.
+ */
+
+/* Functions for marshalling key/value data to/from Redis. */
+
+static int ocsp_cache_get_json_key(pool *p, const char *fingerprint,
+    void **key, size_t *keysz) {
+  pr_json_object_t *json;
+  char *json_text;
+
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_string(p, json, "fingerprint", fingerprint);
+
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
+
+  /* Include the terminating NUL in the key. */
+  *keysz = strlen(json_text) + 1;
+  *key = pstrndup(p, json_text, *keysz - 1);
+
+  return 0;
+}
+
+static int ocsp_cache_get_key(pool *p, const char *fingerprint, void **key,
+    size_t *keysz) {
+  int res;
+
+  res = ocsp_cache_get_json_key(p, fingerprint, key, keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error constructing ocsp cache JSON lookup key for fingerprint '%s'",
+      fingerprint);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_entry_decode_json(pool *p, void *value, size_t valuesz,
+    struct ocspcache_entry *oe) {
+  int res;
+  pr_json_object_t *json;
+  const char *key;
+  char *entry, *text;
+  double number;
+
+  entry = value;
+  if (pr_json_text_validate(p, entry) == FALSE) {
+    tls_log(MOD_TLS_REDIS_VERSION
+      ": unable to decode invalid JSON ocsp cache entry: '%s'", entry);
+    errno = EINVAL;
+    return -1;
+  }
+
+  json = pr_json_object_from_text(p, entry);
+
+  key = OCSP_CACHE_JSON_KEY_AGE;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  oe->age = (uint32_t) number;
+
+  key = OCSP_CACHE_JSON_KEY_RESPONSE;
+  res = entry_get_json_string(p, json, key, &text, entry);
+  if (res == 0) {
+    int have_padding = FALSE;
+    char *base64_data;
+    size_t base64_datalen;
+    unsigned char *data;
+
+    base64_data = text;
+    base64_datalen = strlen(base64_data);
+
+    /* Due to Base64's padding, we need to detect if the last block was
+     * padded with zeros; we do this by looking for '=' characters at the
+     * end of the text being decoded.  If we see these characters, then we
+     * will "trim" off any trailing zero values in the decoded data, on the
+     * ASSUMPTION that they are the auto-added padding bytes.
+     */
+    if (base64_data[base64_datalen-1] == '=') {
+      have_padding = TRUE;
+    }
+
+    data = oe->resp_der;
+    res = EVP_DecodeBlock(data, (unsigned char *) base64_data,
+      (int) base64_datalen);
+    if (res <= 0) {
+      /* Base64-decoding error. */
+      pr_trace_msg(trace_channel, 5,
+        "error base64-decoding OCSP data in '%s', rejecting", entry);
+      (void) pr_json_object_free(json);
+      errno = EINVAL;
+      return -1;
+    }
+
+    if (have_padding) {
+      /* Assume that only one or two zero bytes of padding were added. */
+      if (data[res-1] == '\0') {
+        res -= 1;
+
+        if (data[res-1] == '\0') {
+          res -= 1;
+        }
+      }
+    }
+
+  } else {
+    return -1;
+  }
+
+  key = OCSP_CACHE_JSON_KEY_RESPONSE_LENGTH;
+  res = entry_get_json_number(p, json, key, &number, entry);
+  if (res < 0) {
+    return -1;
+  }
+  oe->resp_derlen = (unsigned int) number;
+
+  pr_json_object_free(json);
+  return 0;
+}
+
+static int ocsp_cache_redis_entry_get(pool *p, const char *fingerprint,
+    struct ocspcache_entry *oe) {
+  int res;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+
+  res = ocsp_cache_get_key(p, fingerprint, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to get ocsp cache entry: error getting cache key: %s",
+      strerror(errno));
+
+    return -1;
+  }
+
+  value = pr_redis_kget(p, ocsp_redis, &tls_redis_module, (const char *) key,
+    keysz, &valuesz);
+  if (value == NULL) {
+    pr_trace_msg(trace_channel, 3,
+      "no matching Redis entry found for fingerprint '%s'", fingerprint);
+    errno = ENOENT;
+    return -1;
+  }
+
+  /* Decode the cached response data. */
+  res = ocsp_cache_entry_decode_json(p, value, valuesz, oe);
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 9, "retrieved JSON response data from cache");
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_redis_entry_delete(pool *p, const char *fingerprint) {
+  int res;
+  void *key = NULL;
+  size_t keysz = 0;
+
+  res = ocsp_cache_get_key(p, fingerprint, &key, &keysz);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to remove ocsp cache entry: error getting cache key: %s",
+      strerror(errno));
+
+    return -1;
+  }
+
+  res = pr_redis_kremove(ocsp_redis, &tls_redis_module, (const char *) key,
+    keysz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "unable to remove Redis entry for fingerpring '%s': %s", fingerprint,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_entry_encode_json(pool *p, void **value, size_t *valuesz,
+    struct ocspcache_entry *oe) {
+  pr_json_object_t *json;
+  pool *tmp_pool;
+  char *base64_data = NULL, *json_text;
+
+  json = pr_json_object_alloc(p);
+  (void) pr_json_object_set_number(p, json, OCSP_CACHE_JSON_KEY_AGE,
+    (double) oe->age);
+
+  /* Base64-encode the response data.  Note that EVP_EncodeBlock does
+   * NUL-terminate the encoded data.
+   */
+  tmp_pool = make_sub_pool(p);
+  base64_data = pcalloc(tmp_pool, (oe->resp_derlen * 2) + 1);
+
+  EVP_EncodeBlock((unsigned char *) base64_data, oe->resp_der,
+    (int) oe->resp_derlen);
+  (void) pr_json_object_set_string(p, json, OCSP_CACHE_JSON_KEY_RESPONSE,
+    base64_data);
+  (void) pr_json_object_set_number(p, json, OCSP_CACHE_JSON_KEY_RESPONSE_LENGTH,
+    (double) oe->resp_derlen);
+  destroy_pool(tmp_pool);
+
+  json_text = pr_json_object_to_text(p, json, "");
+  (void) pr_json_object_free(json);
+
+  /* Safety check */
+  if (pr_json_text_validate(p, json_text) == FALSE) {
+    pr_trace_msg(trace_channel, 1, "invalid JSON emitted: '%s'", json_text);
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Include the terminating NUL in the value. */
+  *valuesz = strlen(json_text) + 1;
+  *value = pstrndup(p, json_text, *valuesz - 1);
+
+  return 0;
+}
+
+static int ocsp_cache_redis_entry_set(pool *p, const char *fingerprint,
+    struct ocspcache_entry *oe) {
+  int res, xerrno = 0;
+  void *key = NULL, *value = NULL;
+  size_t keysz = 0, valuesz = 0;
+
+  /* Encode the OCSP response data. */
+  res = ocsp_cache_entry_encode_json(p, &value, &valuesz, oe);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 4, "error JSON encoding OCSP response data: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = ocsp_cache_get_key(p, fingerprint, &key, &keysz);
+  xerrno = errno;
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "unable to set ocsp cache entry: error getting cache key: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = pr_redis_kset(ocsp_redis, &tls_redis_module, (const char *) key, keysz,
+    value, valuesz, 0);
+  xerrno = errno;
+
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "unable to add Redis entry for fingerprint '%s': %s", fingerprint,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9, "stored OCSP JSON response data in cache");
+  return 0;
+}
+
+static int ocsp_cache_open(tls_ocsp_cache_t *cache, char *info) {
+  config_rec *c;
+
+  pr_trace_msg(trace_channel, 9, "opening Redis cache %p (info '%s')",
+    cache, info ? info : "(none)");
+
+  cache->cache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(cache->cache_pool, MOD_TLS_REDIS_VERSION);
+
+  /* This is a little messy, but necessary. The mod_redis module does not set
+   * the configured Redis server until a connection arrives.  But mod_tls
+   * opens its session cache prior to that, when the server is starting up.
+   * Thus we need to set the configured Redis server ourselves.
+   */
+  c = find_config(main_server->conf, CONF_PARAM, "RedisEngine", FALSE);
+  if (c != NULL) {
+    int engine;
+
+    engine = *((int *) c->argv[0]);
+    if (engine == FALSE) {
+      pr_trace_msg(trace_channel, 2, "%s",
+        "Redis support disabled (see RedisEngine directive)");
+      errno = EPERM;
+      return -1;
+    }
+  }
+
+  ocsp_redis = pr_redis_conn_new(cache->cache_pool, &tls_redis_module, 0);
+  if (ocsp_redis == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error connecting to Redis: %s", strerror(errno));
+    errno = EPERM;
+    return -1;
+  }
+
+  /* Configure a namespace prefix for our Redis keys. */
+  if (pr_redis_conn_set_namespace(ocsp_redis, &tls_redis_module,
+      "mod_tls_redis.ocsp.", 19) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting Redis namespace prefix: %s", strerror(errno));
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_close(tls_ocsp_cache_t *cache) {
+  pr_trace_msg(trace_channel, 9, "closing Redis ocsp cache %p", cache);
+
+  if (cache != NULL &&
+      cache->cache_pool != NULL) {
+
+    /* We do NOT destroy the cache_pool here or close the redis connection;
+     * both were created at daemon startup, and should live as long as
+     * the daemon lives.
+     */
+
+    if (ocspcache_resp_list != NULL) {
+      register unsigned int i;
+      struct ocspcache_large_entry *entries;
+
+      entries = ocspcache_resp_list->elts;
+      for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+        struct ocspcache_large_entry *entry;
+
+        entry = &(entries[i]);
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+        entry->age = 0;
+      }
+
+      clear_array(ocspcache_resp_list);
+    }
+  }
+
+  return 0;
+}
+
+static int ocsp_cache_add_large_resp(tls_ocsp_cache_t *cache,
+    const char *fingerprint, OCSP_RESPONSE *resp, time_t resp_age) {
+  struct ocspcache_large_entry *entry = NULL;
+  int resp_derlen;
+  unsigned char *ptr;
+
+  resp_derlen = i2d_OCSP_RESPONSE(resp, NULL);
+  if (resp_derlen > TLS_MAX_OCSP_RESPONSE_SIZE) {
+    const char *exceeds_key = ocspcache_keys[OCSPCACHE_KEY_EXCEEDS].key,
+      *max_len_key = ocspcache_keys[OCSPCACHE_KEY_MAX_LEN].key;
+    void *value = NULL;
+    size_t valuesz = 0;
+    pool *tmp_pool;
+
+    if (pr_redis_incr(ocsp_redis, &tls_redis_module, exceeds_key, 1,
+        NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", exceeds_key, strerror(errno));
+    }
+
+    /* XXX Yes, this is subject to race conditions; other proftpd servers
+     * might also be modifying this value in Redis.  Oh well.
+     */
+
+    tmp_pool = make_sub_pool(cache->cache_pool);
+    value = pr_redis_get(tmp_pool, ocsp_redis, &tls_redis_module, max_len_key,
+      &valuesz);
+    if (value != NULL) {
+      uint64_t max_len;
+
+      memcpy(&max_len, value, valuesz);
+      if ((uint64_t) resp_derlen > max_len) {
+        if (pr_redis_set(ocsp_redis, &tls_redis_module, max_len_key, &max_len,
+            sizeof(max_len), 0) < 0) {
+          pr_trace_msg(trace_channel, 2,
+            "error setting '%s' value: %s", max_len_key, strerror(errno));
+        }
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 2,
+        "error getting '%s' value: %s", max_len_key, strerror(errno));
+    }
+
+    destroy_pool(tmp_pool);
+  }
+
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+    time_t now;
+    int ok = FALSE;
+
+    /* Look for any expired sessions in the list to overwrite/reuse. */
+    entries = ocspcache_resp_list->elts;
+    time(&now);
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      entry = &(entries[i]);
+
+      if (entry->age > (now - 3600)) {
+        /* This entry has expired; clear and reuse its slot. */
+        entry->age = 0;
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+
+        ok = TRUE;
+        break;
+      }
+    }
+
+    if (!ok) {
+      /* We didn't find an open slot in the list.  Need to add one. */
+      entry = push_array(ocspcache_resp_list);
+    }
+
+  } else {
+    ocspcache_resp_list = make_array(cache->cache_pool, 1,
+      sizeof(struct ocspcache_large_entry));
+    entry = push_array(ocspcache_resp_list);
+  }
+
+  entry->age = resp_age;
+  entry->fingerprint_len = strlen(fingerprint);
+  entry->fingerprint = pstrdup(cache->cache_pool, fingerprint);
+  entry->resp_derlen = resp_derlen;
+  entry->resp_der = ptr = palloc(cache->cache_pool, resp_derlen);
+  i2d_OCSP_RESPONSE(resp, &ptr);
+
+  return 0;
+}
+
+static int ocsp_cache_add(tls_ocsp_cache_t *cache, const char *fingerprint,
+    OCSP_RESPONSE *resp, time_t resp_age) {
+  struct ocspcache_entry entry;
+  int resp_derlen;
+  unsigned char *ptr;
+
+  pr_trace_msg(trace_channel, 9, "adding response to Redis ocsp cache %p",
+    cache);
+
+  /* First we need to find out how much space is needed for the serialized
+   * response data.  There is no known maximum size for OCSP response data;
+   * this module is currently designed to allow only up to a certain size.
+   */
+  resp_derlen = i2d_OCSP_RESPONSE(resp, NULL);
+  if (resp_derlen > TLS_MAX_OCSP_RESPONSE_SIZE) {
+    pr_trace_msg(trace_channel, 2,
+      "length of serialized OCSP response data (%d) exceeds maximum size (%u), "
+      "unable to add to shared Redis, adding to list", resp_derlen,
+      TLS_MAX_OCSP_RESPONSE_SIZE);
+
+    /* Instead of rejecting the add here, we add the response to a "large
+     * response" list.  Thus the large response would still be cached per
+     * process and will not be lost.
+     */
+
+    return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+  }
+
+  entry.age = resp_age;
+  entry.resp_derlen = resp_derlen;
+  ptr = entry.resp_der;
+  i2d_OCSP_RESPONSE(resp, &ptr);
+
+  if (ocsp_cache_redis_entry_set(cache->cache_pool, fingerprint, &entry) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error adding response to Redis: %s", strerror(errno));
+
+    /* Add this response to the "large response" list instead as a fallback. */
+    return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+
+  } else {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_STORES].key;
+
+    if (pr_redis_incr(ocsp_redis, &tls_redis_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+  }
+
+  return 0;
+}
+
+static OCSP_RESPONSE *ocsp_cache_get(tls_ocsp_cache_t *cache,
+    const char *fingerprint, time_t *resp_age) {
+  struct ocspcache_entry entry;
+  OCSP_RESPONSE *resp = NULL;
+  size_t fingerprint_len;
+  const unsigned char *ptr;
+
+  pr_trace_msg(trace_channel, 9, "getting response from Redis ocsp cache %p",
+    cache);
+
+  fingerprint_len = strlen(fingerprint);
+
+  /* Look for the requested response in the "large response" list first. */
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *large_entry;
+
+      large_entry = &(entries[i]);
+      if (large_entry->fingerprint_len > 0 &&
+          large_entry->fingerprint_len == fingerprint_len &&
+          memcmp(large_entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+        ptr = large_entry->resp_der;
+        resp = d2i_OCSP_RESPONSE(NULL, &ptr, large_entry->resp_derlen);
+        if (resp == NULL) {
+          pr_trace_msg(trace_channel, 2,
+            "error retrieving response from ocsp cache: %s",
+            redis_get_errors());
+
+        } else {
+          *resp_age = large_entry->age;
+          break;
+        }
+      }
+    }
+  }
+
+  if (resp) {
+    return resp;
+  }
+
+  if (ocsp_cache_redis_entry_get(cache->cache_pool, fingerprint, &entry) < 0) {
+    return NULL;
+  }
+
+  ptr = entry.resp_der;
+  resp = d2i_OCSP_RESPONSE(NULL, &ptr, entry.resp_derlen);
+  if (resp != NULL) {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_HITS].key;
+
+    *resp_age = entry.age;
+
+    if (pr_redis_incr(ocsp_redis, &tls_redis_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+
+  } else {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_ERRORS].key;
+
+    pr_trace_msg(trace_channel, 2,
+      "error retrieving response from ocsp cache: %s", redis_get_errors());
+
+    if (pr_redis_incr(ocsp_redis, &tls_redis_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+  }
+
+  if (resp == NULL) {
+    const char *key = ocspcache_keys[OCSPCACHE_KEY_MISSES].key;
+
+    if (pr_redis_incr(ocsp_redis, &tls_redis_module, key, 1, NULL) < 0) {
+      pr_trace_msg(trace_channel, 2,
+        "error incrementing '%s' value: %s", key, strerror(errno));
+    }
+
+    errno = ENOENT;
+  }
+
+  return resp;
+}
+
+static int ocsp_cache_delete(tls_ocsp_cache_t *cache,
+    const char *fingerprint) {
+  const char *key = ocspcache_keys[OCSPCACHE_KEY_DELETES].key;
+  int res;
+  size_t fingerprint_len;
+
+  pr_trace_msg(trace_channel, 9, "deleting response from Redis ocsp cache %p",
+    cache);
+
+  fingerprint_len = strlen(fingerprint);
+
+  /* Look for the requested response in the "large response" list first. */
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      if (entry->fingerprint_len == fingerprint_len &&
+          memcmp(entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+        entry->age = 0;
+
+        return 0;
+      }
+    }
+  }
+
+  res = ocsp_cache_redis_entry_delete(cache->cache_pool, fingerprint);
+  if (res < 0) {
+    return -1;
+  }
+
+  /* Don't forget to update the stats. */
+
+  if (pr_redis_incr(ocsp_redis, &tls_redis_module, key, 1, NULL) < 0) {
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing '%s' value: %s", key, strerror(errno));
+  }
+
+  return res;
+}
+
+static int ocsp_cache_clear(tls_ocsp_cache_t *cache) {
+  register unsigned int i;
+  int res = 0;
+
+  if (ocsp_redis == NULL) {
+    pr_trace_msg(trace_channel, 9, "missing required Redis connection");
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9, "clearing Redis ocsp cache %p", cache);
+
+  if (ocspcache_resp_list != NULL) {
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      entry->age = 0;
+      pr_memscrub(entry->resp_der, entry->resp_derlen);
+      entry->resp_derlen = 0;
+      pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+      entry->fingerprint_len = 0;
+    }
+  }
+
+  /* XXX iterate through keys, kremoving any "mod_tls_redis" prefixed keys */
+
+  return res;
+}
+
+static int ocsp_cache_remove(tls_ocsp_cache_t *cache) {
+  int res;
+
+  pr_trace_msg(trace_channel, 9, "removing Redis ocsp cache %p", cache);
+
+  res = ocsp_cache_clear(cache);
+  /* XXX close Redis conn */
+
+  return res;
+}
+
+static int ocsp_cache_status(tls_ocsp_cache_t *cache,
+    void (*statusf)(void *, const char *, ...), void *arg, int flags) {
+  register unsigned int i;
+  pool *tmp_pool;
+
+  pr_trace_msg(trace_channel, 9, "checking Redis ocsp cache %p", cache);
+
+  tmp_pool = make_sub_pool(permanent_pool);
+
+  statusf(arg, "%s", "Redis OCSP response cache provided by "
+    MOD_TLS_REDIS_VERSION);
+  statusf(arg, "%s", "");
+  statusf(arg, "Redis server: ");
+
+  for (i = 0; ocspcache_keys[i].key != NULL; i++) {
+    const char *key, *desc;
+    void *value = NULL;
+    size_t valuesz = 0;
+
+    key = ocspcache_keys[i].key;
+    desc = ocspcache_keys[i].desc;
+
+    value = pr_redis_get(tmp_pool, ocsp_redis, &tls_redis_module, key,
+      &valuesz);
+    if (value != NULL) {
+      uint64_t num = 0;
+      memcpy(&num, value, valuesz);
+      statusf(arg, "%s: %lu", desc, (unsigned long) num);
+    }
+  }
+
+  /* XXX run stats on Redis servers? */
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+#endif /* PR_USE_OPENSSL_OCSP */
+
+/* Event Handlers
+ */
+
+#if defined(PR_SHARED_MODULE)
+static void tls_redis_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_tls_redis.c", (const char *) event_data) == 0) {
+    pr_event_unregister(&tls_redis_module, NULL, NULL);
+    tls_sess_cache_unregister("redis");
+# if defined(PR_USE_OPENSSL_OCSP)
+    tls_ocsp_cache_unregister("redis");
+# endif /* PR_USE_OPENSSL_OCSP */
+
+    if (sess_redis != NULL) {
+      (void) pr_redis_conn_destroy(sess_redis);
+      sess_redis = NULL;
+    }
+
+    if (ocsp_redis != NULL) {
+      (void) pr_redis_conn_destroy(ocsp_redis);
+      ocsp_redis = NULL;
+    }
+  }
+}
+#endif /* !PR_SHARED_MODULE */
+
+/* Initialization functions
+ */
+
+static int tls_redis_init(void) {
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&tls_redis_module, "core.module-unload",
+    tls_redis_mod_unload_ev, NULL);
+#endif /* !PR_SHARED_MODULE */
+
+  /* Prepare our SSL session cache handler. */
+  memset(&sess_cache, 0, sizeof(sess_cache));
+
+  sess_cache.cache_name = "redis";
+  pr_pool_tag(sess_cache.cache_pool, MOD_TLS_REDIS_VERSION);
+
+  sess_cache.open = sess_cache_open;
+  sess_cache.close = sess_cache_close;
+  sess_cache.add = sess_cache_add;
+  sess_cache.get = sess_cache_get;
+  sess_cache.delete = sess_cache_delete;
+  sess_cache.clear = sess_cache_clear;
+  sess_cache.remove = sess_cache_remove;
+  sess_cache.status = sess_cache_status;
+
+#ifdef SSL_SESS_CACHE_NO_INTERNAL
+  /* Take a chance, and inform OpenSSL that it does not need to use its own
+   * internal session cache lookups/storage; using the external session cache
+   * (i.e. us) will be enough.
+   */
+  sess_cache.cache_mode = SSL_SESS_CACHE_NO_INTERNAL;
+#endif
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  /* Prepare our OCSP response cache handler. */
+  memset(&ocsp_cache, 0, sizeof(ocsp_cache));
+
+  ocsp_cache.cache_name = "redis";
+  pr_pool_tag(ocsp_cache.cache_pool, MOD_TLS_REDIS_VERSION);
+
+  ocsp_cache.open = ocsp_cache_open;
+  ocsp_cache.close = ocsp_cache_close;
+  ocsp_cache.add = ocsp_cache_add;
+  ocsp_cache.get = ocsp_cache_get;
+  ocsp_cache.delete = ocsp_cache_delete;
+  ocsp_cache.clear = ocsp_cache_clear;
+  ocsp_cache.remove = ocsp_cache_remove;
+  ocsp_cache.status = ocsp_cache_status;
+#endif /* PR_USE_OPENSSL_OCSP */
+
+#if defined(PR_USE_REDIS)
+  if (tls_sess_cache_register("redis", &sess_cache) < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_REDIS_VERSION
+      ": notice: error registering 'redis' SSL session cache: %s",
+      strerror(errno));
+    return -1;
+  }
+
+# if defined(PR_USE_OPENSSL_OCSP)
+  if (tls_ocsp_cache_register("redis", &ocsp_cache) < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_REDIS_VERSION
+      ": notice: error registering 'redis' OCSP response cache: %s",
+      strerror(errno));
+    return -1;
+  }
+# endif /* PR_USE_OPENSSL_OCSP */
+
+#else
+  pr_log_debug(DEBUG1, MOD_TLS_REDIS_VERSION
+    ": unable to register 'redis' SSL session cache: Redis support not enabled");
+# if defined(PR_USE_OPENSSL_OCSP)
+  pr_log_debug(DEBUG1, MOD_TLS_REDIS_VERSION
+    ": unable to register 'redis' OCSP response cache: Redis support not enabled");
+# endif /* PR_USE_OPENSSL_OCSP */
+#endif /* PR_USE_REDIS */
+
+  return 0;
+}
+
+static int tls_redis_sess_init(void) {
+  /* Reset our Redis handles. */
+
+/* XXX Are these necessary? */
+
+#if 0
+  if (sess_redis != NULL) {
+    if (pr_redis_conn_clone(session.pool, sess_redis) < 0) {
+      tls_log(MOD_TLS_REDIS_VERSION
+        ": error resetting Redis handle: %s", strerror(errno));
+    }
+  }
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  if (ocsp_redis != NULL) {
+    if (pr_redis_conn_clone(session.pool, ocsp_redis) < 0) {
+      tls_log(MOD_TLS_REDIS_VERSION
+        ": error resetting Redis handle: %s", strerror(errno));
+    }
+  }
+#endif /* PR_USE_OPENSSL_OCSP */
+#endif
+
+  return 0;
+}
+
+/* Module API tables
+ */
+
+module tls_redis_module = {
+  NULL, NULL,
+
+  /* Module API version 2.0 */
+  0x20,
+
+  /* Module name */
+  "tls_redis",
+
+  /* Module configuration handler table */
+  NULL,
+
+  /* Module command handler table */
+  NULL,
+
+  /* Module authentication handler table */
+  NULL,
+
+  /* Module initialization function */
+  tls_redis_init,
+
+  /* Session initialization function */
+  tls_redis_sess_init,
+
+  /* Module version */
+  MOD_TLS_REDIS_VERSION
+};
diff --git a/contrib/mod_tls_shmcache.c b/contrib/mod_tls_shmcache.c
index ea31925..22407af 100644
--- a/contrib/mod_tls_shmcache.c
+++ b/contrib/mod_tls_shmcache.c
@@ -1,8 +1,8 @@
 /*
- * ProFTPD: mod_tls_shmcache -- a module which provides a shared SSL session
- *                              cache using SysV shared memory
- *
- * Copyright (c) 2009-2016 TJ Saunders
+ * ProFTPD: mod_tls_shmcache -- a module which provides shared SSL session
+ *                              and OCSP response caches using SysV shared
+ *                              memory segments
+ * Copyright (c) 2009-2017 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
@@ -25,9 +25,6 @@
  *
  * This is mod_tls_shmcache, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- *  --- DO NOT DELETE BELOW THIS LINE ----
- *  $Libraries: -lssl -lcrypto$
  */
 
 #include "conf.h"
@@ -41,16 +38,21 @@
 # include <sys/mman.h>
 #endif
 
-#define MOD_TLS_SHMCACHE_VERSION		"mod_tls_shmcache/0.1"
+/* Define if you have the LibreSSL library.  */
+#if defined(LIBRESSL_VERSION_NUMBER)
+# define HAVE_LIBRESSL	1
+#endif
+
+#define MOD_TLS_SHMCACHE_VERSION		"mod_tls_shmcache/0.2"
 
 /* Make sure the version of proftpd is as necessary. */
-#if PROFTPD_VERSION_NUMBER < 0x0001030301
-# error "ProFTPD 1.3.3rc1 or later required"
+#if PROFTPD_VERSION_NUMBER < 0x0001030602
+# error "ProFTPD 1.3.6rc2 or later required"
 #endif
 
 module tls_shmcache_module;
 
-#define TLS_SHMCACHE_PROJ_ID	247
+#define TLS_SHMCACHE_SESS_PROJECT_ID		247
 
 /* Assume a maximum SSL session (serialized) length of 10K.  Note that this
  * is different from the SSL_MAX_SSL_SESSION_ID_LENGTH provided by OpenSSL.
@@ -72,7 +74,7 @@ module tls_shmcache_module;
  * bytes (500KB).
  */
 
-struct shmcache_entry {
+struct sesscache_entry {
   time_t expires;
   unsigned int sess_id_len;
   unsigned char sess_id[SSL_MAX_SSL_SESSION_ID_LENGTH];
@@ -80,24 +82,24 @@ struct shmcache_entry {
   unsigned char sess_data[TLS_MAX_SSL_SESSION_SIZE];
 };
 
-/* The difference between shmcache_entry and shmcache_large_entry is that the
+/* The difference between sesscache_entry and sesscache_large_entry is that the
  * buffers in the latter are dynamically allocated from the heap, not
  * allocated out of the shm segment.  The large_entry struct is used for
  * storing sessions which don't fit into the normal entry struct; this also
  * means that these large entries are NOT shared across processes.
  */
-struct shmcache_large_entry {
+struct sesscache_large_entry {
   time_t expires;
   unsigned int sess_id_len;
-  unsigned char *sess_id;
+  const unsigned char *sess_id;
   unsigned int sess_datalen;
-  unsigned char *sess_data;
+  const unsigned char *sess_data;
 };
 
 /* The number of entries in the list is determined at run-time, based on
  * the maximum desired size of the shared memory segment.
  */
-struct shmcache_data {
+struct sesscache_data {
 
   /* Cache metadata. */
   unsigned int nhits;
@@ -126,41 +128,125 @@ struct shmcache_data {
   unsigned int sd_listlen, sd_listsz;
 
   /* It is important that this field be the last in the struct! */
-  struct shmcache_entry *sd_entries;
+  struct sesscache_entry *sd_entries;
+};
+
+static tls_sess_cache_t sess_cache;
+static struct sesscache_data *sesscache_data = NULL;
+static size_t sesscache_datasz = 0;
+static int sesscache_shmid = -1;
+static pr_fh_t *sesscache_fh = NULL;
+static array_header *sesscache_sess_list = NULL;
+
+#if defined(PR_USE_OPENSSL_OCSP)
+# define TLS_SHMCACHE_OCSP_PROJECT_ID		249
+
+/* Assume a maximum OCSP response (serialized) length of 4K.
+ */
+# ifndef TLS_MAX_OCSP_RESPONSE_SIZE
+#  define TLS_MAX_OCSP_RESPONSE_SIZE		1024 * 4
+# endif
+
+struct ocspcache_entry {
+  time_t age;
+  unsigned int fingerprint_len;
+  unsigned char fingerprint[EVP_MAX_MD_SIZE];
+  unsigned int resp_derlen;
+  unsigned char resp_der[TLS_MAX_OCSP_RESPONSE_SIZE];
+};
+
+/* The difference between ocspcache_entry and ocspcache_large_entry is that the
+ * buffers in the latter are dynamically allocated from the heap, not
+ * allocated out of the shm segment.  The large_entry struct is used for
+ * storing sessions which don't fit into the normal entry struct; this also
+ * means that these large entries are NOT shared across processes.
+ */
+struct ocspcache_large_entry {
+  time_t age;
+  unsigned int fingerprint_len;
+  unsigned char *fingerprint;
+  unsigned int resp_derlen;
+  unsigned char *resp_der;
 };
 
-static tls_sess_cache_t shmcache;
+/* The number of entries in the list is determined at run-time, based on
+ * the maximum desired size of the shared memory segment.
+ */
+struct ocspcache_data {
+
+  /* Cache metadata. */
+  unsigned int nhits;
+  unsigned int nmisses;
+
+  unsigned int nstored;
+  unsigned int ndeleted;
+  unsigned int nexpired;
+  unsigned int nerrors;
+
+  /* This tracks the number of sessions that could not be added because
+   * they exceeded TLS_MAX_OCSP_RESPONSE_SIZE.
+   */
+  unsigned int nexceeded;
+  unsigned int exceeded_maxsz;
+
+  /* These listlen/listsz track the number of entries in the cache and total
+   * entries possible, and thus can be used for determining the fullness of
+   * the cache.
+   */
+  unsigned int od_listlen, od_listsz;
 
-static struct shmcache_data *shmcache_data = NULL;
-static size_t shmcache_datasz = 0;
-static int shmcache_shmid = -1;
-static pr_fh_t *shmcache_fh = NULL;
+  /* It is important that this field be the last in the struct! */
+  struct ocspcache_entry *od_entries;
+};
 
-static array_header *shmcache_sess_list = NULL;
+static tls_ocsp_cache_t ocsp_cache;
+static struct ocspcache_data *ocspcache_data = NULL;
+static size_t ocspcache_datasz = 0;
+static int ocspcache_shmid = -1;
+static pr_fh_t *ocspcache_fh = NULL;
+static array_header *ocspcache_resp_list = NULL;
+#endif /* PR_USE_OPENSSL_OCSP */
 
 static const char *trace_channel = "tls.shmcache";
 
-static int shmcache_close(tls_sess_cache_t *);
+static int sess_cache_close(tls_sess_cache_t *);
+#if defined(PR_USE_OPENSSL_OCSP)
+static int ocsp_cache_close(tls_ocsp_cache_t *);
+#endif /* PR_USE_OPENSSL_OCSP */
 
-static const char *shmcache_get_crypto_errors(void) {
+static const char *shmcache_get_errors(void) {
   unsigned int count = 0;
-  unsigned long e = ERR_get_error();
+  unsigned long error_code;
   BIO *bio = NULL;
   char *data = NULL;
   long datalen;
-  const char *str = "(unknown)";
+  const char *error_data = NULL, *str = "(unknown)";
+  int error_flags = 0;
 
   /* Use ERR_print_errors() and a memory BIO to build up a string with
    * all of the error messages from the error queue.
    */
 
-  if (e)
+  error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
+  if (error_code) {
     bio = BIO_new(BIO_s_mem());
+  }
 
-  while (e) {
+  while (error_code) {
     pr_signals_handle();
-    BIO_printf(bio, "\n  (%u) %s", ++count, ERR_error_string(e, NULL));
-    e = ERR_get_error();
+
+    if (error_flags & ERR_TXT_STRING) {
+      BIO_printf(bio, "\n  (%u) %s [%s]", ++count,
+        ERR_error_string(error_code, NULL), error_data);
+
+    } else {
+      BIO_printf(bio, "\n  (%u) %s", ++count,
+        ERR_error_string(error_code, NULL));
+    }
+
+    error_data = NULL;
+    error_flags = 0;
+    error_code = ERR_get_error_line_data(NULL, NULL, &error_data, &error_flags);
   }
 
   datalen = BIO_get_mem_data(bio, &data);
@@ -169,8 +255,9 @@ static const char *shmcache_get_crypto_errors(void) {
     str = pstrdup(permanent_pool, data);
   }
 
-  if (bio)
+  if (bio != NULL) {
     BIO_free(bio);
+  }
 
   return str;
 }
@@ -203,7 +290,7 @@ static const char *shmcache_get_lock_desc(int lock_type) {
  * if the process dies tragically.  Could possibly deal with this in an
  * exit event handler, though.  Something to keep in mind.
  */
-static int shmcache_lock_shm(int lock_type) {
+static int shmcache_lock_shm(pr_fh_t *fh, int lock_type) {
   const char *lock_desc;
   int fd;
   struct flock lock;
@@ -214,10 +301,10 @@ static int shmcache_lock_shm(int lock_type) {
   lock.l_start = 0;
   lock.l_len = 0;
 
-  fd = PR_FH_FD(shmcache_fh);
+  fd = PR_FH_FD(fh);
   lock_desc = shmcache_get_lock_desc(lock_type);
 
-  pr_trace_msg(trace_channel, 9, "attempting to %s shmcache fd %d", lock_desc,
+  pr_trace_msg(trace_channel, 19, "attempting to %s shmcache fd %d", lock_desc,
     fd);
 
   while (fcntl(fd, F_SETLK, &lock) < 0) {
@@ -261,25 +348,24 @@ static int shmcache_lock_shm(int lock_type) {
     return -1;
   }
 
-  pr_trace_msg(trace_channel, 9, "%s of shmcache fd %d succeeded", lock_desc,
+  pr_trace_msg(trace_channel, 19, "%s of shmcache fd %d succeeded", lock_desc,
     fd);
   return 0;
 }
 
-/* Use a hash function to hash the given lookup key to a slot in the
- * sd_entries list.  This hash, module the number of entries, is the initial
- * iteration start point.  This will hopefully avoid having to do many linear
- * scans for the add/get/delete operations.
+/* Use a hash function to hash the given lookup key to a slot in the entries
+ * list.  This hash, module the number of entries, is the initial iteration
+ * start point.  This will hopefully avoid having to do many linear scans for
+ * the add/get/delete operations.
  *
  * Use Perl's hashing algorithm.
  */
-static unsigned int shmcache_hash(unsigned char *sess_id,
-    unsigned int sess_id_len) {
+static unsigned int shmcache_hash(const unsigned char *id, unsigned int len) {
   unsigned int i = 0;
-  size_t sz = sess_id_len;
+  size_t sz = len;
 
   while (sz--) {
-    const unsigned char *k = sess_id;
+    const unsigned char *k = id;
     unsigned int c = *k;
     k++;
 
@@ -292,33 +378,13 @@ static unsigned int shmcache_hash(unsigned char *sess_id,
   return i;
 }
 
-static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
-    size_t requested_size) {
-  int rem, shmid, xerrno = 0;
-  int shm_existed = FALSE;
-  struct shmcache_data *data = NULL;
-  size_t shm_size;
-  unsigned int shm_sess_max = 0;
+static void *shmcache_get_shm(pr_fh_t *fh, size_t *shm_size, int project_id,
+    int *shm_id) {
+  int rem, shm_existed = FALSE, xerrno = 0;
   key_t key;
+  void *data = NULL;
 
-  /* Calculate the size to allocate.  First, calculate the maximum number
-   * of sessions we can cache, given the configured size.  Then
-   * calculate the shm segment size to allocate to hold that number of
-   * sessions.  Round the segment size up to the nearest SHMLBA boundary.
-   */
-  shm_sess_max = (requested_size - sizeof(struct shmcache_data)) /
-    (sizeof(struct shmcache_entry));
-  shm_size = sizeof(struct shmcache_data) +
-    (shm_sess_max * sizeof(struct shmcache_entry));
-
-  rem = shm_size % SHMLBA;
-  if (rem != 0) {
-    shm_size = (shm_size - rem + SHMLBA);
-    pr_trace_msg(trace_channel, 9,
-      "rounded requested size up to %lu bytes", (unsigned long) shm_size);
-  }
-
-  key = ftok(fh->fh_path, TLS_SHMCACHE_PROJ_ID);
+  key = ftok(fh->fh_path, project_id);
   if (key == (key_t) -1) {
     xerrno = errno;
 
@@ -329,6 +395,14 @@ static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
     return NULL;
   }
 
+  /* Round the requested segment size up to the nearest SHMBLA boundary. */
+  rem = *shm_size % SHMLBA;
+  if (rem != 0) {
+    *shm_size = (*shm_size - rem + SHMLBA);
+    pr_trace_msg(trace_channel, 9,
+      "rounded requested size up to %lu bytes", (unsigned long) *shm_size);
+  }
+
   /* Try first using IPC_CREAT|IPC_EXCL, to check if there is an existing
    * shm for this key.  If so, use a flags value of zero.
    *
@@ -339,20 +413,20 @@ static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
    */
 
   PRIVS_ROOT
-  shmid = shmget(key, shm_size, IPC_CREAT|IPC_EXCL|0600);
+  *shm_id = shmget(key, *shm_size, IPC_CREAT|IPC_EXCL|0600);
   xerrno = errno;
   PRIVS_RELINQUISH
 
-  if (shmid < 0) {
+  if (*shm_id < 0) {
     if (xerrno == EEXIST) {
       shm_existed = TRUE;
 
       PRIVS_ROOT
-      shmid = shmget(key, 0, 0);
+      *shm_id = shmget(key, 0, 0);
       xerrno = errno;
       PRIVS_RELINQUISH
 
-      if (shmid < 0) {
+      if (*shm_id < 0) {
         pr_trace_msg(trace_channel, 1,
           "unable to get shm for existing key: %s", strerror(xerrno));
         errno = xerrno;
@@ -364,7 +438,7 @@ static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
       if (xerrno == ENOMEM) {
         pr_trace_msg(trace_channel, 1,
           "not enough memory for %lu shm bytes; try specifying a smaller size",
-          (unsigned long) shm_size);
+          (unsigned long) *shm_size);
 
       } else if (xerrno == ENOSPC) {
         pr_trace_msg(trace_channel, 1, "%s",
@@ -377,17 +451,17 @@ static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
   }
 
   /* Attach to the shm. */
-  pr_trace_msg(trace_channel, 10,
-    "attempting to attach to shm ID %d", shmid);
+  pr_trace_msg(trace_channel, 10, "attempting to attach to shm ID %d",
+    *shm_id);
 
   PRIVS_ROOT
-  data = (struct shmcache_data *) shmat(shmid, NULL, 0);
+  data = shmat(*shm_id, NULL, 0);
   xerrno = errno;
   PRIVS_RELINQUISH
 
   if (data == NULL) {
     pr_trace_msg(trace_channel, 1,
-      "unable to attach to shm ID %d: %s", shmid, strerror(xerrno));
+      "unable to attach to shm ID %d: %s", *shm_id, strerror(xerrno));
     errno = xerrno;
     return NULL;
   }
@@ -402,7 +476,7 @@ static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
      */
 
     PRIVS_ROOT
-    res = shmctl(shmid, IPC_STAT, &ds);
+    res = shmctl(*shm_id, IPC_STAT, &ds);
     xerrno = errno;
     PRIVS_RELINQUISH
 
@@ -410,89 +484,145 @@ static struct shmcache_data *shmcache_get_shm(pr_fh_t *fh,
       pr_trace_msg(trace_channel, 10,
         "existing shm size: %u bytes", (unsigned int) ds.shm_segsz);
 
-      if (ds.shm_segsz != shm_size) {
-        if (ds.shm_segsz > shm_size) {
+      if ((unsigned long) ds.shm_segsz != (unsigned long) *shm_size) {
+        if ((unsigned long) ds.shm_segsz > (unsigned long) *shm_size) {
           pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
             ": requested shm size (%lu bytes) is smaller than existing shm "
             "size, migrating to smaller shm (may result in loss of cache data)",
-            (unsigned long) shm_size);
+            (unsigned long) *shm_size);
 
-        } else if (ds.shm_segsz < shm_size) {
+        } else if ((unsigned long) ds.shm_segsz < (unsigned long) *shm_size) {
           pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
             ": requested shm size (%lu bytes) is larger than existing shm "
-            "size, migrating to larger shm", (unsigned long) shm_size);
+            "size, migrating to larger shm", (unsigned long) *shm_size);
         }
 
-        /* XXX In future versions, we could probably handle the migration
-         * of the existing cache data to a different size shm segment.
-         *
-         * If the shm size was increased:
-         *   Simply iterate through existing valid cached sessions and
-         *   load them into the new shm segment; the hash index will change
-         *   because of the change in shm size, hence the reloading.
-         *
-         * If the shm size was decreased:
-         *   Need to first sort existing valid cached sessions by expiration
-         *   timestamp, in _decreasing_ order; this sorted order is the order
-         *   in which they will be loaded into the new shm.  This guarantees
-         *   that the sessions _most_ likely to expire will be added later,
-         *   when the possibility of a cache fill in the smaller shm is higher.
-         *   Those older sessions, once the cache is full, would be lost.
-         *
-         * For now, though, we complain about this, and tell the admin to
-         * manually remove shm.
-         */
-
         pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
           ": remove existing shmcache using 'ftpdctl tls sesscache remove' "
-          "before using new size");
-
-        shmcache_close(NULL);
+          "or 'ftpdctl tls ocspcache remove' before using new size");
 
-        errno = EINVAL;
+        errno = EEXIST;
         return NULL;
       }
 
     } else {
       pr_trace_msg(trace_channel, 1,
-        "unable to stat shm ID %d: %s", shmid, strerror(xerrno));
+        "unable to stat shm ID %d: %s", *shm_id, strerror(xerrno));
       errno = xerrno;
     }
 
   } else {
     /* Make sure the memory is initialized. */
-    if (shmcache_lock_shm(F_WRLCK) < 0) {
-      pr_trace_msg(trace_channel, 1,
-        "error write-locking shmcache: %s", strerror(errno));
+    if (shmcache_lock_shm(fh, F_WRLCK) < 0) {
+      pr_trace_msg(trace_channel, 1, "error write-locking shm: %s",
+        strerror(errno));
     }
 
-    memset(data, 0, shm_size);
+    memset(data, 0, *shm_size);
 
-    if (shmcache_lock_shm(F_UNLCK) < 0) {
-      pr_trace_msg(trace_channel, 1,
-        "error unlocking shmcache: %s", strerror(errno));
+    if (shmcache_lock_shm(fh, F_UNLCK) < 0) {
+      pr_trace_msg(trace_channel, 1, "error unlocking shm: %s",
+        strerror(errno));
     }
   }
 
-  shmcache_datasz = shm_size;
+  return data;
+}
+
+static struct sesscache_data *sess_cache_get_shm(pr_fh_t *fh,
+    size_t requested_size) {
+  int shmid, xerrno = 0;
+  struct sesscache_data *data = NULL;
+  size_t shm_size;
+  unsigned int shm_sess_max = 0;
+
+  /* Calculate the size to allocate.  First, calculate the maximum number
+   * of sessions we can cache, given the configured size.  Then
+   * calculate the shm segment size to allocate to hold that number of
+   * sessions.
+   */
+  shm_sess_max = (requested_size - sizeof(struct sesscache_data)) /
+    (sizeof(struct sesscache_entry));
+  shm_size = sizeof(struct sesscache_data) +
+    (shm_sess_max * sizeof(struct sesscache_entry));
+
+  data = shmcache_get_shm(fh, &shm_size, TLS_SHMCACHE_SESS_PROJECT_ID, &shmid);
+  if (data == NULL) {
+    xerrno = errno;
+
+    if (errno == EEXIST) {
+      sess_cache_close(NULL);
+    }
+
+    errno = xerrno;
+    return NULL;
+  }
 
-  shmcache_shmid = shmid;
+  sesscache_datasz = shm_size;
+  sesscache_shmid = shmid;
   pr_trace_msg(trace_channel, 9,
-    "using shm ID %d for shmcache path '%s'", shmcache_shmid, fh->fh_path);
+    "using shm ID %d for sesscache path '%s' (%u sessions)", sesscache_shmid,
+    fh->fh_path, shm_sess_max);
 
-  data->sd_entries = (struct shmcache_entry *) (data + sizeof(struct shmcache_data));
+  data->sd_entries = (struct sesscache_entry *) (data + sizeof(struct sesscache_data));
   data->sd_listsz = shm_sess_max;
 
   return data;
 }
 
+#if defined(PR_USE_OPENSSL_OCSP)
+static struct ocspcache_data *ocsp_cache_get_shm(pr_fh_t *fh,
+    size_t requested_size) {
+  int shmid, xerrno = 0;
+  struct ocspcache_data *data = NULL;
+  size_t shm_size;
+  unsigned int shm_resp_max = 0;
+
+  /* Calculate the size to allocate.  First, calculate the maximum number
+   * of responses we can cache, given the configured size.  Then
+   * calculate the shm segment size to allocate to hold that number of
+   * responses.
+   */
+  shm_resp_max = (requested_size - sizeof(struct ocspcache_data)) /
+    (sizeof(struct ocspcache_entry));
+  shm_size = sizeof(struct ocspcache_data) +
+    (shm_resp_max * sizeof(struct ocspcache_entry));
+
+  data = shmcache_get_shm(fh, &shm_size, TLS_SHMCACHE_OCSP_PROJECT_ID, &shmid);
+  if (data == NULL) {
+    xerrno = errno;
+
+    if (errno == EEXIST) {
+      ocsp_cache_close(NULL);
+    }
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  ocspcache_datasz = shm_size;
+  ocspcache_shmid = shmid;
+  pr_trace_msg(trace_channel, 9,
+    "using shm ID %d for ocspcache path '%s' (%u responses)", ocspcache_shmid,
+    fh->fh_path, shm_resp_max);
+
+  data->od_entries = (struct ocspcache_entry *) (data + sizeof(struct ocspcache_data));
+  data->od_listsz = shm_resp_max;
+
+  return data;
+}
+#endif /* PR_USE_OPENSSL_OCSP */
+
+/* SSL session cache implementation callbacks.
+ */
+
 /* Scan the entire list, clearing out expired sessions.  Logs the number
  * of sessions that expired and updates the header stat.
  *
  * NOTE: Callers are assumed to handle the locking of the shm before/after
  * calling this function!
  */
-static unsigned int shmcache_flush(void) {
+static unsigned int sess_cache_flush(void) {
   register unsigned int i;
   unsigned int flushed = 0;
   time_t now, next_expiring = 0;
@@ -500,19 +630,19 @@ static unsigned int shmcache_flush(void) {
   now = time(NULL);
 
   /* We always scan the in-memory large session entry list. */
-  if (shmcache_sess_list != NULL) {
-    struct shmcache_large_entry *entries;
+  if (sesscache_sess_list != NULL) {
+    struct sesscache_large_entry *entries;
 
-    entries = shmcache_sess_list->elts;
-    for (i = 0; i < shmcache_sess_list->nelts; i++) {
-      struct shmcache_large_entry *entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
 
       entry = &(entries[i]);
 
       if (entry->expires > now) {
         /* This entry has expired; clear its slot. */
         entry->expires = 0;
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
       }
     }
   }
@@ -520,21 +650,21 @@ static unsigned int shmcache_flush(void) {
   /* If now is earlier than the earliest expiring session in the cache,
    * then a scan will be pointless.
    */
-  if (now < shmcache_data->next_expiring) {
+  if (now < sesscache_data->next_expiring) {
     unsigned int secs;
 
-    secs = shmcache_data->next_expiring - now;
+    secs = sesscache_data->next_expiring - now;
     tls_log("shmcache: no expired sessions to flush; %u secs to next "
       "expiration", secs);
     return 0;
   }
 
-  tls_log("shmcache: flushing cache of expired sessions");
+  tls_log("shmcache: flushing session cache of expired sessions");
 
-  for (i = 0; i < shmcache_data->sd_listsz; i++) {
-    struct shmcache_entry *entry;
+  for (i = 0; i < sesscache_data->sd_listsz; i++) {
+    struct sesscache_entry *entry;
 
-    entry = &(shmcache_data->sd_entries[i]);
+    entry = &(sesscache_data->sd_entries[i]);
     if (entry->expires > 0) {
       if (entry->expires > now) {
         if (entry->expires < next_expiring) {
@@ -544,41 +674,38 @@ static unsigned int shmcache_flush(void) {
       } else {
         /* This entry has expired; clear its slot. */
         entry->expires = 0;
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
 
         /* Don't forget to update the stats. */
-        shmcache_data->nexpired++;
+        sesscache_data->nexpired++;
 
-        if (shmcache_data->sd_listlen > 0) {
-          shmcache_data->sd_listlen--;
+        if (sesscache_data->sd_listlen > 0) {
+          sesscache_data->sd_listlen--;
         }
 
         flushed++;
       }
     }
 
-    shmcache_data->next_expiring = next_expiring;
+    sesscache_data->next_expiring = next_expiring;
   }
 
-  tls_log("shmcache: flushed %u expired %s from cache", flushed,
+  tls_log("shmcache: flushed %u expired %s from session cache", flushed,
     flushed != 1 ? "sessions" : "session");
   return flushed;
 }
 
-/* Cache implementation callbacks.
- */
-
-static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
+static int sess_cache_open(tls_sess_cache_t *cache, char *info, long timeout) {
   int fd, xerrno;
   char *ptr;
   size_t requested_size;
   struct stat st;
 
-  pr_trace_msg(trace_channel, 9, "opening shmcache cache %p", cache);
+  pr_trace_msg(trace_channel, 9, "opening shmcache session cache %p", cache);
 
   /* The info string must be formatted like:
    *
-   *  /file=%s[&size=%u]
+   *  /path=%s[&size=%u]
    *
    * where the optional size is in bytes.  There is a minimum size; if the
    * configured size is less than the minimum, it's an error.  The default
@@ -614,10 +741,10 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
         size_t min_size;
 
         /* The bare minimum size MUST be able to hold at least one session. */
-        min_size = sizeof(struct shmcache_data) +
-          sizeof(struct shmcache_entry);
+        min_size = sizeof(struct sesscache_data) +
+          sizeof(struct sesscache_entry);
 
-        if (size < min_size) {
+        if ((size_t) size < min_size) {
           pr_trace_msg(trace_channel, 1,
             "requested size (%lu bytes) smaller than minimum size "
             "(%lu bytes), ignoring", (unsigned long) size,
@@ -658,18 +785,18 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
     return -1;
   }
 
-  /* If shmcache_fh is not null, then we are a restarted server.  And if
+  /* If sesscache_fh is not null, then we are a restarted server.  And if
    * the 'info' path does not match that previous fh, then the admin
    * has changed the configuration.
    *
    * For now, we complain about this, and tell the admin to manually remove
    * the old file/shm.
    */
-  if (shmcache_fh != NULL &&
-      strcmp(shmcache_fh->fh_path, info) != 0) {
+  if (sesscache_fh != NULL &&
+      strcmp(sesscache_fh->fh_path, info) != 0) {
     pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
       ": file '%s' does not match previously configured file '%s'",
-      info, shmcache_fh->fh_path);
+      info, sesscache_fh->fh_path);
     pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
       ": remove existing shmcache using 'ftpdctl tls sesscache remove' "
       "before using new file");
@@ -679,11 +806,11 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
   }
 
   PRIVS_ROOT
-  shmcache_fh = pr_fsio_open(info, O_RDWR|O_CREAT);
+  sesscache_fh = pr_fsio_open(info, O_RDWR|O_CREAT);
   xerrno = errno;
   PRIVS_RELINQUISH
 
-  if (shmcache_fh == NULL) {
+  if (sesscache_fh == NULL) {
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
       ": error: unable to open file '%s': %s", info, strerror(xerrno));
 
@@ -691,14 +818,14 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
     return -1;
   }
 
-  if (pr_fsio_fstat(shmcache_fh, &st) < 0) {
+  if (pr_fsio_fstat(sesscache_fh, &st) < 0) {
     xerrno = errno;
 
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
       ": error: unable to stat file '%s': %s", info, strerror(xerrno));
 
-    pr_fsio_close(shmcache_fh);
-    shmcache_fh = NULL;
+    pr_fsio_close(sesscache_fh);
+    sesscache_fh = NULL;
 
     errno = EINVAL;
     return -1;
@@ -710,8 +837,8 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
       ": error: unable to use file '%s': %s", info, strerror(xerrno));
 
-    pr_fsio_close(shmcache_fh);
-    shmcache_fh = NULL;
+    pr_fsio_close(sesscache_fh);
+    sesscache_fh = NULL;
     
     errno = EINVAL;
     return -1;
@@ -721,7 +848,7 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
    * descriptors (stdin/stdout/stderr), as can happen especially if the
    * server has restarted.
    */
-  fd = PR_FH_FD(shmcache_fh);
+  fd = PR_FH_FD(sesscache_fh);
   if (fd <= STDERR_FILENO) {
     int res;
 
@@ -733,96 +860,97 @@ static int shmcache_open(tls_sess_cache_t *cache, char *info, long timeout) {
  
     } else {
       close(fd);
-      PR_FH_FD(shmcache_fh) = res;
+      PR_FH_FD(sesscache_fh) = res;
     }
   }
 
   pr_trace_msg(trace_channel, 9,
-    "requested shmcache file: %s (fd %d)", shmcache_fh->fh_path,
-    PR_FH_FD(shmcache_fh));
+    "requested session cache file: %s (fd %d)", sesscache_fh->fh_path,
+    PR_FH_FD(sesscache_fh));
   pr_trace_msg(trace_channel, 9, 
-    "requested shmcache size: %lu bytes", (unsigned long) requested_size);
+    "requested session cache size: %lu bytes", (unsigned long) requested_size);
 
-  shmcache_data = shmcache_get_shm(shmcache_fh, requested_size);
-  if (shmcache_data == NULL) {
+  sesscache_data = sess_cache_get_shm(sesscache_fh, requested_size);
+  if (sesscache_data == NULL) {
     xerrno = errno;
 
     pr_trace_msg(trace_channel, 1,
-      "unable to allocate shm: %s", strerror(xerrno));
+      "unable to allocate session shm: %s", strerror(xerrno));
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
-      ": unable to allocate shm: %s", strerror(xerrno));
+      ": unable to allocate session shm: %s", strerror(xerrno));
 
-    pr_fsio_close(shmcache_fh);
-    shmcache_fh = NULL;
+    pr_fsio_close(sesscache_fh);
+    sesscache_fh = NULL;
 
     errno = EINVAL;
     return -1;
   }
 
-  cache->cache_pool = make_sub_pool(session.pool);
+  cache->cache_pool = make_sub_pool(permanent_pool);
   pr_pool_tag(cache->cache_pool, MOD_TLS_SHMCACHE_VERSION);
 
   cache->cache_timeout = timeout;
   return 0;
 }
 
-static int shmcache_close(tls_sess_cache_t *cache) {
+static int sess_cache_close(tls_sess_cache_t *cache) {
 
   if (cache != NULL) {
-    pr_trace_msg(trace_channel, 9, "closing shmcache cache %p", cache);
+    pr_trace_msg(trace_channel, 9, "closing shmcache session cache %p", cache);
   }
 
   if (cache != NULL &&
       cache->cache_pool != NULL) {
     destroy_pool(cache->cache_pool);
 
-    if (shmcache_sess_list != NULL) {
+    if (sesscache_sess_list != NULL) {
       register unsigned int i;
-      struct shmcache_large_entry *entries;
+      struct sesscache_large_entry *entries;
 
-      entries = shmcache_sess_list->elts;
-      for (i = 0; i < shmcache_sess_list->nelts; i++) {
-        struct shmcache_large_entry *entry;
+      entries = sesscache_sess_list->elts;
+      for (i = 0; i < sesscache_sess_list->nelts; i++) {
+        struct sesscache_large_entry *entry;
 
         entry = &(entries[i]);
         if (entry->expires > 0) {
-          pr_memscrub(entry->sess_data, entry->sess_datalen);
+          pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
         }
       }
 
-      shmcache_sess_list = NULL;
+      sesscache_sess_list = NULL;
     }
   }
 
-  if (shmcache_shmid >= 0) {
+  if (sesscache_shmid >= 0) {
     int res, xerrno = 0;
 
     PRIVS_ROOT
 #if !defined(_POSIX_SOURCE)
-    res = shmdt((char *) shmcache_data);
+    res = shmdt((char *) sesscache_data);
 #else
-    res = shmdt((const char *) shmcache_data);
+    res = shmdt((const char *) sesscache_data);
 #endif
     xerrno = errno;
     PRIVS_RELINQUISH
 
     if (res < 0) {
       pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
-        ": error detaching shm ID %d: %s", shmcache_shmid, strerror(xerrno));
+        ": error detaching session shm ID %d: %s", sesscache_shmid,
+        strerror(xerrno));
     }
 
-    shmcache_data = NULL;
+    sesscache_data = NULL;
   }
 
-  pr_fsio_close(shmcache_fh);
-  shmcache_fh = NULL;
+  pr_fsio_close(sesscache_fh);
+  sesscache_fh = NULL;
   return 0;
 }
 
-static int shmcache_add_large_sess(tls_sess_cache_t *cache,
-    unsigned char *sess_id, unsigned int sess_id_len, time_t expires,
+static int sess_cache_add_large_sess(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len, time_t expires,
     SSL_SESSION *sess, int sess_len) {
-  struct shmcache_large_entry *entry = NULL;
+  struct sesscache_large_entry *entry = NULL;
 
   if (sess_len > TLS_MAX_SSL_SESSION_SIZE) {
     /* We may get sessions to add to the list which do not exceed the max
@@ -830,47 +958,45 @@ static int shmcache_add_large_sess(tls_sess_cache_t *cache,
      * shmcache.  Don't track these in the 'exceeded' stats'.
      */
 
-    if (shmcache_lock_shm(F_WRLCK) == 0) {
-      shmcache_data->nexceeded++;
-      if (sess_len > shmcache_data->exceeded_maxsz) {
-        shmcache_data->exceeded_maxsz = sess_len;
+    if (shmcache_lock_shm(sesscache_fh, F_WRLCK) == 0) {
+      sesscache_data->nexceeded++;
+      if ((size_t) sess_len > sesscache_data->exceeded_maxsz) {
+        sesscache_data->exceeded_maxsz = sess_len;
       }
 
-      if (shmcache_lock_shm(F_UNLCK) < 0) {
-        tls_log("shmcache: error unlocking shmcache: %s",
-        strerror(errno));
+      if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
+        tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
       }
 
     } else {
-      tls_log("shmcache: error write-locking shmcache: %s",
-        strerror(errno));
+      tls_log("shmcache: error write-locking shmcache: %s", strerror(errno));
     }
   }
 
-  if (shmcache_sess_list != NULL) {
+  if (sesscache_sess_list != NULL) {
     register unsigned int i;
-    struct shmcache_large_entry *entries;
+    struct sesscache_large_entry *entries;
     time_t now;
 
     /* Look for any expired sessions in the list to overwrite/reuse. */
-    entries = shmcache_sess_list->elts;
+    entries = sesscache_sess_list->elts;
     now = time(NULL);
-    for (i = 0; i < shmcache_sess_list->nelts; i++) {
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
       entry = &(entries[i]);
 
       if (entry->expires > now) {
         /* This entry has expired; clear and reuse its slot. */
         entry->expires = 0;
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
 
         break;
       }
     }
 
   } else {
-    shmcache_sess_list = make_array(cache->cache_pool, 1,
-      sizeof(struct shmcache_large_entry));
-    entry = push_array(shmcache_sess_list);
+    sesscache_sess_list = make_array(cache->cache_pool, 1,
+      sizeof(struct sesscache_large_entry));
+    entry = push_array(sesscache_sess_list);
   }
 
   /* Be defensive, and catch the case where entry might still be null here. */
@@ -882,21 +1008,22 @@ static int shmcache_add_large_sess(tls_sess_cache_t *cache,
   entry->expires = expires;
   entry->sess_id_len = sess_id_len;
   entry->sess_id = palloc(cache->cache_pool, sess_id_len);
-  memcpy(entry->sess_id, sess_id, sess_id_len);
+  memcpy((char *) entry->sess_id, sess_id, sess_id_len);
   entry->sess_datalen = sess_len;
   entry->sess_data = palloc(cache->cache_pool, sess_len);
-  i2d_SSL_SESSION(sess, &(entry->sess_data));
+  i2d_SSL_SESSION(sess, (unsigned char **) &(entry->sess_data));
 
   return 0;
 }
 
-static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
+static int sess_cache_add(tls_sess_cache_t *cache, const unsigned char *sess_id,
     unsigned int sess_id_len, time_t expires, SSL_SESSION *sess) {
   register unsigned int i;
   unsigned int h, idx, last;
   int found_slot = FALSE, need_lock = TRUE, res = 0, sess_len;
 
-  pr_trace_msg(trace_channel, 9, "adding session to shmcache cache %p", cache);
+  pr_trace_msg(trace_channel, 9, "adding session to shmcache session cache %p",
+    cache);
 
   /* First we need to find out how much space is needed for the serialized
    * session data.  There is no known maximum size for SSL session data;
@@ -917,17 +1044,17 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
      * so that we can cache these large records in the shm segment.
      */
 
-    return shmcache_add_large_sess(cache, sess_id, sess_id_len, expires,
+    return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
       sess, sess_len);
   }
 
-  if (shmcache_data->sd_listlen == shmcache_data->sd_listsz) {
+  if (sesscache_data->sd_listlen == sesscache_data->sd_listsz) {
     /* It appears that the cache is full.  Try flushing any expired
      * sessions.
      */
 
-    if (shmcache_lock_shm(F_WRLCK) == 0) {
-      if (shmcache_flush() > 0) {
+    if (shmcache_lock_shm(sesscache_fh, F_WRLCK) == 0) {
+      if (sess_cache_flush() > 0) {
         /* If we made room, then do NOT release the lock; we keep the lock
          * so that we can add the session.
          */
@@ -935,11 +1062,11 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
 
       } else {
         /* Release the lock, and use the "large session" list fallback. */
-        if (shmcache_lock_shm(F_UNLCK) < 0) {
+        if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
           tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
         }
 
-        return shmcache_add_large_sess(cache, sess_id, sess_id_len, expires,
+        return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
           sess, sess_len);
       }
 
@@ -948,22 +1075,22 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
         "shmcache: %s", strerror(errno));
 
       /* Add this session to the "large session" list instead as a fallback. */
-      return shmcache_add_large_sess(cache, sess_id, sess_id_len, expires,
+      return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
         sess, sess_len);
     }
   }
 
   /* Hash the key, start looking for an open slot. */
   h = shmcache_hash(sess_id, sess_id_len);
-  idx = h % shmcache_data->sd_listsz;
+  idx = h % sesscache_data->sd_listsz;
 
   if (need_lock) {
-    if (shmcache_lock_shm(F_WRLCK) < 0) {
+    if (shmcache_lock_shm(sesscache_fh, F_WRLCK) < 0) {
       tls_log("shmcache: unable to add session to shm cache: error "
         "write-locking shmcache: %s", strerror(errno));
 
       /* Add this session to the "large session" list instead as a fallback. */
-      return shmcache_add_large_sess(cache, sess_id, sess_id_len, expires,
+      return sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires,
         sess, sess_len);
     }
   }
@@ -972,12 +1099,12 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
   last = idx > 0 ? (idx - 1) : 0;
 
   do {
-    struct shmcache_entry *entry;
+    struct sesscache_entry *entry;
 
     pr_signals_handle();
 
     /* Look for the first open slot (i.e. expires == 0). */
-    entry = &(shmcache_data->sd_entries[i]);
+    entry = &(sesscache_data->sd_entries[i]);
     if (entry->expires == 0) {
       unsigned char *ptr;
 
@@ -989,23 +1116,23 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
       ptr = entry->sess_data;
       i2d_SSL_SESSION(sess, &ptr);
 
-      shmcache_data->sd_listlen++;
-      shmcache_data->nstored++;
+      sesscache_data->sd_listlen++;
+      sesscache_data->nstored++;
 
-      if (shmcache_data->next_expiring > 0) {
-        if (expires < shmcache_data->next_expiring) {
-          shmcache_data->next_expiring = expires;
+      if (sesscache_data->next_expiring > 0) {
+        if (expires < sesscache_data->next_expiring) {
+          sesscache_data->next_expiring = expires;
         }
 
       } else {
-        shmcache_data->next_expiring = expires;
+        sesscache_data->next_expiring = expires;
       }
 
       found_slot = TRUE;
       break;
     }
 
-    if (i < shmcache_data->sd_listsz) {
+    if (i < sesscache_data->sd_listsz) {
       i++;
 
     } else {
@@ -1019,12 +1146,12 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
    * an open slot at this point, add it to the "large session" list.
    */
   if (!found_slot) {
-    res = shmcache_add_large_sess(cache, sess_id, sess_id_len, expires, sess,
+    res = sess_cache_add_large_sess(cache, sess_id, sess_id_len, expires, sess,
       sess_len);
   }
 
   if (need_lock) {
-    if (shmcache_lock_shm(F_UNLCK) < 0) {
+    if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
       tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
     }
   }
@@ -1032,22 +1159,22 @@ static int shmcache_add(tls_sess_cache_t *cache, unsigned char *sess_id,
   return res;
 }
 
-static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
-    unsigned char *sess_id, unsigned int sess_id_len) {
+static SSL_SESSION *sess_cache_get(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len) {
   unsigned int h, idx;
   SSL_SESSION *sess = NULL;
 
-  pr_trace_msg(trace_channel, 9, "getting session from shmcache cache %p",
-    cache); 
+  pr_trace_msg(trace_channel, 9,
+    "getting session from shmcache session cache %p", cache);
 
   /* Look for the requested session in the "large session" list first. */
-  if (shmcache_sess_list != NULL) {
+  if (sesscache_sess_list != NULL) {
     register unsigned int i;
-    struct shmcache_large_entry *entries;
+    struct sesscache_large_entry *entries;
 
-    entries = shmcache_sess_list->elts;
-    for (i = 0; i < shmcache_sess_list->nelts; i++) {
-      struct shmcache_large_entry *entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
 
       entry = &(entries[i]);
       if (entry->expires > 0 &&
@@ -1062,8 +1189,8 @@ static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
           ptr = entry->sess_data;
           sess = d2i_SSL_SESSION(NULL, &ptr, entry->sess_datalen);
           if (sess == NULL) {
-            tls_log("shmcache: error retrieving session from cache: %s",
-              shmcache_get_crypto_errors());
+            tls_log("shmcache: error retrieving session from session cache: %s",
+              shmcache_get_errors());
 
           } else {
             break;
@@ -1078,9 +1205,9 @@ static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
   }
 
   h = shmcache_hash(sess_id, sess_id_len);
-  idx = h % shmcache_data->sd_listsz;
+  idx = h % sesscache_data->sd_listsz;
 
-  if (shmcache_lock_shm(F_WRLCK) == 0) {
+  if (shmcache_lock_shm(sesscache_fh, F_WRLCK) == 0) {
     register unsigned int i;
     unsigned int last;
 
@@ -1088,11 +1215,11 @@ static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
     last = idx > 0 ? (idx -1) : 0;
 
     do {
-      struct shmcache_entry *entry;
+      struct sesscache_entry *entry;
 
       pr_signals_handle();
 
-      entry = &(shmcache_data->sd_entries[i]);
+      entry = &(sesscache_data->sd_entries[i]);
       if (entry->expires > 0 &&
           entry->sess_id_len == sess_id_len &&
           memcmp(entry->sess_id, sess_id, entry->sess_id_len) == 0) {
@@ -1107,19 +1234,19 @@ static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
           ptr = entry->sess_data;
           sess = d2i_SSL_SESSION(NULL, &ptr, entry->sess_datalen);
           if (sess != NULL) {
-            shmcache_data->nhits++;
+            sesscache_data->nhits++;
 
           } else {
-            tls_log("shmcache: error retrieving session from cache: %s",
-              shmcache_get_crypto_errors());
-            shmcache_data->nerrors++;
+            tls_log("shmcache: error retrieving session from session cache: %s",
+              shmcache_get_errors());
+            sesscache_data->nerrors++;
           }
         }
 
         break;
       }
 
-      if (i < shmcache_data->sd_listsz) {
+      if (i < sesscache_data->sd_listsz) {
         i++;
 
       } else {
@@ -1129,16 +1256,16 @@ static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
     } while (i != last);
 
     if (sess == NULL) {
-      shmcache_data->nmisses++;
+      sesscache_data->nmisses++;
       errno = ENOENT;
     }
 
-    if (shmcache_lock_shm(F_UNLCK) < 0) {
+    if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
       tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
     }
 
   } else {
-    tls_log("shmcache: unable to retrieve session from cache: error "
+    tls_log("shmcache: unable to retrieve session from session cache: error "
       "write-locking shmcache: %s", strerror(errno));
 
     errno = EPERM;
@@ -1147,28 +1274,28 @@ static SSL_SESSION *shmcache_get(tls_sess_cache_t *cache,
   return sess;
 }
 
-static int shmcache_delete(tls_sess_cache_t *cache,
-    unsigned char *sess_id, unsigned int sess_id_len) {
+static int sess_cache_delete(tls_sess_cache_t *cache,
+    const unsigned char *sess_id, unsigned int sess_id_len) {
   unsigned int h, idx;
   int res;
 
-  pr_trace_msg(trace_channel, 9, "removing session from shmcache cache %p",
-    cache);
+  pr_trace_msg(trace_channel, 9,
+    "removing session from shmcache session cache %p", cache);
 
   /* Look for the requested session in the "large session" list first. */
-  if (shmcache_sess_list != NULL) {
+  if (sesscache_sess_list != NULL) {
     register unsigned int i;
-    struct shmcache_large_entry *entries;
+    struct sesscache_large_entry *entries;
 
-    entries = shmcache_sess_list->elts;
-    for (i = 0; i < shmcache_sess_list->nelts; i++) {
-      struct shmcache_large_entry *entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
 
       entry = &(entries[i]);
       if (entry->sess_id_len == sess_id_len &&
           memcmp(entry->sess_id, sess_id, entry->sess_id_len) == 0) {
 
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
         entry->expires = 0;
         return 0;
       }
@@ -1176,9 +1303,9 @@ static int shmcache_delete(tls_sess_cache_t *cache,
   }
 
   h = shmcache_hash(sess_id, sess_id_len);
-  idx = h % shmcache_data->sd_listsz;
+  idx = h % sesscache_data->sd_listsz;
 
-  if (shmcache_lock_shm(F_WRLCK) == 0) {
+  if (shmcache_lock_shm(sesscache_fh, F_WRLCK) == 0) {
     register unsigned int i;
     unsigned int last;
 
@@ -1186,35 +1313,35 @@ static int shmcache_delete(tls_sess_cache_t *cache,
     last = idx > 0 ? (idx - 1) : 0;
 
     do {
-      struct shmcache_entry *entry;
+      struct sesscache_entry *entry;
 
       pr_signals_handle();
 
-      entry = &(shmcache_data->sd_entries[i]);
+      entry = &(sesscache_data->sd_entries[i]);
       if (entry->sess_id_len == sess_id_len &&
           memcmp(entry->sess_id, sess_id, entry->sess_id_len) == 0) {
         time_t now;
 
-        pr_memscrub(entry->sess_data, entry->sess_datalen);
+        pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
 
-        if (shmcache_data->sd_listlen > 0) {
-          shmcache_data->sd_listlen--;
+        if (sesscache_data->sd_listlen > 0) {
+          sesscache_data->sd_listlen--;
         }
 
         /* Don't forget to update the stats. */
         now = time(NULL);
         if (entry->expires > now) {
-          shmcache_data->ndeleted++;
+          sesscache_data->ndeleted++;
 
         } else {
-          shmcache_data->nexpired++;
+          sesscache_data->nexpired++;
         }
 
         entry->expires = 0;
         break;
       }
 
-      if (i < shmcache_data->sd_listsz) {
+      if (i < sesscache_data->sd_listsz) {
         i++;
 
       } else {
@@ -1223,14 +1350,14 @@ static int shmcache_delete(tls_sess_cache_t *cache,
 
     } while (i != last);
 
-    if (shmcache_lock_shm(F_UNLCK) < 0) {
+    if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
       tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
     }
 
     res = 0;
 
   } else {
-    tls_log("shmcache: unable to delete session from cache: error "
+    tls_log("shmcache: unable to delete session from session cache: error "
       "write-locking shmcache: %s", strerror(errno));
 
     errno = EPERM;
@@ -1240,91 +1367,93 @@ static int shmcache_delete(tls_sess_cache_t *cache,
   return res;
 }
 
-static int shmcache_clear(tls_sess_cache_t *cache) {
+static int sess_cache_clear(tls_sess_cache_t *cache) {
   register unsigned int i;
   int res;
 
-  pr_trace_msg(trace_channel, 9, "clearing shmcache cache %p", cache); 
+  pr_trace_msg(trace_channel, 9, "clearing shmcache session cache %p", cache);
 
-  if (shmcache_shmid < 0) {
+  if (sesscache_shmid < 0) {
     errno = EINVAL;
     return -1;
   }
 
-  if (shmcache_sess_list != NULL) {
-    struct shmcache_large_entry *entries;
+  if (sesscache_sess_list != NULL) {
+    struct sesscache_large_entry *entries;
     
-    entries = shmcache_sess_list->elts;
-    for (i = 0; i < shmcache_sess_list->nelts; i++) {
-      struct shmcache_large_entry *entry;
+    entries = sesscache_sess_list->elts;
+    for (i = 0; i < sesscache_sess_list->nelts; i++) {
+      struct sesscache_large_entry *entry;
 
       entry = &(entries[i]);
       entry->expires = 0;
-      pr_memscrub(entry->sess_data, entry->sess_datalen);
+      pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
     }
   }
 
-  if (shmcache_lock_shm(F_WRLCK) < 0) {
+  if (shmcache_lock_shm(sesscache_fh, F_WRLCK) < 0) {
     tls_log("shmcache: unable to clear cache: error write-locking shmcache: %s",
       strerror(errno));
     return -1;
   }
 
-  for (i = 0; i < shmcache_data->sd_listsz; i++) {
-    struct shmcache_entry *entry;
+  for (i = 0; i < sesscache_data->sd_listsz; i++) {
+    struct sesscache_entry *entry;
 
-    entry = &(shmcache_data->sd_entries[i]);
+    entry = &(sesscache_data->sd_entries[i]);
 
     entry->expires = 0;
-    pr_memscrub(entry->sess_data, entry->sess_datalen);
+    pr_memscrub((void *) entry->sess_data, entry->sess_datalen);
   }
 
-  res = shmcache_data->sd_listlen; 
-  shmcache_data->sd_listlen = 0;
+  res = sesscache_data->sd_listlen; 
+  sesscache_data->sd_listlen = 0;
 
-  if (shmcache_lock_shm(F_UNLCK) < 0) {
+  if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
     tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
   }
 
   return res;
 }
 
-static int shmcache_remove(tls_sess_cache_t *cache) {
+static int sess_cache_remove(tls_sess_cache_t *cache) {
   int res;
   struct shmid_ds ds;
   const char *cache_file;
 
-  if (shmcache_fh == NULL) {
+  if (sesscache_fh == NULL) {
     return 0;
   }
 
   if (cache != NULL) {
-    pr_trace_msg(trace_channel, 9, "removing shmcache cache %p", cache); 
+    pr_trace_msg(trace_channel, 9, "removing shmcache session cache %p",
+      cache);
   }
 
-  cache_file = shmcache_fh->fh_path;
-  (void) shmcache_close(cache);
+  cache_file = sesscache_fh->fh_path;
+  (void) sess_cache_close(cache);
 
-  if (shmcache_shmid < 0) {
+  if (sesscache_shmid < 0) {
     errno = EINVAL;
     return -1;
   }
 
   pr_log_debug(DEBUG9, MOD_TLS_SHMCACHE_VERSION
-    ": attempting to remove shm ID %d", shmcache_shmid);
+    ": attempting to remove session cache shm ID %d", sesscache_shmid);
 
   PRIVS_ROOT
-  res = shmctl(shmcache_shmid, IPC_RMID, &ds);
+  res = shmctl(sesscache_shmid, IPC_RMID, &ds);
   PRIVS_RELINQUISH
 
   if (res < 0) {
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
-      ": error removing shm ID %d: %s", shmcache_shmid, strerror(errno));
+      ": error removing session cache shm ID %d: %s", sesscache_shmid,
+      strerror(errno));
 
   } else {
     pr_log_debug(DEBUG9, MOD_TLS_SHMCACHE_VERSION
-      ": removed shm ID %d", shmcache_shmid);
-    shmcache_shmid = -1;
+      ": removed session cache shm ID %d", sesscache_shmid);
+    sesscache_shmid = -1;
   }
 
   /* Don't forget to remove the on-disk file as well. */
@@ -1333,15 +1462,15 @@ static int shmcache_remove(tls_sess_cache_t *cache) {
   return res;
 }
 
-static int shmcache_status(tls_sess_cache_t *cache,
+static int sess_cache_status(tls_sess_cache_t *cache,
     void (*statusf)(void *, const char *, ...), void *arg, int flags) {
   int res, xerrno = 0;
   struct shmid_ds ds;
   pool *tmp_pool;
 
-  pr_trace_msg(trace_channel, 9, "checking shmcache cache %p", cache); 
+  pr_trace_msg(trace_channel, 9, "checking shmcache session cache %p", cache);
 
-  if (shmcache_lock_shm(F_RDLCK) < 0) {
+  if (shmcache_lock_shm(sesscache_fh, F_RDLCK) < 0) {
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
       ": error read-locking shmcache: %s", strerror(errno));
     return -1;
@@ -1352,10 +1481,10 @@ static int shmcache_status(tls_sess_cache_t *cache,
   statusf(arg, "%s", "Shared memory (shm) SSL session cache provided by "
     MOD_TLS_SHMCACHE_VERSION);
   statusf(arg, "%s", "");
-  statusf(arg, "Shared memory segment ID: %d", shmcache_shmid);
+  statusf(arg, "Shared memory segment ID: %d", sesscache_shmid);
 
   PRIVS_ROOT
-  res = shmctl(shmcache_shmid, IPC_STAT, &ds);
+  res = shmctl(sesscache_shmid, IPC_STAT, &ds);
   xerrno = errno;
   PRIVS_RELINQUISH
 
@@ -1369,27 +1498,27 @@ static int shmcache_status(tls_sess_cache_t *cache,
 
   } else {
     statusf(arg, "Unable to stat shared memory segment ID %d: %s",
-      shmcache_shmid, strerror(xerrno));
+      sesscache_shmid, strerror(xerrno));
   } 
 
   statusf(arg, "%s", "");
-  statusf(arg, "Max session cache size: %u", shmcache_data->sd_listsz);
-  statusf(arg, "Current session cache size: %u", shmcache_data->sd_listlen);
+  statusf(arg, "Max session cache size: %u", sesscache_data->sd_listsz);
+  statusf(arg, "Current session cache size: %u", sesscache_data->sd_listlen);
   statusf(arg, "%s", "");
-  statusf(arg, "Cache lifetime hits: %u", shmcache_data->nhits);
-  statusf(arg, "Cache lifetime misses: %u", shmcache_data->nmisses);
+  statusf(arg, "Cache lifetime hits: %u", sesscache_data->nhits);
+  statusf(arg, "Cache lifetime misses: %u", sesscache_data->nmisses);
   statusf(arg, "%s", "");
-  statusf(arg, "Cache lifetime sessions stored: %u", shmcache_data->nstored);
-  statusf(arg, "Cache lifetime sessions deleted: %u", shmcache_data->ndeleted);
-  statusf(arg, "Cache lifetime sessions expired: %u", shmcache_data->nexpired);
+  statusf(arg, "Cache lifetime sessions stored: %u", sesscache_data->nstored);
+  statusf(arg, "Cache lifetime sessions deleted: %u", sesscache_data->ndeleted);
+  statusf(arg, "Cache lifetime sessions expired: %u", sesscache_data->nexpired);
   statusf(arg, "%s", "");
   statusf(arg, "Cache lifetime errors handling sessions in cache: %u",
-    shmcache_data->nerrors);
+    sesscache_data->nerrors);
   statusf(arg, "Cache lifetime sessions exceeding max entry size: %u",
-    shmcache_data->nexceeded);
-  if (shmcache_data->nexceeded > 0) {
+    sesscache_data->nexceeded);
+  if (sesscache_data->nexceeded > 0) {
     statusf(arg, "  Largest session exceeding max entry size: %u",
-      shmcache_data->exceeded_maxsz);
+      sesscache_data->exceeded_maxsz);
   }
 
   if (flags & TLS_SESS_CACHE_STATUS_FL_SHOW_SESSIONS) {
@@ -1398,7 +1527,7 @@ static int shmcache_status(tls_sess_cache_t *cache,
     statusf(arg, "%s", "");
     statusf(arg, "%s", "Cached sessions:");
 
-    if (shmcache_data->sd_listlen == 0) {
+    if (sesscache_data->sd_listlen == 0) {
       statusf(arg, "%s", "  (none)");
     }
 
@@ -1412,12 +1541,12 @@ static int shmcache_status(tls_sess_cache_t *cache,
      * of rolling our own printing function.
      */
 
-    for (i = 0; i < shmcache_data->sd_listsz; i++) {
-      struct shmcache_entry *entry;
+    for (i = 0; i < sesscache_data->sd_listsz; i++) {
+      struct sesscache_entry *entry;
 
       pr_signals_handle();
 
-      entry = &(shmcache_data->sd_entries[i]);
+      entry = &(sesscache_data->sd_entries[i]);
       if (entry->expires > 0) {
         SSL_SESSION *sess;
         TLS_D2I_SSL_SESSION_CONST unsigned char *ptr;
@@ -1428,8 +1557,8 @@ static int shmcache_status(tls_sess_cache_t *cache,
         sess = d2i_SSL_SESSION(NULL, &ptr, entry->sess_datalen); 
         if (sess == NULL) {
           pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
-            ": error retrieving session from cache: %s",
-            shmcache_get_crypto_errors());
+            ": error retrieving session from session cache: %s",
+            shmcache_get_errors());
           continue;
         }
 
@@ -1438,32 +1567,27 @@ static int shmcache_status(tls_sess_cache_t *cache,
 #if OPENSSL_VERSION_NUMBER < 0x10100000L
         /* XXX Directly accessing these fields cannot be a Good Thing. */
         if (sess->session_id_length > 0) {
-          register unsigned int j;
           char *sess_id_str;
 
-          sess_id_str = pcalloc(tmp_pool, (sess->session_id_length * 2) + 1);
-          for (j = 0; j < sess->session_id_length; j++) {
-            sprintf((char *) &(sess_id_str[j*2]), "%02X", sess->session_id[j]);
-          }
+          sess_id_str = pr_str_bin2hex(tmp_pool, sess->session_id,
+            sess->session_id_length, PR_STR_FL_HEX_USE_UC);
 
           statusf(arg, "    Session ID: %s", sess_id_str);
         }
 
         if (sess->sid_ctx_length > 0) {
-          register unsigned int j;
           char *sid_ctx_str;
 
-          sid_ctx_str = pcalloc(tmp_pool, (sess->sid_ctx_length * 2) + 1);
-          for (j = 0; j < sess->sid_ctx_length; j++) {
-            sprintf((char *) &(sid_ctx_str[j*2]), "%02X", sess->sid_ctx[j]);
-          }
+          sid_ctx_str = pr_str_bin2hex(tmp_pool, sess->sid_ctx,
+            sess->sid_ctx_length, PR_STR_FL_HEX_USE_UC);
 
           statusf(arg, "    Session ID Context: %s", sid_ctx_str);
         }
 
         ssl_version = sess->ssl_version;
 #else
-# if OPENSSL_VERSION_NUMBER >= 0x10100006L
+# if OPENSSL_VERSION_NUMBER >= 0x10100006L && \
+     !defined(HAVE_LIBRESSL)
         ssl_version = SSL_SESSION_get_protocol_version(sess);
 # else
         ssl_version = 0;
@@ -1479,6 +1603,16 @@ static int shmcache_status(tls_sess_cache_t *cache,
             statusf(arg, "    Protocol: %s", "TLSv1");
             break;
 
+#if OPENSSL_VERSION_NUMBER >= 0x10001000L
+          case TLS1_1_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1.1");
+            break;
+
+          case TLS1_2_VERSION:
+            statusf(arg, "    Protocol: %s", "TLSv1.2");
+            break;
+#endif
+
           default:
             statusf(arg, "    Protocol: %s", "unknown");
         }
@@ -1496,7 +1630,7 @@ static int shmcache_status(tls_sess_cache_t *cache,
     }
   }
 
-  if (shmcache_lock_shm(F_UNLCK) < 0) {
+  if (shmcache_lock_shm(sesscache_fh, F_UNLCK) < 0) {
     pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
       ": error unlocking shmcache: %s", strerror(errno));
   }
@@ -1505,87 +1639,1025 @@ static int shmcache_status(tls_sess_cache_t *cache,
   return 0;
 }
 
-/* Event Handlers
+#if defined(PR_USE_OPENSSL_OCSP)
+
+/* OCSP response cache implementation callbacks.
  */
 
-/* Daemon PID */
-extern pid_t mpid;
+/* Scan the entire list, and clear out the oldest response.  Logs the number
+ * of responses cleared and updates the header stat.
+ *
+ * NOTE: Callers are assumed to handle the locking of the shm before/after
+ * calling this function!
+ */
+static unsigned int ocsp_cache_flush(void) {
+  register unsigned int i;
+  unsigned int flushed = 0;
+  time_t now;
 
-static void shmcache_shutdown_ev(const void *event_data, void *user_data) {
-  if (mpid == getpid() &&
-      ServerType == SERVER_STANDALONE) {
+  now = time(NULL);
 
-    /* Remove external session caches on shutdown; the security policy/config
-     * may have changed, e.g. becoming more strict, and allow clients to
-     * resumed cached sessions from a more relaxed security config is not a 
-     * Good Thing at all.
-     */
-    shmcache_remove(NULL);
+  /* We always scan the in-memory large response entry list. */
+  if (ocspcache_resp_list != NULL) {
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+
+      if (entry->age > (now - 3600)) {
+        /* This entry has expired; clear its slot. */
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+      }
+    }
   }
-}
 
-#if defined(PR_SHARED_MODULE)
-static void shmcache_mod_unload_ev(const void *event_data, void *user_data) {
-  if (strcmp("mod_tls_shmcache.c", (const char *) event_data) == 0) {
-    pr_event_unregister(&tls_shmcache_module, NULL, NULL);
-    tls_sess_cache_unregister("shm");
+  tls_log("shmcache: flushing ocsp cache of oldest responses");
 
-    /* This clears our cache by detaching and destroying the shared memory
-     * segment.
-     */
-    shmcache_remove(NULL);
+  for (i = 0; i < ocspcache_data->od_listsz; i++) {
+    struct ocspcache_entry *entry;
+
+    entry = &(ocspcache_data->od_entries[i]);
+    if (entry->age > (now - 3600)) {
+      /* This entry has expired; clear its slot. */
+      pr_memscrub(entry->resp_der, entry->resp_derlen);
+      entry->resp_derlen = 0;
+      pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+      entry->fingerprint_len = 0;
+      entry->age = 0;
+
+      /* Don't forget to update the stats. */
+      ocspcache_data->nexpired++;
+
+      if (ocspcache_data->od_listlen > 0) {
+        ocspcache_data->od_listlen--;
+      }
+
+      flushed++;
+    }
   }
-}
-#endif /* !PR_SHARED_MODULE */
 
-static void shmcache_restart_ev(const void *event_data, void *user_data) {
-  /* Clear external session caches on shutdown; the security policy/config
-   * may have changed, e.g. becoming more strict, and allow clients to
-   * resumed cached sessions from a more relaxed security config is not a 
-   * Good Thing at all.
-   */
-  shmcache_clear(NULL);
+  tls_log("shmcache: flushed %u old %s from ocsp cache", flushed,
+    flushed != 1 ? "responses" : "response");
+  return flushed;
 }
 
-/* Initialization functions
- */
-
-static int tls_shmcache_init(void) {
-#if defined(PR_SHARED_MODULE)
-  pr_event_register(&tls_shmcache_module, "core.module-unload",
-    shmcache_mod_unload_ev, NULL);
-#endif /* !PR_SHARED_MODULE */
-  pr_event_register(&tls_shmcache_module, "core.restart", shmcache_restart_ev,
-    NULL);
-  pr_event_register(&tls_shmcache_module, "core.shutdown", shmcache_shutdown_ev,
-    NULL);
+static int ocsp_cache_open(tls_ocsp_cache_t *cache, char *info) {
+  int fd, xerrno;
+  char *ptr;
+  size_t requested_size;
+  struct stat st;
 
-  /* Prepare our cache handler. */
-  memset(&shmcache, 0, sizeof(shmcache));
-  shmcache.open = shmcache_open;
-  shmcache.close = shmcache_close;
-  shmcache.add = shmcache_add;
-  shmcache.get = shmcache_get;
-  shmcache.delete = shmcache_delete;
-  shmcache.clear = shmcache_clear;
-  shmcache.remove = shmcache_remove;
-  shmcache.status = shmcache_status;
+  pr_trace_msg(trace_channel, 9, "opening shmcache ocsp cache %p", cache);
 
-#ifdef SSL_SESS_CACHE_NO_INTERNAL_LOOKUP
-  /* Take a chance, and inform OpenSSL that it does not need to use its own
-   * internal session cache lookups; using the external session cache (i.e. us)
-   * will be enough.
+  /* The info string must be formatted like:
+   *
+   *  /file=%s[&size=%u]
+   *
+   * where the optional size is in bytes.  There is a minimum size; if the
+   * configured size is less than the minimum, it's an error.  The default
+   * size (when no size is explicitly configured) is, of course, larger than
+   * the minimum size.
    */
-  shmcache.cache_mode = SSL_SESS_CACHE_NO_INTERNAL_LOOKUP;
-#endif
 
-  /* Register ourselves with mod_tls. */
-  if (tls_sess_cache_register("shm", &shmcache) < 0) {
+  if (strncmp(info, "/file=", 6) != 0) {
     pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
-      ": notice: error registering 'shm' SSL session cache: %s",
+      ": badly formatted info '%s', unable to open shmcache", info);
+    errno = EINVAL;
+    return -1;
+  }
+
+  info += 6;
+
+  /* Check for the optional size parameter. */
+  ptr = strchr(info, '&');
+  if (ptr != NULL) {
+    if (strncmp(ptr + 1, "size=", 5) == 0) {
+      char *tmp = NULL;
+      long size;
+
+      size = strtol(ptr + 6, &tmp, 10);
+      if (tmp && *tmp) {
+        pr_trace_msg(trace_channel, 1,
+          "badly formatted size parameter '%s', ignoring", ptr + 1);
+
+        /* Default size of 1.5M. */
+        requested_size = 1538 * 1024;
+
+      } else {
+        size_t min_size;
+
+        /* The bare minimum size MUST be able to hold at least one response. */
+        min_size = sizeof(struct ocspcache_data) +
+          sizeof(struct ocspcache_entry);
+
+        if ((size_t) size < min_size) {
+          pr_trace_msg(trace_channel, 1,
+            "requested size (%lu bytes) smaller than minimum size "
+            "(%lu bytes), ignoring", (unsigned long) size,
+            (unsigned long) min_size);
+
+          /* Default size of 1.5M.  */
+          requested_size = 1538 * 1024;
+
+        } else {
+          requested_size = size;
+        }
+      }
+
+    } else {
+      pr_trace_msg(trace_channel, 1,
+        "badly formatted size parameter '%s', ignoring", ptr + 1);
+
+      /* Default size of 1.5M.  */
+      requested_size = 1538 * 1024;
+    }
+
+    *ptr = '\0';
+
+  } else {
+    /* Default size of 1.5M.  */
+    requested_size = 1538 * 1024;
+  }
+
+  if (pr_fs_valid_path(info) < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
+      ": file '%s' not an absolute path", info);
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* If ocspcache_fh is not null, then we are a restarted server.  And if
+   * the 'info' path does not match that previous fh, then the admin
+   * has changed the configuration.
+   *
+   * For now, we complain about this, and tell the admin to manually remove
+   * the old file/shm.
+   */
+
+  if (ocspcache_fh != NULL &&
+      strcmp(ocspcache_fh->fh_path, info) != 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
+      ": file '%s' does not match previously configured file '%s'",
+      info, ocspcache_fh->fh_path);
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
+      ": remove existing shmcache using 'ftpdctl tls ocspcache remove' "
+      "before using new file");
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  PRIVS_ROOT
+  ocspcache_fh = pr_fsio_open(info, O_RDWR|O_CREAT);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (ocspcache_fh == NULL) {
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": error: unable to open file '%s': %s", info, strerror(xerrno));
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (pr_fsio_fstat(ocspcache_fh, &st) < 0) {
+    xerrno = errno;
+
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": error: unable to stat file '%s': %s", info, strerror(xerrno));
+
+    pr_fsio_close(ocspcache_fh);
+    ocspcache_fh = NULL;
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (S_ISDIR(st.st_mode)) {
+    xerrno = EISDIR;
+
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": error: unable to use file '%s': %s", info, strerror(xerrno));
+
+    pr_fsio_close(ocspcache_fh);
+    ocspcache_fh = NULL;
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Make sure that we don't inadvertently get one of the Big Three file
+   * descriptors (stdin/stdout/stderr), as can happen especially if the
+   * server has restarted.
+   */
+  fd = PR_FH_FD(ocspcache_fh);
+  if (fd <= STDERR_FILENO) {
+    int res;
+
+    res = pr_fs_get_usable_fd(fd);
+    if (res < 0) {
+      pr_log_debug(DEBUG0,
+        "warning: unable to find good fd for shmcache fd %d: %s", fd,
+        strerror(errno));
+
+    } else {
+      close(fd);
+      PR_FH_FD(ocspcache_fh) = res;
+    }
+  }
+
+  pr_trace_msg(trace_channel, 9,
+    "requested OCSP response cache file: %s (fd %d)", ocspcache_fh->fh_path,
+    PR_FH_FD(ocspcache_fh));
+  pr_trace_msg(trace_channel, 9,
+    "requested OCSP cache size: %lu bytes", (unsigned long) requested_size);
+  ocspcache_data = ocsp_cache_get_shm(ocspcache_fh, requested_size);
+  if (ocspcache_data == NULL) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1,
+      "unable to allocate OCSP response shm: %s", strerror(xerrno));
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": unable to allocate OCSP response shm: %s", strerror(xerrno));
+
+    pr_fsio_close(ocspcache_fh);
+    ocspcache_fh = NULL;
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  cache->cache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(cache->cache_pool, MOD_TLS_SHMCACHE_VERSION);
+
+  return 0;
+}
+
+static int ocsp_cache_close(tls_ocsp_cache_t *cache) {
+  if (cache != NULL) {
+    pr_trace_msg(trace_channel, 9, "closing shmcache ocsp cache %p", cache);
+  }
+
+  if (cache != NULL &&
+      cache->cache_pool != NULL) {
+    if (ocspcache_resp_list != NULL) {
+      register unsigned int i;
+      struct ocspcache_large_entry *entries;
+
+      entries = ocspcache_resp_list->elts;
+      for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+        struct ocspcache_large_entry *entry;
+
+        entry = &(entries[i]);
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+      }
+
+      ocspcache_resp_list = NULL;
+    }
+
+    destroy_pool(cache->cache_pool);
+  }
+
+  if (ocspcache_shmid >= 0) {
+    int res, xerrno = 0;
+
+    PRIVS_ROOT
+#if !defined(_POSIX_SOURCE)
+    res = shmdt((char *) ocspcache_data);
+#else
+    res = shmdt((const char *) ocspcache_data);
+#endif
+    xerrno = errno;
+    PRIVS_RELINQUISH
+
+    if (res < 0) {
+      pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+        ": error detaching ocsp shm ID %d: %s", ocspcache_shmid,
+        strerror(xerrno));
+    }
+
+    ocspcache_data = NULL;
+  }
+
+  pr_fsio_close(ocspcache_fh);
+  ocspcache_fh = NULL;
+  return 0;
+}
+
+static int ocsp_cache_add_large_resp(tls_ocsp_cache_t *cache,
+    const char *fingerprint, OCSP_RESPONSE *resp, time_t resp_age) {
+  struct ocspcache_large_entry *entry = NULL;
+  int resp_derlen = 0;
+  unsigned char *ptr;
+
+  resp_derlen = i2d_OCSP_RESPONSE(resp, NULL);
+
+  if (resp_derlen > TLS_MAX_OCSP_RESPONSE_SIZE) {
+    /* We may get responses to add to the list which do not exceed the max
+     * size, but instead are here because we couldn't get the lock on the
+     * shmcache.  Don't track these in the 'exceeded' stats'.
+     */
+
+    if (shmcache_lock_shm(ocspcache_fh, F_WRLCK) == 0) {
+      ocspcache_data->nexceeded++;
+      if ((size_t) resp_derlen > ocspcache_data->exceeded_maxsz) {
+        ocspcache_data->exceeded_maxsz = resp_derlen;
+      }
+
+      if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+        tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
+      }
+
+    } else {
+      tls_log("shmcache: error write-locking shmcache: %s", strerror(errno));
+    }
+  }
+
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+    time_t now;
+
+    /* Look for any expired responses in the list to overwrite/reuse. */
+    entries = ocspcache_resp_list->elts;
+    now = time(NULL);
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      entry = &(entries[i]);
+
+      if (entry->age > (now - 3600)) {
+        /* This entry has expired; clear and reuse its slot. */
+        entry->age = 0;
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+
+        break;
+      }
+    }
+
+  } else {
+    ocspcache_resp_list = make_array(cache->cache_pool, 1,
+      sizeof(struct ocspcache_large_entry));
+    entry = push_array(ocspcache_resp_list);
+  }
+
+  /* Be defensive, and catch the case where entry might still be null here. */
+  if (entry == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  entry->age = resp_age;
+  entry->fingerprint_len = strlen(fingerprint);
+  entry->fingerprint = palloc(cache->cache_pool, entry->fingerprint_len);
+  memcpy(entry->fingerprint, fingerprint, entry->fingerprint_len);
+  entry->resp_derlen = resp_derlen;
+  entry->resp_der = palloc(cache->cache_pool, resp_derlen);
+
+  ptr = entry->resp_der;
+  i2d_OCSP_RESPONSE(resp, &ptr);
+
+  return 0;
+}
+
+static int ocsp_cache_add(tls_ocsp_cache_t *cache, const char *fingerprint,
+    OCSP_RESPONSE *resp, time_t resp_age) {
+  register unsigned int i;
+  unsigned int h, idx, last;
+  int found_slot = FALSE, need_lock = TRUE, res = 0, resp_derlen;
+  size_t fingerprint_len;
+
+  pr_trace_msg(trace_channel, 9, "adding response to shmcache ocsp cache %p",
+    cache);
+
+  /* First we need to find out how much space is needed for the serialized
+   * response data.  There is no known maximum size for OCSP response data;
+   * this module is currently designed to allow only up to a certain size.
+   */
+
+  resp_derlen = i2d_OCSP_RESPONSE(resp, NULL);
+  if (resp_derlen <= 0) {
+    pr_trace_msg(trace_channel, 1,
+      "error DER-encoding OCSP response: %s", shmcache_get_errors());
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (resp_derlen > TLS_MAX_OCSP_RESPONSE_SIZE) {
+    tls_log("shmcache: length of serialized OCSP response data (%d) exceeds "
+      "maximum size (%u), unable to add to shared shmcache", resp_derlen,
+      TLS_MAX_OCSP_RESPONSE_SIZE);
+
+    /* Instead of rejecting the add here, we add the response to a "large
+     * response" list.  Thus the large response would still be cached per
+     * process and will not be lost.
+     *
+     * XXX We should also track how often this happens, and possibly trigger
+     * a shmcache resize (using a larger record size, vs larger cache size)
+     * so that we can cache these large records in the shm segment.
+     */
+
+    return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+  }
+
+  if (ocspcache_data->od_listlen == ocspcache_data->od_listsz) {
+    /* It appears that the cache is full.  Flush the oldest response. */
+    if (shmcache_lock_shm(ocspcache_fh, F_WRLCK) == 0) {
+      if (ocsp_cache_flush() > 0) {
+        /* If we made room, then do NOT release the lock; we keep the lock
+         * so that we can add the response.
+         */
+        need_lock = FALSE;
+
+      } else {
+        /* Release the lock, and use the "large response" list fallback. */
+        if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+          tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
+        }
+
+        return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+      }
+
+    } else {
+      tls_log("shmcache: unable to flush ocsp shmcache: error write-locking "
+        "shmcache: %s", strerror(errno));
+
+      /* Add this response to the "large response" list instead as a
+       * fallback.
+       */
+      return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+    }
+  }
+
+  /* Hash the key, start looking for an open slot. */
+  fingerprint_len = strlen(fingerprint);
+  h = shmcache_hash((unsigned char *) fingerprint, fingerprint_len);
+  idx = h % ocspcache_data->od_listsz;
+
+  if (need_lock) {
+    if (shmcache_lock_shm(ocspcache_fh, F_WRLCK) < 0) {
+      tls_log("shmcache: unable to add response to ocsp shmcache: error "
+        "write-locking shmcache: %s", strerror(errno));
+
+      /* Add this response to the "large response" list instead as a
+       * fallback.
+       */
+      return ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+    }
+  }
+
+  i = idx;
+  last = idx > 0 ? (idx - 1) : 0;
+
+  do {
+    struct ocspcache_entry *entry;
+
+    pr_signals_handle();
+
+    /* Look for the first open slot (i.e. fingerprint_len == 0). */
+    entry = &(ocspcache_data->od_entries[i]);
+    if (entry->fingerprint_len == 0) {
+      unsigned char *ptr;
+
+      entry->age = resp_age;
+      entry->fingerprint_len = fingerprint_len;
+      memcpy(entry->fingerprint, fingerprint, fingerprint_len);
+      entry->resp_derlen = resp_derlen;
+
+      ptr = entry->resp_der;
+      i2d_OCSP_RESPONSE(resp, &ptr);
+
+      ocspcache_data->od_listlen++;
+      ocspcache_data->nstored++;
+
+      found_slot = TRUE;
+      break;
+    }
+
+    if (i < ocspcache_data->od_listsz) {
+      i++;
+
+    } else {
+      i = 0;
+    }
+  } while (i != last);
+
+  /* There is a race condition possible between the open slots check
+   * above and the scan through the slots.  So if we didn't actually find
+   * an open slot at this point, add it to the "large response" list.
+   */
+  if (!found_slot) {
+    res = ocsp_cache_add_large_resp(cache, fingerprint, resp, resp_age);
+  }
+
+  if (need_lock) {
+    if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+      tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
+    }
+  }
+
+  return res;
+}
+
+static OCSP_RESPONSE *ocsp_cache_get(tls_ocsp_cache_t *cache,
+    const char *fingerprint, time_t *resp_age) {
+  unsigned int h, idx;
+  OCSP_RESPONSE *resp = NULL;
+  size_t fingerprint_len = 0;
+
+  pr_trace_msg(trace_channel, 9,
+    "getting response from shmcache ocsp cache %p", cache);
+
+  fingerprint_len = strlen(fingerprint);
+
+  /* Look for the requested response in the "large response" list first. */
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      if (entry->fingerprint_len > 0 &&
+          entry->fingerprint_len == fingerprint_len &&
+          memcmp(entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+        const unsigned char *ptr;
+
+        ptr = entry->resp_der;
+        resp = d2i_OCSP_RESPONSE(NULL, &ptr, entry->resp_derlen);
+        if (resp == NULL) {
+          tls_log("shmcache: error retrieving response from ocsp cache: %s",
+            shmcache_get_errors());
+
+        } else {
+          *resp_age = entry->age;
+          break;
+        }
+      }
+    }
+  }
+
+  if (resp) {
+    return resp;
+  }
+
+  h = shmcache_hash((unsigned char *) fingerprint, fingerprint_len);
+  idx = h % ocspcache_data->od_listsz;
+
+  if (shmcache_lock_shm(ocspcache_fh, F_WRLCK) == 0) {
+    register unsigned int i;
+    unsigned int last;
+
+    i = idx;
+    last = idx > 0 ? (idx -1) : 0;
+
+    do {
+      struct ocspcache_entry *entry;
+
+      pr_signals_handle();
+
+      entry = &(ocspcache_data->od_entries[i]);
+      if (entry->fingerprint_len > 0 &&
+          entry->fingerprint_len == fingerprint_len &&
+          memcmp(entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+        const unsigned char *ptr;
+
+        /* Don't forget to update the stats. */
+
+        ptr = entry->resp_der;
+        resp = d2i_OCSP_RESPONSE(NULL, &ptr, entry->resp_derlen);
+        if (resp != NULL) {
+          *resp_age = entry->age;
+          ocspcache_data->nhits++;
+
+        } else {
+          tls_log("shmcache: error retrieving response from ocsp cache: %s",
+            shmcache_get_errors());
+          ocspcache_data->nerrors++;
+        }
+
+        break;
+      }
+
+      if (i < ocspcache_data->od_listsz) {
+        i++;
+
+      } else {
+        i = 0;
+      }
+    } while (i != last);
+
+    if (resp == NULL) {
+      ocspcache_data->nmisses++;
+      errno = ENOENT;
+    }
+
+    if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+      tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
+    }
+
+  } else {
+    tls_log("shmcache: unable to retrieve response from ocsp cache: error "
+      "write-locking shmcache: %s", strerror(errno));
+
+    errno = EPERM;
+  }
+
+  return resp;
+}
+
+static int ocsp_cache_delete(tls_ocsp_cache_t *cache, const char *fingerprint) {
+  unsigned int h, idx;
+  int res;
+  size_t fingerprint_len = 0;
+
+  pr_trace_msg(trace_channel, 9,
+    "removing response from shmcache ocsp cache %p", cache);
+
+  fingerprint_len = strlen(fingerprint);
+
+  /* Look for the requested response in the "large response" list first. */
+  if (ocspcache_resp_list != NULL) {
+    register unsigned int i;
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      if (entry->fingerprint_len == fingerprint_len &&
+          memcmp(entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+        entry->age = 0;
+        return 0;
+      }
+    }
+  }
+
+  h = shmcache_hash((unsigned char *) fingerprint, fingerprint_len);
+  idx = h % ocspcache_data->od_listsz;
+
+  if (shmcache_lock_shm(ocspcache_fh, F_WRLCK) == 0) {
+    register unsigned int i;
+    unsigned int last;
+
+    i = idx;
+    last = idx > 0 ? (idx - 1) : 0;
+
+    do {
+      struct ocspcache_entry *entry;
+
+      pr_signals_handle();
+
+      entry = &(ocspcache_data->od_entries[i]);
+      if (entry->fingerprint_len == fingerprint_len &&
+          memcmp(entry->fingerprint, fingerprint, fingerprint_len) == 0) {
+        time_t now;
+
+        pr_memscrub(entry->resp_der, entry->resp_derlen);
+        entry->resp_derlen = 0;
+        pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+        entry->fingerprint_len = 0;
+
+        if (ocspcache_data->od_listlen > 0) {
+          ocspcache_data->od_listlen--;
+        }
+
+        /* Don't forget to update the stats. */
+        now = time(NULL);
+        if (entry->age > (now - 3600)) {
+          ocspcache_data->nexpired++;
+
+        } else {
+          ocspcache_data->ndeleted++;
+        }
+
+        entry->age = 0;
+        break;
+      }
+
+      if (i < ocspcache_data->od_listsz) {
+        i++;
+
+      } else {
+        i = 0;
+      }
+
+    } while (i != last);
+
+    if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+      tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
+    }
+
+    res = 0;
+
+  } else {
+    tls_log("shmcache: unable to delete response from ocsp cache: error "
+      "write-locking shmcache: %s", strerror(errno));
+
+    errno = EPERM;
+    res = -1;
+  }
+
+  return res;
+}
+
+static int ocsp_cache_clear(tls_ocsp_cache_t *cache) {
+  register unsigned int i;
+  int res;
+
+  pr_trace_msg(trace_channel, 9, "clearing shmcache ocsp cache %p", cache);
+
+  if (ocspcache_shmid < 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (ocspcache_resp_list != NULL) {
+    struct ocspcache_large_entry *entries;
+
+    entries = ocspcache_resp_list->elts;
+    for (i = 0; i < ocspcache_resp_list->nelts; i++) {
+      struct ocspcache_large_entry *entry;
+
+      entry = &(entries[i]);
+      entry->age = 0;
+      pr_memscrub(entry->resp_der, entry->resp_derlen);
+      entry->resp_derlen = 0;
+      pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+      entry->fingerprint_len = 0;
+    }
+  }
+
+  if (shmcache_lock_shm(ocspcache_fh, F_WRLCK) < 0) {
+    tls_log("shmcache: unable to clear cache: error write-locking shmcache: %s",
+      strerror(errno));
+    return -1;
+  }
+
+  for (i = 0; i < ocspcache_data->od_listsz; i++) {
+    struct ocspcache_entry *entry;
+
+    entry = &(ocspcache_data->od_entries[i]);
+
+    entry->age = 0;
+    pr_memscrub(entry->resp_der, entry->resp_derlen);
+    entry->resp_derlen = 0;
+    pr_memscrub(entry->fingerprint, entry->fingerprint_len);
+    entry->fingerprint_len = 0;
+  }
+
+  res = ocspcache_data->od_listlen;
+  ocspcache_data->od_listlen = 0;
+
+  if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+    tls_log("shmcache: error unlocking shmcache: %s", strerror(errno));
+  }
+
+  return res;
+}
+
+static int ocsp_cache_remove(tls_ocsp_cache_t *cache) {
+  int res;
+  struct shmid_ds ds;
+  const char *cache_file;
+
+  if (ocspcache_fh == NULL) {
+    return 0;
+  }
+
+  if (cache != NULL) {
+    pr_trace_msg(trace_channel, 9, "removing shmcache ocsp cache %p", cache);
+  }
+
+  cache_file = ocspcache_fh->fh_path;
+  (void) ocsp_cache_close(cache);
+
+  if (ocspcache_shmid < 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_log_debug(DEBUG9, MOD_TLS_SHMCACHE_VERSION
+    ": attempting to remove OCSP response cache shm ID %d", ocspcache_shmid);
+
+  PRIVS_ROOT
+  res = shmctl(ocspcache_shmid, IPC_RMID, &ds);
+  PRIVS_RELINQUISH
+
+  if (res < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": error removing OCSP response cache shm ID %d: %s", ocspcache_shmid,
+      strerror(errno));
+
+  } else {
+    pr_log_debug(DEBUG9, MOD_TLS_SHMCACHE_VERSION
+      ": removed OCSP response cache shm ID %d", ocspcache_shmid);
+    ocspcache_shmid = -1;
+  }
+
+  /* Don't forget to remove the on-disk file as well. */
+  unlink(cache_file);
+
+  return res;
+}
+
+static int ocsp_cache_status(tls_ocsp_cache_t *cache,
+    void (*statusf)(void *, const char *, ...), void *arg, int flags) {
+  int res, xerrno = 0;
+  struct shmid_ds ds;
+  pool *tmp_pool;
+
+  pr_trace_msg(trace_channel, 9, "checking shmcache ocsp cache %p", cache);
+
+  if (shmcache_lock_shm(ocspcache_fh, F_RDLCK) < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": error read-locking shmcache: %s", strerror(errno));
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(permanent_pool);
+
+  statusf(arg, "%s", "Shared memory (shm) OCSP response cache provided by "
+    MOD_TLS_SHMCACHE_VERSION);
+  statusf(arg, "%s", "");
+  statusf(arg, "Shared memory segment ID: %d", ocspcache_shmid);
+
+  PRIVS_ROOT
+  res = shmctl(ocspcache_shmid, IPC_STAT, &ds);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (res == 0) {
+    statusf(arg, "Shared memory segment size: %u bytes",
+      (unsigned int) ds.shm_segsz);
+    statusf(arg, "Shared memory cache created on: %s",
+      pr_strtime(ds.shm_ctime));
+    statusf(arg, "Shared memory attach count: %u",
+      (unsigned int) ds.shm_nattch);
+
+  } else {
+    statusf(arg, "Unable to stat shared memory segment ID %d: %s",
+      ocspcache_shmid, strerror(xerrno));
+  }
+
+  statusf(arg, "%s", "");
+  statusf(arg, "Max response cache size: %u", ocspcache_data->od_listsz);
+  statusf(arg, "Current response cache size: %u", ocspcache_data->od_listlen);
+  statusf(arg, "%s", "");
+  statusf(arg, "Cache lifetime hits: %u", ocspcache_data->nhits);
+  statusf(arg, "Cache lifetime misses: %u", ocspcache_data->nmisses);
+  statusf(arg, "%s", "");
+  statusf(arg, "Cache lifetime responses stored: %u", ocspcache_data->nstored);
+  statusf(arg, "Cache lifetime responses deleted: %u",
+    ocspcache_data->ndeleted);
+  statusf(arg, "Cache lifetime responses expired: %u",
+    ocspcache_data->nexpired);
+  statusf(arg, "%s", "");
+  statusf(arg, "Cache lifetime errors handling responses in cache: %u",
+    ocspcache_data->nerrors);
+  statusf(arg, "Cache lifetime responses exceeding max entry size: %u",
+    ocspcache_data->nexceeded);
+  if (ocspcache_data->nexceeded > 0) {
+    statusf(arg, "  Largest response exceeding max entry size: %u",
+      ocspcache_data->exceeded_maxsz);
+  }
+
+  if (shmcache_lock_shm(ocspcache_fh, F_UNLCK) < 0) {
+    pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
+      ": error unlocking shmcache: %s", strerror(errno));
+  }
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+#endif /* PR_USE_OPENSSL_OCSP */
+
+/* Event Handlers
+ */
+
+/* Daemon PID */
+extern pid_t mpid;
+
+static void shmcache_shutdown_ev(const void *event_data, void *user_data) {
+  if (mpid == getpid() &&
+      ServerType == SERVER_STANDALONE) {
+
+    /* Remove external session caches on shutdown; the security policy/config
+     * may have changed, e.g. becoming more strict, and allow clients to
+     * resumed cached sessions from a more relaxed security config is not a
+     * Good Thing at all.
+     */
+    sess_cache_remove(NULL);
+#if defined(PR_USE_OPENSSL_OCSP)
+    ocsp_cache_remove(NULL);
+#endif /* PR_USE_OPENSSL_OCSP */
+  }
+}
+
+#if defined(PR_SHARED_MODULE)
+static void shmcache_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_tls_shmcache.c", (const char *) event_data) == 0) {
+    pr_event_unregister(&tls_shmcache_module, NULL, NULL);
+    tls_sess_cache_unregister("shm");
+
+    /* This clears our cache by detaching and destroying the shared memory
+     * segment.
+     */
+    sess_cache_remove(NULL);
+
+#if defined(PR_USE_OPENSSL_OCSP)
+    tls_ocsp_cache_unregister("shm");
+#endif /* PR_USE_OPENSSL_OCSP */
+  }
+}
+#endif /* !PR_SHARED_MODULE */
+
+static void shmcache_restart_ev(const void *event_data, void *user_data) {
+  /* Clear external session caches on shutdown; the security policy/config
+   * may have changed, e.g. becoming more strict, and allow clients to
+   * resumed cached sessions from a more relaxed security config is not a
+   * Good Thing at all.
+   */
+  sess_cache_clear(NULL);
+}
+
+/* Initialization functions
+ */
+
+static int tls_shmcache_init(void) {
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&tls_shmcache_module, "core.module-unload",
+    shmcache_mod_unload_ev, NULL);
+#endif /* !PR_SHARED_MODULE */
+  pr_event_register(&tls_shmcache_module, "core.restart", shmcache_restart_ev,
+    NULL);
+  pr_event_register(&tls_shmcache_module, "core.shutdown", shmcache_shutdown_ev,
+    NULL);
+
+  /* Prepare our SSL session cache handler. */
+  memset(&sess_cache, 0, sizeof(sess_cache));
+  sess_cache.open = sess_cache_open;
+  sess_cache.close = sess_cache_close;
+  sess_cache.add = sess_cache_add;
+  sess_cache.get = sess_cache_get;
+  sess_cache.delete = sess_cache_delete;
+  sess_cache.clear = sess_cache_clear;
+  sess_cache.remove = sess_cache_remove;
+  sess_cache.status = sess_cache_status;
+
+#ifdef SSL_SESS_CACHE_NO_INTERNAL_LOOKUP
+  /* Take a chance, and inform OpenSSL that it does not need to use its own
+   * internal session cache lookups; using the external session cache (i.e. us)
+   * will be enough.
+   */
+  sess_cache.cache_mode = SSL_SESS_CACHE_NO_INTERNAL_LOOKUP;
+#endif
+
+  /* Register ourselves with mod_tls. */
+  if (tls_sess_cache_register("shm", &sess_cache) < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
+      ": notice: error registering 'shm' SSL session cache: %s",
+      strerror(errno));
+    return -1;
+  }
+
+#if defined(PR_USE_OPENSSL_OCSP)
+  /* Prepare our OCSP response cache handler. */
+  memset(&ocsp_cache, 0, sizeof(ocsp_cache));
+  ocsp_cache.open = ocsp_cache_open;
+  ocsp_cache.close = ocsp_cache_close;
+  ocsp_cache.add = ocsp_cache_add;
+  ocsp_cache.get = ocsp_cache_get;
+  ocsp_cache.delete = ocsp_cache_delete;
+  ocsp_cache.clear = ocsp_cache_clear;
+  ocsp_cache.remove = ocsp_cache_remove;
+  ocsp_cache.status = ocsp_cache_status;
+
+  /* Register ourselves with mod_tls. */
+  if (tls_ocsp_cache_register("shm", &ocsp_cache) < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_TLS_SHMCACHE_VERSION
+      ": notice: error registering 'shm' OCSP response cache: %s",
       strerror(errno));
     return -1;
   }
+#endif /* PR_USE_OPENSSL_OCSP */
 
   return 0;
 }
@@ -1593,7 +2665,7 @@ static int tls_shmcache_init(void) {
 static int tls_shmcache_sess_init(void) {
 
 #ifdef HAVE_MLOCK
-  if (shmcache_data != NULL) {
+  if (sesscache_data != NULL) {
     int res, xerrno = 0;
 
     /* Make sure the memory is pinned in RAM where possible.
@@ -1603,19 +2675,19 @@ static int tls_shmcache_sess_init(void) {
      * when the session process exits.
      */
     PRIVS_ROOT
-    res = mlock(shmcache_data, shmcache_datasz);
+    res = mlock(sesscache_data, sesscache_datasz);
     xerrno = errno;
     PRIVS_RELINQUISH
 
     if (res < 0) {
       pr_log_debug(DEBUG1, MOD_TLS_SHMCACHE_VERSION
-        ": error locking 'shm' cache (%lu bytes) into memory: %s",
-        (unsigned long) shmcache_datasz, strerror(xerrno));
+        ": error locking 'shm' session cache (%lu bytes) into memory: %s",
+        (unsigned long) sesscache_datasz, strerror(xerrno));
 
     } else {
       pr_log_debug(DEBUG5, MOD_TLS_SHMCACHE_VERSION
-        ": 'shm' cache locked into memory (%lu bytes)",
-        (unsigned long) shmcache_datasz);
+        ": 'shm' session cache locked into memory (%lu bytes)",
+        (unsigned long) sesscache_datasz);
     }
   }
 #endif
diff --git a/contrib/mod_unique_id.c b/contrib/mod_unique_id.c
index 904c68a..96dacbe 100644
--- a/contrib/mod_unique_id.c
+++ b/contrib/mod_unique_id.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_unique_id -- a module for generating a unique ID for each
  *                           FTP session.
- *
- * Copyright (c) 2006-2011 TJ Saunders
+ * Copyright (c) 2006-2017 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
@@ -25,8 +24,6 @@
  *
  * This is mod_unique_id, contrib software for proftpd 1.2.x/1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_unique_id.c,v 1.6 2011-05-23 20:56:40 castaglia Exp $
  */
 
 #include "conf.h"
@@ -91,11 +88,11 @@ static void uniqid_mod_unload_ev(const void *event_data, void *user_data) {
 static void uniqid_postparse_ev(const void *event_data, void *user_data) {
   pool *tmp_pool = make_sub_pool(main_server->pool);
   const char *host_name = NULL;
-  pr_netaddr_t *host_addr = NULL;
+  const pr_netaddr_t *host_addr = NULL;
   void *addr_data = NULL;
 
   host_name = pr_netaddr_get_localaddr_str(tmp_pool);
-  if (!host_name) {
+  if (host_name == NULL) {
     pr_log_pri(PR_LOG_WARNING, MOD_UNIQUE_ID_VERSION
       ": unable to determine hostname");
     destroy_pool(tmp_pool);
@@ -104,7 +101,7 @@ static void uniqid_postparse_ev(const void *event_data, void *user_data) {
   }
 
   host_addr = pr_netaddr_get_addr(tmp_pool, host_name, NULL);
-  if (!host_addr) {
+  if (host_addr == NULL) {
     pr_log_pri(PR_LOG_WARNING, MOD_UNIQUE_ID_VERSION
       ": unable to resolve '%s' to an IP address", host_name);
     destroy_pool(tmp_pool);
diff --git a/contrib/mod_wrap.c b/contrib/mod_wrap.c
index 46f7b42..801d430 100644
--- a/contrib/mod_wrap.c
+++ b/contrib/mod_wrap.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_wrap -- use Wietse Venema's TCP wrappers library for
  *                      access control
- *
- * Copyright (c) 2000-2013 TJ Saunders
+ * Copyright (c) 2000-2017 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,7 +23,6 @@
  *
  * -- DO NOT MODIFY THE TWO LINES BELOW --
  * $Libraries: -lwrap -lnsl$
- * $Id: mod_wrap.c,v 1.28 2013-10-13 22:51:36 castaglia Exp $
  */
 
 #define MOD_WRAP_VERSION "mod_wrap/1.2.4"
@@ -39,13 +37,16 @@
 int allow_severity = PR_LOG_INFO;
 int deny_severity = PR_LOG_WARNING;
 
-/* function prototypes */
+module wrap_module;
+
+/* Necessary prototypes */
 static int wrap_eval_expression(char **, array_header *);
-static char *wrap_get_user_table(cmd_rec *, char *, char *);
-static int wrap_is_usable_file(char *);
+static const char *wrap_get_user_table(cmd_rec *, const char *, char *);
+static int wrap_is_usable_file(const char *);
 static void wrap_log_request_allowed(int, struct request_info *);
 static void wrap_log_request_denied(int, struct request_info *);
-static config_rec *wrap_resolve_user(pool *, char **);
+static config_rec *wrap_resolve_user(pool *, const char **);
+static int wrap_sess_init(void);
 
 static char *wrap_service_name = "proftpd";
 
@@ -58,7 +59,7 @@ static int wrap_eval_expression(char **config_expr,
     array_header *session_expr) {
 
   unsigned char found = FALSE;
-  int i = 0;
+  unsigned int i = 0;
   char *elem = NULL, **list = NULL;
 
   /* sanity check */
@@ -96,12 +97,9 @@ static int wrap_eval_expression(char **config_expr,
   return FALSE;
 }
 
-/* Determine logging-in user's access table locations.  This function was
- * "borrowed" (ie plagiarized/copied/whatever) liberally from modules/
- * mod_auth.c -- the _true_ author is MacGuyver <macguyver at tos.net>.
- */
-static char *wrap_get_user_table(cmd_rec *cmd, char *user,
+static const char *wrap_get_user_table(cmd_rec *cmd, const char *user,
     char *path) {
+  int xerrno = 0;
 
   char *real_path = NULL;
   struct passwd *pw = NULL;
@@ -121,21 +119,25 @@ static char *wrap_get_user_table(cmd_rec *cmd, char *user,
 
   PRIVS_USER
   real_path = dir_realpath(cmd->pool, path);
+  xerrno = errno;
   PRIVS_RELINQUISH
 
-  if (real_path)
+  if (real_path) {
     path = real_path;
+  }
 
+  errno = xerrno;
   return path;
 }
 
-static int wrap_is_usable_file(char *filename) {
+static int wrap_is_usable_file(const char *filename) {
   struct stat st;
   pr_fh_t *fh = NULL;
 
   /* check the easy case first */
-  if (filename == NULL)
+  if (filename == NULL) {
     return FALSE;
+  }
 
   /* Make sure that the current process can _read_ the file. */
   fh = pr_fsio_open(filename, O_RDONLY);
@@ -203,12 +205,7 @@ static void wrap_log_request_denied(int severity,
   return;
 }
 
-/* yet more plagiarizing...this one raided from mod_auth's _auth_resolve_user()
- * function [in case you haven't noticed yet, I'm quite the hack, in the
- * _true_ sense of the world]. =) hmmm...I wonder if it'd be feasible
- * to make some of mod_auth's functions visible from src/auth.c?
- */
-static config_rec *wrap_resolve_user(pool *p, char **user) {
+static config_rec *wrap_resolve_user(pool *p, const char **user) {
   config_rec *conf = NULL, *top_conf;
   char *ourname = NULL, *anonname = NULL;
   unsigned char is_alias = FALSE, force_anon = FALSE;
@@ -249,26 +246,30 @@ static config_rec *wrap_resolve_user(pool *p, char **user) {
       is_alias = TRUE;
   }
 
-  if (conf) {
+  if (conf != NULL) {
     *user = conf->argv[1];
 
     /* If the alias is applied inside an <Anonymous> context, we have found
      * our anon block
      */
-    if (conf->parent && conf->parent->config_type == CONF_ANON)
+    if (conf->parent &&
+        conf->parent->config_type == CONF_ANON) {
       conf = conf->parent;
-    else
+
+    } else {
       conf = NULL;
+    }
   }
 
   /* Next, search for an anonymous entry */
-  if (!conf)
+  if (conf == NULL) {
     conf = find_config(main_server->conf, CONF_ANON, NULL, FALSE);
 
-  else
+  } else {
     find_config_set_top(conf);
+  }
 
-  if (conf) do {
+  if (conf != NULL) do {
     anonname = (char*) get_param_ptr(conf->subset, "UserName", FALSE);
 
     if (!anonname)
@@ -287,15 +288,18 @@ static config_rec *wrap_resolve_user(pool *p, char **user) {
     if (find_config((conf ? conf->subset :
         main_server->conf), CONF_PARAM, "AuthAliasOnly", FALSE)) {
 
-      if (conf && conf->config_type == CONF_ANON)
+      if (conf != NULL &&
+          conf->config_type == CONF_ANON) {
         conf = NULL;
 
-      else
+      } else {
         *user = NULL;
+      }
 
-      if (*user && find_config(main_server->conf, CONF_PARAM, "AuthAliasOnly",
-          FALSE))
+      if (*user != NULL &&
+          find_config(main_server->conf, CONF_PARAM, "AuthAliasOnly", FALSE)) {
         *user = NULL;
+      }
     }
   }
 
@@ -399,8 +403,8 @@ MODRET set_tcpaccessfiles(cmd_rec *cmd) {
 }
 
 MODRET set_tcpgroupaccessfiles(cmd_rec *cmd) {
-  int group_argc = 1;
-  char **group_argv = NULL;
+  unsigned int group_argc = 1;
+  char *expr, **group_argv = NULL;
   array_header *group_acl = NULL;
   config_rec *c = NULL;
 
@@ -487,7 +491,8 @@ MODRET set_tcpgroupaccessfiles(cmd_rec *cmd) {
 
   c = add_config_param(cmd->argv[0], 0);
 
-  group_acl = pr_expr_create(cmd->tmp_pool, &group_argc, &cmd->argv[0]);
+  expr = (char *) cmd->argv[0];
+  group_acl = pr_expr_create(cmd->tmp_pool, &group_argc, &expr);
 
   /* build the desired config_rec manually */
   c->argc = group_argc + 2;
@@ -515,8 +520,8 @@ MODRET set_tcpgroupaccessfiles(cmd_rec *cmd) {
 }
 
 MODRET set_tcpuseraccessfiles(cmd_rec *cmd) {
-  int user_argc = 1;
-  char **user_argv = NULL;
+  unsigned int user_argc = 1;
+  char *expr, **user_argv = NULL;
   array_header *user_acl = NULL;
   config_rec *c = NULL;
 
@@ -603,7 +608,8 @@ MODRET set_tcpuseraccessfiles(cmd_rec *cmd) {
 
   c = add_config_param_str(cmd->argv[0], 0);
 
-  user_acl = pr_expr_create(cmd->tmp_pool, &user_argc, &cmd->argv[0]);
+  expr = (char *) cmd->argv[0];
+  user_acl = pr_expr_create(cmd->tmp_pool, &user_argc, &expr);
 
   /* build the desired config_rec manually */
   c->argc = user_argc + 2;
@@ -728,7 +734,7 @@ MODRET wrap_handle_request(cmd_rec *cmd) {
    */
   struct request_info request;
 
-  char *user = NULL;
+  const char *user = NULL;
   config_rec *conf = NULL, *access_conf = NULL, *syslog_conf = NULL;
   hosts_allow_table = NULL;
   hosts_deny_table = NULL;
@@ -745,8 +751,9 @@ MODRET wrap_handle_request(cmd_rec *cmd) {
    * handler, so it won't be protected from this case; we'll need to do
    * it manually.
    */
-  if (!user)
+  if (user == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   /* Use mod_auth's _auth_resolve_user() [imported for use here] to get the
    * right configuration set, since the user may be logging in anonymously,
@@ -844,7 +851,7 @@ MODRET wrap_handle_request(cmd_rec *cmd) {
    */
   if (hosts_allow_table != NULL && hosts_allow_table[0] == '~' &&
       hosts_allow_table[1] == '/') {
-    char *allow_real_table = NULL;
+    const char *allow_real_table = NULL;
 
     allow_real_table = wrap_get_user_table(cmd, user, hosts_allow_table);
 
@@ -854,7 +861,7 @@ MODRET wrap_handle_request(cmd_rec *cmd) {
       hosts_allow_table = NULL;
 
     } else
-      hosts_allow_table = allow_real_table;
+      hosts_allow_table = (char *) allow_real_table;
   }
 
   if (hosts_deny_table != NULL && hosts_deny_table[0] == '~' &&
@@ -953,14 +960,16 @@ MODRET wrap_handle_request(cmd_rec *cmd) {
     pr_event_generate("mod_wrap.connection-denied", NULL);
 
     /* check for AccessDenyMsg */
-    if ((denymsg = (char *) get_param_ptr(TOPLEVEL_CONF, "AccessDenyMsg",
-        FALSE)) != NULL)
-      denymsg = sreplace(cmd->tmp_pool, denymsg, "%u", user, NULL);
+    denymsg = (char *) get_param_ptr(TOPLEVEL_CONF, "AccessDenyMsg", FALSE);
+    if (denymsg != NULL) {
+      denymsg = (char *) sreplace(cmd->tmp_pool, denymsg, "%u", user, NULL);
+    }
 
-    if (denymsg)
+    if (denymsg != NULL) {
       return PR_ERROR_MSG(cmd, R_530, denymsg);
-    else
-      return PR_ERROR_MSG(cmd, R_530, _("Access denied"));
+    }
+
+    return PR_ERROR_MSG(cmd, R_530, _("Access denied"));
   }
 
   /* If request is allowable, return DECLINED (for engine to act as if this
@@ -972,15 +981,38 @@ MODRET wrap_handle_request(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void wrap_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&wrap_module, "core.session-reinit", wrap_sess_reinit_ev);
+
+  /* Reset defaults */
+  wrap_service_name = "proftpd";
+
+  res = wrap_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&wrap_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED,
+      NULL);
+  }
+}
+
 /* Initialization routines
  */
 
 static int wrap_sess_init(void) {
+  pr_event_register(&wrap_module, "core.session-reinit", wrap_sess_reinit_ev,
+    NULL);
 
   /* look up any configured TCPServiceName */
-  if ((wrap_service_name = get_param_ptr(main_server->conf,
-      "TCPServiceName", FALSE)) == NULL)
+  wrap_service_name = get_param_ptr(main_server->conf, "TCPServiceName", FALSE);
+  if (wrap_service_name == NULL) {
     wrap_service_name = "proftpd";
+  }
 
   return 0;
 }
diff --git a/contrib/mod_wrap2/Makefile.in b/contrib/mod_wrap2/Makefile.in
index 0dc88d9..8e546a6 100644
--- a/contrib/mod_wrap2/Makefile.in
+++ b/contrib/mod_wrap2/Makefile.in
@@ -36,7 +36,7 @@ install:
 	fi
 
 clean:
-	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).a $(MODULE_NAME).la *.o *.lo .libs/*.o
+	$(LIBTOOL) --mode=clean $(RM) $(MODULE_NAME).a $(MODULE_NAME).la *.o *.gnco *.lo .libs/*.o
 
 dist: clean
 	$(RM) Makefile $(MODULE_NAME).h config.status config.cache config.log
diff --git a/contrib/mod_wrap2/configure b/contrib/mod_wrap2/configure
index 07e533f..9315ac4 100755
--- a/contrib/mod_wrap2/configure
+++ b/contrib/mod_wrap2/configure
@@ -2672,7 +2672,7 @@ do
     cat >>$CONFIG_STATUS <<_ACEOF
     # First, check the format of the line:
     cat >"\$tmp/defines.sed" <<\\CEOF
-/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*/b def
+/^[	 ]*#[	 ]*undef[	 ][	 ]*$ac_word_re[	 ]*\$/b def
 /^[	 ]*#[	 ]*define[	 ][	 ]*$ac_word_re[(	 ]/b def
 b
 :def
diff --git a/contrib/mod_wrap2/configure.in b/contrib/mod_wrap2/configure.in
index 53feb56..c0d491a 100644
--- a/contrib/mod_wrap2/configure.in
+++ b/contrib/mod_wrap2/configure.in
@@ -1,3 +1,22 @@
+dnl ProFTPD - mod_wrap2
+dnl Copyright (c) 2012-2015 TJ Saunders <tj at castaglia.org>
+dnl
+dnl This program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful,
+dnl but WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+dnl GNU General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program; if not, write to the Free Software
+dnl Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+dnl
+dnl Process this file with autoconf to produce a configure script.
+
 AC_INIT(./mod_wrap2.c)
 
 ac_wrap2_libs=""
diff --git a/contrib/mod_wrap2/mod_wrap2.c b/contrib/mod_wrap2/mod_wrap2.c
index bc97e37..aa50da0 100644
--- a/contrib/mod_wrap2/mod_wrap2.c
+++ b/contrib/mod_wrap2/mod_wrap2.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_wrap2 -- tcpwrappers-like access control
- *
- * Copyright (c) 2000-2014 TJ Saunders
+ * Copyright (c) 2000-2016 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
@@ -36,7 +35,7 @@ typedef struct regtab_obj {
   const char *regtab_name;
 
   /* Initialization function for this type of table source */
-  wrap2_table_t *(*regtab_open)(pool *, char *);
+  wrap2_table_t *(*regtab_open)(pool *, const char *);
 
 } wrap2_regtab_t;
 
@@ -55,11 +54,11 @@ static wrap2_regtab_t *wrap2_regtab_list = NULL;
 
 /* Logging data */
 static int wrap2_logfd = -1;
-static char *wrap2_logname = NULL;
+static const char *wrap2_logname = NULL;
 
 static int wrap2_engine = FALSE;
-static char *wrap2_service_name = WRAP2_DEFAULT_SERVICE_NAME;
-static char *wrap2_client_name = NULL;
+static const char *wrap2_service_name = WRAP2_DEFAULT_SERVICE_NAME;
+static const char *wrap2_client_name = NULL;
 static config_rec *wrap2_ctxt = NULL;
 
 /* Access check variables */
@@ -108,6 +107,9 @@ typedef struct conn_info {
 #define INADDR_NONE 0xffffffff
 #endif
 
+/* Necessary prototypes. */
+static int wrap2_sess_init(void);
+
 /* Logging routines */
 
 static int wrap2_closelog(void) {
@@ -172,6 +174,11 @@ static wrap2_table_t *wrap2_open_table(char *name) {
   wrap2_table_t *tab = NULL;
 
   info = ptr = strchr(name, ':');
+  if (info == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   *info++ = '\0';
 
   /* Look up the table source open routine by name, and invoke it */
@@ -287,7 +294,7 @@ static wrap2_conn_t *wrap2_conn_set(wrap2_conn_t *conn, ...) {
 static char *wrap2_get_user(wrap2_conn_t *conn) {
 
   if (*conn->user == '\0') {
-    char *rfc1413_ident;
+    const char *rfc1413_ident;
 
     /* RFC1413 lookups may have already been done by the mod_ident module.
      * If so, use the ident name stashed; otherwise, use the user name issued
@@ -296,14 +303,14 @@ static char *wrap2_get_user(wrap2_conn_t *conn) {
 
     rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident",
       NULL);
-    if (rfc1413_ident) {
+    if (rfc1413_ident != NULL) {
       sstrncpy(conn->user, rfc1413_ident, sizeof(conn->user));
 
     } else {
-      char *user;
+      const char *user;
 
       user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-      if (user) {
+      if (user != NULL) {
         sstrncpy(conn->user, user, sizeof(conn->user));
       }
     }
@@ -329,12 +336,16 @@ static char *wrap2_get_hostname(wrap2_host_t *host) {
 
     reverse_dns = pr_netaddr_set_reverse_dns(TRUE);
     if (reverse_dns) {
+      pr_netaddr_t *remote_addr;
+
       /* If UseReverseDNS is on, then clear any caches, so that we really do
        * use the DNS name here if possible.
        */
       pr_netaddr_clear_cache();
 
-      session.c->remote_addr->na_have_dnsstr = FALSE;
+      remote_addr = (pr_netaddr_t *) session.c->remote_addr;
+      remote_addr->na_have_dnsstr = FALSE;
+
       sstrncpy(host->name, pr_netaddr_get_dnsstr(session.c->remote_addr),
         sizeof(host->name));
 
@@ -346,7 +357,7 @@ static char *wrap2_get_hostname(wrap2_host_t *host) {
       }
 
       pr_netaddr_set_reverse_dns(reverse_dns);
-      session.c->remote_addr->na_have_dnsstr = TRUE;
+      remote_addr->na_have_dnsstr = TRUE;
 
     } else {
       wrap2_log("'UseReverseDNS off' in effect, NOT resolving %s to DNS name "
@@ -600,7 +611,7 @@ static unsigned char wrap2_match_host(char *tok, wrap2_host_t *host) {
   } else if (pr_netaddr_use_ipv6() &&
              *tok == '[') {
     char *cp;
-    pr_netaddr_t *acl_addr;
+    const pr_netaddr_t *acl_addr;
 
     /* IPv6 address */
 
@@ -656,7 +667,7 @@ static unsigned char wrap2_match_host(char *tok, wrap2_host_t *host) {
     return (wrap2_match_netmask(tok, mask, wrap2_get_hostaddr(host)));
 
   } else {
-    pr_netaddr_t *acl_addr;
+    const pr_netaddr_t *acl_addr;
 
     /* Anything else.
      *
@@ -1208,7 +1219,7 @@ static unsigned char wrap2_eval_or_expression(char **acl, array_header *creds) {
   list = (char **) creds->elts;
 
   for (; *acl; acl++) {
-    register int i = 0;
+    register unsigned int i = 0;
     elem = *acl;
     found = FALSE;
 
@@ -1244,7 +1255,7 @@ static unsigned char wrap2_eval_and_expression(char **acl, array_header *creds)
   list = (char **) creds->elts;
 
   for (; *acl; acl++) {
-    register int i = 0;
+    register unsigned int i = 0;
     elem = *acl;
     found = FALSE;
 
@@ -1268,7 +1279,7 @@ static unsigned char wrap2_eval_and_expression(char **acl, array_header *creds)
 }
 
 int wrap2_register(const char *srcname,
-    wrap2_table_t *(*srcopen)(pool *, char *)) {
+    wrap2_table_t *(*srcopen)(pool *, const char *)) {
 
   /* Note: I know that use of permanent_pool is discouraged as much as
    * possible, but in this particular instance, I need a pool that
@@ -1290,7 +1301,6 @@ int wrap2_register(const char *srcname,
   }
 
   wrap2_regtab_list = regtab;
-
   return 0;
 }
 
@@ -1356,7 +1366,7 @@ static array_header *builtin_fetch_options_cb(wrap2_table_t *tab,
   return NULL;
 }
 
-static wrap2_table_t *builtin_open_cb(pool *parent_pool, char *srcinfo) {
+static wrap2_table_t *builtin_open_cb(pool *parent_pool, const char *srcinfo) {
   wrap2_table_t *tab = NULL;
   pool *tab_pool = make_sub_pool(parent_pool);
 
@@ -1423,9 +1433,8 @@ MODRET set_wrapgrouptables(cmd_rec *cmd) {
   register unsigned int i = 0;
   unsigned char have_registration = FALSE;
   config_rec *c = NULL;
-
-  int argc = 1;
-  char **argv = NULL;
+  unsigned int argc = 1;
+  void **argv = NULL;
   array_header *acl = NULL;
 
   CHECK_ARGS(cmd, 3);
@@ -1438,7 +1447,7 @@ MODRET set_wrapgrouptables(cmd_rec *cmd) {
     tmp = strchr(cmd->argv[i], ':');
     if (tmp == NULL) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "badly table parameter: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
     }
 
     *tmp = '\0';
@@ -1452,20 +1461,19 @@ MODRET set_wrapgrouptables(cmd_rec *cmd) {
 
     if (!have_registration) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unsupported table source type: '",
-        cmd->argv[1], "'", NULL));
+        (char *) cmd->argv[1], "'", NULL));
     }
 
     *tmp = ':';
   }
 
   c = add_config_param(cmd->argv[0], 0);
-
-  acl = pr_expr_create(cmd->tmp_pool, &argc, &cmd->argv[0]);
+  acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) &cmd->argv[0]);
 
   /* Build the desired config_rec manually. */
   c->argc = argc + 2;
-  c->argv = pcalloc(c->pool, (argc + 3) * sizeof(char *));
-  argv = (char **) c->argv;
+  c->argv = pcalloc(c->pool, (argc + 3) * sizeof(void *));
+  argv = c->argv;
 
   /* The tables are the first two parameters */
   *argv++ = pstrdup(c->pool, cmd->argv[2]);
@@ -1492,7 +1500,6 @@ MODRET set_wraplog(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
-
   return PR_HANDLED(cmd);
 }
 
@@ -1502,8 +1509,9 @@ MODRET set_wrapoptions(cmd_rec *cmd) {
   register unsigned int i = 0;
   unsigned long opts = 0UL;
 
-  if (cmd->argc-1 == 0)
+  if (cmd->argc-1 == 0) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -1586,9 +1594,8 @@ MODRET set_wrapusertables(cmd_rec *cmd) {
   register unsigned int i = 0;
   unsigned char have_registration = FALSE;
   config_rec *c = NULL;
-
-  int argc = 1;
-  char **argv = NULL;
+  unsigned int argc = 1;
+  void **argv = NULL;
   array_header *acl = NULL;
 
   CHECK_ARGS(cmd, 3);
@@ -1601,7 +1608,7 @@ MODRET set_wrapusertables(cmd_rec *cmd) {
     tmp = strchr(cmd->argv[i], ':');
     if (tmp == NULL) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "badly table parameter: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
     }
 
     *tmp = '\0';
@@ -1615,37 +1622,36 @@ MODRET set_wrapusertables(cmd_rec *cmd) {
 
     if (!have_registration) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unsupported table source type: '",
-        cmd->argv[1], "'", NULL));
+        (char *) cmd->argv[1], "'", NULL));
     }
 
     *tmp = ':';
   }
 
   c = add_config_param(cmd->argv[0], 0);
-
-  acl = pr_expr_create(cmd->tmp_pool, &argc, &cmd->argv[0]);
+  acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) &cmd->argv[0]);
 
   /* Build the desired config_rec manually. */
   c->argc = argc + 2;
-  c->argv = pcalloc(c->pool, (argc + 3) * sizeof(char *));
-  argv = (char **) c->argv;
+  c->argv = pcalloc(c->pool, (argc + 3) * sizeof(void *));
+  argv = c->argv;
 
   /* The tables are the first two parameters */
   *argv++ = pstrdup(c->pool, cmd->argv[2]);
   *argv++ = pstrdup(c->pool, cmd->argv[3]); 
 
   /* Now populate the user-expression names */
-  if (argc && acl)
+  if (argc && acl) {
     while (argc--) {
       *argv++ = pstrdup(c->pool, *((char **) acl->elts));
       acl->elts = ((char **) acl->elts) + 1;
     }
+  }
 
   /* Do not forget the terminating NULL */
   *argv = NULL;
 
   c->flags |= CF_MERGEDOWN;
-
   return PR_HANDLED(cmd);
 }
 
@@ -1655,23 +1661,26 @@ MODRET set_wrapusertables(cmd_rec *cmd) {
 MODRET wrap2_pre_pass(cmd_rec *cmd) {
   wrap2_conn_t conn;
   unsigned char have_tables = FALSE;
-  char *user = NULL;
+  const char *user = NULL;
   config_rec *c = NULL;
 
-  if (!wrap2_engine)
+  if (wrap2_engine == FALSE) {
     return PR_DECLINED(cmd);
+  }
 
   /* Hide passwords */
   session.hide_password = TRUE;
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-  if (!user)
+  if (user == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   wrap2_ctxt = pr_auth_get_anon_config(cmd->pool, &user, NULL, NULL);
 
-  if (!user)
+  if (user == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   {
     /* Cheat a little here, and pre-populate some of session's members.  Do
@@ -1818,7 +1827,7 @@ MODRET wrap2_pre_pass(cmd_rec *cmd) {
   wrap2_log("%s", "checking access rules for connection");
 
   if (wrap2_allow_access(&conn) == FALSE) {
-    char *msg = NULL;
+    const char *msg = NULL;
 
     /* Log the denied connection */
     wrap2_log("refused connection from %s", wrap2_get_client(&conn));
@@ -1846,10 +1855,11 @@ MODRET wrap2_pre_pass(cmd_rec *cmd) {
 }
 
 MODRET wrap2_post_pass(cmd_rec *cmd) {
-  char *msg = NULL;
+  const char *msg = NULL;
 
-  if (!wrap2_engine)
+  if (wrap2_engine == FALSE) {
     return PR_DECLINED(cmd);
+  }
 
   /* Check for a configured WrapAllowMsg.  If the connection were denied,
    * it would have been terminated before reaching this command handler.
@@ -1857,7 +1867,7 @@ MODRET wrap2_post_pass(cmd_rec *cmd) {
   msg = get_param_ptr(wrap2_ctxt ? wrap2_ctxt->subset : main_server->conf,
     "WrapAllowMsg", FALSE);
   if (msg != NULL) {
-    char *user;
+    const char *user;
 
     user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
     msg = sreplace(cmd->tmp_pool, msg, "%u", user, NULL);
@@ -1925,6 +1935,33 @@ static void wrap2_restart_ev(const void *event_data, void *user_data) {
   pr_pool_tag(wrap2_pool, MOD_WRAP2_VERSION);
 }
 
+static void wrap2_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer; reinitialize ourselves. */
+
+  pr_event_unregister(&wrap2_module, "core.exit", wrap2_exit_ev);
+  pr_event_unregister(&wrap2_module, "core.session-reinit",
+    wrap2_sess_reinit_ev);
+
+  /* Reset defaults. */
+  wrap2_engine = FALSE;
+  (void) close(wrap2_logfd);
+  wrap2_logfd = -1;
+  wrap2_logname = NULL;
+  wrap2_service_name = WRAP2_DEFAULT_SERVICE_NAME;
+  wrap2_opts = 0UL;
+  wrap2_allow_table = NULL;
+  wrap2_deny_table = NULL;
+  wrap2_client_name = NULL;
+
+  res = wrap2_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&wrap2_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -1951,12 +1988,15 @@ static int wrap2_init(void) {
 static int wrap2_sess_init(void) {
   config_rec *c;
 
+  pr_event_register(&wrap2_module, "core.session-reinit", wrap2_sess_reinit_ev,
+    NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "WrapEngine", FALSE);
-  if (c) {
+  if (c != NULL) {
     wrap2_engine = *((int *) c->argv[0]);
   }
 
-  if (!wrap2_engine) {
+  if (wrap2_engine == FALSE) {
     return 0;
   }
 
@@ -2003,7 +2043,7 @@ static int wrap2_sess_init(void) {
       wrap2_log("%s", "checking access rules for connection");
 
       if (wrap2_allow_access(&conn) == FALSE) {
-        char *msg = NULL;
+        const char *msg = NULL;
 
         /* Log the denied connection */
         wrap2_log("refused connection from %s", wrap2_get_client(&conn));
diff --git a/contrib/mod_wrap2/mod_wrap2.h.in b/contrib/mod_wrap2/mod_wrap2.h.in
index c311b80..c2e31da 100644
--- a/contrib/mod_wrap2/mod_wrap2.h.in
+++ b/contrib/mod_wrap2/mod_wrap2.h.in
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_wrap2 -- tcpwrappers-like access control
- *
- * Copyright (c) 2000-2011 TJ Saunders
+ * Copyright (c) 2000-2016 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
@@ -21,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_wrap2.h.in,v 1.10 2011-11-06 21:56:12 castaglia Exp $
  */
 
 #ifndef MOD_WRAP2_H
@@ -31,7 +28,7 @@
 #include "conf.h"
 #include "privs.h"
 
-#define MOD_WRAP2_VERSION 	"mod_wrap2/2.0.6"
+#define MOD_WRAP2_VERSION 	"mod_wrap2/2.0.7"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030402
@@ -83,7 +80,8 @@ int wrap2_log(const char *, ...)
        ;
 #endif
 
-int wrap2_register(const char *, wrap2_table_t *(*tab_open)(pool *, char *));
+int wrap2_register(const char *,
+  wrap2_table_t *(*tab_open)(pool *, const char *));
 int wrap2_unregister(const char *);
 
 char *wrap2_strsplit(char *, int);
diff --git a/contrib/mod_wrap2_file.c b/contrib/mod_wrap2_file.c
index 63dc302..1e36a79 100644
--- a/contrib/mod_wrap2_file.c
+++ b/contrib/mod_wrap2_file.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_wrap2_file -- a mod_wrap2 sub-module for supplying IP-based
  *                            access control data via file-based tables
- *
- * Copyright (c) 2002-2014 TJ Saunders
+ * Copyright (c) 2002-2016 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
@@ -21,8 +20,6 @@
  * As a special exemption, TJ Saunders gives permission to link this program
  * with OpenSSL, and distribute the resulting executable, without including
  * the source code for OpenSSL in the source distribution.
- *
- * $Id: mod_wrap2_file.c,v 1.14 2013-12-19 23:19:50 castaglia Exp $
  */
 
 #include "mod_wrap2.h"
@@ -149,10 +146,10 @@ static void filetab_parse_table(wrap2_table_t *filetab) {
 
       ptr = strpbrk(res, ", \t");
       if (ptr != NULL) {
-        char *dup = pstrdup(filetab->tab_pool, res);
-        char *word;
+        char *dup_opts, *word;
 
-        while ((word = pr_str_get_token(&dup, ", \t")) != NULL) {
+        dup_opts = pstrdup(filetab->tab_pool, res);
+        while ((word = pr_str_get_token(&dup_opts, ", \t")) != NULL) {
           size_t wordlen;
 
           pr_signals_handle();
@@ -165,15 +162,16 @@ static void filetab_parse_table(wrap2_table_t *filetab) {
           /* Remove any trailing comma */
           if (word[wordlen-1] == ',') {
             word[wordlen-1] = '\0';
+            wordlen--;
           }
 
           *((char **) push_array(filetab_clients_list)) = word;
 
           /* Skip redundant whitespaces */
-          while (*dup == ' ' ||
-                 *dup == '\t') {
+          while (*dup_opts == ' ' ||
+                 *dup_opts == '\t') {
             pr_signals_handle();
-            dup++;
+            dup_opts++;
           }
         }
 
@@ -242,7 +240,7 @@ static array_header *filetab_fetch_options_cb(wrap2_table_t *filetab,
   return filetab_options_list;
 }
 
-static wrap2_table_t *filetab_open_cb(pool *parent_pool, char *srcinfo) {
+static wrap2_table_t *filetab_open_cb(pool *parent_pool, const char *srcinfo) {
   struct stat st;
   wrap2_table_t *tab = NULL;
   pool *tab_pool = make_sub_pool(parent_pool);
@@ -273,11 +271,11 @@ static wrap2_table_t *filetab_open_cb(pool *parent_pool, char *srcinfo) {
 
   /* If the path contains a %U variable, interpolate it. */
   if (strstr(srcinfo, "%U") != NULL) {
-    char *orig_user;
+    const char *orig_user;
 
     orig_user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
     if (orig_user != NULL) {
-      char *interp_path;
+      const char *interp_path;
 
       interp_path = sreplace(tab_pool, srcinfo, "%U", orig_user, NULL);
       if (interp_path != NULL) {
diff --git a/contrib/mod_wrap2_redis.c b/contrib/mod_wrap2_redis.c
new file mode 100644
index 0000000..a5866d0
--- /dev/null
+++ b/contrib/mod_wrap2_redis.c
@@ -0,0 +1,481 @@
+/*
+ * ProFTPD: mod_wrap2_redis -- a mod_wrap2 sub-module for supplying IP-based
+ *                             access control data via Redis
+ * Copyright (c) 2017 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 gives permission to link this program
+ * with OpenSSL, and distribute the resulting executable, without including
+ * the source code for OpenSSL in the source distribution.
+ */
+
+#include "mod_wrap2.h"
+#include "redis.h"
+
+#define MOD_WRAP2_REDIS_VERSION		"mod_wrap2_redis/0.1"
+
+#define WRAP2_REDIS_NKEYS		2
+#define WRAP2_REDIS_CLIENT_KEY_IDX	0
+#define WRAP2_REDIS_OPTION_KEY_IDX	1
+
+module wrap2_redis_module;
+
+static char *get_named_key(pool *p, char *key, const char *name) {
+  if (name == NULL) {
+    return key;
+  }
+
+  if (strstr(key, "%{name}") != NULL) {
+    key = (char *) sreplace(p, key, "%{name}", name, NULL);
+  }
+
+  return key;
+}
+
+static int redistab_close_cb(wrap2_table_t *redistab) {
+  pr_redis_t *redis;
+
+  redis = redistab->tab_handle;
+
+  (void) pr_redis_conn_close(redis);
+  redistab->tab_handle = NULL;
+  return 0;
+}
+
+static array_header *redistab_fetch_clients_cb(wrap2_table_t *redistab,
+    const char *name) {
+  register unsigned int i;
+  pool *tmp_pool = NULL;
+  pr_redis_t *redis;
+  char *key = NULL, **vals = NULL;
+  array_header *items = NULL, *itemszs = NULL, *clients = NULL;
+  int res, xerrno = 0, use_list = TRUE;
+
+  /* Allocate a temporary pool for the duration of this read. */
+  tmp_pool = make_sub_pool(redistab->tab_pool);
+
+  key = ((char **) redistab->tab_data)[WRAP2_REDIS_CLIENT_KEY_IDX];
+
+  if (strncasecmp(key, "list:", 5) == 0) {
+    key += 5;
+
+  } else if (strncasecmp(key, "set:", 4) == 0) {
+    use_list = FALSE;
+    key += 4;
+  }
+
+  key = get_named_key(tmp_pool, key, name);
+  redis = redistab->tab_handle;
+
+  if (use_list == TRUE) {
+    res = pr_redis_list_getall(tmp_pool, redis, &wrap2_redis_module, key,
+      &items, &itemszs);
+    xerrno = errno;
+
+  } else {
+    res = pr_redis_set_getall(tmp_pool, redis, &wrap2_redis_module, key,
+      &items, &itemszs);
+    xerrno = errno;
+  }
+
+  /* Check the results. */
+  if (res < 0) {
+    if (use_list == TRUE) {
+      wrap2_log("error obtaining clients from Redis using list '%s': %s",
+        key, strerror(xerrno));
+
+    } else {
+      wrap2_log("error obtaining clients from Redis using set '%s': %s",
+        key, strerror(xerrno));
+    }
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return NULL;
+  }
+
+  if (items->nelts < 1) {
+    if (use_list == TRUE) {
+      wrap2_log("no clients found in Redis using list '%s'", key);
+
+    } else {
+      wrap2_log("no clients found in Redis using set '%s'", key);
+    }
+
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  clients = make_array(redistab->tab_pool, items->nelts, sizeof(char *));
+
+  /* Iterate through each returned row.  If there are commas or whitespace
+   * in the row, parse them as separate client names.  Otherwise, a comma-
+   * or space-delimited list of names will be treated as a single name, and
+   * violate the principle of least surprise for the site admin.
+   */
+
+  vals = (char **) items->elts;
+
+  for (i = 0; i < items->nelts; i++) {
+    char *ptr, *val;
+
+    if (vals[i] == NULL) {
+      continue;
+    }
+
+    val = vals[i];
+
+    /* Values in Redis are NOT NUL-terminated. */
+    val = pstrndup(tmp_pool, val, ((size_t *) itemszs->elts)[i]);
+
+    ptr = strpbrk(val, ", \t");
+    if (ptr != NULL) {
+      char *dup_opts, *word;
+
+      dup_opts = pstrdup(redistab->tab_pool, val);
+      while ((word = pr_str_get_token(&dup_opts, ", \t")) != NULL) {
+        size_t wordlen;
+
+        pr_signals_handle();
+
+        wordlen = strlen(word);
+        if (wordlen == 0) {
+          continue;
+        }
+
+        /* Remove any trailing comma */
+        if (word[wordlen-1] == ',') {
+          word[wordlen-1] = '\0';
+          wordlen--;
+        }
+
+        *((char **) push_array(clients)) = word;
+
+        /* Skip redundant whitespaces */
+        while (*dup_opts == ' ' ||
+               *dup_opts == '\t') {
+          pr_signals_handle();
+          dup_opts++;
+        }
+      }
+
+    } else {
+      *((char **) push_array(clients)) = pstrdup(redistab->tab_pool, val);
+    }
+  }
+
+  destroy_pool(tmp_pool);
+  return clients;
+}
+
+static array_header *redistab_fetch_daemons_cb(wrap2_table_t *redistab,
+    const char *name) {
+  array_header *daemons_list;
+
+  /* Simply return the service name we're given. */
+  daemons_list = make_array(redistab->tab_pool, 1, sizeof(char *));
+  *((char **) push_array(daemons_list)) = pstrdup(redistab->tab_pool, name);
+
+  return daemons_list;
+}
+
+static array_header *redistab_fetch_options_cb(wrap2_table_t *redistab,
+    const char *name) {
+  register unsigned int i;
+  pool *tmp_pool = NULL;
+  pr_redis_t *redis;
+  char *key = NULL, **vals = NULL;
+  array_header *items = NULL, *itemszs = NULL, *options = NULL;
+  int res, xerrno = 0, use_list = TRUE;
+
+  /* Allocate a temporary pool for the duration of this read. */
+  tmp_pool = make_sub_pool(redistab->tab_pool);
+
+  key = ((char **) redistab->tab_data)[WRAP2_REDIS_OPTION_KEY_IDX];
+
+  /* The options key is not necessary.  Skip if not present. */
+  if (key == NULL) {
+    destroy_pool(tmp_pool);
+    return NULL;
+  }
+
+  if (strncasecmp(key, "list:", 5) == 0) {
+    key += 5;
+
+  } else if (strncasecmp(key, "set:", 4) == 0) {
+    use_list = FALSE;
+    key += 4;
+  }
+
+  key = get_named_key(tmp_pool, key, name);
+  redis = redistab->tab_handle;
+
+  if (use_list == TRUE) {
+    res = pr_redis_list_getall(tmp_pool, redis, &wrap2_redis_module, key,
+      &items, &itemszs);
+    xerrno = errno;
+
+  } else {
+    res = pr_redis_set_getall(tmp_pool, redis, &wrap2_redis_module, key,
+      &items, &itemszs);
+    xerrno = errno;
+  }
+
+  /* Check the results. */
+  if (res < 0) {
+    if (use_list == TRUE) {
+      wrap2_log("error obtaining options from Redis using list '%s': %s",
+        key, strerror(xerrno));
+
+    } else {
+      wrap2_log("error obtaining options from Redis using set '%s': %s",
+        key, strerror(xerrno));
+    }
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return NULL;
+  }
+
+  if (items->nelts < 1) {
+    if (use_list == TRUE) {
+      wrap2_log("no options found in Redis using list '%s'", key);
+
+    } else {
+      wrap2_log("no options found in Redis using set '%s'", key);
+    }
+
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  options = make_array(redistab->tab_pool, items->nelts, sizeof(char *));
+
+  vals = (char **) items->elts;
+
+  for (i = 0; i < items->nelts; i++) {
+    char *val;
+
+    if (vals[i] == NULL) {
+      continue;
+    }
+
+    /* Values in Redis are NOT NUL-terminated. */
+    val = pstrndup(tmp_pool, vals[i], ((size_t *) itemszs->elts)[i]);
+
+    *((char **) push_array(options)) = pstrdup(redistab->tab_pool, val);
+  }
+
+  destroy_pool(tmp_pool);
+  return options;
+}
+
+static wrap2_table_t *redistab_open_cb(pool *parent_pool, const char *srcinfo) {
+  wrap2_table_t *tab = NULL;
+  pool *tab_pool = make_sub_pool(parent_pool),
+    *tmp_pool = make_sub_pool(parent_pool);
+  char *start = NULL, *finish = NULL, *info;
+  char *client_key = NULL, *option_key = NULL;
+  pr_redis_t *redis;
+
+  tab = (wrap2_table_t *) pcalloc(tab_pool, sizeof(wrap2_table_t));
+  tab->tab_pool = tab_pool;
+
+  /* The srcinfo string for this case should look like:
+   *  "/list|set:<client-key>[/list|set:<options-key>]"
+   */
+
+  info = pstrdup(tmp_pool, srcinfo);
+  start = strchr(info, '/');
+  if (start == NULL) {
+    wrap2_log("error: badly formatted source info '%s'", srcinfo);
+    destroy_pool(tab_pool);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  /* Find the next slash. */
+  finish = strchr(++start, '/');
+  if (finish != NULL) {
+    *finish = '\0';
+  }
+
+  client_key = pstrdup(tab->tab_pool, start);
+
+  /* Handle the options list, if present. */
+  if (finish != NULL) {
+    option_key = pstrdup(tab->tab_pool, ++finish);
+  }
+
+  if (strncasecmp(client_key, "list:", 5) != 0 &&
+      strncasecmp(client_key, "set:", 4) != 0) {
+    wrap2_log("error: client key '%s' lacks required 'list:' or 'set:' prefix",
+      client_key);
+    destroy_pool(tab_pool);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (option_key != NULL) {
+    if (strncasecmp(option_key, "list:", 5) != 0 &&
+        strncasecmp(option_key, "set:", 4) != 0) {
+      wrap2_log("error: option key '%s' lacks required 'list:' or 'set:' "
+        "prefix", option_key);
+      destroy_pool(tab_pool);
+      destroy_pool(tmp_pool);
+      errno = EINVAL;
+      return NULL;
+    }
+  }
+
+  redis = pr_redis_conn_new(tab->tab_pool, &wrap2_redis_module, 0);
+  if (redis == NULL) {
+    int xerrno = errno;
+
+    wrap2_log("error: unable to open Redis connection: %s", strerror(xerrno));
+    destroy_pool(tab_pool);
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return NULL;
+  }
+
+  tab->tab_handle = redis;
+  tab->tab_name = pstrcat(tab->tab_pool, "Redis(", info, ")", NULL);
+
+  tab->tab_data = pcalloc(tab->tab_pool, WRAP2_REDIS_NKEYS * sizeof(char *));
+  ((char **) tab->tab_data)[WRAP2_REDIS_CLIENT_KEY_IDX] =
+    pstrdup(tab->tab_pool, client_key);
+
+  ((char **) tab->tab_data)[WRAP2_REDIS_OPTION_KEY_IDX] =
+    pstrdup(tab->tab_pool, option_key);
+
+  /* Set the necessary callbacks. */
+  tab->tab_close = redistab_close_cb;
+  tab->tab_fetch_clients = redistab_fetch_clients_cb;
+  tab->tab_fetch_daemons = redistab_fetch_daemons_cb;
+  tab->tab_fetch_options = redistab_fetch_options_cb;
+
+  destroy_pool(tmp_pool);
+  return tab;
+}
+
+/* Event handlers
+ */
+
+#if defined(PR_SHARED_MODULE)
+static void redistab_mod_unload_ev(const void *event_data, void *user_data) {
+  if (strcmp("mod_wrap2_redis.c", (const char *) event_data) == 0) {
+    pr_event_unregister(&wrap2_redis_module, NULL, NULL);
+    wrap2_unregister("redis");
+  }
+}
+#endif /* PR_SHARED_MODULE */
+
+/* Initialization routines
+ */
+
+static int redistab_init(void) {
+
+  /* Initialize the wrap source objects for type "redis".  */
+  wrap2_register("redis", redistab_open_cb);
+
+#if defined(PR_SHARED_MODULE)
+  pr_event_register(&wrap2_redis_module, "core.module-unload",
+    redistab_mod_unload_ev, NULL);
+#endif /* PR_SHARED_MODULE */
+
+  return 0;
+}
+
+static int redistab_sess_init(void) {
+  config_rec *c;
+  int engine;
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisEngine", FALSE);
+  if (c == NULL) {
+    return 0;
+  }
+
+  engine = *((int *) c->argv[0]);
+  if (engine == FALSE) {
+    return 0;
+  }
+
+  /* Note: These lookups duplicate what mod_redis does.  But we do it here
+   * due to module load ordering; we want to make sure that Redis-based
+   * ACLs work properly with minimal fuss with regard to the module load
+   * order.
+   */
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisServer", FALSE);
+  if (c != NULL) {
+    const char *server, *password;
+    int port;
+
+    server = c->argv[0];
+    port = *((int *) c->argv[1]);
+    password = c->argv[2];
+
+    (void) redis_set_server(server, port, password);
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisTimeouts", FALSE);
+  if (c) {
+    unsigned long connect_millis, io_millis;
+
+    connect_millis = *((unsigned long *) c->argv[0]);
+    io_millis = *((unsigned long *) c->argv[1]);
+
+    (void) redis_set_timeouts(connect_millis, io_millis);
+  }
+
+  return 0;
+}
+
+/* Module API tables
+ */
+
+module wrap2_redis_module = {
+  NULL, NULL,
+
+  /* Module API version 2.0 */
+  0x20,
+
+  /* Module name */
+  "wrap2_redis",
+
+  /* Module configuration handler table */
+  NULL,
+
+  /* Module command handler table */
+  NULL,
+
+  /* Module authentication handler table */
+  NULL,
+
+  /* Module initialization function */
+  redistab_init,
+
+  /* Session initialization function */
+  redistab_sess_init,
+
+  /* Module version */
+  MOD_WRAP2_REDIS_VERSION
+};
diff --git a/contrib/mod_wrap2_sql.c b/contrib/mod_wrap2_sql.c
index 61fd149..eaf6ea7 100644
--- a/contrib/mod_wrap2_sql.c
+++ b/contrib/mod_wrap2_sql.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_wrap2_sql -- a mod_wrap2 sub-module for supplying IP-based
  *                           access control data via SQL tables
- *
- * Copyright (c) 2002-2012 TJ Saunders
+ * Copyright (c) 2002-2016 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
@@ -21,8 +20,6 @@
  * As a special exemption, TJ Saunders gives permission to link this program
  * with OpenSSL, and distribute the resulting executable, without including
  * the source code for OpenSSL in the source distribution.
- *
- * $Id: mod_wrap2_sql.c,v 1.12 2012-05-01 21:31:48 castaglia Exp $
  */
 
 #include "mod_wrap2.h"
@@ -36,10 +33,10 @@
 
 module wrap2_sql_module;
 
-static cmd_rec *sql_cmd_create(pool *parent_pool, int argc, ...) {
+static cmd_rec *sql_cmd_create(pool *parent_pool, unsigned int argc, ...) {
+  register unsigned int i = 0;
   pool *cmd_pool = NULL;
   cmd_rec *cmd = NULL;
-  register unsigned int i = 0;
   va_list argp;
 
   cmd_pool = make_sub_pool(parent_pool);
@@ -47,14 +44,15 @@ static cmd_rec *sql_cmd_create(pool *parent_pool, int argc, ...) {
   cmd->pool = cmd_pool;
 
   cmd->argc = argc;
-  cmd->argv = (char **) pcalloc(cmd->pool, argc * sizeof(char *));
+  cmd->argv = pcalloc(cmd->pool, argc * sizeof(void *));
 
   /* Hmmm... */
   cmd->tmp_pool = cmd->pool;
 
   va_start(argp, argc);
-  for (i = 0; i < argc; i++)
+  for (i = 0; i < argc; i++) {
     cmd->argv[i] = va_arg(argp, char *);
+  }
   va_end(argp);
 
   return cmd;
@@ -81,7 +79,8 @@ static array_header *sqltab_fetch_clients_cb(wrap2_table_t *sqltab,
   query = ((char **) sqltab->tab_data)[WRAP2_SQL_CLIENT_QUERY_IDX];
 
   /* Find the cmdtable for the sql_lookup command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     wrap2_log("error: unable to find SQL hook symbol 'sql_lookup': "
       "perhaps your proftpd.conf needs 'LoadModule mod_sql.c'?");
@@ -137,29 +136,32 @@ static array_header *sqltab_fetch_clients_cb(wrap2_table_t *sqltab,
 
     ptr = strpbrk(vals[i], ", \t");
     if (ptr != NULL) {
-      char *dup = pstrdup(sqltab->tab_pool, vals[i]);
-      char *word;
+      char *dup_opts, *word;
 
-      while ((word = pr_str_get_token(&dup, ", \t")) != NULL) {
+      dup_opts = pstrdup(sqltab->tab_pool, vals[i]);
+      while ((word = pr_str_get_token(&dup_opts, ", \t")) != NULL) {
         size_t wordlen;
 
         pr_signals_handle();
 
         wordlen = strlen(word);
-        if (wordlen == 0)
+        if (wordlen == 0) {
           continue;
+        }
 
         /* Remove any trailing comma */
-        if (word[wordlen-1] == ',')
+        if (word[wordlen-1] == ',') {
           word[wordlen-1] = '\0';
+          wordlen--;
+        }
 
         *((char **) push_array(clients_list)) = word;
 
         /* Skip redundant whitespaces */
-        while (*dup == ' ' ||
-               *dup == '\t') {
+        while (*dup_opts == ' ' ||
+               *dup_opts == '\t') {
           pr_signals_handle();
-          dup++;
+          dup_opts++;
         }
       }
 
@@ -205,7 +207,8 @@ static array_header *sqltab_fetch_options_cb(wrap2_table_t *sqltab,
   }
 
   /* Find the cmdtable for the sql_lookup command. */
-  sql_cmdtab = pr_stash_get_symbol(PR_SYM_HOOK, "sql_lookup", NULL, NULL);
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
+    NULL);
   if (sql_cmdtab == NULL) {
     wrap2_log("error: unable to find SQL hook symbol 'sql_lookup': "
       "perhaps your proftpd.conf needs 'LoadModule mod_sql.c'?");
@@ -267,13 +270,13 @@ static array_header *sqltab_fetch_options_cb(wrap2_table_t *sqltab,
   return options_list;
 }
 
-static wrap2_table_t *sqltab_open_cb(pool *parent_pool, char *srcinfo) {
+static wrap2_table_t *sqltab_open_cb(pool *parent_pool, const char *srcinfo) {
   wrap2_table_t *tab = NULL;
   pool *tab_pool = make_sub_pool(parent_pool),
     *tmp_pool = make_sub_pool(parent_pool);
   config_rec *c = NULL;
   char *start = NULL, *finish = NULL, *query = NULL, *clients_query = NULL,
-    *options_query = NULL;
+    *options_query = NULL, *info;
 
   tab = (wrap2_table_t *) pcalloc(tab_pool, sizeof(wrap2_table_t));
   tab->tab_pool = tab_pool;
@@ -286,7 +289,8 @@ static wrap2_table_t *sqltab_open_cb(pool *parent_pool, char *srcinfo) {
    *  "/<clients-named-query>[/<options-named-query>]"
    */
 
-  start = strchr(srcinfo, '/');
+  info = pstrdup(tmp_pool, srcinfo);
+  start = strchr(info, '/');
   if (start == NULL) {
     wrap2_log("error: badly formatted source info '%s'", srcinfo);
     destroy_pool(tab_pool);
@@ -297,9 +301,9 @@ static wrap2_table_t *sqltab_open_cb(pool *parent_pool, char *srcinfo) {
 
   /* Find the next slash. */
   finish = strchr(++start, '/');
-
-  if (finish)
+  if (finish != NULL) {
     *finish = '\0';
+  }
 
   clients_query = pstrdup(tab->tab_pool, start);
 
@@ -322,7 +326,7 @@ static wrap2_table_t *sqltab_open_cb(pool *parent_pool, char *srcinfo) {
   }
 
   /* Handle the options-query, if present. */
-  if (finish) {
+  if (finish != NULL) {
     options_query = pstrdup(tab->tab_pool, ++finish);
 
     query = pstrcat(tmp_pool, "SQLNamedQuery_", options_query, NULL);
@@ -338,7 +342,7 @@ static wrap2_table_t *sqltab_open_cb(pool *parent_pool, char *srcinfo) {
     }
   }
 
-  tab->tab_name = pstrcat(tab->tab_pool, "SQL(", srcinfo, ")", NULL);
+  tab->tab_name = pstrcat(tab->tab_pool, "SQL(", info, ")", NULL);
 
   tab->tab_data = pcalloc(tab->tab_pool, WRAP2_SQL_NSLOTS * sizeof(char *));
   ((char **) tab->tab_data)[WRAP2_SQL_CLIENT_QUERY_IDX] =
diff --git a/contrib/xferstats.holger-preiss b/contrib/xferstats.holger-preiss
index 6666954..4a5fe9f 100755
--- a/contrib/xferstats.holger-preiss
+++ b/contrib/xferstats.holger-preiss
@@ -61,13 +61,15 @@ $usage_file = "/var/log/xferlog";
 # Edit the following lines for default report settings.
 # Entries defined here will be over-ridden by the command line.
 
+$opt_a = 0;
 $opt_h = 0; 
 $opt_d = 0;
 $opt_t = 1;
 $opt_l = 3;
+$opt_r = 0;
 
-require 'getopts.pl';
-&Getopts('f:rahdD:l:s:A:iotu:e');
+use Getopt::Std;
+getopts('f:rahdD:l:s:A:iotu:e');
 
 if ($opt_r) { $real = 1;}
 if ($opt_a) { $anon = 1;}
@@ -209,21 +211,17 @@ line: while (<LOG>) {
 }
 close LOG;
 
- at syslist = keys(systemfiles);
- at dates = sort datecompare keys(xferbytes);
+ at syslist = keys(%systemfiles);
+ at dates = sort datecompare keys(%xferbytes);
 
 if ($xferfiles == 0) {die "There was no data to process.\n";}
 
-
 print "TOTALS FOR SUMMARY PERIOD ", $dates[0], " TO ", $dates[$#dates], "\n\n";
 printf ("Files Transmitted During Summary Period  %12.0f\n", $xferfiles);
 printf ("Bytes Transmitted During Summary Period  %12.0f\n", $xferbytes); 
 printf ("Systems Using Archives                   %12.0f\n\n", $#syslist+1);
-
-printf ("Average Files Transmitted Daily          %12.0f\n",
-   $xferfiles / ($#dates + 1));
-printf ("Average Bytes Transmitted Daily          %12.0f\n",
-   $xferbytes / ($#dates + 1));
+printf ("Average Files Transmitted Daily          %12.0f\n", $xferfiles / ($#dates + 1));
+printf ("Average Bytes Transmitted Daily          %12.0f\n", $xferbytes / ($#dates + 1));
 
 format top1 =
 
@@ -242,9 +240,8 @@ $date,           $nfiles,    $nbytes,     $avgrate,   $pctfiles,  $pctbytes
 $^ = top1;
 $~ = line1;
 
-# sort daily traffic by bytes sendt
-#foreach $date ( sort datecompare keys(nbytes) ) {
-foreach $date ( sort datecompare keys(xferbytes) ) {
+# sort daily traffic by bytes sent
+foreach $date (sort datecompare keys(%xferbytes)) {
 
    $nfiles = $xferfiles{$date};
    $nbytes = $xferbytes{$date};
@@ -263,14 +260,14 @@ format top2 =
 
 Total Transfers from each Archive Section (By bytes)
 
-                                                 ---- Percent  Of ----
-     Archive Section      Files Sent Bytes Sent  Files Sent Bytes Sent
-------------------------- ---------- ----------- ---------- ----------
+                                                   ---- Percent  Of ----
+     Archive Section      Files Sent Bytes Sent    Files Sent Bytes Sent
+------------------------- ---------- ------------- ---------- ----------
 .
 
 format line2 =
-@<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>> @>>>>>>>>>> @>>>>>>>   @>>>>>>>
-$section,                 $files,    $bytes,     $pctfiles, $pctbytes
+@<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>> @>>>>>>>>>>>> @>>>>>>>   @>>>>>>>
+$section,                 $files,    $bytes,       $pctfiles, $pctbytes
 .
 
 $| = 1;
@@ -279,7 +276,7 @@ $^ = top2;
 $~ = line2;
 
 # sort total transfer for each archive by # files transfered
-foreach $section ( sort bytecompare keys(groupfiles) ) {
+foreach $section (sort bytecompare keys(%groupfiles)) {
 
    $files = $groupfiles{$section};
    $bytes = $groupbytes{$section};
@@ -311,7 +308,7 @@ $^ = top3;
 $~ = line3;
 
 # sort amount per domain by files
-foreach $domain ( sort domnamcompare keys(domainfiles) ) {
+foreach $domain (sort domnamcompare keys(%domainfiles)) {
 
    $files = $domainfiles{$domain};
    $bytes = $domainbytes{$domain};
@@ -353,7 +350,7 @@ $^ = top8;
 $~ = line8;
 
 # sort hourly transmission by sent bytes
-foreach $time ( sort keys(xfertbytes) ) {
+foreach $time (sort keys(%xfertbytes)) {
 
    $nfiles   = $xfertfiles{$time};
    $nbytes   = $xfertbytes{$time};
diff --git a/doc/contrib/ftpasswd.html b/doc/contrib/ftpasswd.html
index 3e4fe88..b0f8093 100644
--- a/doc/contrib/ftpasswd.html
+++ b/doc/contrib/ftpasswd.html
@@ -1,6 +1,4 @@
-<!-- $Id: ftpasswd.html,v 1.3 2013-12-08 17:06:12 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/ftpasswd.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ftpasswd: tool for ProFTPD's AuthUserFile, AuthGroupFile, UserPassword </title>
@@ -14,16 +12,15 @@
 </center>
 <hr><br>
 
+<p>
 This program is used to create and manage files, correctly formatted, suitable
 for use with ProFTPD's <a href="http://www.proftpd.org/docs/configuration.html#AuthUserFile"><code>AuthUserFile</code></a> and <a href="http://www.proftpd.org/docs/configuration.html#AuthGroupFile"><code>AuthGroupFile</code></a>
 configuration directives.  It can also generate password hashes for ProFTPD's
 <a href="http://www.proftpd.org/docs/configuration.html#UserPassword"><code>UserPassword</code></a> directive.
 
 <p>
-The most current version of <code>ftpasswd</code> can be found at:
-<pre>
-  <a href="http://www.castaglia.org/proftpd/">http://www.castaglia.org/proftpd/</a>
-</pre>
+The most current version of <code>ftpasswd</code> is distributed with the
+ProFTPD source code.
 
 <h2>Author</h2>
 <p>
@@ -353,19 +350,12 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-12-08 17:06:12 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2012 TJ Saunders<br>
+© Copyright 2000-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/contrib/ftpmail.html b/doc/contrib/ftpmail.html
index f7be70d..873b92f 100644
--- a/doc/contrib/ftpmail.html
+++ b/doc/contrib/ftpmail.html
@@ -1,6 +1,4 @@
-<!-- $Id: ftpmail.html,v 1.4 2013-02-27 05:40:07 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/ftpmail.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ftpmail: Automated Email Notifications of Uploads</title>
@@ -41,8 +39,8 @@ First, you have the following Perl modules installed:
 </ul>
 You can easily determine if your Perl has these modules present, using:
 <pre>
-  # perl -MMail::Sendmail -e0
-  # perl -MTime::HiRes -e0
+  $ perl -MMail::Sendmail -e0
+  $ perl -MTime::HiRes -e0
 </pre>
 If you see an error similar to:
 <pre>
@@ -62,13 +60,13 @@ for this.
 After that, you need to start <code>ftpmail</code> running, <i>before</i>
 starting <code>proftpd</code>.  For example, you might do:
 <pre>
-   # ./ftpmail \
+   $ ./ftpmail \
        --fifo=/var/proftpd/log/transfer.fifo \
        --from='tj at castaglia.org' \
        --recipient='tj at castaglia.org' \
        --smtp-server=mail.domain.com \
        --attach-file \
-       --log=/var/proftpd/log/transfer.log &
+       --log=/var/proftpd/log/transfer.log &
 </pre>
 The key is to make <code>ftpmail</code> run in the background, so that it is
 constantly running.  If the <code>ftpmail</code> process dies, then
@@ -91,10 +89,10 @@ the <code>proftpd</code> daemon was started.
 
 <p>
 <b>Options</b><br>
-The following shows the full list of <code>ftpmail<code> options; this
+The following shows the full list of <code>ftpmail</code> options; this
 can also be obtained by running:
 <pre>
-  # ftpmail --help
+  $ ftpmail --help
 
   usage: ftpmail [--help] [--fifo $path] [--from $addr] [--log $path]
     [--recipient $addrs] [--upload-recipient \$addrs]
@@ -206,19 +204,12 @@ Then start instances of <code>ftpmail</code> running, but only for the
 <code>TransferLog</code> files of the domains/virtual servers to be monitored.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-02-27 05:40:07 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2008-2013 TJ Saunders<br>
+© Copyright 2008-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/contrib/ftpquota.html b/doc/contrib/ftpquota.html
index 468e5e4..1ff13cb 100644
--- a/doc/contrib/ftpquota.html
+++ b/doc/contrib/ftpquota.html
@@ -1,6 +1,4 @@
-<!-- $Id: ftpquota.html,v 1.1 2004-11-03 19:37:44 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/ftpquota.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ftpquota: tool for ProFTPD mod_quotatab</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This program is distributed with the
 <a href="./mod_quotatab.html"><code>mod_quotatab</code></a> module for
 ProFTPD 1.2.  It is used to create and manage quota tables of type
@@ -42,18 +41,18 @@ Tables of type "file" have identifying magic headers such that
 <code>ftpquota</code> is needed to initially create both tables.  It is an
 error to attempt to create a table that already exists:
 <pre>
-  # ftpquota --create-table --type=limit
+  $ ftpquota --create-table --type=limit
   ftpquota: cannot create existing table
 </pre>
 
 <p>
 To create the initial limit table:
 <pre>
-  ftpquota --create-table --type=limit
+  $ ftpquota --create-table --type=limit
 </pre>
 To create the initial tally table:
 <pre>
-  ftpquota --create-table --type=tally
+  $ ftpquota --create-table --type=tally
 </pre>
 The population of a tally table file with the tally for <code>user</code>s,
 <code>group</code>s, <code>class</code>es, and <code>all</code> is done
@@ -73,7 +72,7 @@ you, as the administrator, wish to impose quotas.
 To a basic limit record for a user <code>bob</code> using the default quotas
 (which are all "unlimited" by default):
 <pre>
-  ftpquota --add-record --type=limit --name=bob --quota-type=user
+  $ ftpquota --add-record --type=limit --name=bob --quota-type=user
 </pre>
 Similarly for quotas of type <code>group</code> or <code>class.</code>  Note
 that for quotas of type <code>all</code>, the <code>--name</code> option
@@ -92,12 +91,12 @@ combination of name and quota type.
 An example of creating a limit record with more complex quotas for a class
 named "browsers"
 <pre>
-  ftpquota --add-record --type=limit --name=browsers --quota-type=class \
+  $ ftpquota --add-record --type=limit --name=browsers --quota-type=class \
     --files-upload=0  --files-download=25 --table-path=/usr/local/etc/ftplimit.tab
 </pre>
 or, another way:
 <pre>
-  ftpquota --add-record --type=limit --name=browsers --quota-type=class \
+  $ ftpquota --add-record --type=limit --name=browsers --quota-type=class \
     --bytes-upload=0 --bytes-download=2 --units=Gb --table-path=/usr/local/etc/ftplimit.tab
 </pre>
 In general, it is a good idea to use either byte quotas or file quotas, but
@@ -121,7 +120,7 @@ tally table (effectively resetting the quotas to zero for that
 <code>user/group/class/all</code>), there is the
 <code>--delete-record</code> option:
 <pre>
-  ftpquota --delete-record --type=limit --name=browsers --quota-type=class
+  $ ftpquota --delete-record --type=limit --name=browsers --quota-type=class
 </pre>
 
 <p>
@@ -129,7 +128,7 @@ In addition to adding and removing records, an administrator may want to
 simply change the quotas in a given record, either in the limit or tally
 tables.  In this case, use <code>--update-record</code>:
 <pre>
-  ftpquota --update-record --type=tally --name=bob --quota-type=user \
+  $ ftpquota --update-record --type=tally --name=bob --quota-type=user \
     --bytes-download=100 --files-download=1
 </pre>
 This example command manually sets user <code>bob</code>'s download tally to
@@ -140,7 +139,7 @@ There are some administrators who wish to impose quotas on a daily basis, that
 is, to have a per-day quota.  At present, the easiest way to do this is to
 have a cron job that, once a day, does the following:
 <pre>
-  ftpquota --update-record --type=tally --name=<i>name</i> --quota-type=<i>quotatype</i>
+  $ ftpquota --update-record --type=tally --name=<i>name</i> --quota-type=<i>quotatype</i>
 </pre>
 This command will "reset" the matching tally record.  By default, if
 no byte or file quotas are set, the default values are used: <b>unlimited</b>
@@ -162,12 +161,12 @@ current tally.  <code>ftpquota</code> thus supports the dumping out of
 table contents in human-legible form via the <code>--show-records</code>
 option:
 <pre>
-  ftpquota --show-records --type=limit
+  $ ftpquota --show-records --type=limit
 </pre>
 or, for a tally table not in the location where the scripts expects it to
 be:
 <pre>
-  ftpquota --show-records --type=tally --table-path=/usr/local/etc/ftpd/quota-tally.tab
+  $ ftpquota --show-records --type=tally --table-path=/usr/local/etc/ftpd/quota-tally.tab
 </pre>
 When viewing tables this way, the <code>--units</code> option can be used
 to display byte quotas in other units (<i>e.g.</i> Kb, Mb, etc).
@@ -268,20 +267,12 @@ usage: ftpquota [options]
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2004-11-03 19:37:44 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2000-2002 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/index.html b/doc/contrib/index.html
index 301d6cd..caaee32 100644
--- a/doc/contrib/index.html
+++ b/doc/contrib/index.html
@@ -1,6 +1,4 @@
-<!-- $Id: index.html,v 1.15 2013-09-18 21:51:36 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/index.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD Contrib Module Documentation</title>
@@ -35,6 +33,12 @@ distribution.
   </dd>
 
   <p>
+  <dt>The <a href="mod_auth_otp.html"><code>mod_ban</code></a> module
+  <dd>For supporting HOTP/TOTP One-Time Passwords (OTP) for multi-factor
+      authentication, <i>e.g.</i> using Google Authenticator
+  </dd>
+
+  <p>
   <dt>The <a href="mod_ban.html"><code>mod_ban</code></a> module
   <dd>For supporting automatic bans based on configurable thresholds
   </dd>
@@ -52,6 +56,11 @@ distribution.
   </dd>
 
   <p>
+  <dt>The <a href="mod_digest.html"><code>mod_digest</code></a> module
+  <dd>For suppporting FTP <code>HASH</code> commands for files
+  </dd>
+
+  <p>
   <dt>The <a href="mod_dnsbl.html"><code>mod_dnsbl</code></a> module
   <dd>For using DNS blacklists for access control
   </dd>
@@ -187,6 +196,12 @@ distribution.
   </dd>
 
   <p>
+  <dt>The <a href="mod_statcache.html"><code>mod_statcache</code></a> module
+  <dd>Supports caching <code>stat(2)</code>/<code>lstat(2)</code> data in a
+      shared location, for reuse across sessions/processes.
+  </dd>
+
+  <p>
   <dt>The <a href="mod_tls.html"><code>mod_tls</code></a> module
   <dd>Adds the ability to encrypt the control and data connections using
       SSL/TLS
@@ -232,6 +247,13 @@ distribution.
   </dd>
 
   <p>
+  <dt>The <a href="mod_wrap2_redis.html"><code>mod_wrap2_redis</code></a>
+      module
+  <dd>This is one of the supporting modules for <code>mod_wrap2</code>
+      which handles access rules that are stored in Redis lists/sets
+  </dd>
+
+  <p>
   <dt>The <a href="mod_wrap2_sql.html"><code>mod_wrap2_sql</code></a>
       module
   <dd>This is one of the supporting modules for <code>mod_wrap2</code>
@@ -255,7 +277,10 @@ all of the directives to see everything that ProFTPD is capable of supporting.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-09-18 21:51:36 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/contrib/mod_auth_otp.html b/doc/contrib/mod_auth_otp.html
new file mode 100644
index 0000000..ffe5355
--- /dev/null
+++ b/doc/contrib/mod_auth_otp.html
@@ -0,0 +1,469 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_auth_otp</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_auth_otp</code></b></h2>
+</center>
+<hr><br>
+
+<p>
+The purpose of the <code>mod_auth_otp</code> module is to enable the use
+of <em>one-time-passwords (OTP)</em> for proftpd authentication.  There have
+been multiple different OTP algorithms devised over the years; this module
+implements the HOTP and TOTP algorithms.  Note that <code>mod_auth_otp</code>
+requires storage/retrieval for per-user shared keys and counters, and thus
+this module currently <b>requires mod_sql</b>.
+
+<p>
+<b>One-Time Password RFCs</b><br>
+For those wishing to learn more about these one-time password algorithms, see:
+<ul>
+  <li><a href="http://www.faqs.org/rfcs/rfc4226.html">HOTP: An HMAC-Based One-Time Password Algorithm (RFC 4226)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc6238.html">TOTP: Time-Based One-Time Password Algorithm(RFC 6238)</a>
+</ul>
+Note that <a href="http://en.wikipedia.org/wiki/Google_Authenticator"><code>Google Authenticator</code></a> is based on the TOTP algorithm;
+<code>mod_auth_otp</code> thus enables use of Google Authenticator for ProFTPD
+authentication.
+
+<p>
+Installation instructions are discussed <a href="#Installation">here</a>;
+detailed notes on best practices for using this module are
+<a href="#Usage">here</a>.
+
+<p>
+The most current version of <code>mod_auth_otp</code> is distributed with the
+ProFTPD source code.
+
+<p>
+This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/).
+
+<p>
+This product includes cryptographic software written by Eric Young (eay at cryptsoft.com).
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
+questions, concerns, or suggestions regarding this module.
+
+<h2>Directives</h2>
+<ul>
+  <li><a href="#AuthOTPAlgorithm">AuthOTPAlgorithm</a>
+  <li><a href="#AuthOTPEngine">AuthOTPEngine</a>
+  <li><a href="#AuthOTPLog">AuthOTPLog</a>
+  <li><a href="#AuthOTPOptions">AuthOTPOptions</a>
+  <li><a href="#AuthOTPTable">AuthOTPTable</a>
+  <li><a href="#AuthOTPTableLock">AuthOTPTableLock</a>
+</ul>
+
+<p>
+<hr>
+<h3><a name="AuthOTPAlgorithm">AuthOTPAlgorithm</a></h3>
+<strong>Syntax:</strong> AuthOTPAlgorithm <em>hotp|totp</em><br>
+<strong>Default:</strong> AuthOTPAlgorithm totp<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth_otp<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>AuthOTPAlgorithm</code> directive configures which one-time password
+algorithm will be used when calculating codes for connections to the virtual
+host.
+
+<p>
+The supported algorithm names are:
+<ul>
+  <li><code>hotp</code> (counter-based codes)
+  <li><code>totp</code> (time-based codes, using HMAC-SHA1)
+  <li><code>totp-sha256</code> (time-based codes, using HMAC-SHA256)
+  <li><code>totp-sha512</code> (time-based codes, using HMAC-SHA512)
+</ul>
+The default algorithm is "totp".
+
+<p>
+<hr>
+<h3><a name="AuthOTPEngine">AuthOTPEngine</a></h3>
+<strong>Syntax:</strong> AuthOTPEngine <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth_otp<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>AuthOTPEngine</code> directive enables the handling of one-time
+password codes for authentication, both for FTP/FTPS as well as SFTP/SCP
+sessions.  By default, use of one-time passwords is disabled.
+
+<p>
+<hr>
+<h3><a name="AuthOTPLog">AuthOTPLog</a></h3>
+<strong>Syntax:</strong> AuthOTPLog <em>path|"none"</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth_otp<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>AuthOTPLog</code> directive is used to specify a log file for
+<code>mod_auth_otp</code>'s reporting on a per-server basis.  The <em>path</em>
+parameter given must be the full path to the file to use for logging.
+
+<p>
+Note that this path must <b>not</b> be to a world-writable directory and,
+unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
+(generally a bad idea), the path must <b>not</b> be a symbolic link.
+
+<p>
+<hr>
+<h3><a name="AuthOTPOptions">AuthOTPOptions</a></h3>
+<strong>Syntax:</strong> AuthOTPOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth_otp<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>AuthOTPOptions</code> directive is used to configure various optional
+behavior of <code>mod_auth_otp</code>.
+
+<p>
+For example:
+<pre>
+  AuthOTPOptions StandardResponse
+</pre>
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>DisplayVerificationCode</code><br>
+    When <code>mod_auth_otp</code> prompts the user for the OTP code, it
+    requests that the client <b>not</b> echo/display the verification code
+    as it is entered by the user.  In some cases, however, administrators
+    may wish to have the OTP code be displayed.  For these situations, use
+    this option, <i>e.g.</i>:
+    <pre>
+      AuthOTPOptions DisplayVerificationCode
+    </pre>
+  </li>
+
+  <p>
+  <li><code>RequireTableEntry</code><br>
+    <p>
+    When <code>mod_auth_otp</code> requests information for a user from the
+    <code>AuthOTPTable</code> and no information is found, it will normally
+    allow other auth modules to handle the login attempt, <b>even if
+    <code>mod_auth_otp</code> is authoritative</b>.  This behavior allows
+    for a seemless transition of your user base, provisioning users with
+    shared keys/secrets for their one-time passwords as time allows.
+
+    <p>
+    However, there may be sites which <b>require</b> the use of one-time
+    passwords; any login attempt which does not use a valid one-time
+    password <b>must</b> be rejected.  Thus the lack of an entry for a user
+    in the <code>AuthOTPTable</code> is, for this policy, a fatal error and
+    should be handled as such.  For this kind of very secure configuration,
+    use this option, in conjunction with the <code>AuthOrder</code>
+    directive, <i>e.g.</i>:
+    <pre>
+      AuthOrder mod_auth_otp.c* ...
+      AuthOTPOptions RequireTableEntry StandardResponse
+    </pre>
+  </li>
+
+  <p>
+  <li><code>StandardResponse</code><br>
+    <p>
+    When <code>mod_auth_otp</code> is handling FTP sessions, it will respond
+    to a <code>USER</code> command with a response message indicating the
+    expectation of a one-time password:
+    <pre>
+      331 One-time password required for <i>user</i>
+    </pre>
+    However, this change of the response message "leaks" information about
+    the server configuration, <i>i.e.</i> that OTPs will be used.  To
+    tell <code>mod_auth_otp</code> to continue using the standard/normal
+    response message, use this option.
+  </li>
+</ul>
+
+<p>
+<hr>
+<h3><a name="AuthOTPTable">AuthOTPTable</a></h3>
+<strong>Syntax:</strong> AuthOTPTable <em>table-info</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth_otp<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>AuthOTPTable</code> directive configures the information necessary
+for <code>mod_auth_otp</code> to retrieve the shared key/secret and current
+counter, on a per-user basis; this directive is <b>required</b> for
+<code>mod_auth_otp</code> to function.  If <code>AuthOTPTable</code> is
+<b>not configured</b>, <code>mod_auth_otp</code> will refuse to work.
+
+<p>
+The <code>mod_auth_otp</code> module currently expects/uses SQL tables for
+retrieval/storage of its data on a per-user basis.  Thus the
+<code>AuthOTPTable</code> directives requires two separate
+<code>SQLNamedQuery</code> directives: one for looking up the needed data,
+the other for updating that data.  The <em>table-info</em> parameter
+encodes these <code>SQLNamedQuery</code> names like so:
+<pre>
+  SQLNamedQuery get-user-totp SELECT ...
+  SQLNamedQuery update-user-totp UPDATE ...
+
+  AuthOTPTable sql:/get-user-totp/update-user-totp
+</pre>
+See the <a href="#Usage">usage</a> section for a more detailed example.
+
+<p>
+<hr>
+<h3><a name="AuthOTPTableLock">AuthOTPTableLock</a></h3>
+<strong>Syntax:</strong> AuthOTPTableLock <em>path</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth_otp<br>
+<strong>Compatibility:</strong> 1.3.5rc4 and later
+
+<p>
+The <code>AuthOTPTableLock</code> directive sets the <em>path</em> for a
+synchronization lockfile which <code>mod_auth_otp</code> needs when updating
+the <code>AuthOTPTable</code> for <i>e.g.</i> counter-based codes.  Use of
+<code>AuthOTPTableLock</code> is recommended, but not required.
+
+<p>
+If <code>AuthOTPTableLock</code> is used, it is <b>strongly advised</b> that
+the configured <em>path</em> <b>not</b> be on an NFS (or any other network)
+filesystem.
+
+<p>
+<hr>
+<h2><a name="Usage">Usage</a></h2>
+
+<p>
+Note that the following examples assume the existing of an SQL table whose
+schema looks like the following (using the SQLite schema syntax):
+<pre>
+  CREATE TABLE auth_otp (
+    user TEXT PRIMARY KEY,
+    secret TEXT,
+    counter INTEGER
+  );
+</pre>
+
+The <code>auth_otp.secret</code> column <b>must</b> contain the
+<b>base32-encoded</b> shared key for the user.  Why Base32-encoding?  That
+is what Google Authenticator expects/uses for its shared key storage; its
+<code>google-authenticator</code> command-line tool generates a Base32-encoded
+string for entering into the Google Authenticator app on your mobile device.
+
+<p>
+To get the base32-encoded shared key using <code>google-authenticator</code>:
+<pre>
+  $ ./google-authenticator
+  Do you want authentication tokens to be time-based (y/n) y
+  <em>Here you will see generated QR code</em>
+  Your new secret key is: <em>base32-encoded secret here</em>
+  ...
+</pre>
+
+<p>
+<b>Example Time-based (TOTP) Configuration</b><br>
+<pre>
+  <IfModule mod_auth_otp.c>
+    AuthOTPEngine on
+
+    # Use time-based codes (TOTP)
+    AuthOTPAlgorithm totp
+
+    AuthOTPTable sql:/get-user-totp/update-user-totp
+  </IfModule>
+
+  <IfModule mod_sql.c>
+    ...
+
+    # Notice that for time-based counters, we do <b>not</b> need to retrieve
+    # the auth_otp.counter column; the counter value is determined from the
+    # system clock.
+    SQLNamedQuery get-user-totp SELECT "secret FROM auth_otp WHERE user = \'%{0}\'"
+    SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp
+  </IfModule>
+</pre>
+
+<p>
+<b>Example Counter-based (HOTP) Configuration</b><br>
+<pre>
+  <IfModule mod_auth_otp.c>
+    AuthOTPEngine on
+
+    # Use counter-based codes (HOTP)
+    AuthOTPAlgorithm hotp
+
+    AuthOTPTable sql:/get-user-hotp/update-user-hotp
+  </IfModule>
+
+  <IfModule mod_sql.c>
+    ...
+    SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"
+    SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp
+  </IfModule>
+</pre>
+
+<p>
+<b>Secure/Paranoid Configurations</b><br>
+Security-conscious adminstrators may not want users to notice if/when they
+have started expecting one-time passwords for their logins; not all users may
+have been provisioned with the necessary shared key.  To prevent
+<code>mod_auth_otp</code> from "leaking" its presence/usage and instead to
+continue using the standard FTP response messages, use the following in
+your configuration for <code>mod_auth_otp</code>:
+<pre>
+  AuthOTPOption StandardResponse
+</pre>
+
+<p>
+If, on the other hand, you have successfully provisioned <b>all</b> of your
+users with OTP shared keys, and now <b>require</b> that all logins use a
+one-time password (but still want to <b>not</b> leak this information), then
+you would use:
+<pre>
+  # Make mod_auth_otp authoritative; if it fails to handle a login attempt,
+  # that login attempt MUST fail.
+  AuthOrder mod_auth_otp.c* ...
+
+  # Use the standard FTP response message, and fail the login if we find
+  # a user that has not been provisioned.
+  AuthOTPOptions RequireTableEntry StandardResponse
+</pre>
+
+<p>
+<b>SFTP/SCP Support</b><br>
+One-time passwords can also be used for <code>mod_sftp</code> sessions,
+<i>i.e.</i> for SFTP and SCP clients.  The SSH RFCs define any non-standard
+"password-like" authentication method as "keyboard-interactive".  Thus to
+use <code>mod_auth_otp</code> for your SFTP connections, simply include
+both <code>mod_sftp</code> and <code>mod_auth_otp</code> in your build.  That's
+it.
+
+<p>
+Now, if you want <code>mod_sftp</code> to <b>only</b> try to use one-time
+passwords (or public keys), and <b>not</b> normal passwords, then you might
+use a <code>mod_sftp</code> configuration like this:
+<pre>
+  SFTPAuthMethods publickey keyboard-interactive
+</pre>
+
+<p>
+<b>Module Load Order and <code>mod_sftp</code></b><br>
+In order for <code>mod_auth_otp</code> to work its magic, it <b>must</b>
+come <b>after</b> the <code>mod_sftp</code> module in the module load order.
+To do this as a static module, you would use something like this when building
+proftpd:
+<pre>
+  $ ./configure --with-modules=...:mod_sftp:mod_auth_otp:...
+</pre>
+ensuring that <code>mod_auth_otp</code> comes after <code>mod_sftp</code> in
+your <code>--with-modules</code> list.
+
+<p>
+As a shared module, configuring <code>mod_auth_otp</code> to be after
+<code>mod_sftp</code> is much easier.  Your configuration will have a list
+of <code>LoadModule</code> directives; make sure <code>mod_auth_otp</code>
+appears after <code>mod_sftp</code>:
+<pre>
+  LoadModule mod_sftp.c
+  ...
+  LoadModule mod_auth_otp.c
+  ...
+</pre>
+You will know if the module load ordering is wrong if you see the following
+log message appear in your logs:
+<pre>
+  proftpd[87129]: mod_auth_otp/0.0: mod_sftp not loaded, skipping keyboard-interactive support
+</pre>
+
+<p>
+<b>Logging</b><br>
+The <code>mod_auth_otp</code> module supports different forms of logging.  The
+main module logging is done via the <code>AuthOTPLog</code> directive.  This
+log is used for successes/failures.  For example, if the user provides an OTP
+code, but that user is not configured in the <code>AuthOTPTable</code>, you
+would see a log message such as:
+<pre>
+  2016-01-18 12:35:46,725 mod_auth_otp/0.2[27192]: user 'foobar' has no OTP info in AuthOTPTable
+  2016-01-18 12:36:47,152 mod_auth_otp/0.2[27192]: FAILED: user 'foobar' provided invalid OTP code
+</pre>
+If the user <em>is</em> provisioned in the <code>AuthOTPTable</code>, but the
+OTP code is invalid, you would see <em>just</em> this message:
+<pre>
+  2016-01-18 12:40:09,500 mod_auth_otp/0.2[27235]: FAILED: user 'foobar' provided invalid OTP code
+</pre>
+And finally, for valid OTP codes, the following is logged:
+<pre>
+  2016-01-18 12:42:40,115 mod_auth_otp/0.2[27484]: SUCCESS: user 'foobar' provided valid OTP code
+</pre>
+
+<p>
+For debugging purposes, the module also uses
+<a href="http://www.proftpd.org/docs/howto/Tracing.html">trace logging</a>,
+via the module-specific channels:
+<ul>
+  <li>auth_otp
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/auth-trace.log
+  Trace auth_otp:20
+</pre>
+This trace logging can generate large files; it is intended for debugging
+use only, and should be removed from any production configuration.
+
+<p>
+<b>Suggested Future Features</b><br>
+The following lists the features I hope to add to <code>mod_auth_otp</code>,
+according to need, demand, inclination, and time:
+<ul>
+  <li>Configurable number of digits in the expected code (currently hardcoded as 6)
+  <li>Support "emergency recovery" codes
+  <li>Support resynchronization with clients
+</ul>
+
+<p><a name="FAQ">
+<b>Frequently Asked Questions</b><br>
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_auth_otp</code> module is distributed with ProFTPD.  Simply follow
+the normal steps for using third-party modules in ProFTPD.  For including
+<code>mod_auth_otp</code> as a staticly linked module:
+<pre>
+  $ ./configure --enable-openssl --with-modules=mod_sql:mod_sql_sqlite:mod_auth_otp:...
+</pre>
+To build <code>mod_auth_otp</code> as a DSO module:
+<pre>
+  $ ./configure --enable-dso --enable-openssl --with-shared=mod_auth_otp:...
+</pre>
+Then follow the usual steps:
+<pre>
+  $ make
+  $ make install
+</pre>
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2015-2016 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/contrib/mod_ban.html b/doc/contrib/mod_ban.html
index a9df1df..aba4db5 100644
--- a/doc/contrib/mod_ban.html
+++ b/doc/contrib/mod_ban.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ban.html,v 1.21 2014-02-23 17:11:36 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_ban.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ban</title>
@@ -75,6 +73,44 @@ questions, concerns, or suggestions regarding this module.
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
 <p>
+The <code>BanCacheOptions</code> directive is used to configure various optional
+caching behavior of <code>mod_ban</code>.  <b>Note</b>: all of the configured
+<code>BanCacheOptions</code> parameters <b>must</b> appear on the same line in
+the configuration; only the first <code>BanCacheOptions</code> directive that
+appears in the configuration is used.
+
+<p>
+Example:
+<pre>
+  BanCacheOptions UseJSON
+</pre>
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>MatchServer</code><br>
+    <p>
+    This option tells <code>mod_ban</code> to ignore cached entries unless
+    they <b>match</b> the IP address/port of the server handling the FTP
+    session.  There might be other servers adding their own entries to
+    a common cache, for example; it is for these situations that the
+    <code>MatchServer</code> option should be used, to avoid false positives.
+  </li>
+
+  <p>
+  <li><code>UseJSON</code><br>
+    <p>
+    This option tells <code>mod_ban</code> to store the ban rules in the
+    cache as JSON-formatted data.  This makes it possible for other tools
+    to discover/display the cached ban rules.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc2</code>.
+  </li>
+</ul>
+
+<p>
 <hr>
 <h3><a name="BanControlsACLs">BanControlsACLs</a></h3>
 <strong>Syntax:</strong> BanControlsACLs <em>actions|"all" "allow"|"deny" "user"|"group" list</em><br>
@@ -183,7 +219,7 @@ The <code>BanOnEvent</code> directive is used to configure a "rule"
 that is triggered whenever the named <em>event</em> occurs.  The currently
 supported events are:
 <p>
-<table border=1>
+<table border=1 summary="Ban Events">
   <tr>
     <td><b>Directive</b></td>
     <td><b>Class/Host/User Ban</b></td>
@@ -195,11 +231,21 @@ supported events are:
   </tr>
 
   <tr>
+    <td><code>BadProtocol</code></td>
+    <td>Host ban</td>
+  </tr>
+
+  <tr>
     <td><code>ClientConnectRate</code></td>
     <td>Host ban</td>
   </tr>
 
   <tr>
+    <td><code>EmptyPassword</code></td>
+    <td>Host ban</td>
+  </tr>
+
+  <tr>
     <td><code>MaxClientsPerClass</code></td>
     <td>Class ban</td>
   </tr>
@@ -278,12 +324,12 @@ specifies a number of hours, minutes, and seconds.  This parameter says
 that if <i>N</i> occurrences of <em>event</em> happen within the given
 time interval, then a ban is automatically added.  The IP address of
 the connecting client is banned when the following event rules are
-triggered: <code>AnonRejectPasswords</code>, <code>MaxCommandRate</code>,
-<code>MaxClientsPerHost</code>, <code>MaxConnectionsPerHost</code>,
-<code>MaxLoginAttempts</code>, <code>TimeoutIdle</code>,
-<code>TimeoutNoTransfer</code>, and <code>UnhandledCommand</code>.  The class
-of the connected client, if any, is banned when a rule for
-<code>MaxClientsPerClass</code> is triggered.  Rules for
+triggered: <code>AnonRejectPasswords</code>, <code>BadProtocol</code>,
+<code>MaxCommandRate</code>, <code>MaxClientsPerHost</code>,
+<code>MaxConnectionsPerHost</code>, <code>MaxLoginAttempts</code>,
+<code>TimeoutIdle</code>, <code>TimeoutNoTransfer</code>, and
+<code>UnhandledCommand</code>.  The class of the connected client, if any, is
+banned when a rule for <code>MaxClientsPerClass</code> is triggered.  Rules for
 <code>MaxClientsPerUser</code> and <code>MaxHostsPerUser</code> will cause
 the connected username to be banned.
 
@@ -437,26 +483,21 @@ See also: <a href="#ban"><code>ban</code></a>
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_ban</code>, copy the <code>mod_ban.c</code> file
-into:
+The <code>mod_ban</code> module is distributed with ProFTPD.  Simply follow
+the normal steps for using third-party modules in ProFTPD, making sure to
+include the <code>--enable-ctrls</code> configure option, which
+<code>mod_ban</code> requires:
 <pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.2.<i>x</i> source code.  Then follow the
-usual steps for using third-party modules in proftpd, making sure to include
-the <code>--enable-ctrls</code> configure option, which <code>mod_ban</code>
-requires:
-<pre>
-  ./configure --enable-ctrls --with-modules=mod_ban
+  $ ./configure --enable-ctrls --with-modules=mod_ban
 </pre>
 To build <code>mod_ban</code> as a DSO module:
 <pre>
-  ./configure --enable-ctrls --enable-dso --with-shared=mod_ban
+  $ ./configure --enable-ctrls --enable-dso --with-shared=mod_ban
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -464,7 +505,7 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_ban</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_ban.c
+  $ prxs -c -i -d mod_ban.c
 </pre>
 
 <p>
@@ -486,7 +527,7 @@ to configure an automatic ban for <code>MaxLoginAttempts</code>:
   <IfModule mod_ban.c>
     BanEngine on
     BanLog /var/log/proftpd/ban.log
-    BanTable /var/data//proftpd/ban.tab
+    BanTable /var/data/proftpd/ban.tab
 
     # If the same client reaches the MaxLoginAttempts limit 2 times
     # within 10 minutes, automatically add a ban for that client that
@@ -508,6 +549,21 @@ space for more bans, you will need to recompile proftpd, and use the
 </pre>
 or whatever your necessary ban list size is.
 
+<p>
+<b>Logging</b><br>
+The <code>mod_ban</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>ban
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace ban:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ"></a>
 <b>Frequently Asked Questions</b><br>
 <font color=red>Question</font>: Why does <code>mod_ban</code> not store ban
@@ -624,16 +680,10 @@ login as root.  How would I do this?<br>
   BanOnEvent mod_auth.root-login 2/00:10:00 06:00:00
 </pre>
 
-<p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-02-23 17:11:36 $</i><br>
-
 <br><hr>
 
 <font size=2><b><i>
-© Copyright 2004-2014 TJ Saunders<br>
+© Copyright 2004-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
 
diff --git a/doc/contrib/mod_copy.html b/doc/contrib/mod_copy.html
index ce6f151..12e9982 100644
--- a/doc/contrib/mod_copy.html
+++ b/doc/contrib/mod_copy.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_copy.html,v 1.1 2010-03-10 19:20:43 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_copy.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_copy</title>
@@ -38,6 +36,7 @@ questions, concerns, or suggestions regarding this module.
 <h2>Directives</h2>
 <ul>
   <li><a href="#CopyEngine">CopyEngine</a>
+  <li><a href="#CopyOptions">CopyOptions</a>
 </ul>
 
 <h2><code>SITE</code> Commands</h2>
@@ -48,11 +47,11 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 <hr>
-<h2><a name="CopyEngine">CopyEngine</a></h2>
+<h3><a name="CopyEngine">CopyEngine</a></h3>
 <strong>Syntax:</strong> CopyEngine <em>on|off</em><br>
 <strong>Default:</strong> CopyEngine on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
-<strong>Module:</strong> mod_radius<br>
+<strong>Module:</strong> mod_copy<br>
 <strong>Compatibility:</strong> 1.3.6rc1 and later
 
 <p>
@@ -62,7 +61,31 @@ handling of <code>SITE COPY</code> <i>et al</i> commands.  If it is set to
 
 <p>
 <hr>
-<h2><a name="SITE_CPFR">SITE CPFR</a></h2>
+<h3><a name="CopyOptions">CopyOptions</a></h3>
+<strong>Syntax:</strong> CopyOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_copy<br>
+<strong>Compatibility:</strong> 1.3.6rc3 and later
+
+<p>
+The <code>CopyOptions</code> directive is used to configure various optional
+behavior of <code>mod_copy</code>.
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>NoDeleteOnFailure</code><br>
+    <p>
+    The <code>mod_copy</code> file, when a copying operation fails, will
+    delete the <i>destination</i> file by default.  If, however, you <b>do</b>
+    want to keep that destination file when a failure happens, use this option.
+  </li>
+</ul>
+
+<p>
+<hr>
+<h3><a name="SITE_CPFR">SITE CPFR</a></h3>
 This <code>SITE</code> command specifies the source file/directory to use
 for copying from one place to another directly on the server.
 
@@ -75,8 +98,9 @@ The syntax for <code>SITE CPFR</code> is:
 <p>
 See also: <a href="#SITE_CPTO">SITE CPTO</a>
 
+<p>
 <hr>
-<h2><a name="SITE_CPTO">SITE CPTO</a></h2>
+<h3><a name="SITE_CPTO">SITE CPTO</a></h3>
 This <code>SITE</code> command specifies the destination file/directory to use
 for copying from one place to another directly on the server.
 
@@ -93,7 +117,7 @@ handled using <code>RNFR</code> and <code>RNTO</code>.
 
 <p>
 Use of these <code>SITE</code> command can be controlled via
-<code><Limit><code> sections, <i>e.g.</i>:
+<code><Limit></code> sections, <i>e.g.</i>:
 <pre>
   <Limit SITE_COPY>
     AllowUser alex
@@ -104,25 +128,37 @@ Use of these <code>SITE</code> command can be controlled via
 <p>
 See also: <a href="#SITE_CPFR">SITE CPFR</a>
 
-<hr>
-<h2><a name="Installation">Installation</a></h2>
-To install <code>mod_copy</code>, copy the <code>mod_copy.c</code> file into:
+<p>
+<b>Logging</b><br>
+The <code>mod_copy</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>copy
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
 <pre>
-  <i>proftpd-dir</i>/contrib/
+  TraceLog /path/to/ftpd/trace.log
+  Trace copy:20
 </pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  For including
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_copy</code> module is distributed with ProFTPD.  Simply follow
+the normal steps for using third-party modules in ProFTPD.  For including
 <code>mod_copy</code> as a staticly linked module:
 <pre>
-  ./configure --with-modules=mod_copy
+  $ ./configure --with-modules=mod_copy
 </pre>
 To build <code>mod_copy</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_copy
+  $ ./configure --enable-dso --with-shared=mod_copy
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -130,19 +166,16 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_copy</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_copy.c
+  $ prxs -c -i -d mod_copy.c
 </pre>
 
 <p>
-<hr><br>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2009-2015 TJ Saunders<br>
+© Copyright 2009-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_ctrls_admin.html b/doc/contrib/mod_ctrls_admin.html
index d3abd40..8f99555 100644
--- a/doc/contrib/mod_ctrls_admin.html
+++ b/doc/contrib/mod_ctrls_admin.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ctrls_admin.html,v 1.12 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_ctrls_admin.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ctrls_admin</title>
@@ -39,6 +37,7 @@ questions, concerns, or suggestions regarding this module.
 
 <h2>Control Actions</h2>
 <ul>
+  <li><a href="#config"><code>config</code></a>
   <li><a href="#debug"><code>debug</code></a>
   <li><a href="#dns"><code>dns</code></a>
   <li><a href="#down"><code>down</code></a>
@@ -58,7 +57,7 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 <hr>
-<h2><a name="AdminControlsACLs">AdminControlsACLs</a></h2>
+<h3><a name="AdminControlsACLs">AdminControlsACLs</a></h3>
 <strong>Syntax:</strong> AdminControlsACLs <em>actions|all allow|deny user|group list</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -82,7 +81,7 @@ configure ACLs for different control actions, and for both users and groups.
 
 <p>
 <hr>
-<h2><a name="AdminControlsEngine">AdminControlsEngine</a></h2>
+<h3><a name="AdminControlsEngine">AdminControlsEngine</a></h3>
 <strong>Syntax:</strong> AdminControlsEngine <em>on|off|actions</em><br>
 <strong>Default:</strong> None<br> 
 <strong>Context:</strong> server config<br>
@@ -107,7 +106,50 @@ the module's control actions are registered.
 
 <p>
 <hr>
-<h2><a name="debug"><code>debug</code></a></h2>
+<h3><a name="config"><code>config</code></a></h3>
+<strong>Syntax:</strong> ftpdctl config set|remove <em>ip-address|dns-name[:port] directive ...</em><br>
+<strong>Purpose:</strong> Update configuration of a virtual server
+
+<p>
+The <code>config</code> control action can be used to update the configuration
+of an individual virtual server, without requiring a <code>SIGHUP</code>
+to restart the daemin and reparse the configuration.  The <code>config</code>
+action supports two subcommands: "set" and "remove".
+
+<p>
+Both <code>config</code> subcommands take the address of the virtual server
+whose configuration is to be changed.  This can be specified as a DNS name
+or an IP address, and optionally the port number.  If no port number is
+explicitly provided, a default of port 21 is assumed.  IPv6 addresses <b>are</b>
+supported.  Examples:
+<pre>
+  $ ftpdctl config set 10.1.2.3 ...
+  $ ftpdctl config set ftp.example.com:2121 ...
+  $ ftpdctl config set '[::1]:4242' ...
+</pre>
+<b>Note</b> that using the IPv6 syntax may require that you enclose the
+address in single quotes, to prevent the shell from parsing those characters.
+
+<p>
+When <i>setting</i> a configuration directive, all of the parameters of that
+configuration directive are required, just as if you were typing that
+configuration directive in the config file:
+<pre>
+  $ ftpdctl config set 192.168.0.101:2121 TLSRequired off
+  ftpdctl: config set: TLSRequired configured
+</pre>
+
+<p>
+When <i>removing</i> a configuration directive, only the configuration directive
+name is needed:
+<pre>
+  $ ftpdctl config remove 192.168.0.101 TLSRequired
+  ftpdctl: config remove: TLSRequired removed
+</pre>
+
+<p>
+<hr>
+<h3><a name="debug"><code>debug</code></a></h3>
 <strong>Syntax:</strong> ftpdctl debug <em>[level [number]]|memory|config</em><br>
 <strong>Purpose:</strong> Obtain debug information from the running daemon
 
@@ -124,19 +166,19 @@ daemon to a more silent state of logging.
 <p>
 Example:
 <pre>
-  ftpdctl debug level 9
+  $ ftpdctl debug level 9
 </pre>
 raises the verbosity of the daemon to its maximum level.  Once enough output
 has been collected, use:
 <pre>
-  ftpdctl debug level 0
+  $ ftpdctl debug level 0
 </pre>
 to return the daemon to its default debug output level.
 
 <p>
 Instead, to simply check the current debug logging verbosity, use:
 <pre>
-  ftpdctl debug level
+  $ ftpdctl debug level
 </pre>
 
 <p>
@@ -144,14 +186,14 @@ Alternatively, for developers (<i>i.e.</i> if <code>proftpd</code> was
 compiled with the <code>--enable-devel</code> configure option), this control
 action can be used to display the current memory allocation of the daemon:
 <pre>
-  ftpdctl debug memory
+  $ ftpdctl debug memory
 </pre>
 Memory allocations for session processes are currently not available via
 this control action.
 
 <p>
 <hr>
-<h2><a name="dns"><code>dns</code></a></h2>
+<h3><a name="dns"><code>dns</code></a></h3>
 <strong>Syntax:</strong> ftpdctl dns <em>on|off</em><br>
 <strong>Purpose:</strong> DNS configuration
 
@@ -160,22 +202,22 @@ The <code>dns</code> control action can be used to enable or disable
 the <code>UseReverseDNS</code> configuration at run time:
 <pre>
   # Enable resolution of IP addresses to DNS names
-  ftpdctl dns on
+  $ ftpdctl dns on
 
   # Disable resolution of IP addresses to DNS names
-  ftpdctl dns off
+  $ ftpdctl dns off
 </pre>
 
 <p>
 Note that the <code>dns</code> control action also supports a command for
 clearing any cached DNS lookup information:
 <pre>
-  ftpdctl dns clear cache
+  $ ftpdctl dns clear cache
 </pre>
 
 <p>
 <hr>
-<h2><a name="down"><code>down</code></a></h2>
+<h3><a name="down"><code>down</code></a></h3>
 <strong>Syntax:</strong> ftpdctl down <em>ip-address|dns-name[#port]|"all"</em><br>
 <strong>Purpose:</strong> Turn down a virtual server
 
@@ -197,7 +239,7 @@ Current sessions are not affected.
 
 <p>
 <hr>
-<h2><a name="get"><code>get</code></a></h2>
+<h3><a name="get"><code>get</code></a></h3>
 <strong>Syntax:</strong> ftpdctl get <em>"config"|"directives"</em><br>
 <strong>Purpose:</strong> Obtain configuration information
 
@@ -218,7 +260,7 @@ which handles the corresponding directive.
 
 <p>
 <hr>
-<h2><a name="kick"><code>kick</code></a></h2>
+<h3><a name="kick"><code>kick</code></a></h3>
 <strong>Syntax:</strong> ftpdctl kick <em>[class name]|[host dns-name|ip-address][user name]</em><br>
 <strong>Purpose:</strong> Kick a currently connected class, host or user from the daemon
 
@@ -229,15 +271,15 @@ connected <em>class</em>, <em>host</em> or <em>user</em> from the daemon.
 <p>
 Examples:
 <pre>
-  ftpdctl kick user bob dave 
+  $ ftpdctl kick user bob dave 
 </pre>
 will kick all sessions that have logged in as user "bob" or user "dave".
 <pre>
-  ftpdctl kick host luser.host.net
+  $ ftpdctl kick host luser.host.net
 </pre>
 will kick all sessions that have connected from host "luser.host.net".
 <pre>
-  ftpdctl kick class eval intranet
+  $ ftpdctl kick class eval intranet
 </pre>
 will kick all sessions that belong to classes "eval" and "intranet".
 
@@ -247,12 +289,12 @@ specify the maximum number of clients to be kicked.  For example, perhaps
 you only want to kick off 10 clients from host "luser.host.net"
 rather than kicking all of them off.  To do this, use:
 <pre>
-  ftpdctl kick host -n 10 luser.host.net
+  $ ftpdctl kick host -n 10 luser.host.net
 </pre>
 
 <p>
 <hr>
-<h2><a name="restart"><code>restart</code></a></h2>
+<h3><a name="restart"><code>restart</code></a></h3>
 <strong>Syntax:</strong> ftpdctl restart<br>
 <strong>Purpose:</strong> Restart the daemon
 
@@ -265,18 +307,18 @@ privileges, nor knowledge of the daemon's PID, to restart the daemon.
 <p>
 Example:
 <pre>
-  # ftpdctl restart
+  $ ftpdctl restart
 </pre>
 In addition, you can use the <code>restart</code> control to ask the daemon
 how many times it has been restarted:
 <pre>
-  # ftpdctl restart count
+  $ ftpdctl restart count
   ftpdctl: restarted 4 times since 2010-01-06 23:20:09 GMT
 </pre>
 
 <p>
 <hr>
-<h2><a name="scoreboard"><code>scoreboard</code></a></h2>
+<h3><a name="scoreboard"><code>scoreboard</code></a></h3>
 <strong>Syntax:</strong> ftpdctl scoreboard scrub<br>
 <strong>Purpose:</strong> Scrubs the ScoreboardFile for dead processes
 
@@ -285,15 +327,15 @@ The <code>scoreboard</code> control action can be used to force the
 <code>ScoreboardFile</code> to be "scrubbed" for dead session
 processes which may not have exited cleanly.
 <pre>
-  ftpdctl scoreboard scrub
+  $ ftpdctl scoreboard scrub
 
   # The verb "clean" is synonymous with "scrub"
-  ftpdctl scoreboard clean
+  $ ftpdctl scoreboard clean
 </pre>
 
 <p>
 <hr>
-<h2><a name="shutdown"><code>shutdown</code></a></h2>
+<h3><a name="shutdown"><code>shutdown</code></a></h3>
 <strong>Syntax:</strong> ftpdctl shutdown <em>["graceful" seconds]</em><br>
 <strong>Purpose:</strong> Stop the daemon
 
@@ -310,14 +352,14 @@ for all current sessions to end, before shutting down.
 <p>
 Example:
 <pre>
-  ftpdctl shutdown graceful 30
+  $ ftpdctl shutdown graceful 30
 </pre>
 will cause <code>proftpd</code> to wait for 30 seconds for all current
 sessions to end before shutting down completely.
 
 <p>
 <hr>
-<h2><a name="status"><code>status</code></a></h2>
+<h3><a name="status"><code>status</code></a></h3>
 <strong>Syntax:</strong> ftpdctl status <em>ip-address|dns-name[#port]|"all"</em><br>
 <strong>Purpose:</strong> Display the status of virtual servers
 
@@ -334,7 +376,7 @@ displayed.
 
 <p>
 <hr>
-<h2><a name="trace"><code>trace</code></a></h2>
+<h3><a name="trace"><code>trace</code></a></h3>
 <strong>Syntax:</strong> ftpdctl trace <em>channel:level|"info"</em><br>
 <strong>Purpose:</strong> Configure trace channel log levels
 
@@ -345,7 +387,7 @@ the log levels of trace log levels.
 <p>
 Example:
 <pre>
-  ftpdctl trace delay:10
+  $ ftpdctl trace delay:10
 </pre>
 will set the log verbosity level of the <em>delay</em> trace log channel to 10.
 
@@ -353,7 +395,7 @@ will set the log verbosity level of the <em>delay</em> trace log channel to 10.
 Additionally, the <code>trace</code> control action can be used to display
 the list of current trace channels and their log levels, <i>e.g.</i>:
 <pre>
-  # ftpdctl trace info
+  $ ftpdctl trace info
   ftpdctl: Channel    Level 
   ftpdctl: ---------- ------
   ftpdctl:        pam 10    
@@ -383,7 +425,7 @@ the list of current trace channels and their log levels, <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="up"><code>up</code></a></h2>
+<h3><a name="up"><code>up</code></a></h3>
 <strong>Syntax:</strong> ftpdctl up <em>ip-address|dns-name[#port]</em><br>
 <strong>Purpose:</strong> Turn up a "downed" virtual server
 
@@ -404,16 +446,16 @@ This module requires that controls support be enabled in <code>proftpd</code>
 via the <code>--enable-ctrls</code> configure option.  Follow the normal
 steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --enable-ctrls --with-modules=mod_ctrls_admin
+  $ ./configure --enable-ctrls --with-modules=mod_ctrls_admin
 </pre>
 To build <code>mod_ctrls_admin</code> as a DSO module:
 <pre>
-  ./configure --enable-ctrls --enable-dso --with-shared=mod_ctrls_admin
+  $ ./configure --enable-ctrls --enable-dso --with-shared=mod_ctrls_admin
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -421,22 +463,16 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_ctrls_admin</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_ctrls_admin.c
+  $ prxs -c -i -d mod_ctrls_admin.c
 </pre>
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
-
 <font size=2><b><i>
-© Copyright 2004-2013 The ProFTPD Project<br>
+© Copyright 2004-2016 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_deflate.html b/doc/contrib/mod_deflate.html
index 61e407b..63ecd0d 100644
--- a/doc/contrib/mod_deflate.html
+++ b/doc/contrib/mod_deflate.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_deflate.html,v 1.3 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_deflate.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_deflate</title>
@@ -81,24 +79,19 @@ If <em>path</em> is "none", no logging will be done at all.
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_deflate</code>, copy the <code>mod_deflate.c</code> file
-into:
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  Then follow the
-usual steps for using third-party modules in proftpd:
+The <code>mod_deflate</code> module is distributed with ProFTPD.  Follow the
+usual steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_deflate
+  $ ./configure --with-modules=mod_deflate
 </pre>
 To build <code>mod_deflate</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_deflate
+  $ ./configure --enable-dso --with-shared=mod_deflate
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -106,7 +99,7 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_deflate</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_deflate.c
+  $ prxs -c -i -d mod_deflate.c
 </pre>
 
 <p>
@@ -135,6 +128,23 @@ well:
   </IfModule>
 </pre>
 
+<p>
+<b>Logging</b><br>
+The <code>mod_deflate</code> module supports different forms of logging.  The
+main module logging is done via the <code>DeflateLog</code> directive.
+For debugging purposes, the module also uses <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>deflate
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace deflate:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -187,20 +197,12 @@ The solution is make sure that the client uploads (and downloads) non-ASCII
 files as binary files, not as ASCII files.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2006-2013 TJ Saunders<br>
+© Copyright 2006-2014 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_digest.html b/doc/contrib/mod_digest.html
new file mode 100644
index 0000000..671cc61
--- /dev/null
+++ b/doc/contrib/mod_digest.html
@@ -0,0 +1,394 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_digest</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_digest</code></b></h2>
+</center>
+<hr><br>
+
+<p>
+The <code>mod_digest</code> module offers functionality for calculating the hash
+(or <em>digest</em>) value of files.  This is particularly useful when verifying
+the integrity of files.  This functionality is used by the following custom
+FTP commands:
+<ul>
+  <li><code>XCRC</code> (requests CRC32 digest/checksum)
+  <li><code>MD5/XMD5</code> (requests MD5 digest/checksum)
+  <li><code>XSHA</code>/<code>XSHA1</code> (requests SHA1 digest/checksum)
+  <li><code>XSHA256</code> (requests SHA256 digest/checksum)
+  <li><code>XSHA512</code> (requests SHA512 digest/checksum)
+</ul>
+In addition, <code>mod_digest</code> supports the more modern <a href="https://tools.ietf.org/html/draft-bryan-ftpext-hash-02"><code>HASH</code></a> command.
+
+<p>
+Depending on the file size and the hash function, it takes a fair amount of
+CPU and IO resources to calculate the result.  Therefore decide wisely where
+to enable the features and set the <a href="#DigestMaxSize">DigestMaxSize</a>
+configuration directive appropriately.
+
+<p>
+This module was compiled and tested against ProFTPD 1.3.3 Installation
+instructions are discussed <a href="#Installation">here</a>.
+
+<p>
+The most current version of <code>mod_digest</code> is distributed with the
+ProFTPD source code.
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
+questions, concerns, or suggestions regarding this module.
+
+<h2>Thanks</h2>
+<p>
+<i>2016-01-09</i>: Thanks to Mathias Berchtold <mb <i>at</i>
+smartftp.com> for his original <code>mod_digest</code>, upon which this
+version is based.
+
+<h2>Directives</h2>
+<ul>
+  <li><a href="#DigestAlgorithms">DigestAlgorithms</a>
+  <li><a href="#DigestCache">DigestCache</a>
+  <li><a href="#DigestDefaultAlgorithm">DigestDefaultAlgorithm</a>
+  <li><a href="#DigestEnable">DigestEnable</a>
+  <li><a href="#DigestEngine">DigestEngine</a>
+  <li><a href="#DigestMaxSize">DigestMaxSize</a>
+  <li><a href="#DigestOptions">DigestOptions</a>
+</ul>
+
+<hr>
+<h3><a name="DigestAlgorithms">DigestAlgorithms</a></h3>
+<strong>Syntax:</strong> DigestAlgorithms <em>["crc32"|"md5"|"sha1"|"sha256"|"sha512"|"all"]</em><br>
+<strong>Default:</strong> DigestAlgorithms all<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Anonymous><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc2 or later
+
+<p>
+The <code>DigestAlgorithms</code> directive configures the enabled digest
+algorithms.  If no <code>DigestAlgorithms</code> directive is configured, then
+<b>all</b> supported digest algorithms are enabled.
+
+<p>
+Enabled digest algorithms are announced/discovered via the <code>FEAT</code>
+response.
+
+The following algorithms are currently supported by <code>mod_digest</code>:
+<ul>
+  <li><code>crc32</code> (<i>e.g.</i> for the <code>XCRC</code> command)
+  <li><code>md5</code> (<i>e.g.</i> for the <code>XMD5</code> command)
+  <li><code>sha1</code> (<i>e.g.</i> for the <code>XSHA</code>/<code>XSHA1</code> commands)
+  <li><code>sha256</code> (<i>e.g.</i> for the <code>XSHA256</code> command)
+  <li><code>sha512</code> (<i>e.g.</i> for the <code>XSHA512</code> command)
+</ul>
+
+<p>
+<hr>
+<h3><a name="DigestCache">DigestCache</a></h3>
+<strong>Syntax:</strong> DigestCache <em>on|off|"size" count ["maxAge" secs]</em><br>
+<strong>Default:</strong> DigestCache size 10000 maxAge 30s<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Anonymous><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc2 or later
+
+<p>
+The <code>mod_digest</code> module will cache the results of any checksum
+command, on a per-file basis.  This improves performance, and reduces
+computational overhead.  To disable this caching for any reason, use this
+directive:
+<pre>
+  # Disable checksum caching
+  DigestCache off
+</pre>
+<b>This is not recommended.</b>
+
+<p>
+The <code>DigestCache</code> directive can also be used to configure/tune the
+<em>max-size</em> of the in-memory cache.  Note that once the maximum cache
+size is reached, any checksum FTP commands will be temporarily refused:
+<pre>
+  # Use a smaller cache size
+  DigestCache size 100
+</pre>
+Cached digests will be expired/ignored after 30 seconds, by default.  To change
+the expiration, you would use:
+<pre>
+  # Retain cached entries longer
+  DigestCache maxAge 60s
+</pre>
+
+<p>
+If <em>on</em> is used, <code>mod_digest</code> will use the default
+<em>max-size</em> of 10000:
+<pre>
+  DigestCache on
+</pre>
+
+<p>
+<hr>
+<h3><a name="DigestDefaultAlgorithm">DigestDefaultAlgorithm</a></h3>
+<strong>Syntax:</strong> DigestDefaultAlgorithm <em>algo</em><br>
+<strong>Default:</strong> DigestDefaultAlgorithm sha1<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc3 or later
+
+<p>
+The default digest algorithm that the <code>mod_digest</code> module uses,
+for <i>e.g.</i> opportunistic digesting of file transfers, is SHA1.  For
+selecting a different default algorithm, use the
+<code>DigestDefaultAlgorithm</code> directive:
+<pre>
+  # Use MD5 rather than SHA1 as the default algorithm
+  DigestDefaultAlgorithm md5
+</pre>
+
+<p>
+<b>Note</b> that the <code>DigestAlgorithms</code> directive takes precedence;
+if the <code>DigestDefaultAlgorithm</code> is not included in the
+<code>DigestAlgorithms</code>, the default algorithm setting will be ignored.
+
+<p>
+<hr>
+<h3><a name="DigestEnable">DigestEnable</a></h3>
+<strong>Syntax:</strong> DigestEnable <em>on|off</em><br>
+<strong>Default:</strong> Non<br>
+<strong>Context:</strong> <code><Directory></code>, <code>.ftpaccess</code><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc2 or later
+
+<p>
+The <code>DigestEnable</code> directive can be used to block or prevent
+checksumming/digests on files in the configured <code><Directory></code>.
+This can be <b>very</b> useful for preventing checksumming of files located
+on network-mounted filesystems, for example.
+
+<p>
+<hr>
+<h3><a name="DigestEngine">DigestEngine</a></h3>
+<strong>Syntax:</strong> DigestEngine <em>on|off</em><br>
+<strong>Default:</strong> DigestEngine on<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Anonymous><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc2 or later
+
+<p>
+The <code>DigestEngine</code> directive enables or disables the handling of
+the checksum-related FTP commands by <code>mod_digest</code>, <i>i.e.</i>:
+<ul>
+  <li><code>XCRC</code>
+  <li><code>XMD5</code>
+  <li><code>XSHA</code>
+  <li><code>XSHA1</code>
+  <li><code>XSHA256</code>
+  <li><code>XSHA512</code>
+</ul>
+If the parameter is <em>off</em>, then these commands will be ignored.
+
+<p>
+<hr>
+<h3><a name="DigestMaxSize">DigestMaxSize</a></h3>
+<strong>Syntax:</strong> DigestMaxSize <em>number [units]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Anonymous><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc2 or later
+
+<p>
+The <code>DigestMaxSize</code> directive configures the maximum number of bytes
+a single hash command is allowed to read from a file.  If the number of bytes
+to be read from the file is greater than the configured <em>number</em> the
+server will refuse that command.
+
+<p>
+If no <code>DigestMaxSize</code> directive is configured, then there is no
+limit. It is highly <b>recommended</b> to set an upper limit.
+
+<p>
+Example:
+<pre>
+  # Limit hashing to 1GB of data
+  DigestMaxSize 1 GB
+</pre>
+
+<p>
+<hr>
+<h3><a name="DigestOptions">DigestOptions</a></h3>
+<strong>Syntax:</strong> DigestOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_digest<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>DigestOptions</code> directive is used to configure various optional
+behavior of <code>mod_digest</code>.
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>NoTransferCache</code><br>
+    <p>
+    The <code>mod_digest</code> module will automatically calculate <b>and</b>
+    cache the results of any transferred file, on a per-file basis.  This is
+    done assuming that many FTP clients will want to verify the integrity of
+    the file just uploaded/downloaded.	This improves performance, and
+    reduces computational overhead.  To disable this caching for any reason,
+    use this option.  <b>Not recommended.</b>
+
+    <p>
+    <b>Note</b>: The <code>NoTransferCache</code> option is
+    <em>automatically</em> enabled when using ProFTPD versions before
+    1.3.6rc2, due to bugs/missing support in the older versions.
+  </li>
+</ul>
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_digest</code> module is distributed with ProFTPD.  Follow the
+normal steps for using third-party modules in ProFTPD:
+<pre>
+  $ ./configure --enable-openssl --with-modules=mod_digest
+</pre>
+To build <code>mod_digest</code> as a shared/DSO module:
+<pre>
+  $ ./configure --enable-dso --enable-openssl --with-shared=mod_digest
+</pre>
+Then follow the usual steps:
+<pre>
+  $ make
+  $ make install
+</pre>
+
+<p>
+Alternatively, if your proftpd was compiled with DSO support, you can
+use the <code>prxs</code> tool to build <code>mod_digest</code> as a shared
+module:
+<pre>
+  $ prxs -c -i -d mod_digest.c
+</pre>
+
+<p>
+<hr>
+<h2>Usage</h2>
+Example Configuration
+<pre>
+  <IfModule mod_digest.c>
+    # Set a limit on file sizes that can be digested
+    DigestMaxSize 1 GB
+  </IfModule>
+</pre>
+
+<p>
+<b>Recording Uploaded/Downloaded File Checksums</b><br>
+One particular use case that comes up is whether the <code>mod_digest</code>
+can be used to record the digests ("checksums") of uploaded/downloaded files
+in <i>e.g.</i> a SQL database.  The answer is "yes", with some caveats.
+
+<p>
+First, here is a configuration excerpt showing show such functionality might
+be implemented, using <code>mod_digest</code> and <code>mod_sql</code>:
+<pre>
+  <IfModule mod_digest.c>
+  </IfModule>
+
+  <IfModule mod_sql.c>
+    ...
+    SQLNamedQuery log-file-checksum FREEFORM "INSERT INTO file_checksums (user, file, algo, checksum) VALUES ('%u', '%f', '%{note:mod_digest.algo}', '%{note:mod_digest.digest}')"
+    SQLLog RETR,STOR log-file-checksum
+    ...
+  </IfModule>
+</pre>
+As you can see, this makes use of the <code>%{note:...}</code> syntax of
+the <code>SQLLog</code> directive; the same syntax <em>also</em> works for
+<code>LogFormat</code> definitions as well.  The <code>mod_digest</code> module
+uses the following notes:
+<ul>
+  <li><em>mod_digest.algo</em>
+    <p>
+    Name of the digest algorithm used, <i>e.g.</i> "SHA1".
+  </li>
+
+  <p>
+  <li><em>mod_digest.digest</em>
+    <p>
+    Calculated digest of the file as a hex-encoded lowercase string.
+  </li>
+</ul>
+
+<p>
+Now, the caveats with this technique:
+<ul>
+  <li>Does <b>not</b> work if the <code>NoTransferCache</code> <a href="#DigestOption">DigestOption</a> is used.
+  <li>Only works for binary, not ASCII, FTP uploads/downloads currently.
+  <li>Only works for uploads (<code>STOR</code>) and downloads (<code>RETR</code>), but not for appends (<code>APPE</code>) <b>or</b> resumed uploads/downloads (<code>REST</code> + <code>RETR/STOR</code>).
+  <li>Does <b>not</b> work for FTP downloads if <code>UseSendfile</code> is in effect.
+</ul>
+In addition, the order in which the <code>mod_digest</code> and
+<code>mod_sql</code> appear in your build command is important;
+<code>mod_digest</code> <em>must come <b>after</b></em> <code>mod_sql</code>,
+otherwise the note values will <b>not</b> be populated properly in the
+<code>SQLLog</code> statement.  Thus, if you are building static modules,
+your <code>--with-modules</code> parameter would look something like:
+<pre>
+  $ ./configure --with-modules=mod_sql:mod_sql_mysql:mod_digest ...
+</pre>
+Or, if you are using shared modules, then your <code>LoadModule</code>
+directives must look like:
+<pre>
+  LoadModule mod_sql.c
+  LoadModule mod_sql_mysql.c
+  LoadModule mod_digest.c
+</pre>
+
+<!--
+Why?
+
+TCP-level checksums
+packet-level checksums
+_file_-level checksums (which is really what most people usually have in mind)
+
+transfers interrupted by timeouts
+
+SFTP has different ways of achieving this, via extensions (link to mod_sftp
+docs on extensions)
+
+validating uploads AND downloads (did I download everything?  Did the upload
+succeed?)
+
+<p>
+It's also recommended to disable all features within the <Anonymous> context.  How?
+
+  <Anonymous>
+    <IfModule mod_digest.c>
+      DigestEngine off
+    </IfModule>
+  </Anonymous>
+
+<p>
+<b>Supported FTP Commands</b><br>
+ cmd path
+ cmd path [end]
+ cmd path [off] [len]
+<pre>
+  XCRC "/path/to/file with spaces" 0 100
+</pre>
+-->
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2016 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/contrib/mod_dnsbl.html b/doc/contrib/mod_dnsbl.html
index 11cfef1..43e6b75 100644
--- a/doc/contrib/mod_dnsbl.html
+++ b/doc/contrib/mod_dnsbl.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_dnsbl.html,v 1.1 2013-09-18 21:18:51 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_dnsbl.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_dnsbl</title>
@@ -31,8 +29,7 @@ FTP client's address should be allowed or rejected by an FTP server.  Thus
 the <code>mod_dnsbl</code> module was written for ProFTPD, for such a purpose.
 
 <p>
-This module is contained in the <code>mod_dnsbl.c</code> file for
-ProFTPD 1.3.<i>x</i>, and is not compiled by default.  Installation
+The <code>mod_dnsbl</code> module is not compiled by default; build/installation
 instructions are discussed <a href="#Installation">here</a>.
 
 <p>
@@ -53,7 +50,7 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="DNSBLDomain">DNSBLDomain</a></h2>
+<h3><a name="DNSBLDomain">DNSBLDomain</a></h3>
 <strong>Syntax:</strong> DNSBLDomain <em>domain</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -78,7 +75,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="DNSBLEngine">DNSBLEngine</a></h2>
+<h3><a name="DNSBLEngine">DNSBLEngine</a></h3>
 <strong>Syntax:</strong> DNSBLEngine <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -95,7 +92,7 @@ virtual hosts.
 
 <p>
 <hr>
-<h2><a name="DNSBLLog">DNSBLLog</a></h2>
+<h3><a name="DNSBLLog">DNSBLLog</a></h3>
 <strong>Syntax:</strong> DNSBLLog <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -114,7 +111,7 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="DNSBLPolicy">DNSBLPolicy</a></h2>
+<h3><a name="DNSBLPolicy">DNSBLPolicy</a></h3>
 <strong>Syntax:</strong> DNSBLPolicy <em>"allow,deny"|"deny,allow"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -141,48 +138,48 @@ the <code>mod_dnsbl</code> module will <b>not</b> allow the connection,
 <p>
 <hr><br>
 <h2><a name="Installation">Installation</a></h2>
-After unpacking the <code>mod_dnsbl</code> tarball, move the directory into
-the ProFTPD source directory:
+The <code>mod_dnsbl</code> module is distributed with ProFTPD.  Simply follow
+the normal steps for using third-party modules in ProFTPD:
 <pre>
-  mv mod_dnsbl/ /path/to/proftpd/contrib/
+  $ ./configure --with-modules=mod_dnsbl
+  $ make
+  $ make install
 </pre>
-<b>Note</b> that it is necessary to move the entire <code>mod_dnsbl</code>
-directory, not just the <code>mod_dnsbl.c</code> source file, into the
-<code>contrib/</code> directory in the ProFTPD source directory.  Failure to
-do so will result in a failed build.
-
-<p>
-Next:
+Alternatively, <code>mod_dnsbl</code> can be built as a DSO module:
+<pre>
+  $ ./configure --enable-dso --with-shared=mod_dnbsl ...
+</pre>
+Then follow the usual steps:
 <pre>
-  cd /path/to/proftpd/contrib/mod_dnsbl/
-  ./configure
+  $ make
+  $ make install
 </pre>
-This step is also necessary.  If not done, then the proftpd build system
-will not pick up the correct linker flags for the resolver library.
 
 <p>
-Then follow the normal steps for using third-party modules in proftpd:
+<b>Logging</b><br>
+The <code>mod_dnbsl</code> module supports different forms of logging.  The
+main module logging is done via the <code>DNSBLLog</code> directive.
+For debugging purposes, the module also uses <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>dnsbl
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
 <pre>
-  ./configure --with-modules=mod_dnsbl
-  make
-  make install
+  TraceLog /path/to/ftpd/trace.log
+  Trace dnsbl:20
 </pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-09-18 21:18:51 $</i><br>
-
-<br><hr>
+<hr>
 
 <font size=2><b><i>
-© Copyright 2007-2013 TJ Saunders<br>
+© Copyright 2007-2014 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
 
-<hr><br>
-
+<hr>
 </body>
 </html>
-
diff --git a/doc/contrib/mod_dynmasq.html b/doc/contrib/mod_dynmasq.html
index c8cc7b1..3e6ac93 100644
--- a/doc/contrib/mod_dynmasq.html
+++ b/doc/contrib/mod_dynmasq.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_dynmasq.html,v 1.3 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_dynmasq.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_dynmasq</title>
@@ -43,10 +41,10 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="DynMasqRefresh">DynMasqRefresh</a></h2>
+<h3><a name="DynMasqRefresh">DynMasqRefresh</a></h3>
 <strong>Syntax:</strong> DynMasqRefresh <em>secs</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_dynmasq<br>
 <strong>Compatibility:</strong> 1.2.10
 
@@ -81,19 +79,19 @@ See also:
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install the <code>mod_dynmasq</code> module, follow the usual steps for
-using third-party modules in proftpd:
+The <code>mod_dynmasq</code> module is distributed with ProFTPD.  Follow the
+usual steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_dynmasq
+  $ ./configure --with-modules=mod_dynmasq
 </pre>
 To build <code>mod_dynmasq</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_dynmasq
+  $ ./configure --enable-dso --with-shared=mod_dynmasq
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -101,24 +99,16 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_dynmasq</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_dynmasq.c
+  $ prxs -c -i -d mod_dynmasq.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2004-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_exec.html b/doc/contrib/mod_exec.html
index a56e5c9..d10ace2 100644
--- a/doc/contrib/mod_exec.html
+++ b/doc/contrib/mod_exec.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_exec.html,v 1.10 2014-01-21 22:39:11 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_exec.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_exec</title>
@@ -52,6 +50,7 @@ questions, concerns, or suggestions regarding this module.
 <h2>Directives</h2>
 <ul>
   <li><a href="#ExecBeforeCommand">ExecBeforeCommand</a>
+  <li><a href="#ExecEnable">ExecEnable</a>
   <li><a href="#ExecEngine">ExecEngine</a>
   <li><a href="#ExecEnviron">ExecEnviron</a>
   <li><a href="#ExecLog">ExecLog</a>
@@ -66,10 +65,10 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="ExecBeforeCommand">ExecBeforeCommand</a></h2>
+<h3><a name="ExecBeforeCommand">ExecBeforeCommand</a></h3>
 <strong>Syntax:</strong> ExecBeforeCommand <em>cmds path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.8 and later
 
@@ -108,10 +107,24 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecEngine">ExecEngine</a></h2>
+<h3><a name="ExecEnable">ExecEnable</a></h3>
+<strong>Syntax:</strong> ExecEnable <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
+<strong>Module:</strong> mod_exec<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>ExecEnable</code> directive can be used to disable the execution
+of commands by <code>mod_exec</code> for particular directories or anonymous
+logins.
+
+<p>
+<hr>
+<h3><a name="ExecEngine">ExecEngine</a></h3>
 <strong>Syntax:</strong> ExecEngine <em>on|off</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -124,10 +137,10 @@ directive to disable the module instead of commenting out all
 
 <p>
 <hr>
-<h2><a name="ExecEnviron">ExecEnviron</a></h2>
+<h3><a name="ExecEnviron">ExecEnviron</a></h3>
 <strong>Syntax:</strong> ExecEnviron <em>key value</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -165,10 +178,10 @@ used (<i>e.g.</i> PATH).  If there is no environment of name <i>key</i> when
 
 <p>
 <hr>
-<h2><a name="ExecLog">ExecLog</a></h2>
+<h3><a name="ExecLog">ExecLog</a></h3>
 <strong>Syntax:</strong> ExecLog <em>file|"none"</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -188,10 +201,10 @@ a <code><Global></code> context.
 
 <p>
 <hr>
-<h2><a name="ExecOnCommand">ExecOnCommand</a></h2>
+<h3><a name="ExecOnCommand">ExecOnCommand</a></h3>
 <strong>Syntax:</strong> ExecOnCommand <em>cmds path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -230,10 +243,10 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecOnConnect">ExecOnConnect</a></h2>
+<h3><a name="ExecOnConnect">ExecOnConnect</a></h3>
 <strong>Syntax:</strong> ExecOnConnect <em>path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -265,10 +278,10 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecOnError">ExecOnError</a></h2>
+<h3><a name="ExecOnError">ExecOnError</a></h3>
 <strong>Syntax:</strong> ExecOnError <em>cmds path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -306,10 +319,10 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecOnEvent">ExecOnEvent</a></h2>
+<h3><a name="ExecOnEvent">ExecOnEvent</a></h3>
 <strong>Syntax:</strong> ExecOnEvent <em>event[*|~] path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.10rc1 and later
 
@@ -367,10 +380,10 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecOnExit">ExecOnExit</a></h2>
+<h3><a name="ExecOnExit">ExecOnExit</a></h3>
 <strong>Syntax:</strong> ExecOnExit <em>path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.8 and later
 
@@ -402,10 +415,10 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecOnRestart">ExecOnRestart</a></h2>
+<h3><a name="ExecOnRestart">ExecOnRestart</a></h3>
 <strong>Syntax:</strong> ExecOnRestart <em>path [arg1 arg2 ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.5rc2 and later
 
@@ -436,10 +449,10 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="ExecOptions">ExecOptions</a></h2>
+<h3><a name="ExecOptions">ExecOptions</a></h3>
 <strong>Syntax:</strong> ExecOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.9rc2 and later
 
@@ -513,10 +526,10 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="ExecTimeout">ExecTimeout</a></h2>
+<h3><a name="ExecTimeout">ExecTimeout</a></h3>
 <strong>Syntax:</strong> ExecTimeout <em>seconds</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config" <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_exec<br>
 <strong>Compatibility:</strong> 1.2.9rc2 and later
 
@@ -572,31 +585,27 @@ FIFO reader to execute the necessary scripts.
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_exec</code>, copy the <code>mod_exec.c</code> file into
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  Then follow the
-usual steps for using third-party modules in proftpd:
+The <code>mod_exec</code> module is distributed with ProFTPD.  Follow the
+usual steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_exec
+  $ ./configure --with-modules=mod_exec
 </pre>
 To build <code>mod_exec</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_exec
+  $ ./configure --enable-dso --with-shared=mod_exec
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_exec</code> as a shared
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_exec</code> as a shared
 module:
 <pre>
-  prxs -c -i -d mod_exec.c
+  $ prxs -c -i -d mod_exec.c
 </pre>
 
 <p><a name="FAQ"></a>
@@ -619,19 +628,12 @@ commands, the server will have the necessary user information to populate
 the <code>%U/%u</code> variables.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-21 22:39:11 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2000-2014 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/contrib/mod_geoip.html b/doc/contrib/mod_geoip.html
index 857b1bf..7b394e6 100644
--- a/doc/contrib/mod_geoip.html
+++ b/doc/contrib/mod_geoip.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_geoip.html,v 1.6 2013-12-12 16:52:28 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_geoip.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_geoip</title>
@@ -51,10 +49,10 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="GeoIPAllowFilter">GeoIPAllowFilter</a></h2>
-<strong>Syntax:</strong> GeoIPAllowFilter <em>filter</em> <em>pattern</em><br>
+<h3><a name="GeoIPAllowFilter">GeoIPAllowFilter</a></h3>
+<strong>Syntax:</strong> GeoIPAllowFilter <em>filter1 pattern1 [filter2 pattern2 ...]</em><br>
 <strong>Default:</strong> <em>none</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_geoip<br>
 <strong>Compatibility:</strong> 1.3.3rc1 and later
 
@@ -96,6 +94,13 @@ The <em>pattern</em> parameter is <b>case-insensitive</b> regular expression
 that will be applied to the specified <em>filter</em> value, if available.
 
 <p>
+Note that as of <code>proftpd-1.3.6rc3</code> and later, the
+<code>GeoIPAllowFilter</code> directive can also take a <em>single</em>
+parameter which specifies a SQL query (via <code>mod_sql</code>'s
+<a href="mod_sql.html#SQLNamedQuery"><code>SQLNamedQuery</code></a>), which
+will be used to retrieve the <em>filter</em> and <em>pattern</em> values to use.
+
+<p>
 Examples:
 <pre>
   # Allow clients with high-speed connections
@@ -104,13 +109,48 @@ Examples:
   # Reject clients connecting from North America or South America
   GeoIPDenyFilter Continent (NA|SA)
 </pre>
+The following more complex configuration demonstrates what can be done using
+SQL querires:
+<pre>
+  <IfModule mod_sql.c>
+    ...
+    SQLNamedQuery get-geo-allowed SELECT "filter_name, pattern FROM allowed_user_geo WHERE user_name = '%u'"
+    SQLNamedQuery get-geo-denied SELECT "filter_name, pattern FROM denied_user_geo WHERE user_name = '%u'"
+  </IfModule>
+
+  <IfModule mod_geoip.c>
+    GeoIPEngine on
+
+    GeoIPAllowFilter sql:/get-geo-allowed
+    GeoIPDenyFilter sql:/get-geo-denied
+  </IfModule>
+</pre>
+The above assumes SQL tables with schema similar to the following (expressed
+using SQLite syntax):
+<pre>
+  CREATE TABLE allowed_user_geo (
+    user_name TEXT,
+    filter_name TEXT,
+    pattern TEXT
+  );
+
+  CREATE TABLE denied_user_geo (
+    user_name TEXT,
+    filter_name TEXT,
+    pattern TEXT
+  );
+
+  # Note that we create separate indexes, to allow for multiple rows per user
+  CREATE INDEX allowed_user_geo_name_idx ON allowed_user_geo (user_name);
+  CREATE INDEX denied_user_geo_name_idx ON denied_user_geo (user_name);
+</pre>
 
 <p>
 <hr>
-<h2><a name="GeoIPDenyFilter">GeoIPDenyFilter</a></h2>
-<strong>Syntax:</strong> GeoIPDenyFilter <em>filter</em> <em>pattern</em><br>
+<h3><a name="GeoIPDenyFilter">GeoIPDenyFilter</a></h3>
+<strong>Syntax:</strong> GeoIPDenyFilter <em>filter1 pattern1 [filter2 pattern2 ...]</em><br>
 <strong>Default:</strong> <em>none</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_geoip<br>
 <strong>Compatibility:</strong> 1.3.3rc1 and later
 
@@ -124,15 +164,22 @@ supported; if <b>any</b> filter matches the connecting client, the connection
 will be rejected.
 
 <p>
+Note that as of <code>proftpd-1.3.6rc3</code> and later, the
+<code>GeoIPDenyFilter</code> directive can also take a <em>single</em>
+parameter which specifies a SQL query (via <code>mod_sql</code>'s
+<a href="mod_sql.html#SQLNamedQuery"><code>SQLNamedQuery</code></a>), which
+will be used to retrieve the <em>filter</em> and <em>pattern</em> values to use.
+
+<p>
 See <a href="#GeoIPAllowFilter"><code>GeoIPAllowFilter</code></a> for
 a description of the directive syntax and parameters.
 
 <p>
 <hr>
-<h2><a name="GeoIPEngine">GeoIPEngine</a></h2>
+<h3><a name="GeoIPEngine">GeoIPEngine</a></h3>
 <strong>Syntax:</strong> GeoIPEngine <em>on|off</em><br>
 <strong>Default:</strong> <em>off</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_geoip<br>
 <strong>Compatibility:</strong> 1.3.3rc1 and later
 
@@ -143,10 +190,10 @@ enforcement of any configured ACLs.
 
 <p>
 <hr>
-<h2><a name="GeoIPLog">GeoIPLog</a></h2>
+<h3><a name="GeoIPLog">GeoIPLog</a></h3>
 <strong>Syntax:</strong> GeoIPLog <em>file|"none"</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_geoip<br>
 <strong>Compatibility:</strong> 1.3.3rc1 and later
 
@@ -162,7 +209,7 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="GeoIPPolicy">GeoIPPolicy</a></h2>
+<h3><a name="GeoIPPolicy">GeoIPPolicy</a></h3>
 <strong>Syntax:</strong> GeoIPPolicy <em>"allow,deny"|"deny,allow"</em><br>
 <strong>Default:</strong> GeoIPPolicy allow,deny<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -187,10 +234,10 @@ the <code>mod_geoip</code> module will <b>reject</b> any connection,
 
 <p>
 <hr>
-<h2><a name="GeoIPTable">GeoIPTable</a></h2>
+<h3><a name="GeoIPTable">GeoIPTable</a></h3>
 <strong>Syntax:</strong> GeoIPTable <em>path</em> <em>[flags]</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_geoip<br>
 <strong>Compatibility:</strong> 1.3.3rc1 and later
 
@@ -272,43 +319,33 @@ Examples:
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_geoip</code> module requires that the GeoIP library be installed.
-
-<p>
-After installing GeoIP, follow the usual steps for using contrib modules in
-proftpd:
-To install <code>mod_geoip</code>, copy the <code>mod_geoip.c</code>
-file into:
+For including <code>mod_geoip</code> as a staticly linked module:
 <pre>
-  cp mod_geoip.c <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  For including
-<code>mod_geoip</code> as a staticly linked module:
-<pre>
-  ./configure --with-modules=mod_geoip
+  $ ./configure --with-modules=mod_geoip
 </pre>
 Alternatively, <code>mod_geoip</code> could be built as a DSO module:
 <pre>
-  ./configure --with-shared=mod_geoip
+  $ ./configure --with-shared=mod_geoip
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 You may need to specify the location of the GeoIP header and library files in
 your <code>configure</code> command, <i>e.g.</i>:
 <pre>
-  ./configure --with-modules=mod_geoip \
+  $ ./configure --with-modules=mod_geoip \
     --with-includes=/usr/local/geoip/include \
     --with-libraries=/usr/local/geoip/lib
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_geoip</code> as a shared
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_geoip</code> as a shared
 module:
 <pre>
-  prxs -c -i -d mod_geoip.c
+  $ prxs -c -i -d mod_geoip.c
 </pre>
 
 <p>
@@ -385,9 +422,12 @@ under keys of the names above.
 <b>Logging</b><br>
 The <code>mod_geoip</code> module supports different forms of logging.  The
 main module logging is done via the <code>GeoIPLog</code> directive.
-For debugging purposes, the module also uses <a href="http://www.proftpd.org/docs/howto/Tracing.html">trace logging</a>, via the module-specific "geoip" log
-channel.  Thus for trace logging, to aid in debugging, you
-would use the following in your <code>proftpd.conf</code>:
+For debugging purposes, the module also uses <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>geoip
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
 <pre>
   TraceLog /path/to/ftpd/trace.log
   Trace geoip:20
@@ -435,21 +475,36 @@ according to need, demand, inclination, and time:
   </IfClass>
 </pre>
 
-<p>
-<hr><br>
+<p><a name="GeoIPMultipleRules">
+<font color=red>Question</font>: How I can require that a connection match
+multiple rules, <i>e.g.</i> both a <code>RegionCode</code> <i>and</i> a
+<code>CountryCode</code>?<br>
+<font color=blue>Answer</font>: In a given <code>GeoIPAllowFilter</code> or
+<code>GeoIPDenyFilter</code>, you can configure a <i>list</i> of
+filters/patterns.  And <b>all</b> of these filters <b>must</b> be matched,
+in order for that <code>GeoIPAllowFilter</code> or <code>GeoIPDenyFilter</code>
+to be matched.  Thus you can use:
+<pre>
+  # Deny all connections, unless they are explicitly allowed
+  GeoIPPolicy deny,allow
 
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-12-12 16:52:28 $</i><br>
+  # Allow only connections from TX, US
+  GeoIPAllowFilter RegionCode TX CountryCode US
+</pre>
 
-<br><hr>
+<p><a name="GeoIPIPv6">
+<font color=red>Question</font>: Does <code>mod_geoip</code> support IPv6?<br>
+<font color=blue>Answer</font>: Yes, <em>assuming</em> that the IPv6
+MaxMind tables are configured via <code>GeoIPTable</code>.  You can find
+the free IPv6 country and city "legacy" files; see this MaxMind <a href="https://www.maxmind.com/en/ipv6-information-and-faq">IPv6 FAQ</a> for details.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2010-2013 TJ Saunders<br>
+© Copyright 2010-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_ifsession.html b/doc/contrib/mod_ifsession.html
index 76f4999..436907a 100644
--- a/doc/contrib/mod_ifsession.html
+++ b/doc/contrib/mod_ifsession.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ifsession.html,v 1.7 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_ifsession.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ifsession</title>
@@ -86,7 +84,7 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="IfAuthenticated"><IfAuthenticated></a></h2>
+<h3><a name="IfAuthenticated"><IfAuthenticated></a></h3>
 <strong>Syntax:</strong> <IfAuthenticated><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -112,7 +110,7 @@ Examples:
 See also: <a href="#IfGroup"><IfGroup></a>, <a href="#IfUser"><IfUser></a>
 
 <hr>
-<h2><a name="IfClass"><IfClass></a></h2>
+<h3><a name="IfClass"><IfClass></a></h3>
 <strong>Syntax:</strong> <IfClass <em>["AND"|"OR"] class-expression|"regex" regexp</em>><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -157,7 +155,7 @@ See also: <a href="#IfGroup"><IfGroup></a>, <a href="#IfUser"><IfUser&g
 
 <p>
 <hr>
-<h2><a name="IfGroup"><IfGroup></a></h2>
+<h3><a name="IfGroup"><IfGroup></a></h3>
 <strong>Syntax:</strong> <IfGroup <em>["AND"|"OR"] group-expression|"regex" regexp</em>><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -199,7 +197,7 @@ See also: <a href="#IfClass"><IfClass></a>, <a href="#IfUser"><IfUser&g
 
 <p>
 <hr>
-<h2><a name="IfUser"><IfUser></a></h2>
+<h3><a name="IfUser"><IfUser></a></h3>
 <strong>Syntax:</strong> <IfUser <em>["AND"|"OR"] user-expression|"regex" regexp</em>><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -276,17 +274,12 @@ Expressions, AND vs OR
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_ifsession</code>, copy the <code>mod_ifsession.c</code>
-file into:
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  Then follow the
-usual steps for using third-party modules in proftpd:
+The <code>mod_ifsession</code> module is distributed with ProFTPD.  Follow the
+usual steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_ifsession
-  make
-  make install
+  $ ./configure --with-modules=mod_ifsession
+  $ make
+  $ make install
 </pre>
 Note that <code>mod_ifsession</code> should be the <b>last</b> module
 in the <code>--with-modules</code> list, if multiple modules are listed.
@@ -296,37 +289,44 @@ properly by other modules.
 <p>
 To build <code>mod_ifsession</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_ifsession
+  $ ./configure --enable-dso --with-shared=mod_ifsession
 </pre>
 Then follow the usual steps:
 <pre>
-  make 
-  make install
+  $ make 
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_ifsession</code> as a shared
-module:
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_ifsession</code> as a
+shared module:
 <pre>
-  prxs -c -i -d mod_ifsession.c
+  $ prxs -c -i -d mod_ifsession.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_ifsession</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>ifsession
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace ifsession:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2013 TJ Saunders<br>
+© Copyright 2000-2014 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_ifversion.html b/doc/contrib/mod_ifversion.html
index 6518b12..7d8b05a 100644
--- a/doc/contrib/mod_ifversion.html
+++ b/doc/contrib/mod_ifversion.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ifversion.html,v 1.1 2010-06-29 14:52:30 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_ifversion.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ifversion</title>
@@ -43,7 +41,7 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="IfVersion"><IfVersion></a></h2>
+<h3><a name="IfVersion"><IfVersion></a></h3>
 <strong>Syntax:</strong> <IfVersion <em>[[!]operator] version</em>><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <em>All</em><br>
@@ -66,7 +64,7 @@ candidate (RC) or maintenance release.
 <p>
 The following numerical comparison operators are supported:
 <p>
-<table border=1>
+<table border=1 summary="Numeric Comparators">
   <tr>
     <td><em>operator</em></td>
     <td>Description</td>
@@ -99,10 +97,10 @@ The following numerical comparison operators are supported:
 </table>
 
 <p>
-It is also possible to use regular expressions to match the proftpd version.
+It is also possible to use regular expressions to match the ProFTPD version.
 To use a regular expression, the <em>operators</em> are:
 <p>
-<table border=1>
+<table border=1 summary="Regex Operators">
   <tr>
     <td><em>operator</em></td>
     <td>Description</td>
@@ -133,24 +131,19 @@ If <em>operator</em> is omitted, it is assumed to be <code>=</code>.
 
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_ifversion</code>, copy the <code>mod_ifversion.c</code>
-file into:
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  For including
-<code>mod_ifversion</code> as a staticly linked module:
+The <code>mod_ifversion</code> module is distributed with ProFTPD.  For
+including <code>mod_ifversion</code> as a staticly linked module:
 <pre>
-  ./configure --with-modules=mod_ifversion
+  $ ./configure --with-modules=mod_ifversion
 </pre>
 To build <code>mod_ifversion</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_ifversion
+  $ ./configure --enable-dso --with-shared=mod_ifversion
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -158,7 +151,7 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_ifversion</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_ifversion.c
+  $ prxs -c -i -d mod_ifversion.c
 </pre>
 
 <hr>
@@ -194,20 +187,12 @@ Using a reversed regular expression (<i>i.e.</i> in this case, meaning
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2010-06-29 14:52:30 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2009-2010 TJ Saunders<br>
+© Copyright 2009-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_ldap.html b/doc/contrib/mod_ldap.html
index 4efb0f7..a928c33 100644
--- a/doc/contrib/mod_ldap.html
+++ b/doc/contrib/mod_ldap.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ldap.html,v 1.2 2014-01-21 22:11:45 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_ldap.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ldap</title>
@@ -32,32 +30,29 @@ questions, concerns, or suggestions regarding this module.
   <li><a href="#LDAPAliasDereference">LDAPAliasDereference</a>
   <li><a href="#LDAPAttr">LDAPAttr</a>
   <li><a href="#LDAPAuthBinds">LDAPAuthBinds</a>
-  <li><a href="#LDAPDNInfo">LDAPDNInfo</a>
+  <li><a href="#LDAPBindDN">LDAPBindDN</a>
   <li><a href="#LDAPDefaultAuthScheme">LDAPDefaultAuthScheme</a>
   <li><a href="#LDAPDefaultGID">LDAPDefaultGID</a>
+  <li><a href="#LDAPDefaultQuota">LDAPDefaultQuota</a>
   <li><a href="#LDAPDefaultUID">LDAPDefaultUID</a>
-  <li><a href="#LDAPDoAuth">LDAPDoAuth</a>
-  <li><a href="#LDAPDoGIDLookups">LDAPDoGIDLookups</a>
-  <li><a href="#LDAPDoQuotaLookups">LDAPDoQuotaLookups</a>
-  <li><a href="#LDAPDoUIDLookups">LDAPDoUIDLookups</a>
   <li><a href="#LDAPForceDefaultGID">LDAPForceDefaultGID</a>
   <li><a href="#LDAPForceDefaultUID">LDAPForceDefaultUID</a>
   <li><a href="#LDAPForceGeneratedHomedir">LDAPForceGeneratedHomedir</a>
   <li><a href="#LDAPGenerateHomedir">LDAPGenerateHomedir</a>
   <li><a href="#LDAPGenerateHomedirPrefix">LDAPGenerateHomedirPrefix</a>
   <li><a href="#LDAPGenerateHomedirPrefixNoUsername">LDAPGenerateHomedirPrefixNoUsername</a>
+  <li><a href="#LDAPGroups">LDAPGroups</a>
   <li><a href="#LDAPLog">LDAPLog</a>
-  <li><a href="#LDAPNegativeCache">LDAPNegativeCache</a>
   <li><a href="#LDAPProtocolVersion">LDAPProtocolVersion</a>
   <li><a href="#LDAPQueryTimeout">LDAPQueryTimeout</a>
   <li><a href="#LDAPSearchScope">LDAPSearchScope</a>
   <li><a href="#LDAPServer">LDAPServer</a>
-  <li><a href="#LDAPUseSSL">LDAPUseSSL</a>
+  <li><a href="#LDAPUsers">LDAPUsers</a>
   <li><a href="#LDAPUseTLS">LDAPUseTLS</a>
 </ul>
 
 <hr>
-<h2><a name="LDAPAliasDereference">LDAPAliasDereference</a></h2>
+<h3><a name="LDAPAliasDereference">LDAPAliasDereference</a></h3>
 <strong>Syntax:</strong> LDAPAliasDereference <em>never|always|search|find</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -102,7 +97,7 @@ The default is "never", <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="LDAPAttr">LDAPAttr</a></h2>
+<h3><a name="LDAPAttr">LDAPAttr</a></h3>
 <strong>Syntax:</strong> LDAPAttr <em>old-attr-name new-attr-name</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -132,22 +127,25 @@ The following LDAP attributes can be renamed in this manner:
 
 <p>
 <hr>
-<h2><a name="LDAPAuthBinds">LDAPAuthBinds</a></h2>
+<h3><a name="LDAPAuthBinds">LDAPAuthBinds</a></h3>
 <strong>Syntax:</strong> LDAPAuthBinds <em>on|off</em><br>
-<strong>Default:</strong> None<br>
+<strong>Default:</strong> LDAPAuthBinds on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-By default, the DN specified by the <a href="#LDAPDNInfo"><code>LDAPDNInfo</code></a> will be used to bind to the LDAP server to obtain user information,
-including the <code>userPassword</code> attribute.  If <code>LDAPAuthBinds</code> is set to <em>on</em>, the DN specified by <code>LDAPDNInfo</code> will be
-used to fetch all user information <i>except</i> the <code>userPassword</code>
-attribute.  Then, the <code>mod_ldap</code> module will bind to the LDAP server
-as the user who is logging in via FTP with the user-supplied password.  If this
-bind succeeds, the user is considered authenticated and is allowed to log in.
-This method of LDAP authentication has the added benefit of supporting any
-password encryption scheme that your LDAP server supports.
+By default, the DN specified by the
+<a href="#LDAPBindDNo"><code>LDAPBindDN</code></a> will be used to bind to the
+LDAP server to obtain user information, including the <code>userPassword</code>
+attribute.  If <code>LDAPAuthBinds</code> is set to <em>on</em>, the DN
+specified by <code>LDAPDNInfo</code> will be used to fetch all user information
+<i>except</i> the <code>userPassword</code> attribute.  Then, the
+<code>mod_ldap</code> module will bind to the LDAP server as the user who is
+logging in via FTP with the user-supplied password.  If this bind succeeds,
+the user is considered authenticated and is allowed to log in.  This method of
+LDAP authentication has the added benefit of supporting any password encryption
+scheme that your LDAP server supports.
 
 <p>
 In versions of <code>mod_ldap</code> up to 2.7.6, the default for
@@ -156,24 +154,25 @@ the default value for <code>LDAPAuthBinds</code> is <em>on</em>.
 
 <p>
 <hr>
-<h2><a name="LDAPDNInfo">LDAPDNInfo</a></h2>
-<strong>Syntax:</strong> LDAPDNInfo <em>dn password</em><br>
+<h3><a name="LDAPBindDN">LDAPBindDN</a></h3>
+<strong>Syntax:</strong> LDAPBindDN <em>dn password</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
+<strong>Compatibility:</strong> 1.3.5rc1 and later
 
 <p>
-The <code>LDAPDNInfo</code> directive configures the DN and the password that
-<code>mod_ldap</code> will use when binding to the LDAP directory.  If this
-configuration directive is missing, then anonymous binds are used.
+The <code>LDAPBindDN</code> directive configures the <em>DN</em> and the
+<em>password</em> that <code>mod_ldap</code> will use when binding to the LDAP
+directory.  If this configuration directive is missing, then anonymous binds
+are used.
 
 <p>
 The default is:
 <pre>
   <IfModule mod_ldap.c>
     # Use anonymous binds
-    LDAPDNInfo "" ""
+    LDAPBindDN "" ""
   </IfModule>
 </pre>
 
@@ -182,7 +181,7 @@ See also: <a href="#LDAPServer"><code>LDAPServer</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPDefaultAuthScheme">LDAPDefaultAuthScheme</a></h2>
+<h3><a name="LDAPDefaultAuthScheme">LDAPDefaultAuthScheme</a></h3>
 <strong>Syntax:</strong> LDAPDefaultAuthScheme <em>crypt|clear</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -204,7 +203,7 @@ The default value is <em>crypt</em>.
 
 <p>
 <hr>
-<h2><a name="LDAPDefaultGID">LDAPDefaultGID</a></h2>
+<h3><a name="LDAPDefaultGID">LDAPDefaultGID</a></h3>
 <strong>Syntax:</strong> LDAPDefaultGID <em>gid</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -228,7 +227,21 @@ See also: <a href="#LDAPDefaultUID"><code>LDAPDefaultUID</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPDefaultUID">LDAPDefaultUID</a></h2>
+<h3><a name="LDAPDefaultQuota">LDAPDefaultQuota</a></h3>
+<strong>Syntax:</strong> LDAPDefaultQuota <em>default-quota</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_ldap<br>
+<strong>Compatibility:</strong> 1.3.5rc1 and later
+
+<p>
+The <code>LDAPDefaultQuota</code> directive configures a <em>default-quota</em>
+to use if a user does not have an <code>ftpQuota</code> attribute.  This
+parameter is formatted the same way as the <code>ftpQuota</code> LDAP attribute.
+
+<p>
+<hr>
+<h3><a name="LDAPDefaultUID">LDAPDefaultUID</a></h3>
 <strong>Syntax:</strong> LDAPDefaultUID <em>uid</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -250,22 +263,7 @@ automatically assign those users a single UID.
 <p>
 See also: <a href="#LDAPDefaultGID"><code>LDAPDefaultGID</code></a>
 
-<p>
-<hr>
-<h2><a name="LDAPDoAuth">LDAPDoAuth</a></h2>
-<strong>Syntax:</strong> LDAPDoAuth <em>off|on base-dn search-filter-template</em><br>
-<strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
-<strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
-
-<p>
-The <code>LDAPDoAuth</code> configuration directive activates LDAP
-authentication.  The second parameter to this directive is the LDAP base DN to
-use for authentication.  The third parameter is a template to be used for the
-search filter; <code>%v</code> will be replaced with the username that is being
-authenticated.
-
+DoAuth
 <p>
 By default, the search filter template used is:
 <pre>
@@ -273,180 +271,241 @@ By default, the search filter template used is:
 </pre>
 The <em>uid</em> for the the search filter is taken from the
 <code>LDAPAttr</code> directive.  Search filter templates are only supported
-in versions of <code>mod_ldap</code> 2.7 and later.</para>
+in versions of <code>mod_ldap</code> 2.7 and later.
 
 <p>
 See also: <a href="#LDAPAttr"><code>LDAPAttr</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPDoGIDLookups">LDAPDoGIDLookups</a></h2>
-<strong>Syntax:</strong> LDAPDoGIDLookups <em>off|on base-dn cn-filter-template gid-number-filter-template member-uid-filter-template</em><br>
+<h3><a name="LDAPForceDefaultGID">LDAPForceDefaultGID</a></h3>
+<strong>Syntax:</strong> LDAPForceDefaultGID <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-The <code>LDAPDoGIDLookups</code> directive activates LDAP GID-to-name lookups
-for directory listings.  The second parameter to this directive is the LDAP
-base DN to use for GID-to-name lookups.  The third through fifth parameters are
-templates to be used for the search filter; <code>%v</code> will be replaced
-with the GID that is being looked up.
+Even when a <a href="#LDAPDefaultGID"><code>LDAPDefaultGID</code></a> is
+configured, the <code>mod_ldap</code> module will allow individual users to
+have <code>gidNumber</code> attributes that will override this default GID.
+With <code>LDAPForceDefaultGID</code> directive configured to be <em>on</em>,
+all LDAP-authenticated users are given the default GID; GIDs may not be
+overridden by <code>gidNumber</code> attributes.
 
 <p>
-By default, the CN filter template look like this:
-<pre>
-  (&(LDAPAttr_cn=%v)(objectclass=posixGroup))
-</pre>
-The <code>gidNumber</code> filter template is:
-<pre>
-  (&(LDAPAttr_gidNumber=%v)(objectclass=posixGroup))
-</pre>
-and the <code>memberUid</code> filter template used is:
-  (&(LDAPAttr_memberUid=%v)(objectclass=posixGroup))
-</pre>
-Note that filter templates are only supported in <code>mod_ldap</code>
-version 2.8.3 and later.
+<hr>
+<h3><a name="LDAPForceDefaultUID">LDAPForceDefaultUID</a></h3>
+<strong>Syntax:</strong> LDAPForceDefaultUID <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_ldap<br>
+<strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-The attribute names used in the default search filters are taken from the
-<a href="#LDAPAttr"><code>LDAPAttr</code></a> directive.
+Even when a <a href="#LDAPDefaultUID"><code>LDAPDefaultUID</code></a> is
+configured, the <code>mod_ldap</code> module will allow individual users to
+have <code>uidNumber</code> attributes that will override this default UID.
+With <code>LDAPForceDefaultUID</code> directive configured to be <em>on</em>,
+all LDAP-authenticated users are given the default UID; UIDs may not be
+overridden by <code>uidNumber</code> attributes.
 
 <p>
 <hr>
-<h2><a name="LDAPDoQuotaLookups">LDAPDoQuotaLookups</a></h2>
-<strong>Syntax:</strong> LDAPDoQuotaLookups <em>off|on base-dn quota-filter-template default-quota</em><br>
+<h3><a name="LDAPForceGeneratedHomedir">LDAPForceGeneratedHomedir</a></h3>
+<strong>Syntax:</strong> LDAPForceGeneratedHomedir <em>off|on</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code
+><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-The <code>LDAPDoQuotaLookups</code> directive enables LDAP quota lookups.  The
-second parameter of this directive is the LDAP base DN to use for quota limit
-search.  The third parameter is a template to be used for the search filter;
-<code>%v</code> will be replaced with the username that is being authenticated.
+When no <code>homeDirectory</code> attribute is found, the <code>mod_ldap</code>
+module can be configured to <em>generate</em> a home directory using the
+<a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a> directive.
+If there <i>is</i> a <code>homeDirectory</code> attribute present, however,
+the <code>mod_ldap</code> module will use that attribute value as the home
+directory.
+
+<p>
+However, there may be cases where the administrator wishes to <em>override</em>
+the <code>homeDirectory</code> attribute, and thus to <i>always</i> use the
+home directory value that <code>mod_ldap</code> would generate.  The
+<code>LDAPForceGeneratedHomedir</code> directive is used in such cases.
 
 <p>
-By default, the search filter template is:
+For example, assume that the user logging in is named "tj", and has an
+LDAP object whose <code>homeDirectory</code> attribute value is "/home/tj".
+To <em>force</em> the use of <code>mod_ldap</code>'s generated home directory
+instead of that <code>homeDirectory</code> value, the configuration might
+look like:
 <pre>
-  (&(LDAPAttr_uid=%v)(objectclass=posixAccount))
+  LDAPForceGeneratedHomedir on
+  LDAPGenerateHomedir on
+  LDAPGenerateHomedirPrefix /var/ftp
 </pre>
-The <em>uid</em> for the the search filter is taken from the
-<a href="#LDAPAttr"><code>LDAPAttr</code></a> directive.  Note that search
-filter templates are only supported in <code>mod_ldap</code> version 2.7 and
-later.
+Using the above configuration, the home directory that the
+<code>mod_ldap</code> module would use is <code>/var/ftp/tj</code>, despite
+what <code>homeDirectory</code> attribute may be in the LDAP directory.
 
 <p>
-If specified, the <em>default-quota</em> parameter indicates the quota limits
-to use if a user does not have an <code>ftpQuota</code> attribute.  This
-parameter is formatted the same way as the <code>ftpQuota</code> LDAP
-attribute.
+<b>Note</b> that if <code>LDAPForceGeneratedHomedir</code> is enabled, then
+<code>LDAPGenerateHomedir</code> must <b>also</b> be enabled.  It is an error
+to enable <code>LDAPForceGeneratedHomedir</code> without also enabling
+<code>LDAPGenerateHomdir</code>.
+
+<p>
+See also: <a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a>, <a href="#LDAPGenerateHomedirPrefix"><code>LDAPGenerateHomedirPrefix</code></a>, <a href="#LDAPGenerateHomedirPrefixNoUsername"><code>LDAPGenerateHomedirPrefixNoUsername</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPDoUIDLookups">LDAPDoUIDLookups</a></h2>
-<strong>Syntax:</strong> LDAPDoUIDLookups <em>off|on base-dn uid-filter-template</em><br>
+<h3><a name="LDAPGenerateHomedir">LDAPGenerateHomedir</a></h3>
+<strong>Syntax:</strong> LDAPGenerateHomedir <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-The <code>LDAPDoUIDLookups</code> directive activates LDAP UID-to-name lookups
-for directory listings.  The second parameter to this directive is the LDAP
-base DN to use for UID-to-name lookups.  The third parameter is a
-template to be used for the search filter; <code>%v</code> will be replaced
-with the UID that is being looked up.
+By default, the <code>mod_ldap</code> module uses the <code>homeDirectory</code>
+attribute to determine what home directory to use for the session.  Sometimes,
+however, an administrator will want to use a <i>different</i> home directory
+for these FTP/SFTP sessions, something other than the path in the
+<code>homeDirectory</code> attribute.  The <code>LDAPGenerateHomedir</code>
+directive is used for situations like this.
 
 <p>
-By default, the search filter template looks like this:
-<pre>
-  (&(LDAPAttr_uidNumber=%v)(objectclass=posixGroup))
-</pre>
-The <em>uidNumber</em> attribute name used in the search filter comes from
-the <a href="#LDAPAttr"><code>LDAPAttr</code></a> directive.
-Note that filter templates are only supported in <code>mod_ldap</code>
-version 2.7 and later.
+The <code>LDAPGenerateHomedir</code> directive configures the
+<code>mod_ldap</code> module to "generate" a new home directory value,
+<em>overriding</em> the value from the <code>homeDirectory</code> attribute.
+The generated home directory value <b>requires</b> that a starting point
+for the new home directory, a "prefix", also be provided using the
+<a href="#LDAPGenerateHomedirPrefix"><code>LDAPGenerateHomedirPrefix</code></a>
+directive.
 
 <p>
-<hr>
-<h2><a name="LDAPForceDefaultGID">LDAPForceDefaultGID</a></h2>
-<strong>Syntax:</strong> LDAPForceDefaultGID <em>on|off</em><br>
-<strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
-<strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
+The <code>LDAPGenerateHomedir</code> directives does <b>not</b> cause the
+new home directory to be <em>created on the filesystem</em>.  It only changes
+the home directory value that the <code>mod_ldap</code> module provides to
+the ProFTPD engine.  The <i>creation</i> of the home directory, if it does
+not already exist, is done using the
+<a href="../howto/CreateHome.html"><code>CreateHome</code></a> directive.
 
 <p>
-Even when a <a href="#LDAPDefaultGID"><code>LDAPDefaultGID</code></a> is
-configured, the <code>mod_ldap</code> module will allow individual users to
-have <code>gidNumber</code> attributes that will override this default GID.
-With <code>LDAPForceDefaultGID</code> directive configured to be <em>on</em>,
-all LDAP-authenticated users are given the default GID; GIDs may not be
-overridden by <code>gidNumber</code> attributes.
+See also: <a href="#LDAPGenerateHomedirPrefix"><code>LDAPGenerateHomedirPrefix</code></a>, <a href="#LDAPGenerateHomedirPrefixNoUsername"><code>LDAPGenerateHomedirPrefixNoUsername</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPForceDefaultUID">LDAPForceDefaultUID</a></h2>
-<strong>Syntax:</strong> LDAPForceDefaultUID <em>on|off</em><br>
+<h3><a name="LDAPGenerateHomedirPrefix">LDAPGenerateHomedirPrefix</a></h3>
+<strong>Syntax:</strong> LDAPGenerateHomedirPrefix <em>prefix</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> server config<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-Even when a <a href="#LDAPDefaultUID"><code>LDAPDefaultUID</code></a> is
-configured, the <code>mod_ldap</code> module will allow individual users to
-have <code>uidNumber</code> attributes that will override this default UID.
-With <code>LDAPForceDefaultUID</code> directive configured to be <em>on</em>,
-all LDAP-authenticated users are given the default UID; UIDs may not be
-overridden by <code>uidNumber</code> attributes.
+The <code>LDAPGenerateHomedirPrefix</code> directive is used when
+<code>LDAPGenerateHomedir</code> is enabled, causing the <code>mod_ldap</code>
+module to <em>generate</em> a <b>default</b> home directory, when the
+<code>homeDirectory</code> attribute value is not present.  The generated home
+directory value like this:
+<pre>
+  <i>prefix</i>/<i>username</i>
+</pre>
+The configured <em>prefix</em> string has the username (from the
+<code>uid</code> attribute) appended to generate the home directory value for
+the user.
 
 <p>
-<hr>
-<h2><a name="LDAPForceGeneratedHomedir">LDAPForceGeneratedHomedir</a></h2>
-<strong>Syntax:</strong> LDAPForceGeneratedHomedir <em>off|on directory-mode</em><br>
-<strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code
-><Global></code><br>
-<strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
+For example:
+<pre>
+  LDAPGenerateHomedir on
+  LDAPGenerateHomedirPrefix /var/ftp
+</pre>
+Using the above configuration, and assuming a user name of "tj", the home
+directory that the <code>mod_ldap</code> module would use is
+<code>/var/ftp/tj</code>, no matter what the <code>homeDirectory</code>
+attribute may be in the LDAP directory.
 
 <p>
-See also: <a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a>, <a href="#LDAPGenerateHomedirPrefix"><code>LDAPGenerateHomedirPrefix</code></a>, <a href="#LDAPGenerateHomedirPrefixNoUsername"><code>LDAPGenerateHomedirPrefixNoUsername</code></a>
+See also: <a href="#LDAPForceGeneratedHomedir"><code>LDAPForceGeneratedHomedir</code></a>, <a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a>, <a href="#LDAPGenerateHomedirPrefixNoUsername"><code>LDAPGenerateHomedirPrefixNoUsername</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPGenerateHomedir">LDAPGenerateHomedir</a></h2>
-<strong>Syntax:</strong> LDAPGenerateHomedir <em>on|off</em><br>
+<h3><a name="LDAPGenerateHomedirPrefixNoUsername">LDAPGenerateHomedirPrefixNoUsername</a></h3>
+<strong>Syntax:</strong> LDAPGenerateHomedirPrefixNoUsername <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-<hr>
-<h2><a name="LDAPGenerateHomedirPrefix">LDAPGenerateHomedirPrefix</a></h2>
-<strong>Syntax:</strong> LDAPGenerateHomedirPrefix <em>prefix</em><br>
-<strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
-<strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
+When the <a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a>
+and <a href="#LDAPGenerateHomedirPrefix"><code>LDAPGenerateHomedirPrefix</code></a> directives are used, the <em>generated</em> home directory value for
+the session is:
+<pre>
+  <i>prefix</i>/<i>username</i>
+</pre>
+However, there may be cases where the administrator does <b>not</b> want
+the username automatically appended to the generated value, and instead wishes
+to use <i>just</i> the prefix as the home directory.  For these use cases,
+use the <code>LDAPGenerateHomedirPrefixNoUsername</code> directive.
+
+<p>
+For example:
+<pre>
+  LDAPGenerateHomedir on
+  LDAPGenerateHomedirPrefix /var/ftp
+  LDAPGenerateHomedirPrefixNoUsername on
+</pre>
+Using the above configuration, and assuming a user name of "tj", the home
+directory that the <code>mod_ldap</code> module would use is
+<code>/var/ftp</code>, no matter what the <code>homeDirectory</code> attribute
+may be in the LDAP directory.
+
+<p>
+See also: <a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a>, <a href="#LDAPGenerateHomedirPrefix"><code>LDAPGenerateHomedirPrefix</code></a>
 
 <p>
 <hr>
-<h2><a name="LDAPGenerateHomedirPrefixNoUsername">LDAPGenerateHomedirPrefixNoUsername</a></h2>
-<strong>Syntax:</strong> LDAPGenerateHomedirPrefixNoUsername <em>on|off</em><br>
+<h3><a name="LDAPGroups">LDAPGroups</a></h3>
+<strong>Syntax:</strong> LDAPGroups <em>base-dn cn-filter-template gid-number-filter-template member-uid-filter-template</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
+<strong>Compatibility:</strong> 1.3.5rc1 and later
+
+<p>
+The <code>LDAPGroups</code> directive activates LDAP GID-to-name lookups for
+directory listings.  The first parameter to this directive is the LDAP
+<em>base DN</em> to use for GID-to-name lookups.  The second through fourth
+optional parameters are templates to be used for the search filter;
+<code>%v</code> will be replaced with the GID that is being looked up.
+
+<p>
+By default, the CN filter template look like this:
+<pre>
+  (&(LDAPAttr_cn=%v)(objectclass=posixGroup))
+</pre>
+The <code>gidNumber</code> filter template is:
+<pre>
+  (&(LDAPAttr_gidNumber=%v)(objectclass=posixGroup))
+</pre>
+and the <code>memberUid</code> filter template used is:
+<pre>
+  (&(LDAPAttr_memberUid=%v)(objectclass=posixGroup))
+</pre>
+Note that filter templates are only supported in <code>mod_ldap</code>
+version 2.8.3 and later.
+
+<p>
+The attribute names used in the default search filters are taken from the
+<a href="#LDAPAttr"><code>LDAPAttr</code></a> directive.
 
 <p>
 <hr>
-<h2><a name="LDAPLog">LDAPLog</a></h2>
+<h3><a name="LDAPLog">LDAPLog</a></h3>
 <strong>Syntax:</strong> LDAPLog <em>file|"none"</em><br>
 <strong>Default:</strong> <em>None</em><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -465,29 +524,7 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="LDAPNegativeCache">LDAPNegativeCache</a></h2>
-<strong>Syntax:</strong> LDAPNegativeCache <em>on|off</em><br>
-<strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
-<strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.2.7rc1 and later
-
-<p>
-The <code>LDAPNegativeCache</code> directive specifies whether or not to cache
-negative responses from the LDAP server when using LDAP for UID/GID lookups.
-This option is useful if you also use/are in transition from another
-authentication system; if there are many users in your old authentication
-system that aren't in the LDAP database, there can be a significant delay when
-a directory listing is performed as the UIDs not in the LDAP database are
-repeatedly looked up in an attempt to present usernames instead of UIDs in
-directory listings. With <code>LDAPNegativeCache</code> set to <em>on</em>,
-negative ("not found") responses from the LDAP server will be cached and speed
-will improve on directory listings that contain many users not present in the
-LDAP database.
-
-<p>
-<hr>
-<h2><a name="LDAPProtocolVersion">LDAPProtocolVersion</a></h2>
+<h3><a name="LDAPProtocolVersion">LDAPProtocolVersion</a></h3>
 <strong>Syntax:</strong> LDAPProtocolVersion <em>2|3</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -501,7 +538,7 @@ LDAP servers.  The default protocol version used is <em>3</em>.
 
 <p>
 <hr>
-<h2><a name="LDAPQueryTimeout">LDAPQueryTimeout</a></h2>
+<h3><a name="LDAPQueryTimeout">LDAPQueryTimeout</a></h3>
 <strong>Syntax:</strong> LDAPQueryTimeout <em>secs</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -515,8 +552,8 @@ value is determined by your LDAP API.
 
 <p>
 <hr>
-<h2><a name="LDAPSearchScope">LDAPSearchScope</a></h2>
-<strong>Syntax:</strong> LDAPSearchScope <em>onelevel|subtree</em><br>
+<h3><a name="LDAPSearchScope">LDAPSearchScope</a></h3>
+<strong>Syntax:</strong> LDAPSearchScope <em>base|onelevel|subtree</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
@@ -529,9 +566,45 @@ in the tree from the current level down.  Setting this directive to
 <em>onelevel</em> searches only one level deep in the LDAP tree.
 
 <p>
+<b>Note</b> that the <code>LDAPSearchScope</code> directive <b>cannot</b> be
+used when the LDAP URL syntax, rather than hostname/port, is used for your
+<a href="#LDAPServer"><code>LDAPServer</code></a> configuration.  Why not?
+The search scope can be specified as part of the URL itself.  This, combined
+with the fact that the <code>LDAPServer</code> directive can take
+<i>multiple</i> hosts/URLs, makes it clear to include the search scope in the
+URLs as needed.
+
+<p>
+If you are <b>not</b> using the LDAP URL syntax, then the following will
+use the <em>subtree</em> search scope:
+<pre>
+  LDAPServer ldap.example.com
+</pre>
+or, to make it explicit in your configuration:
+<pre>
+  LDAPServer ldap.example.com
+  LDAPSearchScope subtree
+</pre>
+On the other hand, if you <b>are</b> using LDAP URLs, then you specify the
+search scope as part of the URL:
+<pre>
+  LDAPServer ldap://ldap.example.com/??sub
+</pre>
+It is <b>important</b> that the "/" after the hostname/port be part of your
+LDAP URL when specifying the search scope.  That is, using:
+<pre>
+  LDAPServer ldap://ldap.example.com??sub
+</pre>
+<b>will not work as expected</b>; see
+<a href="https://tools.ietf.org/html/rfc2255">RFC 2255</a>, Section 3.  LDAP
+URL parameters are <b>not</b> like HTTP URL query parameters; LDAP URL
+parameters <b>are</b> order-specific.  And the "/" before any of the
+optional parameters <b>is required</b>.
+
+<p>
 <hr>
-<h2><a name="LDAPServer">LDAPServer</a></h2>
-<strong>Syntax:</strong> LDAPServer <em>"host1:port1 host2:port2"</em><br>
+<h3><a name="LDAPServer">LDAPServer</a></h3>
+<strong>Syntax:</strong> LDAPServer <em>"url1|host1:port1 url2|host2:port2"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
@@ -541,7 +614,8 @@ in the tree from the current level down.  Setting this directive to
 The <code>LDAPServer</code> directive allows you to to specify the hostname(s)
 and port(s) of the LDAP server(s) to use for LDAP authentication. If no
 <code>LDAPServer</code> configuration directive is present, the default LDAP
-servers specified by your LDAP library will be used.
+servers specified by your LDAP library will be used.  Note that the LDAP
+URL syntax may also be used.
 
 <p>
 To specify multiple LDAP servers, enclose the entire list of servers in
@@ -549,19 +623,64 @@ quotation marks.  For example:
 <pre>
   LDAPServer "host1:port1 host2:port2"
 </pre>
+or:
+<pre>
+  LDAPServer "url1 url2"
+</pre>
+Th default search scope for LDAP URLs is "base" (unless a scope is explicitly
+provided in the URL). This behavior differs from the
+<a href="#LDAPSearchScope"><code>LDAPSearchScope</code></a> directive, which
+defaults to "subtree".
+
+<p>
+<b>Note</b> that to use LDAPS (LDAP over SSL), use the <em>URL</em> format,
+<i>e.g.</i>:
+<pre>
+  LDAPServer ldaps://host1:port1 ldaps://host2:port2
+</pre>
+Alternatively, you can use the <a href="#LDAPUseTLS"><code>LDAPUseTLS</code></a>
+directive, <i>e.g.</i>:
+<pre>
+  LDAPServer ldap://host1:port1 ldap://host2:port2
+  LDAPUseTLS on
+</pre>
+to tell <code>mod_ldap</code> to use LDAP's <a href="https://en.wikipedia.org/wiki/STARTTLS">STARTTLS</a> mechanism.
 
 <p>
 <hr>
-<h2><a name="LDAPUseSSL">LDAPUseSSL</a></h2>
-<strong>Syntax:</strong> LDAPUseSSL <em>on|off</em><br>
-<strong>Default:</strong> off<br>
+<h3><a name="LDAPUsers">LDAPUsers</a></h3>
+<strong>Syntax:</strong> LDAPUsers <em>base-dn [name-filter-template [uid-filter-template]]</em><br>
+<strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ldap<br>
-<strong>Compatibility:</strong> 1.3.1rc1 and later
+<strong>Compatibility:</strong> 1.3.5rc1 and later
+
+<p>
+The <code>LDAPUsers</code> directive activates LDAP UID-to-name lookups
+for directory listings.  The first parameter to this directive is the LDAP
+<em>base DN</em> to use for UID-to-name lookups.  The optional second parameter
+is a template to be used for the search filter for the username; <code>%v</code>
+will be replaced with the UID that is being looked up.  Similarly, an optional
+third parameter is also a template, to be used for the search filter for
+the UID.
+
+<p>
+By default, the name search filter template looks like this:
+<pre>
+  (&(uid=%v)(objectclass=posixAccount))
+</pre>
+and the UID search filter template looks like this:
+<pre>
+  (&(LDAPAttr_uidNumber=%v)(objectclass=posixGroup))
+</pre>
+The <em>uidNumber</em> attribute name used in the search filter comes from
+the <a href="#LDAPAttr"><code>LDAPAttr</code></a> directive.
+Note that filter templates are only supported in <code>mod_ldap</code>
+version 2.7 and later.
 
 <p>
 <hr>
-<h2><a name="LDAPUseTLS">LDAPUseTLS</a></h2>
+<h3><a name="LDAPUseTLS">LDAPUseTLS</a></h3>
 <strong>Syntax:</strong> LDAPUseTLS <em>on|off</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -570,8 +689,7 @@ quotation marks.  For example:
 
 <p>
 The <code>LDAPUseTLS</code> directive configures whether <code>mod_ldap</code>
-will use SSL/TLS to protect the connections made to the configured LDAP
-servers.
+will use SSL/TLS via <a href="https://en.wikipedia.org/wiki/STARTTLS">STARTTLS</a> to protect the connections made to the configured LDAP servers.
 
 <p>
 By default, the <code>mod_ldap</code> module connects to the LDAP server via 
@@ -582,28 +700,24 @@ authenticate users; <code>mod_ldap</code> will <b>not</b> fall back to an
 unsecure connection.
 
 <p>
-<hr><br>
-<h2><a name="Usage">Usage</a></h2>
-
-<p>
-<hr><br>
+<hr>
 <h2><a name="Installation">Installation</a></h2>
-Follow the normal steps for using third-party modules in proftpd: 
+Follow the normal steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_ldap
-  make
-  make install
+  $ ./configure --with-modules=mod_ldap
+  $ make
+  $ make install
 </pre>
 You may need to specify the location of the OpenLDAP header and library files
-in your <code>configure</i> command, <i>e.g.</i>:
+in your <code>configure</code> command, <i>e.g.</i>:
 <pre>
- ./configure --with-modules=mod_ldap \
+ $ ./configure --with-modules=mod_ldap \
     --with-includes=/usr/local/openldap/include \
     --with-libraries=/usr/local/openldap/lib
 </pre>
 
 <p>
-<hr><br>
+<hr>
 <h2><a name="Usage">Usage</a></h2>
 
 <p>
@@ -613,12 +727,12 @@ server.  Note that this configuration has not been tested; if it works for
 you (or not), please let us know:
 <pre>
   <IfModule mod_ldap.c>
-    LDAPServer dc.example.org:3268
+    LDAPServer ldaps://dc.example.org:3268
     LDAPUseTLS on
     LDAPAuthBinds on
-    LDAPDNInfo "cn=SRV_ACC_SVN_AUTH,ou=special accounts,ou=Sales,dc=example,dc=org" ******************
+    LDAPBindDN "cn=SRV_ACC_SVN_AUTH,ou=special accounts,ou=Sales,dc=example,dc=org" ******************
 
-    LDAPDoAuth on ou=Users,ou=Sales,dc=example,dc=org "(&(sAMAccountName=%u)(objectclass=user)(memberOf=cn=Linux Admins,ou=Groups,ou=Sales,dc=example,DC=org))"
+    LDAPUsers ou=Users,ou=Sales,dc=example,dc=org "(&(sAMAccountName=%u)(objectclass=user)(memberOf=cn=Linux Admins,ou=Groups,ou=Sales,dc=example,dc=com))"
     LDAPSearchScope subtree
 
     # Assign default IDs
@@ -632,24 +746,106 @@ you (or not), please let us know:
     # Use different attribute names where necessary
     LDAPAttr uid sAMAccountName
     LDAPAttr gidNumber primaryGroupID
-
   </IfModule>
 </pre>
 
 <p>
-<hr><br>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-21 22:11:45 $</i><br>
+<b>Logging</b><br>
+The <code>mod_ldap</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>ldap
+  <li>ldap.library
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace ldap:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
-<br><hr>
+<p><a name="FAQ">
+<b>Frequently Asked Questions</b><br>
 
+<p><a name="ScopesFAQ">
+<font color=red>Question</font>: Why is <code>mod_ldap</code> using a "base"
+scope by default, rather than "subtree"?  I configured:
+<pre>
+  LDAPSearchScope subtree
+</pre>
+but it is not working; I see the following in my LDAP server logs:
+<pre>
+  slapd[31709]: conn=20239 op=1 SRCH <b>base</b>="ou=people,dc=example,dc=com" scope=0 deref=0 filter="(&(uid=tj)(objectClass=posixAccount))"
+</pre>
+<font color=blue>Answer</font>: The use of the "base" scope for searches, in
+spite of any <code>LDAPSearchScope</code> directive, happens when a URL, rather
+than hostname/port, are used in the <code>LDAPServer</code> directive. <a href="https://tools.ietf.org/html/rfc2255">RFC 2255</a>, Section 3 specifies that the default scope is "base".
+
+<p>
+Thus instead of:
+<pre>
+  LDAPServer ldap://ldap.example.com
+</pre>
+you will need to use:
+<pre>
+  LDAPServer ldap://ldap.example.com/??sub
+</pre>
+See the <a href="#LDAPSearchScope"><code>LDAPSearchScope</code></a>
+documentation for more details.
+
+<p><a name="HomedirsFAQ">
+<font color=red>Question</font>: How do I use <code>LDAPGenerateHomedir</code>
+and <code>CreateHome</code> together successfully?  Can I use <i>just</i>
+<code>LDAPGenerateHomedir</code>?<br>
+<font color=blue>Answer</font>: If you want to have home directories for your
+LDAP users automatically <b>created</b>, you <b>do</b> need to use the
+<a href="../howto/CreateHome.html"><code>CreateHome</code></a> directive.
+Whether you <em>need</em> to use the <a href="#LDAPGenerateHomedir"><code>LDAPGenerateHomedir</code></a> directive is a different (but related) question.
+
+<p>
+The <code>LDAPGenerateHomedir</code> directive (and its relative <a href="#LDAPForceGeneratedHomedir"><code>LDAPForceGeneratedHomedir</code></a>) should be
+used <i>when you want to users to have a different home directory than is
+configured for them in LDAP</i>.  They are <b>not</b> used for creating these
+directories, just generating the paths to use.
+
+<p>
+Thus to <i>generate</i> a different home directory for your LDAP-defined users,
+<i>and</i> to have these different home directories <em>created</em>, you
+might use something like this:
+<pre>
+  <IfModule mod_ldap.c>
+    ...
+    LDAPGenerateHomedir on
+    LDAPGenerateHomedirPrefix /data
+    LDAPForceGeneratedHomedir on
+
+    # And make sure these home directories are created
+    CreateHome on 0770 skel /opt/ProFTPD/etc/skel
+    ...
+  </IfModule>
+</pre>
+
+<p><a name="MultipleBindsFAQ">
+<font color=red>Question</font>: In my LDAP server logs, I see ProFTPD make
+<i>multiple</i> binds for the same client logging in:
+<pre>
+  slapd[31709]: conn=20239 op=0 BIND dn="cn=admin,dc=example,dc=com" method=128
+  slapd[31709]: conn=20239 op=0 BIND dn="cn=admin,dc=example,dc=com" mech=SIMPLE ssf=0
+</pre>
+I was expecting just <i>one</i> bind.  Is this a bug, or is it expected
+behavior?<br>
+<font color=blue>Answer</font>: Yes, this <em>is</em> the expected behavior.
+See the <a href="#LDAPAuthBinds"><code>LDAPAuthBinds</code></a> directive
+for details.
+
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2013-2014 TJ Saunders<br>
+© Copyright 2013-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_load.html b/doc/contrib/mod_load.html
index 5590522..8618120 100644
--- a/doc/contrib/mod_load.html
+++ b/doc/contrib/mod_load.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_load.html,v 1.3 2005-01-01 20:15:55 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_load.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_load</title>
@@ -42,7 +40,7 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="MaxLoad">MaxLoad</a></h2>
+<h3><a name="MaxLoad">MaxLoad</a></h3>
 <strong>Syntax:</strong> MaxLoad <em>number|"none" [message]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -91,28 +89,20 @@ can be used in any <code>Display</code> file <i>e.g.</i>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 To install <code>mod_load</code>, follow the usual steps for using third-party
-modules in proftpd:
+modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_load
-  make
-  make install
+  $ ./configure --with-modules=mod_load
+  $ make
+  $ make install
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2005-01-01 20:15:55 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2000-2005 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_log_forensic.html b/doc/contrib/mod_log_forensic.html
index d15a230..ec36625 100644
--- a/doc/contrib/mod_log_forensic.html
+++ b/doc/contrib/mod_log_forensic.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_log_forensic.html,v 1.3 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_log_forensic.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_log_forensic</title>
@@ -24,10 +22,8 @@ the buffered log messages out to a file.  Installation instructions are
 discussed <a href="#Installation">here</a>.
 
 <p>
-The most current version of <code>mod_log_forensic</code> can be found at:
-<pre>
-  <a href="http://www.castaglia.org/proftpd/">http://www.castaglia.org/proftpd/</a>
-</pre>
+The most current version of <code>mod_log_forensic</code> is distributed with
+the ProFTPD source code.
 
 <h2>Author</h2>
 <p>
@@ -45,7 +41,7 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 <hr>
-<h2><a name="ForensicLogBufferSize">ForensicLogBufferSize</a></h2>
+<h3><a name="ForensicLogBufferSize">ForensicLogBufferSize</a></h3>
 <strong>Syntax:</strong> ForensicLogBufferSize <em>count</em><br>
 <strong>Default:</strong> 1024<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -61,7 +57,7 @@ see logged, when one of the
 
 <p>
 <hr>
-<h2><a name="ForensicLogCapture">ForensicLogCapture</a></h2>
+<h3><a name="ForensicLogCapture">ForensicLogCapture</a></h3>
 <strong>Syntax:</strong> ForensicLogCapture <em>log-type1 ...</em><br>
 <strong>Default:</strong> Unspec TransferLog syslog SystemLog ExtendedLog TraceLog<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -93,7 +89,7 @@ The supported log types are:
 
 <p>
 <hr>
-<h2><a name="ForensicLogCriteria">ForensicLogCriteria</a></h2>
+<h3><a name="ForensicLogCriteria">ForensicLogCriteria</a></h3>
 <strong>Syntax:</strong> ForensicLogCriteria <em>criterion1 ...</em><br>
 <strong>Default:</strong> FailedLogin UntimelyDeath<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -134,7 +130,7 @@ The currently supported criteria are:
 
 <p>
 <hr>
-<h2><a name="ForensicLogEngine">ForensicLogEngine</a></h2>
+<h3><a name="ForensicLogEngine">ForensicLogEngine</a></h3>
 <strong>Syntax:</strong> ForensicLogEngine <em>on|off</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -147,7 +143,7 @@ The <code>ForensicLogEngine</code> directive enables or disables the
 
 <p>
 <hr>
-<h2><a name="ForensicLogFile">ForensicLogFile</a></h2>
+<h3><a name="ForensicLogFile">ForensicLogFile</a></h3>
 <strong>Syntax:</strong> ForensicLogFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -166,33 +162,27 @@ absolute path.
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_log_forensic</code>, copy the
-<code>mod_log_forensic.c</code> into the third-party module directory in
-the proftpd source code:
+The <code>mod_log_forensic</code> module is distributed with ProFTPD.  For
+including <code>mod_log_forensic</code> as a staticly linked module, use:
 <pre>
-  # cp mod_log_forensic.c <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  For including
-<code>mod_log_forensic</code> as a staticly linked module:
-<pre>
-  ./configure --with-modules=mod_log_forensic ...
+  $ ./configure --with-modules=mod_log_forensic ...
 </pre>
 Alternatively, <code>mod_log_forensic</code> can be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_log_forensic ...
+  $ ./configure --enable-dso --with-shared=mod_log_forensic ...
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_log_forensic</code> as a
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_log_forensic</code> as a
 shared module:
 <pre>
-  prxs -c -i -d mod_log_forensic.c
+  $ prxs -c -i -d mod_log_forensic.c
 </pre>
 
 <p>
@@ -301,20 +291,12 @@ logging needed to help diagnose failed logins and such, without having
 the verbose logging enabled all of the time.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2011-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_qos.html b/doc/contrib/mod_qos.html
index 144ed1c..a3bb175 100644
--- a/doc/contrib/mod_qos.html
+++ b/doc/contrib/mod_qos.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_qos.html,v 1.2 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_qos.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_qos</title>
@@ -27,7 +25,7 @@ instructions are discussed <a href="#Installation">here</a>.
 
 <p>
 The most current version of <code>mod_qos</code> is distributed with
-the <code>proftpd<code> source code.
+the ProFTPD source code.
 
 <h2>Directives</h2>
 <ul>
@@ -35,10 +33,10 @@ the <code>proftpd<code> source code.
 </ul>
 
 <hr>
-<h2><a name="QoSOptions">QoSOptions</a></h2>
+<h3><a name="QoSOptions">QoSOptions</a></h3>
 <strong>Syntax:</strong> QoSOptions <em>"ctrlqos" value|"dataqos" value</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code><br>
 <strong>Module:</strong> mod_qos<br>
 <strong>Compatibility:</strong> 1.3.4rc1 and later
 
@@ -87,43 +85,36 @@ deprecated, and can have adverse effects on TCP congestion control:
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-For including <code>mod_qos</code> as a staticly linked module in your <code>proftpd</code>, use:
+For including <code>mod_qos</code> as a staticly linked module in your
+<code>proftpd</code>, use:
 <pre>
-  ./configure --with-modules=mod_qos
+  $ ./configure --with-modules=mod_qos
 </pre>
 Alternatively, <code>mod_qos</code> could be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_qos
+  $ ./configure --enable-dso --with-shared=mod_qos
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_qos</code> as a shared
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_qos</code> as a shared
 module:
 <pre>
-  prxs -c -i -d mod_qos.c
+  $ prxs -c -i -d mod_qos.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2010-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_quotatab.html b/doc/contrib/mod_quotatab.html
index 88d6292..c506930 100644
--- a/doc/contrib/mod_quotatab.html
+++ b/doc/contrib/mod_quotatab.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_quotatab.html,v 1.20 2014-01-27 22:08:41 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_quotatab.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_quotatab</title>
@@ -83,7 +81,7 @@ for overwritten files.
 </ul>
 
 <hr>
-<h2><a name="QuotaDefault">QuotaDefault</a></h2>
+<h3><a name="QuotaDefault">QuotaDefault</a></h3>
 <strong>Syntax:</strong> QuotaDefault <em>quota-type per-session limit-type bytes-avail-in bytes-avail-out bytes-avail-xfer files-avail-in files-avail-out files-avail-xfer</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -95,7 +93,7 @@ The <code>QuotaDefault</code> directive configures a "default" quota limit,
 to be used when a limit is not found for the current user (<i>e.g.</i> a limit
 has not yet been configured for the user via one of the mod_quotatab backends).
 Since this "default" needs to provide all of the limit information required
-by <code>mod_quotatab</b>, there are quite a few required parameters:
+by <code>mod_quotatab</code>, there are quite a few required parameters:
 <ul>
   <li><em>quota-type</em>
   <li><em>per-session</em>
@@ -118,7 +116,7 @@ For example, you might use:
 
 <p>
 <hr>
-<h2><a name="QuotaDirectoryTally">QuotaDirectoryTally</a></h2>
+<h3><a name="QuotaDirectoryTally">QuotaDirectoryTally</a></h3>
 <strong>Syntax:</strong> QuotaDirectoryTally <em>on|off</em><br>
 <strong>Default:</strong> QuotaDirectoryTally off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -132,7 +130,7 @@ a directory, removing a directory) into account when tallying.
 
 <p>
 <hr>
-<h2><a name="QuotaDisplayUnits">QuotaDisplayUnits</a></h2>
+<h3><a name="QuotaDisplayUnits">QuotaDisplayUnits</a></h3>
 <strong>Syntax:</strong> QuotaDisplayUnits <em>"b"|"Kb"|"Mb"|"Gb"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -158,7 +156,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="QuotaEngine">QuotaEngine</a></h2>
+<h3><a name="QuotaEngine">QuotaEngine</a></h3>
 <strong>Syntax:</strong> QuotaEngine <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -173,7 +171,7 @@ commenting out all <code>mod_quotatab</code> directives.
 
 <p>
 <hr>
-<h2><a name="QuotaExcludeFilter">QuotaExcludeFilter</a></h2>
+<h3><a name="QuotaExcludeFilter">QuotaExcludeFilter</a></h3>
 <strong>Syntax:</strong>  QuotaExcludeFilter <em>regex|"none"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -197,7 +195,7 @@ exclude particular directories, or just certain filenames.
 
 <p>
 <hr>
-<h2><a name="QuotaLimitTable">QuotaLimitTable</a></h2>
+<h3><a name="QuotaLimitTable">QuotaLimitTable</a></h3>
 <strong>Syntax:</strong>  QuotaLimitTable <em>source-type:source-info</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -223,7 +221,7 @@ See also: <a href="#QuotaTallyTable">QuotaTallyTable</a>
 
 <p>
 <hr>
-<h2><a name="QuotaLock">QuotaLock</a></h2>
+<h3><a name="QuotaLock">QuotaLock</a></h3>
 <strong>Syntax:</strong>  QuotaLock <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -243,7 +241,7 @@ configured lock file <b>not</b> be on an NFS (or any other network) filesystem.
 
 <p>
 <hr>
-<h2><a name="QuotaLog">QuotaLog</a></h2>
+<h3><a name="QuotaLog">QuotaLog</a></h3>
 <strong>Syntax:</strong>  QuotaLog <em>file|"none"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -266,7 +264,7 @@ a <code><Global></code> context.
 
 <p>
 <hr>
-<h2><a name="QuotaOptions">QuotaOptions</a></h2>
+<h3><a name="QuotaOptions">QuotaOptions</a></h3>
 <strong>Syntax:</strong> QuotaOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -305,7 +303,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="QuotaShowQuotas">QuotaShowQuotas</a></h2>
+<h3><a name="QuotaShowQuotas">QuotaShowQuotas</a></h3>
 <strong>Syntax:</strong>  QuotaShowQuotas <em>on|off</em><br>
 <strong>Default:</strong> on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -321,7 +319,7 @@ may consider this a definite feature.
 
 <p>
 <hr>
-<h2><a name="QuotaTallyTable">QuotaTallyTable</a></h2>
+<h3><a name="QuotaTallyTable">QuotaTallyTable</a></h3>
 <strong>Syntax:</strong> QuotaTallyTable <em>source-type:source-info</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -348,7 +346,7 @@ See also: <a href="#QuotaLimitTable">QuotaLimitTable</a>
 <hr><br>
 
 <p>
-<h2><a name="SITE_QUOTA">SITE QUOTA</a></h2>
+<h3><a name="SITE_QUOTA">SITE QUOTA</a></h3>
 <p>
 The <code>SITE QUOTA</code> command will display the quota, both the limit
 and the current tally, to the client.  This <code>SITE</code> command accepts
@@ -570,11 +568,11 @@ Note that the values for the various byte variables honor any
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-Follow the usual steps for using third-party modules in proftpd: 
+Follow the usual steps for using third-party modules in ProFTPD: 
 <pre>
-  ./configure --with-modules=<i>mod_quotatab:...</i>
-  make
-  make install
+  $ ./configure --with-modules=<i>mod_quotatab:...</i>
+  $ make
+  $ make install
 </pre>
 where the list of modules, including <code>mod_quotatab</code>, will depend on
 the types of quota tables you wish to support.
@@ -630,20 +628,12 @@ Consult the <code>mod_sql</code> documentation for installation instructions
 for that module.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-27 22:08:41 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2014 TJ Saunders<br>
+© Copyright 2000-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_quotatab_file.html b/doc/contrib/mod_quotatab_file.html
index 836bab0..efc4f5e 100644
--- a/doc/contrib/mod_quotatab_file.html
+++ b/doc/contrib/mod_quotatab_file.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_quotatab_file.html,v 1.3 2013-08-14 21:40:17 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_quotatab_file.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_quotatab_file</title>
@@ -64,20 +62,12 @@ Examples:
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:17 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2000-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_quotatab_ldap.html b/doc/contrib/mod_quotatab_ldap.html
index d3c08ed..c3a3f12 100644
--- a/doc/contrib/mod_quotatab_ldap.html
+++ b/doc/contrib/mod_quotatab_ldap.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_quotatab_ldap.html,v 1.3 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_quotatab_ldap.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_quotatab_ldap</title>
@@ -77,15 +75,13 @@ An example <code>proftpd.conf</code> configuration might look like:
   </IfModule>
 </pre>
 
-<br><hr>
-
+<p>
+<hr>
 <font size=2><b><i>
 © Copyright 2003 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_quotatab_radius.html b/doc/contrib/mod_quotatab_radius.html
index fcb1ad3..780684e 100644
--- a/doc/contrib/mod_quotatab_radius.html
+++ b/doc/contrib/mod_quotatab_radius.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_quotatab_radius.html,v 1.3 2007-02-20 22:01:40 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_quotatab_radius.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_quotatab_radius</title>
@@ -61,15 +59,13 @@ An example <code>proftpd.conf</code> configuration might look like:
   </IfModule>
 </pre>
 
-<br><hr>
-
+<p>
+<hr>
 <font size=2><b><i>
 © Copyright 2005-2007 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_quotatab_sql.html b/doc/contrib/mod_quotatab_sql.html
index dd1d5d5..4fc19ac 100644
--- a/doc/contrib/mod_quotatab_sql.html
+++ b/doc/contrib/mod_quotatab_sql.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_quotatab_sql.html,v 1.2 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_quotatab_sql.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_quotatab_sql</title>
@@ -119,7 +117,7 @@ And, for the <code>QuotaTallyTable</code> directive:
 Also note that SQL-based tally tables have an issue with proper synchronization
 of updates, especially when multiple sessions involving the same tally
 are ocurring.  In order to prevent the tally table from becoming out of
-sync, you are <b>strongly</b> encouraged to define a <a href="http://www.castaglia.org/proftpd/modules/mod_quotatab.html#QuotaLock"><code>QuotaLock</code></a>
+sync, you are <b>strongly</b> encouraged to define a <a href="../contrib/mod_quotatab.html#QuotaLock"><code>QuotaLock</code></a>
 file.
 
 <p>
@@ -237,7 +235,6 @@ Here are some example table schema for SQL-based quota tables:
     );
 </pre>
   </li>
-  <br>
 
   <li><b><i>Tally table</i></b><br>
     <i>MySQL example</i>:
@@ -279,19 +276,12 @@ Here are some example table schema for SQL-based quota tables:
 calculations. NULL values should be avoided whenever possible.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:18 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2003 TJ Saunders<br>
+© Copyright 2000-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/contrib/mod_radius.html b/doc/contrib/mod_radius.html
index c063d7a..8a268ae 100644
--- a/doc/contrib/mod_radius.html
+++ b/doc/contrib/mod_radius.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_radius.html,v 1.9 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_radius.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_radius</title>
@@ -15,8 +13,8 @@
 <hr><br>
 
 This module is contained in the <code>mod_radius.c</code> file for
-ProFTPD 1.2.<i>x</i>/1.3.<i>x</i>, and is not compiled by default.
-Installation instructions are discussed <a href="#Installation">here</a>.
+ProFTPD 1.3.<i>x</i>, and is not compiled by default.  Installation
+instructions are discussed <a href="#Installation">here</a>.
 
 <p>
 This module is used to authenticate users using the <code>RADIUS</code>
@@ -53,6 +51,7 @@ some circumstances.
   <li><a href="#RadiusGroupInfo">RadiusGroupInfo</a>
   <li><a href="#RadiusLog">RadiusLog</a>
   <li><a href="#RadiusNASIdentifier">RadiusNASIdentifier</a>
+  <li><a href="#RadiusOptions">RadiusOptions</a>
   <li><a href="#RadiusQuotaInfo">RadiusQuotaInfo</a>
   <li><a href="#RadiusRealm">RadiusRealm</a>
   <li><a href="#RadiusUserInfo">RadiusUserInfo</a>
@@ -60,7 +59,7 @@ some circumstances.
 </ul>
 
 <hr>
-<h2><a name="RadiusAcctServer">RadiusAcctServer</a></h2>
+<h3><a name="RadiusAcctServer">RadiusAcctServer</a></h3>
 <strong>Syntax:</strong> RadiusAcctServer <em>server[:port] shared-secret [timeout]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -89,7 +88,7 @@ See also: <a href="#RadiusAuthServer">RadiusAuthServer</a>
 
 <p>
 <hr>
-<h2><a name="RadiusAuthServer">RadiusAuthServer</a></h2>
+<h3><a name="RadiusAuthServer">RadiusAuthServer</a></h3>
 <strong>Syntax:</strong> RadiusAuthServer <em>server[:port] shared-secret [timeout]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -118,7 +117,7 @@ See also: <a href="#RadiusAcctServer">RadiusAcctServer</a>
 
 <p>
 <hr>
-<h2><a name="RadiusEngine">RadiusEngine</a></h2>
+<h3><a name="RadiusEngine">RadiusEngine</a></h3>
 <strong>Syntax:</strong> RadiusEngine <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -133,7 +132,7 @@ module instead of commenting out all <code>mod_radius</code> directives.
 
 <p>
 <hr>
-<h2><a name="RadiusGroupInfo">RadiusGroupInfo</a></h2>
+<h3><a name="RadiusGroupInfo">RadiusGroupInfo</a></h3>
 <strong>Syntax:</strong> RadiusGroupInfo <em>primary-group-name suppl-group-names suppl-group-ids</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -175,7 +174,7 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="RadiusLog">RadiusLog</a></h2>
+<h3><a name="RadiusLog">RadiusLog</a></h3>
 <strong>Syntax:</strong> RadiusLog <em>file|"none"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -198,7 +197,7 @@ a <code><Global></code> context.
 
 <p>
 <hr>
-<h2><a name="RadiusNASIdentifier">RadiusNASIdentifier</a></h2>
+<h3><a name="RadiusNASIdentifier">RadiusNASIdentifier</a></h3>
 <strong>Syntax:</strong> RadiusNASIdentifier <em>id</em><br>
 <strong>Default:</strong> RadiusNASIdentifier ftp<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -219,20 +218,102 @@ Example:
 
 <p>
 <hr>
-<h2><a name="RadiusQuotaInfo">RadiusQuotaInfo</a></h2>
-<strong>Syntax:</strong> RadiusQuotaInfo <em>per-sess limit-type bytes-in bytes-out bytes-xfer files-in files-out files-xfer</em><br>
+<h3><a name="RadiusOptions">RadiusOptions</a></h3>
+<strong>Syntax:</strong> RadiusOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_radius<br>
-<strong>Compatibility:</strong> 1.3.0rc1 and later
+<strong>Compatibility:</strong> 1.3.6rc1 and later
 
-    QuotaLimitTable radius:
-    QuotaTallyTable file:/home/tj/proftpd/devel/build/cvs/etc/ftpquota.tallytab
+<p>
+The <code>RadiusOptions</code> directive is used to configure various optional
+behavior of <code>mod_radius</code>.
 
-    RadiusEngine on
-    RadiusAuthServer localhost:1812 testing123 5
-    RadiusLog /home/tj/proftpd/devel/build/cvs/etc/radius.log
-    RadiusQuotaInfo false soft 3.0 2.0 1.0 7 8 9
+<p>
+For example:
+<pre>
+  RadiusOptions RequireMAC IgnoreReplyMessage
+</pre>
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>IgnoreClass</code><br>
+    <p>
+    Some RADIUS servers will send the <code>Class</code> attribute in their
+    <code>Access-Accept</code> response, containing a value that should be
+    sent in every accounting requesting.  To tell <code>mod_radius</code> to
+    ignore/not send this <code>Class</code> attribute, use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
+  <li><code>IgnoreReplyMessage</code><br>
+    <p>
+    Some RADIUS servers will send the <code>Reply-Message</code> attribute in
+    their <code>Access-Accept</code> and <code>Access-Reject</code> responses,
+    containing messages that should be displayed to the connecting user.
+    To tell <code>mod_radius</code> to ignore/not display these
+    <code>Reply-Message</code> attributes, use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
+  <li><code>IgnoreIdleTimeout</code><br>
+    <p>
+    Some RADIUS servers will send the <code>Idle-Timeout</code> attribute in
+    their <code>Access-Accept</code> response, containing a timeout value to
+    be used for idle sessions.  To tell <code>mod_radius</code> to ignore/not
+    use this <code>Idle-Timeout</code> value, use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
+  <li><code>IgnoreSessionTimeout</code><br>
+    <p>
+    Some RADIUS servers will send the <code>Session-Timeout</code> attribute in
+    their <code>Access-Accept</code> response, containing a timeout value to
+    be used for maximum session durations.  To tell <code>mod_radius</code> to
+    ignore/not use this <code>Session-Timeout</code> value, use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
+  <li><code>RequireMAC</code><br>
+    <p>
+    Some RADIUS servers will send the <code>Message-Authenticator</code>
+    attribute in their <code>Access-Accept</code> and <code>Access-Reject</code>
+    responses, used for protecting against spoof attacks.  Some RADIUS servers,
+    though, do not use this attribute.  To be very secure, and to tell
+    <code>mod_radius</code> to <b>require</b> the use of this attribute, use
+    this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+</ul>
+
+<p>
+<hr>
+<h3><a name="RadiusQuotaInfo">RadiusQuotaInfo</a></h3>
+<strong>Syntax:</strong> RadiusQuotaInfo <em>per-sess limit-type bytes-in bytes-out bytes-xfer files-in files-out files-xfer</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_radius<br>
+<strong>Compatibility:</strong> 1.3.0rc1 and later
 
 <p>
 The <code>RadiusQuotaInfo</code> directive is used to configure quota
@@ -278,12 +359,13 @@ An example configuration might look like:
 
   </IfModule>
 </pre>
+
 <p>
 See Also: <a href="#RadiusVendor"><code>RadiusVendor</code></a>
 
 <p>
 <hr>
-<h2><a name="RadiusRealm">RadiusRealm</a></h2>
+<h3><a name="RadiusRealm">RadiusRealm</a></h3>
 <strong>Syntax:</strong> RadiusRealm <em>realm</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -302,7 +384,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="RadiusUserInfo">RadiusUserInfo</a></h2>
+<h3><a name="RadiusUserInfo">RadiusUserInfo</a></h3>
 <strong>Syntax:</strong> RadiusUserInfo <em>uid gid home shell</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -345,7 +427,7 @@ See Also:
 
 <p>
 <hr>
-<h2><a name="RadiusVendor">RadiusVendor</a></h2>
+<h3><a name="RadiusVendor">RadiusVendor</a></h3>
 <strong>Syntax:</strong> RadiusVendor <em>name id</em><br>
 <strong>Default:</strong> RadiusVendor Unix 4<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -433,6 +515,14 @@ following data:
   <li><b>Acct-Output-Octets</b>: The number of bytes downloaded<br>
   </li>
   <br>
+
+  <li><b>Acct-Terminate-Cause</b>: The reason the session ended<br>
+  </li>
+  <br>
+
+  <li><b>Event-Timestamp</b>: The number of seconds since the Unix epoch<br>
+  </li>
+  <br>
 </ul>
 Merely configuring a <code>RadiusAcctServer</code> enables the module's
 accounting capabilities.
@@ -465,49 +555,62 @@ by <code>mod_radius</code>:
   <li><b>Calling-Station-Id</b>: IP address of connecting client<br>
   </li>
   <br>
+
+  <li><b>Message-Authenticator</b>: MAC of request, per RFC 3579<br>
+  </li>
+  <br>
 </ul>
 
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_radius</code> module is distributed with ProFTPD.  Simply follow
-the normal steps for using third-party modules in proftpd:
+the normal steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_radius
+  $ ./configure --enable-openssl --with-modules=mod_radius
 </pre>
 To build <code>mod_radius</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_radius
+  $ ./configure --enable-dso --enable-openssl --with-shared=mod_radius
 </pre>
 Then follow the usual steps:
 <pre>
-  make 
-  make install
+  $ make 
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_radius</code> as a shared
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_radius</code> as a shared
 module:
 <pre>
-  prxs -c -i -d mod_radius.c
+  $ prxs -c -i -d mod_radius.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:18 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_radius</code> module supports different forms of logging.  The
+main module logging is done via the <code>RadiusLog</code> directive.  For
+debugging purposes, the module also uses <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>radius
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/sftp-trace.log
+  Trace radius:20
+</pre>
+This trace logging can generate large files; it is intended for debugging
+use only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2013 TJ Saunders<br>
+© Copyright 2000-2015 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_ratio.html b/doc/contrib/mod_ratio.html
index f9415bf..1767fa2 100644
--- a/doc/contrib/mod_ratio.html
+++ b/doc/contrib/mod_ratio.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ratio.html,v 1.1 2013-10-03 06:33:19 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_ratio.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ratio</title>
@@ -50,10 +48,10 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 <hr>
-<h2><a name="AnonRatio">AnonRatio</a></h2>
+<h3><a name="AnonRatio">AnonRatio</a></h3>
 <strong>Syntax:</strong> AnonRatio <em>name file-ratio file-credit byte-ratio byte-credit</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -62,10 +60,10 @@ The <code>AnonRatio</code> directive
 
 <p>
 <hr>
-<h2><a name="ByteRatioErrMsg">ByteRatioErrMsg</a></h2>
+<h3><a name="ByteRatioErrMsg">ByteRatioErrMsg</a></h3>
 <strong>Syntax:</strong> ByteRatioErrMsg <em>message</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -74,10 +72,10 @@ The <code>ByteRatioErrMsg</code> directive
 
 <p>
 <hr>
-<h2><a name="CwdRatioMsg">CwdRatioMsg</a></h2>
+<h3><a name="CwdRatioMsg">CwdRatioMsg</a></h3>
 <strong>Syntax:</strong> CwdRatioMsg <em>message</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -86,10 +84,10 @@ The <code>CwdRatioMsg</code> directive
 
 <p>
 <hr>
-<h2><a name="FileRatioErrMsg">FileRatioErrMsg</a></h2>
+<h3><a name="FileRatioErrMsg">FileRatioErrMsg</a></h3>
 <strong>Syntax:</strong> FileRatioErrMsg <em>message</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -98,10 +96,10 @@ The <code>FileRatioErrMsg</code> directive
 
 <p>
 <hr>
-<h2><a name="GroupRatio">GroupRatio</a></h2>
+<h3><a name="GroupRatio">GroupRatio</a></h3>
 <strong>Syntax:</strong> GroupRatio <em>name file-ratio file-credit byte-ratio byte-credit</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -110,10 +108,10 @@ The <code>GroupRatio</code> directive
 
 <p>
 <hr>
-<h2><a name="HostRatio">HostRatio</a></h2>
+<h3><a name="HostRatio">HostRatio</a></h3>
 <strong>Syntax:</strong> HostRatio <em>name file-ratio file-credit byte-ratio byte-credit</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -122,10 +120,10 @@ The <code>HostRatio</code> directive
 
 <p>
 <hr>
-<h2><a name="LeechRatioMsg">LeechRatioMsg</a></h2>
+<h3><a name="LeechRatioMsg">LeechRatioMsg</a></h3>
 <strong>Syntax:</strong> LeechRatioMsg <em>message</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -134,10 +132,10 @@ The <code>LeechRatioMsg</code> directive
 
 <p>
 <hr>
-<h2><a name="RatioFile">RatioFile</a></h2>
+<h3><a name="RatioFile">RatioFile</a></h3>
 <strong>Syntax:</strong> RatioFile <em>path</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -146,10 +144,10 @@ The <code>RatioFile</code> directive
 
 <p>
 <hr>
-<h2><a name="Ratios">Ratios</a></h2>
+<h3><a name="Ratios">Ratios</a></h3>
 <strong>Syntax:</strong> Ratios <em>on|off</em><br>
 <strong>Default:</strong> Off<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -158,10 +156,10 @@ The <code>Ratios</code> directive
 
 <p>
 <hr>
-<h2><a name="RatioTempFile">RatioTempFile</a></h2>
+<h3><a name="RatioTempFile">RatioTempFile</a></h3>
 <strong>Syntax:</strong> RatioTempFile <em>path</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -170,10 +168,10 @@ The <code>RatioTempFile</code> directive
 
 <p>
 <hr>
-<h2><a name="SaveRatios">SaveRatios</a></h2>
+<h3><a name="SaveRatios">SaveRatios</a></h3>
 <strong>Syntax:</strong> SaveRatios <em>on|off</em><br>
 <strong>Default:</strong> Off<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -182,10 +180,10 @@ The <code>SaveRatios</code> directive
 
 <p>
 <hr>
-<h2><a name="UserRatio">UserRatio</a></h2>
+<h3><a name="UserRatio">UserRatio</a></h3>
 <strong>Syntax:</strong> UserRatio <em>name file-ratio file-credit byte-ratio byte-credit</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Directory>, <Anonymous><br>
 <strong>Module:</strong> mod_ratio<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -196,20 +194,20 @@ The <code>UserRatio</code> directive
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_ratio</code> module is distributed with ProFTPD.  Simply follow
-the normal steps for using third-party modules in proftpd:
+the normal steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_ratio
-  make
-  make install
+  $ ./configure --with-modules=mod_ratio
+  $ make
+  $ make install
 </pre>
 Alternatively, <code>mod_ratio</code> can be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_ratio ...
+  $ ./configure --enable-dso --with-shared=mod_ratio ...
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -217,30 +215,21 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_ratio</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_ratio.c
+  $ prxs -c -i -d mod_ratio.c
 </pre>
 
-
 <p>
 <hr><br>
 <h2><a name="Usage">Usage</a></h2>
 To use <code>mod_ratio</code>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-10-03 06:33:19 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_readme.html b/doc/contrib/mod_readme.html
index 79e655d..9aa43af 100644
--- a/doc/contrib/mod_readme.html
+++ b/doc/contrib/mod_readme.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_readme.html,v 1.2 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_readme.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_readme</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_readme.c</code> file for
 ProFTPD 1.3.<i>x</i>, and is not compiled by default.  Installation
 instructions are discussed <a href="#Installation">here</a>.
@@ -28,7 +27,7 @@ ProFTPD source code.
 </ul>
 
 <hr>
-<h2><a name="DisplayReadme">DisplayReadme</a></h2>
+<h3><a name="DisplayReadme">DisplayReadme</a></h3>
 <strong>Syntax:</strong> DisplayReadme <em>path|pattern</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -71,18 +70,18 @@ might result in the following being sent to the client:
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 To install <code>mod_readme</code>, follow the usual steps for using
-third-party modules in <code>proftpd</code>:
+third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_readme ...
+  $ ./configure --with-modules=mod_readme ...
 </pre>
 To build <code>mod_readme</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_readme
+  # ./configure --enable-dso --with-shared=mod_readme
 </pre>
 Then follow the usual steps:
 <pre>
-  make 
-  make install
+  $ make 
+  $ make install
 </pre>
 
 <p>
@@ -90,24 +89,16 @@ Alternatively, if your <code>proftpd</code> was compiled with DSO support,
 you can use the <code>prxs</code> tool to build <code>mod_readme</code> as
 a shared module:
 <pre>
-  prxs -c -i -d mod_readme.c
+  $ prxs -c -i -d mod_readme.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:18 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2011-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_rewrite.html b/doc/contrib/mod_rewrite.html
index dd1c28d..9f912f3 100644
--- a/doc/contrib/mod_rewrite.html
+++ b/doc/contrib/mod_rewrite.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_rewrite.html,v 1.10 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_rewrite.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_rewrite</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_rewrite.c</code> file for ProFTPD
 1.3.<i>x</i>, and is not compiled by default.  Installation instructions are
 discussed <a href="#Installation">here</a>.
@@ -42,7 +41,7 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="RewriteCondition">RewriteCondition</a></h2>
+<h3><a name="RewriteCondition">RewriteCondition</a></h3>
 <strong>Syntax:</strong> RewriteCondition <em>condition pattern [flags]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
@@ -107,7 +106,7 @@ constructs in addition to plain text:
   <li><b>Variable substitutions:</b><br>
     These are substitutions of the form:
 <p>
-<table border=0>
+<table border=0 summary="Rewrite Variables">
   <tr>
     <td><b>%a</b></td>
     <td>Client IP address</td> 
@@ -292,7 +291,7 @@ evaluated.  Supported flags are:
 
   <p>
   <li><b>ornext|OR</b> (<b>or</b> next condition)<br>
-      Use this to combine rule conditions with a logical <coe>OR</code>
+      Use this to combine rule conditions with a logical <code>OR</code>
       instead of the implicit <code>AND</code>. Typical example:
 <pre>
     RewriteCondition %h  ^host1.*  [OR]
@@ -307,7 +306,7 @@ evaluated.  Supported flags are:
 
 <p>
 <hr>
-<h2><a name="RewriteEngine">RewriteEngine</a></h2>
+<h3><a name="RewriteEngine">RewriteEngine</a></h3>
 <strong>Syntax:</strong> RewriteEngine <em>on|off</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -322,7 +321,7 @@ of commenting out all <code>mod_rewrite</code> directives.
 
 <p>
 <hr>
-<h2><a name="RewriteLock">RewriteLock</a></h2>
+<h3><a name="RewriteLock">RewriteLock</a></h3>
 <strong>Syntax:</strong> RewriteLock <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -338,7 +337,7 @@ FIFO. It is not required for other types of rewriting maps.
 
 <p>
 <hr>
-<h2><a name="RewriteLog">RewriteLog</a></h2>
+<h3><a name="RewriteLog">RewriteLog</a></h3>
 <strong>Syntax:</strong> RewriteLog <em>file|"none"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -364,7 +363,7 @@ a <code><Global></code> context.
 
 <p>
 <hr>
-<h2><a name="RewriteMap">RewriteMap</a></h2>
+<h3><a name="RewriteMap">RewriteMap</a></h3>
 <strong>Syntax:</strong> RewriteMap <em>map-name map-type:map-source</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -660,7 +659,7 @@ parsing of the text files only happens once!
 
 <p>
 <hr>
-<h2><a name="RewriteMaxReplace">RewriteMaxReplace</a></h2>
+<h3><a name="RewriteMaxReplace">RewriteMaxReplace</a></h3>
 <strong>Syntax:</strong> RewriteMaxReplace <em>count</em><br>
 <strong>Default:</strong> 8<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -684,7 +683,7 @@ For example, to increase the limit to 32 occurrences to be replaced, use:
 
 <p>
 <hr>
-<h2><a name="RewriteRule">RewriteRule</a></h2>
+<h3><a name="RewriteRule">RewriteRule</a></h3>
 <strong>Syntax:</strong> RewriteRule <em>pattern substitution [flags]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
@@ -759,7 +758,7 @@ regex, <i>etc.</i>) have a look at the following dedicated book on this topic:
     Mastering Regular Expressions<br>
     Jeffrey E.F. Friedl<br>
     Nutshell Handbook Series<br>
-    O'Reilly & Associates, Inc. 1997<br>
+    O'Reilly & Associates, Inc. 1997<br>
     ISBN 1-56592-257-3<br>
 </blockquote>
 
@@ -890,43 +889,50 @@ underscores:
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 To install <code>mod_rewrite</code>, follow the usual steps for using
-third-party modules in proftpd:
+third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_rewrite
+  $ ./configure --with-modules=mod_rewrite
 </pre>
 To build <code>mod_rewrite</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_rewrite
+  $ ./configure --enable-dso --with-shared=mod_rewrite
 </pre>
 Then follow the usual steps:
 <pre>
-  make 
-  make install
+  $ make 
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_rewrite</code> as a shared
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_rewrite</code> as a shared
 module:
 <pre>
-  prxs -c -i -d mod_rewrite.c
+  $ prxs -c -i -d mod_rewrite.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:18 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_rewrite</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>rewrite
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace rewrite:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2013 TJ Saunders<br>
+© Copyright 2000-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sftp.html b/doc/contrib/mod_sftp.html
index d10c350..f247691 100644
--- a/doc/contrib/mod_sftp.html
+++ b/doc/contrib/mod_sftp.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sftp.html,v 1.82 2014-03-03 05:40:13 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sftp.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sftp</title>
@@ -51,21 +49,21 @@ to FTPS for secure transfer of data.
 <b>SSH2 RFCs and SFTP Drafts</b><br>
 For those wishing to learn more about the SSH2 and SFTP protocols, see:
 <ul>
-  <li><a href="http://www.faqs.org/rfcs/rfc4250.html">SSH Assigned Numbers (RFC4250)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4251.html">SSH Architecture (RFC4251)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4252.html">SSH Authentication (RFC4252)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4253.html">SSH Transport Layer (RFC4253)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4254.html">SSH Connection (RFC4254)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4256.html">SSH Keyboard-Interactive Authentication (RFC4256)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4335.html">SSH Channel "Break" Extension (RFC4335)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4344.html">SSH Transport Encryption Modes (RFC4344)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4345.html">SSH Improved Arcfour Modes (RFC4345)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4419.html">SSH Diffie-Hellman Group Exchange Protocol (RFC4419)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4432.html">SSH RSA Key Exchange Protocol (RFC4432)</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc4716.html">SSH Public Key File Format (RFC4716)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4250.html">SSH Assigned Numbers (RFC 4250)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4251.html">SSH Architecture (RFC 4251)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4252.html">SSH Authentication (RFC 4252)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4253.html">SSH Transport Layer (RFC 4253)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4254.html">SSH Connection (RFC 4254)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4256.html">SSH Keyboard-Interactive Authentication (RFC 4256)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4335.html">SSH Channel "Break" Extension (RFC 4335)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4344.html">SSH Transport Encryption Modes (RFC 4344)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4345.html">SSH Improved Arcfour Modes (RFC 4345)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4419.html">SSH Diffie-Hellman Group Exchange Protocol (RFC 4419)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4432.html">SSH RSA Key Exchange Protocol (RFC 4432)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc4716.html">SSH Public Key File Format (RFC 4716)</a>
   <li><a href="http://www.vandyke.com/technology/draft-ietf-secsh-filexfer.txt">SFTP Draft</a>
   <li><a href="http://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00">SFTP Extensions Draft</a>
-  <li><a href="http://www.faqs.org/rfcs/rfc5656.html">SSH Elliptic Curve Algorithms (RFC5656)</a>
+  <li><a href="http://www.faqs.org/rfcs/rfc5656.html">SSH Elliptic Curve Algorithms (RF C5656)</a>
 </ul>
 
 <p>
@@ -86,7 +84,7 @@ subsystem, for secure file transfer over an SSH2 connection.  The
   <li>Blacklisted public keys
   <li>Configurable traffic analysis protection
   <li>Passphrase-protected host keys
-  <li>SFTP extensions: check-file, copy-file, vendor-id, version-select, posix-rename at openssh.com, statvfs at openssh.com, fstatvfs at openssh.com
+  <li>SFTP extensions: check-file, copy-file, vendor-id, version-select, posix-rename at openssh.com, statvfs at openssh.com, fstatvfs at openssh.com, hardlink at openssh.com
 </ul>
 This module supports the SFTP and SCP file transfer protocols; it does
 <b>not</b> support shell access.
@@ -138,6 +136,7 @@ questions, concerns, or suggestions regarding this module.
   <li><a href="#SFTPHostKey">SFTPHostKey</a>
   <li><a href="#SFTPKeyBlacklist">SFTPKeyBlacklist</a>
   <li><a href="#SFTPKeyExchanges">SFTPKeyExchanges</a>
+  <li><a href="#SFTPKeyLimits">SFTPKeyLimits</a>
   <li><a href="#SFTPLog">SFTPLog</a>
   <li><a href="#SFTPMaxChannels">SFTPMaxChannels</a>
   <li><a href="#SFTPOptions">SFTPOptions</a>
@@ -148,11 +147,10 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 <hr>
-<h2><a name="SFTPAcceptEnv">SFTPAcceptEnv</a></h2>
+<h3><a name="SFTPAcceptEnv">SFTPAcceptEnv</a></h3>
 <strong>Syntax:</strong> SFTPAcceptEnv <em>env1 ...</em><br>
 <strong>Default:</strong> <em>LANG</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Gl
-obal><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.3rc3 and later
 
@@ -170,10 +168,10 @@ underlying libraries; care should be taken in the use of this directive.
 
 <p>
 <hr>
-<h2><a name="SFTPAuthMethods">SFTPAuthMethods</a></h2>
+<h3><a name="SFTPAuthMethods">SFTPAuthMethods</a></h3>
 <strong>Syntax:</strong> SFTPAuthMethods <em>meth1 ...</em><br>
 <strong>Default:</strong> <em>publickey password</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -212,11 +210,40 @@ clients if the <a href="mod_sftp_pam.html"><code>mod_sftp_pam</code></a>
 module is present.
 
 <p>
+As of <code>proftpd-1.3.6rc2</code> and later, you can use the
+<code>SFTPAuthMethods</code> directive to configure <em>chains</em> of
+methods.  An authentication <em>chain</em> is a list of authentication
+methods, <b>all</b> of which must succeed in order for login to succeed.
+For example, using:
+<pre>
+  # Require both publickey and password authentication
+  SFTPAuthMethods publickey+password
+</pre>
+Note that <i>order of the methods in a chain is important</i>.  In the above
+example, the publickey authentication <b>must</b> succeed first, before
+password authentication will be offered.  If you want require both
+publickey and password authentication, but that they can be used in any
+order, you can configure multiple chains:
+<pre>
+  # Require both publickey and password authentication, in any order
+  SFTPAuthMethods publickey+password password+publickey
+</pre>
+As long as any <i>one</i> chain is completed, login will succeed.
+
+<p>
+You can even require that multiple different keys be used (<i>e.g.</i> a
+RSA and a DSA public key, or multiple different RSA/DSA keys) using:
+<pre>
+  # Require different keys for publickey authentication
+  SFTPAuthMethods publickey+publickey
+</pre>
+
+<p>
 <hr>
-<h2><a name="SFTPAuthorizedHostKeys">SFTPAuthorizedHostKeys</a></h2>
+<h3><a name="SFTPAuthorizedHostKeys">SFTPAuthorizedHostKeys</a></h3>
 <strong>Syntax:</strong> SFTPAuthorizedHostKeys <em>store1 ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -237,17 +264,17 @@ hostbased authentication properly.
 RFC4716 format (see the <a href="#UsageRFC4716Format">Usage</a> section).
 This is done using OpenSSH's <code>ssh-keygen</code> tool:
 <pre>
-  # ssh-keygen -e -f /path/to/file
+  $ ssh-keygen -e -f /path/to/file
 </pre>
 The output from this command can then be added to the
 <code>SFTPAuthorizedHostKeys</code> file.
 
 <p>
 <hr>
-<h2><a name="SFTPAuthorizedUserKeys">SFTPAuthorizedUserKeys</a></h2>
+<h3><a name="SFTPAuthorizedUserKeys">SFTPAuthorizedUserKeys</a></h3>
 <strong>Syntax:</strong> SFTPAuthorizedUserKeys <em>store1 ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -269,7 +296,7 @@ Example:
 RFC4716 format (see the <a href="#UsageRFC4716Format">Usage</a> section).
 This is done using OpenSSH's <code>ssh-keygen</code> tool:
 <pre>
-  # ssh-keygen -e -f ~/.ssh/id_rsa.pub
+  $ ssh-keygen -e -f ~/.ssh/id_rsa.pub
 </pre>
 The output from this command can then be added to the
 <code>SFTPAuthorizedUserKeys</code> file.
@@ -286,10 +313,10 @@ users to manage their own authorized keys.  For example:
 
 <p>
 <hr>
-<h2><a name="SFTPCiphers">SFTPCiphers</a></h2>
+<h3><a name="SFTPCiphers">SFTPCiphers</a></h3>
 <strong>Syntax:</strong> SFTPCiphers <em>algo1 ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -358,10 +385,10 @@ cipher algorithms as OpenSSH, you would use:
 
 <p>
 <hr>
-<h2><a name="SFTPClientAlive">SFTPClientAlive</a></h2>
+<h3><a name="SFTPClientAlive">SFTPClientAlive</a></h3>
 <strong>Syntax:</strong> SFTPClientAlive <em>count</em> <em>interval</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.4rc1 and later
 
@@ -384,10 +411,10 @@ seconds.
 
 <p>
 <hr>
-<h2><a name="SFTPClientMatch">SFTPClientMatch</a></h2>
+<h3><a name="SFTPClientMatch">SFTPClientMatch</a></h3>
 <strong>Syntax:</strong> SFTPClientMatch <em>pattern key1 val1 ...</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -466,10 +493,10 @@ size, <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="SFTPCompression">SFTPCompression</a></h2>
+<h3><a name="SFTPCompression">SFTPCompression</a></h3>
 <strong>Syntax:</strong> SFTPCompression <em>on|off|delayed</em><br>
 <strong>Default:</strong> <em>off</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -485,10 +512,10 @@ after the client has successfully authenticated.
 
 <p>
 <hr>
-<h2><a name="SFTPCryptoDevice">SFTPCryptoDevice</a></h2>
+<h3><a name="SFTPCryptoDevice">SFTPCryptoDevice</a></h3>
 <strong>Syntax:</strong> SFTPCryptoDevice <em>driver|"all"|"none"</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -520,10 +547,10 @@ may be used to list all of the engine drivers supported by OpenSSL.
 
 <p>
 <hr>
-<h2><a name="SFTPDHParamFile">SFTPDHParamFile</a></h2>
+<h3><a name="SFTPDHParamFile">SFTPDHParamFile</a></h3>
 <strong>Syntax:</strong> SFTPDHParamFile <em>path</em><br>
 <strong>Default:</strong> <em>dhparams.pem</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -531,7 +558,7 @@ may be used to list all of the engine drivers supported by OpenSSL.
 The <code>SFTPDHParamFile</code> directive is used to configure the path
 to a file containing pre-computed Diffie-Hellman (DH) group parameters.
 These parameters will be used by <code>mod_sftp</code> when handling the
-Diffie-Hellman group exchange (see RFC4419) portion of the key exchange.
+Diffie-Hellman group exchange (see RFC 4419) portion of the key exchange.
 
 <p>
 The <code>SFTPDHParamFile</code> is produced using OpenSSL, which uses
@@ -553,10 +580,10 @@ The <em>nbits</em> value used should vary between 1024 and 8192, inclusive.
 
 <p>
 <hr>
-<h2><a name="SFTPDigests">SFTPDigests</a></h2>
+<h3><a name="SFTPDigests">SFTPDigests</a></h3>
 <strong>Syntax:</strong> SFTPDigests <em>algo1 ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -573,6 +600,7 @@ of supported MAC algorithms is:
   <li>hmac-md5-96
   <li>hmac-ripemd160
   <li>umac-64 at openssh.com
+  <li>umac-128 at openssh.com
 </ul>
 By default, all of the above MAC algorithms are presented to the client,
 in the above order, during the key exchange.  <b>Note</b> that some algorithms
@@ -600,21 +628,23 @@ by default are:
   <li>hmac-sha1-96
   <li>hmac-md5-96
   <li>umac-64 at openssh.com
+  <li>umac-128 at openssh.com
 </ul>
 Thus if you wanted to configure <code>mod_sftp</code> to present the same
 MAC algorithms as OpenSSH, you would use:
 <pre>
   # Make mod_sftp present the MAC ciphers as OpenSSH
   SFTPDigests hmac-md5 hmac-sha1 hmac-sha2-256 hmac-sha2-512 \
-    hmac-ripemd160 hmac-sha1-96 hmac-md5-96 umac-64 at openssh.com
+    hmac-ripemd160 hmac-sha1-96 hmac-md5-96 umac-64 at openssh.com \
+    umac-128 at openssh.com
 </pre>
 
 <p>
 <hr>
-<h2><a name="SFTPDisplayBanner">SFTPDisplayBanner</a></h2>
+<h3><a name="SFTPDisplayBanner">SFTPDisplayBanner</a></h3>
 <strong>Syntax:</strong> SFTPDisplayBanner <em>path</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -625,11 +655,16 @@ files are often used to sent terms of use or other such policy terms to
 connecting clients.
 
 <p>
+See the <a href="../howto/DisplayFiles.html">Display files</a> howto for
+more information on the variables that can be used in an
+<code>SFTPDisplayBanner</code> file.
+
+<p>
 <hr>
-<h2><a name="SFTPEngine">SFTPEngine</a></h2>
+<h3><a name="SFTPEngine">SFTPEngine</a></h3>
 <strong>Syntax:</strong> SFTPEngine <em>on|off</em><br>
 <strong>Default:</strong> <em>off</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -642,10 +677,10 @@ the main server and all configured virtual hosts.
 
 <p>
 <hr>
-<h2><a name="SFTPExtensions">SFTPExtensions</a></h2>
+<h3><a name="SFTPExtensions">SFTPExtensions</a></h3>
 <strong>Syntax:</strong> SFTPExtensions <em>ext1 ... extN</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.3rc3 and later
 
@@ -662,8 +697,11 @@ The extension names used for the <code>SFTPExtensions</code> directive are:
   <li>posixRename
   <li>spaceAvailable
   <li>statvfs
+  <li>hardlink
+  <li>xattr
 </ul>
-All extensions <i>except</i> <code>vendorID</code> are enabled by default.
+All extensions <i>except</i> <code>vendorID</code> <b>and</b>
+<code>xattr</code> are enabled by default.
 
 <p>
 To enable an extension, preface the extension name with a '+' (plus) character;
@@ -675,10 +713,10 @@ to disable the extension, use a '-' (minus) character prefix.  For example:
 
 <p>
 <hr>
-<h2><a name="SFTPHostKey">SFTPHostKey</a></h2>
-<strong>Syntax:</strong> SFTPHostKey <em>file</em>|agent:<em>/path</em><br>
+<h3><a name="SFTPHostKey">SFTPHostKey</a></h3>
+<strong>Syntax:</strong> SFTPHostKey <em>file</em>|agent:<em>/path</em>|"NoRSA"|"NoDSA"|"NoECDSA"<br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -718,11 +756,33 @@ the host keys are not stored on files on the server system, <i>e.g.</i>
 the keys can be loaded into the SSH agent from a PKCS#11 token.
 
 <p>
+The <code>SFTPHostKey</code> directive, as of <code>proftpd-1.3.6rc1</code>
+and later, can be used to <em>disable</em> host keys configured via
+<code><Global></code>.  For example, if you have a global RSA key that
+you wish to disable in all vhosts, you might use the "NoRSA" flag, <i>e.g.</i>:
+<pre>
+  <Global>
+    SFTPHostKey /path/to/ssh_rsa_host_key
+    ...
+  </Global>
+
+  <VirtualHost a.b.c.d>
+    ...
+    # Configure our per-vhost DSA host key
+    SFTPHostKey /path/to/ssh_dsa_host_key
+
+    # ...and disable the global RSA host key
+    SFTPHostKey NoRSA
+    ...
+  </VirtualHost>
+</pre>
+
+<p>
 <hr>
-<h2><a name="SFTPKeyBlacklist">SFTPKeyBlacklist</a></h2>
+<h3><a name="SFTPKeyBlacklist">SFTPKeyBlacklist</a></h3>
 <strong>Syntax:</strong> SFTPKeyBlacklist <em>"none"|path</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -752,10 +812,10 @@ need to generate your own <code>SFTPDKeyBlacklist</code>, use the
 
 <p>
 <hr>
-<h2><a name="SFTPKeyExchanges">SFTPKeyExchanges</a></h2>
+<h3><a name="SFTPKeyExchanges">SFTPKeyExchanges</a></h3>
 <strong>Syntax:</strong> SFTPKeyExchanges <em>algo1 ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -764,9 +824,13 @@ The <code>SFTPKeyExchanges</code> directive is used to specify the list of
 key exchange algorithms that <code>mod_sftp</code> should use.  The current list
 of supported key exchange algorithms is:
 <ul>
-  <li>ecdh-sha2-nistp256
-  <li>ecdh-sha2-nistp384
+  <li>curve25519-sha256 at libssh.org
   <li>ecdh-sha2-nistp521
+  <li>ecdh-sha2-nistp384
+  <li>ecdh-sha2-nistp256
+  <li>diffie-hellman-group18-sha512
+  <li>diffie-hellman-group16-sha512
+  <li>diffie-hellman-group14-sha256
   <li>diffie-hellman-group-exchange-sha256
   <li>diffie-hellman-group-exchange-sha1
   <li>diffie-hellman-group14-sha1
@@ -782,10 +846,39 @@ key exchange algorithm must be used.
 
 <p>
 <hr>
-<h2><a name="SFTPLog">SFTPLog</a></h2>
+<h3><a name="SFTPKeyLimits">SFTPKeyLimits</a></h3>
+<strong>Syntax:</strong> SFTPKeyLimits <em>limit1 ...</em><br>
+<strong>Default:</strong> MinimumRSASize 768 MiniumumDSASize 384 MinimumECSize 160<em>None</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_sftp<br>
+<strong>Compatibility:</strong> 1.3.6rc3 and later
+
+<p>
+The <code>SFTPKeyLimits</code> directive is used to configure limits on the
+sizes of keys (in terms of <em>bits</em>), both host keys <b>and</b> user keys.
+The currently supported limits are:
+<ul>
+  <li><code>MinimumDSASize</code>
+  <li><code>MinimumECSize</code>
+  <li><code>MinimumRSASize</code>
+</ul>
+
+<p>
+Examples:
+<pre>
+  # Require at least 1024-bit RSA keys; leave other default limits alone
+  SFTPKeyLimits MinimumRSASize 1024
+
+  # Require at least 256-bit EC keys, and remove other default limits
+  SFTPKeyLimits MinimumECSize 256 MinimumDSASize 0 MinimumRSASize 0
+</pre>
+
+<p>
+<hr>
+<h3><a name="SFTPLog">SFTPLog</a></h3>
 <strong>Syntax:</strong> SFTPLog <em>file|"none"</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -801,7 +894,7 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="SFTPMaxChannels">SFTPMaxChannels</a></h2>
+<h3><a name="SFTPMaxChannels">SFTPMaxChannels</a></h3>
 <strong>Syntax:</strong> SFTPMaxChannels <em>count</em><br>
 <strong>Default:</strong> 10<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -820,7 +913,7 @@ SSH2 clients only ever open one channel.
 
 <p>
 <hr>
-<h2><a name="SFTPOptions">SFTPOptions</a></h2>
+<h3><a name="SFTPOptions">SFTPOptions</a></h3>
 <strong>Syntax:</strong> SFTPOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -829,10 +922,7 @@ SSH2 clients only ever open one channel.
 
 <p>
 The <code>SFTPOptions</code> directive is used to configure various optional
-behavior of <code>mod_sftp</code>.  <b>Note</b>: all of the configured
-<code>SFTPOptions</code> parameters <b>must</b> appear on the same line in the
-configuration; only the first <code>SFTPOptions</code> directive that
-appears in the configuration is used.
+behavior of <code>mod_sftp</code>.
 
 <p>
 For example:
@@ -858,6 +948,35 @@ The currently implemented options are:
   </li>
 
   <p>
+  <li><code>AllowWeakDH</code><br>
+    <p>
+    The <code>mod_sftp</code> will not use Diffie-Hellman groups of less
+    than 2048 bits, due to <a href="https://www.weakdh.org">weaknesses</a>
+    that can downgrade the security of an SSH session.  If for any reason
+    your SFTP/SCP clients <b>require</b> smaller Diffie-Hellman groups, then
+    use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
+  <li><code>IgnoreFIFOs</code>
+    <p>
+    By default, <code>mod_sftp</code> allows uploading and downloading of FIFOs,
+    just as if they were regular files.  However, in some cases this can lead
+    to "hangs" on the SFTP/SCP client end, especially when the FIFO
+    reader/writer processes are not running at the time.  Thus to tell
+    <code>mod_sftp</code> to ignore/reject requests to read from/write to FIFOs,
+    for both SFTP and SCP, use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc3</code>.
+  </li>
+
+  <p>
   <li><code>IgnoreSCPUploadPerms</code><br>
     <p>
     When an SCP client uploads a file, the desired permissions on the file
@@ -880,6 +999,18 @@ The currently implemented options are:
   </li>
 
   <p>
+  <li><code>IgnoreSFTPSetExtendedAttributes</code><br>
+    <p>
+    Use this option to have <code>mod_sftp</code> silently ignore any
+    extended attributes sent by SFTP clients via the <code>SETSTAT</code> or
+    <code>FSETSTAT</code> SFTP requests.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc3</code>.
+  </li>
+
+  <p>
   <li><code>IgnoreSFTPSetOwners</code><br>
     <p>
     Use this option to have <code>mod_sftp</code> silently ignore any
@@ -919,6 +1050,21 @@ The currently implemented options are:
   </li>
 
   <p>
+  <li><code>IgnoreSFTPUploadExtendedAttributes</code><br>
+    <p>
+    When an SFTP client uploads a file or creates a directory, the desired
+    extended attributes ("xattrs") on the path are sent to the server as part
+    of the upload.  (This is different from FTP, which does <b>not</b> include
+    the file attributes in an upload.) If you need more FTP-like functionality
+    for any reason and wish to have <code>mod_sftp</code> silently ignore any
+    extended attributes sent by the SFTP client, use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc3</code>.
+  </li>
+
+  <p>
   <li><code>IgnoreSFTPUploadPerms</code><br>
     <p>
     When an SFTP client uploads a file or creates a directory, the desired
@@ -929,6 +1075,18 @@ The currently implemented options are:
     permissions sent by the SFTP client, use this option.
 
   <p>
+  <li><code>InsecureHostKeyPerms</code><br>
+    <p>
+    When this option is used, <code>mod_sftp</code> will ignore insecure
+    permissions (<i>i.e.</i> group- or world-readable) on
+    <code>SFTPHostKey</code> files.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
   <li><code>MatchKeySubject</code><br>
     <p>
     When this option is used, if public key authentication is used, the
@@ -975,7 +1133,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="SFTPPassPhraseProvider">SFTPPassPhraseProvider</a></h2>
+<h3><a name="SFTPPassPhraseProvider">SFTPPassPhraseProvider</a></h3>
 <strong>Syntax:</strong> SFTPPassPhraseProvider <em>path</em><br>
 <strong>Default:</strong> <em>None</em><br>
 <strong>Context:</strong> server config<br>
@@ -1010,10 +1168,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="SFTPRekey">SFTPRekey</a></h2>
+<h3><a name="SFTPRekey">SFTPRekey</a></h3>
 <strong>Syntax:</strong> SFTPRekey <em>"none"|"required" [[interval bytes] timeout]</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -1031,7 +1189,7 @@ By default, <code>mod_sftp</code> will only <b>require</b> rekeying when
 the packet sequence number (for sent <i>or</i> received SSH2 packets) reaches
 a rather high limit.  The digest algorithm used, for example, might begin
 to leak cryptographically sensitive information if used for too many
-packets (see RFC4344).  Outside of these sequence number limits, however,
+packets (see RFC 4344).  Outside of these sequence number limits, however,
 <code>mod_sftp</code> will not attempt to initiate rekeying.
 
 <p>
@@ -1078,10 +1236,10 @@ Note that normally such rekey timeouts are not necessary.
 
 <p>
 <hr>
-<h2><a name="SFTPTrafficPolicy">SFTPTrafficPolicy</a></h2>
+<h3><a name="SFTPTrafficPolicy">SFTPTrafficPolicy</a></h3>
 <strong>Syntax:</strong> SFTPTrafficPolicy <em>policy</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -1104,7 +1262,7 @@ of policies; the chances columns list the probability that
 <code>mod_sftp</code> will send an <code>SSH_MSG_IGNORE</code> message.
 
 <p>
-<table border=1>
+<table border=1 summary="SFTP Traffic Policies">
   <tr>
     <td><b>Policy Name</b></td>
     <td><b>Chances</b></td>
@@ -1162,57 +1320,34 @@ a <code>SFTPTrafficPolicy</code> setting in your configuration, <i>e.g.</i>
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_sftp</code>, go to the third-party module area in
-the proftpd source code and unpack the <code>mod_sftp</code> source tarball:
-<pre>
-  cd <i>proftpd-dir</i>/contrib/
-  tar zxvf /path/to/mod_sftp-<i>version</i>.tar.gz
-</pre>
-after unpacking the latest proftpd-1.3.2 source code.  For including
-<code>mod_sftp</code> as a staticly linked module:
+The <code>mod_sftp</code> module is distributed with ProFTPD.  For including
+<code>mod_sftp</code> as a staticly linked module, use:
 <pre>
-  ./configure --enable-openssl --with-modules=mod_sftp ...
+  $ ./configure --enable-openssl --with-modules=mod_sftp ...
 </pre>
 Alternatively, <code>mod_sftp</code> can be built as a DSO module:
 <pre>
-  ./configure --enable-dso --enable-openssl --with-shared=mod_sftp ...
+  $ ./configure --enable-dso --enable-openssl --with-shared=mod_sftp ...
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
-
-<p>
-<hr>
-<h2><a name="Usage">Usage</a></h2>
-<p>
-
-<p>
-<b>Compiler Warnings</b><br>
-When compiling <code>mod_sftp</code>, you may notice the following compiler
-warning:
+<b>Note</b> that if the <code>libsodium</code> library is available,
+<code>mod_sftp</code> will auto-detect this, which enables other supported
+algorithms.  You can ensure that <code>mod_sftp</code> compiles with the
+<code>libsodium</code> library like so:
 <pre>
-  contrib/mod_sftp/mod_sftp.a(fxp.o): In function `fxp_handle_create':
-  fxp.c:(.text+0x4dc8): warning: the use of `mktemp' is dangerous, better use `mkstemp'
+  $ ./configure --enable-openssl --with-modules=mod_sftp ... \
+    --with-includes=/path/to/libsodium/include \
+    --with-libraries=/path/to/libsodium/lib
 </pre>
-This warning can be ignored.
 
 <p>
-The <code>mktemp(3)</code> function is generally considered unsafe because,
-in the past, many applications would use this function to create the names of
-temporary files.  The actual problem was not the <code>mktemp(3)</code>
-function itself, but rather that the applications did not check to see
-if the name generated by <code>mktemp(3)</code> already existed in filesytem
-first.  This lead to race conditions where local users could create symlinks
-of the generated names, and the application would overwrite the symlinked file.
-
+<hr>
+<h2><a name="Usage">Usage</a></h2>
 <p>
-In the case of <code>mod_sftp</code>, however, the <code>mktemp(3)</code>
-function is <b>not</b> used to create files.  Instead, it is used to create
-arbitrary unique strings that are used as "handles", as references, in the
-protocol.  These strings have no relation to any actual path on the filesystem,
-and thus do not suffer from the above race condition.
 
 <p>
 <b>Access Controls for SFTP Requests</b><br>
@@ -1327,9 +1462,9 @@ that FTP supports these commands:
     Applies to the <code>LINK</code> and <code>SYMLINK</code> SFTP requests
   </li>
 </ul>
-Note that not all versions of SFTP support all of these requests; the
-<code>LOCK</code> and <code>UNLOCK</code> requests, for example, appear
-only in later versions of SFTP.
+Not all versions of SFTP support all of these requests; the <code>LOCK</code>
+and <code>UNLOCK</code> requests, for example, appear only in later versions
+of the SFTP protocol.
 
 <p>
 What about the <code>READ</code> and <code>WRITE</code> command groups?
@@ -1345,7 +1480,7 @@ What about the <code>READ</code> and <code>WRITE</code> command groups?
   </li>
 </ul>
 
-<p>
+<p><a name="EnvironmentVariables">
 <b>Environment Variables</b><br>
 The <code>mod_sftp</code> module will set the following environment
 variables whenever an SSH2 client connects:
@@ -1358,12 +1493,20 @@ In addition, the following environment variables will be set (with appropriate
 values), once the client has successfully exchanged keys with
 <code>mod_sftp</code>:
 <ul>
-  <li>SFTP_CLIENT_CIPHER_ALGO
-  <li>SFTP_CLIENT_COMPRESSION_ALGO
-  <li>SFTP_CLIENT_MAC_ALGO
-  <li>SFTP_SERVER_CIPHER_ALGO
-  <li>SFTP_SERVER_COMPRESSION_ALGO
-  <li>SFTP_SERVER_MAC_ALGO
+  <li><code>SFTP_CLIENT_BANNER</code>
+  <li><code>SFTP_CLIENT_CIPHER_ALGO</code>
+  <li><code>SFTP_CLIENT_COMPRESSION_ALGO</code>
+  <li><code>SFTP_CLIENT_MAC_ALGO</code>
+  <li><code>SFTP_SERVER_CIPHER_ALGO</code>
+  <li><code>SFTP_SERVER_COMPRESSION_ALGO</code>
+  <li><code>SFTP_SERVER_MAC_ALGO</code>
+</ul>
+And after the client has successfully authenticated, the following variables
+<i>may</i> be set, depending on the authentication mechanism used:
+<ul>
+  <li><code>SFTP_USER_PUBLICKEY_ALGO</code>
+  <li><code>SFTP_USER_PUBLICKEY_FINGERPRINT</code>
+  <li><code>SFTP_USER_PUBLICKEY_FINGERPRINT_ALGO</code>
 </ul>
 
 <p>
@@ -1386,7 +1529,7 @@ This means that if you wish to use your OpenSSH public keys with the
 format to the RFC4716 format.  Fortunately, this is supported by
 OpenSSH's <code>ssh-keygen</code> utility, <i>e.g.</i>:
 <pre>
-  # ssh-keygen -e -f ~/.ssh/id_rsa.pub
+  $ ssh-keygen -e -f ~/.ssh/id_rsa.pub
 </pre>
 The output from this command can be added to the
 <code>SFTPAuthorizedUserKeys</code> used by <code>mod_sftp</code>.
@@ -1564,7 +1707,7 @@ mode, it will refuse to advertise support for the following cipher algorithms:
   <li><code>cast128-cbc</code>
 </ul>
 and for the following digest algorithms:
-</ul>
+<ul>
   <li><code>hmac-md5</code>
   <li><code>hmac-md5-96</code>
   <li><code>hmac-ripemd160</code>
@@ -1624,9 +1767,14 @@ multiple <code><VirtualHost></code> sections for the same <i>address</i>
 <b>Logging</b><br>
 The <code>mod_sftp</code> module supports different forms of logging.  The
 main module logging is done via the <code>SFTPLog</code> directive.  For
-debugging purposes, the module also uses <a href="http://www.proftpd.org/docs/howto/Tracing.html">trace logging</a>, via the module-specific "scp", "sftp",
-and "ssh2" log channels.  Thus for trace logging, to aid in debugging, you
-would use the following in your <code>proftpd.conf</code>:
+debugging purposes, the module also uses <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>ssh2
+  <li>sftp
+  <li>scp
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
 <pre>
   TraceLog /path/to/sftp-trace.log
   Trace scp:20 sftp:20 ssh2:20
@@ -1799,6 +1947,11 @@ configuration, listening on the address and port that you wish, <i>e.g.</i>:
     </VirtualHost>
   </IfModule>
 </pre>
+<b>Note</b> that if you use the above configuration, and you wish to handle
+<b><i>any</i></b> address requested by incoming connections, use
+<code>0.0.0.0</code> as the <i>a.b.c.d</i> IP address.  <code>0.0.0.0</code>
+is known as the "wildcard address", and indicates that ProFTPD wants to listen
+on any/all addresses, on those ports.
 
 <p><a name="SFTPOnly">
 <font color=red>Question</font>: How can I configure <code>proftpd</code>
@@ -1842,12 +1995,102 @@ Chances are that the offending line in the RFC4716 key is a "Comment: " header
 that is too long.  Simply delete that "Comment: " line; the
 <code>mod_sftp</code> ignores this header.
 
+<p><a name="SFTPAuthorizedKeyFormat">
+<font color=red>Question</font>: I generated an SSH key using
+<code>ssh-keygen</code>, then added it to my <code>authorized_keys</code> file
+using:
+<pre>
+  $ cat id_rsa.pub >> authorized_keys
+</pre>
+But logging into <code>mod_sftp</code> still prompts me for my password.
+Why?<br>
+<font color=blue>Answer</font>:  The most likely culprit is that the format
+of your <code>authorized_keys</code> file is not what <code>mod_sftp</code>
+expects.
+
+Per the <a href="#SFTPAuthorizedUserKeys"><code>SFTPAuthorizedUserKeys</code></a> documentation, <code>mod_sftp</code> expects that the keys be in RFC 4716
+format.  If your <code>authorized_keys</code> file contains data that looks
+like this:
+<pre>
+  ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN1tCK5uHUGl....
+</pre>
+rather than looking like this:
+<pre>
+  ---- BEGIN SSH2 PUBLIC KEY ----
+  AAAAB3NzaC1yc2EAAAADAQABAAABAQDN1tCK5uHUGlpxye+KEmB0EIr9zqvMmgyNdWvJBW
+  ...
+  PZ1+hOi5QECWQYD0BpiWvj
+  ---- END SSH2 PUBLIC KEY ----
+</pre>
+Then the format of some of the keys in your <code>authorized_keys</code> file
+is wrong.  Make sure that you convert your OpenSSH-generated keys to RFC 4716
+format using <code>ssh-keygen</code>, <em>before</em> adding it to your
+<code>authorized_keys</code> file, using something like this:
+<pre>
+  $ ssh-keygen -e -f ~/.ssh/id_rsa.pub >> authorized_keys
+</pre>
+See the <a href="#UsageRFC4716Format">Usage</a> documentation for more
+information.
+
+<p><a name="SFTPBrokenMaxFileSize">
+<font color=red>Question</font>: I'm using the <a href="../modules/mod_xfer.html"><code>MaxStoreFileSize</code></a> directive in my <code>mod_sftp</code>
+configuration, but when I upload a file using <i>e.g.</i> OpenSSH's
+<code>sftp</code> client, I see the file uploaded past my configured limit.  Is
+this a bug?<br>
+<font color=blue>Answer</font>: No.  The <code>mod_sftp</code> module honors
+a configured <code>MaxStoreFileSize</code> limit properly.  <i>However</i>,
+what you see <i>in the SFTP client</i> may not reflect this as you would expect.
+Some SFTP clients, like that from OpenSSH, will only reflect the failure
+<i>after the entire file has been uploaded</i>:
+<pre>
+  sftp> put /path/to/file.dat
+  Uploading /path/to/file.dat to /file.dat
+  /path/to/file.dat                           100%  704KB 704.1KB/s   00:00
+  Couldn't write to remote file "/file.dat": Failure
+</pre>
+In the <code>SFTPLog</code> for the above situation, you would most likely
+see log messages about the SFTP <code>WRITE</code> requests being rejected:
+<pre>
+  mod_sftp/0.9.9[32297]: error writing 32768 bytes to '/file.dat': File too large (MaxStoreFileSize 204800 exceeded)
+  mod_sftp/0.9.9[32297]: error writing 32768 bytes to '/file.dat': File too large (MaxStoreFileSize 204800 exceeded)
+</pre>
+
+<p>
+<b>There is a discrepancy between what the client shows the user, and what the
+server is actually doing.</b>  The <code>mod_sftp</code> module, when handling
+SFTP uploads, checks the size of the file on disk, after receiving the buffer
+of data from the network, but <i>before</i> writing that data to disk.  If the
+size of the file on disk exceeds the <code>MaxStoreFileSize</code> limit, then
+that SFTP <code>WRITE</code> request is denied.
+
+<p>
+Many SFTP clients will send multiple SFTP <code>WRITE</code> requests
+concurrently, optimistically hoping that they will succeed; this makes for
+faster SFTP uploads.  This is why you might see multiple "File too large"
+messages logged by <code>mod_sftp</code>.  And it also means that the client
+<i>thinks</i> it has sent more data to the server than has actually been
+written to disk.
+
+<p>
+Now, this may lead you to ask "Due to SFTP protocol itself, there is no way to
+control the file size uploaded by a client, right?"  This is not quite correct.
+
+<p>
+There's no way <em>for an SFTP server</em> to control how much data the
+SFTP client will send.  But there <em>is</em> a way to control how much data is
+written to disk by the SFTP server; that is what <code>MaxStoreFileSize</code>
+does.  But this means that the SFTP client may report how much data it sent
+over the network, which <b>is not the same</b> as how much data was actually
+stored by the SFTP server.  Believing what your client shows you, though, is
+always suspect, since what the client shows may not actually reflect the
+reality of things on the server side of things.
+
 <p><a name="SFTPPCBCModeAttacks">
 <font color=red>Question</font>: Is it true that using CBC mode ciphers in
 SSH is insecure?<br>
 <font color=blue>Answer</font>: It is true that there are a couple of published
 <i>theoretical</i> attacks against the SSH protocol when CBC ciphers are
-used, including Rogaway, Wai, and Bellare (see RFC4251, Section 9.3.1) and
+used, including Rogaway, Wai, and Bellare (see RFC 4251, Section 9.3.1) and
 <a href="http://www.cpni.gov.uk/Docs/Vulnerability_Advisory_SSH.txt">"Plaintext Recovery Attacks Against SSH"</a> (CPNI-957037).
 
 <p>
@@ -1920,6 +2163,16 @@ This explicitly tells the <code>mod_sftp</code> to <b>not</b> drop root
 privileges after authentication, and instead to keep them for the duration
 of the session.
 
+<p>
+In addition, <b>if</b> you are on a Linux machine, you may <em>also</em>
+need to disable the <a href="../modules/mod_cap.html"><code>mod_cap</code></a>
+module, as that can also restrict what can be done with root privileges:
+<pre>
+  <IfModule mod_cap.c>
+    CapabilitiesEngine off
+  </IfModule>
+</pre>
+
 <p><a name="SFTPShell">
 <font color=red>Question</font>: Why can't I use <code>ssh</code> to connect
 to my proftpd+mod_sftp server?  When I try, I see:
@@ -2092,6 +2345,30 @@ configure option is required to enable NLS support, which includes support
 for UTF8.  SFTP protocol versions 4-6 require UTF8 support, which is why
 the <code>--enable-nls</code> configure option is needed.
 
+<p><a name="SFTPExtraREADs">
+<font color=red>Question</font>: Why is my <code>SFTPLog</code> often spammed
+with lots of messages that look like:
+<pre>
+  requested read offset (20480 bytes) greater than size of '/some/path/to.file' (20307 bytes)
+</pre>
+Is there something wrong with my SFTP client, or my configuration?<br>
+<font color=blue>Answer</font>: The short answer is that these extraneous
+<code>READ</code> requests <i>are</i> normal, and do <i>not</i> indicate any
+problem or issue.
+
+<p>
+Many SFTP clients try to improve their download speed by issuing many
+concurrent <code>READ</code> requests, each request with a different
+<em>offset</em>. Thus the client can read multiple "chunks" simultaneously,
+and do the reassembly of the "chunks" into the file itself.  However, sometimes
+these SFTP clients do not necessarily know how <i>many</i> such
+<code>READ</code> requests are needed before the end-of-file is reached.  So
+the SFTP client sends more <code>READ</code>s that might be necessary; the
+server will reply with an error if the requested <em>offset</em> is beyond the
+end of the file.  When this "end of file" reply is received by the SFTP client,
+it will know that it is done downloading.  On the server side, though, this
+behavior results in the above log messages.
+
 <p><a name="SFTPNotUnderstood">
 <font color=red>Question</font>: When I use my SFTP client, I see something
 like this when I try to connect to <code>proftpd</code> with
@@ -2141,21 +2418,30 @@ module, or configure it to preserve the SGID bit (<i>e.g.</i> via the
 <a href="../modules/mod_cap.html#CapabilitiesSet"><code>CAP_FSETID</code></a>
 capability).
 
-<p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-03-03 05:40:13 $</i><br>
+<p><a name="SFTPWarnings">
+<font color=red>Question</font>: In my SFTP logs, I see these warnings:
+<pre>
+  WARNING: unable to read SFTPDHParamFile '<i>path</i>': Permission denied
+  unable to open SFTPKeyBlacklist '<i>path</i>': Permission denied
+</pre>
+What can I do to fix this?<br>
+<font color=blue>Answer</font>: This indicates that the files are not
+readable (usually <i>world</i>-readable) by the users logging in,
+<b>or</b> that the directories containing the files are not readable
+by the users.
 
-<br><hr>
+<p>
+Fortunately <code>mod_sftp</code> will function without issue if it
+cannot read either the <code>SFTPDHParamFile</code> or the
+<code>SFTPKeyBlacklist</code> files.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2008-2014 TJ Saunders<br>
+© Copyright 2008-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sftp_pam.html b/doc/contrib/mod_sftp_pam.html
index 3e70d7c..132ab53 100644
--- a/doc/contrib/mod_sftp_pam.html
+++ b/doc/contrib/mod_sftp_pam.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sftp_pam.html,v 1.5 2013-10-03 05:21:56 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sftp_pam.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sftp_pam</title>
@@ -46,10 +44,10 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="SFTPPAMEngine">SFTPPAMEngine</a></h2>
+<h3><a name="SFTPPAMEngine">SFTPPAMEngine</a></h3>
 <strong>Syntax:</strong> SFTPPAMEngine <em>on|off</em><br>
 <strong>Default:</strong> On<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp_pam<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -60,10 +58,10 @@ By default <code>mod_sftp_pam</code> is enabled.
 
 <p>
 <hr>
-<h2><a name="SFTPPAMOptions">SFTPPAMOptions</a></h2>
+<h3><a name="SFTPPAMOptions">SFTPPAMOptions</a></h3>
 <strong>Syntax:</strong> SFTPPAMOptions <em>opt1 opt2 ... optN</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp_pam<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -98,10 +96,10 @@ The currently supported options are:
 
 <p>
 <hr>
-<h2><a name="SFTPPAMServiceName">SFTPPAMServiceName</a></h2>
+<h3><a name="SFTPPAMServiceName">SFTPPAMServiceName</a></h3>
 <strong>Syntax:</strong> SFTPPAMServiceName <em>service</em><br>
 <strong>Default:</strong> SFTPPAMServiceName sshd<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sftp_pam<br>
 <strong>Compatibility:</strong> 1.3.2rc2 and later
 
@@ -127,20 +125,20 @@ The <code>SFTPPAMServiceName</code> directive is directly analogous to
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_sftp_pam</code> module is distributed with ProFTPD.  Simply follow
-the normal steps for using third-party modules in proftpd:
+the normal steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=mod_sftp:mod_sftp_pam ...
-  make
-  make install
+  $ ./configure --with-modules=mod_sftp:mod_sftp_pam ...
+  $ make
+  $ make install
 </pre>
 Alternatively, <code>mod_sftp_pam</code> can be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_sftp_pam ...
+  $ ./configure --enable-dso --with-shared=mod_sftp_pam ...
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -148,7 +146,7 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_sftp_pam</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_sftp_pam.c
+  $ prxs -c -i -d mod_sftp_pam.c
 </pre>
 
 <p>
@@ -167,20 +165,12 @@ service name as the <code>mod_auth_pam</code> module; this allows you to have
 different PAM configurations for FTP versus SSH2 logins.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-10-03 05:21:56 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2008-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sftp_sql.html b/doc/contrib/mod_sftp_sql.html
index 3a25d00..6bbbec1 100644
--- a/doc/contrib/mod_sftp_sql.html
+++ b/doc/contrib/mod_sftp_sql.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sftp_sql.html,v 1.5 2010-03-02 19:03:55 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sftp_sql.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sftp_sql</title>
@@ -15,11 +13,11 @@
 <hr><br>
 
 <p>
-The <a href="http://www.castaglia.org/proftpd/modules/mod_sftp.html"><code>mod_sftp</code></a> module for ProFTPD can support different storage formats for
-its user- and host-based authorized keys.  By default, the <code>mod_sftp</code>
-module supports storing authorized keys in flats.  This
-<code>mod_sftp_sql</code> module allows for authorized SSH keys to be stored
-in SQL tables.
+The <a href="../contrib/mod_sftp.html"><code>mod_sftp</code></a> module for
+ProFTPD can support different storage formats for its user- and host-based
+authorized keys.  By default, the <code>mod_sftp</code> module supports storing
+authorized keys in flat files.  This <code>mod_sftp_sql</code> module allows
+for authorized SSH keys to be stored in SQL tables.
 
 <p>
 This module is contained in the <code>mod_sftp_sql.c</code> file for
@@ -45,20 +43,15 @@ questions, concerns, or suggestions regarding this module.
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_sftp_sql</code>, copy the <code>mod_sftp_sql.c</code> file
-into:
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  Then follow the
-usual steps for using third-party modules in proftpd, making sure to include
-the <code>mod_sftp</code> and <code>mod_sql</code> modules, which
+To build <code>mod_sftp_sql</code>, follow the usual steps for using
+third-party modules in ProFTPD, making sure to include the
+<code>mod_sftp</code> and <code>mod_sql</code> modules, which
 <code>mod_sftp_sql</code> requires.  For example, if you use MySQL as your
 SQL database, then you might use:
 <pre>
-  ./configure --with-modules=mod_sql:mod_sql_mysql:mod_sftp:mod_sftp_sql ...
-  make
-  make install
+  $ ./configure --with-modules=mod_sql:mod_sql_mysql:mod_sftp:mod_sftp_sql ...
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -134,14 +127,14 @@ recommended.
 An example MySQL schema looks like:
 <pre>
   CREATE TABLE sftpuserkeys (
-    name VARCHAR(255) NOT NULL,
-    key VARCHAR(255) NOT NULL
+    name TEXT(8192) NOT NULL,
+    key TEXT(8192) NOT NULL
   );
   CREATE INDEX sftpuserkeys_idx ON sftpuserkeys (name);
 
   CREATE TABLE sftphostkeys (
-    host VARCHAR(255) NOT NULL,
-    key VARCHAR(255) NOT NULL
+    host TEXT(8192) NOT NULL,
+    key TEXT(8192) NOT NULL
   );
   CREATE INDEX sftphostkeys_idx ON sftphostkeys (host);
 </pre>
@@ -162,20 +155,20 @@ bulk data loading tools which can also be used to load a CSV file containing
 keys into your SQL tables, for use via <code>mod_sftp_sql</code>.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2010-03-02 19:03:55 $</i><br>
-
-<br><hr>
+<b>Note</b> that the newlines which are part of the RFC 4716 formatted key
+data <b>are important</b>.  Use of the wrong data type in your SQL schema
+could lead to unexpected parsing issues, which will be logged as:
+<pre>
+  mod_sftp_sql/0.4[16284]: error base64-decoding raw key data from database
+</pre>
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2009-2010 TJ Saunders<br>
+© Copyright 2009-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_shaper.html b/doc/contrib/mod_shaper.html
index 33001d6..a58d605 100644
--- a/doc/contrib/mod_shaper.html
+++ b/doc/contrib/mod_shaper.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_shaper.html,v 1.4 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_shaper.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_shaper</title>
@@ -374,34 +372,29 @@ See also: <a href="#shaper_all"><code>shaper all</code></a>
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_shaper</code>, copy the <code>mod_shaper.c</code> file
-into:
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.2.<i>x</i> or proftpd-1.3.<i>x</i> source
-code.  Then follow the usual steps for using third-party modules in proftpd,
-making sure to include the <code>--enable-ctrls</code> configure option,
-which <code>mod_shaper</code> requires:
+The <code>mod_shaper</code> module is distributed with ProFTPD.  Follow the
+usual steps for using third-party modules in ProFTPD, making sure to include
+the <code>--enable-ctrls</code> configure option, which
+<code>mod_shaper</code> requires:
 <pre>
-  ./configure --enable-ctrls --with-modules=mod_shaper
+  $ ./configure --enable-ctrls --with-modules=mod_shaper
 </pre>
 To build <code>mod_shaper</code> as a DSO module:
 <pre>
-  ./configure --enable-ctrls --enable-dso --with-shared=mod_shaper
+  $ ./configure --enable-ctrls --enable-dso --with-shared=mod_shaper
 </pre>
 Then follow the usual steps:
 <pre>
-  make 
-  make install
+  $ make 
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_shaper</code> as a shared
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_shaper</code> as a shared
 module:
 <pre>
-  prxs -c -i -d mod_shaper.c
+  $ prxs -c -i -d mod_shaper.c
 </pre>
 
 <p>
@@ -495,7 +488,7 @@ session.  The priority for <code>TransferRate</code> directives depends on
 the configuration context in which the directive appears:
 <p>
 <b>TransferRate Priority</b><br>
-<table border=1>
+<table border=1 summary="TransferRate Priorities">
   <tr>
     <td align=left><i>Context</i></td>
     <td align=left><i>Priority</i></td>
@@ -518,11 +511,11 @@ the configuration context in which the directive appears:
   </tr>
   <tr>
     <td align=left><code><Directory></code></td>
-    <td align=right>4</code>
+    <td align=right>4</td>
   </tr>
   <tr>
     <td align=left><code>.ftpaccess</code></td>
-    <td align=right>5</code>
+    <td align=right>5</td>
   </tr>
 </table>
 <p>
@@ -607,20 +600,12 @@ The control actions supported by <code>mod_shaper</code> can also be used,
 while the daemon and sessions are running, to set the desired shaping values.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:18 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2004-2013 TJ Saunders<br>
+© Copyright 2004-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_site_misc.html b/doc/contrib/mod_site_misc.html
index a3eb3d5..ed09387 100644
--- a/doc/contrib/mod_site_misc.html
+++ b/doc/contrib/mod_site_misc.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_site_misc.html,v 1.5 2014-05-04 19:49:57 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_site_misc.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_site_misc</title>
@@ -53,12 +51,12 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="SiteMiscEngine">SiteMiscEngine</a></h2>
+<h3><a name="SiteMiscEngine">SiteMiscEngine</a></h3>
 <strong>Syntax:</strong> SiteMiscEngine <em>on|off</em><br>
 <strong>Default:</strong> on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_site_misc<br>
-<strong>Compatibility:</strong> 1.3.3c and later</a>
+<strong>Compatibility:</strong> 1.3.3c and later
 
 <p>
 The <code>SiteMiscEngine</code> directive enables or disables the module's
@@ -68,7 +66,7 @@ directive to disable the module.
 
 <p>
 <hr>
-<h2><a name="SITE_MKDIR">SITE MKDIR</a></h2>
+<h3><a name="SITE_MKDIR">SITE MKDIR</a></h3>
 This <code>SITE</code> command allows the creation of a full directory
 path, similar to <code>mkdir -p /path/to/dir</code>.  It is primarily for
 convenience, instead of having to use a loop of <code>MKD</code> and
@@ -83,7 +81,7 @@ The syntax for <code>SITE MKDIR</code> is:
 
 <p>
 Use of this <code>SITE</code> command can be controlled via
-<code><Limit><code> sections, <i>e.g.</i>:
+<code><Limit></code> sections, <i>e.g.</i>:
 <pre>
   <Limit SITE_MKDIR>
     AllowUser alex
@@ -93,7 +91,7 @@ Use of this <code>SITE</code> command can be controlled via
 
 <p>
 <hr>
-<h2><a name="SITE_RMDIR">SITE RMDIR</a></h2>
+<h3><a name="SITE_RMDIR">SITE RMDIR</a></h3>
 This <code>SITE</code> command allows the deletion of a full directory
 path, similar to <code>rm -fr /path/to/dir</code>.  It recursively deletes all
 of the files and directories under the given path.  This command is
@@ -111,7 +109,7 @@ The syntax for <code>SITE RMDIR</code> is:
 
 <p>
 Use of this <code>SITE</code> command can be controlled via
-<code><Limit><code> sections, <i>e.g.</i>:
+<code><Limit></code> sections, <i>e.g.</i>:
 <pre>
   <Limit SITE_RMDIR>
     AllowUser alex
@@ -121,7 +119,7 @@ Use of this <code>SITE</code> command can be controlled via
 
 <p>
 <hr>
-<h2><a name="SITE_SYMLINK">SITE SYMLINK</a></h2>
+<h3><a name="SITE_SYMLINK">SITE SYMLINK</a></h3>
 This <code>SITE</code> command allows the creation of symbolic links, similar
 to <code>ln -s</code>.
 
@@ -133,7 +131,7 @@ The syntax for <code>SITE SYMLINK</code> is:
 
 <p>
 Use of this <code>SITE</code> command can be controlled via
-<code><Limit><code> sections, <i>e.g.</i>:
+<code><Limit></code> sections, <i>e.g.</i>:
 <pre>
   <Limit SITE_SYMLINK>
     AllowUser alex
@@ -143,7 +141,7 @@ Use of this <code>SITE</code> command can be controlled via
 
 <p>
 <hr>
-<h2><a name="SITE_UTIME">SITE UTIME</a></h2>
+<h3><a name="SITE_UTIME">SITE UTIME</a></h3>
 This <code>SITE</code> command allows for setting the access and modification
 timestamps on files, similar to the <code>touch /path/to/file</code> command.
 This allows sites to have the often-requested ability to set the modification
@@ -159,16 +157,25 @@ For example:
   SITE UTIME 200402240836 file.txt
   SITE UTIME 20040224083655 file.txt
 </pre>
-would set the access <b>and</b> modification timestamps on <code>file.txt</code>
 to 8:36 AM, Febrary 24, 2004 (or 8:36:55 AM, Febrary 24, 2004, respectively).
 
 <p>
+Another variant syntax is also supported:
+<pre>
+  SITE UTIME <i>path</i> <i>YYYYMMDDhhmm[ss]</i> <i>YYYYMMDDhhmm[ss]</i> <i>YYYYMMDDhhmm[ss]</i> UTC
+</pre>
+For example:
+<pre>
+  SITE UTIME file.txt 20040224083655 20040224083655 20040224083655 UTC
+</pre>
+
+<p>
 The timestamp specified is treated as being in GMT/UTC, rather than in the
 local timezone.
 
 <p>
 Use of this <code>SITE</code> command can be controlled via
-<code><Limit><code> sections, <i>e.g.</i>:
+<code><Limit></code> sections, <i>e.g.</i>:
 <pre>
   <Limit SITE_UTIME>
     AllowUser alex
@@ -182,41 +189,33 @@ Use of this <code>SITE</code> command can be controlled via
 The <code>mod_site_misc</code> module is distributed with ProFTPD.  Simply
 follow the normal steps for using third-party modules in proftpd:
 <pre>
-  ./configure --with-modules=mod_site_misc
+  $ ./configure --with-modules=mod_site_misc
 </pre>
 To build <code>mod_site_misc</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_site_misc
+  $ ./configure --enable-dso --with-shared=mod_site_misc
 </pre>
 Then follow the usual steps:
 <pre>
-  make 
-  make install
+  $ make 
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_site_misc</code> as a shared
-module:
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_site_misc</code> as a
+shared module:
 <pre>
-  prxs -c -i -d mod_site_misc.c
+  $ prxs -c -i -d mod_site_misc.c
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-05-04 19:49:57 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2004-2014 TJ Saunders<br>
+© Copyright 2004-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_snmp.html b/doc/contrib/mod_snmp.html
index c92a980..0957442 100644
--- a/doc/contrib/mod_snmp.html
+++ b/doc/contrib/mod_snmp.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_snmp.html,v 1.1 2013-05-15 15:21:44 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_snmp.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_snmp</title>
@@ -33,10 +31,8 @@ default.  Installation instructions are discussed
 <a href="#Installation">here</a>.
 
 <p>
-The most current version of <code>mod_snmp</code> can be found at:
-<pre>
-  <a href="https://github.com/Castaglia/proftpd-mod_snmp">https://github.com/Castaglia/proftpd-mod_snmp</a>
-</pre>
+The most current version of <code>mod_sftp</code> is distributed with the
+ProFTPD source code.
 
 <h2>Author</h2>
 <p>
@@ -63,10 +59,10 @@ functionality, and providing OpenNMS support/examples.
 
 <p>
 <hr>
-<h2><a name="SNMPAgent">SNMPAgent</a></h2>
-<strong>Syntax:</strong> SNMPAgent master|agentx <em>address[:port]</em><br>
+<h3><a name="SNMPAgent">SNMPAgent</a></h3>
+<strong>Syntax:</strong> SNMPAgent master|agentx <em>address[:port] ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -83,16 +79,28 @@ for UDP SNMP packets.  By default, a port of 161 is assumed, use
 <pre>
   SNMPAgent master localhost:1161
 </pre>
+Note that IPv6 addresses should be enclosed in square brackets, as they can
+contain colons as well, <i>e.g.</i>:
+<pre>
+  SNMPAgent master [::1]:1161
+</pre>
+
+<p>
+Multiple addresses can be supplied, allowing <code>mod_snmp</code> to listen
+on multiple addresses/ports simultaneously:
+<pre>
+  SNMPAgent master 1.2.3.4:1161 [a::f]:2262
+</pre>
 
 <p>
 Note that the <code>SNMPAgent</code> directive is <b>required</b>.
 
 <p>
 <hr>
-<h2><a name="SNMPCommunity">SNMPCommunity</a></h2>
+<h3><a name="SNMPCommunity">SNMPCommunity</a></h3>
 <strong>Syntax:</strong> SNMPCommunity <em>community</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -106,10 +114,10 @@ Note that the <code>SNMPCommunity</code> directive is <b>required</b>.
 
 <p>
 <hr>
-<h2><a name="SNMPEngine">SNMPEngine</a></h2>
+<h3><a name="SNMPEngine">SNMPEngine</a></h3>
 <strong>Syntax:</strong> SNMPEngine <em>on|off</em><br>
 <strong>Default:</strong> <em>off</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -119,10 +127,10 @@ will run as an SNMP agent, and handle SNMP messages.
 
 <p>
 <hr>
-<h2><a name="SNMPLog">SNMPLog</a></h2>
+<h3><a name="SNMPLog">SNMPLog</a></h3>
 <strong>Syntax:</strong> SNMPLog <em>file|"none"</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -138,10 +146,10 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="SNMPNotify">SNMPNotify</a></h2>
+<h3><a name="SNMPNotify">SNMPNotify</a></h3>
 <strong>Syntax:</strong> SNMPNotify <em>address[:port]</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -164,10 +172,10 @@ Multiple <code>SNMPNotify</code> directives can be configured;
 
 <p>
 <hr>
-<h2><a name="SNMPOptions">SNMPOptions</a></h2>
+<h3><a name="SNMPOptions">SNMPOptions</a></h3>
 <strong>Syntax:</strong> SNMPOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -193,10 +201,10 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="SNMPTables">SNMPTables</a></h2>
+<h3><a name="SNMPTables">SNMPTables</a></h3>
 <strong>Syntax:</strong> SNMPTables <em>path</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_snmp<br>
 <strong>Compatibility:</strong> 1.3.4rc3 and later
 
@@ -208,31 +216,24 @@ are used for tracking the various statistics reported via SNMP.
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_snmp</code>, go to the third-party module area in
-the proftpd source code and unpack the <code>mod_snmp</code> source tarball:
+The <code>mod_snmp</code> module is distributed with ProFTPD.  For including
+<code>mod_snmp</code> as a staticly linked module, use:
 <pre>
-  cd <i>proftpd-dir</i>/contrib/
-  tar zxvf /path/to/mod_snmp-<i>version</i>.tar.gz
-</pre>
-after unpacking the latest proftpd-1.3.4 (or later) source code.  For including
-<code>mod_snmp</code> as a staticly linked module:
-<pre>
-  ./configure --with-modules=mod_snmp ...
+  $ ./configure --with-modules=mod_snmp ...
 </pre>
 Alternatively, <code>mod_snmp</code> can be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_snmp ...
+  $ ./configure --enable-dso --with-shared=mod_snmp ...
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
 <hr>
 <h2><a name="Usage">Usage</a></h2>
-<p>
 
 <p>
 <b>Important Security Considerations</b><br>
@@ -307,9 +308,19 @@ can reach the <code>mod_snmp</code> address/port.
 <b>Logging</b><br>
 The <code>mod_snmp</code> module supports different forms of logging.  The
 main module logging is done via the <code>SNMPLog</code> directive.  For
-debugging purposes, the module also uses <a href="http://www.proftpd.org/docs/howto/Tracing.html">trace logging</a>, via the module-specific "snmp" and
-"snmp.db" log channels.  Thus for trace logging, to aid in debugging, you
-would use the following in your <code>proftpd.conf</code>:
+debugging purposes, the module also uses <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>snmp
+  <li>snmp.asn1
+  <li>snmp.db
+  <li>snmp.mib
+  <li>snmp.msg
+  <li>snmp.notify
+  <li>snmp.pdu
+  <li>snmp.smi
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
 <pre>
   TraceLog /path/to/snmp-trace.log
   Trace snmp:20
@@ -324,13 +335,13 @@ use only, and should be removed from any production configuration.
 contains the ProFTPD versions where the OID is present.
 
 <p>
-<table border=1>
+<table border=1 summary="ProFTPD SNMP OIDs">
   <tr>
-    <td> <b>OID<b> </td>
-    <td> <b>Name<b> </td>
-    <td> <b>Type<b> </td>
-    <td> <b><code>ProFTPD</code><b> </td>
-    <td> <b>Description<b> </td>
+    <td> <b>OID</b> </td>
+    <td> <b>Name</b> </td>
+    <td> <b>Type</b> </td>
+    <td> <b><code>ProFTPD</code></b> </td>
+    <td> <b>Description</b> </td>
   </tr>
 
   <!-- daemon arc -->
@@ -1264,7 +1275,7 @@ according to need, demand, inclination, and time:
   <li>Controls support (<i>e.g.</i> for "ftpdctl snmp" action)
 </ul>
 
-<p><a name="Notifications">
+<p><a name="Notifications"></a>
 <b>Notifications</b><br>
 The <code>mod_snmp</code> module supports sending notifications (via SNMP
 <i>traps</i>) whenever certain events occur or conditions are met.  Note
@@ -1290,10 +1301,10 @@ that should be notified via the
 Agent process?<br>
 <font color=blue>Answer</font>: You can test if your <code>proftpd</code>
 supports SNMP with the <code>snmpwalk</code> program (<code>snmpwalk</code> is
-a part of the <a href="http://net-snmp.sourceforge.net">Net-SNMP</code></a>
-project). Note that you have to specify the SNMP port, which in
-<code>mod_snmp</code> is configured via the
-<a href="#SNMPAgent"><code>SNMPAgent</code></a> directive.
+a part of the
+<a href="http://net-snmp.sourceforge.net"><code>Net-SNMP</code></a> project).
+Note that you have to specify the SNMP port, which in <code>mod_snmp</code> is
+configured via the <a href="#SNMPAgent"><code>SNMPAgent</code></a> directive.
 
 <p>
 For example, you might try:
@@ -1331,20 +1342,12 @@ counters/gauges, such as the <code>ftp.dataTransfers</code> and
 to get <i>just</i> a directory listing; the protocol only transfers files.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-05-15 15:21:44 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2011-2013 TJ Saunders<br>
+© Copyright 2011-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sql.html b/doc/contrib/mod_sql.html
index 8e2eb91..64ed386 100644
--- a/doc/contrib/mod_sql.html
+++ b/doc/contrib/mod_sql.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sql.html,v 1.32 2014-01-21 22:32:31 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sql.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sql</title>
@@ -14,12 +12,6 @@
 </center>
 <hr><br>
 
-This module is contained in the <code>contrib/mod_sql.c</code>,
-<code>contrib/mod_sql.h</code>, <code>contrib/mod_sql_mysql.c</code>, and
-<code>contrib/mod_sql_postgres.c</code> files for ProFTPD 1.3.<i>x</i>, and is
-not compiled by default.  Installation instructions are discussed
-<a href="#Installation">here</a>.
-
 <p>
 The <code>mod_sql</code> module is an authentication and logging module
 for ProFTPD.  It is comprised of a front end module (<code>mod_sql</code>)
@@ -29,6 +21,10 @@ front end module leaves the specifics of handling database connections to the
 backend modules.
 
 <p>
+The <code>mod_sql</code> module is not compiled by default.  Installation
+instructions are discussed <a href="#Installation">here</a>.
+
+<p>
 This product includes software developed by the OpenSSL Project for use in the
 OpenSSL Toolkit (http://www.openssl.org/).
 
@@ -84,7 +80,7 @@ The most current version of <code>mod_sql</code> is distributed with ProFTPD.
 </ul>
 
 <hr>
-<h2><a name="SQLAuthenticate">SQLAuthenticate</a></h2>
+<h3><a name="SQLAuthenticate">SQLAuthenticate</a></h3>
 <strong>Syntax:</strong> SQLAuthenticate <em>on|off</em> <i>or</i><br>
 <strong>Syntax:</strong> SQLAuthenticate <em>[users] [groups] [userset[fast]] [groupset[fast]]</em><br>
 <strong>Default:</strong> on<br>
@@ -211,8 +207,8 @@ entries are structured like the last example.
 
 <p>
 <hr>
-<h2><a name="SQLAuthTypes">SQLAuthTypes</a></h2>
-<strong>Syntax:</strong> SQLAuthTypes <em>["Backend&quot | "Crypt" | "Empty" | "OpenSSL" | "Plaintext"] ...</em><br>
+<h3><a name="SQLAuthTypes">SQLAuthTypes</a></h3>
+<strong>Syntax:</strong> SQLAuthTypes <em>["Backend"|"Crypt"|"Empty"|"OpenSSL"|"Plaintext"] ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql<br>
@@ -288,7 +284,7 @@ module also provides other <code>SQLAuthTypes</code> values.
 
 <p>
 <hr>
-<h2><a name="SQLBackend">SQLBackend</a></h2>
+<h3><a name="SQLBackend">SQLBackend</a></h3>
 <strong>Syntax:</strong> SQLBackend <em>backend</em><br>
 <strong>Default:</strong> Depends<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -335,8 +331,8 @@ Use "mysql" for the <code>mod_sql_mysql</code> module, and
 
 <p>
 <hr>
-<h2><a name="SQLConnectInfo">SQLConnectInfo</a></h2>
-<strong>Syntax:</strong> SQLConnectInfo <em>connection-info [username] [password] [policy]</em><br>
+<h3><a name="SQLConnectInfo">SQLConnectInfo</a></h3>
+<strong>Syntax:</strong> SQLConnectInfo <em>connection-info [username] [password] [policy] [ssl-ca:<path>] [ssl-cert:<path>] [ssl-key:>path<] [ssl-ciphers:<list>]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql<br>
@@ -444,6 +440,33 @@ library will try to use Unix domain socket communications for that host,
 these connections).
 
 <p>
+In <code>proftpd-1.3.6rc2</code> and later, it is possible to configure SSL/TLS
+parameters for a given connection, which tells <code>mod_sql</code> to try
+to open an SSL session with the database server.  Most of the time, all that
+is needed for the SSL session is the CA (Certificate Authority) to use, for
+verifying the certificate presented by the database server.  Thus:
+<pre>
+  SQLConnectInfo ... ssl-ca:/path/to/cacert.pem
+</pre>
+If your database server is configured to require SSL/TLS mutual authentication
+(also called "client auth"), you may need the <code>ssl-cert:</code> and
+<code>ssl-key:</code> parameters as well:
+<pre>
+  SQLConnectInfo ... ssl-ca:/path/to/cacert.pem \
+    ssl-cert:/path/to/client-cert.pem \
+    ssl-key:/path/to/client-key.pem
+</pre>
+Finally, some database clients (such as MySQL) allow you to configure the
+specific SSL/TLS ciphersuites that should be used; the <code>ssl-ciphers:</code>
+parameter can be used for this:
+<pre>
+  SQLConnectInfo ... ssl-ca:/path/to/cacert.pem \
+    ssl-cert:/path/to/client-cert.pem \
+    ssl-key:/path/to/client-key.pem \
+    ssl-ciphers:DEFAULT:!EXPORT:!DES
+</pre>
+
+<p>
 Examples:
 <pre>
   # Connect to the database 'ftpusers' via the default port at host
@@ -465,13 +488,19 @@ Examples:
   # Use a username of 'admin' and a password of 'mypassword' when
   # connecting.  A 30 second timer of connection inactivity is activated.
   SQLConnectInfo ftpusers at foo.com:3000 admin mypassword 30
+
+  # Connect to the database 'ftpusers' via port 3000 at host 'foo.com'.
+  # Use a username of 'admin' and a password of 'mypassword' when
+  # connection.  A 30 second inactivity/idle timer is used.  In addition,
+  # use SSL for the connection.
+  SQLConnectInfo ftpusers at foo.com:3000 admin mypassword 30 ssl-ca:/path/to/cacert.pem
 </pre>
 Backends may require different information in the <em>connection-info</em>
 field; check your backend module for more detailed information.
 
 <p>
 <hr>
-<h2><a name="SQLDefaultGID">SQLDefaultGID</a></h2>
+<h3><a name="SQLDefaultGID">SQLDefaultGID</a></h3>
 <strong>Syntax:</strong> SQLDefaultGID <em>default-gid</em><br>
 <strong>Default:</strong> 65533<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -487,7 +516,7 @@ See also: <a href="#SQLMinUserGID"><code>SQLMinUserGID</code></a>
 
 <p>
 <hr>
-<h2><a name="SQLDefaultHomedir">SQLDefaultHomedir</a></h2>
+<h3><a name="SQLDefaultHomedir">SQLDefaultHomedir</a></h3>
 <strong>Syntax:</strong> SQLDefaultHomedir <em>path</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -508,7 +537,7 @@ See also: <a href="#SQLUserInfo">SQLUserInfo</a>
 
 <p>
 <hr>
-<h2><a name="SQLDefaultUID">SQLDefaultUID</a></h2>
+<h3><a name="SQLDefaultUID">SQLDefaultUID</a></h3>
 <strong>Syntax:</strong> SQLDefaultUID <em>default-uid</em><br>
 <strong>Default:</strong> 65533<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -524,7 +553,7 @@ See also: <a href="#SQLMinUserUID"><code>SQLMinUserUID</code></a>
 
 <p>
 <hr>
-<h2><a name="SQLEngine">SQLEngine</a></h2>
+<h3><a name="SQLEngine">SQLEngine</a></h3>
 <strong>Syntax:</strong> SQLEngine <em>on|off|auth|log</em><br>
 <strong>Default:</strong> SQLEngine on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -558,10 +587,10 @@ that do not use <code>mod_sql</code>, <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="SQLGroupInfo">SQLGroupInfo</a></h2>
+<h3><a name="SQLGroupInfo">SQLGroupInfo</a></h3>
 <strong>Syntax:</strong> SQLGroupInfo <em>group-table group-name gid members</em><br>
 <strong>Default:</strong> "groups groupname gid members"<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql<br>
 <strong>Compatibility:</strong> 1.2.5rc1 and later
 
@@ -673,10 +702,10 @@ supporting the old, inefficient comma-delimited format for the members column.)
 
 <p>
 <hr>
-<h2><a name="SQLGroupPrimaryKey">SQLGroupPrimaryKey</a></h2>
+<h3><a name="SQLGroupPrimaryKey">SQLGroupPrimaryKey</a></h3>
 <strong>Syntax:</strong> SQLGroupPrimaryKey <em>column | "custom:/"named-query</em><br>
 <strong>Default:</strong> gid<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql<br>
 <strong>Compatibility:</strong> 1.3.5rc3 and later
 
@@ -692,7 +721,7 @@ See also: <a href="#SQLUserPrimaryKey"><code>SQLUserPrimaryKey</code></a>
 
 <p>
 <hr>
-<h2><a name="SQLGroupWhereClause">SQLGroupWhereClause</a></h2>
+<h3><a name="SQLGroupWhereClause">SQLGroupWhereClause</a></h3>
 <strong>Syntax:</strong> SQLGroupWhereClause <em>where-clause</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -729,7 +758,7 @@ parameter can use the same set of variables as supported by the
 
 <p>
 <hr>
-<h2><a name="SQLLog">SQLLog</a></h2>
+<h3><a name="SQLLog">SQLLog</a></h3>
 <strong>Syntax:</strong> SQLLog <em>cmd-set query-name ["IGNORE_ERRORS"]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -740,6 +769,7 @@ parameter can use the same set of variables as supported by the
 This directive is used to log information to a database table. Multiple
 <code>SQLLog</code> directives can be in effect for any command; for example,
 a user changing directories can trigger multiple logging statements.
+<b>Note</b> that this logging occurs <em>at the end of command processing</em>.
 
 <p>
 The first parameter to <code>SQLLog</code>, the <em>cmd-set</em>, is a
@@ -818,7 +848,7 @@ transferred, the user and host doing the transfer, and the time of transfer
 
 <p>
 <hr>
-<h2><a name="SQLLogFile">SQLLogFile</a></h2>
+<h3><a name="SQLLogFile">SQLLogFile</a></h3>
 <strong>Syntax:</strong> SQLLogFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -841,7 +871,7 @@ a <code><Global></code> context.
 
 <p>
 <hr>
-<h2><a name="SQLMinID">SQLMinID</a></h2>
+<h3><a name="SQLMinID">SQLMinID</a></h3>
 <strong>Syntax:</strong> SQLMinID <em>minimum-id</em><br>
 <strong>Default:</strong> 999<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -859,7 +889,7 @@ See also: <a href="#SQLMinUserGID"><code>SQLMinUserGID</code></a>,
 
 <p>
 <hr>
-<h2><a name="SQLMinUserGID">SQLMinUserGID</a></h2>
+<h3><a name="SQLMinUserGID">SQLMinUserGID</a></h3>
 <strong>Syntax:</strong> SQLMinUserGID <em>minimum-gid</em><br>
 <strong>Default:</strong> 999<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -876,7 +906,7 @@ See also: <a href="#SQLDefaultGID"><code>SQLDefaultGID</code></a>
 
 <p>
 <hr>
-<h2><a name="SQLMinUserUID">SQLMinUserUID</a></h2>
+<h3><a name="SQLMinUserUID">SQLMinUserUID</a></h3>
 <strong>Syntax:</strong> SQLMinUserUID <em>minimum-uid</em><br>
 <strong>Default:</strong> 999<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -893,8 +923,8 @@ See also: <a href="#SQLDefaultUID"><code>SQLDefaultUID</code></a>
 
 <p>
 <hr>
-<h2><a name="SQLNamedConnectInfo">SQLNamedConnectInfo</a></h2>
-<strong>Syntax:</strong> SQLConnectInfo <em>connection-name</em> <em>sql-backend</em> <em>connection-info [username] [password] [policy]</em><br>
+<h3><a name="SQLNamedConnectInfo">SQLNamedConnectInfo</a></h3>
+<strong>Syntax:</strong> SQLConnectInfo <em>connection-name</em> <em>sql-backend</em> <em>connection-info [username] [password] [policy] [ssl-ca:<path>] [ssl-cert:<path>] [ssl-key:>path<] [ssl-ciphers:<list>]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql<br>
@@ -913,11 +943,11 @@ other backend-specific information.  The optional <em>username</em> and
 connecting to the database.  Both default to <code>NULL</code>, which the
 backend will treat in some backend-specific manner. If you specify a password,
 you <b>must</b> specify a username.  Multiple <code>SQLNamedConnectInfo</code>
-directives can be configured.
+directives may be configured.
 
 <p>
-<b>Note</b> that <code>SQLNamedConnectInfo</code> directives will only be
-honored if a <a href="#SQLConnectInfo"><code>SQLConnectInfo</code></a>
+<b>Note</b> that <code>SQLNamedConnectInfo</code> directives <b>will only be
+honored</b> if a <a href="#SQLConnectInfo"><code>SQLConnectInfo</code></a>
 directive is configured.
 
 <p>
@@ -926,7 +956,7 @@ See also: <a href="#SQLBackend"><code>SQLBackend</code></a>,
 
 <p>
 <hr>
-<h2><a name="SQLNamedQuery">SQLNamedQuery</a></h2>
+<h3><a name="SQLNamedQuery">SQLNamedQuery</a></h3>
 <strong>Syntax:</strong> SQLNamedQuery <em>name type query-string [table] [connection-name]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1064,7 +1094,7 @@ user "tilda".
 
 <p>
 <hr>
-<h2><a name="SQLNegativeCache">SQLNegativeCache</a></h2>
+<h3><a name="SQLNegativeCache">SQLNegativeCache</a></h3>
 <strong>Syntax:</strong> SQLNegativeCache <em>on|off</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1084,7 +1114,7 @@ SQL database.
 
 <p>
 <hr>
-<h2><a name="SQLOptions">SQLOptions</a></h2>
+<h3><a name="SQLOptions">SQLOptions</a></h3>
 <strong>Syntax:</strong> SQLOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1158,7 +1188,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="SQLRatios">SQLRatios</a></h2>
+<h3><a name="SQLRatios">SQLRatios</a></h3>
 <strong>Syntax:</strong>  <em> </em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <br>
@@ -1167,7 +1197,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="SQLRatioStats">SQLRatioStats</a></h2>
+<h3><a name="SQLRatioStats">SQLRatioStats</a></h3>
 <strong>Syntax:</strong>  <em> </em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <br>
@@ -1176,7 +1206,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="SQLShowInfo">SQLShowInfo</a></h2>
+<h3><a name="SQLShowInfo">SQLShowInfo</a></h3>
 <strong>Syntax:</strong> SQLShowInfo <em>cmd-set numeric query-string</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1245,7 +1275,7 @@ been accepted and the session has started.
 
 <p>
 <hr>
-<h2><a name="SQLUserInfo">SQLUserInfo</a></h2>
+<h3><a name="SQLUserInfo">SQLUserInfo</a></h3>
 <strong>Syntax:</strong> SQLUserInfo <em>user-table user-name passwd uid gid home-dir shell</em><br>
 <strong>Default:</strong> "users userid passwd uid gid homedir shell"<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1382,10 +1412,10 @@ work as expected.
 
 <p>
 <hr>
-<h2><a name="SQLUserPrimaryKey">SQLUserPrimaryKey</a></h2>
+<h3><a name="SQLUserPrimaryKey">SQLUserPrimaryKey</a></h3>
 <strong>Syntax:</strong> SQLUserPrimaryKey <em>column | "custom:/"named-query</em><br>
 <strong>Default:</strong> uid<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql<br>
 <strong>Compatibility:</strong> 1.3.5rc3 and later
 
@@ -1401,7 +1431,7 @@ See also: <a href="#SQLGroupPrimaryKey"><code>SQLGroupPrimaryKey</code></a>
 
 <p>
 <hr>
-<h2><a name="SQLUserWhereClause">SQLUserWhereClause</a></h2>
+<h3><a name="SQLUserWhereClause">SQLUserWhereClause</a></h3>
 <strong>Syntax:</strong> SQLUserWhereClause <em>where-clause </em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1440,9 +1470,9 @@ parameter can use the same set of variables as supported by the
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_sql</code> module is distributed with ProFTPD.  Simply
-follow the normal steps for using third-party modules in proftpd:
+follow the normal steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=<i>sql-module-opts</i>
+  $ ./configure --with-modules=<i>sql-module-opts</i>
 </pre>
 where the specific <i>sql-module-opts</i> depend on your database needs.  For
 example, if using MySQL, <i>sql-module-opts</i> would be
@@ -1456,7 +1486,7 @@ database of choice, <i>sql-module-opts</i> would be
 You will also need to tell <code>configure</code> how to find the
 database-specific libraries and header files:
 <pre>
-  ./configure --with-modules=<i>sql-module-opts</i> \
+  $ ./configure --with-modules=<i>sql-module-opts</i> \
     --with-includes=<i>/path/to/db/header/file/dir</i> \
     --with-libraries=<i>/path/to/db/library/file/dir</i>
 </pre>
@@ -1464,25 +1494,32 @@ database-specific libraries and header files:
 <p>
 Complete the build with the following standard commands:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-21 22:32:31 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_sql</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>sql
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace sql:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2014 The ProFTPD Project<br>
+© Copyright 2000-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sql_odbc.html b/doc/contrib/mod_sql_odbc.html
index 219fe88..3815e3a 100644
--- a/doc/contrib/mod_sql_odbc.html
+++ b/doc/contrib/mod_sql_odbc.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sql_odbc.html,v 1.4 2011-04-19 22:23:12 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sql_odbc.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sql_odbc</title>
@@ -46,6 +44,33 @@ ProFTPD.
 Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
 questions, concerns, or suggestions regarding this module.
 
+<h2>Directives</h2>
+<ul>
+  <li><a href="#SQLODBCVersion">SQLODBCVersion</a>
+</ul>
+
+<hr>
+<h3><a name="SQLODBCVersion">SQLODBCVersion</a></h3>
+<strong>Syntax:</strong> SQLODBCVersion <em>version</em><br>
+<strong>Default:</strong> SQLODBCVersion ODBCv3<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_sql_odbc<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>SQLODBCVersion</code> directive configures the ODBC API
+<em>version</em> that the ODBC driver should use/expect.  The default is
+"ODBCv3", <i>i.e.</i> ODBC version 3.  Some drivers may have issues with the
+default version; this can manifest as errors similar to the following in your
+<code>SQLLogFile</code>:
+<pre>
+  message: '[unixODBC][Driver Manager]Driver does not support the requested version'
+</pre>
+When this happens, you might try using an older version of ODBC, via:
+<pre>
+  SQLODBCVersion ODBCv2
+</pre>
+
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
@@ -59,7 +84,7 @@ One of these ODBC libraries must be installed prior to using
 <code>mod_sql_odbc</code>.
 
 <p>
-Follow the usual steps for using contrib modules in proftpd, making sure to
+Follow the usual steps for using contrib modules in ProFTPD, making sure to
 list <code>mod_sql</code>.  Note that you will need to use the
 <code>LIBS</code> environment variable for indicating which ODBC library
 to link against.
@@ -67,23 +92,23 @@ to link against.
 <p>
 For example, if you wish to use the unixODBC library, you would use:
 <pre>
-  ./configure LIBS=-lodbc --with-modules=mod_sql:mod_sql_odbc ...
-  make
-  make install
+  $ ./configure LIBS=-lodbc --with-modules=mod_sql:mod_sql_odbc ...
+  $ make
+  $ make install
 </pre>
 On the other hand, for using the iODBC library, the invocation is slightly
 different, specifying a different library name:
 <pre>
-  ./configure LIBS=-liodbc --with-modules=mod_sql:mod_sql_odbc ...
-  make
-  make install
+  $ ./configure LIBS=-liodbc --with-modules=mod_sql:mod_sql_odbc ...
+  $ make
+  $ make install
 </pre>
 
 <p>
 You may need to specify the location of the ODBC header and library files in
 your <code>configure</code> command, <i>e.g.</i>:
 <pre>
-  ./configure \
+  $ ./configure \
     LD_LIBRARY_PATH=/usr/local/odbc/lib \
     LDFLAGS=-L/usr/local/odbc/lib \
     LIBS=-lodbc \
@@ -140,7 +165,7 @@ MySQL and Postgres databases:
 
   <p>
   <dt>Postgres ODBC Driver (psqlODBC)</dt>
-  <dd><a href="http://gborg.postgresql.org/project/psqlodbc/">http://gborg.postgresql.org/project/psqlodbc/</a></dd>
+  <dd><a href="https://odbc.postgresql.org/">https://odbc.postgresql.org/</a></dd>
 </dl>
 
 <p>
@@ -246,31 +271,23 @@ configuration directives to set the environment variables directly in
 the <code>proftpd.conf</code> file, which makes configuration more
 centralized.
 
+<p>
 Example configuration using <code>SetEnv</code>:
 <pre>
   <IfModule mod_sql_odbc.c>
     SetEnv LD_LIBRARY_PATH <i>/path/to/odbc/lib</i>
-    SetEnv ODBCINI <i>/path/to/</i><code>odbc.ini</i>
+    SetEnv ODBCINI <i>/path/to/</i><code>odbc.ini</code>
   </IfModule>
 </pre>
-
 Please contact the author directly with any questions or comments.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2011-04-19 22:23:12 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2003-2011 TJ Saunders<br>
+© Copyright 2003-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sql_passwd.html b/doc/contrib/mod_sql_passwd.html
index 66dceec..7f0e862 100644
--- a/doc/contrib/mod_sql_passwd.html
+++ b/doc/contrib/mod_sql_passwd.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sql_passwd.html,v 1.14 2014-05-04 23:15:00 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sql_passwd.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sql_passwd</title>
@@ -26,8 +24,17 @@ or hex-encoded, <i>without</i> the prefix which is required by
 <p>
 The <code>mod_sql_passwd</code> module provides support for some of these
 other formats.  When the <code>mod_sql_passwd</code> module is enabled,
-you can configure <code>SQLAuthTypes</code> of "MD5", "SHA1", "SHA256", or
-"SHA512", as well as the existing types supported by <code>mod_sql</code>.
+you can configure <code>SQLAuthTypes</code> of:
+<ul>
+  <li>Argon2
+  <li>MD5
+  <li>PBKDF2
+  <li>Scrypt
+  <li>SHA1
+  <li>SHA256
+  <li>SHA512
+</ul>
+as well as the existing types supported by <code>mod_sql</code>.
 
 <p>
 This module is contained in the <code>mod_sql_passwd.c</code> file for
@@ -54,20 +61,79 @@ questions, concerns, or suggestions regarding this module.
 
 <h2>Directives</h2>
 <ul>
+  <li><a href="#SQLPasswordArgon2">SQLPasswordArgon2</a>
+  <li><a href="#SQLPasswordCost">SQLPasswordCost</a>
   <li><a href="#SQLPasswordEncoding">SQLPasswordEncoding</a>
   <li><a href="#SQLPasswordEngine">SQLPasswordEngine</a>
   <li><a href="#SQLPasswordOptions">SQLPasswordOptions</a>
   <li><a href="#SQLPasswordPBKDF2">SQLPasswordPBKDF2</a>
   <li><a href="#SQLPasswordRounds">SQLPasswordRounds</a>
+  <li><a href="#SQLPasswordSaltEncoding">SQLPasswordSaltEncoding</a>
   <li><a href="#SQLPasswordSaltFile">SQLPasswordSaltFile</a>
+  <li><a href="#SQLPasswordScrypt">SQLPasswordScrypt</a>
   <li><a href="#SQLPasswordUserSalt">SQLPasswordUserSalt</a>
 </ul>
 
 <hr>
-<h2><a name="SQLPasswordEncoding">SQLPasswordEncoding</a></h2>
+<h3><a name="SQLPasswordArgon2">SQLPasswordArgon2</a></h3>
+<strong>Syntax:</strong> SQLPasswordArgon2 <em>length</em><br>
+<strong>Default:</strong> <em>32</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_sql_passwd<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>SQLPasswordArgon2</code> directive configures the <em>length</em>
+of the calculated Argon2 output hash <em>in bytes</em>.  The default length
+is 32 bytes.
+
+<hr>
+<h3><a name="SQLPasswordCost">SQLPasswordCost</a></h3>
+<strong>Syntax:</strong> SQLPasswordCost <em>interactive|sensitive</em><br>
+<strong>Default:</strong> <em>interactive</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_sql_passwd<br>
+<strong>Compatibility:</strong> 1.3.6rc3 and later
+
+<p>
+The <code>SQLPasswordCost</code> directive configures the high-level
+<em>cost</em> settings to use for memory-hard algorithms like
+<code>scrypt</code> and <code>argon2</code>.  The supported <em>cost</em>
+cost values are:
+<ul>
+  <li>interactive
+    <p>
+    This <em>cost</em> uses parameters where generating the value is part of
+    an "interactive" session.
+
+    <p>
+    For <code>scrypt</code>, depending on the version of <code>libsodium</code>
+    used, this <em>cost</em> uses:
+<pre>
+    N=16384, r=8, p=1
+</pre>
+  </li>
+
+  <p>
+  <li>sensitive
+    <p>
+    This <em>cost</em> uses parameters where the value generated is considered
+    very "sensitive".
+
+    <p>
+    For <code>scrypt</code>, depending on the version of <code>libsodium</code>
+    used, this <em>cost</em> uses:
+<pre>
+    N=1048576, r=8, p=1
+</pre>
+  </li>
+</ul>
+
+<hr>
+<h3><a name="SQLPasswordEncoding">SQLPasswordEncoding</a></h3>
 <strong>Syntax:</strong> SQLPasswordEncoding <em>encoding</em><br>
 <strong>Default:</strong> <em>hex</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><em></em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.3rc2 and later
 
@@ -89,10 +155,10 @@ If no <code>SQLPasswordEncoding</code> directive is configured,
 <code>mod_sql_passwd</code> will use "hex" by default.
 
 <hr>
-<h2><a name="SQLPasswordEngine">SQLPasswordEngine</a></h2>
+<h3><a name="SQLPasswordEngine">SQLPasswordEngine</a></h3>
 <strong>Syntax:</strong> SQLPasswordEngine <em>on|off</em><br>
 <strong>Default:</strong> <em>off</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.3rc2 and later
 
@@ -101,10 +167,10 @@ The <code>SQLPasswordEngine</code> directive enables or disables the module's
 registered <code>SQLAuthType</code> handlers.
 
 <hr>
-<h2><a name="SQLPasswordOptions">SQLPasswordOptions</a></h2>
+<h3><a name="SQLPasswordOptions">SQLPasswordOptions</a></h3>
 <strong>Syntax:</strong> SQLPasswordOptions <em>opts</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><em></em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -136,10 +202,10 @@ description of how <code>mod_sql_passwd</code> operates on the password and
 salt data.
 
 <hr>
-<h2><a name="SQLPasswordPBKDF2">SQLPasswordPBKDF2</a></h2>
+<h3><a name="SQLPasswordPBKDF2">SQLPasswordPBKDF2</a></h3>
 <strong>Syntax:</strong> SQLPasswordPBKDF2 <em>digest</em> <em>iterations</em> <em>length</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><em></em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.5rc3 and later
 
@@ -169,9 +235,9 @@ Example:
   SQLAuthTypes pbkdf2
   ...
 
-  # Use the SHA1 digest algorithm, 1000 iterations, and expect an output
+  # Use the SHA1 digest algorithm, 200K iterations, and expect an output
   # length of 20 bytes.
-  SQLPasswordPBKDF2 sha1 1000 20
+  SQLPasswordPBKDF2 sha1 200000 20
 
   SQLPasswordSaltFile /path/to/salt/file
 </pre>
@@ -186,16 +252,15 @@ As of <code>proftpd-1.3.5</code>, the <code>SQLPasswordPBKDF2</code> directive
 can instead take a named query, for determining the digest algorithm,
 iteration count, and output length <i>on a per-user basis</i>.  For example:
 <pre>
-  SQLNamedQuery get-user-pbkdf2 SELECT "algo, iter, len FROM user_pbkdf2 WHERE
-user = '%{0}'
-  SQLPasswordPBKDF2 sql://get-user-pbkdf2
+  SQLNamedQuery get-user-pbkdf2 SELECT "algo, iter, len FROM user_pbkdf2 WHERE user = '%{0}'
+  SQLPasswordPBKDF2 sql:/get-user-pbkdf2
 </pre>
 
 <hr>
-<h2><a name="SQLPasswordRounds">SQLPasswordRounds</a></h2>
+<h3><a name="SQLPasswordRounds">SQLPasswordRounds</a></h3>
 <strong>Syntax:</strong> SQLPasswordRounds <em>count</em><br>
 <strong>Default:</strong> <em>1</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -210,10 +275,36 @@ description of how <code>mod_sql_passwd</code> operates on the password and
 salt data.
 
 <hr>
-<h2><a name="SQLPasswordSaltFile">SQLPasswordSaltFile</a></h2>
+<h3><a name="SQLPasswordSaltEncoding">SQLPasswordSaltEncoding</a></h3>
+<strong>Syntax:</strong> SQLPasswordSaltEncoding <em>encoding</em><br>
+<strong>Default:</strong> <em>none</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_sql_passwd<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>SQLPasswordSaltEncoding</code> directive configures the encoding that
+<code>mod_sql_passwd</code> expects when handling <b><i>salt</i></b> values
+retrieved either from a SQL database, <i>or</i> from a file.
+
+<p>
+The following <em>encoding</em> values are currently supported:
+<ul>
+  <li>base64
+  <li>hex (<i>for lowercase hex values</i>)
+  <li>HEX (<i>for uppercase hex values</i>)
+  <li>none (use salt value as is)
+</ul>
+
+<p>
+If no <code>SQLPasswordSaltEncoding</code> directive is configured,
+<code>mod_sql_passwd</code> will use "none" by default.
+
+<hr>
+<h3><a name="SQLPasswordSaltFile">SQLPasswordSaltFile</a></h3>
 <strong>Syntax:</strong> SQLPasswordSaltFile <em>path|"none" ["Prepend"|"Append"]</em><br>
 <strong>Default:</strong> <em>none</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.3rc2 and later
 
@@ -248,10 +339,23 @@ behavior is to <i>append</i> the salt as a suffix.
 If no <code>SQLPasswordSaltFile</code> is configured, then no salting is done.
 
 <hr>
-<h2><a name="SQLPasswordUserSalt">SQLPasswordUserSalt</a></h2>
+<h3><a name="SQLPasswordScrypt">SQLPasswordScrypt</a></h3>
+<strong>Syntax:</strong> SQLPasswordScrypt <em>length</em><br>
+<strong>Default:</strong> <em>32</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_sql_passwd<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>SQLPasswordScrypt</code> directive configures the <em>length</em>
+of the calculated Scrypt output hash <em>in bytes</em>.  The default length
+is 32 bytes.
+
+<hr>
+<h3><a name="SQLPasswordUserSalt">SQLPasswordUserSalt</a></h3>
 <strong>Syntax:</strong> SQLPasswordUserSalt <em>"name"|source ["Prepend"|"Append"]</em><br>
 <strong>Default:</strong> <em>none</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_sql_passwd<br>
 <strong>Compatibility:</strong> 1.3.3 and later
 
@@ -277,10 +381,12 @@ optional second parameter to <code>SQLPasswordUserSalt</code> controls how
 this module will use the salt:
 <pre>
   SQLPasswordUserSalt name Prepend
+  SQLPasswordUserSalt sql:/get-user-salt Prepend
 </pre>
 tells <code>mod_sql_passwd</code> to prepend the salt as a prefix, and:
 <pre>
   SQLPasswordUserSalt name Append
+  SQLPasswordUserSalt sql:/get-user-salt Append
 </pre>
 will cause the salt to be appended as a sufix.  <b>Note</b> that the default
 behavior is to <i>append</i> the salt as a suffix.
@@ -290,21 +396,26 @@ behavior is to <i>append</i> the salt as a suffix.
 The <code>mod_sql_passwd</code> module is distributed with ProFTPD.  Simply
 follow the normal steps for using third-party modules in proftpd.  The
 <code>mod_sql_passwd</code> module requires OpenSSL support, so you <b>must</b>
-use the <code>--enable-openssl</code> configuration option.
+use the <code>--enable-openssl</code> configuration option.  In addition,
+if you have the <code>libsodium</code> library installed, simply include the
+<code>libsodium</code> headers/libraries in the build command to enable
+additional algorithms.
+
+<p>
 <b>NOTE</b>: it is <b>important</b> that <code>mod_sql_passwd</code> appear
 <i>after</i> <code>mod_sql</code> in your <code>--with-modules</code> configure
 option:
 <pre>
-  ./configure --enable-openssl --with-modules=mod_sql:mod_sql_passwd ...
+  $ ./configure --enable-openssl --with-modules=mod_sql:mod_sql_passwd ...
 </pre>
 To build <code>mod_sql_passwd</code> as a DSO module:
 <pre>
-  ./configure --enable-dso --enable-openssl --with-shared=mod_sql_passwd
+  $ ./configure --enable-dso --enable-openssl --with-shared=mod_sql_passwd
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
@@ -312,7 +423,7 @@ For those with an existing ProFTPD installation, you can use the
 <code>prxs</code> tool to add <code>mod_sql_passwd</code>, as a DSO module, to
 your existing server:
 <pre>
-  # prxs -c -i -d mod_sql_passwd.c
+  $ prxs -c -i -d mod_sql_passwd.c
 </pre>
 
 <hr>
@@ -371,6 +482,52 @@ the <code>mod_sql_passwd</code> module to use it:
   </IfModule>
 </pre>
 
+<p><a name="SQLPasswordSodium">
+<b>Argon2, Scrypt</b><br>
+When <code>mod_sql_passwd</code> is compiled/linked with the <a href="https://github.com/jedisct1/libsodium"><code>libsodium</code></a> library, then the
+Argon2 and Scrypt algorithms become available for use:
+<pre>
+  <IfModule mod_sql_passwd.c>
+    SQLPasswordEngine on
+    SQLPasswordEncoding hex
+    SQLPasswordSaltFile /path/to/salt
+  </IfModule>
+
+  <IfModule mod_sql.c>
+    ...
+
+    # Now that mod_sql_passwd is used, we can configure "SCRYPT" as an
+    # SQLAuthType that mod_sql will handle.
+    SQLAuthTypes SCRYPT
+  </IfModule>
+</pre>
+
+<p>
+The <code>scrypt</code> algorithm <b>requires</b> 32 bytes of salt data;
+lack of salt, or salt of the wrong amount, will result in authentication
+failure.  The <code>argon2</code> algorithm <b>requires</b> 16 bytes of salt
+data; lack of salt or the wrong amount will result in failure.
+
+<p>
+The <code>argon2</code> algorithm requires <code>libsodium-1.0.9</code> or
+later.
+
+<p>
+<b>Logging</b><br>
+The <code>mod_sql_passwd</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log
+channels:
+<ul>
+  <li>sql.passwd
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace sql.passwd:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="Transformations">
 <b>Processing of Password and Salt Data</b><br>
 The logical description of the processing that <code>mod_sql_passwd</code>
@@ -466,7 +623,7 @@ Let's look at each of the <code>SQLPasswordOptions</code> in turn:
     <code>HASH()</code> function on the salt data before using it.
     If no salt is used, this option is silently ignored.  Thus:
 <pre>
-  <i>data</i> = HASH(<i>salt</i>) + </i>password</i>
+  <i>data</i> = HASH(<i>salt</i>) + <i>password</i>
 </pre>
     which assuming hex and MD5, means:
 <pre>
@@ -541,8 +698,9 @@ of the <code>TRANSFORM</code> function, <i>e.g.</i>:
 </pre>
 In this case, there are 3 rounds of transformation:
 <pre>
-  for (<i>i</i> = 0; <i>i</i> < <i>nrounds</i>; <i>i</i>++)
+  for (<i>i</i> = 0; <i>i</i> < <i>nrounds</i>; <i>i</i>++) {
     <i>data</i> = TRANSFORM(<i>data</i>)
+  }
 </pre>
 
 <p>
@@ -560,20 +718,12 @@ The combination of <code>SQLPasswordOptions</code> and
 values can be supported by the <code>mod_sql_passwd</code> module.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-05-04 23:15:00 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2009-2014 TJ Saunders<br>
+© Copyright 2009-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_sql_sqlite.html b/doc/contrib/mod_sql_sqlite.html
index 6bf9d6b..9bb4c11 100644
--- a/doc/contrib/mod_sql_sqlite.html
+++ b/doc/contrib/mod_sql_sqlite.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_sql_sqlite.html,v 1.4 2009-06-29 17:10:22 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_sql_sqlite.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_sql_sqlite</title>
@@ -44,26 +42,26 @@ The <code>mod_sql_sqlite</code> modules requires that SQLite be installed.
 
 <p>
 After installing SQLite, follow the usual steps for using contrib modules in
-proftpd, making sure to list <code>mod_sql</code>:
+ProFTPD, making sure to list <code>mod_sql</code>:
 <pre>
-  ./configure --with-modules=mod_sql:mod_sql_sqlite
-  make
-  make install
+  $ ./configure --with-modules=mod_sql:mod_sql_sqlite
+  $ make
+  $ make install
 </pre>
 You may need to specify the location of the SQLite header and library files in
 your <code>configure</code> command, <i>e.g.</i>:
 <pre>
-  ./configure --with-modules=mod_sql:mod_sql_sqlite \
+  $ ./configure --with-modules=mod_sql:mod_sql_sqlite \
     --with-includes=/usr/local/sqlite/include \
     --with-libraries=/usr/local/sqlite/lib
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_sql_sqlite</code> as
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_sql_sqlite</code> as
 a shared module:
 <pre>
-  prxs -c -i -d mod_sql_sqlite.c
+  $ prxs -c -i -d mod_sql_sqlite.c
 </pre>
 
 <p>
@@ -84,7 +82,7 @@ For example:
 </pre>
 
 <p>
-Note that due to the way that <code>mod_sql_sqlite<code> implements database
+Note that due to the way that <code>mod_sql_sqlite</code> implements database
 transactions (e.g. <code>INSERT</code> and <code>UPDATE</code> SQL statements
 for <code>SQLLog</code> directives), SQLite-3.6.5 or later is <b>required</b>
 in order for <code>mod_sql_sqlite</code> to support <code>SQLLog</code>
@@ -94,20 +92,12 @@ needed for supporting the <code>SQLLog</code> directives in a chrooted
 process.)
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2009-06-29 17:10:22 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2004-2009 TJ Saunders<br>
+© Copyright 2004-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_statcache.html b/doc/contrib/mod_statcache.html
new file mode 100644
index 0000000..09f3d3f
--- /dev/null
+++ b/doc/contrib/mod_statcache.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_statcache</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_statcache</code></b></h2>
+</center>
+<hr><br>
+
+<p>
+The <code>mod_statcache</code> module is designed to cache the results
+of <code>stat(2)</code> and <code>lstat(2)</code> calls in shared memory,
+so that the results can be shared among multiple session processes.  On
+a busy server, especially one handling many directory listings, these
+<code>stat(2)</code>/<code>lstat(2)</code> calls can add quite a bit of
+overhead.
+
+<p>
+This module is contained in the <code>mod_statcache.c</code> file for
+ProFTPD 1.3.<i>x</i>, and is not compiled by default.  Installation
+instructions are discussed <a href="#Installation">here</a>.  More examples
+of <code>mod_statcache</code> usage can be found <a href="#Usage">here</a>.
+
+<p>
+The most current version of <code>mod_statcache</code> is distributed with the
+ProFTPD source code.
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
+questions, concerns, or suggestions regarding this module.
+
+<h2>Directives</h2>
+<ul>
+  <li><a href="#StatCacheCapacity">StatCacheCapacity</a>
+  <li><a href="#StatCacheControlsACLs">StatCacheControlsACLs</a>
+  <li><a href="#StatCacheEngine">StatCacheEngine</a>
+  <li><a href="#StatCacheMaxAge">StatCacheMaxAge</a>
+  <li><a href="#StatCacheTable">StatCacheTable</a>
+</ul>
+
+<h2>Control Actions</h2>
+<ul>
+  <li><a href="#statcache"><code>statcache</code></a>
+</ul>
+
+<p>
+<hr>
+<h3><a name="StatCacheCapacity">StatCacheCapacity</a></h3>
+<strong>Syntax:</strong> StatCacheCapacity <em>count</em><br>
+<strong>Default:</strong> <em>StatCacheCapacity 5000</em><br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_statcache<br>
+<strong>Compatibility:</strong> 1.3.4rc2 and later
+
+<p>
+The <code>StatCacheCapacity</code> directive configures the <i>capacity</i>
+of the cache, <i>i.e.</i> the maximum number of cache entries.  By default,
+<code>mod_statcache</code> allocates space for 5000 cache entries.
+
+<p>
+The <em>count</em> value be 10 or greater.  The configured <em>count</em>
+is handled as a <i>hint</i>; the actual allocated capacity may be rounded up
+to the nearest multiple of the internal block sizes.
+
+<p>
+<hr>
+<h3><a name="StatCacheControlsACLs">StatCacheControlsACLs</a></h3>
+<strong>Syntax:</strong> StatCacheControlsACLs <em>actions|"all" "allow"|"deny" "user"|"group" list</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_statcache<br>
+<strong>Compatibility:</strong> 1.3.4rc2 and later
+
+<p>
+The <code>StatCacheControlsACLs</code> directive configures access lists of
+<em>users</em> or <em>groups</em> who are allowed (or denied) the ability to
+use the <em>actions</em> implemented by <code>mod_statcache</code>. The default
+behavior is to deny everyone unless an ACL allowing access has been explicitly
+configured.
+
+<p>
+If "allow" is used, then <em>list</em>, a comma-delimited list
+of <em>users</em> or <em>groups</em>, can use the given <em>actions</em>; all
+others are denied.  If "deny" is used, then the <em>list</em> of
+<em>users</em> or <em>groups</em> cannot use <em>actions</em> all others are
+allowed.  Multiple <code>StatCacheControlsACLs</code> directives may be used to
+configure ACLs for different control actions, and for both users and groups.
+
+<p>
+The <em>action</em> provided by <code>mod_statcache</code> is
+<a href="#statcache">"statcache"</a>.
+
+<p>
+Examples:
+<pre>
+  # Allow only user root to examine cache stats
+  StatCacheControlsACLs all allow user root
+</pre>
+
+<p>
+<hr>
+<h3><a name="StatCacheEngine">StatCacheEngine</a></h3>
+<strong>Syntax:</strong> StatCacheEngine <em>on|off</em><br>
+<strong>Default:</strong> <em>StatCacheEngine off</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_statcache<br>
+<strong>Compatibility:</strong> 1.3.4rc2 and later
+
+<p>
+The <code>StatCacheEngine</code> directive enables or disables the module's
+caching of <code>stat(2)</code> and <code>lstat(2)</code> calls.
+
+<p>
+<hr>
+<h3><a name="StatCacheMaxAge">StatCacheMaxAge</a></h3>
+<strong>Syntax:</strong> StatCacheMaxAge <em>positive-cache-age [negative-cache-age]</em><br>
+<strong>Default:</strong> <em>StatCacheMaxAge 5</em><br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_statcache<br>
+<strong>Compatibility:</strong> 1.3.4rc2 and later
+
+<p>
+The <code>StatCacheMaxAge</code> directive configures how long the
+<code>mod_statcache</code> module will keep the <code>stat(2)</code> or
+<code>lstat(2)</code> results cached for a given path.  By default,
+<code>mod_statcache</code> will cache results for 5 seconds (and will
+cache <i>failed</i> <code>stat(2)</code>/<code>lstat(2)</code> calls for
+1 second).
+
+<p>
+If you configure a single age parameter, then that value will be used for
+both positive and negative cache entries.  Thus:
+<pre>
+  StatCacheMaxAge 300
+</pre>
+will cache successful <em>and</em> failed
+<code>stat(2)</code>/<code>lstat(2)</code> calls for 5 minutes (300 seconds).
+
+<p>
+To disable caching of failed <code>stat(2)</code>/<code>lstat(2)</code> calls
+entirely, use a <em>negative-cache-age</em> value of zero, <i>e.g.</i>:
+<pre>
+  # Cache lookups for 60 seconds, and do not cache failed lookups
+  StatCacheMaxAge 60 0
+</pre>
+
+<p>
+<hr>
+<h3><a name="StatCacheTable">StatCacheTable</a></h3>
+<strong>Syntax:</strong> StatCacheTable <em>path</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_statcache<br>
+<strong>Compatibility:</strong> 1.3.4rc2 and later
+
+<p>
+The <code>StatCacheTable</code> directive configures a <em>path</em> to a file
+that <code>mod_statcache</code> uses for handling its cache data.  The given
+<em>path</em> must be an absolute path.  <b>Note</b>: this directive is
+<b>required</b> for <code>mod_statcache</code> to function.  It is recommended
+that this file <b>not</b> be on an NFS mounted partition.
+
+<p>
+Note that statcache data <b>is not</b> kept across daemon stop/starts.  That is,
+once <code>proftpd</code> is shutdown, all current statcache data is lost.
+
+<p>
+<hr>
+<h2>Control Actions</h2>
+
+<p>
+<hr>
+<h3><a name="statcache"><code>statcache</code></a></h3>
+<strong>Syntax:</strong> ftpdctl statcache <em>info|dump</em><br>
+<strong>Purpose:</strong> Display statcache information<br>
+
+<p>
+The <code>statcache</code> action is used to display cache statistics about
+the statcache data maintained by <code>mod_statcache</code>.  For example:
+<pre>
+  # ftpdctl statcache info
+  ftpdctl:  hits 773, misses 67: 92.0% hit rate
+  ftpdctl:    expires 22, rejects 0
+  ftpdctl:  current count: 1 (of 5000) (0.0% usage)
+  ftpdctl:  highest count: 45 (of 5000) (0.9% usage)
+</pre>
+To dump out the entire cache contents (not recommended on a busy server),
+you can use:
+<pre>
+  # ftpdctl statcache dump
+</pre>
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_statcache</code> module is distributed with ProFTPD.  For
+including <code>mod_statcache</code> as a staticly linked module, use:
+<pre>
+  $ ./configure --with-modules=mod_statcache
+</pre>
+To build <code>mod_statcache</code> as a DSO module:
+<pre>
+  $ ./configure --enable-dso --with-shared=mod_statcache
+</pre>
+Then follow the usual steps:
+<pre>
+  $ make
+  $ make install
+</pre>
+
+<p>
+For those with an existing ProFTPD installation, you can use the
+<code>prxs</code> tool to add <code>mod_statcache</code>, as a DSO module, to
+your existing server:
+<pre>
+  $ prxs -c -i -d mod_statcache.c
+</pre>
+
+<p>
+<b>Note</b>: Use of the <code>mod_statcache</code> module <b>will</b>
+interfere with the <code>mod_vroot</code> and <code>mod_quotatab</code>
+modules.
+
+<p>
+<hr>
+<h2><a name="Usage">Usage</a></h2>
+
+<p>
+The <code>mod_statcache</code> module works by allocating a SysV shared
+memory segment.  The different <code>proftpd</code> session processes
+then attach to that shared memory segment so that they can share statcache
+results.
+
+<p>
+Example configuration:
+<pre>
+  <IfModule mod_statcache.c>
+    StatCacheEngine on
+    StatCacheTable /var/run/proftpd/statcache.tab
+  </IfModule>
+</pre>
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2013-2017 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/contrib/mod_tls.html b/doc/contrib/mod_tls.html
index 0abfb51..f533799 100644
--- a/doc/contrib/mod_tls.html
+++ b/doc/contrib/mod_tls.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_tls.html,v 1.43 2013-12-09 23:15:16 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_tls.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_tls</title>
@@ -14,6 +12,10 @@
 </center>
 <hr><br>
 
+<p>
+The <code>mod_tls</code> module implements FTP over SSL/TLS, known as FTPS.
+
+<p>
 This module is contained in the <code>mod_tls.c</code> file for ProFTPD
 1.3.<i>x</i>, and is not compiled by default.  Installation
 instructions are discussed <a href="#Installation">here</a>.
@@ -57,12 +59,15 @@ is based.  His module can be found here:
   <li><a href="#TLSDSACertificateKeyFile">TLSDSACertificateKeyFile</a>
   <li><a href="#TLSECCertificateFile">TLSECACertificateFile</a>
   <li><a href="#TLSECCertificateKeyFile">TLSECCertificateKeyFile</a>
+  <li><a href="#TLSECDHCurve">TLSECDHCurve</a>
   <li><a href="#TLSEngine">TLSEngine</a>
   <li><a href="#TLSLog">TLSLog</a>
   <li><a href="#TLSMasqueradeAddress">TLSMasqueradeAddress</a>
+  <li><a href="#TLSNextProtocol">TLSNextProtocol</a>
   <li><a href="#TLSOptions">TLSOptions</a>
   <li><a href="#TLSPKCS12File">TLSPKCS12File</a>
   <li><a href="#TLSPassPhraseProvider">TLSPassPhraseProvider</a>
+  <li><a href="#TLSPreSharedKey">TLSPreSharedKey</a>
   <li><a href="#TLSProtocol">TLSProtocol</a>
   <li><a href="#TLSRandomSeed">TLSRandomSeed</a>
   <li><a href="#TLSRenegotiate">TLSRenegotiate</a>
@@ -70,7 +75,15 @@ is based.  His module can be found here:
   <li><a href="#TLSRSACertificateFile">TLSRSACertificateFile</a>
   <li><a href="#TLSRSACertificateKeyFile">TLSRSACertificateKeyFile</a>
   <li><a href="#TLSServerCipherPreference">TLSServerCipherPreference</a>
+  <li><a href="#TLSServerInfoFile">TLSServerInfoFile</a>
   <li><a href="#TLSSessionCache">TLSSessionCache</a>
+  <li><a href="#TLSSessionTicketKeys">TLSSessionTicketKeys</a>
+  <li><a href="#TLSSessionTickets">TLSSessionTickets</a>
+  <li><a href="#TLSStapling">TLSStapling</a>
+  <li><a href="#TLSStaplingCache">TLSStaplingCache</a>
+  <li><a href="#TLSStaplingOptions">TLSStaplingOptions</a>
+  <li><a href="#TLSStaplingResponder">TLSStaplingResponder</a>
+  <li><a href="#TLSStaplingTimeout">TLSStaplingTimeout</a>
   <li><a href="#TLSTimeoutHandshake">TLSTimeoutHandshake</a>
   <li><a href="#TLSUserName">TLSUserName</a>
   <li><a href="#TLSVerifyClient">TLSVerifyClient</a>
@@ -87,7 +100,7 @@ is based.  His module can be found here:
 </ul>
 
 <hr>
-<h2><a name="TLSCACertificateFile">TLSCACertificateFile</a></h2>
+<h3><a name="TLSCACertificateFile">TLSCACertificateFile</a></h3>
 <strong>Syntax:</strong> TLSCACertificateFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -127,7 +140,7 @@ See also: <a href="#TLSCACertificatePath"><code>TLSCACertificatePath</code></a>
 
 <p>
 <hr>
-<h2><a name="TLSCACertificatePath">TLSCACertificatePath</a></h2>
+<h3><a name="TLSCACertificatePath">TLSCACertificatePath</a></h3>
 <strong>Syntax:</strong> TLSCACertificatePath <em>directory</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -169,7 +182,7 @@ See also: <a href="#TLSCACertificateFile"><code>TLSCACertificateFile</code></a>
 
 <p>
 <hr>
-<h2><a name="TLSCARevocationFile">TLSCARevocationFile</a></h2>
+<h3><a name="TLSCARevocationFile">TLSCARevocationFile</a></h3>
 <strong>Syntax:</strong> TLSCARevocationFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -195,7 +208,7 @@ See also: <a href="#TLSCARevocationPath"><code>TLSCARevocationPath</code></a>
 
 <p>
 <hr>
-<h2><a name="TLSCARevocationPath">TLSCARevocationPath</a></h2>
+<h3><a name="TLSCARevocationPath">TLSCARevocationPath</a></h3>
 <strong>Syntax:</strong> TLSCARevocationPath <em>directory</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -227,7 +240,7 @@ See also: <a href="#TLSCARevocationFile"><code>TLSCARevocationFile</code></a>
 
 <p>
 <hr>
-<h2><a name="TLSCertificateChainFile">TLSCertificateChainFile</a></h2>
+<h3><a name="TLSCertificateChainFile">TLSCertificateChainFile</a></h3>
 <strong>Syntax:</strong> TLSCertificateChainFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -273,23 +286,16 @@ Example:
 </pre>
 
 <p>
-<b>Note</b>: If you use the <code>NoCertRequest</code>
-<a href="#TLSOptions"><code>TLSOption</code></a>, then any configured
-<code>TLSCertificateChainFile</code> directive will be ignored.  It is a waste
-of time to construct a certificate chain to send to the client if the server
-does not request that the client send a certificate to be verified.
-
-<p>
 <hr>
-<h2><a name="TLSCipherSuite">TLSCipherSuite</a></h2>
+<h3><a name="TLSCipherSuite">TLSCipherSuite</a></h3>
 <strong>Syntax:</strong> TLSCipherSuite <em>cipher-list</em><br>
-<strong>Default:</strong> DEFAULT:!EXPORT:!DES<br>
+<strong>Default:</strong> DEFAULT:!ADH:!EXPORT:!DES<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_tls<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
 <p>
-Default cipher list is "DEFAULT:!EXPORT:!DES".
+Default cipher list is "DEFAULT:!ADH:!EXPORT:!DES".
 
 <p>
 How to put together a <em>cipher list</em> parameter:
@@ -317,12 +323,17 @@ How to put together a <em>cipher list</em> parameter:
   MAC Digest Algorithm:
     "MD5"       MD5 hash function
     "SHA1"      SHA1 hash function
+    "SHA256"    SHA-256 hash function
+    "SHA384"    SHA-384 hash function
+    "SHA512"    SHA-512 hash function
     "SHA"       SHA hash function (should not be used)
 
   Aliases:
     "ALL"       all ciphers
     "SSLv2"     all SSL version 2.0 ciphers (should not be used)
     "SSLv3"     all SSL version 3.0 ciphers
+    "TLSv1"     all TLS version 1.0 ciphers
+    "ECDH"      all ciphers using Elliptic Curve Diffie-Hellman key exchange
     "EXP"       all export ciphers (40-bit)
     "EXPORT56"  all export ciphers (56-bit)
     "LOW"       all low strength ciphers (no export)
@@ -366,14 +377,14 @@ All algorithms including ADH and export but excluding patented algorithms:
 <p>
 The OpenSSL command
 <pre>
-  openssl ciphers -v <em><list of ciphers></em>
+  $ openssl ciphers -v <em><list of ciphers></em>
 </pre>
 may be used to list all of the ciphers and the order described by a specific
 <em><list of ciphers></em>.
 
 <p>
 <hr>
-<h2><a name="TLSControlsACLs">TLSControlsACLs</a></h2>
+<h3><a name="TLSControlsACLs">TLSControlsACLs</a></h3>
 <strong>Syntax:</strong> TLSControlsACLs <em>actions|"all" "allow"|"deny" "user"|"group" list</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -409,7 +420,7 @@ Examples:
 
 <p>
 <hr>
-<h2><a name="TLSCryptoDevice">TLSCryptoDevice</a></h2>
+<h3><a name="TLSCryptoDevice">TLSCryptoDevice</a></h3>
 <strong>Syntax:</strong> TLSCryptoDevice <em>driver|"all"|"none"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -438,13 +449,13 @@ the default engine to use, specify the engine name, <i>e.g.</i>:
 <p>
 The OpenSSL command
 <pre>
-  openssl engine
+  $ openssl engine
 </pre>
 may be used to list all of the engine drivers supported by OpenSSL.
 
 <p>
 <hr>
-<h2><a name="TLSDHParamFile">TLSDHParamFile</a></h2>
+<h3><a name="TLSDHParamFile">TLSDHParamFile</a></h3>
 <strong>Syntax:</strong> TLSDHParamFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -465,9 +476,9 @@ stored in a <code>TLSDHParamFile</code>.  The <code>dhparam</code> utility
 that comes with OpenSSL may be used to generate an appropriate file for this
 directive:
 <pre>
-  # openssl dhparam -outform PEM -2 <i>nbits</i> >> dhparams.pem
-  # openssl dhparam -outform PEM -5 <i>nbits</i> >> dhparams.pem
-<pre>
+  $ openssl dhparam -outform PEM -2 <i>nbits</i> >> dhparams.pem
+  $ openssl dhparam -outform PEM -5 <i>nbits</i> >> dhparams.pem
+</pre>
 Using either -2 or -5 as the generator is fine. The <em>nbits</em> value used
 should vary between 512 and 4096, inclusive.
 
@@ -475,8 +486,16 @@ should vary between 512 and 4096, inclusive.
 The <em>file</em> parameter must be an absolute path.
 
 <p>
+<b>Note</b> that as of <code>proftpd-1.3.6rc1</code> and later, for
+Diffie-Hellman key exchanges, <code>mod_tls</code> will generate DH parameters
+that match the size of the server certificate's RSA/DSA key.  Some clients,
+such as Java 7 (and earlier) code, cannot handle DH parameter lengths greater
+than 1024 bits; see this <a href="#TLSJavaDH">FAQ</a> for a workaround for such
+clients.
+
+<p>
 <hr>
-<h2><a name="TLSDSACertficateFile">TLSDSACertficateFile</a></h2>
+<h3><a name="TLSDSACertficateFile">TLSDSACertficateFile</a></h3>
 <strong>Syntax:</strong> TLSDSACertificateFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -502,7 +521,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSDSACertificateKeyFile">TLSDSACertificateKeyFile</a></h2>
+<h3><a name="TLSDSACertificateKeyFile">TLSDSACertificateKeyFile</a></h3>
 <strong>Syntax:</strong> TLSDSACertificateKeyFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -532,7 +551,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSECCertficateFile">TLSECCertficateFile</a></h2>
+<h3><a name="TLSECCertficateFile">TLSECCertficateFile</a></h3>
 <strong>Syntax:</strong> TLSECCertificateFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -558,7 +577,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSECCertificateKeyFile">TLSECCertificateKeyFile</a></h2>
+<h3><a name="TLSECCertificateKeyFile">TLSECCertificateKeyFile</a></h3>
 <strong>Syntax:</strong> TLSECCertificateKeyFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -588,7 +607,24 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSEngine">TLSEngine</a></h2>
+<h3><a name="TLSECDHCurve">TLSECDHCurve</a></h3>
+<strong>Syntax:</strong> TLSECDHCurve <em>curve</em><br>
+<strong>Default:</strong> TLSECDHCurve prime256v1<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>TLSECDHCurve</code> directive specifies the EC curve to use for
+ECDHE ciphers.  To see the full list of curves supported by your OpenSSL
+library, use:
+<pre>
+  $ openssl ecparam -list_curves
+</pre>
+
+<p>
+<hr>
+<h3><a name="TLSEngine">TLSEngine</a></h3>
 <strong>Syntax:</strong> TLSEngine <em>on|off</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -604,7 +640,7 @@ the main server and all configured virtual hosts.
 
 <p>
 <hr>
-<h2><a name="TLSLog">TLSLog</a></h2>
+<h3><a name="TLSLog">TLSLog</a></h3>
 <strong>Syntax:</strong> TLSLog <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -623,7 +659,7 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="TLSMasqueradeAddress">TLSMasqueradeAddress</a></h2>
+<h3><a name="TLSMasqueradeAddress">TLSMasqueradeAddress</a></h3>
 <strong>Syntax:</strong> TLSMasqueradeAddress <em>ip-address|dns-name|device-name</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code><br>
@@ -683,7 +719,30 @@ but only for FTPS sessions.
 
 <p>
 <hr>
-<h2><a name="TLSOptions">TLSOptions</a></h2>
+<h3><a name="TLSNextProtocol">TLSNextProtocol</a></h3>
+<strong>Syntax:</strong> TLSNextProtocol <em>on|off</em><br>
+<strong>Default:</strong> TLSNextProtocol on<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>TLSNextProtocol</code> directive toggles <code>mod_tls</code>' support
+for the <a href="http://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation">ALPN</a> (and NPN) TLS extensions.  These extensions are used to negotiate
+the "next protocol" that will be once the SSL/TLS session is established; in
+the case of <code>mod_tls</code>, that "next protocol" is <b>always</b> "ftp".
+
+<p>
+Some FTPS clients use the support of ALPN/NPN as a heuristic for knowing when
+to use <a href="https://technotes.googlecode.com/git/falsestart.html">TLS False Start</a>, which can reduce the SSL/TLS handshake network latency.  Initially
+TLS clients used TLS False Start for any/all sites, but encountered
+<a href="https://www.imperialviolet.org/2012/04/11/falsestart.html">issues</a>;
+these clients (<i>e.g.</i> Chrome, Firefox, perhaps others) now only use
+the TLS False Start optimization for ALPN/NPN-enabled SSL/TLS servers.
+
+<p>
+<hr>
+<h3><a name="TLSOptions">TLSOptions</a></h3>
 <strong>Syntax:</strong> TLSOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -692,10 +751,7 @@ but only for FTPS sessions.
 
 <p>
 The <code>TLSOptions</code> directive is used to configure various optional
-behavior of <code>mod_tls</code>.  <b>Note</b>: all of the configured
-<code>TLSOptions</code> parameters <b>must</b> appear on the same line in
-the configuration; only the first <code>TLSOptions</code> directive that
-appears in the configuration is used.
+behavior of <code>mod_tls</code>.
 
 <p>
 Example:
@@ -767,6 +823,20 @@ The currently implemented options are:
     <code>AllowPerUser</code> option.
 
   <p>
+  <li><code>AllowWeakDH</code><br>
+    <p>
+    The <code>mod_tls</code> will not use Diffie-Hellman groups of less
+    than 1024 bits, due to <a href="https://www.weakdh.org">weaknesses</a>
+    that can downgrade the security of an SSL/TLS session.  If for any reason
+    your FTPS clients <b>require</b> smaller Diffie-Hellman groups, then
+    use this option.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
   <li><code>CommonNameRequired</code><br>
     <p>
     This option will cause <code>mod_tls</code> to perform checks on a client's
@@ -791,7 +861,7 @@ The currently implemented options are:
     <p>
     Sets the following environment variables, if applicable.  Note that
     doing so increases the memory size of the process quite a bit:
-    <table border=1>
+    <table border=1 summary="TLS Environment Variables">
       <tr>
         <td><code>TLS_SERVER_CERT</code></td>
         <td>Server certificate, PEM-encoded</td>
@@ -809,17 +879,32 @@ The currently implemented options are:
     </table>
 
   <p>
+  <li><code>NoAutoECDH</code><br>
+    <p>
+    If OpenSSL-1.0.2 or later is used, then <code>mod_tls</code> will
+    attempt to automatically negotiate the best EC curve to use, when needed.
+    Use this option to disable that automatic behavior for any reason.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc1</code>.
+  </li>
+
+  <p>
   <li><code>NoCertRequest</code><br>
     <p>
-    Some FTP clients are known to be buggy when handling a server's certificate
-    request.  This option causes the server to <b>not</b> send such a request
-    during an SSL handshake.
+    <b>Note</b>: As of <code>proftpd-1.3.6rc2</code>, this option is deprecated
+    and thus ignored.  By default, <code>mod_tls</code> will not send the
+    <code>CertificateRequest</code> message to TLS clients during the handshake;
+    that message is only sent when client certificates will be verified
+    via the <a href="#TLSVerifyClient"><code>TLSVerifyClient</code></a>
+    directive.
 
   <p>
   <li><code>NoEmptyFragments</code><br>
     <p>
     In order to prevent certain attacks (<i>e.g.</i> the so-called
-    <a href="http://www.kb.cert.org/vuls/id/864643">&quotBEAST"
+    <a href="http://www.kb.cert.org/vuls/id/864643">"BEAST"
     attack</a>), the <code>mod_tls</code> module was changed to use OpenSSL's
     builtin countermeasure of inserting <a href="http://www.openssl.org/~bodo/tls-cbc.txt">empty fragments</a>.  However, some browsers/clients may not handle
     such empty fragments well.  Use this <code>NoEmptyFragaments</code>
@@ -858,7 +943,7 @@ The currently implemented options are:
     Sets the following environment variables, if applicable.  These environment
     variables are then avaiable for use, such as in <code>LogFormat</code>s.
     Note that doing so may increase the memory size of the process quite a bit:
-    <table border=1>
+    <table border=1 summary="Standard Environment Variables">
       <tr>
         <td><code>FTPS</code></td>
         <td>Present if FTP over SSL/TLS is being used</td>
@@ -915,7 +1000,7 @@ The currently implemented options are:
       </tr>
 
       <tr>
-        <td><code>TLS_CLIENT_S_DN_</code><i>x509<i></td>
+        <td><code>TLS_CLIENT_S_DN_</code><i>x509</i></td>
         <td>Component of client certificate's Subject DN, where <i>x509</i> is
           a component of a X509 DN:<br>
           C,CN,D,I,G,L,O,OU,S,ST,T,UID,Email</td>
@@ -974,6 +1059,11 @@ The currently implemented options are:
       </tr>
 
       <tr>
+        <td><code>TLS_SERVER_NAME</code></td>
+        <td>Server name requested via Server Name Indication (SNI), if present</td>
+      </tr>
+
+      <tr>
         <td><code>TLS_SERVER_S_DN</code></td>
         <td>Subject DN of server certificate</td>
       </tr>
@@ -1078,7 +1168,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="TLSPKCS12File">TLSPKCS12File</a></h2>
+<h3><a name="TLSPKCS12File">TLSPKCS12File</a></h3>
 <strong>Syntax:</strong> TLSPKCS12File <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1103,7 +1193,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSPassPhraseProvider">TLSPassPhraseProvider</a></h2>
+<h3><a name="TLSPassPhraseProvider">TLSPassPhraseProvider</a></h3>
 <strong>Syntax:</strong> TLSPassPhraseProvider <em>path</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -1139,9 +1229,54 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSProtocol">TLSProtocol</a></h2>
+<h3><a name="TLSPreSharedKey">TLSPreSharedKey</a></h3>
+<strong>Syntax:</strong> TLSPreSharedKey <em>identity</em> <em>key-info</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>TLSPreSharedKey</code> directive is used to configure a <em>pre-shared
+key</em> (PSK), for use in
+<a href="http://en.wikipedia.org/wiki/TLS-PSK">TLS-PSK</a> ciphersuites.  Each
+PSK has an <em>identity</em> (a string/name used by clients to request the use
+of that PSK), and the actual key data.  The key data may be encoded in different
+ways; the <code>TLSPreSharedKey</code> directive requires that the data be
+hex-encoded, as indicated in the <em>key-info</em> parameter.
+
+<p>
+The <em>key-info</em> parameter is comprised of: the type of encoding used for
+the key data, and the full path to the key file.  Only "hex" encoding is
+supported right now.  Thus an example <code>TLSPreSharedKey</code> directive
+would be:
+<pre>
+  TLSPreSharedKey psk-name hex:/path/to/psk.key
+</pre>
+The configured file <b>cannot be world-readable or world-writable</b>; the
+<code>mod_tls</code> module will skip/ignore such insecure permissions.
+
+<p>
+To generate this shared key (which is just a randomly generated bit of data),
+you can use:
+<pre>
+  $ openssl rand 80 -out /path/to/identity.key -hex
+</pre>
+Note that <code>TLSPreSharedKey</code> requires at least 20 bytes of key data.
+Have generated the random key data, tell <code>mod_tls</code> to use it via:
+<pre>
+  TLSPreSharedKey identity hex:/path/to/identity.key
+</pre>
+
+<p>
+Multiple <code>TLSPreSharedKey</code> directives can be used to configure
+different PSKs for different identity names.
+
+<p>
+<hr>
+<h3><a name="TLSProtocol">TLSProtocol</a></h3>
 <strong>Syntax:</strong> TLSProtocol <em>protocol1</em> ... <em>protocolN</em><br>
-<strong>Default:</strong> SSLv3 TLSv1<br>
+<strong>Default:</strong> TLSv1<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_tls<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
@@ -1160,7 +1295,7 @@ in the "server config" context.
 <p>
 The allowed protocols are:
 <p>
-<table>
+<table summary="TLS Protocol Versions">
   <tr>
     <td><code>SSLv3</code></td>
     <td>Allow only SSLv3</td>
@@ -1193,11 +1328,26 @@ Note that the parameter "SSLv23" is supported as a legacy style for saying
 "all versions".
 
 <p>
-All use of SSLv2 is disabled.  SSLv2 <b>should not</b> be used.
+All use of SSLv2 is disabled.  SSLv2 <b>should not</b> be used.  As of
+<code>proftpd-1.3.6rc1</code>, SSLv3 support has been disabled as well.
+
+<p>
+In <code>proftpd-1.3.6rc2</code> and later, you can use the
+<code>TLSProtocol</code> directive in a different manner, to <em>add</em>
+or <em>subtract</em> protocol support.  For example, to enable all protocols
+<b>except</b> SSLv3, you can use:
+<pre>
+  TLSProtocol ALL -SSLv3
+</pre>
+Using the directive in this manner requires that "ALL" be the <b>first</b>
+parameter, <i>and</i> that all protocols have either a <code>+</code>
+(<em>add</em>) or <code>-</code> (<em>subtract</em>) prefix.  "ALL" will
+always be expanded to all of the supported SSL/TLS protocols known by
+<code>mod_tls</code> and supported by <code>OpenSSL</code>.
 
 <p>
 <hr>
-<h2><a name="TLSRandomSeed">TLSRandomSeed</a></h2>
+<h3><a name="TLSRandomSeed">TLSRandomSeed</a></h3>
 <strong>Syntax:</strong> TLSRandomSeed <em>seed</em><br>
 <strong>Default:</strong> <i>openssl-dir</i><code>/.rnd</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1229,7 +1379,7 @@ speed up entropy gathering when the daemon starts up again.
 
 <p>
 <hr>
-<h2><a name="TLSRenegotiate">TLSRenegotiate</a></h2>
+<h3><a name="TLSRenegotiate">TLSRenegotiate</a></h3>
 <strong>Syntax:</strong> TLSRenegotiate <em>["ctrl" secs] ["data" Kbytes] ["timeout" secs]|["required" on|off]|"none"</em><br>
 <strong>Default:</strong> ctrl 14400 data 25165824 required true <i>(for OpenSSL 0.9.7 or greater)</i><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1292,7 +1442,7 @@ Examples:
 
 <p>
 <hr>
-<h2><a name="TLSRequired">TLSRequired</a></h2>
+<h3><a name="TLSRequired">TLSRequired</a></h3>
 <strong>Syntax:</strong> TLSRequired <em>on|off|ctrl|[!]data|auth|auth+[!]data</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -1368,7 +1518,7 @@ the following:
 
 <p>
 <hr>
-<h2><a name="TLSRSACertificateFile">TLSRSACertificateFile</a></h2>
+<h3><a name="TLSRSACertificateFile">TLSRSACertificateFile</a></h3>
 <strong>Syntax:</strong> TLSRSACertificateFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1394,7 +1544,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSRSACertificateKeyFile">TLSRSACertificateKeyFile</a></h2>
+<h3><a name="TLSRSACertificateKeyFile">TLSRSACertificateKeyFile</a></h3>
 <strong>Syntax:</strong> TLSRSACertificateKeyFile <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1424,7 +1574,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSServerCipherPreference">TLSServerCipherPreference</a></h2>
+<h3><a name="TLSServerCipherPreference">TLSServerCipherPreference</a></h3>
 <strong>Syntax:</strong> TLSServerCipherPreference <em>on|off</em><br>
 <strong>Default:</strong> Off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1445,7 +1595,20 @@ For example:
 
 <p>
 <hr>
-<h2><a name="TLSSessionCache">TLSSessionCache</a></h2>
+<h3><a name="TLSServerInfoFile">TLSServerInfoFile</a></h3>
+<strong>Syntax:</strong> TLSServerInfoFile <em>file</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSServerInfoFile</code> directive uses the configured <em>file</em>
+as custom TLS extensions.  See the OpenSSL documentation for the <a href="https://www.openssl.org/docs/man1.0.2/ssl/SSL_CTX_use_serverinfo_file.html"><code>SSL_CTX_use_serverinfo_file()</code></a> for more information.
+
+<p>
+<hr>
+<h3><a name="TLSSessionCache">TLSSessionCache</a></h3>
 <strong>Syntax:</strong> TLSSessionCache <em>"off"|type:/info [timeout]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -1454,7 +1617,7 @@ For example:
 
 <p>
 The <code>TLSSessionCache</code> directive configures an external SSL session
-cache, which can be used for storing and shared SSL sessions across multiple
+cache, which can be used for storing and sharing SSL sessions across multiple
 processes.  An external SSL session cache is an optional facility which speeds
 up parallel FTPS session connections.
 
@@ -1494,7 +1657,10 @@ for using a shared memory external SSL session cache, see the
 The optional <em>timeout</em> parameters sets the time-to-live, in seconds, for
 the SSL session datal stored in the external SSL session cache.  It can be set
 as low as 15 for testing, but should be set to higher values like 600 in real
-life.  The default timeout is 1800 seconds (30 minutes).
+life.  The default timeout is 1800 seconds (30 minutes).  <b>Note</b> that to
+ensure that the session cache is <em>aggressively</em> pruned of expired
+sessions, <b>each</b> FTP session, upon ending, will flush <b>any</b> expired
+sessions from the session cache.
 
 <p>
 Use of SSL session caching can be disabled entirely by using:
@@ -1504,7 +1670,188 @@ Use of SSL session caching can be disabled entirely by using:
 
 <p>
 <hr>
-<h2><a name="TLSTimeoutHandshake">TLSTimeoutHandshake</a></h2>
+<h3><a name="TLSSessionTicketKeys">TLSSessionTicketKeys</a></h3>
+<strong>Syntax:</strong> TLSSessionTicketKeys [age <em>secs</em>] [count <em>number</em>]<br>
+<strong>Default:</strong> TLSSessionTicketKeys age 12h count 25<br>
+<strong>Context:</strong> server config</br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSSessionTicketKeys</code> directive can be used to control how
+often <code>mod_tls</code> generates new session ticket keys (assuming that
+<a href="#TLSSessionTickets"><code>TLSSessionTickets</code></a> is enabled),
+and how many ticket keys will be kept in one time in memory.
+
+<p>
+By default, <code>mod_tls</code> expires a session ticket key after 12 hours;
+this can be changed using the <em>age</em> parameter, to specify a maximum
+age.  <b>Note</b> that the <i>minimum</i> key age is 60 seconds.
+
+<p>
+Only a maximum of 25 session ticket keys will be kept in memory by default;
+older/expired keys will be destroyed.  This maximum count of keys can
+be changed using the <em>count</em> parameter.  <b>Note</b> that there is a
+minimum count (1) of ticket keys; attemping to specify a smaller <em>count</em>
+is a configuration error.
+
+<p>
+<hr>
+<h3><a name="TLSSessionTickets">TLSSessionTickets</a></h3>
+<strong>Syntax:</strong> TLSSessionTickets <em>on|off</em><br>
+<strong>Default:</strong> off<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSSessionTickets</code> directive enables the use of TLS
+"session tickets" (see <a href="http://www.faqs.org/rfcs/rfc5077.html">RFC 5077</a>), which allow for session resumption, similar to TLS session caching.
+
+<p>
+The <code>mod_tls</code> module does <b>not</b> support configuration/use of
+a static session ticket <em>key</em>, unlike Apache or nginx.  Instead,
+<code>mod_tls</code> <i>always</i> randomly generates its own session ticket
+keys.  These keys are only kept in memory, and are automatically generated
+on a schedule; older keys are destroyed automatically.
+
+<p>
+When a session is resumed using a session ticket encrypted with an older
+session ticket key (which has not yet expired), the <code>mod_tls</code> will
+honor that session ticket, <i>but</i> will also <b>renew</b> the encryption
+of the session ticket using a newer session ticket key.  Session tickets
+encrypted with keys which have expired will not be honored, and a full TLS
+handshake will occur.
+
+<p>
+For control over the key expiration and generation schedule, use the
+<a href="#TLSSessionTicketKeys"><code>TLSSessionTicketKeys</code></a>
+directive.
+
+<p>
+<hr>
+<h3><a name="TLSStapling">TLSStapling</a></h3>
+<strong>Syntax:</strong> TLStapling <em>on|off</em><br>
+<strong>Default:</strong> off<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSStapling</code> directive enables
+<a href="https://en.wikipedia.org/wiki/OCSP_stapling">OCSP stapling</a>,
+as defined by the "Certificate Status Request" TLS extension
+(<a href="http://www.faqs.org/rfcs/rfc6066.html">RFC 6066</a>).  If
+<code>TLSStapling</code> is enabled, <b>and</b> the certificate status request
+extension is used by the SSL/TLS client, then <code>mod_tls</code> will
+include an OCSP response for the server certificate, in the TLS handshake.
+
+<p>
+By default, <code>TLSStapling</code> is <em>off</em>, but will automatically
+be enabled if a <a href="#TLSStaplingCache"><code>TLSStaplingCache</code></a>
+is configured.
+
+<p>
+See also: <a href="#TLSStaplingCache"><code>TLSStaplingCache</code></a>,
+<a href="#TLSStaplingOptions"><code>TLSStaplingOptions</code></a>,
+<a href="#TLSStaplingResponder"><code>TLSStaplingResponder</code></a>, and
+<a href="#TLSStaplingTimeout"><code>TLSStaplingTimeout</code></a>
+
+<p>
+<hr>
+<h3><a name="TLSStaplingCache">TLSStaplingCache</a></h3>
+<strong>Syntax:</strong> TLStaplingCache <em>type:/info</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSStaplingCache</code> directive configures an external OCSP response
+cache, which can be used for storing and sharing OCSP responses across multiple
+processes.  An external OCSP response cache is an optional facility which
+speeds up TLS handshakes when
+<a href="#TLSStapling"><code>TLSStapling</code></a> is enabled.
+
+<p>
+The <em>type</em> and <em>info</em> parameters all depend on the module
+implementing the external OCSP response cache being configured.  For example,
+for using a filesystem-based external OCSP response cache, see the
+<a href="mod_tls_fscache.html"><code>mod_tls_fscache</code></a> documentation,
+or see <a href="mod_tls_shmcache.html"><code>mod_tls_shmcache</code></a> for
+a shared memory-based OCSP response cache, or
+<a href="mod_tls_memcache.html"><code>mod_tls_memcache</code></a> for using
+memcached servers as an OCSP response cache.
+
+<p>
+<hr>
+<h3><a name="TLSStaplingOptions">TLSStaplingOptions</a></h3>
+<strong>Syntax:</strong> TLStaplingOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSStaplingOptions</code> directive configures various optional
+behaviors of <code>mod_tls</code> when querying OCSP responders.
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>NoNonce</code><br>
+    <p>
+    To defend against replay attacks of OCSP responses, the protocol allows
+    for a nonce to be included in the request; this nonce is then expected
+    to be in the OCSP response.  However, many OCSP responders pre-generate
+    the OCSP responses, as it less computationally expensive to do so.  Thus
+    to tell <code>mod_tls</code> to not include a nonce in its OCSP request
+    (and not expect to see that nonce in the OCSP response), use this option:
+    <pre>
+  # Many OCSP responders pregenerate their responses, and thus cannot
+  # include nonces in the response.
+  TLSStaplingOptions NoNonce
+    </pre>
+  </li>
+</ul>
+
+<p>
+<hr>
+<h3><a name="TLSStaplingResponder">TLSStaplingResponder</a></h3>
+<strong>Syntax:</strong> TLStaplingResponder <em>url</em><br>
+<strong>Default:</strong> none<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSStaplingResponder</code> directive overrides the URL of the OCSP
+responder specified in the "Authority Information Access" certificate extension.
+
+<p>
+Example:
+<pre>
+  # Use our own custom OCSP responder
+  TLSStaplingResponder http://gw.example.com/ocsp/
+</pre>
+
+<p>
+<hr>
+<h3><a name="TLSStaplingTimeout">TLSStaplingTimeout</a></h3>
+<strong>Syntax:</strong> TLStaplingTimeout <em>secs</em><br>
+<strong>Default:</strong> 10s<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_tls<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>TLSStaplingTimeout</code> directive sets the timeout for queries
+to OCSP responders when <code>TLSStapling</code> is enabled, and
+<code>mod_tls</code> is querying a responder for OCSP stapling purposes.
+
+<p>
+<hr>
+<h3><a name="TLSTimeoutHandshake">TLSTimeoutHandshake</a></h3>
 <strong>Syntax:</strong> TLSTimeoutHandshake <em>seconds</em><br>
 <strong>Default:</strong> 300<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1519,7 +1866,7 @@ default is 300 seconds (five minutes).
 
 <p>
 <hr>
-<h2><a name="TLSUserName">TLSUserName</a></h2>
+<h3><a name="TLSUserName">TLSUserName</a></h3>
 <strong>Syntax:</strong> TLSUserName <em>attribute</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1558,7 +1905,7 @@ Note that for the <code>TLSUserName</code> directive to be effective,
 certificates, <i>i.e.</i>:
 <pre>
   # Verify clients
-  TLSVerifyClient on
+  TLSVerifyClient optional
 
   # and possibly verify the user based on the client certs
   TLSUserName CommonName
@@ -1569,8 +1916,8 @@ See also: <a href="#TLSVerifyClient"><code>TLSVerifyClient</code></a>
 
 <p>
 <hr>
-<h2><a name="TLSVerifyClient">TLSVerifyClient</a></h2>
-<strong>Syntax:</strong> TLSVerifyClient <em>on|off</em><br>
+<h3><a name="TLSVerifyClient">TLSVerifyClient</a></h3>
+<strong>Syntax:</strong> TLSVerifyClient <em>on|off|optional</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_tls<br>
@@ -1579,19 +1926,19 @@ See also: <a href="#TLSVerifyClient"><code>TLSVerifyClient</code></a>
 <p>
 The <code>TLSVerifyClient</code> directive configures how <code>mod_tls</code>
 handles certificates presented by clients.  If <em>off</em>, the module
-will accept the certificate and establish an SSL/TLS session, but will not
-verify the certificate.  If <em>on</em>, the module will verify a client's
-certificate and, furthermore, will fail all SSL handshake attempts <b>unless</b>
-the client presents a certificate when the server requests one.  Note that the
-server can be configured to <i>not</i> request a client certificate via
-the <code>TLSOptions</code> directive's "NoCertRequest" parameter.
+will not request the client certificate while establishing an SSL/TLS session.
+If <em>on</em>, the module will verify a client's certificate and, furthermore,
+will fail all SSL handshake attempts <b>unless</b> the client presents a
+certificate when the server requests one.  If <em>optional</em>, then
+<code>mod_tls</code> will <i>request</i> that a client send its certificate,
+but will not fail the handshake if the client fails to provide a certificate.
 
 <p>
 See also: <a href="#TLSOptions"><code>TLSOptions</code></a>
 
 <p>
 <hr>
-<h2><a name="TLSVerifyDepth">TLSVerifyDepth</a></h2>
+<h3><a name="TLSVerifyDepth">TLSVerifyDepth</a></h3>
 <strong>Syntax:</strong> TLSVerifyDepth <em>depth</em><br>
 <strong>Default:</strong> 9<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1617,7 +1964,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="TLSVerifyOrder">TLSVerifyOrder</a></h2>
+<h3><a name="TLSVerifyOrder">TLSVerifyOrder</a></h3>
 <strong>Syntax:</strong> TLSVerifyOrder <em>crl|ocsp</em><br>
 <strong>Default:</strong> crl<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1663,7 +2010,7 @@ See also: <a href="#TLSCARevocationFile"><code>TLSCARevocationFile</code></a>,
 
 <p>
 <hr>
-<h2><a name="TLSVerifyServer">TLSVerifyServer</a></h2>
+<h3><a name="TLSVerifyServer">TLSVerifyServer</a></h3>
 <strong>Syntax:</strong> TLSVerifyServer <em>on|off|NoReverseDNS</em><br>
 <strong>Default:</strong> on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1791,7 +2138,7 @@ See also: <a href="#TLSSessionCache"><code>TLSSessionCache</code></a>
 Much of the documentation for Apache's <code>mod_ssl</code>, concerning
 certificates, OpenSSL usage, <i>etc</i> applies to this module as well:
 <pre>
-  <a href="http://www.modssl.org/docs/2.7/">http://www.modssl.org/docs/2.7/</a>
+  <a href="http://httpd.apache.org/docs/2.4/mod/mod_ssl.html">http://httpd.apache.org/docs/2.4/mod/mod_ssl.html</a>
 </pre>
 The OpenSSL documentation, and its
 <a href="http://www.openssl.org/support/faq.cgi">FAQ</a>, are recommended as
@@ -1812,47 +2159,94 @@ A copy of the Draft describing FTP over SSL/TLS is included with the source
 code for this module.
 
 <p>
+<b>Logging</b><br>
+The <code>mod_tls</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>tls
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace tls:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
+<p><a name="FAQ">
+<b>Frequently Asked Questions</b><br>
+
+<p><a name="TLSJavaDH">
+<font color=red>Question</font>: When I use a Java client to connect to my
+<code>proftpd</code> server using FTPS, it fails with exceptions such as:
+<pre>
+  java.lang.RuntimeException: Could not generate DH keypair and java.security.InvalidAlgorithmParameterException: Prime size must be multiple of 64, and can only range from 512 to 1024 (inclusive)
+</pre>
+How can I fix this?<br>
+<font color=blue>Answer</font>: This happens because <code>mod_tls</code> tries
+to use longer DH parameter lengths when it can, but not all clients support
+longer DH parameter lengths.
+
+<p>
+To address this, you need to configure a custom 1024-bit DH parameter via the
+<a href="#TLSDHParamFile"><code>TLSDHParamFile</code></a> directive.  You
+can generate a custom DH parameter using <code>openssl dhparam</code>,
+<i>e.g.</i>:
+<pre>
+  $ openssl dhparam -outform PEM -5 1024 > dh1024.pem
+</pre>
+Alternatively, you can append the following standard 1024-bit DH parameters
+from <a href="http://www.faqs.org/rfcs/rfc2409">RFC 2409</a>, section 6.2,
+into a <code>dh1024.pem</code> file:
+<pre>
+-----BEGIN DH PARAMETERS-----
+MIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJR
+Sgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL
+/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgEC
+-----END DH PARAMETERS-----
+</pre>
+Then tell <code>mod_tls</code> to use that DH parameter:
+<pre>
+  # Use 1024-bit DH parameters for the Java clients
+  TLSDHParamFile /path/to/dh1024.pem
+</pre>
+
+<p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_tls</code> module is distributed with ProFTPD.  Simply follow
-the normal steps for using third-party modules in proftpd: 
+the normal steps for using third-party modules in ProFTPD: 
 <pre>
-  ./configure --with-modules=mod_tls
-  make
-  make install
+  $ ./configure --with-modules=mod_tls
+  $ make
+  $ make install
 </pre>
 Alternatively, <code>mod_tls</code> can be built as a DSO module:
 <pre>
-  ./configure --enable-dso --with-shared=mod_tls ...
+  $ ./configure --enable-dso --with-shared=mod_tls ...
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
 You may need to specify the location of the OpenSSL header and library files
-in your <code>configure</i> command, <i>e.g.</i>:
+in your <code>configure</code> command, <i>e.g.</i>:
 <pre>
- ./configure --with-modules=mod_tls \
+ $ ./configure --with-modules=mod_tls \
     --with-includes=/usr/local/openssl \
     --with-libraries=/usr/local/openssl
 </pre>
 
 <p>
-<hr><br>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-12-09 23:15:16 $</i><br>
-
 <hr>
 <font size=2><b><i>
-© Copyright 2002-2013 TJ Saunders<br>
+© Copyright 2002-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_tls_fscache.html b/doc/contrib/mod_tls_fscache.html
new file mode 100644
index 0000000..8d62eee
--- /dev/null
+++ b/doc/contrib/mod_tls_fscache.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_tls_fscache</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_tls_fscache</code></b></h2>
+</center>
+<hr>
+
+<p>
+The <code>mod_tls_fscache</code> submodule is contained in the
+<code>mod_tls_fscache.c</code> file, and is not compiled by default.
+Installation instructions are discussed <a href="#Installation">here</a>.
+
+<p>
+This submodule a filesystem-based implementation of an external OCSP response
+cache for use by the <code>mod_tls</code> module's
+<a href="mod_tls.html#TLStaplingCache"><code>TLSStaplingCache</code></a>
+directive.
+
+<p>
+This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/).
+
+<p>
+This product includes cryptographic software written by Eric Young (eay at cryptsoft.com).
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
+questions, concerns, or suggestions regarding this module.
+
+<p>
+The <code>mod_tls_fscache</code> module supports the "fs" string for
+the <em>type</em> parameter of the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLStaplingCache</code></a>
+configuration directive.  The <em>info</em> parameter for
+<code>mod_tls_fscache</code> must be the path to a directory, on disk,
+in which <code>mod_tls_fscache</code> will store OCSP responses.  This means
+that the <code>TLSStaplingCache</code> setting will look like:
+<pre>
+  TLSStaplingCache fs:/path=/var/ftpd/ocsp
+</pre>
+
+<p>
+<b>Logging</b><br>
+The <code>mod_tls_fscache</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>tls.fscache
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace tls.fscache:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_tls_fscache</code> module is distributed with the ProFTPD
+source code.  Simply follow the normal steps for using third-party modules
+in ProFTPD, being sure to include the <code>mod_tls</code> module (on which
+<code>mod_tls_fscache</code> depends):
+<pre>
+  $ ./configure --with-modules=mod_tls:mod_tls_fscache
+  $ make
+  $ make install
+</pre>
+
+<p>
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_tls_fscache</code> as
+a shared module:
+<pre>
+  $ prxs -c -i -d mod_tls_fscache.c
+</pre>
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2015 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/contrib/mod_tls_memcache.html b/doc/contrib/mod_tls_memcache.html
index 693c7b2..c81d557 100644
--- a/doc/contrib/mod_tls_memcache.html
+++ b/doc/contrib/mod_tls_memcache.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_tls_memcache.html,v 1.1 2011-02-16 00:27:50 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_tls_memcache.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_tls_memcache</title>
@@ -23,6 +21,9 @@ Installation instructions are discussed <a href="#Installation">here</a>.
 This submodule a memcached-based implementation of an external SSL session
 cache for use by the <code>mod_tls</code> module's
 <a href="mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a>
+directive.  The module also implements a memcached-based implementation of an
+external OCSP response cache for the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLSStaplingCache</code></a>
 directive.
 
 <p>
@@ -37,19 +38,36 @@ Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
 questions, concerns, or suggestions regarding this module.
 
 <p>
-The <code>mod_tls_memmcache</code> module supports the "memcache"
+The <code>mod_tls_memcache</code> module supports the "memcache"
 string for the <em>type</em> parameter of the
 <a href="mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a>
 configuration directive.  The <em>info</em> parameter for
-<code>mod_tls_memcache</code> is blank.  This means the
+<code>mod_tls_memcache</code> can be empty/blank, indicating a native binary
+encoding of the cached data), <i>or</i> it can be "/json", indicating that
+the cached data will be encoded using JSON. This means the
 <code>TLSSessionCache</code> setting will look like:
 <pre>
+  # Use binary encoding for cached data
   TLSSessionCache memcache:
 </pre>
+or:
+<pre>
+  # Use JSON encoding for cached data
+  TLSSessionCache memcache:/json
+</pre>
 If memcache support has not been enabled in your proftpd, this configuration
 <b>cannot</b> be used.
 
 <p>
+The <code>mod_tls_memcache</code> module also supports the "memcache"
+string for the <em>type</em> parameter of the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLStaplingCache</code></a>
+configuration directive, <i>e.g.</i>:
+<pre>
+  TLSStaplingCache memcache:
+</pre>
+
+<p>
 <b>Examples</b><br>
 
 <p>
@@ -63,46 +81,57 @@ If memcache support has not been enabled in your proftpd, this configuration
     ...
 
     <IfModule mod_tls_memcache.c>
-      TLSSessionCache memcache:
+      # Store the data formatted as JSON
+      TLSSessionCache memcache:/json
+      TLSStaplingCache memcache:
     </IfModule>
   </IfModule>
 </pre>
 
 <p>
+<b>Logging</b><br>
+The <code>mod_tls_memcache</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>tls.memcache
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace tls.memcache:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
+<p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_tls_memcache</code> module is distributed with the ProFTPD
 source code.  Simply follow the normal steps for using third-party modules
-in proftpd, being sure to include the <code>mod_tls</code> module (on which
+in ProFTPD, being sure to include the <code>mod_tls</code> module (on which
 <code>mod_tls_memcache</code> depends), <b>and</b> enabling memcache
 support:
 <pre>
-  ./configure --enable-memcache --with-modules=mod_tls:mod_tls_memcache
-  make
-  make install
+  $ ./configure --enable-memcache --with-modules=mod_tls:mod_tls_memcache
+  $ make
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_tls_memcache</code> as
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_tls_memcache</code> as
 a shared module:
 <pre>
-  prxs -c -i -d mod_tls_memcache.c
+  $ prxs -c -i -d mod_tls_memcache.c
 </pre>
 
 <p>
 <hr>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2011-02-16 00:27:50 $</i><br>
-
-<hr>
 <font size=2><b><i>
-© Copyright 2011 TJ Saunders<br>
+© Copyright 2011-2015 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_tls_redis.html b/doc/contrib/mod_tls_redis.html
new file mode 100644
index 0000000..6a1bb64
--- /dev/null
+++ b/doc/contrib/mod_tls_redis.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_tls_redis</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_tls_redis</code></b></h2>
+</center>
+<hr>
+
+<p>
+The <code>mod_tls_redis</code> submodule is contained in the
+<code>mod_tls_redis.c</code> file, and is not compiled by default.
+Installation instructions are discussed <a href="#Installation">here</a>.
+
+<p>
+This submodule a Redis-based implementation of an external SSL session
+cache for use by the <code>mod_tls</code> module's
+<a href="mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a>
+directive.  The module also implements a Redis-based implementation of an
+external OCSP response cache for the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLSStaplingCache</code></a>
+directive.
+
+<p>
+This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/).
+
+<p>
+This product includes cryptographic software written by Eric Young (eay at cryptsoft.com).
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
+questions, concerns, or suggestions regarding this module.
+
+<p>
+The <code>mod_tls_redis</code> module supports the "redis"
+string for the <em>type</em> parameter of the
+<a href="mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a>
+configuration directive.  The <em>info</em> parameter for
+<code>mod_tls_redis</code> can be empty/blank, indicating JSON encoding of
+the cached data.  This means that the
+<code>TLSSessionCache</code> setting would look like:
+<pre>
+  # Use JSON encoding for caching data using Redis
+  TLSSessionCache redis:
+</pre>
+If Redis support has not been enabled in your ProFTPD, this configuration
+<b>cannot</b> be used.
+
+<p>
+The <code>mod_tls_redis</code> module also supports the "redis"
+string for the <em>type</em> parameter of the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLStaplingCache</code></a>
+configuration directive, <i>e.g.</i>:
+<pre>
+  TLSStaplingCache redis:
+</pre>
+
+<p>
+<b>Examples</b><br>
+
+<p>
+<pre>
+  <IfModule mod_redis.c>
+    RedisEngine on
+    RedisServer <i>redis-server</i>
+  </IfModule>
+
+  <IfModule mod_tls.c>
+    ...
+
+    <IfModule mod_tls_redis.c>
+      TLSSessionCache redis:
+      TLSStaplingCache redis:
+    </IfModule>
+  </IfModule>
+</pre>
+
+<p>
+<b>Logging</b><br>
+The <code>mod_tls_redis</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>tls.redis
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace tls.redis:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_tls_redis</code> module is distributed with the ProFTPD
+source code.  Simply follow the normal steps for using third-party modules
+in ProFTPD, being sure to include the <code>mod_tls</code> module (on which
+<code>mod_tls_redis</code> depends), <b>and</b> enabling redis support:
+<pre>
+  $ ./configure --enable-redis --with-modules=mod_tls:mod_tls_redis
+  $ make
+  $ make install
+</pre>
+
+<p>
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_tls_redis</code> as
+a shared module:
+<pre>
+  $ prxs -c -i -d mod_tls_redis.c
+</pre>
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/contrib/mod_tls_shmcache.html b/doc/contrib/mod_tls_shmcache.html
index fdd1d14..12b72b0 100644
--- a/doc/contrib/mod_tls_shmcache.html
+++ b/doc/contrib/mod_tls_shmcache.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_tls_shmcache.html,v 1.3 2013-11-05 21:33:21 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_tls_shmcache.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_tls_shmcache</title>
@@ -23,6 +21,9 @@ Installation instructions are discussed <a href="#Installation">here</a>.
 This submodule provides a SysV shared memory-based implementation of
 an external SSL session cache for use by the <code>mod_tls</code> module's
 <a href="mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a>
+directive.  The module also implements a SysV shared memory-based
+implementation of an external OCSP response cache for the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLSStaplingCache</code></a>
 directive.
 
 <p>
@@ -53,6 +54,22 @@ configure a different size, in bytes.  Note that the configured size
 is configured, that size will be ignored and the default size will be used.
 
 <p>
+The <code>mod_tls_shmcache</code> module also supports the "shm"
+string for the <em>type</em> parameter of the
+<a href="mod_tls.html#TLSStaplingCache"><code>TLStaplingCache</code></a>
+configuration directive.  The <em>info</em> parameter for
+<code>mod_tls_shmcache</code> must be formatted like:
+<pre>
+  /file=<i>/path/to/cache/file</i>[&size=<i>bytes</i>]
+</pre>
+The configured path is used for synchronizing access to the shared memory
+segment among the various server processes.  The default shared memory
+segment size allocated is 1.5MB; use the optional <em>size</em> key to
+configure a different size, in bytes.  Note that the configured size
+<i>must</i> be able to hold at least one cached OCSP response; if a too-small
+size is configured, that size will be ignored and the default size will be used.
+
+<p>
 <b>Examples</b><br>
 
 <p>
@@ -62,7 +79,8 @@ Use the default shared memory segment size and timeout:
     ...
 
     <IfModule mod_tls_shmcache.c>
-      TLSSessionCache shm:/file=/var/ftpd/sesscache
+      TLSSessionCache shm:/file=/var/ftpd/sess_cache
+      TLSStaplingCache shm:/file=/var/ftpd/ocsp_pcache
     </IfModule>
   </IfModule>
 </pre>
@@ -74,7 +92,8 @@ Use a larger shared memory segment size:
     ...
 
     <IfModule mod_tls_shmcache.c>
-      TLSSessionCache shm:/file=/var/ftpd/sesscache&size=2097152
+      TLSSessionCache shm:/file=/var/ftpd/sess_cache&size=2097152
+      TLSStaplingCache shm:/file=/var/ftpd/ocsp_cache&size=2097152
     </IfModule>
   </IfModule>
 </pre>
@@ -86,7 +105,10 @@ Use a smaller shared memory size, and a shorter timeout:
     ...
 
     <IfModule mod_tls_shmcache.c>
-      TLSSessionCache shm:/file=/var/ftpd/sesscache&size=512000 600
+      TLSSessionCache shm:/file=/var/ftpd/sess_cache&size=512000 600
+
+      # Note that TLSStaplingCache does not use a timeout
+      TLSStaplingCache shm:/file=/var/ftpd/ocsp_cache&size=512000
     </IfModule>
   </IfModule>
 </pre>
@@ -96,20 +118,20 @@ Use a smaller shared memory size, and a shorter timeout:
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_tls_shmcache</code> module is distributed with the ProFTPD
 source code.  Simply follow the normal steps for using third-party modules
-in proftpd, being sure to include the <code>mod_tls</code> module (on which
+in ProFTPD, being sure to include the <code>mod_tls</code> module (on which
 <code>mod_tls_shmcache</code> depends):
 <pre>
-  ./configure --with-modules=mod_tls:mod_tls_shmcache
-  make
-  make install
+  $ ./configure --with-modules=mod_tls:mod_tls_shmcache
+  $ make
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_tls_shmcache</code> as
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_tls_shmcache</code> as
 a shared module:
 <pre>
-  prxs -c -i -d mod_tls_shmcache.c
+  $ prxs -c -i -d mod_tls_shmcache.c
 </pre>
 
 <p>
@@ -120,7 +142,7 @@ sure that this module is loaded <i>after</i> the <code>mod_tls</code> module,
   # Load mod_tls first
   LoadModule mod_tls.c
  
-  # Then load any SSL session caching modules
+  # Then load any SSL caching modules
   LoadModule mod_tls_shmcache.c
 </pre>
 
@@ -128,6 +150,21 @@ sure that this module is loaded <i>after</i> the <code>mod_tls</code> module,
 <hr>
 <h2><a name="Usage">Usage</a></h2>
 
+<p>
+<b>Logging</b><br>
+The <code>mod_tls_shmcache</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>tls.shmcache
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace tls.shmcache:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -159,17 +196,11 @@ your config looks something like this:
 
 <p>
 <hr>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-11-05 21:33:21 $</i><br>
-
-<hr>
 <font size=2><b><i>
-© Copyright 2009-2013 TJ Saunders<br>
+© Copyright 2009-2015 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_unique_id.html b/doc/contrib/mod_unique_id.html
index c4392d5..171b244 100644
--- a/doc/contrib/mod_unique_id.html
+++ b/doc/contrib/mod_unique_id.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_unique_id.html,v 1.3 2013-08-14 21:40:18 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_unique_id.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_unique_id</title>
@@ -39,7 +37,7 @@ instructions are discussed <a href="#Installation">here</a>.
 
 <p>
 The most current version of <code>mod_unique_id</code> is distributed with
-the <code>proftpd<code> source code.
+the <code>proftpd</code> source code.
 
 <h2>Author</h2>
 <p>
@@ -52,10 +50,10 @@ questions, concerns, or suggestions regarding this module.
 </ul>
 
 <hr>
-<h2><a name="UniqueIDEngine">UniqueIDEngine</a></h2>
+<h3><a name="UniqueIDEngine">UniqueIDEngine</a></h3>
 <strong>Syntax:</strong> UniqueIDEngine <em>on|off</em><br>
 <strong>Default:</strong> <em>on</em><br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_unique_id<br>
 <strong>Compatibility:</strong> 1.3.1rc1 and later
 
@@ -69,32 +67,27 @@ By default, <code>UniqueIDEngine</code> is <em>on</em>.
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-To install <code>mod_unique_id</code>, copy the <code>mod_unique_id.c</code>
-file into:
-<pre>
-  <i>proftpd-dir</i>/contrib/
-</pre>
-after unpacking the latest proftpd-1.3.<i>x</i> source code.  For including
-<code>mod_unique_id</code> as a staticly linked module:
+The <code>mod_unique_id</code> module is distributed with ProFTPD.  For
+including <code>mod_unique_id</code> as a staticly linked module, use:
 <pre>
-  ./configure --with-modules=mod_unique_id
+  $ ./configure --with-modules=mod_unique_id
 </pre>
 Alternatively, <code>mod_unique_id</code> could be built as a DSO module:
 <pre>
-  ./configure --with-shared=mod_unique_id
+  $ ./configure --with-shared=mod_unique_id
 </pre>
 Then follow the usual steps:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 
 <p>
-Alternatively, if your proftpd was compiled with DSO support, you can
-use the <code>prxs</code> tool to build <code>mod_unique_id</code> as a shared
-module:
+Alternatively, if your <code>proftpd</code> was compiled with DSO support, you
+can use the <code>prxs</code> tool to build <code>mod_unique_id</code> as a
+shared module:
 <pre>
-  prxs -c -i -d mod_unique_id.c
+  $ prxs -c -i -d mod_unique_id.c
 </pre>
 
 <p>
@@ -123,20 +116,12 @@ Example configuration:
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-08-14 21:40:18 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2006-2013 TJ Saunders<br>
+© Copyright 2006-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_wrap.html b/doc/contrib/mod_wrap.html
index 8434728..ff5cb78 100644
--- a/doc/contrib/mod_wrap.html
+++ b/doc/contrib/mod_wrap.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_wrap.html,v 1.4 2009-12-23 23:46:35 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_wrap.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_wrap</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>contrib/mod_wrap.c</code> file for
 ProFTPD 1.2.x, and is not compiled by default.  It enables the daemon to
 use the common tcpwrappers access control library while in
@@ -37,10 +36,8 @@ and use of this module will allow a ProFTPD daemon running in
 is attempted, it will add hosts to the <code>/etc/hosts.deny</code> file.
 
 <p>
-The most current version of <code>mod_wrap</code> can be found at:
-<pre>
-  <a href="http://www.castaglia.org/proftpd/">http://www.castaglia.org/proftpd/</a>
-</pre>
+The most current version of <code>mod_wrap</code> is distributed with the
+ProFTPD source code.
 
 <h2>Author</h2>
 <p>
@@ -82,7 +79,7 @@ for pointing out the issue with passwords not being properly hidden
 </ul>
 
 <hr>
-<h2><a name="TCPAccessFiles">TCPAccessFiles</a></h2>
+<h3><a name="TCPAccessFiles">TCPAccessFiles</a></h3>
 <strong>Syntax:</strong> TCPAccessFiles <em>allow-filename deny-filename</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost</code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -139,7 +136,7 @@ See also:
 
 <p>
 <hr>
-<h2><a name="TCPAccessSyslogLevels">TCPAccessSyslogLevels</a></h2>
+<h3><a name="TCPAccessSyslogLevels">TCPAccessSyslogLevels</a></h3>
 <strong>Syntax:</strong> TCPAccessSyslogLevels <em>allow-level deny-level</em><br>
 <strong>Default:</strong> <code>TCPAccessSyslogLevels info warn</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -165,7 +162,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="#TCPGroupAccessFiles">TCPGroupAccessFiles</a></h2>
+<h3><a name="#TCPGroupAccessFiles">TCPGroupAccessFiles</a></h3>
 <strong>Syntax:</strong> TCPGroupAccessFiles <em>group-expression allow-filename deny-filename</em><br>
 <strong>Default:</strong> <em>None</em><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -199,7 +196,7 @@ See Also: <a href="#TCPAccessFiles">TCPAccessFiles</a>
 
 <p>
 <hr>
-<h2><a name="TCPServiceName">TCPServiceName</a></h2>
+<h3><a name="TCPServiceName">TCPServiceName</a></h3>
 <strong>Syntax:</strong> TCPServiceName <em>name</em><br>
 <strong>Default:</strong> <code>TCPServiceName proftpd</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -215,10 +212,10 @@ such as "ftpd"; use this directive for such needs.
 
 <p>
 <hr>
-<h2><a name="TCPUserAccessFiles">TCPUserAccessFiles</a></h2>
+<h3><a name="TCPUserAccessFiles">TCPUserAccessFiles</a></h3>
 <strong>Syntax:</strong> TCPUserAccessFiles <em>user-expression allow-filename deny-filename</em><br>
 <strong>Default:</strong> <em>None</em><br>
-<strong>Context:</strong> server config, virtual host<BR>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_wrap<br>
 <strong>Compatibility:</strong> 1.2.1 and later
 
@@ -311,27 +308,20 @@ In FreeBSD and Mac OSX, these functions are implemented in <code>libc</code>.
 
 <p>
 Then, you need simply follow the normal steps for using third-party modules in
-proftpd:
+ProFTPD:
 <pre>
-  ./configure --with-modules=mod_wrap
-  make
-  make install
+  $ ./configure --with-modules=mod_wrap
+  $ make
+  $ make install
 </pre>
 
 <p>
 <hr>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2009-12-23 23:46:35 $</i><br>
-
-<br><hr>
-
 <font size=2><b><i>
 © Copyright 2000-2009 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_wrap2.html b/doc/contrib/mod_wrap2.html
index a2d592d..6c6d7e4 100644
--- a/doc/contrib/mod_wrap2.html
+++ b/doc/contrib/mod_wrap2.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_wrap2.html,v 1.7 2013-12-19 17:55:10 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_wrap2.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_wrap2</title>
@@ -14,6 +12,7 @@
 </center>
 <hr>
 
+<p>
 The <code>mod_wrap2</code> package allows the <code>proftpd</code> daemon to
 provide <code>tcpwrapper</code>-like access control rules while running
 in standalone mode.  It also allows for those access rules to be stored
@@ -36,6 +35,7 @@ storage of access table information in various formats:
 <a name="submodules"></a>
 <ul>
   <li><a href="mod_wrap2_file.html"><code>mod_wrap2_file</code></a> for file-based access tables<br>
+  <li><a href="mod_wrap2_redis.html"><code>mod_wrap2_redis</code></a> for Redis-based access lists/sets<br>
   <li><a href="mod_wrap2_sql.html"><code>mod_wrap2_sql</code></a> for SQL-based access tables<br>
 </ul>
 
@@ -73,7 +73,7 @@ the module code.
 </ul>
 
 <hr>
-<h2><a name="WrapAllowMsg">WrapAllowMsg</a></h2>
+<h3><a name="WrapAllowMsg">WrapAllowMsg</a></h3>
 <strong>Syntax:</strong> WrapAllowMsg <em>mesg</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost</code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -95,7 +95,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="WrapDenyMsg">WrapDenyMsg</a></h2>
+<h3><a name="WrapDenyMsg">WrapDenyMsg</a></h3>
 <strong>Syntax:</strong> WrapDenyMsg <em>mesg</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost</code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -117,12 +117,12 @@ Example:
 
 <p>
 <hr>
-<h2><a name="WrapEngine">WrapEngine</a></h2>
+<h3><a name="WrapEngine">WrapEngine</a></h3>
 <strong>Syntax:</strong> WrapEngine <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_wrap2<br>
-<strong>Compatibility:</strong> 1.3.1rc1 and later</a>
+<strong>Compatibility:</strong> 1.3.1rc1 and later
 
 <p>
 The <code>WrapEngine</code> directive enables or disables the module's runtime
@@ -132,7 +132,7 @@ commenting out all <code>mod_wrap2</code> directives.
 
 <p>
 <hr>
-<h2><a name="WrapGroupTables">WrapGroupTables</a></h2>
+<h3><a name="WrapGroupTables">WrapGroupTables</a></h3>
 <strong>Syntax:</strong> WrapGroupTables <em>group-AND-expression source-type:allow-source-info source-type:deny-source-info [source-type:options-source-info]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost</code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -170,12 +170,12 @@ See also:
 
 <p>
 <hr>
-<h2><a name="WrapLog">WrapLog</a></h2>
+<h3><a name="WrapLog">WrapLog</a></h3>
 <strong>Syntax:</strong>  WrapLog <em>file|"none"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_wrap2<br>
-<strong>Compatibility:</strong> 1.3.1rc1 and later</a>
+<strong>Compatibility:</strong> 1.3.1rc1 and later
 
 <p>
 The <code>WrapLog</code> directive is used to specify a log file for
@@ -193,7 +193,7 @@ a <code><Global></code> context.
 
 <p>
 <hr>
-<h2><a name="WrapOptions">WrapOptions</a></h2>
+<h3><a name="WrapOptions">WrapOptions</a></h3>
 <strong>Syntax:</strong> WrapOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -240,13 +240,29 @@ The currently implemented options are:
 <pre>
     --with-modules=mod_wrap2:mod_wrap2_sql:mod_sql:mod_sql_mysql
 </pre>
-  You will also need to ensure that <code>mod_sql</code> creates a
-  database connection at connect time, using the PERCONNECTION
+  Or, if you are using shared/DSO modules, then you <b>must</b> ensure that
+  the modules are loaded in the correct order via <code>LoadModule</code>:
+<pre>
+  ...
+  LoadModule mod_wrap2.c
+  LoadModule mod_wrap2_sql.c
+  ...
+  LoadModule mod_sql.c
+  LoadModule mod_sql_mysql.c
+  ...
+</pre>
+  <p>
+  You will <b>also</b> need to ensure that <code>mod_sql</code> creates a
+  database connection at connect time, using the <code>PERCONNECTION</code>
   connection policy; see
   <a href="mod_sql.html#SQLConnectInfo"><code>SQLConnectInfo</code></a> for
-  more details.
+  more details:
+<pre>
+  SQLConnectInfo <i>database</i> <i>user</i> <i>password</i> PERCONNECTION
+</pre>
 
-  Without this, the database connection will not be created at connect
+  <p>
+  Without this, the database connection will <b>not</b> be created at connect
   time, and the <code>mod_wrap2</code> module will be unable to check
   any allow/deny rules stored in SQL tables when the client connects.
   </li>
@@ -254,7 +270,7 @@ The currently implemented options are:
 
 <p>
 <hr>
-<h2><a name="WrapServiceName">WrapServiceName</a></h2>
+<h3><a name="WrapServiceName">WrapServiceName</a></h3>
 <strong>Syntax:</strong> WrapServiceName <em>name</em><br>
 <strong>Default:</strong> <code>WrapServiceName proftpd</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -271,7 +287,7 @@ using the configured <em>name</em> is <b>case-sensitive</b>.
 
 <p>
 <hr>
-<h2><a name="WrapTables">WrapTables</a></h2>
+<h3><a name="WrapTables">WrapTables</a></h3>
 <strong>Syntax:</strong> WrapTables <em>source-type:allow-source-info source-info:deny-source-info</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost</code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -303,7 +319,7 @@ See also:
 
 <p>
 <hr>
-<h2><a name="WrapUserTables">WrapUserTables</a></h2>
+<h3><a name="WrapUserTables">WrapUserTables</a></h3>
 <strong>Syntax:</strong> WrapUserTables <em>user-OR-expression source-type:allow-source-info source-type:deny-source-info [source-type:option-source-info]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost</code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -374,7 +390,7 @@ explicity denied by rules in that table, <code>mod_wrap2</code> will
 disconnect the client.  By default, if neither explicitly allowed or
 explicitly denied, <code>mod_wrap2</code> will allow the client to continue.
 
-<p><a name="#builtin"></a>
+<p><a name="builtin"></a>
 In addition to the various formats supported by the
 <a href="#submodules">submodules</a>, there is a special source type:
 "builtin".  This is used in the situations where the administrator
@@ -597,12 +613,11 @@ When a syntax error is found in an options list, the error is reported in the
 <p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
-After unpacking the latest proftpd-1.3.<i>x</i> source code, follow the usual
-steps for using third-party modules in <code>proftpd</code>:
+Follow the usual steps for using third-party modules in ProFTPD:
 <pre>
-  ./configure --with-modules=<em>wrap-modules</em>
-  make
-  make install
+  $ ./configure --with-modules=<em>wrap-modules</em>
+  $ make
+  $ make install
 </pre>
 where <em>wrap-modules</em> will depend on the types of access tables you wish
 to support.
@@ -628,17 +643,11 @@ documentation for installation instructions for that module.
 
 <p>
 <hr>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-12-19 17:55:10 $</i><br>
-<hr>
-
 <font size=2><b><i>
-© Copyright 2000-2013 TJ Saunders<br>
+© Copyright 2000-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/contrib/mod_wrap2_file.html b/doc/contrib/mod_wrap2_file.html
index 090f664..f115857 100644
--- a/doc/contrib/mod_wrap2_file.html
+++ b/doc/contrib/mod_wrap2_file.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_wrap2_file.html,v 1.2 2008-12-17 00:24:47 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_wrap2_file.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_wrap2_file</title>
@@ -14,11 +12,6 @@
 </center>
 <hr><br>
 
-This <code>mod_wrap2</code> submodule is contained in the
-<code>mod_wrap2_file.c</code>, and is not compiled by default.  See the
-<code>mod_wrap2</code> <a href="mod_wrap2.html#Installation">installation</a>
-instructions.
-
 <p>
 This submodule provides the file-specific "driver" for storing
 IP/DNS-based access control information in files.
@@ -31,6 +24,12 @@ mode to adapt as these entries are added.  The <code>portsentry</code> program
 does this, for example: when illegal access is attempted, it will add hosts to
 the <code>/etc/hosts.deny</code> file.
 
+<p>
+This <code>mod_wrap2</code> submodule is contained in the
+<code>mod_wrap2_file.c</code>, and is not compiled by default.  See the
+<code>mod_wrap2</code> <a href="mod_wrap2.html#Installation">installation</a>
+instructions.
+
 <h2>Author</h2>
 <p>
 Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
@@ -60,7 +59,8 @@ of that path will be delayed until a user requests a connection, at which time
 the path will be resolved to that user's home directory; or if the path starts
 with <code>~user/</code>, where user is some system user.  In this latter case,
 <code>mod2_wrap</code> will attempt to resolve and verify the given user's home
-directory on start-up.
+directory on start-up.  The <code>%U</code> variable can also be used in the
+paths; it will be resolved to the <code>USER</code> name sent by the client.
 
 <p>
 The format for the files used by <code>mod_wrap2_file</code> is described
@@ -77,6 +77,9 @@ Examples:
 
   # Per-user access files, which are to be found in the user's home directory
   WrapUserTables file:~/my.allow file:~/my.deny
+
+  # Per-user access files, which are <b>not</b> found in the user's home.
+  WrapUserTables file:/etc/ftpd/acls/%U.allow file:/etc/ftpd/acls/%U.deny
 </pre>
 
 <p>
@@ -126,22 +129,14 @@ file. For example:
 </pre>
 The first rule denies some hosts and domains all services; the second rule
 still permits finger requests from other hosts and domains.
-</pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2008-12-17 00:24:47 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2008 TJ Saunders<br>
+© Copyright 2000-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/contrib/mod_wrap2_redis.html b/doc/contrib/mod_wrap2_redis.html
new file mode 100644
index 0000000..eb023ad
--- /dev/null
+++ b/doc/contrib/mod_wrap2_redis.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_wrap2_redis</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_wrap2_redis</code></b></h2>
+</center>
+<hr>
+
+<p>
+This submodule provides the Redis server "driver" for storing
+IP/DNS-based access control information in Redis lists/sets.
+
+<p>
+This <code>mod_wrap2</code> submodule is contained in the
+<code>mod_wrap2_redis.c</code> file, and is not compiled by default.  See the
+<code>mod_wrap2</code> <a href="mod_wrap2.html#Installation">installation</a>
+instructions.
+
+<h2>Author</h2>
+<p>
+Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
+questions, concerns, or suggestions regarding this module.
+
+<p>
+<hr><h2><a name="RedisListsSets">Redis Access Lists/Sets</a></h2>
+The <code>mod_wrap2_redis</code> module supports the "redis" string
+for the <em>source-type</em> parameter of the
+<a href="mod_wrap2.html#WrapUserTables"><code>WrapUserTables</code></a>,
+<a href="mod_wrap2.html#WrapGroupTables"><code>WrapGroupTables</code></a>,
+and
+<a href="mod_wrap2.html#WrapTables"><code>WrapTables</code></a>,
+configuration directives. If the "redis" <em>source-type</em> is used,
+then the <em>source-info</em> parameter must be as described below.  Note
+that support for Redis-based lists/sets <b>requires</b> the use of
+<code>mod_redis</code> (<i>e.g.</i> via the <code>--enable-redis</code>
+configure option).
+
+<p>
+<code>mod_wrap2_redis</code> requires only that the
+<a href="../modules/mod_redis.html"><code>mod_redis</code></a> module be
+enabled and configured to access a Redis server.
+
+<p>
+One Redis list (or set) is needed to retrieving access information from the
+<em>allow</em> "table", and another list/set for access information from the
+<em>deny</em> "table".  These lists/sets should contain a list of words,
+where each word is a host name, host address, pattern, or wildcard (see
+<a href="mod_wrap2.html#AccessRules">here</a> for how these things are
+defined).
+
+<p>
+Optionally, other Redis lists/sets can be defined to look up access
+<a href="mod_wrap2.html#AccessOptions">options</a> from the <em>allow</em> and
+<em>deny</em> tables.
+
+<p>
+For Redis lists, the format for the <code>WrapUserTables</code>,
+<code>WrapGroupTables</code>, and <code>WrapTables</code> directives is:
+<pre>
+  WrapTables redis:/<i><b>list:</b><code>allow-list-key</code></i>[/<i><b>list:</b><code>allow-list-options-key</code></i>] \
+    redis:<i><b>list:</b><code>deny-list-key</code></i>[/<i><b>list:</b><code>deny-list-options-key</code></i>]
+</pre>
+where the <i>allow-list-options-key</i> and <i>deny-list-options-key</i>
+portions of the string are optional.
+
+<p>
+For Redis <em>sets</em>, the format is similar to the above, only you specify
+a key prefix of "set:" rather than "list", <i>e.g.</i>:
+<pre>
+  WrapTables redis:/<i><b>set:</b><code>allow-set-key</code></i>[/<i><b>set:</b><code>allow-set-options-key</code></i>] \
+    redis:<i><b>set:</b><code>deny-set-key</code></i>[/<i><b>set:</b><code>deny-set-options-key</code></i>]
+</pre>
+
+<p>
+<b>Redis Access Lists/Sets Example</b><br>
+Here are example directives to help demonstrate how the <code>mod_redis</code>
+hooks are used by <code>mod_wrap2_redis</code>.
+These example directives assume the existence of two lists: a
+<code>wrapallow</code> list that defines allowed clients, and a
+<code>wrapdeny</code> list that defines the denied clients.
+
+<p>
+<pre>
+  # Using Redis lists
+  WrapTables redis:/list:wrapallow redis:/list:wrapdeny
+
+  # Using Redis sets
+  WrapTables redis:/set:wrapallow redis:/set:wrapdeny
+</pre>
+
+<p>
+For per-user/per-group lists/sets, the key name can use the <code>%{name}</code>
+variable, like so:
+<pre>
+  WrapUserTables * redis:/list:wrapallow.%{name} redis:/list:wrapdeny.%{name}
+  WrapGroupTables * redis:/list:wrapallow.%{name} redis:/list:wrapdeny.%{name}
+</pre>
+In the case of <code>WrapUserTables</code>, the <code>%{name}</code> variable
+will be resolved to the user name (from the <code>USER</code> command) of the
+logging-in user; for <code>WrapGroupTables</code>, <code>%{name}</code> will be
+resolved to the name of the user's primary group.
+
+<p>
+If the administrator wants to make use of access options, then URIs for those
+options would need to be similarly defined:
+<pre>
+  # Access tables for users (with options)
+  WrapUserTables user1,user2 \
+    redis:/list:allowed.%{name}/list:allowed-options.%{name} \
+    redis:/list:denied.%{name}/list:denied-options.%{name}
+
+  # Access tables for groups (with options)
+  WrapGroupTables group1,group2 \
+    redis:/list:allowed.%{name}/list:allowed-options.%{name} \
+    redis:/list:denied.%{name}/list:denied-options.%{name}
+
+  # Access tables for everyone else (without options)
+  WrapTables redis:/list:allowed redis:/list:denied
+</pre>
+
+<p>
+When constructing the client and options lists to return to
+<code>mod_wrap2</code>'s access control engine, <code>mod_wrap2_redis</code>
+will parse each returned item separately, handling both comma- and space-limited
+names in an item, into client list items.  This means that the administrator can
+store multiple client and option tokens in multiple items, as in the above
+schema, or the administrator can choose to store all of the clients and/or
+options in a single item, in an appropriately formatted string.
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 TJ Saunders<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/contrib/mod_wrap2_sql.html b/doc/contrib/mod_wrap2_sql.html
index 8fd0258..ab7486b 100644
--- a/doc/contrib/mod_wrap2_sql.html
+++ b/doc/contrib/mod_wrap2_sql.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_wrap2_sql.html,v 1.3 2013-05-12 23:13:43 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/contrib/mod_wrap2_sql.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_wrap2_sql</title>
@@ -14,15 +12,16 @@
 </center>
 <hr>
 
+<p>
+This submodule provides the SQL database "driver" for storing
+IP/DNS-based access control information in SQL tables.
+
+<p>
 This <code>mod_wrap2</code> submodule is contained in the
 <code>mod_wrap2_sql.c</code> file, and is not compiled by default.  See the
 <code>mod_wrap2</code> <a href="mod_wrap2.html#Installation">installation</a>
 instructions.
 
-<p>
-This submodule provides the SQL database "driver" for storing
-IP/DNS-based access control information in SQL tables.
-
 <h2>Author</h2>
 <p>
 Please contact TJ Saunders <tj <i>at</i> castaglia.org> with any
@@ -86,7 +85,7 @@ optional.
 <p>
 <b>SQL Access Tables Example</b><br>
 Here are example <code>SQLNamedQuery</code> directives to help demonstrate
-how the <code>mod_sql</code> hooks are used by <code>mod_wrap2</code>.
+how the <code>mod_sql</code> hooks are used by <code>mod_wrap2_sql</code>.
 These example SQL statements assume the existence of two tables: a
 <code>wrapallow</code> table that defines allowed clients, and a
 <code>wrapdeny</code> table that defines the denied clients.
@@ -94,6 +93,9 @@ These example SQL statements assume the existence of two tables: a
 <pre>
   SQLNamedQuery get-allowed-clients SELECT "allowed FROM wrapallow WHERE name = '%{0}'"
   SQLNamedQuery get-denied-clients SELECT "denied FROM wrapdeny WHERE name = '%{0}'"
+  ...
+  SQLNamedQuery get-all-allowed-clients SELECT "allowed FROM wrapallow"
+  SQLNamedQuery get-all-denied-clients SELET "denied FROM wrapdeny"
 </pre>
 These define the SQL statements to return the required list of words. The
 <code>%{0}</code> meta sequence will be substituted with the name being looked
@@ -101,6 +103,7 @@ up (<i>e.g.</i> user name for <code>WrapUserTables</code>, primary group name
 for <code>WrapGroupTables</code>, or the empty string for
 <code>WrapTables</code>).
 
+<p>
 If the administrator wants to make use of access options, then queries for
 those options would need to be similarly defined:
 <pre>
@@ -123,8 +126,10 @@ be:
   WrapGroupTables group1,group2 sql:/get-allowed-clients/get-allowed-options \
     sql:/get-denied-clients/get-denied-options
 
-  # Access tables for everyone else (without options)
-  WrapTables sql:/get-allowed-clients sql:/get-denied-clients
+  # Access tables for everyone else (without options).  Note that these
+  # query names are different, since these tables are global, not
+  # per-user/group.
+  WrapTables sql:/get-all-allowed-clients sql:/get-all-denied-clients
 </pre>
 One thing to keep in mind, however, is that the <code>%{0}</code> part of an
 SQL query will only be expanded with the client's <code>USER</code> argument
@@ -138,29 +143,59 @@ often be easily reused.
 <b>Example Schema</b><br>
 Here are some example table schema for SQL-based access tables:
 <ul>
-  <li><b><i>Allow table</i></b><br>
+  <li><b><i>Per-User/Group Allow Table</i></b><br>
 <pre>
   CREATE TABLE wrapallow (
-    name VARCHAR(30),
+    name VARCHAR(64) PRIMARY KEY,
     allowed VARCHAR(255) NOT NULL,
     options VARCHAR(255)
   );
-
-  CREATE INDEX idx_wrapallow_name ON wrapallow.name;
 </pre>
   </li>
-  <br>
 
-  <li><b><i>Deny table</i></b><br>
+  <p>
+  <li><b><i>Per-User/Group Deny Table</i></b><br>
 <pre>
   CREATE TABLE wrapdeny (
-    name VARCHAR(30),
+    name VARCHAR(64) PRIMARY KEY,
     denied VARCHAR(255) NOT NULL,
     options VARCHAR(255)
   );
+</pre>
+  </li>
 
-  CREATE INDEX idx_wrapdeny_name ON wrapdeny.name;
+  <p>
+  <li><b><i>Per-IP Allow Table</i></b><br>
+<pre>
+  CREATE TABLE wrapallowip (
+    allowed VARCHAR(128) PRIMARY KEY
+  );
+</pre>
+  The idea here is that the <code>allowed</code> column would contain the IP
+  address of the client to be allowed; one row per IP address.  The
+  <code>SQLNamedQuery</code> for this would then be:
+<pre>
+  SQLNamedQuery get-allowed-client-ip SELECT "allowed FROM wrapallowip WHERE allowed = '%a'"
+</pre>
+  Yes, this looks odd, to be returning the value that is used for the lookup,
+  but this interface is necessary due to the <code>mod_wrap2</code> engine.
+  </li>
+
+  <p>
+  <li><b><i>Per-IP Deny Table</i></b><br>
+<pre>
+  CREATE TABLE wrapdenyip (
+    denied VARCHAR(128) PRIMARY KEY
+  );
+</pre>
+  The idea here is that the <code>denied</code> column would contain the IP
+  address of the client to be denied; one row per IP address.  The
+  <code>SQLNamedQuery</code> for this would then be:
+<pre>
+  SQLNamedQuery get-denied-client-ip SELECT "denied FROM wrapdenyip WHERE denied = '%a'"
 </pre>
+  Yes, this looks odd, to be returning the value that is used for the lookup,
+  but this interface is necessary due to the <code>mod_wrap2</code> engine.
   </li>
 </ul>
 
@@ -175,17 +210,11 @@ options in a single row, in an appropriately formatted string.
 
 <p>
 <hr>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-05-12 23:13:43 $</i><br>
-
-<br><hr>
-
 <font size=2><b><i>
-© Copyright 2000-2013 TJ Saunders<br>
+© Copyright 2000-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
+<hr>
 
-<hr><br>
 </body>
 </html>
-
diff --git a/doc/howto/ASCII.html b/doc/howto/ASCII.html
index 04507cc..72858a7 100644
--- a/doc/howto/ASCII.html
+++ b/doc/howto/ASCII.html
@@ -1,15 +1,13 @@
-<!-- $Id: ASCII.html,v 1.2 2009-03-20 16:58:34 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/ASCII.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ASCII Transfers</title>
+<title>ProFTPD: ASCII Transfers</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and ASCII Transfers</b></h2></center>
+<center><h2><b>ProFTPD: ASCII Transfers</b></h2></center>
 <hr>
 
 <p>
@@ -81,7 +79,11 @@ a wrong answer.
 
 <p>
 <hr>
-<i>$Date: 2009-03-20 16:58:34 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/AWS.html b/doc/howto/AWS.html
new file mode 100644
index 0000000..e19a12b
--- /dev/null
+++ b/doc/howto/AWS.html
@@ -0,0 +1,532 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD: AWS</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center><h2><b>AWS and ProFTPD</b></h2></center>
+<hr>
+
+<p>
+So you want to run ProFTPD on an AWS
+<a href="https://aws.amazon.com/ec2/">EC2</a> instance?  Due to FTP's nature as
+a multi-connection protocol, it is not as straightforward to use FTP within AWS
+EC2, but it <b>can be done</b>.  Read on to find out how.  <i>Note</i> that the
+following documentation assumes that you know how to install and configure
+ProFTPD already.
+
+If you are only running individual FTP servers, then the sections on
+<a href="#SGS">AWS security groups</a> and <a href="#Addresses">addresses</a>
+are relevant.  If you want to provide a "scalable" <em>pool/cluster</em> of
+FTP servers, then the <a href="#ELBs">AWS Elastic Load Balancing</a> and
+<a href="#Route53">AWS Route53</a> sections will also be of interest.
+
+<p><a name="SGs">
+<b>Security Groups</b><br>
+Every EC2 instance belongs to one or more AWS <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html">Security Groups</a>
+(often abbreviated as simply "SGs").  As the AWS documentation states, a
+"security group" is a effectively a set of firewall rules controlling network
+access to your EC2 instance.  I tend to think of SGs more like NAT rules,
+since the "firewall" is the EC2 network perimeter managed by Amazon, and
+an SG dictates what holes to allow from the outside world into the EC2 internal
+networks.
+
+<p>
+Clients wishing to make a connection to the <code>proftpd</code> running on
+your EC2 instance, be it FTP, FTPS, SFTP, or SCP, will thus need to be allowed
+to connect by one (or more) of your SGs.  Assuming your <code>proftpd</code>
+listens on the standard FTP control port (21), you would configure one of your
+SGs to allow access to that port, from any IP address, using the AWS CLI like
+so:
+<pre>
+  $ aws ec2 authorize-security-group-ingress \
+    --group-id <em>sg-XXXX</em> \
+    --protocol tcp \
+    --port 21 \
+    --cidr 0.0.0.0/0
+</pre>
+<b>Note</b> that you do <b>not</b> need to allow access to port 20!  Many,
+many sites/howtos recommend opening port 20 in addition to port 21 for FTP
+access, but it <i>simply not needed</i>.  For <i>active</i> data transfers
+(<i>i.e.</i> where the FTP server <i>actively</i> connects back to the
+client machine for the data transfer), the <i>source</i> port will be port 20.
+But <i>incoming</i> connections for FTP will <b>never</b> be to port 20.
+
+<p>
+If you are allowing SFTP/SCP connections, <i>e.g.</i> to your
+<code>proftpd</code>, running the <a href="http://www.proftpd.org/docs/contrib/mod_sftp.html"><code>mod_sftp</code></a> module on the standard SSH port (22):
+<pre>
+  $ aws ec2 authorize-security-group-ingress \
+    --group-id <em>sg-YYYY</em> \
+    --protocol tcp \
+    --port 22 \
+    --cidr 0.0.0.0/0
+</pre>
+<b>Note</b>: I recommend using different SGs for your FTP/FTPS rules and your
+SFTP/SCP rules.  FTP/FTPS rules are more complex, and it is more clear to
+manage an SG named "FTP", with all of the related FTP rules, and separately
+to have an SG named "SFTP", with the SFTP/SCP related rules.
+
+<p>
+If you are <em>only</em> allowing SFTP/SCP access, that should suffice for
+the security group configuration for your instance. Allowing FTP/FTPS
+connections requires more security group tweaks.
+
+<p>
+FTP uses multiple TCP connections: one for the control connection, and separate
+<em>other</em> connections for data transfers (directory listings and file
+uploads/downloads).  The ports used for these data connections are dynamically
+negotiated over the control connection; it is this dynamic nature of the
+data connections which causes complexity with network access rules.  This
+site does a great job of describing these issues more in detail:
+<pre>
+  <a href="http://slacksite.com/other/ftp.html">http://slacksite.com/other/ftp.html</a>
+</pre>
+Remember how I said that SGs are similar to NAT rules?  This similarity is
+one of the reasons why the ProFTPD <a href="http://www.proftpd.org/docs/howto/NAT.html">NAT</a> howto is relevant here as well.
+
+<p>
+We want to configure ProFTPD to use a known range of ports for its passive
+data transfers, and then we want to configure our FTP SG to allow access to
+that known port range.  Thus we would use something like this in the
+<code>proftpd.conf</code>:
+<pre>
+  PassivePorts 60000 65535
+</pre>
+And then, to configure the SG to allow those ports:
+<pre>
+  $ aws ec2 authorize-security-group-ingress \
+    --group-id <em>sg-XXXX</em> \
+    --protocol tcp \
+    --port 60000-65534 \
+    --cidr 0.0.0.0/0
+</pre>
+The SFTP/SCP protocols only use a single TCP connection, and thus they do not
+require any other special configuration/access rules.
+
+<p><a name="Addresses">
+<b>Public <i>vs</i> Private Instance Addresses</b><br>
+Every EC2 instance with have its own local/private IP address and DNS name,
+automatically assigned by AWS.  Instances <i>may</i> also be automatically
+assigned <i>public</i> IP addresses/DNS names as well, depending on various
+factors.  The AWS docs on <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-instance-addressing.html">instance addressing</a> discuss those
+factors in greater detail.
+
+<p>
+If your EC2 instance will be supporting FTP/FTPS sessions, then you will need
+to determine whether your instance has a public address.  If so, that address
+needs to be configured using the <a href="http://www.proftpd.org/docs/modules/mod_core.html#MasqueradeAddress"><code>MasqueradeAddress</code></a> directive.
+Why?  When an FTP client negotiates a
+<a href="http://slacksite.com/other/ftp.html">passive data transfer</a>,
+ProFTPD tells that FTP client an address, and a port, to which to connect to
+transfer the data.  For EC2 instances with a public address, that public
+address is what ProFTPD needs to convey to the FTP client, and the
+<code>MasqueradeAddress</code> is the directive that does so.
+
+<p>
+So how can you tell what the public address of your EC2 instance is, if it
+even has one?  You can use the EC2 <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html">instance metadata</a>, via
+<code>curl</code>, like so:
+<pre>
+  $ curl http://169.254.169.254/latest/meta-data/public-hostname
+</pre>
+<b>If</b> your instance has a public address, the DNS name to use would be
+returned.  Otherwise, you might see something like this:
+<pre>
+  $ curl http://169.254.169.254/latest/meta-data/public-hostname
+  <?xml version="1.0" encoding="iso-8859-1"?>
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+   <head>
+    <title>404 - Not Found</title>
+   </head>
+   <body>
+    <h1>404 - Not Found</h1>
+   </body>
+  </html>
+</pre>
+which indicates that your EC2 instances does <b>not</b> have a public address.
+And if your instance does not <em>have</em> a public address, then you do
+<em>not</em> need to use the <code>MasqueradeAddress</code> directive.
+
+<p>
+Here's one solution for handling this situation: obtain the public hostname
+for your instance, store it in an environment variable, and then use that
+environment variable in your <code>proftpd.conf</code>:
+<pre>
+  $ export EC2_PUBLIC_HOSTNAME=`curl -f -s http://169.254.169.254/latest/meta-data/public-hostname`
+</pre>
+The <code>-f</code> option is necessary, in case the instance does not
+<i>have</i> a public address.  The <code>-s</code> option simply makes for
+quieter shell scripts.  Then, in your <code>proftpd.conf</code>, you might
+use:
+<pre>
+  MasqueradeAddress %{env:EC2_PUBLIC_HOSTNAME}
+</pre>
+If the instance does not have a public address, though, that environment
+variable will be the empty string, and <code>proftpd</code> will fail to start
+up because of that.  Better would be to automatically handle the
+"no public address" case, if we can.  Assume you have a shell script for
+starting <code>proftpd</code> which does something like this, using our
+<code>EC2_PUBLIC_HOSTNAME</code> environment variable:
+<pre>
+  PROFTPD_ARGS=""
+
+  # If we have a public hostname, then the string will not be
+  # zero length, and we define a property for ProFTPD's use.
+  if [ ! -z "$EC2_PUBLIC_HOSTNAME" ]; then
+    PROFTPD_ARGS="$PROFTPD_ARGS -DUSE_MASQ_ADDR"
+  fi
+</pre>
+Then, in your <code>proftpd.conf</code>, you use <i>both</i> that property
+<i>and</i> the environment variable notation:
+<pre>
+  <IfDefined USE_MASQ_ADDR>
+    MasqueradeAddress %{env:EC2_PUBLIC_HOSTNAME}
+  </IfDefined>
+</pre>
+
+<p>
+Fortunately the EC2 instance addressing does not require any additional
+changes/tweaks to the AWS Security Groups.
+
+<p><a name="ELBs">
+<b>Elastic Load Balancing</b><br>
+Now that you have ProFTPD up and running on your EC2 instance, and you can
+connect using FTP/FTPS and SFTP/SCP, and browse directories and upload and
+download files, you are probably thinking about how to have more than one
+instance for your FTP service.  After all, you want redundancy for your FTP
+servers just like you have for your HTTP servers, right?  And for HTTP servers,
+you would use an AWS <a href="http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elastic-load-balancing.html">Elastic Load Balancer</a>
+(often called an "ELB").  Why not use the same technique for FTP?  <i>Can</i>
+you configure an ELB for FTP?
+
+<p>
+Yes, ELBs can be used for FTP.  Like SGs, though, it's complicated by FTP's
+use of multiple TCP connections; for SFTP/SCP, ELBs are simpler to configure.
+
+<p>
+The first thing to keep in mind is that ELBs only distribute (<i>i.e.</i>
+"balance") connections in a <a href="http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/how-elb-works.html">round-robin fashion</a> among
+the backend TCP servers; they do <b>not</b> distribute connections based on the
+<em>load</em> of those backend servers.  (The balancing algorithm is slightly
+different for HTTP servers, but that does not apply to ProFTPD.)  This means
+that <em>any</em> user might connect to <em>any</em> of your ProFTPD instances;
+this, in turn, means that users must be able to login on all instances,
+<b>and</b> that the files for all users should be available on all instances.
+These requirements lead to the requirements for centralized/shared
+authentication data, and for shared filesystems.  The centralized/shared
+authentication data can be handled by using <i>e.g.</i> SQL databases,
+LDAP directories, or even synchronized password files.  For shared filesystems,
+the popular approaches are:
+<ul>
+  <li><a href="https://github.com/s3fs-fuse/s3fs-fuse">s3fs</a>
+  <li>NFS
+  <li>Samba
+  <li>Gluster
+  <li><a href="https://aws.amazon.com/efs/">AWS EFS</a>
+</ul>
+There are probably other solutions as well; the key is to have the users'
+files available on any/every instance.
+
+<p>
+The next thing to keep in mind is whether you have an EC2 Classic account,
+or whether you are using AWS <a href="https://aws.amazon.com/vpc/">VPC</a>.
+Chances are that you are using a VPC.  ELBs for an EC2 Classic account can
+only be configured to listen on a <a href="http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-listener-config.html">restricted list</a>
+of ports, <i>i.e.</i>:
+<ul>
+  <li>25 (SMTP)
+  <li>80 (HTTP)
+  <li>443 (HTTPS)
+  <li>465 (SMTPS)
+  <li>587 (SMTP mail submission)
+  <li>1024-65535
+</ul>
+This means that if you have an EC2 Classic account <b>and</b> want to use
+an ELB for your FTP/SFTP servers, you will need to run those servers on
+non-standard ports, in the 1024-65535 range.  ELBs within a VPC, on the
+other hand, can listen on <b>any</b> port.
+
+<p>
+Let's assume that you are using a VPC, and thus you configure a TCP listener
+on your ELB for port 21, which uses the instance port 21.  And for SFTP/SCP,
+it would be a TCP listener for port 22, using instance port 22.  Obviously
+you would not use HTTP or HTTPS listeners, but what about an SSL listener,
+for FTPS?  No.  An SSL listener performs the SSL/TLS handshake <i>first</i>,
+then forwards the plaintext messages to the backend instance.  But FTPS is
+a <a href="https://en.wikipedia.org/wiki/STARTTLS">"STARTTLS"</a> protocol,
+which means the connection is <i>first</i> unencrypted, and then feature
+negotiation happens on that connection, and <i>then</i> the SSL/TLS handshake
+happens.  ELBs do not support STARTTLS protocols, thus you cannot use them
+for terminating SSL/TLS sessions for FTP servers.
+
+<p>
+Your ProFTPD configuration might use multiple different ports, for different
+<code><VirtualHost></code>s.  Your ELB would need a different TCP
+listener for each of those separate ports.  However, now that ProFTPD supports
+the FTP <code>HOST</code> command (which allows for proper name-based virtual
+hosts in FTP, just like HTTP 1.1 has via its <code>Host</code> header), you
+<i>should</i> only need on TCP listener now.
+
+<p>
+An ELB wants to perform health checks on its backend instances, to know that
+that instance is up, running, and available to handle connections.  ELBs
+can perform HTTP requests as healthchecks, or make TCP connections.  ProFTPD
+is not an HTTP server, so using TCP health checks is necessary.  You would
+configure the ELB to make TCP connections to ProFTPD port, <i>e.g.</i> port
+21 for FTP/FTPS, and/or port 22 for SFTP/SCP.
+
+<p>
+What about the range of ports defined via <code>PassivePorts</code>, that
+you had to allow in your SG?  Does your ELB need TCP listeners for all of
+those ports, too?  No.  To understand why, we need to examine in detail just
+how passive data transfers work in FTP.  An FTP client connects to your
+FTP server, through the ELB, like this, for its control connection:
+<pre>
+     client --- <i>ctrl</i> ---> ELB:21 --- <i>ctrl</i> ---> instance:21
+</pre>
+The client and server negotiate a passive data transfer; the FTP server
+tells the client, over the control connection, an address and port to which
+to connect.  Now, let's assume that ProFTPD gives the address of the ELB,
+and one of the <code>PassivePorts</code>; we'l use port 65000 for this example.
+The FTP client connects to the address/port <em>on the ELB</em>, like this:
+<pre>
+     client --- <i>data</i> ---> ELB:65000 --- <i>data</i> ---> instance:65000
+</pre>
+This would mean that the ELB <i>would</i> need TCP listeners for the
+<code>PassivePorts</code>, <i>and</i> that <code>MasqueradeAddress</code> would
+need to point to the ELB DNS name.  So why did I say that the ELB did not
+need those extra TCP listeners?
+
+<p>
+<b>If</b> your ELB will only ever have <b>just one backend instance</b>, then
+the above configuration would work.  Your EC2 instance might be in a VPC,
+with no public address, and thus perhaps the <i>only</i> way to make your
+FTP server there reachable <i>is</i> using an ELB.  Where forcing passive
+data connections through an ELB starts to fail is when there are <i>multiple
+backend instances</i>.  Consider the case where your ELB might have 3 instances:
+<pre>
+              +--> instance1:21
+     ELB:21 --|--> instance2:21
+              +--> instance3:21
+</pre>
+An FTP client connects to the ELB, and the ELB selects instance #2:
+<pre>
+     client --- <i>ctrl</i> ---> ELB:21 --- <i>ctrl</i> ---> instance2:21
+</pre>
+So far, so good.  The client requests a passive data transfer; the FTP server
+tells the client to connect to the ELB address, port 65000, <b>but</b> the
+ELB sends that connection to instance <i>#3</i>, <b>not</b> instance #2:
+<pre>
+     client --- <i>data</i> ---> ELB:65000 --- <i>data</i> ---> instance3:65000
+</pre>
+This can happen because the ELB does not understand FTP; it does not know
+that the data connection is related, in any way, to any other connections.  To
+the ELB, all TCP connections are independent, and thus any connection will be
+routed, round-robin, to any backend instance.  There is no guarantee that the
+data connections, going through the ELB, will connect to the proper backend
+instance.  If there is only <b>one backend instance</b>, though, everything
+will work as expected.
+
+<p>
+In order to properly support multiple backend instances (which is one of the
+goals/benefits of using an ELB in the first place) for FTP, then, the trick
+is to <b>not</b> force data connections through the ELB.  Instead, the
+<code>MasqueradeAddress</code> directive points to each backend instance's
+respective public hostname.  With this configuration, the FTP client connects
+to the ELB for its control connection, like usual:
+<pre>
+     client --- <i>ctrl</i> ---> ELB:21 --- <i>ctrl</i> ---> instance2:21
+</pre>
+And for the data transfer, ProFTPD tells the client the instance public
+hostname, and port 65000:
+<pre>
+     client -------------- <i>data</i> -------------> instance2:65000
+</pre>
+Notice how, with this configuration, the TCP connection for the data transfer
+bypasses the ELB completely.  <i>This</i> is why you do not need to configure
+any TCP listeners on the ELB for those <code>PassivePorts</code>, and why
+you do <b>not</b> want <code>MasqueradeAddress</code> using the ELB DNS name;
+you do not want passive data connections going through the ELB.
+
+<p>
+Now you have an ELB with multiple backend FTP servers.  Success, right?  Maybe.
+There <i>are</i> some caveats.  FTP clients might notice that they connect to
+one name (the ELB DNS name), <i>but</i> for data transfers, they are being told
+(by the FTP server) to connect to a <i>different</i> name; some FTP clients
+might warn/complain about this mismatch.  ProFTPD would definitely complain
+about this mismatch, for it would see the control connection as originating
+from the ELB, but the data connection originating from a different address,
+and would refuse the data transfer.  To allow data transfers to work, then,
+you would need to add the following to your <code>proftpd.conf</code>:
+<pre>
+  # Allow "site-to-site" transfers, since that is what FTP traffic with
+  # an ELB looks like.
+  AllowForeignAddress on
+</pre>
+which has its own <a href="http://www.proftpd.org/docs/howto/FXP.html">security implications</a>.
+
+<p>
+Next, there is the ELB <a href="http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/config-idle-timeout.html">idle timeout</a> setting to
+adjust.  The default is 60 seconds.  During a data transfer, most FTP clients
+will be handling the data connection, and the control connection is idle.
+Thus if the data transfer lasts longer than 60 seconds, the ELB might terminate
+the idle control connection, and the FTP session is lost.  Unfortunately the
+maximum allowed idle timeout for ELBs is 1 hour (3600 seconds); for <i>large</i>
+(or slow) data transfers, even that timeout could be a problem.  There are
+ways of keeping the control connection from being idle for too long, using
+<a href="http://www.proftpd.org/docs/howto/KeepAlives.html">keepalives</a>.
+<b>Note</b> that this idle timeout is not really an issue for SFTP/SCP
+sessions, as all data transfers for them use the same single TCP connection.
+
+<p>
+Last, using an ELB only for FTP control connections, and using direct
+connections for the FTP data transfers only works if your backend EC2 instances
+<i>have</i> public hostnames; for instances in a VPC, that may not be true.
+So how can we use an ELB for multiple backend instances that only have private
+addresses?  Sadly, the answer is: you can't.  For load balancing FTP sessions
+among multiple backend EC2 instances with private addresses, you need an
+FTP-aware proxy, such as ProFTPD with the <a href="https://github.com/Castaglia/proftpd-mod_proxy"><code>mod_proxy</code></a> module.  This means running your
+own instance for doing that load balancing, rather than having AWS manage it.
+Of course, if the clients using your ELB for FTP services are <i>also</i>
+within your VPC, then the lack of public hostnames for your EC2 instances
+is not an issue, and using an ELB as described above will work.
+
+<p><a name="Route53">
+<b>DNS and AWS Route53</b><br>
+Using an ELB for balancing connections across your pool of FTP servers is
+rather complex.  Are there alternatives?  Yes: "DNS load balancing".
+
+<p>
+Instead of using an AWS ELB for balancing/distributing connections across
+your pool of ProFTPD-running instances, you can use DNS tricks to implement
+the same functionality.  Note, however, these DNS tricks still assume that
+your EC2 instances are publicly reachable, <i>i.e.</i> have public hostnames.
+
+<p>
+With DNS load balancing, the client resolves a DNS name to an IP address,
+and connects to that IP address:
+<pre>
+     client1 ----------------- <i>ctrl</i> ----------------> instance2:21
+     client1 ----------------- <i>data</i> ----------------> instance2:65000
+</pre>
+But the DNS server might be configured with <i>several</i> IP addresses for
+the same DNS name; the client then chooses <i>one</i> IP address from the given
+list (usually the first address), and connects to that.  Some DNS servers will
+<i>shuffle</i> the list of returned addresses for a name, so that clients will
+choose different addresses, and thus distribute/balance their connections
+across all of the addresses:
+<pre>
+     client1 ----------------- <i>ctrl</i> ----------------> instance2:21
+     client1 ----------------- <i>data</i> ----------------> instance2:65000
+
+     client2 ----------------- <i>ctrl</i> ----------------> instance1:21
+     client2 ----------------- <i>data</i> ----------------> instance1:65000
+
+     client3 ----------------- <i>ctrl</i> ----------------> instance3:21
+     client3 ----------------- <i>data</i> ----------------> instance3:65000
+</pre>
+
+<p>
+Within AWS, the <a href="https://aws.amazon.com/route53/">Route53</a> service
+can be used as the DNS service for your domain names.  AWS Route53 calls this round robin of addresses a <a href="http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-weighted">weighted</a> routing
+policy, as each address associated with a name can be given a "weight",
+affecting the probability that that address will be returned, by Route53,
+when the DNS name is resolved to an IP address.  Other routing policies are
+supported, <i>e.g.</i> <a href="http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-latency">latency-based</a> routing
+(so that the instance with the fastest response time is chosen), and
+<a href="http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-geo">geolocation-based</a> routing (the instance address
+chosen is based on the location of the resolving client).
+
+<p>
+If you are using AWS Route53, then you will need to configure health checks,
+just as you would for an ELB.  Route53 supports TCP health checks, which
+you would point at your FTP/FTPS port (21) or SFTP/SCP port (22) on your
+instances.
+
+<p>
+Since any/all clients could connect to any/all of the EC2 instances associated
+with your DNS name, all of the users would need to be able to login on any
+instance, and have their files/data available.  Thus using a shared filesystem
+for the files (such as <a href="https://github.com/s3fs-fuse/s3fs-fuse">s3fs</a>, NFS, Samba, gluster, <i>etc</i>) and a centralized/shared authentication
+mechanism (<i>e.g.</i> SQL database, LDAP directory, <i>etc</i>) would be
+needed.
+
+<p><a name="FutureWork">
+<b>Future Work</b><br>
+In order to automate much of the above manual steps, work is progressing on
+a <a href="https://github.com/Castaglia/proftpd-mod_aws/"><code>mod_aws</code></a> module for ProFTPD, which will eventually:
+<ul>
+  <li>automatically set <code>PassivePorts</code> for FTP/FTPS vhost, if needed
+  <li>automatically set <code>MasqueradeAddress</code> if needed
+  <li>automatically adjust Security Group rules for FTP/FTPS, SFTP/SCP
+</ul>
+in addition to other interactions with AWS services.
+
+<p><a name="FAQ">
+<b>Frequently Asked Questions</b><br>
+<p><a name="PerUserLoadBalancing">
+<font color=red>Question</font>: I need to send particular users only to
+a particular instance/set of instances.  How do I configure AWS to do this?<br>
+<font color=blue>Answer</font>: Short answer: you cannot.  But it <b>can</b>
+be done!
+
+<p>
+The AWS services like ELBs and Route53 understand TCP connections, and the
+HTTP protocol, but they do not understand FTP.  And understanding of the
+protocol is necessary, so that you know how/when to expect the user name, and
+how to redirect/proxy the backend connection.  This is why you cannot use
+<i>AWS</i> to do per-user balancing.  However, you <i>can</i> use the
+<a href="https://github.com/Castaglia/proftpd-mod_proxy"><code>mod_proxy</code></a> module for ProFTPD, which <i>is</i> protocol-aware, and thus can balance
+FTP/FTPS connections in multiple ways, including per-user.
+
+<p><a name="XForwardedFor">
+<font color=red>Question</font>: I am using ELBs for my pool of ProFTPD
+servers.  I would like my logs to show the IP address of the connecting
+clients, but all I get is the IP adress of the ELB.  Is there a way to get
+the original IP address, an equivalent to the <code>X-Forwarded-For</code>
+HTTP header?<br>
+<font color=blue>Answer</font>:  Yes, there is an equivalent mechanism
+that is supported by ELBs for TCP listeners: the <a href="http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt">PROXY protocol</a>.
+
+<p>
+To enable use of the <code>PROXY</code> protocol by your ELB, see <a href="http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/enable-proxy-protocol.html">here</a>.  You will <b>also</b> need to tell ProFTPD to expect
+the <code>PROXY</code> protocol, which means using the <a href="https://github.com/Castaglia/proftpd-mod_proxy_protocol"><code>mod_proxy_protocol</code></a>
+module.
+
+<p>
+The <code>PROXY</code> protocol, and the <code>mod_proxy_protocol</code> module,
+work equally well for FTP/FTPS and SFTP/SCP sessions.
+
+<p><a name="PerInstanceFirewall">
+<font color=red>Question</font>: Should I run a firewall on my instance as well?<br>
+<font color=blue>Answer</font>: It is considered a good network security
+practice to do so, as it provides security in depth.  <b>However</b>, care
+must be taken with those firewall rules; they need to allow the same ports/
+addresses as your SGs.  (Also note that local/instance firewall rules CANNOT
+be applied to the connecting client's IP address when connecting through ELB.)
+
+<!--
+iptables examples from SO
+
+IPv6
+  VPC instance support, Route53, ELB
+
+most common: transfers fail/can't connect/can't list directory/can't upload/download
+-->
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/howto/AuthFiles.html b/doc/howto/AuthFiles.html
index 3dec3e9..60fb8cf 100644
--- a/doc/howto/AuthFiles.html
+++ b/doc/howto/AuthFiles.html
@@ -1,15 +1,13 @@
-<!-- $Id: AuthFiles.html,v 1.1 2006-01-11 13:55:37 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/AuthFiles.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Using AuthUserFiles</title>
+<title>ProFTPD: Using AuthUserFiles</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Using <code>AuthUserFile</code>s</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Using <code>AuthUserFile</code>s</i></b></h2></center>
 <hr>
 
 <p>
@@ -46,12 +44,11 @@ Any configured <code>AuthUserFile</code> is used <b>in addition to</b>
 <code>/etc/passwd</code>, not <b>instead of</b> it (prior to 1.2.8rc1, however,
 an <code>AuthUserFile</code> was used instead of <code>/etc/passwd</code>);
 similarly for <code>AuthGroupFile</code> and <code>/etc/group</code>.  The
-<a href="http://www.castaglia.org/proftpd/doc/contrib/ProFTPD-mini-HOWTO-Authentication.html#order"><code>AuthOrder</code></a> directive can be used if
-you want <code>proftpd</code> to use only the <code>AuthUserFile</code>. The
-format of an <code>AuthUserFile</code> is the same as <code>/etc/passwd</code>
-(<code>man passwd(5)</code>), and the format of an <code>AuthGroupFile</code>
-is the same as <code>/etc/group</code> (<code>man group(5)</code>).  There is
-an <a href="http://www.castaglia.org/proftpd/contrib/ftpasswd.html"><code>ftpasswd</code></a> script available that can be used to create and update
+<a href="Authentication.html#order"><code>AuthOrder</code></a> directive can be used if you want <code>proftpd</code> to use only the <code>AuthUserFile</code>.
+The format of an <code>AuthUserFile</code> is the same as
+<code>/etc/passwd</code> (<code>man passwd(5)</code>), and the format of an
+<code>AuthGroupFile</code> is the same as <code>/etc/group</code> (<code>man
+group(5)</code>).  There is an <a href="../contrib/ftpasswd.html"><code>ftpasswd</code></a> script available that can be used to create and update
 these files.
 
 <p>
@@ -71,7 +68,8 @@ and that <code>AuthGroupFile</code>s have this format:
 <pre>
   groupname:grouppasswd:gid:member1,member2,...member<i>N</i>
 </pre>
-The <a href="http://www.castaglia.org/proftpd/contrib/ftpasswd.html"><code>ftpasswd</code></a> script mentioned creates files in these required formats.
+The <a href="../contrib/ftpasswd.html"><code>ftpasswd</code></a> script
+mentioned creates files in these required formats.
 
 <p>
 <b>Choice of IDs</b><br>
@@ -180,5 +178,11 @@ can help in situations like this.
 
 <p>
 <hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </body>
 </html>
diff --git a/doc/howto/Authentication.html b/doc/howto/Authentication.html
index c82923a..2945082 100644
--- a/doc/howto/Authentication.html
+++ b/doc/howto/Authentication.html
@@ -1,15 +1,13 @@
-<!-- $Id: Authentication.html,v 1.11 2013-08-19 16:32:23 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Authentication.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Logins and Authentication</title>
+<title>ProFTPD: Logins and Authentication</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Logins and Authentication</b></h2></center>
+<center><h2><b>ProFTPD: Logins and Authentication</b></h2></center>
 <hr>
 
 <p>
@@ -296,7 +294,10 @@ code goes out of its way to ensure that the password is never logged.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-08-19 16:32:23 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/BCP.html b/doc/howto/BCP.html
index 6724f12..ffa4ce0 100644
--- a/doc/howto/BCP.html
+++ b/doc/howto/BCP.html
@@ -1,9 +1,7 @@
-<!-- $Id: BCP.html,v 1.2 2004-05-25 17:41:34 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/BCP.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Best Common Practices</title>
+<title>ProFTPD: Best Common Practices</title>
 </head>
 
 <body bgcolor=white>
@@ -96,19 +94,35 @@ There are also several third-party modules that can add various resource
 limiting abilities to a <code>proftpd</code> server:
 <ul>
   <li><a href="http://www.castaglia.org/proftpd/modules/mod_diskuse.html">mod_diskuse</a>
-  <li><a href="http://www.castaglia.org/proftpd/modules/mod_load.html">mod_load</a>
-  <li><a href="http://www.castaglia.org/proftpd/modules/mod_quotatab.html">mod_quotatab</a>
+  <li><a href="../contrib/mod_load.html">mod_load</a>
+  <li><a href="../contrib/mod_quotatab.html">mod_quotatab</a>
 </ul>
 
 <p><a name="AccessControls"></a>
 <b>Access Controls</b><br>
 ProFTPD provides a <a href="Limit.html"><code><Limit></code></a>
 directive for configuring fine-grained access controls that can be applied to
-logins as well as FTP commands.  The contributed <code>mod_wrap</code> module
+logins as well as FTP commands.  The contributed
+<a href="../contrib/mod_wrap.html"><code>mod_wrap</code></a> module
 allows a <code>proftpd</code> daemon to use the standard
 <code>/etc/hosts.allow</code> and <code>/etc/hosts.deny</code> access control
 files.  Of interest to some is the next generation of this module,
-<a href="http://www.castaglia.org/proftpd/modules/mod_wrap-2.0.html"><code>mod_wrap-2.0</code></a>, which allows for storing access control rules in SQL tables.
+<a href="../contrib/mod_wrap2.html"><code>mod_wrap2</code></a>, which allows
+for storing access control rules in SQL tables.
+
+<p>
+On systems which support the /proc filesystem, such as Linux and several others,
+it is <b>highly recommended</b> that the /proc filesystem be guarded, especially
+for non-chroot sessions.  Malicious clients can read or write to that filesystem
+and cause quite a bit of damage.  Thus we encourge administrators to use
+the following in their <code>proftpd.conf</code>:
+<pre>
+  <Directory /proc>
+    <Limit ALL>
+      DenyAll
+    </Limit>
+  </Directory>
+</pre>
 
 <p><a name="PerformanceTuning"></a>
 <b>Performance Tuning</b><br>
@@ -205,8 +219,8 @@ log files.
 If your <code>proftpd</code> server is on a high-bandwidth Internet link,
 it may benefit from tuning the size of <b>kernel</b>-level (as opposed
 to <b>application</b>-level) socket buffers used when transferring data.
-The <a href="http://www.proftpd.org/docs/directives/linked/config_ref_SocketOptions.html"><code>SocketOptions</code></a> configuration directive can be used
-to specify larger buffer sizes:
+The
+<a href="../modules/mod_core.html#SocketOptions"><code>SocketOptions</code></a> configuration directive can be used to specify larger buffer sizes:
 <pre>
   # The 'sndbuf' parameter tunes the size of the buffer used for sending
   # files to clients; the 'rcvbuf' tunes the buffer used for receiving
@@ -262,25 +276,29 @@ situation.  First, the daemon administrator can make sure that any
 after the <code>Include</code>, in the main <code>proftpd.conf</code>.
 
 <p>
-The <a href="http://www.castaglia.org/proftpd/modules/mod_auth_file.html"><code>mod_auth_file</code></a> module, now part of the core distribution, was
-developed specifically to provide finer control over the contents of
-<code>AuthUserFile</code> and <code>AuthGroupFile</code> files.  It does so by
-enhancing these configuration directives to support optional
-"filters" that restrict the UIDs, GIDs, user names, and/or home
-directories in such files.
+The <a href="../modules/mod_auth_file.html"><code>mod_auth_file</code></a>
+module, now part of the core distribution, was developed specifically to
+provide finer control over the contents of <code>AuthUserFile</code> and
+<code>AuthGroupFile</code> files.  It does so by enhancing these configuration
+directives to support optional "filters" that restrict the UIDs, GIDs,
+user names, and/or home directories in such files.
 
 <p><a name="Miscellaneous"></a>
 <b>Miscellaneous</b><br>
 For the security-wary administrator, there are a few more directives that
-may be of interest: <a href="http://www.proftpd.org/docs/directives/linked/config_ref_AnonRejectPasswords.html"><code>AnonRejectPasswords</code></a>,
+may be of interest: <a href="../modules/mod_auth.html#AnonRejectPasswords"><code>AnonRejectPasswords</code></a>,
 which configures a regular expression that matches and blocks scripted FTP
 clients that try to find and exploit ill-configured anonymous FTP sites, and
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_RootRevoke.html"><code>RootRevoke</code></a>, which causes <code>proftpd</code> to
-drop root privileges completely (read the description for details).
+<a href="../modules/mod_auth.html#RootRevoke"><code>RootRevoke</code></a>,
+which causes <code>proftpd</code> to drop root privileges completely (read the
+description for details).
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2004-05-25 17:41:34 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Chroot.html b/doc/howto/Chroot.html
index 8d0d6ad..c1fac73 100644
--- a/doc/howto/Chroot.html
+++ b/doc/howto/Chroot.html
@@ -1,9 +1,7 @@
-<!-- $Id: Chroot.html,v 1.9 2014-03-13 18:06:26 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Chroot.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Symlinks and chroot()</title>
+<title>ProFTPD: Symlinks and chroot()</title>
 </head>
 
 <body bgcolor=white>
@@ -20,7 +18,7 @@ way, "How can I put my users in a chroot jail?"  As a common
 question, it definitely has a place in the
 <a href="http://www.proftpd.org/docs/faq/linked/faq-ch5.html#AEN524">FAQ</a>.
 Many users, I fear, do not read the FAQ carefully, and so miss that section.
-The answer is ProFTPD's <a href="http://www.proftpd.org/docs/directives/linked/config_ref_DefaultRoot.html"><code>DefaultRoot</code></a> configuration
+The answer is ProFTPD's <a href="../modules/mod_auth.html#DefaultRoot"><code>DefaultRoot</code></a> configuration
 directive, which accomplishes this functionality by using the
 <code>chroot(2)</code> function.
 
@@ -291,14 +289,27 @@ used for the chroot.  It's always a good idea of have a "applies to everyone"
 <code>DefaultRoot</code> directive in your proftpd.conf, at the
 <i>end of the list</i> of <code>DefaultRoot</code>s, as a catch-all.
 
-<p><a name="Symlinks">
+<p><a name="SymlinksFAQ">
 <font color=red>Question</font>: Does <code>DefaultRoot</code> work properly if
 the path/home directory is a symlink?<br>
 <font color=blue>Answer</font>: Yes.
 
+<p>
+Note that some sites consider this a security risk; if that home directory
+can be deleted by remote users, and replaced with a symlink of their own
+creation (<i>e.g.</i> via SSH or some other webapp), this can be a problem.
+To help mitigate situations like this, you can use the <a href="../modules/mod_auth.html#AllowChrootSymlinks"><code>AllowChrootSymlinks</code></a> directive:
+<pre>
+  # Do not follow symlinks when chrooting
+  AllowChrootSymlinks off
+</pre>
+As stated in the documentation, using <code>AllowChrootSymlinks</code> does
+<b>not</b> prevent this problem entirely; it simply means that
+<i><code>proftpd</code></i> cannot be used to get around the restrictions.
+
 <p><a name="ChrootNotWorking">
 <font color=red>Question</font>: I have configured <code>DefaultRoot</code> in my proftpd.conf, but my clients still see the root directory.  Is it a bug?<br>
-<cont color=blue>Answer</font>.  Usually not.
+<font color=blue>Answer</font>.  Usually not.
 
 <p>
 First, make sure that you have restarted <code>proftpd</code>, so that the
@@ -325,7 +336,10 @@ edited, or maybe the <code>DefaultRoot</code> directive is not in a
 
 <p>
 <hr>
-Last Updated: $Date: 2014-03-13 18:06:26 $<br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Classes.html b/doc/howto/Classes.html
index 7e6dff5..567ae2f 100644
--- a/doc/howto/Classes.html
+++ b/doc/howto/Classes.html
@@ -1,15 +1,13 @@
-<!-- $Id: Classes.html,v 1.4 2008-06-10 16:29:07 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Classes.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Classes</title>
+<title>ProFTPD: Classes</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Classes</b></h2></center>
+<center><h2><b>ProFTPD: Classes</b></h2></center>
 <hr>
 
 <p>
@@ -110,7 +108,7 @@ To illustrate, the following class definition will never match:
     From 127.0.0.1
     From !127.0.0.1
     Satisfy all
-  &lt/Class>
+  </Class>
 </pre>
 It is impossible to both an address and <b>not</b> match that same address,
 but that is what is demanded by the "Satisfy all" setting in the above
@@ -124,7 +122,7 @@ rule with exceptions:
     From .domain.com
     From !host1.domain.com !host2.domain.com
     Satisfy all
-  &lt/Class>
+  </Class>
 </pre>
 Specifically, the use of "Satisfy all" is necessary when you have multiple
 <i>not</i> matches (<i>i.e.</i> using the <code>!</code> prefix), <i>all</i>
@@ -152,11 +150,12 @@ main directives to use, for example in <code><Limit></code> sections:
 </pre>
 
 <p>
-The <a href="http://www.castaglia.org/proftpd/modules/mod_ifsession.html"><code>mod_ifsession</code></a> module also makes use of classes with its
-<code><IfClass></code> configuration section.  Using classes and
-<code>mod_ifsession</code>, you can write a <code>proftpd.conf</code> that
-has specific configurations for specific classes of clients.  Here's an
-example snippet demonstrating use of <code><IfClass></code>:
+The <a href="../contrib/mod_ifsession.html"><code>mod_ifsession</code></a>
+module also makes use of classes with its <code><IfClass></code>
+configuration section.  Using classes and <code>mod_ifsession</code>, you can
+write a <code>proftpd.conf</code> that has specific configurations for specific
+classes of clients.  Here's an example snippet demonstrating use of
+<code><IfClass></code>:
 <pre>
   <IfClass internal>
     MaxClients 100
@@ -172,7 +171,10 @@ This allows clients from class "internal" to see an effective
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2008-06-10 16:29:07 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Compiling.html b/doc/howto/Compiling.html
index 8d285be..7d268ea 100644
--- a/doc/howto/Compiling.html
+++ b/doc/howto/Compiling.html
@@ -1,9 +1,7 @@
-<!-- $Id: Compiling.html,v 1.10 2013-10-06 16:02:23 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Compiling.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Compiling ProFTPD</title>
+<title>ProFTPD: Compiling ProFTPD</title>
 </head>
 
 <body bgcolor=white>
@@ -113,7 +111,7 @@ You can also change only portions of the layout, for just the binaries
 or the configuration file or other parts:
 
 <p>
-<table>
+<table summary="Configure Options">
   <tr>
     <td><code>--bindir</code></td>
     <td>Change <code><i>prefix-dir</i>/bin/</code></td>
@@ -266,7 +264,7 @@ about the more common of these feature options:
   </li>
 
   <p>
-  <li>--with-lastlog</code><em>=/path/to/lastlog</em><br>
+  <li><code>--with-lastlog</code><em>=/path/to/lastlog</em><br>
     Enables support for lastlog logging; see the <code>lastlog(8)</code>
     man page.  The optional <em>/path/to/lastlog</em> argument is only
     needed if your lastlog file location is not a standard location.
@@ -333,7 +331,7 @@ list of the module names you wish to add, as staticly linked modules.  Thus if
 you wanted to add LDAP and SSL/TLS support to your <code>proftpd</code>, you
 would list the appropriate modules in the <code>--with-modules</code> list:
 <pre>
-  # ./configure --with-modules=mod_ldap:mod_tls ...
+  $ ./configure --with-modules=mod_ldap:mod_tls ...
 </pre>
 Do <b>not</b> include the names of any modules under the <code>modules/</code>
 directory; the modules there are either mandatory or conditional.  Attempting
@@ -348,7 +346,7 @@ of modules to be compiled as shared modules.  Note that using the
 <code>--enable-dso</code>.  Thus to add LDAP and SSL/TLS support as
 shared modules, you would use:
 <pre>
-  # ./configure --enable-dso --with-shared=mod_ldap:mod_tls ...
+  $ ./configure --enable-dso --with-shared=mod_ldap:mod_tls ...
 </pre>
 Failure to include the <code>--enable-dso</code> option when using
 <code>--with-shared</code> results in a configure error:
@@ -370,7 +368,7 @@ modules, but not all of them.  The following automatically included modules
 If your <code>--with-shared</code> list includes one of the above modules,
 it will result in a configure error:
 <pre>
-  # ./configure --enable-dso --with-shared=mod_auth_unix ...
+  $ ./configure --enable-dso --with-shared=mod_auth_unix ...
   configure: error: cannot build mod_auth_unix as a shared module
 </pre>
 Why can't these be shared modules?  These modules <b>must</b> appear in the
@@ -398,14 +396,14 @@ For example, on Solaris, you have to explicitly tell the compiler and linker
 to use the <code>/usr/local/include</code> and <code>/usr/local/lib</code>
 directories:
 <pre>
-  # ./configure --with-includes=/usr/local/include --with-libraries=/usr/local/lib ...
+  $ ./configure --with-includes=/usr/local/include --with-libraries=/usr/local/lib ...
 </pre>
 
 <p>
 Or you may have the MySQL or OpenSSL packages installed in custom locations,
 and you need to tell <code>configure</code> about those locations:
 <pre>
-  # ./configure --with-modules=mod_sql:mod_sql_mysql:mod_tls \
+  $ ./configure --with-modules=mod_sql:mod_sql_mysql:mod_tls \
     --with-includes=/usr/local/mysql/include/mysql:/usr/local/openssl/include \
     --with-libraries=/usr/local/mysql/lib/mysql:/usr/local/openssl/lib
 </pre>
@@ -442,7 +440,7 @@ does not include IDENT support, is installed under <code>/opt/proftpd/</code>
 as user and group 'ftpd', has staticly linked SSL/TLS support and dynamically
 loaded SQL backend modules:
 <pre>
-  # install_user=ftpd install_group=ftpd ./configure \
+  $ install_user=ftpd install_group=ftpd ./configure \
     --prefix=/opt/proftpd \
     --disable-ident \
     --enable-dso \
@@ -468,8 +466,8 @@ Now you are ready to actually compile the source code into working executables,
 using all of your selected options and features and capabilities.  To do
 this, imply run 'make' from the top-level source directory:
 <pre>
-  # cd proftpd-<i>version</i>/
-  # make
+  $ cd proftpd-<i>version</i>/
+  $ make
 </pre>
 On some systems (<i>e.g.</i> BSDI), you may need to use GNU <code>make</code>
 (often called <code>gmake</code> or <code>gnumake</code>) instead of the
@@ -485,8 +483,8 @@ the ProFTPD source code.
 <p>
 To install the compiled executables, use:
 <pre>
-  # cd proftpd-<i>version</i>/
-  # make install
+  $ cd proftpd-<i>version</i>/
+  $ make install
 </pre>
 You are now prepared to start up and use your compiled <code>proftpd</code>.
 
@@ -498,7 +496,7 @@ development purposes; it is <b>not recommended for use in production
 builds or packages</b>.  If you are using a <code>proftpd</code> installed
 from a package, try this command:
 <pre>
-  # /usr/local/sbin/proftpd -V
+  $ /usr/local/sbin/proftpd -V
 </pre>
 if you see "+ Developer support" in the output, it means that your
 package provider used the <code>--enable-devel</code> option when they
@@ -512,12 +510,12 @@ option, like many of ProFTPD's configure options, takes a colon-delimited
 list of option names.  The full list of supported developer options is
 shown here:
 <pre>
-  # ./configure --enable-devel=coredump:nodaemon:nofork:profile:stacktrace ...
+  $ ./configure --enable-devel=coredump:nodaemon:nofork:profile:stacktrace ...
 </pre>
 You can also use just <code>--enable-devel</code> by itself, without any
 specific option names:
 <pre>
-  # ./configure --enable-devel ...
+  $ ./configure --enable-devel ...
 </pre>
 When <code>--enable-devel</code> is used, the executables that are installed
 by the ProFTPD build system will not be stripped of their debugging symbols,
@@ -575,7 +573,7 @@ developers.
 <p>
 Let's say you configured the build for stacktrace support:
 <pre>
-  # ./configure --enable-devel=stacktrace ...
+  $ ./configure --enable-devel=stacktrace ...
 </pre>
 If/when a SIGSEGV occurs, the logs should show something like this:
 <pre>
@@ -605,11 +603,11 @@ frame; in this case, "[0] ./proftpd [0x809b1e1]" and the memory address
 install separately on your system) and that memory address, you can determine
 the location of the segfault source:
 <pre>
-  # addr2line -e /usr/local/sbin/proftpd 0x809b1e1
+  $ addr2line -e /usr/local/sbin/proftpd 0x809b1e1
 </pre>
 In this particular case, I saw:
 <pre>
-  # addr2line -e /usr/local/sbin/proftpd 0x809b1e1
+  $ addr2line -e /usr/local/sbin/proftpd 0x809b1e1
   /home/tj/proftpd/cvs/proftpd/modules/mod_auth.c:1723
 </pre>
 which is the location of test code I added to deliberately trigger a segfault.
@@ -663,7 +661,7 @@ Note that older releases of ProFTPD did not check for these types of
 <code>proftpd</code> and you see the same module appear multiple times in
 the output from running:
 <pre>
-  # /usr/local/sbin/proftpd -l
+  $ /usr/local/sbin/proftpd -l
 </pre>
 Then you should re-configure and re-compile your <code>proftpd</code>, making
 sure that no automatically included modules appear in your
@@ -679,21 +677,50 @@ there is "d_auth_pam.c", rather than "mod_auth_pam.c"?  If you see a mangled
 module name like this, it probably means that your <code>--with-modules</code>
 or <code>--with-shared</code> module lists contain a double colon, <i>e.g.</i>:
 <pre>
-  # ./configure --with-modules=mod_sql<b>::</b>mod_sql_mysql:...
+  $ ./configure --with-modules=mod_sql<b>::</b>mod_sql_mysql:...
 </pre>
 or:
 <pre>
-  # ./configure --with-shared=mod_sql<b>::</b>mod_sql_mysql:...
+  $ ./configure --with-shared=mod_sql<b>::</b>mod_sql_mysql:...
 </pre>
 <i>Use only a single colon between module names</i>; this should fix this error.
 
 <p>
+<font color=red>Question</font>: I installed ProFTPD, and encountered this
+error when trying to use <code>ftptop</code>:
+<pre>
+  $ ftptop
+  ftptop: no curses or ncurses library on this system
+</pre>
+So I installed the <code>ncurses</code> library on my system, and compiled
+ProFTPD again.  But the error still occurs!  Why?<br>
+<font color=blue>Answer</font>: The ProFTPD build system discovers the
+libraries it needs, like <code>curses</code> or <code>ncurses</code>, as part
+of the running of the <code>configure</code> command; the results of those
+discoveries are recorded in the generated <code>config.h</code> (and other)
+files.
+
+<p>
+This means that if you install a needed library <i>after</i> building ProFTPD,
+you need to <i>re-run <code>configure</code>, <code>make</code>, etc</i> in
+order for ProFTPD to discover that new library and use it:
+<pre>
+  $ cd /path/to/proftpd/
+  $ make clean
+  $ ./configure ...
+  $ make
+  $ make install
+</pre>
+The "make clean" step is necessary, to clean up any leftover state from the
+previous build.
+
+<p>
 <font color=red>Question</font>: I can't seem to compile <code>mod_tls</code>
 <b>and</b> <code>mod_sql</code>.  Using one or the other works (<i>i.e.</i>
 shows up in the <code>`proftpd -l`</code> list), but not both.  I'm using the
 following configure command:
 <pre>
-  # ./configure --with-modules=mod_tls --with-modules=mod_sql:mod_sql_mysql ...
+  $ ./configure --with-modules=mod_tls --with-modules=mod_sql:mod_sql_mysql ...
 </pre>
 <font color=blue>Answer</font>: The problem is that the
 <code>--with-modules</code> option cannot appear multiple times in the
@@ -701,8 +728,8 @@ configure command.  If it does, only the last one seen on the command line
 wins.  In this case, the user would need to put all of the desired modules
 into <b>one</b> colon-delimited list, <i>e.g.</i>:
 <pre>
-  # make clean
-  # ./configure --with-modules=mod_tls:mod_sql:mod_sql_mysql ...
+  $ make clean
+  $ ./configure --with-modules=mod_tls:mod_sql:mod_sql_mysql ...
 </pre>
 And keep in mind that the same restriction holds true for the
 <code>--with-includes</code> and <code>--with-libraries</code> options.
@@ -724,7 +751,7 @@ particular about the order in which they appear in the
 the modules are loaded when <code>proftpd</code> starts up.  The above
 error, for example, happens when <code>--with-modules</code> looks like:
 <pre>
-  # ./configure --with-modules=mod_sql_mysql:<b>mod_sql</b> (<i><font color=red>wrong</font></i>)
+  $ ./configure --with-modules=mod_sql_mysql:<b>mod_sql</b> (<i><font color=red>wrong</font></i>)
 </pre>
 but the <code>mod_sql</code> module needs to be loaded first, so that later,
 when <code>mod_sql_mysql</code> loads, the expected <code>mod_sql</code>
@@ -732,7 +759,7 @@ code is present.  Thus the proper ordering is to make sure that
 <code>mod_sql</code> appears <i>before</i> any of the database-specific
 backend modules:
 <pre>
-  # ./configure --with-modules=<b>mod_sql</b>:mod_sql_mysql (<i><font color=green>correct</font></i>)
+  $ ./configure --with-modules=<b>mod_sql</b>:mod_sql_mysql (<i><font color=green>correct</font></i>)
 </pre>
 
 <p>
@@ -758,16 +785,16 @@ your configure command when compiling <code>proftpd</code>.
 <code>proftpd</code> binary?<br>
 <font color=blue>Answer</font>: To do this, using the following:
 <pre>
-  # make clean
-  # ./configure LDFLAGS="-Wl,-static" ...
+  $ make clean
+  $ ./configure LDFLAGS="-Wl,-static" ...
 </pre>
 Do <b>not</b> try to use the "-static" value for the CFLAGS or LDFLAGS
 environment variables, or try to edit the Make.rules file to tweak the
 options.  ProFTPD's build system uses libtool, and thus the above method
 is the way to get a statically linked <code>proftpd</code> executable:
 <pre>
-  # make
-  # ldd ./proftpd
+  $ make
+  $ ldd ./proftpd
     not a dynamic executable
 </pre>
 
@@ -798,7 +825,7 @@ a single parameter which exceeds the 1024 byte buffer limit, and which
 <code>proftpd</code> with a larger buffer size.  To do this, use the
 <code>--enable-buffer-size</code> configure option, <i>e.g.</i>:
 <pre>
-  # ./configure --enable-buffer-size=2048 ...
+  $ ./configure --enable-buffer-size=2048 ...
 </pre>
 
 <p>
@@ -820,7 +847,11 @@ and <code>--enable-autoshadow</code>.
 
 <p>
 <hr>
-<i>$Date: 2013-10-06 16:02:23 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/ConfigFile.html b/doc/howto/ConfigFile.html
index 7f5837e..f4f2392 100644
--- a/doc/howto/ConfigFile.html
+++ b/doc/howto/ConfigFile.html
@@ -1,18 +1,13 @@
-<!-- $Id: ConfigFile.html,v 1.6 2009-09-04 22:26:49 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/ConfigFile.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Configuring ProFTPD (via proftpd.conf)</title>
+<title>ProFTPD: Configuring ProFTPD</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>
-Configuring ProFTPD<br>
-(via <code>proftpd.conf</code>)
-</b></h2></center>
+<center><h2><b>ProFTPD: Configuring ProFTPD</b></h2></center>
 <hr>
 
 <p>
@@ -33,31 +28,162 @@ server configurations be explicitly written in the <code>proftpd.conf</code>
 file.  This default server attempts to bind to the IP address of the hostname
 indicated by the <code>hostname(1)</code> command.
 
-<p><a name="Format"></a>
+<p><a name="Format">
 <b>Configuration Format</b><br>
 The format of the <code>proftpd.conf</code> file is deliberately designed
-to resemble the format used by Apache: lines of configuration directives
-contained with different contexts.  A list of the configuration directives
-for ProFTPD is available here:
+to resemble the format used by Apache: lines of configuration <i>directives</i>
+contained with different <i>contexts</i> (or <i>sections</i>).
+
+<p>
+Each of configuration <i>directive</i> has the following properties:
+<ul>
+  <li><a href="#Syntax">Syntax</a>
+  <li><a href="#Default">Default</a>
+  <li><a href="#Context">Context</a>
+  <li><a href="#Module">Module</a>
+  <li><a href="#Compatibility">Compatibility</a>
+</ul>
+
+<p><a name="Syntax">
+<b><i>Syntax</i></b><br>
+The directive <em>syntax</em> indicates the format of the directive as it would
+appear in a configuration file.  This syntax is extremely directive-specific,
+so refer to the text of the directive's description for details.
+
+<p><a name="Default">
+<b><i>Default</i></b><br>
+If the directive has a default value (<i>i.e.</i>, if you omit it from your
+configuration entirely, the ProFTPD server will behave as though you set it to
+this particular value), it is described here.  If there is no default value,
+this section should say "<em>None</em>".
+
+<p><a name="Context">
+<b><i>Context</i></b><br>
+The <em>context</em> of a directive indicates where in the server's
+configuration files the directive is legal/allowed.  It is a comma-separated
+list of one or more of the following values:
+<ul>
+  <li><strong><i>server config</i></strong><br>
+    <p>
+    This means that the directive may be used in the server configuration
+    file (<i>e.g.</i>, <code>proftpd.conf</code>) <b>outside of any other
+    context</b> (<i>i.e.</i> not inside a <code><VirtualHost></code> or
+    <code><Global></code> context).  This context defines a
+    "main" or "default" server.<br>
+  </li>
+
+  <p>
+  <li><strong><i><code><VirtualHost></code></i></strong><br>
+    <p>
+    This context means that the directive may appear inside
+    <a href="../modules/mod_core.html#VirtualHost"><code><VirtualHost></code></a> sections in the server configuration file.<br>
+  </li>
+
+  <p>
+  <li><strong><i><code><Global></code></i></strong><br>
+    <p>
+    This context means that the directive may appear inside
+    <a href="../modules/mod_core.html#Global"><code><Global></code></a>
+    sections in the server configuration file.  This context is used as a
+    shortcut for placing directives with all server contexts, <i>i.e.</i> the
+    "server config" context as well as any
+    <code><VirtualHost></code> contexts within the configuration file.
+    Any directives of the same name <em>within</em> those server sections will
+    have precedence over a <code><Global></code> setting.<br>
+  </li>
+
+  <p>
+  <li><strong><i><code><Anonymous></code></i></strong><br>
+    <p>
+    This context means that the directive may appear inside any
+    <a href="../modules/mod_core.html#Anonymous"><code><Anonymous></code></a>
+    sections in the server configuration file.  An <Anonymous> section is
+    not a true virtual server, but rather is a section <i>within</i> a server
+    context (<i>i.e.</i> "server config",
+    <code><VirtualHost></code> or <code><Global></code>).
+    Anonymous sections are <b>automatically <code>chroot()</code>ed</b>.  Any
+    configuration directives set for the containing server will be in effect
+    for the anonymous section as well, unless overridden by a directive of
+    the same name within the anonymous section.<br>
+  </li>
+
+  <p>
+  <li><strong><i><code><Limit></code></i></strong><br>
+    <p>
+    This context means that the directive may appear inside any
+    <a href="../modules/mod_core.html#Limit"><code><Limit></code></a>
+    sections in the server configuration file.  This context is used to place
+    limits on who and how individual FTP commands, or groupings of FTP
+    commands, may be used.<br>
+  </li>
+
+  <p>
+  <li><strong><i><code><Directory></code></i></strong><br>
+    <p>
+    A directive marked as being valid in this context may be used inside
+    <a href="../modules/mod_core.html#Directory"><code><Directory></code></a>
+    sections in the server configuration file.  This context configures views
+    of the contained files based on the logged-in user's username or group
+    membership, or on the name of the files (<i>e.g.</i> Unix-style
+    "hidden" files), and on whether the user has permission to see
+    the files.  By definition, directives set using a
+    <a href="ftpaccess.html"><code>.ftpaccess</code></a> file are considered
+    to be occurring within a <code><Directory></code> context.<br>
+   </li>
+
+  <p>
+  <li><strong><i>.ftpaccess</i></strong><br>
+    <p>
+    If a directive is valid in this context, it means that it can appear
+    inside <em>per</em>-directory
+    <a href="ftpaccess.html"><code>.ftpaccess</code></a> files.
+    These files are akin to Apache's <code>.htaccess</code> files:
+    parsed-on-the-fly mini-configuration files that users can place within
+    their own directories.<br>
+  </li>
+</ul>
+
+<p>
+A configuration directive is <em>only</em> allowed within the designated
+contexts; if you try to use that directive elsewhere, you will get a
+configuration error that will either prevent the server from handling requests
+in that context correctly, or will keep the server from operating at all
+<em>i.e.</em>, the server will not even start.  A directive that is marked as
+being valid in "<code>server config, .ftpaccess</code>" can be used
+in the <code>proftpd.conf</code> file and in <code>.ftpaccess</code> files,
+but not within any <code><Directory></code>,
+<code><VirtualHost></code>, or other contexts.
+
+<p>
+Two new configuration directives were introduced in
+<code>proftpd-1.2.6rc1</code>: <code><IfModule></code> and
+<code><IfDefine></code>.  These work exactly like Apache's directives of
+the same names, providing the ability to have conditional sections in the
+configuration file.
+
+<p><a name="Module">
+<b><i>Module</i></b><br>
+This quite simply lists the name of the module (<i>e.g.</i>
+<code>mod_xfer</code>, <code>mod_tls</code>, <code>mod_sql</code>, <i>etc</i>)
+which defines and implements the directive.
+
+<p><a name="Compatibility">
+<b><i>Compatibility</i></b><br>
+This usually lists the version in which the directive first appeared.
+
+<p>
+A list of the configuration directives for ProFTPD is available here:
 <pre>
   <a href="http://www.proftpd.org/docs/">http://www.proftpd.org/docs/</a>
 </pre>
+
 When reading the description for the configuration directives, this key
 might be useful:
-<pre>
-  <a href="http://www.castaglia.org/proftpd/doc/contrib/configuration-directive-key.html">http://www.castaglia.org/proftpd/doc/contrib/configuration-directive-key.html</a>
-</pre>
 It describes the description format, and lists the different contexts in the
 configuration file.  The "server config" context is the one in
 which most of your configuration directives will most likely be placed.
 
-<p>
-Two new configuration directives were introduced in <code>1.2.6rc1</code>:
-<code><IfModule></code> and <code><IfDefine></code>.  These
-work exactly like Apache's directives of the same names, providing the ability
-to have conditional sections in the configuration file.
-
-<p><a name="Starting"></a>
+<p><a name="Starting">
 <b>Starting the Daemon</b><br>
 One of the first decisions you will need to make is whether you will be running
 your ProFTPD server as an <code>inetd</code> service, or as a
@@ -65,7 +191,7 @@ your ProFTPD server as an <code>inetd</code> service, or as a
 <code>proftpd.conf</code> using the <code>ServerType</code> configuration
 directive (see the <a href="ServerType.html">ServerType</a> page).
 
-<p><a name="Identity"></a>
+<p><a name="Identity">
 <b>Server Identity</b><br>
 The daemon must be started with root privileges in order to do things like
 binding to port 21 and chrooting FTP sessions.  However, it is not a good
@@ -81,7 +207,8 @@ this is because the program displays the real UID/GID of processes.
 The <code>proftpd</code> daemon retains root privileges for operations
 such as chroots and binding to port 20 for active data transfers.
 If you wish <code>proftpd</code> to drop all root privileges, use the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_RootRevoke.html"><code>RootRevoke</code></a> configuration directive.)
+<a href="../modules/mod_auth.html#RootRevoke"><code>RootRevoke</code></a>
+configuration directive.)
 
 <p>
 For this reason, it is recommended that a non-privileged identity be
@@ -117,7 +244,7 @@ and supplemental GIDs, <i>etc</i>) of the authenticated user.  Thus all
 browsing, uploads, and downloads that clients do happen as the user as which
 they are logged in.
 
-<p><a name="Login"></a>
+<p><a name="Login">
 <b>Logging in</b><br>
 By default, the <code>proftpd</code> daemon reads the host's
 <code>/etc/passwd</code> file for logging in users.  This means that to
@@ -131,24 +258,28 @@ directive can be used (see <a href="AuthFiles.html">here</a> for details).
 
 <p>
 For the purpose of authenticating users using other means, there are various
-authentication modules: <code>mod_sql</code>, <code>mod_ldap</code>,
-<code>mod_radius</code>, <i>etc</i>.  Authentication and the login process
-is discussed <a href="Authentication.html">here</a> in more detail.
+authentication modules:
+<a href="../contrib/mod_sql.html"><code>mod_sql</code></a>,
+<a href="../contrib/mod_ldap.html"><code>mod_ldap</code></a>,
+<a href="../contrib/mod_radius.html"><code>mod_radius</code></a>, <i>etc</i>.
+Authentication and the login process is discussed
+<a href="Authentication.html">here</a> in more detail.
 
 <p>
-For setting up anonymous logins, there is the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_Anonymous.html"><code><Anonymous></code></a> configuration context.  If there are no <code><Anonymous></code>
-sections in your <code>proftpd.conf</code>, then no anonymous logins will be
-allowed - simple.  As mentioned in the description, the <code>User</code>
-directive in an <code><Anonymous></code> context determines what username
-is treated as an anonymous login.  The main other thing to know about
-anonymous logins is that ProFTPD automatically chroots anonymous logins.
+For setting up anonymous logins, there is the <a href="../modules/mod_core.html#Anonymous"><code><Anonymous></code></a> configuration context.  If there
+are no <code><Anonymous></code> sections in your
+<code>proftpd.conf</code>, then no anonymous logins will be allowed - simple.
+As mentioned in the description, the <code>User</code> directive in an
+<code><Anonymous></code> context determines what username is treated as
+an anonymous login.  The main other thing to know about anonymous logins is
+that ProFTPD automatically chroots anonymous logins.
 
-<p><a name="Jailing"></a>
+<p><a name="Jailing">
 For normal, non-anonymous logins, jails/chroots are configured using the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_DefaultRoot.html"><code>DefaultRoot</code></a> directive.  This is the configuration directive
-used to restrict users to their home directories, to keep them from browsing
-around the site.  There is a page covering chrooting
-<a href="Chroot.html">here</a>.
+<a href="../modules/mod_auth.html#DefaultRoot"><code>DefaultRoot</code></a>
+directive.  This is the configuration directive used to restrict users to
+their home directories, to keep them from browsing around the site.  There is
+a page covering chrooting <a href="Chroot.html">here</a>.
 
 <p>
 If you use <code><VirtualHost></code> sections, and it seems that your
@@ -171,11 +302,14 @@ used: the RFCs mandate that the daemon, for the purposes of active data
 transfers (as opposed to passive) use port <code><i>L</i>-1</code> as the source
 port for the data connection, where <code><i>L</i></code> is the port number
 at which the client contacted the server.  This means that servers that use the
-standard port 21 for FTP will use port 20 as the source port for their active
-data transfers.  Passive data transfers do not have this restriction.  The
-restriction comes into play when choosing non-standard port numbers for
-virtual hosts.  For example, this configuration would cause problems for
-clients of the second virtual server that wanted to use active data transfers:
+standard port 21 for FTP will use port 20 as the <i>source</i> port for their
+active data transfers.  (<b>Note</b> that this <i>also</i> means that you do
+<b>not</b> need to have port 20 open in your firewall for <i>inbound</i>
+connections for FTP data transfers).  Passive data transfers do not have this
+restriction.  The restriction comes into play when choosing non-standard port
+numbers for virtual hosts.  For example, this configuration would cause
+problems for clients of the second virtual server that wanted to use active
+data transfers:
 <pre>
   <VirtualHost <i>a.b.c.d</i>>
     Port 2121
@@ -191,7 +325,7 @@ The second virtual would attempt to use port 2121 as the source port for
 an active data transfer, but would be blocked, as the first virtual server
 is already using that port for listening.
 
-<p><a name="Access"></a>
+<p><a name="Access">
 <b>Access Restrictions</b><br>
 Many sites like to have specific directories for uploads, and other directories
 only for downloads; some sites like to allow downloads, but no browsing
@@ -204,14 +338,15 @@ pages that cover these configuration sections:
   <li><a href="Limit.html"><code><Limit></code></a>
 </ul>
 
-<p><a name="Questions"></a>
+<p><a name="Questions">
 <b>Further Questions</b><br>
 Hopefully this document answers some of your questions, or at least enough
 to get you started.  In addition, you should take a look at some of the
-<a href="http://www.proftpd.org/docs/example-conf.html">example configuration files</a>.  Once you are comfortable with the configuration file format, a reading of
-all the configuration directives' descriptions is recommended, especially if
-you plan on having more complex configurations.  When trying to figure out why
-something is not working, make use of server
+<a href="http://www.proftpd.org/docs/example-conf.html">example configuration
+files</a>.  Once you are comfortable with the configuration file format, a
+reading of all the configuration directives' descriptions is recommended,
+especially if you plan on having more complex configurations.  When trying to
+figure out why something is not working, make use of server
 <a href="Debugging.html">debugging</a> output.
 
 <p>
@@ -220,7 +355,10 @@ users</a> mailing list is the best place to post them.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2009-09-04 22:26:49 $</i><br>
+<font size=2><b><i>
+© Copyright 2000-2016 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/ConfigurationTricks.html b/doc/howto/ConfigurationTricks.html
index dce8c10..2e64029 100644
--- a/doc/howto/ConfigurationTricks.html
+++ b/doc/howto/ConfigurationTricks.html
@@ -1,15 +1,13 @@
-<!-- $Id: ConfigurationTricks.html,v 1.6 2013-10-06 15:46:36 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/ConfigurationTricks.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Configuration Tricks</title>
+<title>ProFTPD: Configuration Tricks</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Configuration Tricks</b></h2></center>
+<center><h2><b>ProFTPD: Configuration Tricks</b></h2></center>
 <hr>
 
 <p>
@@ -427,7 +425,11 @@ approaching full LAN network speed for the FTP data transfer.
 
 <p>
 <hr>
-<i>$Date: 2013-10-06 15:46:36 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/ConnectionACLs.html b/doc/howto/ConnectionACLs.html
index 161fb05..e914f29 100644
--- a/doc/howto/ConnectionACLs.html
+++ b/doc/howto/ConnectionACLs.html
@@ -1,15 +1,13 @@
-<!-- $Id: ConnectionACLs.html,v 1.3 2011-01-05 19:26:53 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/ConnectionACLs.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Connection ACLs</title>
+<title>ProFTPD: Connection ACLs</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Connection ACLs</b></h2></center>
+<center><h2><b>ProFTPD: Connection ACLs</b></h2></center>
 <hr>
 
 <p>
@@ -186,8 +184,7 @@ can easily support large numbers of addresses efficiently.
 <p>
 And if you find yourself starting to block large blocks of addresses from
 countries/regions, you should start thinking about connection ACLs in terms of
-geolocation information.  For this, the <a href="http://www.castaglia.org/proftpd/modules/mod_geoip.html"><code>mod_geoip</code></a> module for proftpd is
-quite useful.
+geolocation information.  For this, the <a href="../contrib/mod_geoip.html"><code>mod_geoip</code></a> module for ProFTPD is quite useful.
 
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
@@ -216,7 +213,10 @@ can also appear as:
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2011-01-05 19:26:53 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Controls.html b/doc/howto/Controls.html
index 84c5398..2e0f6f4 100644
--- a/doc/howto/Controls.html
+++ b/doc/howto/Controls.html
@@ -1,15 +1,13 @@
-<!-- $Id: Controls.html,v 1.5 2013-01-09 20:53:43 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Controls.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Controls</title>
+<title>ProFTPD: Controls</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Controls and <code>mod_ctrls</code></b></h2></center>
+<center><h2><b>ProFTPD: Controls and <code>mod_ctrls</code></b></h2></center>
 <hr>
 
 <p>
@@ -28,12 +26,12 @@ The functionality involves a client and a server communicating over a Unix
 domain socket, using a simple text-based protocol.  A new program,
 <code>ftpdctl</code>, is distributed with ProFTPD; <code>ftpdctl</code> is a
 Controls client.  The server side of the Controls functionality is the
-<a href="http://www.proftpd.org/docs/modules/mod_ctrls.html"><code>mod_ctrls</code></a> module, which is compiled into a <code>proftpd</code> daemon when
-the <em>--enable-ctrls</em> configure option is used.  Note, however, that
-the Controls functionality only works for <code>proftpd</code> daemons
-whose <code>ServerType</code> is <code>standalone</code>; <code>proftpd</code>
-daemons run via inetd/xinetd cannot support Controls and the
-<code>ftpdctl</code> program.
+<a href="../modules/mod_ctrls.html"><code>mod_ctrls</code></a> module, which is
+compiled into a <code>proftpd</code> daemon when the <em>--enable-ctrls</em>
+configure option is used.  Note, however, that the Controls functionality only
+works for <code>proftpd</code> daemons whose <code>ServerType</code> is
+<code>standalone</code>; <code>proftpd</code> daemons run via inetd/xinetd
+cannot support Controls and the <code>ftpdctl</code> program.
 
 <p>
 <b>Configuring <code>mod_ctrls</code></b><br>
@@ -259,7 +257,7 @@ and useful control actions, including:
 </ul>
 These actions provide basis administrative control over the running
 <code>proftpd</code> daemon; see the <code>mod_ctrls_admin</code>
-<a href="http://www.proftpd.org/docs/contrib/mod_ctrls_admin.html">documentation</a> for more information.
+<a href="../contrib/mod_ctrls_admin.html">documentation</a> for more details.
 
 <p>
 A basic <code>mod_ctrls_admin</code> configuration is:
@@ -309,7 +307,10 @@ If either allows access, the client can use that control action.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-01-09 20:53:43 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/CreateHome.html b/doc/howto/CreateHome.html
index 08776d4..b5fdbc2 100644
--- a/doc/howto/CreateHome.html
+++ b/doc/howto/CreateHome.html
@@ -1,9 +1,7 @@
-<!-- $Id: CreateHome.html,v 1.7 2012-09-05 17:30:05 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/CreateHome.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - CreateHome</title>
+<title>ProFTPD: CreateHome</title>
 </head>
 
 <body bgcolor=white>
@@ -34,7 +32,8 @@ other authentication modules, such as <code>mod_radius</code>, do not support
 it at all.  It was decided then, rather than having authentication modules
 have largely duplicated code for this feature, to have home-on-demand
 creation in the core <code>daemon</code> itself.  And so it is.
-As of 1.2.8rc2, <code>proftpd</code> has the <code>CreateHome</code>
+As of 1.2.8rc2, <code>proftpd</code> has the
+<a href="../modules/mod_auth.html#CreateHome"><code>CreateHome</code></a>
 configuration directive.
 
 <p>
@@ -161,7 +160,7 @@ desired from the FTP daemon.
 <font color=red>Question</font>: Is it possible to have different permissions
 for the <code>CreateHome</code> <em>mode</em> and <em>dirmode</em> based on the
 group of the connecting user?<br>
-<font color=blue>Answer</font>: Yes, if you use the <a href="http://www.proftpd.org/docs/contrib/mod_ifsession.html"><code>mod_ifsession</code></a> module.
+<font color=blue>Answer</font>: Yes, if you use the <a href="../contrib/mod_ifsession.html"><code>mod_ifsession</code></a> module.
 For example:
 <pre>
   <IfGroup special>
@@ -175,7 +174,11 @@ For example:
 
 <p>
 <hr>
-<i>$Date: 2012-09-05 17:30:05 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/DNS.html b/doc/howto/DNS.html
index 1fb3b7d..09b3a30 100644
--- a/doc/howto/DNS.html
+++ b/doc/howto/DNS.html
@@ -1,15 +1,13 @@
-<!-- $Id: DNS.html,v 1.2 2009-02-12 22:41:32 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/DNS.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - DNS</title>
+<title>ProFTPD: DNS</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and DNS</b></h2></center>
+<center><h2><b>ProFTPD: DNS</b></h2></center>
 <hr>
 
 <p>
@@ -58,8 +56,9 @@ resolve the hostname of the server on which the daemon is running, <i>i.e.</i>
 the name displayed by typing <code>`hostname`</code>.  Why does
 <code>proftpd</code> need to know this?  There is always at least one
 server that <code>proftpd</code> will handle: the "server config"
-server (see the <a href="http://www.castaglia.org/proftpd/doc/contrib/ProFTPD-mini-HOWTO-Vhost.html">virtual host</a> howto).  This "server config"
-server defaults to the IP address of the hostname of the machine.
+server (see the <a href="Vhost.html">virtual host</a> howto).  This
+"server config" server defaults to the IP address of the hostname of
+the machine.
 
 <p>
 Once <code>proftpd</code> has the complete list of IP addresses with which
@@ -146,12 +145,12 @@ the host machine, this approach does not work.
 To specify the desired IP address, use <code>-S</code> when starting
 <code>proftpd</code>, <i>e.g.</i>:
 <pre>
-  /usr/local/sbin/proftpd -S 1.2.3.4 ...
+  $ /usr/local/sbin/proftpd -S 1.2.3.4 ...
 </pre>
 And if you want <code>proftpd</code> to listen on all interfaces, you can
 specify a wildcard socket using an IP address of 0.0.0.0:
 <pre>
-  /usr/local/sbin/proftpd -S 0.0.0.0 ...
+  $ /usr/local/sbin/proftpd -S 0.0.0.0 ...
 </pre>
 
 <p>
@@ -161,7 +160,11 @@ not DNS names.
 
 <p>
 <hr>
-<i>$Date: 2009-02-12 22:41:32 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/DSO.html b/doc/howto/DSO.html
index 7c9947e..be7572f 100644
--- a/doc/howto/DSO.html
+++ b/doc/howto/DSO.html
@@ -1,15 +1,13 @@
-<!-- $Id: DSO.html,v 1.10 2012-10-12 02:21:19 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/DSO.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Dynamic Shared Objects</title>
+<title>ProFTPD: Dynamic Shared Objects (DSOs)</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Dynamic Shared Objects</b></h2></center>
+<center><h2><b>ProFTPD: Dynamic Shared Objects (DSOs)</b></h2></center>
 <hr>
 
 <p>
@@ -29,7 +27,7 @@ ProFTPD gained the ability to use DSOs starting with the 1.3.0rc1 release.
 To make sure the compiled <code>proftpd</code> binary can load DSO modules,
 use the <code>--enable-dso</code> configure option:
 <pre>
-  ./configure --enable-dso ...
+  $ ./configure --enable-dso ...
 </pre>
 This causes the build system to build the <code>libltdl</code> supporting
 library, which is used to handle OS-specific ways of loading and unloading
@@ -47,7 +45,7 @@ easily be built as DSO modules, rather than statically linked into the
 <code>--with-modules</code> configure option, you use the
 <code>--with-shared</code> option:
 <pre>
-  ./configure --enable-dso --with-shared=mod_sql:mod_sql_mysql --with-includes=... --with-libraries=...
+  $ ./configure --enable-dso --with-shared=mod_sql:mod_sql_mysql --with-includes=... --with-libraries=...
 </pre>
 These DSO modules will be installed under the <code>libexec/</code> directory
 of your ProFTPD install location.  To control the location of this
@@ -55,7 +53,7 @@ of your ProFTPD install location.  To control the location of this
 will load modules, you can use the <code>--libexecdir</code> configure
 option, <i>e.g.</i>:
 <pre>
-  ./configure --libexecdir=/path/to/custom/libexec --enable-dso ...
+  $ ./configure --libexecdir=/path/to/custom/libexec --enable-dso ...
 </pre>
 
 <p>
@@ -213,8 +211,8 @@ a library <code>libcustom.so</code>, you might have the following:
 Place the <code>Makefile</code> in a directory with your
 <code>mod_custom.c</code> source file, then do:
 <pre>
-  make
-  make install
+  $ make
+  $ make install
 </pre>
 The <code>make install</code> step will install the DSO module into the
 <code>libexec/</code> directory of your ProFTPD install location.
@@ -261,7 +259,7 @@ At least one of the above actions must be specified when using
 <p>
 To use <code>prxs</code> all in one step, you could do:
 <pre>
-  # prxs -c -i -d mod_custom.c
+  $ prxs -c -i -d mod_custom.c
 </pre>
 which will do the compile, install, and clean actions in order.  Once
 installed, update your <code>proftpd.conf</code> to make sure your module is
@@ -276,7 +274,7 @@ For example, you might use <code>prxs</code> to compile the
 <code>mod_sql_sqlite</code> module like so, from the top level of the
 ProFTPD source directory:
 <pre>
-  # prxs -c -i -d contrib/mod_sql_sqlite.c
+  $ prxs -c -i -d contrib/mod_sql_sqlite.c
 </pre>
 
 <p>
@@ -304,8 +302,8 @@ The following options are also supported:
 Using <code>prxs</code>, the above <code>mod_custom</code> example would
 become:
 <pre>
-  # cd /path/to/mod_custom/dir
-  # prxs -c -i -D USE_CUSTOM -I /path/to/custom/include -L /path/to/custom/lib -l custom mod_custom.c
+  $ cd /path/to/mod_custom/dir
+  $ prxs -c -i -D USE_CUSTOM -I /path/to/custom/include -L /path/to/custom/lib -l custom mod_custom.c
 </pre>
 That's it!  No need for a special Makefile, and no need to edit/replace any
 variables.
@@ -318,7 +316,7 @@ installed <code>libtool</code>), you can use the <code>LIBTOOL</code>
 environment variable to point <code>prxs</code> to the <code>libtool</code>
 to use.  For example:
 <pre>
-  # LIBTOOL=/path/to/custom/libtool prxs -c -i -d mod_custom.c
+  $ LIBTOOL=/path/to/custom/libtool prxs -c -i -d mod_custom.c
 </pre>
 
 <p>
@@ -344,7 +342,7 @@ whatever other module you want to add to your proftpd).  Assume, then, that
 you have found the <code>mod_sql_passwd.c</code> source file.  The next
 step is to use <code>prxs</code> to build that module as a DSO module:
 <pre>
-  # /usr/local/bin/prxs -c -i -d mod_sql_passwd.c
+  $ /usr/local/bin/prxs -c -i -d mod_sql_passwd.c
 </pre>
 If the above fails with this error message:
 <pre>
@@ -376,7 +374,11 @@ needing to recompile/reinstall proftpd itself.
 
 <p>
 <hr>
-<i>$Date: 2012-10-12 02:21:19 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Debugging.html b/doc/howto/Debugging.html
index ec38667..9fd2e29 100644
--- a/doc/howto/Debugging.html
+++ b/doc/howto/Debugging.html
@@ -1,15 +1,13 @@
-<!-- $Id: Debugging.html,v 1.7 2012-12-02 00:23:36 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Debugging.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Debugging</title>
+<title>ProFTPD: Debugging</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Debugging Problems</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Debugging Problems</i></b></h2></center>
 <hr>
 
 <p>
@@ -47,8 +45,8 @@ pointing out why that is.
 Various problems afflict various versions of the code, so when tracking down
 problems, it is good to know the version being used:
 <pre>
-  proftpd -V
-  proftpd -vv
+  $ proftpd -V
+  $ proftpd -vv
 </pre>
 When reporting issues, please include the output from <i>both</i> of these
 commands.
@@ -68,7 +66,7 @@ install using RPMs or other package formats may not know the specifics of
 the contained pre-built binary.  To list the modules compiled into the
 server:
 <pre>
-  proftpd -l
+  $ proftpd -l
 </pre>
 Knowing the modules helps to pinpoint the source of error messages (<i>e.g.</i>
 <code>mod_tls</code> and certificate files).
@@ -79,7 +77,7 @@ When making changes to the configuration file, it is often helpful to make sure
 that your changes are valid.  The easiest way to do this is to do an
 informative syntax check:
 <pre>
-  proftpd -td10
+  $ proftpd -td10
 </pre>
 The <code>-t</code> option directs the server to only do a syntax check, to
 parse the configuration file but stop before actually starting its operations
@@ -87,11 +85,12 @@ as a server.  The <code>-d10</code> will cause the server to display debugging
 messages during this testing of the configuration file.  Another useful
 command is:
 <pre>
-  proftpd -c <i>/path/to/new/config/file</i> -td10
+  $ proftpd -c <i>/path/to/new/config/file</i> -td10
 </pre>
 which lets you test the syntax of some new configuration file before it
 is put into production.
 
+<p>
 If you are still having problems, and you have verified that your
 <code>proftpd.conf</code> is correct, the next step is see what debugging
 messages are generated during an FTP session, as described next.
@@ -103,7 +102,7 @@ the trick is in enabling that reporting, and tracking down where it is sent.
 The easiest way to get the debugging information is to start the server from
 the command line using:
 <pre>
-  proftpd -nd10
+  $ proftpd -nd10
 </pre>
 <b>Note</b>: make sure that no other <code>proftpd</code> instances are
 running before using this command, otherwise you will see:
@@ -126,7 +125,7 @@ send debugging information to the mailing list, you can send the relevant
 snippets (if you know what the relevant debug messages are), or you can
 capture the debug output to a file:
 <pre>
-  proftpd -nd10 2>&1 >& /path/to/debug/file
+  $ proftpd -nd10 2>&1 >& /path/to/debug/file
 </pre>
 and send that file, compressed, along with your post.
 
@@ -187,7 +186,7 @@ In version 1.3.1rc1, ProFTPD gained the ability to provide better logging
 to help track down these sorts of bugs.  To enable this ability, you will
 need to configure <code>proftpd</code> with something like:
 <pre>
-  ./configure --enable-devel=stacktrace ...
+  $ ./configure --enable-devel=stacktrace ...
 </pre>
 and run your <code>proftpd</code> like normal.  If a segfault occurs, the logs
 should show something like this:
@@ -217,11 +216,11 @@ The key here for tracking down the location of the segfault is that
 that address and a very handy command called <code>addr2line</code>, you can
 determine the location of that address in the source code:
 <pre>
-  addrline -e ./proftpd 0x809b1e1
+  $ addrline -e ./proftpd 0x809b1e1
 </pre>
 In this example, I saw:
 <pre>
-  golem/tj>addr2line -e ./proftpd 0x809b1e1
+  $ addr2line -e ./proftpd 0x809b1e1
   /home/tj/proftpd/cvs/proftpd/modules/mod_auth.c:1723
 </pre>
 which is the location of test code added to trigger the segfault.
@@ -272,9 +271,11 @@ be answered:
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2012-12-02 00:23:36 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
-<hr>
 </body>
 </html>
diff --git a/doc/howto/Directory.html b/doc/howto/Directory.html
index be59da2..b60a4fe 100644
--- a/doc/howto/Directory.html
+++ b/doc/howto/Directory.html
@@ -1,15 +1,13 @@
-<!-- $Id: Directory.html,v 1.12 2008-12-10 22:40:15 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Directory.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Configuring a <Directory></title>
+<title>ProFTPD: Configuring a <Directory></title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Configuring a <code><Directory></code></b></h2></center>
+<center><h2><b>ProFTPD: Configuring a <code><Directory></code></b></h2></center>
 <hr>
 
 <p>
@@ -29,7 +27,7 @@ like:
 </pre>
 The daemon will not let one do this, in fact.  The daemon will determine
 automatically the relations of <code><Directory></code> paths, depending
-on the path given and surrounding configuration context.
+on the path given and surrounding configuration section.
 
 <p>
 Always use the normal, absolute path for a <code><Directory></code>
@@ -150,21 +148,23 @@ which will achieve the desired effect of allowing uploads only in
 subdirectories of the given directory, <code>upload/</code>.
 
 <p>
-Also, it is good to keep in mind the <a href="http://www.castaglia.org/proftpd/doc/devel-guide/internals/ftpaccess.html">similarity</a> between a
-<code><Directory></code> section and a <code>.ftpaccess</code> file.
-In some cases, using <code>.ftpaccess</code> files might be more convenient.
-The <code>AllowOverride</code> configuration directive (which first appeared
-in the 1.2.7rc1 release) will provide fine-grained control over when
-<code>.ftpaccess</code> files will be honored.
+Also, it is good to keep in mind the <a href="ftpaccess.html">similarity</a>
+between a <code><Directory></code> section and a <code>.ftpaccess</code>
+file.  In some cases, using <code>.ftpaccess</code> files might be more
+convenient.  The
+<a href="../modules/mod_core.html#AllowOverride"><code>AllowOverride</code></a>
+configuration directive (which first appeared in the 1.2.7rc1 release) provides
+fine-grained control over when <code>.ftpaccess</code> files will be honored.
 
 <p>
 The fact that <code><Directory></code> sections can be used to
 refer to specific <i>files</i>, in addition to directories, is not obvious.
 However, there are some cases where it can be useful to use this feature.
 One proftpd user used this feature in the following way: the
-<code>DirFakeMode</code> was used to make all files look read-only (mostly
-so that FTP mirroring tools would create a read-only mirror of the site).
-However, a particular file on the site needed have execute permissions,
+<a href="../modules/mod_ls.html#DirFakeMode"><code>DirFakeMode</code></a>
+directive was used to make all files look read-only (mostly so that FTP
+mirroring tools would create a read-only mirror of the site).  However, a
+particular file on the site needed have execute permissions,
 even in the FTP mirrored site.  A <code><Directorygt;</code> section
 was used just for this one file, <i>e.g.</i>:
 <pre>
@@ -195,7 +195,7 @@ For example, if you tried:
     <Limit ALL>
       DenyAll
     </Limit>
-  &lt/Directory>
+  </Directory>
 
   <Directory /path/to/dir>
     <Limit ALL>
@@ -205,11 +205,11 @@ For example, if you tried:
     <Limit WRITE>
       DenyAll
     </Limit>
-  &lt/Directory>
+  </Directory>
 </pre>
 When starting <code>proftpd</code>, you would see something like:
 <pre>
- - Fatal: <Directory>: <Directory> section already configured for '/path/to/dir' on line 39 of '/etc/ftpd/proftpd.conf'
+ - Fatal: <Directory>: <Directory> section already configured for '/path/to/dir' on line 39 of '/etc/ftpd/proftpd.conf'
 </pre>
 
 <p>
@@ -221,7 +221,7 @@ same path?  For example:
     <Limit ALL>
       DenyAll
     </Limit>
-  &lt/Directory>
+  </Directory>
 
   <Directory /path/*/dir>
     <Limit ALL>
@@ -231,7 +231,7 @@ same path?  For example:
     <Limit WRITE>
       DenyAll
     </Limit>
-  &lt/Directory>
+  </Directory>
 </pre>
 This time, the config parser would not choke; <code>proftpd</code> would start
 up normally.  When it came time to look up the <code><Directory></code>
@@ -255,18 +255,18 @@ However, if you simply reversed the order of the above
     <Limit WRITE>
       DenyAll
     </Limit>
-  &lt/Directory>
+  </Directory>
 
   <Directory /path/to/dir>
     <Limit ALL>
       DenyAll
     </Limit>
-  &lt/Directory>
+  </Directory>
 </pre>
 the upload would succeed, since the non-wildcard-using
 <code><Directory></code> section appeared later in the config.
 
-<p><a name="PreventDirectoryRename">
+<p><a name="PreventDirectoryRename"></a>
 <font color=red>Question</font>: How can I prevent a specific directory from
 being renamed?  I am currently trying:
 <pre>
@@ -314,7 +314,10 @@ list, we make sure that the <code><i>RNFR</i></code> <i>does</i> match the
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2008-12-10 22:40:15 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/DisplayFiles.html b/doc/howto/DisplayFiles.html
index c83ff0f..a6873d9 100644
--- a/doc/howto/DisplayFiles.html
+++ b/doc/howto/DisplayFiles.html
@@ -1,15 +1,13 @@
-<!-- $Id: DisplayFiles.html,v 1.8 2012-05-03 18:29:41 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/DisplayFiles.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Display Files</title>
+<title>ProFTPD: Display Files</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD's <code>Display</code> Files</b></h2></center>
+<center><h2><b>ProFTPD: <code>Display</code> Files</b></h2></center>
 <hr>
 
 <p>
@@ -79,7 +77,7 @@ Other <code>contrib</code> modules may provide additional variables for use
 as well; please consult their documentation for more information.
 
 <p>
-<table border=1>
+<table border=1 summary="Display Variables">
   <tr>
     <td><b>Variable</b></td>
     <td><b>Meaning</b></td>
@@ -255,9 +253,12 @@ and the <code>/etc</code> directory is mounted on a small disk, then
 <code>/etc</code> filesystem, not on other filesystems (<i>e.g.</i> not
 the <code>/home</code> filesystem).
 
-<p>
+<br><hr>
+<font size=2><b><i>
+© Copyright 2012-2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
-<i>$Date: 2012-05-03 18:29:41 $</i><br>
 
 </body>
 </html>
diff --git a/doc/howto/ECCN.html b/doc/howto/ECCN.html
index 151dc24..b88750d 100644
--- a/doc/howto/ECCN.html
+++ b/doc/howto/ECCN.html
@@ -1,6 +1,7 @@
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ECCN</title>
+<title>ProFTPD: ECCN</title>
 </head>
 
 <body bgcolor=white>
@@ -112,7 +113,11 @@ And the text of EAR Section 740, for those interested:
 
 <p>
 <hr>
-<i>$Date: 2013-07-09 17:41:49 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/FTP.html b/doc/howto/FTP.html
index 4798159..9da2e76 100644
--- a/doc/howto/FTP.html
+++ b/doc/howto/FTP.html
@@ -1,15 +1,13 @@
-<!-- $Id: FTP.html,v 1.7 2012-12-27 23:01:25 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/FTP.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD Supported FTP Commands</title>
+<title>ProFTPD: Supported FTP Commands</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Supported FTP Commands</b></h2></center>
+<center><h2><b>ProFTPD: Supported FTP Commands</b></h2></center>
 <hr>
 
 <p>
@@ -20,6 +18,7 @@
 
   <p>
   <li><a name="ALLO"><b><code>ALLO</code></b></a><br>
+    Short for <b>ALLO</b>cate.<br>
   </li>
 
   <p>
@@ -40,6 +39,12 @@
   </li>
 
   <p>
+  <li><a name="CLNT"><b><code>CLNT</code></b></a><br>
+    Short for <b>CL</b>ie<b>NT</b>, this command is used by clients to
+    offer/provide any freeform identification they desire to the server.
+  </li>
+
+  <p>
   <li><a name="CWD"><b><code>CWD</code></b></a><br>
     Short for <b>C</b>hange <b>W</b>orking <b>D</b>irectory.<br>
   </li>
@@ -71,10 +76,24 @@
   </li>
 
   <p>
+  <li><a name="HASH"><b><code>HASH</code></b></a><br>
+    This command is used by clients to request the checksum, or "hash", of
+    a file on the server.  This command is available when the
+    <a href="../contrib/mod_digest.html"><code>mod_digest</code></a> module
+    is compiled/loaded.<br>
+  </li>
+
+  <p>
   <li><a name="HELP"><b><code>HELP</code></b></a><br>
   </li>
 
   <p>
+  <li><a name="HOST"><b><code>HOST</code></b></a><br>
+    This command is the equivalent of HTTP's "Host" header for FTP, providing
+    the ability to have name-based virtual hosting.
+  </li>
+
+  <p>
   <li><a name="LANG"><b><code>LANG</code></b></a><br>
   </li>
 
@@ -462,8 +481,11 @@ and will, be made more informative.)
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2012-12-27 23:01:25 $</i><br>
-</hr>
+<font size=2><b><i>
+© Copyright 2000-2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/FXP.html b/doc/howto/FXP.html
index c4bb90c..753db53 100644
--- a/doc/howto/FXP.html
+++ b/doc/howto/FXP.html
@@ -1,15 +1,13 @@
-<!-- $Id: FXP.html,v 1.2 2004-09-12 23:36:35 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/FXP.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - FXP</title>
+<title>ProFTPD: FXP (Site-to-Site Transfers)</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and FXP</b></h2></center>
+<center><h2><b>ProFTPD: FXP (Site-to-Site Transfers)</b></h2></center>
 <hr>
 
 <p>
@@ -25,14 +23,15 @@ Sometimes "FXP" is referred to as a protocol; in fact, it is not.
 The site-to-site transfer capability was deliberately designed into FTP.
 
 <p>
-<b>Site-to-site Transfers</b><br>
+<b>Site-to-Site Transfers</b><br>
 In a site-to-site transfer, the client logs in to two servers (server A and
 server B).  It then arranges for a file transfer, telling one server (server A)
 that it will be a passive transfer, and the other server (server B) that it
 will be an active transfer.  For a passive transfer, server A will return an
-address/port (via response to the PASV command) to which the client is to
-connect.  The client then passes that address/port in a PORT command to server
-B.  Then, the client sends a RETR to one of the servers and a STOR to the
+address/port (via response to the <code>PASV</code> command) to which the
+client is to connect.  The client then passes that address/port in a
+<code>PORT</code> command to server B.  Then, the client sends a
+<code>RETR</code> to one of the servers and a <code>STOR</code> to the
 other, thus starting the transfer.  The data does not pass to the client
 machine at all.
 
@@ -42,7 +41,7 @@ active and passive FTP data transfers, depending on which server is told to be
 active, which is told to be passive.
 
 <p>
-<b>Example Site-to-site Transfer</b><br>
+<b>Example Site-to-Site Transfer</b><br>
 In the example below, italicized represent responses to the given FTP commands.
 Lines in blue show communications to <font color=blue>server A</font>, while
 those in red are to <font color=red>server B</font>.  Black lines are
@@ -95,8 +94,9 @@ was transferred successfully.
 This example also illustrates that site-to-site transfers use both active
 and passive data transfers; for sites that operate behind firewalls and
 NAT, passive transfers may require extra configuration to operate properly
-(<i>i.e.</i> use of the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_MasqueradeAddress.html"><code>MasqueradeAddress</code></a> and
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_PassivePorts.html"><code>PassivePorts</code></a> configuration directives).
+(<i>i.e.</i> use of the <a href="../modules/mod_core.html#MasqueradeAddress"><code>MasqueradeAddress</code></a> and
+<a href="../modules/mod_core.html#PassivePorts"><code>PassivePorts</code></a>
+configuration directives).
 
 <p>
 <b>"FTP Bounce" Attacks and <code>AllowForeignAddress</code></b><br>
@@ -115,8 +115,7 @@ rejected.
 <p>
 However, some site administrators do want to allow their servers to support
 site-to-site transfers.  ProFTPD must be explicitly configured to allow these
-by using the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_AllowForeignAddress.html"><code>AllowForeignAddress</code></a> configuration
-directive.
+by using the <a href="../modules/mod_core.html#AllowForeignAddress"><code>AllowForeignAddress</code></a> configuration directive.
 
 <p>
 Note that even if <code>AllowForeignAddress</code> is enabled, you may still
@@ -126,7 +125,10 @@ ISPs performing filtering on the FTP port.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2004-09-12 23:36:35 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Filters.html b/doc/howto/Filters.html
index 52a252f..8f4874a 100644
--- a/doc/howto/Filters.html
+++ b/doc/howto/Filters.html
@@ -1,15 +1,13 @@
-<!-- $Id: Filters.html,v 1.13 2013-10-06 06:54:43 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Filters.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Filters</title>
+<title>ProFTPD: Filters</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Using Filters</b></h2></center>
+<center><h2><b>ProFTPD: Filters</b></h2></center>
 <hr>
 
 <p>
@@ -195,7 +193,10 @@ should succeed.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-10-06 06:54:43 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Globbing.html b/doc/howto/Globbing.html
index 6cfa84d..a7f82c6 100644
--- a/doc/howto/Globbing.html
+++ b/doc/howto/Globbing.html
@@ -1,15 +1,13 @@
-<!-- $Id: Globbing.html,v 1.3 2011-11-21 22:24:19 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Globbing.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Globbing</title>
+<title>ProFTPD: Globbing</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Globbing</b></h2></center>
+<center><h2><b>ProFTPD: Globbing</b></h2></center>
 <hr>
 
 <p>
@@ -90,8 +88,9 @@ recommended that globbing be disabled altogether, by adding this to your
 If, on the other hand, your site <i>does</i> need to support globbing (many
 FTP users will assume that globbing is supported), there are other ways of
 limiting the amount of resources used when globbing: the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_RLimitCPU.html"><code>RLimitCPU</code></a> and
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_RLimitMemory.html"><code>RLimitMemory</code></a> configuration directives.  In <code>proftpd-1.2.7</code>, these directives were enhanced so that they could be applied
+<a href="../modules/mod_rlimit.html#RLimitCPU"><code>RLimitCPU</code></a> and
+<a href="../modules/mod_rlimit.html#RLimitMemory"><code>RLimitMemory</code></a>
+configuration directives.  In <code>proftpd-1.2.7</code>, these directives were enhanced so that they could be applied
 strictly to session processes (rather than the daemon process):
 <pre>
   RLimitCPU session ...
@@ -104,7 +103,7 @@ library implementation of globbing).  To change this to a lower number, compile
 <code>proftpd</code> using a <code>configure</code> line that looks
 something like this:
 <pre>
-  ./configure CFLAGS="-DPR_TUNABLE_GLOBBING_MAX_RECURSION=3" ...
+  $ ./configure CFLAGS="-DPR_TUNABLE_GLOBBING_MAX_RECURSION=3" ...
 </pre>
 A globbing expression that contains more than the maximum number of supported
 levels is not executed, but instead an error code signalling
@@ -120,14 +119,17 @@ added: <code>PR_TUNABLE_GLOBBING_MAX_MATCHES</code>.  For sites which really
 do require a higher number of files to be matched for their glob expressions,
 the following <code>configure</code> command can be used:
 <pre>
-  ./configure CFLAGS="-DPR_TUNABLE_GLOBBING_MAX_MATCHES=200000UL" ...
+  $ ./configure CFLAGS="-DPR_TUNABLE_GLOBBING_MAX_MATCHES=200000UL" ...
 </pre>
 A globbing expression that needs to examine more files than this limit will
 have the number of matches silently truncated to the limit (or just below).
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2011-11-21 22:24:19 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/KeepAlives.html b/doc/howto/KeepAlives.html
index cc32f60..7877f94 100644
--- a/doc/howto/KeepAlives.html
+++ b/doc/howto/KeepAlives.html
@@ -1,15 +1,13 @@
-<!-- $Id: KeepAlives.html,v 1.2 2013-03-15 00:05:39 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/KeepAlives.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD KeepAlives</title>
+<title>ProFTPD: KeepAlives</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD KeepAlives</b></h2></center>
+<center><h2><b>ProFTPD: KeepAlives</b></h2></center>
 <hr>
 
 <p>
@@ -26,7 +24,7 @@ Protocols may be layered over the top of another protocol; for example, HTTPS
 consists of HTTP layered over SSL/TLS, itself layered over TCP/IP.  Each
 protocol layer may have its own form of such keepalive functionality.
 
-<p><a name="TCPKeepAlive">
+<p><a name="TCPKeepAlive"></a>
 <b>TCP KeepAlive</b><br>
 For TCP, the definitive specification for keepalive functionality is
 <a href="http://www.faqs.org/rfcs/rfc1122.html">RFC 1122</a>, Section
@@ -43,7 +41,7 @@ connection needlessly.
 OK, so you want to use the TCP keepalive functionality in your program.  The
 question is "How exactly does the TCP keepalive feature work?"  Good question.
 Answering this requires three different numeric values: the <i>idle time</i>,
-the number of probes to send (the probe </i>count</i>), and the <i>interval
+the number of probes to send (the probe <i>count</i>), and the <i>interval
 time</i> between each probe.  Remember, though, that the
 <code>SO_KEEPALIVE</code> TCP socket option <b>must</b> be enabled on the socket
 in order for the TCP keepalive feature to be used.
@@ -382,7 +380,11 @@ and NATs.)
 
 <p>
 <hr>
-<i>$Date: 2013-03-15 00:05:39 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Limit.html b/doc/howto/Limit.html
index 3f4bbd9..79ba816 100644
--- a/doc/howto/Limit.html
+++ b/doc/howto/Limit.html
@@ -1,15 +1,13 @@
-<!-- $Id: Limit.html,v 1.23 2013-05-23 15:43:47 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Limit.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Configuring <Limits></title>
+<title>ProFTPD: Configuring <Limits></title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Configuring <code><Limits></code></b></h2></center>
+<center><h2><b>ProFTPD: Configuring <code><Limits></code></b></h2></center>
 <hr>
 
 <p>
@@ -266,7 +264,6 @@ do this without classes (but still requiring <code>mod_ifsession</code>):
     </Limit>
   </IfUser>
 </pre>
-
 Note that the same effect can be achieved by using the
 <a href="../../contrib/mod_wrap2.html">mod_wrap2</a> module to configure
 user-specific allow/deny files.
@@ -412,7 +409,10 @@ ensuring that it cannot be deleted?  Simply include the <code>RNFR</code> and
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-05-23 15:43:47 $</i><br>
+<font size=2><b><i>
+© Copyright 2000-2016 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/ListOptions.html b/doc/howto/ListOptions.html
index 25667e7..548bc76 100644
--- a/doc/howto/ListOptions.html
+++ b/doc/howto/ListOptions.html
@@ -1,9 +1,7 @@
-<!-- $Id: ListOptions.html,v 1.6 2012-02-02 00:40:13 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/ListOptions.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Directory Lists and ListOptions</title>
+<title>ProFTPD: Directory Lists and ListOptions</title>
 </head>
 
 <body bgcolor=white>
@@ -13,9 +11,10 @@
 <hr>
 
 <p>
-The <code><a href="http://www.proftpd.org/docs/directives/linked/config_ref_ListOptions.html">ListOptions</a></code> directive of ProFTPD can be used
-to control how directory listings are generated.  Directory listings are
-sent in response to the <code>LIST</code> and <code>NLST</code> FTP commands.
+The <code><a href="../modules/mod_ls.html#ListOptions">ListOptions</a></code>
+directive of ProFTPD can be used to control how directory listings are
+generated.  Directory listings are sent in response to the <code>LIST</code>
+and <code>NLST</code> FTP commands.
 
 <p>
 The <code>ListOptions</code> directive supports the following options:
@@ -52,6 +51,8 @@ The <code>ListOptions</code> directive supports the following options:
       <dd>Sort by file size</dd>
   <li><dt>-t</dt>
       <dd>Sort by file modification time</dd>
+  <li><dt>-U<dt>
+      <dd>Do not sort; list entries in directory order</dd>
   <li><dt>-u<dt>
       <dd>Sort by file access time when <code>-t</code> is also used</dd>
 </ul>
@@ -122,6 +123,8 @@ The following keywords are supported, in addition to "strict":
       <dd>Applies the <code>ListOptions</code> only to <code>NLST</code> commands (and not <code>LIST</code> or <code>STAT</code> commands)
   <li><dt>NoErrorIfAbsent</dt>
       <dd>Causes a 226 response code to be returned for <code>LIST/NLST</code> commands for files which do not exist, rather than 450
+  <li><dt>SortedNLST</dt>
+      <dd>Causes the <code>NLST</code> results to be sorted by name
 </ul>
 These keywords were added for finer-grained control over directory listings.
 They make it possible to allow recursive listings and yet still apply limits,
@@ -148,7 +151,11 @@ used to block the <code>LIST</code> and <code>NLST</code> commands altogether.
 
 <p>
 <hr>
-<i>$Date: 2012-02-02 00:40:13 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/LogLevels.html b/doc/howto/LogLevels.html
index 843803b..8f8814a 100644
--- a/doc/howto/LogLevels.html
+++ b/doc/howto/LogLevels.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD Logging: Log Levels</title>
@@ -22,7 +23,7 @@ order.  Thus the <code>DEBUG</code> level has the lowest priority, and the
 <code>EMERG</code> level has the highest priority.
 
 <p>
-<table border=1>
+<table border=1 summary="Log Levels">
   <tr>
     <td><b> Level </b></td>
     <td><b> Description </b></td>
@@ -133,15 +134,11 @@ to trace logging.
 
 <p>
 <hr>
-Last Updated:<i>$Date: 2013-10-06 20:22:58 $</i><br>
-
-<hr>
 <font size=2><b><i>
-© Copyright 2013 The ProFTPD Project<br>
+© Copyright 2013-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/LogMessages.html b/doc/howto/LogMessages.html
index 132779d..a28f13d 100644
--- a/doc/howto/LogMessages.html
+++ b/doc/howto/LogMessages.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD Logging: Log Messages</title>
@@ -185,7 +186,7 @@ on addresses/ports, or necessary cryptographics keys/certs are unusable.
     The configured <a href="AuthFiles.html">AuthUserFile/AuthGroupFile</a>
     has a line which is not in the necessary format.  The most common cause
     for this is when one of the file fields is missing, or if there an extra
-    colon (':') character in a field (<i>e.g.</i> in name field).
+    colon (':') character in a field (<i>e.g.</i> in the name field).
   </li>
 </ul>
 
@@ -236,7 +237,7 @@ idle connections are dropped, <i>etc</i>.
 
 <p><a name="DEBUG">
 <b><code>DEBUG</code> Log Messages</b><br>
-There are many <code>EMERG</code>-level messages logged by
+There are many <code>DEBUG</code>-level messages logged by
 <code>proftpd</code>.  This section will be filled, over time, with the
 ones most commonly seen/asked about by users.
 
@@ -249,10 +250,10 @@ The table below lists the reason strings you may commonly see, with a fuller
 description of what it means.
 
 <p>
-<table border=1>
+<table border=1 summary="Error Messages">
   <tr>
     <td><b>Message</b></td>
-    <td><b>Code<b></td>
+    <td><b>Code</b></td>
     <td><b>Details</b></td>
   </tr>
 
@@ -366,15 +367,11 @@ description of what it means.
 
 <p>
 <hr>
-Last Updated:<i>$Date: 2013-11-10 02:34:15 $</i><br>
-
-<hr>
 <font size=2><b><i>
-© Copyright 2013 The ProFTPD Project<br>
+© Copyright 2013-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Logging.html b/doc/howto/Logging.html
index 738a417..6cb358e 100644
--- a/doc/howto/Logging.html
+++ b/doc/howto/Logging.html
@@ -1,15 +1,13 @@
-<!-- $Id: Logging.html,v 1.13 2013-10-09 16:46:37 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Logging.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD Logging</title>
+<title>ProFTPD: Logging</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>ProFTPD Logging</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Logging</i></b></h2></center>
 <hr>
 
 <p>
@@ -72,12 +70,143 @@ generate: <code>TransferLog</code>, <code>SystemLog</code>, and
 <code>ExtendedLog</code>.
 
 <p><a name="TransferLog"></a>
+<b><i><code>TransferLog</code></i></b><br>
 A <a href="../modules/mod_core.html#TransferLog"><code>TransferLog</code></a>
-is the most common log kept, recording file transfers.  Its format is described
-in the <code>xferlog(5)</code> man page,
-also available <a href="http://www.castaglia.org/proftpd/doc/xferlog.html">here</a>.
+is the most common log kept, recording file transfers.  It is written in the
+<code>xferlog(5)</code> format, described here, <b>and</b> in the
+<code>xferlog(5)</code> man page.
+
+<p>
+Each entry in the <code>TransferLog</code> is a single line of space-delimited
+fields:
+<table border=1 summary="TransferLog Fields">
+  <tr>
+    <td> <b>Field</b></td>
+    <td> <b>Values</b></td>
+    <td> <b>Description</b></td>
+  </tr>
+
+  <tr>
+    <td> <em>current-time</em></td>
+    <td> DDD MMM dd hh:mm:ss YYYY</td>
+    <td> Current <b>local</b> time at the time of transfer</td>
+  </tr>
+
+  <tr>
+    <td> <em>transfer-time</em></td>
+    <td> Seconds</td>
+    <td> Total time in <b>seconds</b> for the transfer</td>
+  </tr>
+
+  <tr>
+    <td> <em>remote-host</em></td>
+    <td> String</td>
+    <td> Remote client DNS name (or IP address)</td>
+  </tr>
+
+  <tr>
+    <td> <em>file-size</em></td>
+    <td> Bytes</td>
+    <td> Number of bytes transferred</td>
+  </tr>
+
+  <tr>
+    <td> <em>filename</em></td>
+    <td> String</td>
+    <td> Name of the transferred file</td>
+  </tr>
+
+  <tr>
+    <td> <em>transfer-type</em></td>
+    <td> Flags</td>
+    <td> Indicates whether the transfer was an ASCII (<code>"a"</code>) or
+        binary (<code>"b"</code>) transfer</td>
+  </tr>
+
+  <tr>
+    <td> <em>special-action-flag</em></td>
+    <td> Flags</td>
+    <td> One or more single character indicating any special action taken: "C" (compressed), "U" (uncompressed), "T" (tarred), "_" (no action)</td>
+  </tr>
+
+  <tr>
+    <td> <em>direction</em></td>
+    <td> Flags</td>
+    <td> Direction of the transfer: "o" (outgoing, <i>i.e.</i> download), "i" (incoming, <i>i.e.</i> upload), "d" (deleted)</td>
+  </tr>
+
+  <tr>
+    <td> <em>access-mode</em></td>
+    <td> Flags</td>
+    <td> Method by which the user is logged in: "a" (anonymous), "g" (guest), "r" (real user)</td>
+  </tr>
+
+  <tr>
+    <td> <em>username</em></td>
+    <td> String</td>
+    <td> Authenticated username</td>
+  </tr>
+
+  <tr>
+    <td> <em>service-name</em></td>
+    <td> String</td>
+    <td> Name of the service used: "ftp", "sftp", "scp", <i>etc</i></td>
+  </tr>
+
+  <tr>
+    <td> <em>authentication-method</em></td>
+    <td> Flags</td>
+    <td> Method of authentication used: "0" (none), "1" (<a href="https://www.ietf.org/rfc/rfc1413.txt">RFC 1413</a>)</td>
+  </tr>
+
+  <tr>
+    <td> <em>authenticated-user-id</em></td>
+    <td> Flags</td>
+    <td> Name of user provided by RFC 1413 lookup, otherwise "*"</td>
+  </tr>
+
+  <tr>
+    <td> <em>completion-status</em></td>
+    <td> Flags</td>
+    <td> Single character indicating the status of the transfer: "c" (complete), "i" (incomplete)</td>
+  </tr>
+</table>
+
+<p>
+<b>History of the <code>xferlog(5)</code> Format</b><br>
+This xferlog(5) format seems a bit odd, right?  To understand this, it helps
+to keep in mind the history of this format.  The xferlog(5) format
+<b>predates</b> ProFTPD.  The ProFTPD Project copied this format from
+<a href="http://wu-ftpd.therockgarden.ca/man/xferlog.html">wu-ftpd</a>, which
+was <em>the</em> popular FTP server at that time.  There were already existing
+tools/scripts which knew how to parse that format, so ProFTPD used it.  Since
+then, <code>wu-ftpd</code> has waned in maintenance and popularity; other
+FTP servers (<i>e.g.</i> <code>pure-ftpd</code>, <code>vsftpd</code>, but
+<b>not</b> the built-in <code>ftpd</code> on FreeBSD) have since then picked
+up the <code>xferlog(5)</code> format from ProFTPD.  Thus this log format has
+a 20+ year history, and keeps going.
+
+<p>
+This history helps explain certain fields in the <code>xferlog(5)</code>
+format, such as the <em>authentication-method</em> and
+<em>authenticated-user-id</em> fields.  These fields were more relevant in
+the past, when the RFC 1413 Identd protocol was more widespread in use.  Now,
+<b>very</b> few sites run <code>identd</code> servers; these fields in the
+log format are thus archaic.  Thus in almost all ProFTPD-generated
+<code>TransferLog</code> entries, <em>authentication-method</em> will be "0",
+and <em>authenticated-user-id</em> will be "*".
+
+<p>
+Likewise, the <em>special-action-flags</em> field mentions "compressed" and
+"uncompressed" files; these flags specifically refer to the
+<a href="https://en.wikipedia.org/wiki/Compress"><code>compress</code></a>
+Unix program, which used the ".Z" file extension.  This compression utility
+has been superceded with <i>e.g.</i> <code>gzip</code>, <code>bzip2</code>,
+and others these days.  Thus the <em>special-action-flags</em> field in
+ProFTPD-generated <code>TransferLog</code> entries is almost always "_".
 
 <p><a name="SystemLog"></a>
+<b><i><code>SystemLog</code></i></b><br>
 If the site administrator wants to have <code>proftpd</code> log its messages
 to a file rather than going through <code>syslogd</code>, the
 <a href="../modules/mod_log.html#SystemLog"><code>SystemLog</code></a>
@@ -90,6 +219,7 @@ directive only applies to <code>SystemLog</code> files; it does not materially
 affect the syslog-based logging messages.
 
 <p><a name="ExtendedLog"></a>
+<b><i><code>ExtendedLog</code></i></b><br>
 The <a href="../modules/mod_log.html#ExtendedLog"><code>ExtendedLog</code></a>
 directive is used to create log files of a very flexible and configurable
 format, and to have granular control over what is logged, and when.  The format
@@ -101,7 +231,7 @@ Multiple <code>ExtendedLogs</code> can be configured, each with a different
 <!-- Add note/chunk about FTP response codes, from RFC959, for ExtendedLog? -->
 
 <p><a name="SyslogVSFileLog"></a>
-<b>Use of syslog versus file logging</b><br>
+<b>Use of <code>syslog</code> versus file logging</b><br>
 Most sites will choose to have <code>proftpd</code> log via syslog (which is
 the default) or to a file (via the <code>SystemLog</code> directive).  In
 either case, there is the question of logging <i>verbosity</i>, <i>i.e.</i>
@@ -251,10 +381,10 @@ logging".  This form of logging is covered in greater detail
 <b>Pid File</b><br>
 On startup, <code>proftpd</code> saves the process ID of the parent daemon
 process to the file <code>var/proftpd/proftpd.pid</code>. This filename can be
-changed with the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_PidFile.html">PidFile</a> directive. The process ID (<i>aka</i> PID) is for
-use by the administrator in restarting and terminating the daemon by sending
-signals to the parent process.  For more information see the
-<a href="Stopping.html">stopping and starting</a> page.
+changed with the <a href="../modules/mod_core.html#PidFile"><code>PidFile</code></a> directive. The process ID (<i>aka</i> PID) is for use by the administrator
+in restarting and terminating the daemon by sending signals to the parent
+process.  For more information see the <a href="Stopping.html">stopping and
+starting</a> page.
 
 <p>
 <b>Scoreboard File</b><br>
@@ -263,7 +393,7 @@ scoreboard is binary-formatted file the server uses to store information
 about each session; it is this file that is read by <code>ftptop</code>,
 <code>ftpwho</code> and <code>ftpcount</code>.  The location for the
 scoreboard file is determined by the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_ScoreboardFile.html"><code>ScoreboardFile</code></a> directive.
+<a href="../modules/mod_core.html#ScoreboardFile"><code>ScoreboardFile</code></a> directive.
 
 <p><a name="FAQ"></a>
 <b>Frequently Asked Questions</b><br>
@@ -381,7 +511,9 @@ please find their related man pages.
 
 <p>
 Now to make the "No such file or directory" log message go away, simply tell
-<code>proftpd</code> to stop trying to use <code>wtmp</code> logging by using:
+<code>proftpd</code> to stop trying to use <code>wtmp</code> logging by using
+the <a href="../modules/mod_auth.html#WtmpLog"><code>WtmpLog</code></a>
+directive:
 <pre>
   WtmpLog off
 </pre>
@@ -389,9 +521,11 @@ in your <code>proftpd.conf</code>.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-10-09 16:46:37 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
 </html>
-
diff --git a/doc/howto/Memcache.html b/doc/howto/Memcache.html
index 20a2d9b..51e9d9a 100644
--- a/doc/howto/Memcache.html
+++ b/doc/howto/Memcache.html
@@ -1,15 +1,13 @@
-<!-- $Id: Memcache.html,v 1.2 2013-05-09 05:28:28 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Memcache.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD and Memcache</title>
+<title>ProFTPD: Memcache</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and Memcache</b></h2></center>
+<center><h2><b>ProFTPD: Memcache</b></h2></center>
 <hr>
 
 <p>
@@ -87,7 +85,7 @@ up your memcached servers for ProFTPD, you can enable trace logging for the
 
 <p>
 <b>Using Memcache for Shared Storage</b><br>
-You have now compiled support for memcached into proftpd, and you have told the
+You have now compiled support for memcached into ProFTPD, and you have told the
 <code>mod_memcache</code> module where to find your <code>memcached</code>
 servers.  Is that all you need to do?  No.  Now you need to tell
 <code>proftpd</code> modules which bits of data to store in your
@@ -135,7 +133,7 @@ can be quite beneficial.
 
 <p>
 To use <code>memcached</code> for SSL/TLS session caching, then, you use the
-<code><a href="../contrib/mod_tls.html#TLSSessionCache">TLSSessionCache</code></a> directive of the <code>mod_tls</code> module, using something like this
+<a href="../contrib/mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a> directive of the <code>mod_tls</code> module, using something like this
 in your <code>proftpd.conf</code>:
 <pre>
   <IfModule mod_tls.c>
@@ -159,14 +157,14 @@ knows how to talk to the <code>memcached</code> servers using
 <font color=red>Question</font>: If I don't use memcache, are there other
 ways for sharing data (such as ban lists) among different <code>proftpd</code>
 instances?<br>
-<font color=blue>Answer</font>: Not really.  It might be possible using
-<code>mod_sql</code> and some <code>SQLLogInfo</code> directives, but that
-would only work for very specific information.  For sharing things like ban
-lists and SSL/TLS sessions across a cluster of <code>proftpd</code> servers,
-memcache support is <b>required</b>.
+<font color=blue>Answer</font>: It might be possible using <code>mod_sql</code>
+and some <code>SQLLogInfo</code> directives, but that would only work for very
+specific information.  For sharing things like ban lists and SSL/TLS sessions
+across a cluster of <code>proftpd</code> servers, Memcache (or
+<a href="Redis.html">Redis</a>) support is <em>recommended</em>.
 
 <p>
-<font color=red>Question</font>: Can I use <code>mod_memcache</code> to cache rerequently accessed files, similar to <code><a href="http://wiki.nginx.org/HttpMemcachedModule">nginx+memcache</a></code>?<br>
+<font color=red>Question</font>: Can I use <code>mod_memcache</code> to cache frequently accessed files, similar to <code><a href="http://wiki.nginx.org/HttpMemcachedModule">nginx+memcache</a></code>?<br>
 <font color=blue>Answer</font>: No.  And in reality, caching of files like that
 will probably not give you the same performance gain for FTP transfers as it
 can for HTTP transfers.
@@ -192,7 +190,7 @@ yourself in this situation, and we will see what can be done to help.
 <font color=red>Question</font>: Why do I see the following error when
 <code>proftpd</code> starts up?<br>
 <pre>
-mod_tls_memcache/0.1: notice: unable to register 'memcache' SSL session cache: Memcache support not enabled  
+  mod_tls_memcache/0.1: notice: unable to register 'memcache' SSL session cache: Memcache support not enabled
 </pre>
 <font color=blue>Answer</font>: This message means that your
 <code>proftpd</code> server has <code>mod_tls_memcache</code> built and
@@ -206,7 +204,11 @@ some of your modules want to use a feature that was not enabled.
 
 <p>
 <hr>
-<i>$Date: 2013-05-09 05:28:28 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/NAT.html b/doc/howto/NAT.html
index 17551f8..0cfc29d 100644
--- a/doc/howto/NAT.html
+++ b/doc/howto/NAT.html
@@ -1,15 +1,13 @@
-<!-- $Id -->
-<!-- $Source -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Firewalls, Routers, and NAT</title>
+<title>ProFTPD: Firewalls, Routers, and NAT</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Firewalls, Routers, and NAT</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Firewalls, Routers, and NAT</i></b></h2></center>
 <hr>
 
 <p>
@@ -42,7 +40,7 @@ or search for information concerning your OS of choice.
 <b>Configuring ProFTPD behind NAT</b><br>
 First configure your installed <code>proftpd</code> so that it works correctly
 from inside the NAT. There are example configuration files included with the
-source.  Then add the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_MasqueradeAddress.html"><code>MasqueradeAddress</code></a> directive
+source.  Then add the <a href="../modules/mod_core.html#MasqueradeAddress"><code>MasqueradeAddress</code></a> directive
 to your <code>proftpd.conf</code> file to define the public name or IP address
 of the NAT.  For example:
 <pre>
@@ -61,7 +59,7 @@ For a good description of active versus passive FTP data transfers, see:
 <pre>
   <a href="http://slacksite.com/other/ftp.html">http://slacksite.com/other/ftp.html</a>
 </pre>
-To resolve this, simply use the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_PassivePorts.html"><code>PassivePorts</code></a> directive
+To resolve this, simply use the <a href="../modules/mod_core.html#PassivePorts"><code>PassivePorts</code></a> directive
 in your <code>proftpd.conf</code> to control what ports <code>proftpd</code>
 will use for its passive data transfers:
 <pre>
@@ -90,21 +88,21 @@ that your FTP server has local address <code>192.168.1.2</code>.
 <p>
 First we need to enable NAT for our FTP server. As <code>root</code>:
 <pre>
-  echo "1">/proc/sys/net/ipv4/ip_forward
-  ipchains -P forward DENY
-  ipchains -I forward -s 192.168.1.2 -j MASQ
+  $ echo "1">/proc/sys/net/ipv4/ip_forward
+  $ ipchains -P forward DENY
+  $ ipchains -I forward -s 192.168.1.2 -j MASQ
 </pre>
 Now we load the <code>autofw</code> kernel module and forward ports 20 and 21
 to the FTP server:
 <pre>
-  insmod ip_masq_autofw
-  ipmasqadm autofw -A -r tcp 20 21 -h 192.168.1.2
+  $ insmod ip_masq_autofw
+  $ ipmasqadm autofw -A -r tcp 20 21 -h 192.168.1.2
 </pre>
 Then we forward ports for passive FTP transfers. In our
 <code>proftpd.conf</code> file we restricted passive transfers to ports
 60000-65535, so that is what we use here as well:
 <pre>
-  ipmasqadm autofw -A -r tcp 60000 65535 -h 192.168.1.2
+  $ ipmasqadm autofw -A -r tcp 60000 65535 -h 192.168.1.2
 </pre>
 
 <p>
@@ -117,7 +115,7 @@ like the following.  First, update your <code>ipf.conf</code> with:
 </pre>
 Then make sure that the changes take effect by using:
 <pre>
-  ipf -Fa -f /path/to/ipf.conf
+  $ ipf -Fa -f /path/to/ipf.conf
 </pre>
 
 <p>
@@ -129,7 +127,7 @@ you are still able to setup relatively tight firewalling rules.  To be sure
 that you have no other processes listening on the ports you have specified
 for passive transfers, use a port scanner such as <code>nmap</code>:
 <pre>
-  nmap -sT -I -p 60000-65535 localhost
+  $ nmap -sT -I -p 60000-65535 localhost
 </pre>
 If the result says something like:
 <pre>
@@ -272,8 +270,15 @@ For those that need to see a concrete example configuration of this:
 <p>
 <hr>
 Contributor: Tobias Ekbom <tobias <i>at</i> vallcom <i>dot</i>com><br>
-Last Updated: <i>$Date: 2012-05-22 16:31:39 $</i>
 <hr>
 
-<body>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
 </html>
diff --git a/doc/howto/Nonroot.html b/doc/howto/Nonroot.html
index 61d2958..2d4f28e 100644
--- a/doc/howto/Nonroot.html
+++ b/doc/howto/Nonroot.html
@@ -1,15 +1,13 @@
-<!-- $Id: Nonroot.html,v 1.1 2009-10-26 15:39:34 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Nonroot.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Running by nonroot user</title>
+<title>ProFTPD: Running by Nonroot User</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Running ProFTPD as a Nonroot User</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Running as a Nonroot User</i></b></h2></center>
 <hr>
 
 <p>
@@ -43,8 +41,8 @@ to run the server without root privileges:<br>
   <code>/etc/shadow</code> for the password.  Comparing stored passwords
   requires root privileges, which this nonroot-running daemon will not have.
   You can get around this requirement by supplying your own passwd (and
-  possibly group) files via the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_AuthUserFile.html"><code>AuthUserFile</code></a> and
-  <a href="http://www.proftpd.org/docs/directives/linked/config_ref_AuthGroupFile.html"><code>AuthGroupFile</code></a> directives.  Make sure the permissions on your
+  possibly group) files via the <a href="../modules/mod_auth_file.html#AuthUserFile"><code>AuthUserFile</code></a> and
+  <a href="../modules/mod_auth_file.html#AuthGroupFile"><code>AuthGroupFile</code></a> directives.  Make sure the permissions on your
   custom files allow for the daemon to read them (but hopefully not other
   users).
 
@@ -108,8 +106,8 @@ to run the server without root privileges:<br>
 
   <li><b><code>User, Group</code></b><br>
   The ability to switch the identity of the server process to those configured
-  by the <a href="http://www.proftpd.org/docs/linked/config_ref_User.html"><code>User</code></a> and
-  <a href="http://www.proftpd.org/docs/linked/config_ref_Group.html"><code>Group</code></a> directives requires, of course, root privileges.  It is best to
+  by the <a href="../modules/mod_core.html#Userl"><code>User</code></a> and
+  <a href="../modules/mod_core.html#Group"><code>Group</code></a> directives requires, of course, root privileges.  It is best to
   configure <code>User</code> to be your username, and <code>Group</code> to
   be the name of your primary group (which is usually the first group listed
   by the <code>groups</code> command).
@@ -139,6 +137,13 @@ The daemon should now start successfully.  Complaints about not being able
 to switch UIDs and such will be logged, but the daemon should still function
 properly.
 
+<p>
 <hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </body>
 </html>
diff --git a/doc/howto/Quotas.html b/doc/howto/Quotas.html
index 0b4c78e..9508f5b 100644
--- a/doc/howto/Quotas.html
+++ b/doc/howto/Quotas.html
@@ -1,15 +1,13 @@
-<!-- $Id: Quotas.html,v 1.8 2009-07-23 15:04:05 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Quotas.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Quotas</title>
+<title>ProFTPD: Quotas</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Quotas</b></h2></center>
+<center><h2><b>ProFTPD: Quotas</b></h2></center>
 <hr>
 
 <p>
@@ -53,20 +51,14 @@ quotas via file tables and SQL tables.  <b>This is an example only</b>.
     QuotaLog /var/log/ftpd/quota.log
 
     # For more information on using files for storing the limit and tally
-    # table quota data, please see the mod_quotatab_file documentation:
-    #
-    #   <a href="../contrib/mod_quotatab_file.html">http://www.castaglia.org/proftpd/modules/mod_quotatab_file.html</a>
-    #
+    # table quota data, please see the <a href="../contrib/mod_quotatab_file.html">mod_quotatab_file</a> documentation.
     <IfModule mod_quotatab_file.c>
       QuotaLimitTable file:/etc/ftpd/ftpquota.limittab
       QuotaTallyTable file:/etc/ftpd/ftpquota.tallytab
     </IfModule>
 
     # For more information on using a SQL database for storing the limit and
-    # tally table quota data, please see the mod_quotatab_file documentation:
-    #
-    #   <a href="../contrib/mod_quotatab_sql.html">http://www.castaglia.org/proftpd/modules/mod_quotatab_sql.html</a>
-    #
+    # tally table quota data, please see the <a href="../contrib/mod_quotatab_sql.html">mod_quotatab_sql</a> documentation
     <IfModule mod_quotatab_sql.c>
       SQLNamedQuery get-quota-limit SELECT "* FROM quotalimits WHERE name = '%{0}' AND quota_type = '%{1}'"
       SQLNamedQuery get-quota-tally SELECT "* FROM quotatallies WHERE name = '%{0}' AND quota_type = '%{1}'"
@@ -77,7 +69,7 @@ quotas via file tables and SQL tables.  <b>This is an example only</b>.
       QuotaLimitTable sql:/get-quota-limit
       QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally
     </IfModule>
-  &lt/IfModule>
+  </IfModule>
 </pre>
 
 <p><a name="FAQ" title="#FAQ"></a>
@@ -169,14 +161,8 @@ automatically update your tally table.
 
 <p><a name="QuotasDefaults" title="#QuotasDefaults"></a>
 <font color=red>Question</font>: How can I set a default quota for all of my users?<br>
-<font color=blue>Answer</font>: Unfortunately, there is no way currently
-to do this.  The <code>mod_quotatab</code> module was designed such that
-the administrator would have to explicitly create limits for every user.
-
-<p>
-However, a <code>mod_quotatab_default</code> module could be written to
-provide default quotas.  I simply do not know if this would be desirable
-enough to users for writing the module.
+<font color=blue>Answer</font>: For this, you can use the <a href="../contrib/mod_quotatab.html#QuotaDefault"><code>QuotaDefault</code></a> directive, which
+first appeared in ProFTPD 1.3.5rc1.
 
 <p><a name="QuotasTallyTable" title="#QuotasTallyTable"></a>
 <font color=red>Question</font>: What is a "tally table"?<br>
@@ -185,7 +171,7 @@ covered in the <code>mod_quotatab</code> <a href="../contrib/mod_quotatab.html#Q
 
 <p><a name="QuotasCreatingFileTables" title="#QuotasCreatingFileTables"></a>
 <font color=red>Question</font>: How do I construct the limit and tally files for file-based quotas?<br>
-<font color=blue>Answer</font>: There is a Perl script called <a href="http://www.castaglia.org/proftpd/contrib/ftpquota.html"><code>ftpquota</code></a> which
+<font color=blue>Answer</font>: There is a Perl script called <a href="http://www.proftpd.org/docs/contrib/ftpquota.html"><code>ftpquota</code></a> which
 can create the necessary files.  This script can also be found under the
 <code>contrib/</code> directory of the <code>proftpd</code> source
 distribution.
@@ -255,8 +241,8 @@ certain <a href="../contrib/mod_quotatab.html#QuotaDisplay"><code>Display</code>
 individual files being transferred?<br>
 <font color=blue>Answer</font>: For this, you do not need the
 <code>mod_quotatab</code> module.  ProFTPD has the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_MaxRetrieveFileSize.html"><code>MaxRetrieveFileSize</code></a> and
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_MaxStoreFileSize.html"><code>MaxStoreFileSize</code></a> directives.
+<a href="../modules/mod_xfer.html#MaxRetrieveFileSize"><code>MaxRetrieveFileSize</code></a> and
+<a href="../modules/mod_xfer.html#MaxStoreFileSize"><code>MaxStoreFileSize</code></a> directives.
 
 <p><a name="QuotasUnsupportedType" title="#QuotasUnsupportedType"></a>
 <font color=red>Question</font>: Why do I see the following error?
@@ -298,7 +284,8 @@ are checked is dependent up on the order in which the groups are returned
 from the auth module providing them, <i>e.g.</i> <code>mod_sql</code>,
 <code>mod_ldap</code>, or perhaps <code>mod_auth_unix</code>.
 
-<p><a name="QuotasOverwrites" title="#QuotasOverwrites"></a><font color=red>Question</font>: Does <code>mod_quotatab</code> handle the case where a client might append to/overwrite an existing file, in terms of tracking bytes?<br>
+<p><a name="QuotasOverwrites" title="#QuotasOverwrites"></a>
+<font color=red>Question</font>: Does <code>mod_quotatab</code> handle the case where a client might append to/overwrite an existing file, in terms of tracking bytes?<br>
 <font color=blue>Answer</font>: Yes.  The <code>mod_quotatab</code> module
 checks the size of the file before the upload/append starts, and then checks
 the size of the file again after the upload/append completes.  The difference
@@ -320,9 +307,41 @@ ends up not changing any tally value for a delete.  More details on this
 can be found in
 <a href="http://bugs.proftpd.org/show_bug.cgi?id=2897">Bug#2897</a>.
 
+<p><a name="QuotasZeroLimits" title="#QuotasZeroLimits"></a>
+<font color=red>Question</font>: Why does the <code>ftpquota</code> tool
+treat limit values of zero as "unlimited"?
+<pre>
+  $ ftpquota --create-table --type=limit
+  $ ftpquota --add-record --type=limit --name=tj --quota-type=user <b>--files-download=0</b>
+  $ ftpquota --show-records --type=limit
+  -------------------------------------------
+    Name: tj
+    Quota Type: User
+    Per Session: False
+    Limit Type: Hard
+      Uploaded bytes: unlimited
+      Downloaded bytes: unlimited
+      Transferred bytes:  unlimited
+      Uploaded files: unlimited
+      <b>Downloaded files: unlimited</b>
+      Transferred files:  unlimited
+</pre>
+I want to use this to prevent a user from uploading/download files.<br>
+<p>
+<font color=blue>Answer</font>:  If you wish to prevent users from uploading
+or downloading, then you should use
+<a href="Limit.html"><code><Limit></code></a> sections in your
+ProFTPD configuration; that is what they are designed to do.  The
+<code>ftpquota</code> tool was <em>explicitly designed</em> to not allow
+being used to prevent uploads/downloads; it is for managing <em>quotas</em>.
+
 <p>
 <hr>
-<i>$Date: 2009-07-23 15:04:05 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Radius.html b/doc/howto/Radius.html
index 82edb85..8af29b2 100644
--- a/doc/howto/Radius.html
+++ b/doc/howto/Radius.html
@@ -1,15 +1,13 @@
-<!-- $Id: Radius.html,v 1.1 2014-01-27 01:28:21 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Radius.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD and RADIUS</title>
+<title>ProFTPD: RADIUS</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and RADIUS</b></h2></center>
+<center><h2><b>ProFTPD: RADIUS</b></h2></center>
 <hr>
 Some sites use the <a href="http://en.wikipedia.org/wiki/RADIUS">RADIUS</a>
 protocol for authenticating users.  For these sites, there is the
@@ -56,6 +54,8 @@ packet to the RADIUS server, which will include the following attributes:
   NAS-Port <em>server-port</em>
   NAS-Port-Type 5 (<i>i.e.</i> Virtual)
   Calling-Station-Id <em>client-ip-address</em>
+  Acct-Session-Id <em>session-pid</em>
+  Message-Authenticator <em>mac</em>
 </pre>
 If the RADIUS server responds with an <code>Access-Accept</code> packet, then
 the login succeeds, and the FTP session is establish.  If, on the other hand,
@@ -91,6 +91,7 @@ which includes:
   <b>Acct-Status-Type 1</b> (<i>i.e.</i> Start)
   Acct-Session-Id <em>session-pid</em>
   Acct-Authentic 1 (<i>i.e.</i> Local)
+  Event-Timestamp <em>timestamp</em>
 </pre>
 Then, when the client disconnects, <code>mod_radius</code> sends another
 <code>Accounting-Request</code> packet, this time with a lot of information
@@ -103,6 +104,9 @@ about the just-ended session:
   Acct-Session-Time <em>session-duration</em>
   Acct-Input-Octets <em>bytes-in</em>
   Acct-Output-Octets <em>bytes-out</em>
+  Acct-Terminate-Cause <em>cause</em>
+  Event-Timestamp <em>timestamp</em>
+  Class <em>class</em> (if provided in <code>Access-Accept</code>)
 </pre>
 
 <p>
@@ -150,6 +154,8 @@ different <code>Service-Type</code> attribute.  Now the packet will look like:
   NAS-Port <em>server-port</em>
   NAS-Port-Type 5 (<i>i.e.</i> Virtual)
   Calling-Station-Id <em>client-ip-address</em>
+  Acct-Session-Id <em>session-pid</em>
+  Message-Authenticator <em>mac</em>
 </pre>
 
 <p>
@@ -170,7 +176,7 @@ With this background, we can explain the <code>RadiusUserInfo</code> and
 <code>mod_radius</code> the vendor ID to look for, using the
 <a href="../contrib/mod_radius.html#RadiusVendor"><code>RadiusVendor</code></a>
 directive:
-</pre>
+<pre>
   RadiusVendor Unix 4
 </pre>
 The above is actually not necessary; <code>mod_radius</code> will look for
@@ -273,7 +279,7 @@ directives:
 </pre>
 which match up with our <code>RadiusUserInfo</code> parameters:
 <pre>
-  RadiusUserInfo $(<b><i>10</i></b>:1000) $(<b></i>11</i></b>:1000) $(<b><i>12</i></b>:/tmp) $(<b><i>13</i></b>:/bin/bash)
+  RadiusUserInfo $(<b><i>10</i></b>:1000) $(<b><i>11</i></b>:1000) $(<b><i>12</i></b>:/tmp) $(<b><i>13</i></b>:/bin/bash)
 </pre>
 
 <p>
@@ -408,7 +414,10 @@ using <code>mod_radius</code>.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2014-01-27 01:28:21 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Redis.html b/doc/howto/Redis.html
new file mode 100644
index 0000000..21ec448
--- /dev/null
+++ b/doc/howto/Redis.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD: Redis</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center><h2><b>ProFTPD: Redis</b></h2></center>
+<hr>
+
+<p>
+<b>What is Redis?</b><br>
+<a href="https://redis.io/">Redis</a> is an open-source, high performance
+memory object caching system.  A simple (and effective) key/value store
+accessible, efficiently, over the network.
+
+<p>
+<b>How Can Redis Be Useful for ProFTPD?</b><br>
+Like any high-performance object store, Redis offers several possibilities to a
+server like ProFTPD.  Many sites use Redis for caching; it can <i>also</i> be
+used as an efficient shared storage mechanism, for sharing data among many
+different servers.  And for ProFTPD specifically, the shared storage aspect is
+what is most useful.  Things like SSL/TLS sessions can be cached and shared
+across a cluster of <code>proftpd</code> servers, as can ban lists for
+badly-behaved clients.
+
+<p>
+<b>Enabling Redis Support for ProFTPD</b><br>
+OK, so you are interested enough in the possibilities that Redis offers that
+you want to try it out.  Excellent!  To do this, you will first need to make
+sure to build your <code>proftpd</code> executable using the
+<code>--enable-redis</code> configure option.  The <code>--enable-redis</code>
+configure option automatically adds the
+<code><a href="../modules/mod_redis.html">mod_redis</a></code> module to
+your <code>proftpd</code> build.
+
+<p>
+The <code>mod_redis</code> module uses the <code><a href="https://github.com/redis/hiredis">hiredis</a></code> library for talking to Redis servers.  If your
+<code>hiredis</code> library is installed in a non-standard location, you may
+need to tell the ProFTPD build system where to find the <code>hiredis</code>
+header files and libraries using the <code>--with-includes</code> and
+<code>--with-libraries</code> configure options.
+
+<p>
+There are other modules which make use of Redis support when available, such as
+<code><a href="../contrib/mod_tls_redis.html">mod_tls_redis</a></code>.
+Thus to take advantage of modules like this, putting everything together, your
+configure command might look like this:
+<pre>
+  $ ./configure --enable-redis \
+    --with-modules=...:mod_tls_redis:... \
+    --with-includes=/path/to/hiredis/include \
+    --with-libraries=/path/to/hiredis/lib
+</pre>
+
+<p>
+<b>Configuring <code>mod_redis</code></b><br>
+Now that you have compiled <code>proftpd</code> with the <code>mod_redis</code>
+module, you need to add the necessary <code>mod_redis</code> directives to
+your <code>proftpd.conf</code>.  The following example demonstrates this:
+<pre>
+  <IfModule mod_redis.c>
+    # Enable mod_redis
+    RedisEngine on
+
+    # Tell mod_redis where to log its messages
+    RedisLog /path/to/proftpd/redis.log
+
+    # Tell mod_redis where to find the Redis server
+    RedisServer 192.168.0.10:6379
+  </IfModule>
+</pre>
+If you wish to see more detailed logging, at least while you are setting up
+your Redis servers for ProFTPD, you can enable trace logging for the
+<code>redis</code> trace channel using <i>e.g.</i>:
+<pre>
+  TraceLog /path/to/proftpd/trace.log
+  Trace DEFAULT:10 redis:20
+</pre>
+
+<p>
+<b>Using Redis for Shared Storage</b><br>
+You have now compiled support for Redis into ProFTPD, and you have told the
+<code>mod_redis</code> module where to find your Redis servers.  Is that all
+you need to do?  No.  Now you need to tell <code>proftpd</code> modules which
+bits of data to store in your Redis server.
+
+<p>
+Currently, only two modules can take advantage of Redis support:
+<code><a href="../contrib/mod_ban.html">mod_ban</a></code> and
+<code><a href="../contrib/mod_tls_redis.html">mod_tls_redis</a></code>.
+
+<p>
+First, let us examine <code>mod_ban</code> and how it would use Redis.  The
+<code>mod_ban</code> module manages ban lists, lists of clients/users which
+have been banned for various reasons.  These lists are stored in shared memory
+by default; this works for a single <code>proftpd</code> server, but if a badly
+behaved client is banned by one <code>proftpd</code> server in pool of servers,
+that client can then connect to a different server which might not have a ban
+for that client -- and the client then gets another chance to be naughty.  To
+configure <code>mod_ban</code> so that it stores its ban lists in Redis,
+simply use the following in your <code>proftpd.conf</code>:
+<pre>
+  <IfModule mod_ban.c>
+    BanEngine on
+
+    # ...other mod_ban directives...
+
+    # Tell mod_ban to store its ban lists using Redis
+    BanCache redis
+  </IfModule>
+</pre>
+With this, <code>mod_ban</code> will use Redis (as well as shared memory) for
+reading/writing its ban lists.  And this, in turn, means that other
+<code>proftpd</code> servers' <code>mod_ban</code> modules can see those bans,
+and reject the badly behaved clients across the pool/cluster.
+
+<p>
+The <code>mod_tls_redis</code> module uses Redis servers for storing SSL/TLS
+sessions; SSL/TLS session caching can greatly improve SSL/TLS session handshake
+times, particularly for data transfers using SSL/TLS.  If you have a pool of
+<code>proftpd</code> servers, and you have FTPS clients which may connect to a
+different node every time, caching the SSL/TLS session data in a shared storage
+mechanism like Redis can be quite beneficial.
+
+<p>
+To use Redis for SSL/TLS session caching, then, you use the <a href="../contrib/mod_tls.html#TLSSessionCache"><code>TLSSessionCache</code></a> directive of the <code>mod_tls</code> module, using something like this
+in your <code>proftpd.conf</code>:
+<pre>
+  <IfModule mod_tls.c>
+    TLSEngine on
+
+    # ...other mod_tls directives...
+
+    <IfModule mod_tls_redis.c>
+      # Tell mod_tls to cache sessions using Redis
+      TLSSessionCache redis:
+    </IfModule>
+  </IfModule>
+</pre>
+That's it.  The <code>mod_tls</code> module now knows to give the SSL/TLS
+session data to <code>mod_tls_redis</code>, and <code>mod_tls_redis</code>
+knows how to talk to the Redis server using <code>mod_redis</code>.
+
+<p><a name="FAQ">
+<b>Frequently Asked Questions</b><br>
+<font color=red>Question</font>: If I don't use Redis, are there other
+ways for sharing data (such as ban lists) among different <code>proftpd</code>
+instances?<br>
+<font color=blue>Answer</font>: It might be possible using <code>mod_sql</code>
+and some <code>SQLLogInfo</code> directives, but that would only work for very
+specific information.  For sharing things like ban lists and SSL/TLS sessions
+across a cluster of <code>proftpd</code> servers, Redis (or
+<a href="Memcache.html">Memcache</a>) support is <em>recommended</em>.
+
+<p>
+<font color=red>Question</font>: Can I use <code>mod_redis</code> to cache
+frequently accessed files, similar to <code><a href="http://wiki.nginx.org/HttpMemcachedModule">nginx+memcache</a></code>?<br>
+<font color=blue>Answer</font>: No.  And in reality, caching of files like that
+will probably not give you the same performance gain for FTP transfers as it
+can for HTTP transfers.
+
+<p>
+Why not?  Many HTTP transfers are for dynamically generated pages; the cost of
+generating each page is expensive, and the generated content may not change
+that frequently (relative to the rate of requests).  FTP transfers, by contrast,
+are for <b>static</b> files; FTP servers do not (usually) dynamically generate
+the bytes of the files being downloaded.  The cost of reading files from disk
+is probably <i>less</i> than reading files from Redis, over the network, even
+a LAN.
+
+<p>
+Now the above may not be true in <b>all</b> cases -- there may be FTP servers
+serving files from network-mounted filesystems (<i>e.g.</i> NFS, CIFS
+<i>et al</i>).  And for these very specific cases, having a cache of frequently
+access files on closer storage such as local disk (or Redis) could make a big
+difference; please contact the ProFTPD Project if you find yourself in this
+situation, and we will see what can be done to help.
+
+<p>
+<font color=red>Question</font>: Why do I see the following error when
+<code>proftpd</code> starts up?<br>
+<pre>
+  mod_tls_redis/0.1: notice: unable to register 'redis' SSL session cache: Redis support not enabled
+</pre>
+<font color=blue>Answer</font>: This message means that your
+<code>proftpd</code> server has <code>mod_tls_redis</code> built and
+loaded, <b>but</b> your <code>proftpd</code> server was <b>not</b> built
+with Redis support (<i>i.e.</i> the <code>--enable-redis</code> configure
+option was not used when compiling <code>proftpd</code>).
+
+<p>
+The above is not a fatal or worrisome error; it is merely pointing out that
+some of your modules want to use a feature that was not enabled.
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/howto/Regex.html b/doc/howto/Regex.html
index 72e0dfb..1692eb1 100644
--- a/doc/howto/Regex.html
+++ b/doc/howto/Regex.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
 <head>
 <title>Regular Expressions Tutorial</title>
@@ -5,15 +6,15 @@
 
 <body bgcolor=white>
 
-<table width=100% border=0>
+<table width=100% border=0 summary="">
   <tr>
     <td align="center">
-      <h4>Found at: http://publish.ez.no/article/articleprint/11/</h3>
+      <h4>Found at: http://publish.ez.no/article/articleprint/11/</h4>
     </td>
   </tr>
 </table>
 
-<table width=100% border=0>
+<table width=100% border=0 summary="">
   <tr>
     <td>
       <h1>Regular Expressions Explained</h1>
@@ -24,7 +25,7 @@
 <hr noshade="noshade" size=4>
 
 <br>
-<table width=100% border=0>
+<table width=100% border=0 summary="">
   <tr>
     <td>
       <p class="byline">Author: Jan Borsodi</p>
@@ -92,7 +93,7 @@ The contents of an expression are, as explained earlier, a combination of
 alphanumeric characters and metacharacters. An alphanumeric character is either
 a letter from the alphabet:
 <p>
-<table width=100% border=0>
+<table width=100% border=0 summary="Alphabetic Quantifiers">
   <tr>
     <td bgcolor="#f0f0f0">
       <code>abc</code>
@@ -102,7 +103,7 @@ a letter from the alphabet:
 <p>
 or a number:
 <p>
-<table width=100% border=0>
+<table width=100% border=0 summary="Numeric Quantifiers">
   <tr>
     <td bgcolor="#f0f0f0">
       <code>123</code>
@@ -118,7 +119,7 @@ very special character is the backslash <b>\</b>, as this turns any
 metacharacters into literal characters, and alphanumeric characters into a
 sort of metacharacter or sequence. The metacharacters are:
 <p>
-<table width=100% border=0>
+<table width=100% border=0 summary="Metacharacters">
   <tr>
     <td bgcolor="#f0f0f0">
       <code>\ | ( ) [  {  ^ $ * + ? . < ></code>
@@ -139,7 +140,7 @@ decimal in a floating number, will lead to strange results. As explained above,
 you need to backslashify it to get the literal meaning. For instance take this
 expression:
 <p>
-<table width=100% border=0>
+<table width=100% border=0 summary="Punctuation Marks">
   <tr>
     <td bgcolor="#f0f0f0">
       <code>1.23</code>
@@ -150,7 +151,7 @@ expression:
 will match the number 1.23 in a text as you might have guessed, but it will
 <b>also</b> match these next lines:
 <p>
-<table width=100% border=0>
+<table width=100% border=0 summary="Punction Marks">
   <tr>
     <td bgcolor="#f0f0f0">
       <code>1x23</code><br>
@@ -645,7 +646,7 @@ versions for commonly used sequences, they are:
 <table width=100% border=0>
   <tr>
     <td bgcolor="#f0f0f0">
-      <code>\d<code>, a digit (<code>[0-9]</code>)<br>
+      <code>\d</code>, a digit (<code>[0-9]</code>)<br>
       <code>\D</code>, a non-digit (<code>[^0-9]</code>)<br>
       <code>\w</code>, a word (alphanumeric) (<code>[a-zA-Z0-9]</code>)<br>
       <code>\W</code>, a non-word (<code>[^a-zA-Z0-9]</code>)<br>
diff --git a/doc/howto/Rewrite.html b/doc/howto/Rewrite.html
index ce168c1..a496142 100644
--- a/doc/howto/Rewrite.html
+++ b/doc/howto/Rewrite.html
@@ -1,15 +1,13 @@
-<!-- $Id: Rewrite.html,v 1.9 2013-01-08 00:20:44 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Rewrite.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Using mod_rewrite</title>
+<title>ProFTPD: Using mod_rewrite</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Using the <code>mod_rewrite</code> Module</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Using the <code>mod_rewrite</code> Module</i></b></h2></center>
 <hr>
 
 <p>
@@ -157,7 +155,7 @@ The following <code>mod_rewrite</code> configuration should do the trick:
     # Use the replaceall internal RewriteMap
     RewriteMap replace int:replaceall
 
-    RewriteRule (.*) "${replace:!$1!\\\\!/}"'
+    RewriteRule (.*) "${replace:!$1!\\\\!/}"
   </IfModule>
 </pre>
 Yes, you will need the four consecutive backslashes there, in order to make it
@@ -358,7 +356,7 @@ handle non-ASCII characters well; it displays them as <code>?</code>.
 
 <p>
 Here's an example, using Perl, to replace "ä" with "ae" in uploaded file
-names.  Note that "&auml" in hex notation is <code>0xE4</code>:
+names.  Note that "ä" in hex notation is <code>0xE4</code>:
 <pre>
   my $rewrite_rule = 'RewriteRule (.*) ${replace:/$1/' . chr(0xE4) . '/ae}';
 
@@ -453,6 +451,13 @@ any other time than during January 2013, then the
 <code>mod_rewrite</code>-rewritten path will not match the name of the file
 on disk, and the download will fail.
 
+<p>
 <hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </body>
 </html>
diff --git a/doc/howto/SQL.html b/doc/howto/SQL.html
index ec83c52..63755c0 100644
--- a/doc/howto/SQL.html
+++ b/doc/howto/SQL.html
@@ -1,15 +1,13 @@
-<!-- $Id: SQL.html,v 1.18 2014-01-22 17:14:30 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/SQL.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - SQL and mod_sql</title>
+<title>ProFTPD: SQL and mod_sql</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>SQL and <code>mod_sql</code></b></h2></center>
+<center><h2><b>ProFTPD: SQL and <code>mod_sql</code></b></h2></center>
 <hr>
 
 <p>
@@ -30,7 +28,7 @@ daemon to use <code>mod_sql</code>, use the <code>--with-modules</code> option
 of the <code>configure</code> script, specifying both <code>mod_sql</code> and
 the backed module, <i>e.g.</i>:
 <pre>
-  ./configure --with-modules=mod_sql:mod_sql_mysql
+  $ ./configure --with-modules=mod_sql:mod_sql_mysql
 </pre>
 Sometimes the necessary header and library files for building in SQL support
 are in non-standard locations; the <code>configure</code> script needs to be
@@ -49,7 +47,7 @@ and the path the library was:
 </pre>
 then, the above <code>configure</code> line would be changed to look like:
 <pre>
-  ./configure \
+  $ ./configure \
     --with-modules=mod_sql:mod_sql_mysql \
     --with-includes=/usr/local/mysql/include/mysql \
     --with-libraries=/usr/local/mysql/lib/mysql
@@ -66,7 +64,7 @@ encrypting passwords stored in database tables.  The <code>configure</code>
 options for building an OpenSSL-capable <code>mod_sql</code> might look
 something like this:
 <pre>
-  CFLAGS=-DHAVE_OPENSSL LIBS=-lcrypto ./configure \
+  $ CFLAGS=-DHAVE_OPENSSL LIBS=-lcrypto ./configure \
     --with-modules=mod_sql:mod_sql_mysql \
     --with-includes=/usr/local/mysql/include/mysql:/usr/local/openssl/include \
     --with-libraries=/usr/local/mysql/lib/mysql:/usr/local/openssl/lib
@@ -86,7 +84,7 @@ be provided is described below.
 
 <p>
 <b>User Information Table</b><sub>1</sub>
-<table border=1>
+<table border=1 summary="User Information Table">
   <tr>
     <td><i>Column</i></td>
     <td><i>Type</i></td>
@@ -153,7 +151,7 @@ be provided is described below.
 
 <p>
 <i>Notes</i>:
-<ol type=number>
+<ol>
   <li>The user table <b>MUST</b> exist.
 
   <p>
@@ -171,7 +169,7 @@ be provided is described below.
 
 <p>
 <b>Group Information Table</b><sub>1</sub>
-<table border=1>
+<table border=1 summary="Group Information Table">
   <tr>
     <td><i>Column</i></td>
     <td><i>Type</i></td>
@@ -207,7 +205,7 @@ be provided is described below.
 
 <p>
 <i>Notes</i>:
-<ol type=number>
+<ol>
   <li><code>mod_sql</code> will normally concatenate all matching group rows;
     you can have multiple rows for each group with only one member per group,
     or have a single row with multiple groups, or a mixing of the two.  However,
@@ -592,12 +590,12 @@ is the base64-encoded value of the digested password string.  To get a list
 of the message digest algorithms supported by your OpenSSL installation, you
 can execute the following command:
 <pre>
-  openssl list-message-digest-commands
+  $ openssl list-message-digest-commands
 </pre>
 To generate the string to put into the SQL tables, using MD5 as the digest
 algorithm and "password" as the password:
 <pre>
-  /bin/echo "{md5}"`/bin/echo -n "password" | openssl dgst -binary -md5 | openssl enc -base64`
+  $ /bin/echo "{md5}"`/bin/echo -n "password" | openssl dgst -binary -md5 | openssl enc -base64`
 </pre>
 The "{md5}" prefix is necessary, so that <code>mod_sql</code> knows
 what digest algorithm was used.
@@ -606,7 +604,7 @@ what digest algorithm was used.
 Here's a quick and dirty example of generating database-ready strings using
 every digest algorithm supported by the installed OpenSSL:
 <pre>
-  for c in `openssl list-message-digest-commands`; do
+  $ for c in `openssl list-message-digest-commands`; do
     /bin/echo "{$c}"`/bin/echo -n "password" | openssl dgst -binary -$c | openssl enc -base64`
   done
 </pre>
@@ -647,6 +645,169 @@ If this prefixed format is not sufficient for your needs, you can also
 use the <a href="../contrib/mod_sql_passwd.html"><code>mod_sql_passwd</code></a>
 module, which knows how to handle "raw" MD5, SHA1, and other encoding schemes.
 
+<p><a name="SQLBackendSQLAuthType"></a>
+<font color=red>Question</font>: I've upgraded to MySQL 5.7, and now I am unable
+to login using my MySQL users.  The <code>SQLLogFile</code> shows something like
+this:
+<pre>
+  mod_sql/4.3[9097]: checking password using SQLAuthType 'Backend'
+  mod_sql/4.3[9097]: entering     mysql cmd_checkauth
+  mod_sql/4.3[9097]: MySQL client library used MySQL SHA256 password format, and Backend SQLAuthType cannot succeed; consider using MD5/SHA1/SHA256 SQLAuthType using mod_sql_passwd
+  mod_sql/4.3[9097]: MySQL server used MySQL 4.1 password format for PASSWORD() value
+  mod_sql/4.3[9097]: password mismatch
+</pre>
+and my <code>mod_sql</code> configuration has not changed; it uses:
+<pre>
+  SQLAuthTypes Backend
+</pre>
+Is this a bug?<br>
+<p>
+<font color=blue>Answer</font>: The short answer is that no, it is not a bug.
+But it <b>is</b> a regression caused by changes in the MySQL API, and the only
+fix is to change the password values stored in your MySQL tables.
+
+<p>
+First, the background.  The "Backend" <code>SQLAuthType</code> is <b>only</b>
+implemented by <code>mod_sql_mysql</code>, for MySQL databases; Postgres,
+SQLite, <i>et al</i> do not have an equivalent.  And in MySQL, the "Backend"
+<code>SQLAuthType</code> is there specifically for the use case where an admin
+uses MySQL's <code>PASSWORD()</code> function to generate the hashed value
+stored in the <code>users</code> table for ProFTPD users.  The way that the
+"Backend" <code>SQLAuthType</code> is implemented is that
+<code>mod_sql_mysql</code> gets the hashed value stored in the row from the
+MySQL server, and then <code>mod_sql_mysql</code> uses a function in the
+<code>libmysqlclient</code> library to try to generate the same hash (using the
+password from the client); it then compares the two hashes to see if they match.
+
+<p>
+MySQL has had <a href="http://dev.mysql.com/doc/refman/5.7/en/encryption-functions.html#function_password">issues</a> with its <code>PASSWORD()</code>
+generated format over the years.  They initially generated a 16 byte string,
+but this was deemed insecure in MySQL 4.1, and changed to a 41 byte string.
+But even that 41 byte string was found to be vulnerable to <i>e.g.</i>
+<a href="https://en.wikipedia.org/wiki/Rainbow_table">rainbow table</a> attacks,
+and thus the format was changed again to be much longer, to use SHA256,
+<b>and</b> (importantly for us) to <i>always include a randomly generated
+salt</i>.  (It is this salt which mitigates the rainbow table attack scenario.)
+This sequence of events means that a MySQL database table used by
+<code>mod_sql_mysql</code> might contain a <code>PASSWORD()</code>-generated
+hashed value that is in the pre-4.1 format (16 bytes), the 4.1 format
+(41 bytes), and/or the sha256 format.
+
+<p>
+For the pre-4.1 and 4.1 hashed value formats, the MySQL <code>PASSWORD()</code>
+function and the <code>libmysqlclient</code> library functions used by
+<code>mod_sql_mysql</code> would generate the <b>same hashed value</b> for the
+<b>same password</b>, and thus worked as expected.  <em>But</em> for sha256
+hashed value formats, <code>PASSWORD()</code> and the <code>libmysqlcient</code>
+library functions will <b><i>not</i></b> generate the same hashed value for
+the same password.  Why not?  Each time those functions are called, they
+<i>internally</i> generate and use a random salt value; there is no way to
+explicitly <i>provide</i> the salt value to the function.  This means that the
+sha256 hashed value <b><i>will</i></b> be different, each time.  If
+<code>PASSWORD()</code> is used to generate an sha256 formatted hashed value in
+the database table using password "test", <b>and</b> if the
+<code>libmysqlclient</code> library function is called to generate an sha256
+formatted hashed using password "test", those functions will generate
+<b><i>different</i></b> hashed values.  Calling those functions again will
+generate different hashes, each and every time; this means that they
+<b>cannot</b> be compared/matched.
+
+<p>
+If sha256 hashed values are different each time, how can MySQL use them to
+authenticate?  The process used for authenticating the client
+(the database client, <code>mod_sql_mysql</code>, not the FTP client connecting
+to ProFTPD) is all internal to MySQL, and is slightly different; it assumes
+that the client being authenticated is defined in the <code>mysql.users</code>
+system table, and <b>only</b> there.  That internal support does <b>not</b>
+extend to any other table which might have used the <code>PASSWORD()</code>
+function for its columns.  (This is why that <code>PASSWORD()</code> function is
+<a href="http://dev.mysql.com/doc/refman/5.7/en/encryption-functions.html#function_password">slated for deprecation</a>, as of MySQL 5.7, and later removal.)
+This means that you can use sha256 formatted hashed values for authenticating
+<i><code>mod_sql_mysql</code></i>, but <b>not</b> for authenticating the
+ProFTPD users stored in the MySQL database tables.
+
+<p>
+Why is it only recently (as of late 2016) that users are encountering this
+issue?  MySQL 5.5 (the default MySQL version for Debian/Ubuntu for a while) did
+not support the sha256 formatted hashed values, only the pre-4.1 and 4.1
+formatted hashed values.  So no issue with the "Backend"
+<code>SQLAuthType</code> there.  The introduction/use of sha256 formatted
+hashed values started in MySQL 5.6.
+
+<p>
+But ProFTPD users using MySQL 5.6 did not encounter this problem, either.  Why
+not?  Turns out that the <code>libmysqlclient</code> library in 5.6 had a
+couple of <em>different</em> functions for making hashed values (of different
+formats).  The <code>mod_sql_mysql</code> module would try all of those
+different functions, for backward compatibility.  And it worked, as one of the
+older <code>libmysqlclient</code> library functions (the
+<code>make_scrambled_password()</code> function, specifically) <em>would</em>
+generate the 4.1 formatted hash value, which would match the values stored
+for ProFTPD users in the <code>users</code> table successfully.
+
+<p>
+However, in MySQL 5.7, those older <code>libmysqlclient</code> library
+functions were <b>removed</b> (they had been deprecated for a while anyway),
+so now, the <b>only</b> <code>libmysqlclient</code> library function for
+generating hashed values is the sha256 one.  And that, as mentioned above,
+uses an internally-generated random salt each time, and its output is not
+consistent/repeatable.  And that means that starting with MySQL 5.7, the
+"Backend" <code>SQLAuthType</code> will no longer work.  As mentioned by the
+log message in the <code>SQLLogFile</code>.
+
+<p>
+The alternatives are to use <em>different</em> <code>SQLAuthTypes</code>;
+this <b>will</b> require that you change the <code>users.passwd</code> value
+for your ProFTPD users.  First, make sure that the <a href="../contrib/mod_sql_passwd.html"><code>mod_sql_passwd</code></a> module is compiled/loaded into your
+ProFTPD.  If you are building from source:
+<pre>
+  $ ./configure --enable-dso --with-shared=mod_sql:mod_sql_mysql:mod_sql_passwd ...
+  $ make
+  $ make install
+</pre>
+And in your <code>proftpd.conf</code>:
+<pre>
+  # Make sure that mod_sql_passwd is loaded, if available
+  LoadModule mod_sql_passwd.c
+
+  <IfModule mod_sql.c>
+    SQLBackend mysql
+
+    <IfModule !mod_sql_passwd.c>
+      # If mod_sql_passwd is not available, then consider using the OpenSSL
+      # SQLAuthType
+      SQLAuthTypes OpenSSL Backend
+    </IfModule>
+
+    <IfModule mod_sql_passwd.c>
+      # If mod_sql_passwd is available, try other SQLAuthTypes, too
+      SQLPasswordEngine on
+
+      # MySQL uses lowercase hex-encoded strings for MD5() et al
+      SQLPasswordEncoding hex
+
+      SQLBackend SHA256 SHA1 MD5 Backend
+    </IfModule>
+  </IfModule>
+</pre>
+When populating the <code>users.passwd</code> field for your users, you can
+use MD5 passwords:
+<pre>
+  mysql> UPDATE users SET passwd = MD5('password') WHERE userid = 'myuser';
+</pre>
+or SHA1 passwords:
+<pre>
+  mysql> UPDATE users SET passwd = SHA1('password') WHERE userid = 'myuser';
+</pre>
+or SHA256 passwords:
+<pre>
+  mysql> UPDATE users SET passwd = SHA2('password', 256) WHERE userid = 'myuser';
+</pre>
+And the <code>SQLAuthTypes</code> configuration will try <b>all</b> of those
+types.  The "Plaintext" and "Empty" <code>SQLAuthTypes</code> should <b>never
+be used</b> <em>except</em> for debugging/development; they are too insecure
+for production systems.
+
 <p><a name="SQLScoreboardError"></a>
 <font color=red>Question</font>: Why do I see "error deleting scoreboard entry: Invalid argument"?<br>
 <font color=blue>Answer</font>: This log message almost always denotes use
@@ -749,7 +910,10 @@ compiled with SSL support.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2014-01-22 17:14:30 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/SSH.html b/doc/howto/SSH.html
index 8e4a7a4..0c824de 100644
--- a/doc/howto/SSH.html
+++ b/doc/howto/SSH.html
@@ -1,15 +1,13 @@
-<!-- $Id: SSH.html,v 1.1 2004-04-10 02:01:38 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/SSH.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Configuring ProFTPD for FTP over SSH</title>
+<title>ProFTPD: Configuring FTP over SSH</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Configuring ProFTPD for FTP over SSH</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Configuring FTP over SSH</i></b></h2></center>
 <hr>
 <i><b>IMPORTANT NOTICE</b>: the original instructions listed on this page,
 based on <a href="http://www.proftpd.org/proftpd-l-archive/98-11/msg00066.html">http://www.proftpd.org/proftpd-l-archive/98-11/msg00066.html</a> were
@@ -47,7 +45,7 @@ request was sent to localhost, rather than to the remote server, which
 effectively set up the encrypted channel between localhost and itself).
 Here's how to set up a local port forward:
 <pre>
-  ssh -L<i>local-port</i>:<i>remote-addr</i>:<i>remote-port</i> <i>user</i>@<i>host</i>
+  $ ssh -L<i>local-port</i>:<i>remote-addr</i>:<i>remote-port</i> <i>user</i>@<i>host</i>
 </pre>
 This says to listen on port <i>local-port</i> on localhost, and to send that
 encrypted traffic to <i>host</i>'s <i>remote-addr</i> at port
@@ -55,7 +53,7 @@ encrypted traffic to <i>host</i>'s <i>remote-addr</i> at port
 specific, the control channel, through which passwords are transmitted, will
 be encrypted; the data channel will not), it would look like:
 <pre>
-  ssh -f -L3000:ftpserver:21 ftpserver 'exec sleep 10' && ftp localhost 3000
+  $ ssh -f -L3000:ftpserver:21 ftpserver 'exec sleep 10' && ftp localhost 3000
 </pre>
 Note that the choice of <i>local-port</i> is arbitrary.  Using port
 <code>3000</code> is not a requirement.  This trick also requires that
@@ -84,10 +82,10 @@ of quasi-shell (although, strictly speaking, there are better, more restrictive
 shells than this example, as it could be escaped from), one can ssh over any
 command:
 <pre>
-  ssh -l test -f -L3000:ftpserver:21 ftpserver true && ftp localhost 3000
+  $ ssh -l test -f -L3000:ftpserver:21 ftpserver true && ftp localhost 3000
 </pre>
 You'll also need to use the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_AllowForeignAddress.html"><code>AllowForeignAddress</code></a> configuration directive in your
+<a href="../modules/mod_core.html#AllowForeignAddress"><code>AllowForeignAddress</code></a> configuration directive in your
 configuration file:
 <pre>
    AllowForeignAddress on
@@ -112,7 +110,10 @@ email.<br>
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2004-04-10 02:01:38 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Scoreboard.html b/doc/howto/Scoreboard.html
index 5873a5a..7496d9b 100644
--- a/doc/howto/Scoreboard.html
+++ b/doc/howto/Scoreboard.html
@@ -1,15 +1,13 @@
-<!-- $Id: Scoreboard.html,v 1.9 2013-01-04 23:07:38 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Scoreboard.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - The ScoreboardFile</title>
+<title>ProFTPD: ScoreboardFile</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and the ScoreboardFile</b></h2></center>
+<center><h2><b>ProFTPD: ScoreboardFile</b></h2></center>
 <hr>
 
 <p>
@@ -215,7 +213,11 @@ daemon will automatically create a new scoreboard in the correct format.
 
 <p>
 <hr>
-<i>$Date: 2013-01-04 23:07:38 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Sendfile.html b/doc/howto/Sendfile.html
index 86391e2..d8874f9 100644
--- a/doc/howto/Sendfile.html
+++ b/doc/howto/Sendfile.html
@@ -1,15 +1,13 @@
-<!-- $Id: Sendfile.html,v 1.4 2011-02-21 02:57:16 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Sendfile.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Sendfile Support</title>
+<title>ProFTPD: Sendfile Support</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Sendfile Support</b></h2></center>
+<center><h2><b>ProFTPD: Sendfile Support</b></h2></center>
 <hr>
 
 <p>
@@ -48,7 +46,7 @@ a few cases where ProFTPD will specifically avoid the use of
     by definition, are ASCII transfers)
   <li>When RFC2228 data channel protection is in effect (<i>e.g.</i>
     <a href="TLS.html">SSL/TLS</a>)
-  </i>When transfers are being throttled via the <code>TransferRate</code>
+  <li>When transfers are being throttled via the <code>TransferRate</code>
     directive
   <li>When <code>MODE Z</code> data compression is being used (via the
     <code>mod_deflate</code> module)
@@ -64,7 +62,7 @@ Sendfile support in the compiled <code>proftpd</code> daemon can also be
 disabled at compile time, by using the <code>--disable-sendfile</code>
 configure option, <i>e.g.</i>:
 <pre>
-  # ./configure --disable-sendfile ...
+  $ ./configure --disable-sendfile ...
 </pre>
 This is not recommended unless necessary.
 
@@ -167,7 +165,11 @@ the size of the file being downloaded:
 
 <p>
 <hr>
-<i>$Date: 2011-02-21 02:57:16 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/ServerType.html b/doc/howto/ServerType.html
index 79c6014..70c0b90 100644
--- a/doc/howto/ServerType.html
+++ b/doc/howto/ServerType.html
@@ -1,9 +1,7 @@
-<!-- $Id: ServerType.html,v 1.7 2012-12-02 00:33:19 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/ServerType.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ServerType</title>
+<title>ProFTPD: ServerType</title>
 </head>
 
 <body bgcolor=white>
@@ -241,7 +239,10 @@ Another solution is simply to <a href="#Switching">switch</a> your
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2012-12-02 00:33:19 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Stopping.html b/doc/howto/Stopping.html
index 7c07289..292557e 100644
--- a/doc/howto/Stopping.html
+++ b/doc/howto/Stopping.html
@@ -1,15 +1,13 @@
-<!-- $Id: Stopping.html,v 1.6 2013-07-02 05:46:51 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Stopping.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Stopping and Starting</title>
+<title>ProFTPD: Stopping and Starting</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b><i>Stopping and Starting ProFTPD</i></b></h2></center>
+<center><h2><b><i>ProFTPD: Stopping and Starting</i></b></h2></center>
 <hr>
 
 <p>
@@ -290,12 +288,15 @@ done.
 
 <p>
 Read <code>ftpshut</code>'s
-<a href="http://www.castaglia.org/proftpd/doc/ftpshut.html">man</a> page
-for more detailed information on its usage.
+<a href="../utils/ftpshut.html">man</a> page for more detailed information on
+its usage.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-07-02 05:46:51 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/TLS.html b/doc/howto/TLS.html
index 660b241..bd6c489 100644
--- a/doc/howto/TLS.html
+++ b/doc/howto/TLS.html
@@ -1,9 +1,7 @@
-<!-- $Id: TLS.html,v 1.39 2014-03-26 18:22:32 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/TLS.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - FTP and SSL/TLS</title>
+<title>ProFTPD: FTP and SSL/TLS</title>
 </head>
 
 <body bgcolor=white>
@@ -464,6 +462,26 @@ All that said, in ProFTPD 1.3.3rc2, the <code>mod_tls</code> module was
 enhanced to support implicit FTPS via the <code>UseImplicitSSL</code>
 <a href="../contrib/mod_tls.html#TLSOption"><code>TLSOption</code></a>.
 
+<p><a name="TLSv1.2">
+<font color=red>Question</font>: How can I require that <b>only</b> TLSv1.2
+be allowed/used?<br>
+<font color=blue>Answer</font>: Assuming your OpenSSL library is new enough,
+you should only need to use the following in your <code>mod_tls</code>
+configuration section:
+<pre>
+  TLSProtocol TLSv1.2
+</pre>
+Note that if you have multiple <code><VirtualHost></code> sections in
+your <code>proftpd.conf</code> and you want this to apply to <em>all</em> of
+those vhosts, then you should place the <code>TLSProtocol</code> directive
+in a <code><Global></code> section, <i>e.g.</i>:
+<pre>
+  <Global>
+    # Only allow TLSv1.2 for any of our FTPS vhosts
+    TLSProtocol TLSv1.2
+  </Global>
+</pre>
+
 <p><a name="TLSPerUser">
 <font color=red>Question</font>: Can I require TLS on a per-user basis?<br>
 <font color=blue>Answer</font>: Prior to ProFTPD 1.2.10rc2, no.  The IETF
@@ -569,7 +587,7 @@ unencrypted.
 (FXP) transfers are supported via the <code>SSCN</code> FTP command.  The
 <a href="../contrib/mod_tls.html#TLSVerifyServer"><code>TLSVerifyServer</code></a> directive is also needed for secure FXP transfers.
 
-<p><a name="TLSv2">
+<p><a name="SSLv2">
 <font color=red>Question</font>: How come <code>mod_tls</code> does not support
 SSLv2?<br>
 <font color=blue>Answer</font>: Various defects have been found in the SSLv2
@@ -628,18 +646,6 @@ the <code>CCC</code> command by clients, the following must appear in your
 </pre>
 See the <a href="../contrib/mod_tls.html#TLSRequired"><code>TLSRequired</code></a> description for more details.
 
-<!-- Note: I am not sure the following advice is relevant or even good anymore
-<p><a name="TLSSlow">
-<font color=red>Question</font>: Sometimes my encrypted transfers are slow.
-Is there a way to speed them up?<br>
-<font color=blue>Answer</font>:  There have been reports that increasing the
-tunable buffer size (using the <code>--enable-buffer-size</code> option
-of the <code>configure</code> script) to 8192 increases transfer speeds, most
-notably on very high speed networks.  Increasing the buffer size does not
-appear to affect normal FTP transfers (in fact, it may benefit them as well,
-depending on the client).
--->
-
 <p><a name="TLSDataProtection">
 <font color=red>Question</font>: I can login using FTPS, but I cannot see
 any directories.  Why not?<br>
@@ -770,6 +776,32 @@ SSL session ID properly for data transfers, when a data transfer is requested,
 the SSL session ID presented by the client should always be fresh and in the
 session cache.
 
+<p><a name="TLSUnknownProtocol">
+<font color=red>Question</font>: My FTPS client is failing to connect to
+<code>proftpd</code> with <code>mod_tls</code>.  The <code>TLSLog</code> shows
+the following log messages, each time the FTPS client tries to connect:
+<pre>
+  2016-01-15 07:32:37,275 mod_tls/2.7[5072]: TLS/TLS-C requested, starting TLS handshake
+  2016-01-15 07:32:37,303 mod_tls/2.7[5072]: unable to accept TLS connection: protocol error:
+    <b>(1) error:140760FC:SSL routines:SSL23_GET_CLIENT_HELLO:unknown protocol</b>
+  2016-01-15 07:32:37,303 mod_tls/2.7[5072]: TLS/TLS-C negotiation failed on control channel
+</pre>
+Why does this happen?<br>
+<font color=blue>Answer</font>: This can happen when <code>mod_tls</code> is
+configured (<i>e.g.</i> using <code>TLSProtocol</code>) to support specific
+TLS versions, and the FTPS client is trying to use one of the unsupported
+protocol versions.  For example, if you use:
+<pre>
+  # Only support TLSv1.1 and TLSv1.2
+  TLSProtocol TLSv1.1 TLSv1.2
+</pre>
+And then connect with an FTPS client using TLSv1, like so:
+<pre>
+  $ openssl s_client -connect <i>address</i>:<i>port</i> -starttls ftp -tls1
+</pre>
+Then you would see the above error.  Note that this same protocol mismatch
+issue can also manifest as the error message "wrong version number".
+
 <p><a name="TLSBuildErrors">
 <font color=red>Question</font>: Why would I see the following errors while attempting to build <code>proftpd</code> with <code>mod_tls</code>?
 <pre>
@@ -837,6 +869,26 @@ section in your <code>proftpd.conf</code>, so that they apply to all
 vhosts configured.  The virtual hosting <a href="Vhost.html">howto</a>
 describes this in more detail.
 
+<p><a name="TLSClientCertChainTooLong">
+<font color=red>Question</font>: When my FTPS client connects to my
+<code>mod_tls</code>-enabled server, the TLS handshake fails.  I see these
+messages in my <code>TLSLog</code>:
+<pre>
+  mod_tls/2.4.3[28786]: error: unable to verify certificate at depth 1
+  mod_tls/2.4.3[28786]: client certificate failed verification: certificate chain too long
+</pre>
+What causes this?<br>
+<font color=blue>Answer</font>: This can happen if you have your
+<code>mod_tls</code> configured with a very small <a href="../contrib/mod_tls.html#TLSVerifyDepth"><code>TLSVerifyDepth</code></a> value, <i>e.g.</i>:
+<pre>
+  TLSVerifyDepth 0
+</pre>
+Using small values, especially a value of 0, is a <b>bad idea</b>; most client
+certificate chains have a "depth" (or <em>length</em>) of 2-3, or perhaps
+longer.  The default <code>TLSVerifyDepth</code> value of 10 is sufficient for
+most cases; it allows for long certificate chains, but still guards against
+chains which might be absurdly long.
+
 <p><a name="TLSRenegotiations">
 <font color=red>Question</font>: My FTPS client sometimes times out after uploading/downloading more than 1 GB of data.  When I turn off SSL/TLS, the upload/download works. Why?<br>
 <font color=blue>Answer</font>:
@@ -865,12 +917,12 @@ them:
   TLSRenegotiate required off
 </pre>
 
-<p><a name="TLSNoCertRequest">
+<p><a name="TLSEOF">
 <font color=red>Question</font>: My FTPS client has trouble connecting to <code>proftpd</code> using SSL/TLS, with the following error appearing in the <code>TLSLog</code>:
 <pre>
-  Sep 17 11:03:46 mod_tls/2.1.2[9628]: TLS/TLS-C requested, starting TLS handshake
-  Sep 17 11:03:46 mod_tls/2.1.2[9628]: unable to accept TLS connection: received EOF that violates protocol
-  Sep 17 11:03:46 mod_tls/2.1.2[9628]: TLS/TLS-C negotiation failed on control channel
+  mod_tls/2.1.2[9628]: TLS/TLS-C requested, starting TLS handshake
+  mod_tls/2.1.2[9628]: unable to accept TLS connection: received EOF that violates protocol
+  mod_tls/2.1.2[9628]: TLS/TLS-C negotiation failed on control channel
 </pre>
 Is this a bug in <code>mod_tls</code>, in the client, or something else?<br><br>
 <font color=blue>Answer</font>: There might be several different causes for
@@ -878,33 +930,13 @@ this error.  It could be a bug in the OpenSSL library, in <code>mod_tls</code>,
 in the FTPS client, or it could be a transient network issue.
 
 <p>
-Now, one possible thing to try is to use the following in your <code>proftpd.conf</code> file:
-<pre>
-  TLSOptions NoCertRequest
-</pre>
-This option tells the OpenSSL library to <b>not</b> include a message requesting
-the client's certificate in the SSL/TLS handshake messages.  Some older
-SSL implementations seem to have trouble with this certificate request message,
-and react badly.  This includes some Windows FTP clients, as well as some
-FTP clients for the Mac.
-
-<p>
-One user tried using both the <a href="http://www.panic.com/transmit/">Transmit</a> and the <a href="http://www.fetchsoftworks.com/">Fetch</a> applications for
-the Mac; both state that they can handle FTP over SSL/TLS.  Using both of
-these applications, the user saw the above error.  The user, when using
-Transmit, saw the following error message appear from the client:
-<pre>
-  Server said:
-  AUTH TLS successful
-  error:00000000:lib(0):func(0):reason(0)
-</pre>
-And when using Fetch, the error message displayed by that client was:
-<pre> 
-  SSL Error -9844
-  Server responded: "AUTH TLS successful"
-</pre>
-In <b>both</b> causes, using "TLSOptions NoCertRequest" appeared to make
-those clients happy.
+The <i>usual</i> culprit for the above error is an <em>FTP-aware</em> network
+device such as a NAT, router, or firewall between the client and the server.
+Such network devices "peek" into the FTP control connection in order to
+dynamically open the necessary ports for data transfers.  However, this
+"peeking" fails once an SSL/TLS handshake starts on that same control
+connection, and when that happens, these network devices usually terminate
+the control connection, resulting in the EOF ("end of file") error reported.
 
 <p><a name="TLSNoPassphrasePrompt">
 <font color=red>Question</font>: When <code>proftpd</code> starts up, I am
@@ -1133,8 +1165,9 @@ To do this, you would use a combination of
 <p>
 <font color=red>Question</font>: I have configured my <code>proftpd</code>
 server for FTPS.  When I use FileZilla to try to connect to it, though, I
-see this error in the FileZilla logs:
+see one of these errors in the FileZilla logs:
 <pre>
+  GnuTLS error -8: A record packet with illegal version was received
   GnuTLS error -9: A TLS packet with unexpected length was received
 </pre>
 Is there a ProFTPD directive to fix this error?<br>
@@ -1144,6 +1177,48 @@ an <i>error unrelated to SSL/TLS</i> on your server.  Check the proftpd
 <a href="../contrib/mod_sql.html#SQLLogFile"><code>SQLLogFile</code></a> if
 you are using the <code>mod_sql</code> module, <i>etc</i>.
 
+<p><a name="TLSFileZilla">
+<font color=red>Question</font>: When I use FileZilla to connect to my
+<code>proftpd</code> server, it fails, and I see this error:
+<pre>
+  gnutls_handshake: An unexpected TLS packet was received.
+</pre>
+How to can I connect to my FTPS server using FileZilla?<br>
+<font color=blue>Answer</font>: The most common cause of this is using
+a URL such as "ftps://..." in your FileZilla client; for FileZilla, you
+<b>must</b> use <b>"ftpes://..."</b> (note the <b><i>e</i></b> there) when
+connecting to <code>proftpd</code>.  Why?  Using "ftpes://..." tells FileZilla
+to use <b>explicit</b> TLS, which is what <code>proftpd</code> implements,
+as that is the RFC-mandated behavior.  See:
+<pre>
+  <a href="https://wiki.filezilla-project.org/SSL/TLS#Explicit_vs_Implicit_FTPS">https://wiki.filezilla-project.org/SSL/TLS#Explicit_vs_Implicit_FTPS</a>
+</pre>
+
+<p><a name="TLSLFTP">
+<font color=red>Question</font>: I'm trying to use <code>lftp</code> as my
+FTPS client for talking to <code>proftpd</code>, configured to use
+<code>mod_tls</code>, but it fails to connect.  I am using:
+<pre>
+  $ lftp ftps://pc -u myuser
+</pre>
+What is going wrong?<br>
+<font color=blue>Answer</font>: You may need to tell <code>lftp</code> that
+using SSL/TLS is <i>allowed</i> when talking to an FTP server:
+<pre>
+  $ lftp pc
+  lftp> set ftp:ssl-allow yes
+  lftp> user <i>user</i>
+  ...
+</pre>
+<b>or</b> put the above setting in your <code>~/.lftprc</code> file.
+
+<p>
+Note that if you <i>always</i> want <code>lftp</code> to use SSL/TLS for FTP
+sessions, then you would use this setting:
+<pre>
+  set ftp:ssl-force yes
+</pre>
+
 <p><a name="TLSShmcacheVsMemcache">
 <font color=red>Question</font>: What is the difference between the
 <code>mod_tls_shmcache</code> and <code>mod_tls_memcache</code> modules?<br>
@@ -1177,7 +1252,7 @@ following errors in the TLSLog:
 </pre>
 What does this mean?<br>
 <font color=blue>Answer</font>: This error means that, somehow, you have
-configured a key for a certificate, but don't have the matching certificate
+configured a key for a certificate, but do not have the matching certificate
 configured.  For example, if you configured <code>mod_tls</code> like so:
 <pre>
   #TLSRSACertificateFile /usr/local/etc/proftpd/ssl/server.cert.pem
@@ -1191,7 +1266,10 @@ configured files were not properly matched up.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2014-03-26 18:22:32 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Testing.html b/doc/howto/Testing.html
index b293194..3055a9d 100644
--- a/doc/howto/Testing.html
+++ b/doc/howto/Testing.html
@@ -1,15 +1,13 @@
-<!-- $Id: Testing.html,v 1.6 2011-05-19 18:26:42 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Testing.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Testing</title>
+<title>ProFTPD: Testsuite</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Testsuite</b></h2></center>
+<center><h2><b>ProFTPD: Testsuite</b></h2></center>
 <hr>
 
 <p>
@@ -25,7 +23,7 @@ In order to run the testsuite, you must configure your proftpd build so that
 the testsuite is prepared.  You do this by using the <code>--enable-tests</code>
 configure option, along with your other build options, <i>e.g.</i>:
 <pre>
-  # ./configure --enable-tests ...
+  $ ./configure --enable-tests ...
 </pre>
 
 <p>
@@ -50,12 +48,12 @@ locations of the <code>libcheck</code> headers and libraries.
 <p>
 To run the testsuite, use the <code>make check</code> target:
 <pre>
-  # ./configure --enable-tests ...
-  # make
+  $ ./configure --enable-tests ...
+  $ make
     ...
-  # make check
+  $ make check
     ...
-  ./api-tests
+  $ ./api-tests
   Running suite(s): pool
    array
    str
@@ -78,7 +76,6 @@ To run the testsuite, use the <code>make check</code> target:
 </pre>
 The <code>make check</code> will also go on to run the integration tests,
 if the API tests all pass.
-</pre>
 
 <p>
 If one of the API tests fails, you will see an error message like:
@@ -133,15 +130,15 @@ The current integration tests are written in Perl, and rely on the
 which version of <code>Test-Unit</code> you have, if at all?  Run the following
 command:
 <pre>
-  # perl -MTest::Unit -e 'print $Test::Unit::VERSION, "\n";'
+  $ perl -MTest::Unit -e 'print $Test::Unit::VERSION, "\n";'
   0.14
 </pre>
 
 <p>
 To run the integration tests manually, use the <code>tests.pl</code> script:
 <pre>
-  # cd tests/
-  # perl tests.pl
+  $ cd tests/
+  $ perl tests.pl
   t/logins.....................ok                                              
   t/commands/user..............ok                                              
   t/commands/pass..............ok                                              
@@ -203,9 +200,9 @@ just the test cases associated with a particular suite, such as <i>env</i> or
 <i>pool</i>.  Only <b>one</b> suite can be specified using the
 <code>PR_TEST_SUITE</code> environment variable:
 <pre>
-  # make check
-  # cd tests/
-  # PR_TEST_SUITE=pool ./api-tests
+  $ make check
+  $ cd tests/
+  $ PR_TEST_SUITE=pool ./api-tests
   Running suite(s): pool
   100%: Checks: 5, Failures: 0, Errors: 0
 </pre>
@@ -215,7 +212,10 @@ to run your changes.
 
 <p>
 <hr>
-Last updated: <i>$Date: 2011-05-19 18:26:42 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Timestamps.html b/doc/howto/Timestamps.html
index aaa19c7..c952d4b 100644
--- a/doc/howto/Timestamps.html
+++ b/doc/howto/Timestamps.html
@@ -1,15 +1,13 @@
-<!-- $Id: Timestamps.html,v 1.7 2012-06-06 17:56:51 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Timestamps.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD and Timestamps</title>
+<title>ProFTPD: Timestamps</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and Timestamps</b></h2></center>
+<center><h2><b>ProFTPD: Timestamps</b></h2></center>
 <hr>
 
 <p>
@@ -22,7 +20,7 @@ with timestamps?
 
 <p>
 If the timestamps in question are those displayed in directory listings, then
-you need to check your <a href="http://www.proftpd.org/docs/directives/linked/config_ref_TimesGMT.html"><code>TimesGMT</code></a> configuration.  Otherwise,
+you need to check your <a href="../modules/mod_core.html#TimesGMT"><code>TimesGMT</code></a> configuration.  Otherwise,
 depending on the logs at which you are looking, the timestamps will be correct
 <i>until</i> the user logs in.  After that, the timestamps are wrong.  This
 is one clue.  The other clue is that these wrong timestamps go away when
@@ -128,12 +126,17 @@ process.  See below for a common follow-up question.
 Why not?  For several reasons:
 <ul>
   <li>The <code>MDTM</code> command is <b>not</b> one of the commands
-      mandated by <a href="http://www.faqs.org/rfcs/rfc959.html">RFC959</a>.
+      mandated by <a href="http://www.faqs.org/rfcs/rfc959.html">RFC 959</a>.
+  </li>
+  
   <li>Some FTP clients use <code>MDTM</code> for <i>retrieving</i> the
       modification time, others try to use it to <i>set</i> the modification
       time, and still others do both.
+  </li>
+
   <li>Among those FTP clients that do use <code>MDTM</code>, there is no
       consistent format of the timestamp used.
+  </li>
 </ul>
 In short, it is a royal mess.  ProFTPD supports <code>MDTM</code> for
 retrieving the modification time, in GMT.  Period.
@@ -147,14 +150,17 @@ command.
 
 <p>
 <font color=red>Question</font>: I thought that the <code>TimesGMT</code> directive was affected the timestamps that proftpd uses?<br>
-<font color=blue>Answer</font>: No.  The <a href="http://www.proftpd.org/docs/directives/linked/config_ref_TimesGMT.html"><code>TimesGMT</code></a> directive
-only affects the timestamps as displayed to FTP clients in
-<i>directory listings</i>; it does <b>not</b> affect the timestamps used in
-log files.
+<font color=blue>Answer</font>: No.  The <a href="../modules/mod_core.html#TimesGMT"><code>TimesGMT</code></a> directive only affects the timestamps as
+displayed to FTP clients in <i>directory listings</i>; it does <b>not</b>
+affect the timestamps used in log files.
 
 <p>
 <hr>
-<i>$Date: 2012-06-06 17:56:51 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Tracing.html b/doc/howto/Tracing.html
index 8c67450..886f398 100644
--- a/doc/howto/Tracing.html
+++ b/doc/howto/Tracing.html
@@ -1,15 +1,13 @@
-<!-- $Id: Tracing.html,v 1.15 2012-09-27 17:52:36 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Tracing.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD Tracing</title>
+<title>ProFTPD: Tracing</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Tracing</b></h2></center>
+<center><h2><b>ProFTPD: Tracing</b></h2></center>
 <hr>
 
 <p>
@@ -96,6 +94,8 @@ keyword:
   <li>ident
   <li>inet
   <li>lock
+  <li>log
+  <li>module
   <li>netacl
   <li>netio
   <li>pam
@@ -208,7 +208,7 @@ proftpd with the updated <code>proftpd.conf</code>.  Later, while proftpd is
 running, you can tune the tracing using the
 <a href="Controls.html"><code>ftpdctl</code></a> utility, like this:
 <pre>
-  # ftpdctl trace lock:10 scoreboard:5
+  $ ftpdctl trace lock:10 scoreboard:5
 </pre>
 which dynamically changes the 'lock' trace channel level to 10, and the
 'scoreboard' trace channel level to 5.  Once you have gathered the necessary
@@ -216,7 +216,7 @@ information in the <code>TraceLog</code> file, you then use <code>ftpdctl</code>
 again and restore the trace levels back to zero, effectively turning off
 trace logging once more:
 <pre>
-  # ftpdctl trace DEFAULT:0
+  $ ftpdctl trace DEFAULT:0
 </pre>
 Note that the changed settings will only apply to <b>new</b> sessions; this
 does <b>not</b> change the trace logging for <i>existing</i> sessions.
@@ -250,7 +250,11 @@ sections as well.
 
 <p>
 <hr>
-<i>$Date: 2012-09-27 17:52:36 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Translations.html b/doc/howto/Translations.html
index 26212da..7ee5e78 100644
--- a/doc/howto/Translations.html
+++ b/doc/howto/Translations.html
@@ -1,15 +1,13 @@
-<!-- $Id: Translations.html,v 1.1 2008-10-08 16:55:34 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Translations.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Translations</title>
+<title>ProFTPD: Translations</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Translations</b></h2></center>
+<center><h2><b>ProFTPD: Translations</b></h2></center>
 <hr>
 
 <p>
@@ -28,15 +26,15 @@ if the language in question already has a translation.  ProFTPD's translations
 are kept, in the source distribution, under the <code>locale/</code> directory,
 <i>e.g.</i>:
 <pre>
-  ls /path/to/proftpd-<i>version</i>/locale/*.po
+  $ ls /path/to/proftpd-<i>version</i>/locale/*.po
 </pre>
 
 <p>
 To create a new translation, you first initialize a <code>.po</code> file
 for your translation using the template PO file (<code>proftpd.pot</code>):
 <pre>
-  cd proftpd-<i>version</i>/locale/
-  msginit -i proftpd.pot -o <i>lang</i>.po -l <i>lang</i>
+  $ cd proftpd-<i>version</i>/locale/
+  $ msginit -i proftpd.pot -o <i>lang</i>.po -l <i>lang</i>
 </pre>
 Then you simply use an editor to edit the generated <code><i>lang</i>.po</code>
 file, adding in the translated versions of the English messages.  There are
@@ -55,8 +53,8 @@ the template <code>.pot</code> file to see if you translated all of the
 messages in the template.  This is accomplished using the <code>msgcmp</code>
 command:
 <pre>
-  cd proftpd-<i>version</i>/locale/
-  msgcmp <i>lang</i>.po proftpd.pot
+  $ cd proftpd-<i>version</i>/locale/
+  $ msgcmp <i>lang</i>.po proftpd.pot
 </pre>
 
 <p>
@@ -64,7 +62,7 @@ Finally, compile your <code>.po</code> file into the machine-specific
 <code>.mo</code> file; this is what the <code>gettext</code> library uses
 in the running code:
 <pre>
-  msgfmt --check-format <i>lang</i>.po -o <i>lang</i>.mo
+  $ msgfmt --check-format <i>lang</i>.po -o <i>lang</i>.mo
 </pre>
 The <code>--check-format</code> option checks that your <code>.po</code> is
 properly formatted, and can be compiled without errors.
@@ -86,8 +84,8 @@ Note that as ProFTPD is developed, new messages may be added to the template
 It is important to keep ProFTPD's translations up-to-date.  You can help
 by periodically checking the existing translations:
 <pre>
-  cd proftpd-<i>version</i>/locale/
-  make check
+  $ cd proftpd-<i>version</i>/locale/
+  $ make check
 </pre>
 If you see that a language translation is out of date and you can help,
 simply update the <code>.po</code> file that language, and open a bug request
@@ -95,7 +93,11 @@ and attach the diff, or send the diff to the ProFTPD developers mailing list.
 
 <p>
 <hr>
-<i>$Date: 2008-10-08 16:55:34 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Umask.html b/doc/howto/Umask.html
index d602e48..3ae234c 100644
--- a/doc/howto/Umask.html
+++ b/doc/howto/Umask.html
@@ -1,9 +1,7 @@
-<!-- $Id: Umask.html,v 1.5 2012-03-24 18:41:57 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Umask.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Umask</title>
+<title>ProFTPD: Umask</title>
 </head>
 
 <body bgcolor=white>
@@ -25,7 +23,7 @@ works something like this:
 <pre>
   new file mode = <i>base-mode</i> - <i>umask</i>
 </pre>
-(Technically, the operation is <code><i>base-mode & ~umask</i></code>).
+(Technically, the operation is <code><i>base-mode & ~umask</i></code>).
 Thus, with a <i>base-mode</i> of <code>0666</code>, and a <i>umask</i> of
 <code>0022</code>, the permissions on the newly created file will be
 <code>0644</code> (<i>e.g.</i> <code>rw-r--r--</code>).
@@ -50,7 +48,7 @@ example is <code>0</code> (no permissions, <i>e.g.</i> <code>---</code>).
 <p>
 Here are some concrete examples to help illustrate things:
 <p>
-<table border=1>
+<table border=1 summary="Umask Examples">
   <tr>
     <td><b>Mode</b></td>
     <td><b>Label</b></td>
@@ -231,7 +229,10 @@ rename files of other users in that directory.  Because of this property,
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2012-03-24 18:41:57 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/Upgrade.html b/doc/howto/Upgrade.html
index a5b308a..7076f6e 100644
--- a/doc/howto/Upgrade.html
+++ b/doc/howto/Upgrade.html
@@ -1,15 +1,13 @@
-<!-- $Id: Upgrade.html,v 1.2 2010-01-21 17:37:16 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Upgrade.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Upgrading ProFTPD</title>
+<title>ProFTPD: Upgrading</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>Upgrading ProFTPD</b></h2></center>
+<center><h2><b>ProFTPD: Upgrading</b></h2></center>
 <hr>
 
 <p>
@@ -31,39 +29,43 @@ One safe way to test the upgrade is to compile the new <code>proftpd</code>,
 and test it out on your existing configuration file before actually installing
 the new version into the "live" location:
 <pre>
-  tar zxvf proftpd-<i>version</i>.tar.gz
-  cd proftpd-<i>version</i>/
-  ./configure ..
-  make
-  ./proftpd -t -d10 -c /path/to/proftpd.conf
+  $ tar zxvf proftpd-<i>version</i>.tar.gz
+  $ cd proftpd-<i>version</i>/
+  $ ./configure ..
+  $ make
+  $ ./proftpd -t -d10 -c /path/to/proftpd.conf
 </pre>
 The "<code>./proftpd</code>" means to use the new
 <code>proftpd</code> binary compiled by <code>make</code>, but not yet
 installed.  If the new binary reports errors, make a copy of your existing
 <code>proftpd.conf</code> file, keeping the old one as a backup:
 <pre>
-  cp /path/to/proftpd.conf /path/to/proftpd.conf.new
+  $ cp /path/to/proftpd.conf /path/to/proftpd.conf.new
 </pre>
 Make any needed changes to the <code>proftpd.conf.new</code> file, until
 the new <code>proftpd</code> binary reports a successful syntax check:
 <pre>
-  ./proftpd -t -d10 -c /path/to/proftpd.conf.new
+  $ ./proftpd -t -d10 -c /path/to/proftpd.conf.new
 </pre>
 
 <p>
 Once everything is configured the way you like, install the new binary and
 configuration file:
 <pre>
-  make install
-  cp /path/to/proftpd.conf /path/to/proftpd.conf.old
-  mv /path/to/proftpd.conf.new /path/to/proftpd.conf
+  $ make install
+  $ cp /path/to/proftpd.conf /path/to/proftpd.conf.old
+  $ mv /path/to/proftpd.conf.new /path/to/proftpd.conf
 </pre>
 Now do a stop/start on <code>proftpd</code>, and the new version of ProFTPD
 will be running.
 
 <p>
 <hr>
-<i>$Date: 2010-01-21 17:37:16 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Versioning.html b/doc/howto/Versioning.html
index e1c784b..f7a591b 100644
--- a/doc/howto/Versioning.html
+++ b/doc/howto/Versioning.html
@@ -1,15 +1,13 @@
-<!-- $Id: Versioning.html,v 1.2 2010-02-25 00:51:40 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Versioning.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD Release Versioning</title>
+<title>ProFTPD: Release Versioning</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Release Versioning</b></h2></center>
+<center><h2><b>ProFTPD: Release Versioning</b></h2></center>
 <hr>
 
 <p>
@@ -79,7 +77,7 @@ The -V option shows the various build-time options used for your
 <code>proftpd</code>; the version information is at the very beginning of that
 output:
 <pre>
-  # proftpd -V 
+  $ proftpd -V
   Compile-time Settings:
     Version: 1.3.2rc1 (devel)
 </pre>
@@ -94,7 +92,7 @@ name.  Release candidates always use a "devel" version label.
 <p>
 If you are running a stable release, then you would see:
 <pre>
-  # proftpd -V 
+  $ proftpd -V
   Compile-time Settings:
     Version: 1.3.2 (stable)
 </pre>
@@ -103,7 +101,7 @@ The version label for stable releases is always "stable", obviously.
 For completeness, here is what the -V output for a maintenance release
 looks like:
 <pre>
-  # proftpd -V 
+  $ proftpd -V
   Compile-time Settings:
     Version: 1.3.2a (maint)
 </pre>
@@ -111,13 +109,13 @@ The version label for a maintenance is "maint".
 
 <p>
 Finally, for developers and users who use the latest and greatest code from
-the trunk in CVS, you will see something like:
+the master branch on GitHub, you will see something like:
 <pre>
-  # proftpd -V 
+  $ proftpd -V
   Compile-time Settings:
-    Version: 1.3.3rc1 (CVS)
+    Version: 1.3.6rc1 (git)
 </pre>
-All code obtained from CVS reports a version label of "CVS".
+All code obtained from GitHub reports a version label of "git".
 
 <p><a name="FAQ"><b>Frequently Asked Questions</b>
 
@@ -132,7 +130,11 @@ newer, supported versions of ProFTPD instead of in the unsupported branches.
 
 <p>
 <hr>
-<i>$Date: 2010-02-25 00:51:40 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
 
 </body>
 </html>
diff --git a/doc/howto/Vhost.html b/doc/howto/Vhost.html
index b2af36a..4cfdfa1 100644
--- a/doc/howto/Vhost.html
+++ b/doc/howto/Vhost.html
@@ -1,15 +1,13 @@
-<!-- $Id: Vhost.html,v 1.7 2009-01-12 23:41:26 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/Vhost.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - Virtual Servers</title>
+<title>ProFTPD: Virtual Servers</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD and Virtual Servers</b></h2></center>
+<center><h2><b>ProFTPD: Virtual Servers</b></h2></center>
 <hr>
 
 <p><a name="Definition"></a>
@@ -20,14 +18,11 @@ machine.  The fact that these multiple sites are being served by the same
 physical machine is transparent to the end user.
 
 <p>
-The definition of the File Transfer Protocol, unfortunately, does not
-(currently) support name-based virtual hosts, as HTTP1.1 supports.  All FTP
-virtual hosts are based on unique IP address/port combinations, not on DNS
-names.  The similarity of ProFTPD's configuration file syntax to Apache's
-sometimes leads users to assuming that <code>proftpd</code> will handle these
-the same way -- but more on this later.  The bottom line is that ProFTPD
-does not support name-based virtual hosts; not because they are not implemented,
-but simply because the protocol itself does not support them.
+Until very recently, the definition of FTP did not allow for <i>name</i>-based
+virtual hosts, such as supported by HTTP/1.1.  That changed with
+<a href="https://tools.ietf.org/html/rfc7151">RFC 7151</a>, which defined a
+<code>HOST</code> FTP command.  ProFTPD virtual hosts are IP-based <i>and</i>
+name-based.
 
 <p>
 In some documents, one might see reference to both "daemon" and
@@ -43,7 +38,8 @@ Hence the "virtual".
 <p><a name="Configuration"></a>
 There are three "server" contexts (sometimes also called
 <i>sections</i>) in the <code>proftpd.conf</code> configuration file:
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_VirtualHost.html"><code><VirtualHost></code></a>, <a href="http://www.proftpd.org/docs/directives/linked/config_ref_Global.html"><code><Global></code></a>, and "server config".
+<a href="../modules/mod_core.html#VirtualHost"><code><VirtualHost></code></a>, <a href="../modules/mod_core.html#Global"><code><Global></code></a>,
+and "server config".
 
 <p>
 The <code><VirtualHost></code> context is used to define the configuration
@@ -56,28 +52,40 @@ for a particular virtual host, bound to an IP address.  For example:
 defines a configuration for a virtual server that <code>proftpd</code> should
 use whenever a remote client connects to the IP address 1.2.3.4.  DNS names,
 too, can be used with the <code><VirtualHost></code> configuration
-directive, and this is where some of the name-based vhost support confusion
-creeps in:
+directive:
 <pre>
   <VirtualHost ftp.mydomain.com>
     ...
   </VirtualHost>
 </pre>
-When <code>proftpd</code> parses this context on startup, it will resolve the
+When <code>proftpd</code> parses this section on startup, it will resolve the
 given DNS name to its IP address and use that, just as if that IP address
-had been used in the first place.  Use of DNS names like this, while convenient,
-can easily lead to confusion when multiple DNS names resolve to the same
-IP address.  If this happens, <code>proftpd</code> will use the first
-context in the configuration file when serving that address.
+had been used in the first place.  In addition, when DNS names are used like
+this, ProFTPD will <i>automatically</i> create a <a href="../modules/mod_core.html#ServerAlias"><code>ServerAlias</code></a> directive using that DNS name.
+This allows for multiple <code><VirtualHost></code> sections using the
+same DNS name to be defined in your <code>proftpd.conf</code>.
 
 <p>
-The <code><Global></code> context is provided as a convenience.  Imagine
-that the administrator has many <code><VirtualHost></code> contexts
+If you want the same vhost to be used for multiple different names at the
+same time (<i>e.g.</i> because you have multiple DNS names for the same
+server), you would use the <code>ServerAlias</code> directive to list those
+other names, like so:
+<pre>
+  <VirtualHost ftp.mydomain.com>
+    # Use this vhost for other names, too
+    ServerAlias ftp.mydomain.org ftp2.mydomain.com ftp.otherdomain.org
+    ...
+  </VirtualHost>
+</pre>
+
+<p>
+The <code><Global></code> section is provided as a convenience.  Imagine
+that the administrator has many <code><VirtualHost></code> sections
 in her <code>proftpd.conf</code>, and yet has a lot of the same configuration
 for each virtual host, such as common <code><Directory></code> sections,
 <code>DefaultRoot</code> settings, <i>etc</i>.  Rather than including the
 same configuration over and over, she could use the <code><Global></code>
-context:
+section:
 <pre>
   <Global>
     ...
@@ -88,26 +96,26 @@ server configuration in the file, to every <code><VirtualHost></code> as
 well as the default "server config" server.
 
 <p>
-Which brings us to the "server config" context.  The name is
+Which brings us to the "server config" section.  The name is
 ill-suited, and is really borrowed directly from Apache's naming conventions.
-The "server config" context refers to anything not in a
-<code><VirtualHost></code> or <code><Global></code> context in
+The "server config" context refers to anything <b>not</b> in a
+<code><VirtualHost></code> or <code><Global></code> section in
 the <code>proftpd.conf</code> file.  Unlike Apache's <code>httpd.conf</code>,
 ProFTPD's configuration is designed such that one should be able to use
 the simplest file as possible.  In fact, <code>proftpd</code> will start
 if the <code>proftpd.conf</code> is completely empty; try it!  This will
 cause the daemon to use all of the default settings, which in most cases
 is not what is wanted, but it <i>is</i> possible.  With this in mind,
-there is always at least one server configuration present: the default server
-context, and it is this context that is known as the "server config".
-Just like the <code><VirtualHost></code> context, any configuration
-directives inside the "server config" context <b>do not apply</b>
-outside of the context.  Many administrators often assume that this is the
-case.  It is not.  This is what the <code><Global></code> context
-is for.
+there is always at least <i>one</i> server configuration present: the default
+server context, and it is this context that is known as the
+"server config".  Just like the <code><VirtualHost></code>
+section, any configuration directives inside the "server config"
+section <b>do not apply</b> outside of the context.  Many administrators often
+<i>assume</i> that this is the case.  It is not.  This is what the
+<code><Global></code> section is for.
 
 <p>
-However, one particular drawback to the "server config" context was
+However, one particular drawback to the "server config" section was
 that it did not provide a way to specify to which IP address that configuration
 pertained.  By default, when <code>proftpd</code> parses the
 <code>proftpd.conf</code> file, it will use the <code>gethostname()</code>
@@ -115,17 +123,18 @@ function to determine the IP address to which the default server should listen.
 On a single address, single interface system, this default is fine. It is one
 a multiple address system that the default handling does not always work;
 the administrator may wish to explicitly specify to which address the default
-server should listen.  This is what the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_DefaultAddress.html"><code>DefaultAddress</code></a>
-configuration directive provides: the ability to specify to which IP address
-the "server config" vhost should listen.
+server should listen.  This is what the <a href="../modules/mod_core.html#DefaultAddress"><code>DefaultAddress</code></a> configuration directive provides: the
+ability to specify to which IP address the "server config" vhost
+should listen.
 
 <p>
 By default, every server will listen to port 21, the IANA standard port for
 FTP.  If you want to have server react to a different port, use the
-<a href="http://www.proftpd.org/docs/directives/linked/config_ref_Port.html"><code>Port</code></a> directive to change the port.  As might be mentioned
-elsewhere, if you have many different <code><VirtualHost></code> contexts
-using the same address but different ports, you'll want to make sure that
-you leave each <code>Port</code>-1 number empty.  <a href="http://www.faqs.org/rfcs/rfc959.html">RFC 959</a> specifies that the source port for an active
+<a href="../modules/mod_core.html#Port"><code>Port</code></a> directive to
+change the port.  As might be mentioned elsewhere, if you have many different
+<code><VirtualHost></code> sections using the <i>same address</i> but
+different <i>ports</i>, you'll want to make sure that you leave each
+<code>Port</code>-1 number empty.  <a href="http://www.faqs.org/rfcs/rfc959.html">RFC 959</a> specifies that the source port for an active
 data transfer (read <a href="http://slacksite.com/other/ftp.html">here</a>)
 must be <code>L-1</code>, where <code>L</code> is the port on which your server
 listens.  Also, as mentioned in the <code>Port</code> documentation, using:
@@ -137,20 +146,20 @@ is sometimes used to disable the "server config" configuration.
 
 <p>
 There is another configuration directive that comes into play in all of this
-: <a href="http://www.proftpd.org/docs/directives/linked/config_ref_DefaultServer.html"><code>DefaultServer</code></a>.  Here is why: when a client
-contacts <code>proftpd</code>, the server has to determine which configuration
-to use for handling the client.  To do this, it searches its list of configured
-vhosts, searching for a vhost whose IP address matches the IP address that the
-client contacted.  If there's a matching vhost for that IP address, simple:
-use that configuration.  If not, <code>proftpd</code> will then resort to
-using the configuration that bears the <code>DefaultServer</code> directive,
-which says that the server configuration in which it appers should be used
-in cases like this.  If there is no <code>DefaultServer</code> directive
-in the <code>proftpd.conf</code> file, and no matching configuration can
-be found, then the client will see a message such as "no server available
-to service your request".  The <code>DefaultServer</code> can be
-used to say that a <code><VirtualHost></code> should be the default,
-and not necessarily the "server config" context, as is common.
+: <a href="../modules/mod_core.html#DefaultServer"><code>DefaultServer</code></a>.  Here is why: when a client contacts <code>proftpd</code>, the server has to
+determine which configuration to use for handling the client.  To do this, it
+searches its list of configured vhosts, searching for a vhost whose IP address
+matches the IP address that the client contacted.  If there is a matching vhost
+for that IP address, simple: use that configuration.  If not,
+<code>proftpd</code> will then resort to using the configuration that bears the
+<code>DefaultServer</code> directive, which says that the server configuration
+in which it appers should be used in cases like this.  If there is no
+<code>DefaultServer</code> directive in the <code>proftpd.conf</code> file,
+and no matching configuration can be found, then the client will see a message
+such as "no server available to service your request".  The
+<code>DefaultServer</code> can be used to say that a
+<code><VirtualHost></code> should be the default, and not necessarily the
+"server config" context, as is common.
 
 <p>
 If you would like the same virtual host configuration to be used for
@@ -169,12 +178,13 @@ of the "server config" context, use <code>DefaultAddress</code>
 
 <p>
 There is one last configuration directive about which an administrator should
-know: <a href="http://www.proftpd.org/docs/directives/linked/config_ref_SocketBindTight.html"><code>SocketBindTight</code></a>.  By default, the
+know: <a href="../modules/mod_core.html#SocketBindTight"><code>SocketBindTight</code></a>.  By default, the
 <code>proftpd</code> daemon will listen on all addresses, port 21, for the
 connection requests of remote clients.  Sometimes, the administrator may
 wish to have the <code>proftpd</code> daemon listen <b>only</b> on the IP
 addresses for which it has been configured, and not <i>every</i> address.
-To accomplish this, simply use the <a href="http://www.proftpd.org/docs/directives/linked/config_ref_SocketBindTight.html"><code>SocketBindTight</code></a> configuration directive:
+To accomplish this, simply use the <code>SocketBindTight</code> configuration
+directive:
 <pre>
   SocketBindTight on
 </pre>
@@ -232,8 +242,12 @@ addresses/DNS names:
   DefaultAddress 1.2.3.4 ftp.example.com
 </pre>
 
+<p>
 <hr>
-Last Updated: <i>$Date: 2009-01-12 23:41:26 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/VirtualUsers.html b/doc/howto/VirtualUsers.html
index e505b10..84e960c 100644
--- a/doc/howto/VirtualUsers.html
+++ b/doc/howto/VirtualUsers.html
@@ -1,15 +1,13 @@
-<!-- $Id: VirtualUsers.html,v 1.1 2005-07-05 16:11:15 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/VirtualUsers.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD mini-HOWTO - ProFTPD Virtual Users</title>
+<title>ProFTPD: Virtual Users</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Virtual Users</b></h2></center>
+<center><h2><b>ProFTPD: Virtual Users</b></h2></center>
 <hr>
 
 <p>
@@ -65,7 +63,7 @@ of virtual user names.  When working with files created by virtual users, use
 those files.  You will then need to manually make sure those IDs are the
 correct ones for the file.
 
-<p><a name="IDs"</a>
+<p><a name="IDs"></a>
 <font color=red>Question</font>: Which IDs should I use for my virtual users?<br>
 <font color=blue>Answer</font>: It does not matter.  The only UID and GID
 which are special are UID 0 (zero) and GID 0 (zero).  These IDs are used
@@ -124,10 +122,7 @@ to your <code>proftpd.conf</code>:
 </pre>
 The <code>ftpasswd</code> tool is a Perl script, distributed with the
 ProFTPD source code, under the <code>contrib/</code> directory.  A copy
-can also be found online:
-<pre>
-  <a href="http://www.castaglia.org/proftpd/contrib/ftpasswd">http://www.castaglia.org/proftpd/contrib/ftpasswd</a>
-</pre>
+can also be found <a href="https://github.com/proftpd/proftpd/blob/master/contrib/ftpasswd">on GitHub</a>.
 
 <p>
 Another very popular authentication mechanism used for virtual users is a SQL
@@ -180,7 +175,10 @@ flexible, and more in line with ProFTPD's design philosophy.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2005-07-05 16:11:15 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/howto/ftpaccess.html b/doc/howto/ftpaccess.html
new file mode 100644
index 0000000..948451c
--- /dev/null
+++ b/doc/howto/ftpaccess.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD: .ftpaccess Files</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center><h2><b>ProFTPD: <code>.ftpaccess</code> Files</b></h2></center>
+<hr>
+
+<p>
+A <code>.ftpaccess</code> file is meant to function like Apache's
+<code>.htaccess</code> file: a file that acts as free-floating section of the
+server's configuration file.  If a <code>.ftpaccess</code> file is present in
+a directory in which ProFTPD performs some action, ProFTPD will parse that
+<code>.ftpaccess</code> file as a configuration file, and act accordingly.
+Note that only <i>some</i> configuration directives are allowed in the
+<code>.ftpaccess</code> section, though.
+
+<p>
+The advantage of having this capability is that users can customize how the
+server treats directories that are under the user's control, by using files
+placed in those directories, instead of allowing the user to modify the main
+server configuration file itself.  The disadvantage is that a user is
+capable of possibly overriding a configuration value that was set in the main
+configuration file for a specific purpose.
+
+<p>
+ProFTPD treats a directory that contains a <code>.ftpaccess</code> file
+exactly as if the configuration directives in that file had been placed in
+a <code><Directory></code> section in the main <code>proftpd.conf</code>
+file.  For example, if there is a <code>/home/users/bob</code> directory on
+your system, and in that directory there was a <code>.ftpaccess</code> file
+that contained:
+<pre>
+  DirFakeUser on ~
+  DirFakeGroup on ~
+  Umask 0077
+</pre>
+it would be treated exactly as if:
+<pre>
+  <Directory /home/users/bob>
+    DirFakeUser on ~
+    DirFakeGroup on ~
+    Umask 0077
+  </Directory>
+</pre>
+was written into <code>proftpd.conf</code>.
+
+<p>
+The <a href="../modules/mod_core.html#AllowOverride"><code>AllowOverride</code></a> directive can be used to disable ProFTPD's support for <code>.ftpaccess</code> files.
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/howto/index.html b/doc/howto/index.html
index cbfacef..0013775 100644
--- a/doc/howto/index.html
+++ b/doc/howto/index.html
@@ -1,19 +1,17 @@
-<!-- $Id: index.html,v 1.33 2014-01-27 01:28:21 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/howto/index.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
-<title>ProFTPD Documentation Index</title>
+<title>ProFTPD: Documentation Index</title>
 </head>
 
 <body bgcolor=white>
 
 <hr>
-<center><h2><b>ProFTPD Documentation Index</b></h2></center>
+<center><h2><b>ProFTPD: Documentation Index</b></h2></center>
 <hr>
 
 <p>
-The following is a collection of mini-HOWTOs that cover most of the common
+The following is a collection of howtos that cover most of the common
 questions asked about ProFTPD and how to configure it.
 
 <p>
@@ -91,6 +89,11 @@ Recommended order of reading:
   </dd>
 
   <p>
+  <dt>On <a href="ftpaccess.html"><code>.ftpaccess</code> files</a>
+  <dd>Covers how <code>.ftpaccess</code> files work
+  </dd>
+
+  <p>
   <dt>On <a href="Limit.html"><code><Limit></code> sections</a>
   <dd>Covers how <code><Limit></code> sections should be configured,
       and how different <code><Limit></code>s interact
@@ -172,7 +175,7 @@ Recommended order of reading:
   </dd>
 
   <p>
-  <dt>On the specific <a href="LogLevel.html">log levels</a> supported
+  <dt>On the specific <a href="LogLevels.html">log levels</a> supported
   <dd>Covers the various log levels that ProFTPD uses for its log
       messages
   </dd>
@@ -194,6 +197,11 @@ Recommended order of reading:
   </dd>
 
   <p>
+  <dt>On <a href="Redis.html">Redis</a> support
+  <dd>Covers configuring and using Redis by various modules
+  </dd>
+
+  <p>
   <dt>On TCP/FTP/SSH <a href="KeepAlives.html">keepalive</a> functionality
   <dd>Covers keeping long-lived connections alive, using TCP, FTP, and SSH
       specific mechanisms
@@ -271,6 +279,11 @@ Recommended order of reading:
   </dd>
 
   <p>
+  <dt>On ProFTPD and <a href="AWS.html">AWS</a>
+  <dd>Covers deploying/using <code>proftpd</code> within AWS
+  </dd>
+
+  <p>
   <dt>On running proftpd as a <a href="Nonroot.html">nonroot user</a>
   <dd>Covers configuring proftpd to run as a nonroot user
   </dd>
@@ -321,7 +334,10 @@ all of the directives to see everything that ProFTPD is capable of supporting.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2014-01-27 01:28:21 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/license.txt b/doc/license.txt
index 75c3ddd..4163f3b 100644
--- a/doc/license.txt
+++ b/doc/license.txt
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/doc/mod_sample.c b/doc/mod_sample.c
index 8c3af5e..36918d5 100644
--- a/doc/mod_sample.c
+++ b/doc/mod_sample.c
@@ -8,7 +8,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -30,9 +30,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* sample module for ProFTPD
- * $Id: mod_sample.c,v 1.11 2011-05-23 21:08:34 castaglia Exp $
- */
+/* Sample module for ProFTPD */
 
 #include "conf.h"
 
diff --git a/doc/modules/index.html b/doc/modules/index.html
index 3472806..55b6087 100644
--- a/doc/modules/index.html
+++ b/doc/modules/index.html
@@ -1,6 +1,4 @@
-<!-- $Id: index.html,v 1.5 2013-02-21 19:56:02 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/index.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD Core Module Documentation</title>
@@ -109,6 +107,11 @@ distribution.
   </dd>
 
   <p>
+  <dt>The <a href="mod_redis.html"><code>mod_redis</code></a> module
+  <dd>Handles Redis configuration
+  </dd>
+
+  <p>
   <dt>The <a href="mod_rlimit.html"><code>mod_rlimit</code></a> module
   <dd>Handles resource limits
   </dd>
@@ -143,7 +146,10 @@ all of the directives to see everything that ProFTPD is capable of supporting.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2013-02-21 19:56:02 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/modules/mod_auth.html b/doc/modules/mod_auth.html
index b0961c3..05b7bae 100644
--- a/doc/modules/mod_auth.html
+++ b/doc/modules/mod_auth.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_auth</title>
@@ -11,18 +12,33 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_auth.c</code> file for
 ProFTPD 1.3.<i>x</i>, and is compiled by default.
 
 <h2>Directives</h2>
 <ul>
+  <li><a href="#AccessDenyMsg">AccessDenyMsg</a>
+  <li><a href="#AccessGrantMsg">AccessGrantMsg</a>
   <li><a href="#AllowChrootSymlinks">AllowChrootSymlinks</a>
+  <li><a href="#AllowEmptyPasswords">AllowEmptyPasswords</a>
+  <li><a href="#AnonAllowRobots">AnonAllowRobots</a>
+  <li><a href="#AnonRejectPasswords">AnonRejectPasswords</a>
   <li><a href="#AnonRequirePassword">AnonRequirePassword</a>
   <li><a href="#AuthAliasOnly">AuthAliasOnly</a>
   <li><a href="#AuthUsingAlias">AuthUsingAlias</a>
   <li><a href="#CreateHome">CreateHome</a>
+  <li><a href="#DefaultChdir">DefaultChdir</a>
   <li><a href="#DefaultRoot">DefaultRoot</a>
+  <li><a href="#DisplayLogin">DisplayLogin</a>
+  <li><a href="#MaxClients">MaxClients</a>
+  <li><a href="#MaxClientsPerClass">MaxClientsPerClass</a>
+  <li><a href="#MaxClientsPerHost">MaxClientsPerHost</a>
+  <li><a href="#MaxClientsPerUser">MaxClientsPerUser</a>
+  <li><a href="#MaxConnectionsPerHost">MaxConnectionsPerHost</a>
+  <li><a href="#MaxHostsPerUser">MaxHostsPerUser</a>
   <li><a href="#MaxLoginAttempts">MaxLoginAttempts</a>
+  <li><a href="#MaxPasswordSize">MaxPasswordSize</a>
   <li><a href="#RequireValidShell">RequireValidShell</a>
   <li><a href="#RewriteHome">RewriteHome</a>
   <li><a href="#RootLogin">RootLogin</a>
@@ -30,12 +46,63 @@ ProFTPD 1.3.<i>x</i>, and is compiled by default.
   <li><a href="#TimeoutLogin">TimeoutLogin</a>
   <li><a href="#TimeoutSession">TimeoutSession</a>
   <li><a href="#UseFtpUsers">UseFtpUsers</a>
+  <li><a href="#UseLastlog">UseLastlog</a>
   <li><a href="#UserAlias">UserAlias</a>
   <li><a href="#UserPassword">UserPassword</a>
+  <li><a href="#WtmpLog">WtmpLog</a>
 </ul>
 
+<p>
+<hr>
+<h3><a name="AccessDenyMsg">AccessDenyMsg</a></h3>
+<strong>Syntax:</strong> AccessDenyMsg <em>message</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.2 and later
+
+<p>
+When an FTP client attempts to authenticate and fails, the client is sent the
+following FTP response:
+<pre>
+  530 Login failed
+</pre>
+This "Login failed" response message can be replaced/customized by using this
+<code>AccessDenyMsg</code> directive.  In the configured <em>message</em>
+text, the <code>%u</code> variable will be resolved to the user name used by
+the FTP client in its authentication attempt.
+
+<p>
+Example:
+<pre>
+  AccessDenyMsg "%u is not authorized"
+</pre>
+
+<p>
 <hr>
-<h2><a name="AllowChrootSymlinks">AllowChrootSymlinks</a></h2>
+<h3><a name="AccessGrantMsg">AccessGrantMsg</a></h3>
+<strong>Syntax:</strong> AccessGrantMsg <em>message</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.2 and later
+
+<p>
+When an FTP client succeeds in authenticating, it will receive an FTP response
+with the 230 response code, and a message.  This successful authentication
+response message can be replaced/customized by using this
+<code>AccessGrantMsg</code> directive.  In the configured <em>message</em>
+text, the <code>%u</code> variable will be resolved to the user name used by
+the FTP client in its authentication attempt.
+
+<p>
+Example:
+<pre>
+  AccessGrantMsg "Welcome, %u!"
+</pre>
+
+<hr>
+<h3><a name="AllowChrootSymlinks">AllowChrootSymlinks</a></h3>
 <strong>Syntax:</strong> AllowChrootSymlinks <em>on|off</em><br>
 <strong>Default:</strong> AllowChrootSymlinks on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -63,7 +130,118 @@ users to run their untrusted webapps (<i>e.g.</i> PHP, Perl, Ruby, Python,
 
 <p>
 <hr>
-<h2><a name="AnonRequirePassword">AnonRequirePassword</a></h2>
+<h3><a name="AllowEmptyPasswords">AllowEmptyPasswords</a></h3>
+<strong>Syntax:</strong> AllowEmptyPasswords <em>on|off</em><br>
+<strong>Default:</strong> AllowEmptyPasswords on<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>AllowEmptyPasswords</code> directive configures whether
+<code>proftpd</code> will accept empty passwords or not.  For backward
+compatibility, the default is <em>on</em>.
+
+<p>
+<b>Note</b> that this applies to <code>mod_sftp</code> password-based logins
+as well.
+
+<p>
+<hr>
+<h3><a name="AnonAllowRobots">AnonAllowRobots</a></h3>
+<strong>Syntax:</strong> AnonAllowRobots <em>on|off</em><br>
+<strong>Default:</strong> AnonAllowRobots off<br>
+<strong>Context:</strong> <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.3.6rc2 and later
+
+<p>
+The <code>AnonAllowRobots</code> directive configures whether the
+<code>mod_auth</code> should provide a fake
+<a href="http://www.robotstxt.org/">"robots.txt"</a> file to web
+crawlers/spiders.
+
+<p>
+Normally such web crawlers/spiders make HTTP requests only.  However,
+<a href="https://developers.google.com/webmasters/control-crawl-index/docs/robots_txt#file-location--range-of-validity">Google</a> <em>is</em> known to crawl
+FTP sites, using anonymous logins only.
+
+<p>
+To prevent such web crawlers from indexing your FTP site unexpectedly,
+the <code>mod_auth</code> module will automatically provide a fake "robots.txt"
+file for anonymous logins, containing:
+<pre>
+  User-agent: *
+  Disallow: /
+</pre>
+
+<p>
+If your FTP site <i>deliberately</i> provides its own separate "robots.txt"
+file already, then <code>mod_auth</code> will serve that existing file as
+expected.  Alternatively, you can disable this behavior using:
+<pre>
+  # Restore previous behavior
+  AnonAllowRobots on
+</pre>
+
+<p>
+<hr>
+<h3><a name="AnonRejectPasswords">AnonRejectPasswords</a></h3>
+<strong>Syntax:</strong> AnonRejectPasswords <em>pattern [flags]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.9rc1 and later
+
+<p>
+The <code>AnonRejectPasswords</code> directive configures a regular expression
+<em>pattern</em> filter for passwords given for anonymous logins.  If the
+given anonymous password matches the configured regular expression
+<em>pattern</em>, the anonymous login is denied.
+
+<p>
+Example:
+<pre>
+  # Reject all <Anonymous> logins that use "evil.org" as part of the password
+  AnonRejectPasswords @evil\.org$
+</pre>
+
+<p>
+The optional <em>flags</em> parameter can be used to specify flags for the
+given regular expression; currently the supported flags are:
+<ul>
+  <li><code>[NC|nocase]</code>
+</ul>
+Thus for a case-insensitive pattern, you would use:
+<pre>
+  # Reject all <Anonymous> logins that use "evil.org" as part of the password
+  AnonRejectPasswords @evil\.org$ [NC]
+</pre>
+or:
+<pre>
+  # Reject all <Anonymous> logins that use "evil.org" as part of the password
+  AnonRejectPasswords @evil\.org$ [nocase]
+</pre>
+
+<p>
+If you want to reject any anonymous passwords which do <b>not</b> match the
+pattern, then prefix your pattern with the <code>!</code> (exclamation point)
+character:
+<pre>
+  # Reject all <Anonymous> logins that do NOT use "good.org" as part of the password
+  AnonRejectPasswords !@good\.org$ [nocase]
+</pre>
+<b>Note</b> that this also allows you to use <code>AnonRejectPasswords</code>
+to <b>require</b> that your anonymous logins use email-like passwords:
+<pre>
+  # Require anonymous passwords that look like email addresses.  See:
+  #  <a href="http://www.regular-expressions.info/email.html">http://www.regular-expressions.info/email.html</a>
+  AnonRejectPasswords !^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$ [NC]
+</pre>
+
+<p>
+<hr>
+<h3><a name="AnonRequirePassword">AnonRequirePassword</a></h3>
 <strong>Syntax:</strong> AnonRequirePassword <em>off|on</em><br>
 <strong>Default:</strong> AnonRequirePassword off<br>
 <strong>Context:</strong> <code><Anonymous></code><br>
@@ -89,7 +267,7 @@ See also: <a href="#AuthUsingAlias"><code>AuthUsingAlias</code></a>, <a href="#U
 
 <p>
 <hr>
-<h2><a name="AuthAliasOnly">AuthAliasOnly</a></h2>
+<h3><a name="AuthAliasOnly">AuthAliasOnly</a></h3>
 <strong>Syntax:</strong> AuthAliasOnly <em>off|on</em><br>
 <strong>Default:</strong> AuthAliasOnly off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -109,7 +287,7 @@ See also: <a href="#AuthUsingAlias"><code>AuthUsingAlias</code></a>, <a href="#U
 
 <p>
 <hr>
-<h2><a name="AuthUsingAlias">AuthUsingAlias</a></h2>
+<h3><a name="AuthUsingAlias">AuthUsingAlias</a></h3>
 <strong>Syntax:</strong> AuthUsingAlias <em>off|on</em><br>
 <strong>Default:</strong> AuthUsingAlias off<br>
 <strong>Context:</strong> <code><Anonymous></code><br>
@@ -161,7 +339,7 @@ See also: <a href="#AnonRequirePassword"><code>AnonRequirePassword</code></a>, <
 
 <p>
 <hr>
-<h2><a name="CreateHome">CreateHome</a></h2>
+<h3><a name="CreateHome">CreateHome</a></h3>
 <strong>Syntax:</strong> CreateHome <em>off|on [mode] [skel path] [dirmode mode]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -217,7 +395,40 @@ with more examples, can be read <a href="../howto/CreateHome.html">here</a>.
 
 <p>
 <hr>
-<h2><a name="DefaultRoot">DefaultRoot</a></h2>
+<h3><a name="DefaultChdir">DefaultChdir</a></h3>
+<strong>Syntax:</strong> DefaultChdir <em>path [group-expression]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.0rc1
+
+<p>
+The <code>DefaultChdir</code> directive determines the directory <em>path</em>
+into which a user is placed, after logging in.
+
+<p>
+By default, the user is put into their home directory.  The specified
+<em>path</em> can be relative to the user's home directory.  <b>Note</b> that
+if the specified <em>path</em> is not available, then <code>DefaultChdir</code>
+will be ignored; the direction in which the user is placed will be determined
+by other directives.
+
+<p>
+Examples:
+<pre>
+  # Admin users start off in /var/www
+  DefaultChdir /var/www admin
+
+  # ..and others start off in their respective public FTP folders
+  DefaultChdir ~/public_ftp
+</pre>
+
+<p>
+See also: <a href="#DefaultRoot"><code>DefaultRoot</code></a>
+
+<p>
+<hr>
+<h3><a name="DefaultRoot">DefaultRoot</a></h3>
 <strong>Syntax:</strong> DefaultRoot <em>path [group-expression]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -231,7 +442,239 @@ found in the <a href="../howto/Chroot.html">Chroot</a> howto.
 
 <p>
 <hr>
-<h2><a name="MaxLoginAttempts">MaxLoginAttempts</a></h2>
+<h3><a name="DisplayLogin">DisplayLogin</a></h3>
+<strong>Syntax:</strong> DisplayLogin <em>path</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 0.99.0
+
+<p>
+The <code>DisplayLogin</code> directive specifies a file <em>path</em> which
+will be displayed to the user when they initially login.  The <em>path</em>
+can be either relative or absolute.  For relative <em>paths</em>, the file is
+searched for in the initial directory in which a user is placed immediately
+after authentication, <i>e.g.</i> the home directory for normal users, or
+the <code><Anonymous></code> directory for anonymous logins.  If the file
+cannot be found or accessed, no error occurs and nothing is displayed to the
+client.
+
+<p>
+The <a href="../howto/DisplayFiles.html">DisplayFiles</a> howto covers such
+files in greater detail.
+
+<p>
+<hr>
+<h3><a name="MaxClients">MaxClients</a></h3>
+<strong>Syntax:</strong> MaxClients <em>count|"none" [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>MaxClients</code> directive configures the maximum number of
+<b>authenticated</b> clients which may be logged into a server or
+<code><Anonymous></code> account.  Once the <em>count</em> limit is
+reached, additional clients attempting to authenticate will be disconnected.
+The special <em>count</em> parameter value of <em>"none"</em> may be used,
+which disables all other applicable <code>MaxClients</code> directives.
+
+<p>
+Additionally, an optional <em>message</em> parameter may be used; this message
+will be displayed to a client attempting to exceed the maximum value,
+immediately before disconnection.  The <em>message</em> parameter is parsed for
+the variable "%m", which is replaced with the configured maximum value. If
+a message is not supplied, then following default message is used:
+<pre>
+  "Sorry, the maximum number of allowed clients (%m) are already connected."
+</pre>
+
+<p>
+For example, using:
+<pre>
+  MaxClients 5
+</pre>
+will result in this FTP response, when the limit is reached:
+<pre>
+  "530 Sorry, the maximum number of allowed users are already connected (5)"
+</pre>
+
+<p>
+See also: <a href="#MaxClientsPerClass"><code>MaxClientsPerClass</code></a>,
+<a href="#MaxClientsPerHost"><code>MaxClientsPerHost</code></a>,
+<a href="#MaxClientsPerUser"><code>MaxClientsPerUser</code></a>,
+<a href="#MaxHostsPerUser"><code>MaxHostsPerUser</code></a>
+
+<p>
+<hr>
+<h3><a name="MaxClientsPerClass">MaxClientsPerClass</a></h3>
+<strong>Syntax:</strong> MaxClientsPerClass <em>class count|"none" [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.10rc1 and later
+
+<p>
+The <code>MaxClientsPerClass</code> directive configures the maximum number of
+clients that may be connected at any given time from the same
+<a href="../howto/Classes.html">Class</a>.  The optional <em>message</em>
+parameter may be used, which will be displayed to a client attempting to exceed
+the <em>count</em> maximum value.  If <em>message</em> is not supplied, 
+then the following default message is used:
+<pre>
+  "Sorry, the maximum number of clients (%m) from your class are already connected."
+</pre>
+
+<p>
+For example:
+<pre>
+  MaxClientsPerClass foo 1 "Only one such client at a time."
+</pre>
+results in this FTP response, to a client exceeding the limit:
+<pre>
+  "530 Only one such client at a time."
+</pre>
+
+<p>
+See also: <a href="#MaxClients"><code>MaxClients</code></a>,
+<a href="#MaxClientsPerHost"><code>MaxClientsPerHost</code></a>,
+<a href="#MaxClientsPerUser"><code>MaxClientsPerUser</code></a>,
+<a href="#MaxHostsPerUser"><code>MaxHostsPerUser</code></a>
+
+<p>
+<hr>
+<h3><a name="MaxClientsPerHost">MaxClientsPerHost</a></h3>
+<strong>Syntax:</strong> MaxClientsPerHost <em>count|"none" [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.1.7 and later
+
+<p>
+The <code>MaxClientsPerHost</code> directive configures the maximum number of
+clients allowed to connect <i>from</i> a host.  The optional <em>message</em>
+parameter may be used, which will be displayed to a client attempting to exceed
+the <em>count</em> maximum value.  If <em>message</em> is not supplied, this
+default message is used:
+<pre>
+  "Sorry, the maximum number clients (%m) from your host are already connected." is used. 
+</pre>
+
+<p>
+For example:
+<pre>
+  MaxClientsPerHost 1 "Sorry, you may not connect more than one time."
+</pre>
+results in this FTP response, to a client exceeding the limit:
+<pre>
+  "530 Sorry, you may not connect more than one time."
+</pre>
+
+<p>
+See also: <a href="#MaxClients"><code>MaxClients</code></a>,
+<a href="#MaxClientsPerClass"><code>MaxClientsPerClass</code></a>,
+<a href="#MaxClientsPerUser"><code>MaxClientsPerUser</code></a>,
+<a href="#MaxHostsPerUser"><code>MaxHostsPerUser</code></a>
+
+<p>
+<hr>
+<h3><a name="MaxClientsPerUser">MaxClientsPerUser</a></h3>
+<strong>Syntax:</strong> MaxClientsPerUser <em>count|"none" [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.7rc1 and later
+
+<p>
+The <code>MaxClientsPerUser</code> directive configures the maximum number of
+clients that may be connected at any given time <i>using the same user name</i>.
+The optional <em>message</em> parameter may be used, which will be displayed to
+a client attempting to exceed the <em>count</em> maximum value.  If
+<em>message</em> is not supplied, the following default message is used:
+<pre>
+  "Sorry, the maximum number of clients (%m) for this user already connected."
+</pre>
+
+<p>
+For example:
+<pre>
+  MaxClientsPerUser 1 "Only one such user at a time."
+</pre>
+results in this FTP response, to a client exceeding the limit:
+<pre>
+  "530 Only one such user at a time."
+</pre>
+
+<p>
+See also: <a href="#MaxClients"><code>MaxClients</code></a>,
+<a href="#MaxClientsPerClass"><code>MaxClientsPerClass</code></a>,
+<a href="#MaxClientsPerHost"><code>MaxClientsPerHost</code></a>,
+<a href="#MaxHostsPerUser"><code>MaxHostsPerUser</code></a>
+
+<p>
+<hr>
+<h3><a name="MaxConnectionsPerHost">MaxConnectionsPerHost</a></h3>
+<strong>Syntax:</strong> MaxConnectionsPerHost <em>count|"none" [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.11 and later
+
+<p>
+The <code>MaxConnectionsPerHost</code> directive configures the maximum number
+of <b>unauthenticated</b> clients allowed to connect from a given host.  The
+optional <em>message</em> parameter may be used, to be displayed to a client
+attempting to exceed the maximum value.   If <em>message</em> is not supplied,
+a default message of "Sorry, the maximum number of connections (%m) from your host are already connected." is used.
+
+<p>
+For example:
+<pre>
+  MaxConnectionsPerHost 1 "Sorry, you may not connect more than one time."
+</pre>
+results in additional FTP login attempts from that same host to receive
+the following FTP response:
+<pre>
+  530 Sorry, you may not connect more than one time.
+</pre>
+
+<p>
+<hr>
+<h3><a name="MaxHostsPerUser">MaxHostsPerUser</a></h3>
+<strong>Syntax:</strong> MaxHostsPerUser <em>count|"none" [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.2.4 and later
+
+<p>
+The <code>MaxHostsPerUser</code> directive configures the maximum number of
+times different hosts, using a given login, can connect at any given time.
+The optional <em>message</em> parameter may be used, which will be displayed to
+a client attempting to exceed the maximum value.  If <em>message</em> is
+<i>not</i> supplied, the following message is used by default:
+<pre>
+  "Sorry, the maximum number of hosts (%m) for this user already connected."
+</pre>
+
+<p>
+For example:
+<pre>
+  MaxHostsPerUser 1 "Sorry, you may not connect more than one time."
+</pre>
+Will result in the following FTP response, when the <em>count</em> limit is
+exceeded:
+<pre>
+  "530 Sorry, you may not connect more than one time."
+</pre>
+
+<p>
+See also: <a href="#MaxClients"><code>MaxClients</code></a>, <a href="#MaxClientsPerHost"><code>MaxClientsPerHost</code></a>
+
+<p>
+<hr>
+<h3><a name="MaxLoginAttempts">MaxLoginAttempts</a></h3>
 <strong>Syntax:</strong> MaxLoginAttempts <em>count</em><br>
 <strong>Default:</strong> 3<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -247,7 +690,26 @@ logged.
 
 <p>
 <hr>
-<h2><a name="RequireValidShell">RequireValidShell</a></h2>
+<h3><a name="MaxPasswordSize">MaxPasswordSize</a></h3>
+<strong>Syntax:</strong> MaxPasswordSize <em>length</em><br>
+<strong>Default:</strong> 1024<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.3.6rc3 and later
+
+<p>
+The <code>MaxPasswordSize</code> directive configures the maximum length
+(in bytes) of a password that ProFTPD will accept.  Passwords longer than
+the configured <em>length</em> will be ignored.
+
+<p>
+This directive is provided as a defensive measure, to protect against CPU
+resource consumption attacks by feeding large amounts of data to <i>e.g.</i>
+the <code>crypt(3)</code> function.
+
+<p>
+<hr>
+<h3><a name="RequireValidShell">RequireValidShell</a></h3>
 <strong>Syntax:</strong> RequireValidShell <em>on|off</em><br>
 <strong>Default:</strong> RequireValidShell on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -264,10 +726,10 @@ default shells are assumed to be valid.
 
 <p>
 <hr>
-<h2><a name="RewriteHome">RewriteHome</a></h2>
+<h3><a name="RewriteHome">RewriteHome</a></h3>
 <strong>Syntax:</strong> RewriteHome <em>on|off</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_auth<br>
 <strong>Compatibility:</strong> 1.3.3rc1 and later
 
@@ -302,7 +764,7 @@ home directories is "REWRITE_HOME".  Thus would you use:
 
 <p>
 <hr>
-<h2><a name="RootLogin">RootLogin</a></h2>
+<h3><a name="RootLogin">RootLogin</a></h3>
 <strong>Syntax:</strong> RootLogin <em>on|off</em><br>
 <strong>Default:</strong> RootLogin off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -332,7 +794,7 @@ in the <code><Anonymous></code> section is set to 'root'.
 
 <p>
 <hr>
-<h2><a name="RootRevoke">RootRevoke</a></h2>
+<h3><a name="RootRevoke">RootRevoke</a></h3>
 <strong>Syntax:</strong> RootRevoke <em>on|off|UseNonCompliantActiveTransfers</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -391,7 +853,7 @@ have been dropped completely.
 
 <p>
 <hr>
-<h2><a name="TimeoutLogin">TimeoutLogin</a></h2>
+<h3><a name="TimeoutLogin">TimeoutLogin</a></h3>
 <strong>Syntax:</strong> TimeoutLogin <em>seconds</em><br>
 <strong>Default:</strong> 300 seconds<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -414,7 +876,7 @@ See also: <a href="mod_core.html#TimeoutIdle"><code>TimeoutIdle</code></a>,
 
 <p>
 <hr>
-<h2><a name="TimeoutSession">TimeoutSession</a></h2>
+<h3><a name="TimeoutSession">TimeoutSession</a></h3>
 <strong>Syntax:</strong> TimeoutSessions <em>seconds</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -431,7 +893,7 @@ indefinitely; this is the default.  There is no maxium value for the
 
 <p>
 <hr>
-<h2><a name="UseFtpUsers">UseFtpUsers</a></h2>
+<h3><a name="UseFtpUsers">UseFtpUsers</a></h3>
 <strong>Syntax:</strong> UseFtpUsers <em>on|off</em><br>
 <strong>Default:</strong> UseFtpUsers on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -452,7 +914,27 @@ using the <code>UseFtpUsers</code> directive, <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="UserAlias">UserAlias</a></h2>
+<h3><a name="UseLastlog">UseLastlog</a></h3>
+<strong>Syntax:</strong> UseLastlog <em>on|off</em><br>
+<strong>Default:</strong> UseLastlog off<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.3.1 and later
+
+<p>
+The <code>UseLastlog</code> directive configures whether ProFTPD will update
+the <a href="https://en.wikipedia.org/wiki/Lastlog"><code>/var/log/lastlog</code></a> file for FTP logins.
+
+<p>
+Example:
+<pre>
+  # Enable recording FTP logins in /var/log/lastlog
+  UseLastlog on
+</pre>
+
+<p>
+<hr>
+<h3><a name="UserAlias">UserAlias</a></h3>
 <strong>Syntax:</strong> UserAlias <em>alias real-user</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -483,7 +965,7 @@ by using the following in the config file:
 
 <p>
 <hr>
-<h2><a name="UserPassword">UserPassword</a></h2>
+<h3><a name="UserPassword">UserPassword</a></h3>
 <strong>Syntax:</strong> UserPassword <em>user encrypted-password</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -504,7 +986,7 @@ cleartext password</b>.  To obtain this <em>encrypted-password</em> value,
 you can use the <a href="../utils/ftpasswd.html"><code>ftpasswd</code></a>
 script's <code>--hash</code> option, <i>e.g.</i>:
 <pre>
-  # ftpasswd --hash
+  $ ftpasswd --hash
 
   Password: 
   Re-type password: 
@@ -521,13 +1003,28 @@ Example configuration:
 
 <p>
 <hr>
+<h3><a name="WtmpLog">WtmpLog</a></h3>
+<strong>Syntax:</strong> WtmpLog <em>on|off</em><br>
+<strong>Default:</strong> WtmpLog on<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_auth<br>
+<strong>Compatibility:</strong> 1.1.7 and later
+
+<p>
+The <code>WtmpLog</code> directive controls the logging of connections to the
+host system <a href="https://en.wikipedia.org/wiki/Utmp"><code>wtmp</code></a>
+file, which used by such commands as <code>last</code>.  By default, <b>all</b>
+connections are logged via <code>wtmp</code>.
+
+<p>
+<hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_auth</code> module is compiled by default.
 
 <p>
 <hr>
 <font size=2><b><i>
-© Copyright 2002-2016<br>
+© Copyright 2002-2017<br>
  All Rights Reserved<br>
 </i></b></font>
 <hr>
diff --git a/doc/modules/mod_auth_file.html b/doc/modules/mod_auth_file.html
index 90ec0d7..39402d5 100644
--- a/doc/modules/mod_auth_file.html
+++ b/doc/modules/mod_auth_file.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_auth_file.html,v 1.6 2014-01-22 04:18:09 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_auth_file.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_auth_file</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_auth_file.c</code> file for
 ProFTPD 1.3.<i>x</i>, and is compiled by default.
 
@@ -24,7 +23,7 @@ ProFTPD 1.3.<i>x</i>, and is compiled by default.
 </ul>
 
 <hr>
-<h2><a name="AuthGroupFile">AuthGroupFile</a></h2>
+<h3><a name="AuthGroupFile">AuthGroupFile</a></h3>
 <strong>Syntax:</strong> AuthGroupFile <em>path [id min-max] [name regex]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -77,7 +76,7 @@ will be logged on server startup/restart.
 
 <p>
 <hr>
-<h2><a name="AuthUserFile">AuthUserFile</a></h2>
+<h3><a name="AuthUserFile">AuthUserFile</a></h3>
 <strong>Syntax:</strong> AuthUserFile <em>path [id min-max] [home regex] [name regex]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -136,6 +135,21 @@ will be logged on server startup/restart.
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_auth_file</code> module is compiled by default.
 
+<p>
+<b>Logging</b><br>
+The <code>mod_auth_file</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>auth.file
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace auth.file:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -175,7 +189,7 @@ or:
 <code>AuthGroupFile</code> is world-writable, then <b>any user on the system
 can edit that file</b>.  They can create new users, or change the entries
 for existing users such that those users have different privileges, perhaps
-even root privileges.  In short, having <code>AuthUserFile<code> or
+even root privileges.  In short, having <code>AuthUserFile</code> or
 <code>AuthGroupFile</code> with world-writable permissions is an unsafe
 configuration, and now <code>mod_auth_file</code> prevents this.
 
@@ -195,19 +209,12 @@ directory would allow <b>any system user to delete the <code>AuthUserFile</code>
 unsafe configuration against which <code>mod_auth_file</code> now guards.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-22 04:18:09 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2002-2014<br>
+© Copyright 2002-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/modules/mod_auth_pam.html b/doc/modules/mod_auth_pam.html
index 18e0dab..4e4b889 100644
--- a/doc/modules/mod_auth_pam.html
+++ b/doc/modules/mod_auth_pam.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_auth_pam.html,v 1.3 2011-03-10 07:28:33 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_auth_pam.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_auth_pam</title>
@@ -18,7 +16,7 @@
 PAM stands for <b>P</b>luggable <b>A</b>uthentication <b>M</b>odules,
 and is used to configure ways for authenticating users.  Now
 "authenticating" a user usually means comparing a password they
-give with some other information, and returning a &quotyes/no"-style
+give with some other information, and returning a "yes/no"-style
 answer.  PAM does <b>not</b> provide all of the other information for a user,
 such as UID, GID, home, and shell.  This means that <code>mod_auth_pam</code>
 cannot be used, by itself, as an auth module for <code>proftpd</code>;
@@ -41,7 +39,7 @@ ProFTPD source distribution.
 </ul>
 
 <hr>
-<h2><a name="AuthPAM">AuthPAM</a></h2>
+<h3><a name="AuthPAM">AuthPAM</a></h3>
 <strong>Syntax:</strong> AuthPAM <em>on|off</em><br>
 <strong>Default:</strong> AuthPAM on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -55,7 +53,7 @@ when authenticating a user.
 
 <p>
 <hr>
-<h2><a name="AuthPAMConfig">AuthPAMConfig</a></h2>
+<h3><a name="AuthPAMConfig">AuthPAMConfig</a></h3>
 <strong>Syntax:</strong> AuthPAMConfig <em>service</em><br>
 <strong>Default:</strong> AuthPAMConfig ftp<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -80,7 +78,7 @@ Here's an example of changing the <em>service</em> used:
 
 <p>
 <hr>
-<h2><a name="AuthPAMOptions">AuthPAMOptions</a></h2>
+<h3><a name="AuthPAMOptions">AuthPAMOptions</a></h3>
 <strong>Syntax:</strong> AuthPAMOptions <em>opt1 opt2 ... optN</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -126,6 +124,21 @@ The <code>mod_auth_pam</code> module is automatically included when
 this automatic inclusion, use the <code>--disable-auth-pam</code> configure
 option.
 
+<p>
+<b>Logging</b><br>
+The <code>mod_auth_pam</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>auth.pam
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace auth.pam:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -149,10 +162,13 @@ The asterisk ("*") after a module name in the <code>AuthOrder</code> directive
 is what tells <code>proftpd</code> to treat that module's results as
 authoritative.
 
+<p>
 <hr>
-Last Updated: <i>$Date: 2011-03-10 07:28:33 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_auth_unix.html b/doc/modules/mod_auth_unix.html
index 07dc2e5..2061644 100644
--- a/doc/modules/mod_auth_unix.html
+++ b/doc/modules/mod_auth_unix.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_auth_unix.html,v 1.7 2013-09-29 23:28:02 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_auth_unix.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_auth_unix</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_auth_unix.c</code> file for
 ProFTPD 1.3.<i>x</i>, and is compiled by default.
 
@@ -24,7 +23,7 @@ ProFTPD 1.3.<i>x</i>, and is compiled by default.
 </ul>
 
 <hr>
-<h2><a name="AuthUnixOptions">AuthUnixOptions</a></h2>
+<h3><a name="AuthUnixOptions">AuthUnixOptions</a></h3>
 <strong>Syntax:</strong> AuthUnixOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -36,7 +35,7 @@ The <code>AuthUnixOptions</code> directive is used to tweak various
 Unix-specific authentication behaviors in <code>mod_auth_unix</code>.  The
 currently implemented options are:
 <ul>
-  <li><code>aixNoRLogin</code><br>
+  <li><code>AIXNoRLogin</code><br>
     <p>
     In <a href="http://bugs.proftpd.org/show_bug.cgi?id=1896">Bug#1896</a>,
     support for checking some AIX-specific functions for whether a login
@@ -47,7 +46,7 @@ currently implemented options are:
     to allow FTP logins.  To enable this specific behavior, a new
     <code>AuthUnixOptions</code> setting (only honored on AIX) was added:
 <pre>
-        AuthUnixOptions aixNoRLogin
+        AuthUnixOptions AIXNoRLogin
 </pre>
     If this setting is used on any other server, it is silently ignored.
     <a href="http://bugs.proftpd.org/show_bug.cgi?id=3300">Bug#3300</a> has
@@ -55,7 +54,7 @@ currently implemented options are:
   </li>
 
   <p>
-  <li><code>magicTokenChroot</code><br>
+  <li><code>MagicTokenChroot</code><br>
     <p>
     This option causes <code>mod_auth_unix</code> to examine the home
     directory retrieved for a user for the magic "/./" token.  If found,
@@ -73,7 +72,7 @@ currently implemented options are:
   </li>
 
   <p>
-  <li><code>noGetgrouplist</code><br>
+  <li><code>NoGetgrouplist</code><br>
     <p>
     On systems which support it, the <code>getgrouplist(3)</code> function
     can be used to get the group membership list of a user in a <i>much</i>
@@ -83,15 +82,33 @@ currently implemented options are:
     Use this option to disable use of the <code>getgrouplist(3)</code>
     function, <i>e.g.</i>:
 <pre>
-        AuthUnixOptions noGetgrouplist
+        AuthUnixOptions NoGetgrouplist
 </pre>
     This setting has no effect on systems which do not support the
     <code>getgrouplist(3)</code> function.
+  </li>
+
+  <p>
+  <li><code>NoInitgroups</code><br>
+    <p>
+    On systems which support it, the <code>initgroups(3)</code> function
+    can be used to get the group membership list of a user in a <i>much</i>
+    faster way.  However, there are limits to the number of groups to which
+    a user can belong, use of this function means that groups which exceed
+    that limit <b>will be silently ignored</b>.  Thus for sites which need
+    users to belong to a <i>large</i> number of groups, use this option to
+    disable the use of the <code>initgroups(3)</code> function, <i>e.g.</i>:
+<pre>
+        AuthUnixOptions NoInitgroups
+</pre>
+    This setting has no effect on systems which do not support the
+    <code>initgroups(3)</code> function.
+  </li>
 </ul>
 
 <p>
 <hr>
-<h2><a name="PersistentPasswd">PersistentPasswd</a></h2>
+<h3><a name="PersistentPasswd">PersistentPasswd</a></h3>
 <strong>Syntax:</strong> PersistentPasswd <em>on|off</em><br>
 <strong>Default:</strong> off<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -125,6 +142,21 @@ from <code>NIS</code> maps or <code>NSS</code> lookups, and local users.
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_auth_unix</code> module is compiled by default.
 
+<p>
+<b>Logging</b><br>
+The <code>mod_auth_unix</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>auth.unix
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace auth.unix:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -194,20 +226,52 @@ But <code>proftpd</code> still has a descriptor to the <i>old</i> file, and
 does not see/know that the file has changed.  That is why it can look like
 <code>proftpd</code> is "caching" your system users.
 
+<p><a name="DeleteViaPasswd">
+<font color=red>Question</font>: I recently deleted a user's password using:
+<pre>
+  $ passwd -d <em>user</em>
+</pre>
+This successfully prevented the user from logging in via <code>ssh</code>,
+but they <i>were</i> able to successfully login using <code>proftpd</code>.
+This is a bug, right?<br>
+<font color=blue>Answer</font>: No, not really.  It's more of a nasty gotcha.
+
 <p>
-<hr><br>
+Per the <code>passwd(1)</code> man page, the <code>-d</code> option does not
+do what you might assume/think it does:
+<pre>
+  -d
+  This is a quick way to delete a password for an account. It will set
+  the named account passwordless. Available to root only.
+</pre>
+Thus the <code>-d</code> option only <i>deletes the password</i>; it does
+<b>not</b> lock the account by setting a password of "*", and it does <b>not</b>
+delete the account/entry.  Rather than using <code>passwd -d</code>, I would
+strongly recommend using <code>passwd -l</code> (to <em>lock</em> the account),
+or <code>userdel(8)</code> or equivalent (to <em>remove</em> the account
+entirely).
 
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-09-29 23:28:02 $</i><br>
+<p>
+The issue then is that <code>proftpd</code> does not <b>require</b> that
+clients provide non-empty passwords by default.  Thus a client providing
+an empty string as the password for a so-called "deleted" account would be
+able to successfully log in.  To address this, there is the
+<a href="./mod_auth.html#AllowEmptyPasswords"><code>AllowEmptyPasswords</code></a> directive.  For example, setting this in your <code>proftpd.conf</code>
+should make using <code>passwd -d</code> work as you'd expect:
+<pre>
+  <Global>
+    AllowEmptyPassword off
+  </Global>
+</pre>
 
-<br><hr>
+<p>
+<hr><br>
 
 <font size=2><b><i>
-© Copyright 2010-2013<br>
+© Copyright 2010-2015 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/modules/mod_cap.html b/doc/modules/mod_cap.html
index 178845f..776259c 100644
--- a/doc/modules/mod_cap.html
+++ b/doc/modules/mod_cap.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_cap.html,v 1.9 2013-07-19 17:17:00 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_cap.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_cap</title>
@@ -88,7 +86,7 @@ ProFTPD source distribution:
 </ul>
 
 <hr>
-<h2><a name="CapabilitiesEngine">CapabilitiesEngine</a></h2>
+<h3><a name="CapabilitiesEngine">CapabilitiesEngine</a></h3>
 <strong>Syntax:</strong> CapabilitiesEngine <em>on|off</em><br>
 <strong>Default:</strong> on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -103,7 +101,7 @@ module.
 
 <p>
 <hr>
-<h2><a name="CapabilitiesRootRevoke">CapabilitiesRootRevoke</a></h2>
+<h3><a name="CapabilitiesRootRevoke">CapabilitiesRootRevoke</a></h3>
 <strong>Syntax:</strong> CapabilitiesRootRevoke <em>on|off</em><br>
 <strong>Default:</strong> CapabilitiesRootRevoke on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -121,7 +119,7 @@ default behavior, <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="CapabilitiesSet">CapabilitiesSet</a></h2>
+<h3><a name="CapabilitiesSet">CapabilitiesSet</a></h3>
 <strong>Syntax:</strong> CapabilitiesSet <em>[+|- cap] ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -216,18 +214,13 @@ you will need to use a configuration such as:
 </pre>
 
 <p>
-<hr><br>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-07-19 17:17:00 $</i><br>
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2000-2013 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
 
-<hr><br>
-
+<hr>
 </body>
 </html>
 
diff --git a/doc/modules/mod_core.html b/doc/modules/mod_core.html
index 2e2ded9..c7a09a3 100644
--- a/doc/modules/mod_core.html
+++ b/doc/modules/mod_core.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_core.html,v 1.43 2014-01-30 16:50:10 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_core.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_core</title>
@@ -19,63 +17,232 @@ The <code>mod_core</code> module handles most of the core FTP commands.
 
 <h2>Directives</h2>
 <ul>
+  <li><a href="#Allow">Allow</a>
+  <li><a href="#AllowAll">AllowAll</a>
+  <li><a href="#AllowClass">AllowClass</a>
   <li><a href="#AllowFilter">AllowFilter</a>
+  <li><a href="#AllowForeignAddress">AllowForeignAddress</a>
+  <li><a href="#AllowGroup">AllowGroup</a>
+  <li><a href="#AllowOverride">AllowOverride</a>
+  <li><a href="#AllowUser">AllowUser</a>
+  <li><a href="#Anonymous"><Anonymous></a>
   <li><a href="#AuthOrder">AuthOrder</a>
+  <li><a href="#Class"><Class></a>
+  <li><a href="#CommandBufferSize">CommandBufferSize</a>
   <li><a href="#DebugLevel">DebugLevel</a>
   <li><a href="#DefaultAddress">DefaultAddress</a>
+  <li><a href="#DefaultServer">DefaultServer</a>
+  <li><a href="#Define">Define</a>
+  <li><a href="#Deny">Deny</a>
+  <li><a href="#DenyAll">DenyAll</a>
+  <li><a href="#DenyClass">DenyClass</a>
   <li><a href="#DenyFilter">DenyFilter</a>
+  <li><a href="#DenyGroup">DenyGroup</a>
+  <li><a href="#DenyUser">DenyUser</a>
+  <li><a href="#Directory"><Directory></a>
   <li><a href="#DisplayChdir">DisplayChdir</a>
   <li><a href="#DisplayConnect">DisplayConnect</a>
   <li><a href="#DisplayQuit">DisplayQuit</a>
+  <li><a href="#FSCachePolicy">FSCachePolicy</a>
+  <li><a href="#FSOptions">FSOptions</a>
+  <li><a href="#Global"><Global></a>
+  <li><a href="#Group">Group</a>
   <li><a href="#GroupOwner">GroupOwner</a>
+  <li><a href="#HideFiles">HideFiles</a>
   <li><a href="#HideGroup">HideGroup</a>
   <li><a href="#HideNoAccess">HideNoAccess</a>
   <li><a href="#HideUser">HideUser</a>
+  <li><a href="#IgnoreHidden">IgnoreHidden</a>
+  <li><a href="#IfDefine"><IfDefine></a>
+  <li><a href="#IfModule"><IfModule></a>
   <li><a href="#Include">Include</a>
+  <li><a href="#IncludeOptions">IncludeOptions</a>
+  <li><a href="#Limit"><Limit></a>
   <li><a href="#MasqueradeAddress">MasqueradeAddress</a>
   <li><a href="#MaxCommandRate">MaxCommandRate</a>
   <li><a href="#MaxConnectionRate">MaxConnectionRate</a>
   <li><a href="#MaxInstances">MaxInstances</a>
+  <li><a href="#MultilineRFC2228">MultilineRFC2228</a>
+  <li><a href="#Order">Order</a>
   <li><a href="#PassivePorts">PassivePorts</a>
   <li><a href="#PathAllowFilter">PathAllowFilter</a>
   <li><a href="#PathDenyFilter">PathDenyFilter</a>
+  <li><a href="#PidFile">PidFile</a>
   <li><a href="#Port">Port</a>
   <li><a href="#ProcessTitles">ProcessTitles</a>
   <li><a href="#Protocols">Protocols</a>
+  <li><a href="#RegexOptions">RegexOptions</a>
   <li><a href="#ScoreboardFile">ScoreboardFile</a>
   <li><a href="#ScoreboardMutex">ScoreboardMutex</a>
+  <li><a href="#ScoreboardScrub">ScoreboardScrub</a>
+  <li><a href="#ServerAdmin">ServerAdmin</a>
+  <li><a href="#ServerAlias">ServerAlias</a>
   <li><a href="#ServerIdent">ServerIdent</a>
+  <li><a href="#ServerName">ServerName</a>
   <li><a href="#ServerType">ServerType</a>
+  <li><a href="#SetEnv">SetEnv</a>
   <li><a href="#SocketBindTight">SocketBindTight</a>
   <li><a href="#SocketOptions">SocketOptions</a>
   <li><a href="#SyslogFacility">SyslogFacility</a>
   <li><a href="#SyslogLevel">SyslogLevel</a>
   <li><a href="#TCPBacklog">TCPBacklog</a>
+  <li><a href="#TCPNoDelay">TCPNoDelay</a>
   <li><a href="#TimeoutIdle">TimeoutIdle</a>
   <li><a href="#TimeoutLinger">TimeoutLinger</a>
+  <li><a href="#TimesGMT">TimesGMT</a>
   <li><a href="#Trace">Trace</a>
   <li><a href="#TraceLog">TraceLog</a>
   <li><a href="#TraceOptions">TraceOptions</a>
   <li><a href="#TransferLog">TransferLog</a>
   <li><a href="#Umask">Umask</a>
+  <li><a href="#UnsetEnv">UnsetEnv</a>
+  <li><a href="#UseIPv6">UseIPv6</a>
+  <li><a href="#User">User</a>
+  <li><a href="#UseReverseDNS">UseReverseDNS</a>
   <li><a href="#UserOwner">UserOwner</a>
   <li><a href="#VirtualHost"><VirtualHost></a>
 </ul>
 
+<p>
+<hr>
+<h3><a name="Allow">Allow</a></h3>
+<strong>Syntax:</strong> Allow <em>[from] "all"|"none"|host|network|...]</em><br>
+<strong>Default:</strong> Allow from all<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0p16 and later
+
+<p>
+The <code>Allow</code> directive is used inside a <code><Limit></code>
+section to explicitly specify which hosts and/or networks have access to the
+commands or operations being limited.  <code>Allow</code> is typically used in
+conjunction with the <a href="#Order"><code>Order</code></a> and
+<a href="#Deny"><code>Deny</code></a> directives in order to create
+sophisticated access control rules.
+
+<p>
+<code>Allow</code> takes an optional first parameter: the keyword "from".
+Using "from" is purely cosmetic.  The remaining parameters are expected to be a
+list of hosts and/or networks which will be explicitly granted access.  The
+keyword "all" can be used to indicate that <b>all</b> hosts will explicitly be
+granted access; this "all" keyword is analogous to the
+<a href="#AllowAll"><code>AllowAll</code></a> directive, except with a lower
+priority.  In addition, the keyword "none" can be used to indicate that
+<b>no</b> hosts or networks will be explicitly granted access.  Note, though,
+that using "none" does <b>not</b> prevent the hosts/networks from being
+<i>implicitly</i> granted access.  If the "all" or "none" keywords are used,
+no other hosts or networks can be supplied.
+
+<p>
+Host and network addresses can be specified by name <i>or</i> by numeric
+address.  For security reasons, it is recommended that all address information
+be supplied using IP addresses.  Relying solely on DNS names causes access
+controls to depend heavily upon DNS servers which themselves may be vulnerable
+to attack or spoofing.  IP addresses which specify an entire network should
+end in a trailing period (<i>i.e.</i> "10.0.0." for the entire 10.0.0 subnet).
+DNS names which specify an entire network should begin with a leading period
+(<i>i.e.</i> ".proftpd.org" for the entire proftpd.org domain).
+
+<p>
+Examples:
+<pre>
+  <Limit LOGIN>
+    Order allow,deny
+    Allow from 128.44.26. 128.44.27. myhost.mydomain.edu .trusted-domain.org
+    Deny from all
+  </Limit>
+</pre>
+
+<p>
+See also: <a href="#Deny"><code>Deny</code></a>,
+<a href="#Limit"><code><Limit></code></a>,
+<a href="#Order"><code>Order</code></a><br>
+
+<p>
+<hr>
+<h3><a name="AllowAll">AllowAllow</a></h3>
+<strong>Syntax:</strong> AllowAll<br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Anonymous></code>, <code><Limit></code>, <code><Directory></code>, .ftpaccess<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>AllowAll</code> directive <i>explicitly</i> allows access to its
+parent <code><Anonymous></code>, <code><Limit></code>, or
+<code><Directory></code> configuration section. 
+
+<p>
+The default ProFTPD behavior is to <i>implicitly</i> allow access, which has a
+low priority.  The <code>AllowAll</code> directive creates an <em>explicit</em>
+allow rule, overriding any higher level <code>Deny</code> directives.
+
+<p>
+See also: <a href="#DenyAll"><code>DenyAll</code></a>
+
+<p>
 <hr>
-<h2><a name="AllowFilter">AllowFilter</a></h2>
+<h3><a name="AllowClass">AllowClass</a></h3>
+<strong>Syntax:</strong> AllowClass <em>["AND"|"OR"|"regex"] expression</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.10rc1 and later
+
+<p>
+The <code>AllowClass</code> directive specifies an <em>expression</em> of
+<a href="../howto/Classes.html">classes</a> that are permitted access within
+the parent <code><Limit></code> configuration section.  The
+<em>expression</em> parameter has a similar syntax as that used in
+<a href="#AllowGroup"><code>AllowGroup</code></a>, in that the parameter should
+contain a comma delimited list of class names (or "not" class names, by
+prefixing a class name with the <code>!</code> character) that are to be
+allowed access in that configuration section.
+
+<p>
+By default, the <em>expression</em> is parsed as a Boolean "OR" list, meaning
+that <b>any</b> elements of the <em>expression</em> must evaluate to logically
+true in order for the explicit allow rule to apply, <i>e.g.</i> "this name
+<b>or</b> that name <b>or</b> this other name...".  In order to treat the
+<em>expression</em> as a Boolean "AND" list, meaning that <b>all</b> of the
+elements must evaluate to logically true (<i>e.g.</i> "this name <b>and</b> not that name..."), use the optional <em>AND</em> keyword. Similarly, to treat the
+<em>expression</em> as a regular expression, use the <em>regex</em> keyword.
+
+<p>
+Examples:
+<pre>
+  # An OR-evaluated AllowClass directive
+  AllowClass OR known,good,trusted
+
+  # An AND-evaluated AllowClass directive
+  AllowClass AND good,!scanner
+
+  # A regular expression AllowClass directive
+  AllowClass regex ^known
+</pre>
+
+<p>
+See also: <a href="#AllowUser"><code>AllowUser</code></a>,
+<a href="#AllowGroup"><code>AllowGroup</code></a>,
+<a href="#DenyClass"><code>DenyClass</code></a>,
+<a href="#DenyGroup"><code>DenyGroup</code></a>,
+<a href="#DenyUser"><code>DenyUser</code></a>
+
+<p>
+<hr>
+<h3><a name="AllowFilter">AllowFilter</a></h3>
 <strong>Syntax:</strong> AllowFilter <em>pattern [flags]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>,
-<code><Directory></code>, .ftpaccess<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.0pre7 and later
 
 <p>
-The <code>AllowFilter</code> allows the configuration of a regular expression
-<em>pattern</em> that must be matched for all command arguments sent to
-ProFTPD.  It is extremely useful in controlling what characters may be sent in
-a command to ProFTPD, preventing some possible types of attacks against ProFTPD.
+The <code>AllowFilter</code> directive allows the configuration of a regular
+expression <em>pattern</em> that must be matched for all command arguments sent
+to ProFTPD.  It is extremely useful in controlling what characters may be sent
+in a command to ProFTPD, preventing some possible types of attacks against
+ProFTPD.
 
 <p>
 The regular expression <em>pattern</em> is applied against the arguments to the
@@ -85,8 +252,8 @@ returned to the client. If the <em>pattern</em> contains whitespace, it
 <b>must</b> be enclosed in quotes.
 
 <p>
-The optional <em>flags</em> parameter, if present, modifies how the
-given<em>pattern</em> will be evaludated.  The supported flags are:
+The optional <em>flags</em> parameter, if present, modifies how the given
+<em>pattern</em> will be evaludated.  The supported flags are:
 <ul>
   <li><b>nocase|NC</b> (<b>n</b>o <b>c</b>ase)<br>
       This makes the <em>pattern</em> case-insensitive, <i>i.e.</i> there is
@@ -111,10 +278,222 @@ See also: <a href="#DenyFilter"><code>DenyFilter</code></a>, <a href="#PathAllow
 
 <p>
 <hr>
-<h2><a name="AuthOrder">AuthOrder</a></h2>
+<h3><a name="AllowForeignAddress">AllowForeignAddress</a></h3>
+<strong>Syntax:</strong> AllowForeignAddress <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.1.7 and later
+
+<p>
+Normally, <code>proftpd</code> disallows clients from using the FTP
+<code>PORT</code> or <code>EPRT</code> command with anything other than their
+own IP address (<i>i.e.</i> the source IP address of the FTP control
+connection), as well as preventing the use of <code>PORT</code> or
+<code>EPRT</code> to specify a low-numbered (<i>i.e.</i> less than 1024) port
+number.  In either case, the client is sent an "Invalid port" response error
+and a message is logged indicating either "address mismatch" or "bounce attack".
+
+<p>
+By enabling the <code>AllowForeignAddress</code> directive, <code>proftpd</code>
+will allow clients to transmit foreign data connection addresses that do not
+match the client's IP address.  This allows such tricks as permitting a client
+to transfer a file between two FTP servers without involving itself in the
+actual data connection.  However, allowing this functionality is generally
+considered a bad idea, security-wise.  The <code>AllowForeignAddress</code>
+directive only affects FTP data connection addresses; not TCP ports.  There is
+no way (and no valid reason) to allow a client to use a low-numbered port in
+its <code>PORT</code> or <code>EPRT</code> command.
+
+<p>
+<hr>
+<h3><a name="AllowGroup">AllowGroup</a></h3>
+<strong>Syntax:</strong> AllowGroup <em>["AND"|"OR"|"regex"] expression</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.1.1 and later
+
+<p>
+The <code>AllowGroup</code> directive configures an <em>expression</em> that is
+specifically <i>permitted</i> within the context of the containing
+<code><Limit></code> section. The <em>expression</em> parameter should
+contain a comma separated list of group names, or "not" groups (by prefixing a
+group name with the <code>!</code> character), that are to be allowed access
+to the section.
+
+<p>
+By default, the <em>expression</em> is evaluated as a Boolean <em>AND</em> list,
+meaning that <b>all</b> elements of the expression must evaluate to logically
+true (<i>i.e.</i> "this group <b>and</b> this group <b>and</b> that group...")
+in order to the explicit allow rule to apply.  To evaluate the
+<em>expression</em> as a Boolean <em>OR</em> list, meaning that <b>any</b> of
+the elements must evaluate to logically true (<i>i.e.</i> "this group <b>or</b>
+this group <b>or</b> that group..."), use the optional <em>OR</em> keyword.
+Similarly, to evalulate the <em>expression</em> as a regular expression, use
+the <em>regex</em> keyword.
+
+<p>
+Examples:
+<pre>
+  <Limit LOGIN>
+    # Allow logins from users in the the www OR doc groups
+    AllowGroup OR www,doc
+
+    # Allow logins from users in the ftp group and not in the admin group
+    AllowGroup AND ftp,!admin
+
+    # Deny logins from any group starting with "sys"
+    DenyGroup regex ^sys
+  </Limit>
+</pre>
+
+<p>
+See also: <a href="#AllowUser"><code>AllowUser</code></a>,
+<a href="#DenyGroup"><code>DenyGroup</code></a>,
+<a href="#DenyUser"><code>DenyUser</code></a>
+
+<p>
+<hr>
+<h3><a name="AllowOverride">AllowOverride</a></h3>
+<strong>Syntax:</strong> AllowOverride <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.7rc1 and later
+
+<p>
+Normally, the <code>proftpd</code> server will look for, and parse, any files
+named <code>.ftpaccess</code> in the encountered directories.  These files
+provide functionality similar to Apache's <code>.htaccess</code> files --
+mini-configuration files.  This <code>AllowOverride</code> directive controls
+when/if these <code>.ftpaccess</code> files will be parsed.
+
+<p>
+<hr>
+<h3><a name="AllowUser">AllowUser</a></h3>
+<strong>Syntax:</strong> AllowUser <em>["AND"|"OR"|"regex"] expression</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.1.7 and later
+
+<p>
+The <code>AllowUser</code> directive configures an <em>expression</em> that is
+specifically <i>permitted</i> within the context of the containing
+<code><Limit></code> section. The <em>expression</em> parameter should
+contain a comma separated list of user names, or "not" users (by prefixing a
+user name with the <code>!</code> character), that are to be allowed access
+to the section.
+
+<p>
+Now, <b>unlike</b> <code>AllowGroup</code>, the <code>AllowUser</code>
+<em>expression</em> is evaluated as a Boolean <em>OR</em> list by default,
+meaning that <b>any</b> elements of the expression must evaluate to logically
+true (<i>i.e.</i> "this user <b>or</b> this user <b>or</b> that user...")
+in order to the explicit allow rule to apply.  To evaluate the
+<em>expression</em> as a Boolean <em>AND</em> list, meaning that <b>all</b> of
+the elements must evaluate to logically true (<i>i.e.</i> "this user <b>and</b>
+this user <b>and</b> that user..."), use the optional <em>AND</em> keyword.
+(Note that a single user <b>cannot</b> be "this user <b>and</b> that user" at
+the same time, thus the value of <em>AND</em> lists for users is debatable.)
+Similarly, to evalulate the <em>expression</em> as a regular expression, use
+the <em>regex</em> keyword.
+
+<p>
+Examples:
+<pre>
+  <Limit RETR>
+    # Allow these users to download
+    AllowUser OR alice,bob,chuck
+
+    # Or these users, based on our regex
+    AllowUser regex ^ftp_
+  </Limit>
+</pre>
+
+<p>
+See also: <a href="#AllowGroup"><code>AllowGroup</code></a>,
+<a href="#DenyGroup"><code>DenyGroup</code></a>,
+<a href="#DenyUser"><code>DenyUser</code></a>
+
+<p>
+<hr>
+<h3><a name="Anonymous"><Anonymous></a></h3>
+<strong>Syntax:</strong> <Anonymous <em>anon-directory</em>><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code><Anonymous></code> configuration section is used to create an
+anonymous FTP login, and is closed by a matching </Anonymous> directive.
+The <em>anon-directory</em> parameter specifies the directory to which the
+daemon, immediately after successful authentication, will restrict the session
+via <code>chroot(2)</code>.
+
+<p>
+Once the <code>chroot(2)</code> successfully completes, higher level
+directories are no longer accessible to that session process (and thus to the
+logged in user).  By default, ProFTPD <i>assumes</i> an anonymous login
+<i>if</i> the remote client attempts to authenticate as the currently running
+<code>User</code> for that server.  Unless the current user is "root",
+in which case anonymous logins are <b>not</b> allowed regardless of the
+presence of an <code><Anonymous></code> section.  To force anonymous
+logins to be bound to a user other than the current user, see the
+<a href="#User"><code>User</code></a> and
+<a href="#Group"><code>Group</code></a> directives.  In addition, if a
+<code>User</code> or <code>Group</code> directive <i>is</i> present in an
+<code><Anonymous></code> section, ProFTPD permanently switches to that
+UID/GID before the <code>chroot(2)</code>.
+
+<p>
+Normally, anonymous logins are <b>not</b> required to authenticate with a
+password, but <b>are</b> expected to enter a valid email address in place of a
+normal password; this email address is logged.  If this behavior is undesirable
+for a given <code><Anonymous></code> configuration section, it can be
+overridden via the
+<a href="mod_auth.html#AnonRequirePassword"><code>AnonRequirePassword</code></a>
+directive.
+
+<p>
+The following is an example of a typical anonymous FTP configuration:
+<pre>
+  <Anonymous /home/ftp>
+    # After anonymous login, daemon runs as user/group ftp.
+    User ftp
+    Group ftp
+
+    # The client login 'anonymous' is aliased to the "real" user 'ftp'.
+    UserAlias anonymous ftp
+
+    # Deny write operations to all directories, except for 'incoming' where
+    # STOR is allowed (but READ operations are prohibited).
+    <Directory *>
+      <Limit WRITE>
+        DenyAll
+      </Limit>
+    </Directory>
+
+    <Directory incoming>
+      <Limit READ>
+        DenyAll
+      </Limit>
+
+      <Limit STOR>
+        AllowAll
+      </Limit>
+    </Directory>
+  </Anonymous>
+</pre>
+
+<p>
+<hr>
+<h3><a name="AuthOrder">AuthOrder</a></h3>
 <strong>Syntax:</strong> AuthOrder <em>module-name1 ...</em><br>
 <strong>Default:</strong> mod_auth_file.c mod_auth_unix.c<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.8rc1 and later
 
@@ -160,10 +539,49 @@ to ensure that the login fails if the PAM check fails.
 
 <p>
 <hr>
-<h2><a name="DebugLevel">DebugLevel</a></h2>
+<h3><a name="Class"><Class></a></h3>
+<strong>Syntax:</strong> <Class <em>name</em>><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.10rc1 and later
+
+<p>
+The <code><Class></code> and <code></Class></code> encompass
+a section which defines and <em>names</em> a connection <em>class</em>.
+
+<p>
+Example:
+<pre>
+  <Class LAN>
+    From 192.168.0.0/16
+  </Class>
+</pre>
+These connection classes are covered in much greater detail in the
+<a href="../howto/Classes.html">Classes</a> howto.
+
+<p>
+<hr>
+<h3><a name="CommandBufferSize">CommandBufferSize</a></h3>
+<strong>Syntax:</strong> CommandBufferSize <em>size</em><br>
+<strong>Default:</strong> CommandBufferSize 512<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.0pre7 and later
+
+<p>
+The <code>CommandBufferSize</code> directive controls the maximum command
+<em>size</em> (in bytes) permitted to be sent to the server.  This allows you
+to effectively control the longest command the server may accept, and can help
+protect the server from various Denial of Service or resource-consumption
+attacks.
+
+<p>
+<hr>
+<h3><a name="DebugLevel">DebugLevel</a></h3>
 <strong>Syntax:</strong> DebugLevel <em>level</em><br>
 <strong>Default:</strong> DebugLevel 0<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.8rc1 and later
 
@@ -179,10 +597,10 @@ greater detail.
 
 <p>
 <hr>
-<h2><a name="DefaultAddress">DefaultAddress</a></h2>
+<h3><a name="DefaultAddress">DefaultAddress</a></h3>
 <strong>Syntax:</strong> DefaultAddress <em>ip-address|dns-name</em> <em>[ip-address|dns-name ...]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.7rc1 and later
 
@@ -227,28 +645,189 @@ See also: <a href="VirtualHost"><code><VirtualHost></code></a>
 
 <p>
 <hr>
-<h2><a name="DenyFilter">DenyFilter</a></h2>
+<h3><a name="DefaultServer">DefaultServer</a></h3>
+<strong>Syntax:</strong> DefaultServer <em>on|off</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>DefaultServer</code> directive controls which server configuration is
+used as the fallback when a matching vhost cannot be found for an incoming
+connection.
+
+<p>
+Normally, if the incoming connection is destined for an IP address which is
+neither the host's primary IP address nor one of the addresses specified in a
+<code><VirtualHost></code> configuration section, the "unknown"
+connection receives the following response:
+<pre>
+  500 Sorry, no server available to handle request on <i>a.b.c.d</i>
+</pre>
+and is disconnected.  When <code>DefaultServer</code> is enabled for either
+the primary server configuration <i>or</i> a virtual server, these "unknown"
+connections are handled by that configuration.
+
+<p>
+Only a single server configuration can be set as the <code>DefaultServer</code>.
+
+<p>
+<hr>
+<h3><a name="Define">Define</a></h3>
+<strong>Syntax:</strong> Define <em>label</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <em>any</em><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.6rc1 and later
+
+<p>
+The <code>Define</code> directive <em>defines</em> a label, and is used
+in conjunction with <a href="#IfDefine"><code><IfDefine></code></a> to
+provide <i>conditional</i> configuration sections.  This directive is the
+configuration file equivalent of the <code>-D</code> command-line option.
+
+<p>
+Example:
+<pre>
+  # Define ANONYMOUS (or comment this out), for anonymous login support
+  Define ANONYMOUS
+
+  # If the label ANONYMOUS is defined, use this <Anonymous> section
+  <IfDefine ANONYMOUS>
+    <Anonymous ~ftp>
+      ...
+    </Anonymous>
+  </IfDefine>
+</pre>
+
+<p>
+<hr>
+<h3><a name="Deny">Deny</a></h3>
+<strong>Syntax:</strong> Deny <em>[from] "all"|"none"|host|network...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0p16 and later
+
+<p>
+The <code>Deny</code> directive is used to create a list of hosts and/or
+networks which will explicitly be denied access to a given
+<code><Limit></code> section.  The keywords "all" and "none" can be used
+to indicate that <b>all</b> hosts are denied access, or that <b>no</b> hosts
+are explicitly denied, respectively.  For more information on the syntax and
+usage of the <code>Deny</code> directive, see the
+<a href="#Allow"><code>Allow</code></a> description.
+
+<p>
+Examples:
+<pre>
+  <Limit LOGIN>
+    Order deny,allow
+    Deny from 128.44.26.,128.44.27.,.evil-domain.com
+    Allow from all
+  </Limit>
+</pre>
+
+<p>
+See also: <a href="#Allow"><code>Allow</code></a>,
+<a href="#Limit"><code><Limit></code></a>,
+<a href="#Order"><code>Order</code></a>
+
+<p>
+<hr>
+<h3><a name="DenyAll">DenyAll</a></h3>
+<strong>Syntax:</strong> DenyAll<br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Anonymous></code>, <code><Limit></code>, <code><Directory></code>, .ftpaccess<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>DenyAll</code> directive <i>explicitly</i> denies access to its
+parent <code><Anonymous></code>, <code><Limit></code>, or
+<code><Directory></code> configuration section.
+
+<p>
+The default ProFTPD behavior is to <i>implicitly</i> allow access, which has a
+low priority.  The <code>DenyAll</code> directive creates an <em>explicit</em>
+deny rule, overriding any higher level <code>Allow</code> directives.
+
+<p>
+See also: <a href="#AllowAll"><code>AllowAll</code></a>
+
+<p>
+<hr>
+<h3><a name="DenyClass">DenyClass</a></h3>
+<strong>Syntax:</strong> DenyClass <em>["AND"|"OR"|"regex"] expression</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.10rc1 and later
+
+<p>
+The <code>DenyClass</code> directive specifies an <em>expression</em> of
+<a href="../howto/Classes.html">classes</a> that are <b>denied</b> access within
+the parent <code><Limit></code> configuration section.  The
+<em>expression</em> parameter has a similar syntax as that used in
+<a href="#AllowGroup"><code>AllowGroup</code></a>, in that the parameter should
+contain a comma delimited list of class names (or "not" class names, by
+prefixing a class name with the <code>!</code> character) that are to be
+denied access in that configuration section.
+
+<p>
+By default, the <em>expression</em> is parsed as a Boolean "OR" list, meaning
+that <b>any</b> elements of the <em>expression</em> must evaluate to logically
+true in order for the explicit deny rule to apply, <i>e.g.</i> "this name
+<b>or</b> that name <b>or</b> this other name...".  In order to treat the
+<em>expression</em> as a Boolean "AND" list, meaning that <b>all</b> of the
+elements must evaluate to logically true (<i>e.g.</i> "this name <b>and</b> not that name..."), use the optional <em>AND</em> keyword. Similarly, to treat the
+<em>expression</em> as a regular expression, use the <em>regex</em> keyword.
+
+<p>
+Examples:
+<pre>
+  # An OR-evaluated DenyClass directive
+  DenyClass OR bad,scanner,spammer
+
+  # An AND-evaluated DenyClass directive
+  DenyClass AND bad,!known
+
+  # A regular expression DenyClass directive
+  DenyClass regex ^spam
+</pre>
+
+<p>
+See also: <a href="#AllowUser"><code>AllowUser</code></a>,
+<a href="#AllowClass"><code>AllowClass</code></a>,
+<a href="#AllowGroup"><code>AllowGroup</code></a>,
+<a href="#DenyGroup"><code>DenyGroup</code></a>,
+<a href="#DenyUser"><code>DenyUser</code></a>
+
+<p>
+<hr>
+<h3><a name="DenyFilter">DenyFilter</a></h3>
 <strong>Syntax:</strong> DenyFilter <em>pattern [flags]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>,<code><Directory></code>, .ftpaccess<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>,<code><Directory></code>, .ftpaccess<br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.0pre7 and later
 
 <p>
-The <code>DenyFilter</code> directive, like the <code>AllowFilter</code>
-directive, specifies a regular expression <em>pattern</em> which must not
-match any of the command arguments.  If the <em>pattern</em> does match, a
-"Forbidden command" error is returned to the client.  This can be especially
-useful for forbidding certain command argument combinations from ever reaching
-ProFTPD.
+The <code>DenyFilter</code> directive, like the
+<a href="AllowFilter"><code>AllowFilter</code></a> directive, specifies a
+regular expression <em>pattern</em> which must not match any of the command
+arguments.  If the <em>pattern</em> does match, a "Forbidden command" error is
+returned to the client.  This can be especially useful for forbidding certain
+command parameter combinations from ever reaching ProFTPD.
 
 <p>
 Note that the <code>PASV</code> SFTP command <b>cannot</b> be blocked using
 this directive.
 
 <p>
-The optional <em>flags</em> parameter, if present, modifies how the
-given<em>pattern</em> will be evaludated.  The supported flags are:
+The optional <em>flags</em> parameter, if present, modifies how the given
+<em>pattern</em> will be evaludated.  The supported flags are:
 <ul>
   <li><b>nocase|NC</b> (<b>n</b>o <b>c</b>ase)<br>
       This makes the <em>pattern</em> case-insensitive, <i>i.e.</i> there is
@@ -269,13 +848,176 @@ The <a href="../howto/Filters.html">Filters</a> howto covers filtering in
 greater detail.
 
 <p>
-See also: <a href="#AllowFilter"><code>AllowFilter</code></a>, <a href="#PathAllowFilter"><code>PathAllowFilter</code></a>, <a href="#PathDenyFilter"><code>PathDenyFilter</code></a>
+See also: <a href="#AllowFilter"><code>AllowFilter</code></a>,
+<a href="#PathAllowFilter"><code>PathAllowFilter</code></a>,
+<a href="#PathDenyFilter"><code>PathDenyFilter</code></a>
 
+<p>
 <hr>
-<h2><a name="DisplayChdir">DisplayChdir</a></h2>
+<h3><a name="DenyGroup">DenyGroup</a></h3>
+<strong>Syntax:</strong> DenyGroup <em>["AND"|"OR"|"regex"] expression</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.1.1 and later
+
+<p>
+The <code>DenyGroup</code> directive configures an <em>expression</em> that is
+specifically <i>permitted</i> within the context of the containing
+<code><Limit></code> section. The <em>expression</em> parameter should
+contain a comma separated list of group names, or "not" groups (by prefixing a
+group name with the <code>!</code> character), that are to be denied access
+to the section.
+
+<p>
+By default, the <em>expression</em> is evaluated as a Boolean <em>AND</em> list,
+meaning that <b>all</b> elements of the expression must evaluate to logically
+true (<i>i.e.</i> "this group <b>and</b> this group <b>and</b> that group...")
+in order to the explicit deny rule to apply.  To evaluate the
+<em>expression</em> as a Boolean <em>OR</em> list, meaning that <b>any</b> of
+the elements must evaluate to logically true (<i>i.e.</i> "this group <b>or</b>
+this group <b>or</b> that group..."), use the optional <em>OR</em> keyword.
+Similarly, to evalulate the <em>expression</em> as a regular expression, use
+the <em>regex</em> keyword.
+
+<p>
+Examples:
+<pre>
+  <Limit LOGIN>
+    # Deny logins from users in the the www OR doc groups
+    DenyGroup OR www,doc
+
+    # Deny logins from users in the ftp group and not in the admin group
+    DenyGroup AND ftp,!admin
+
+    # Allow logins from any group starting with "sys"
+    AllowGroup regex ^sys
+  </Limit>
+</pre>
+
+<p>
+See also: <a href="#AllowGroup"><code>AllowGroup</code></a>,
+<a href="#AllowUser"><code>AllowUser</code></a>,
+<a href="#DenyUser"><code>DenyUser</code></a>
+
+<p>
+<hr>
+<h3><a name="DenyUser">DenyUser</a></h3>
+<strong>Syntax:</strong> DenyUser <em>["AND"|"OR"|"regex"] expression</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.1.7 and later
+
+<p>
+The <code>DenyUser</code> directive configures an <em>expression</em> that is
+specifically <i>denied</i> within the context of the containing
+<code><Limit></code> section. The <em>expression</em> parameter should
+contain a comma separated list of user names, or "not" users (by prefixing a
+user name with the <code>!</code> character), that are to be denied access
+to the section.
+
+<p>
+Now, <b>unlike</b> <code>AllowGroup</code>, the <code>DenyUser</code>
+<em>expression</em> is evaluated as a Boolean <em>OR</em> list by default,
+meaning that <b>any</b> elements of the expression must evaluate to logically
+true (<i>i.e.</i> "this user <b>or</b> this user <b>or</b> that user...")
+in order to the explicit deny rule to apply.  To evaluate the
+<em>expression</em> as a Boolean <em>AND</em> list, meaning that <b>all</b> of
+the elements must evaluate to logically true (<i>i.e.</i> "this user <b>and</b>
+this user <b>and</b> that user..."), use the optional <em>AND</em> keyword.
+(Note that a single user <b>cannot</b> be "this user <b>and</b> that user" at
+the same time, thus the value of <em>AND</em> lists for users is debatable.)
+Similarly, to evalulate the <em>expression</em> as a regular expression, use
+the <em>regex</em> keyword.
+
+<p>
+Examples:
+<pre>
+  <Limit RETR>
+    # Deny to these users downloading
+    DenyUser OR alice,bob,chuck
+
+    # Or these users, based on our regex
+    DenyUser regex ^ftp_
+  </Limit>
+</pre>
+
+<p>
+See also: <a href="#AllowGroup"><code>AllowGroup</code></a>,
+<a href="#AllowUser"><code>AllowUser</code></a>,
+<a href="#DenyGroup"><code>DenyGroup</code></a>
+
+<p>
+<hr>
+<h3><a name="Directory"><Directory></a></h3>
+<strong>Syntax:</strong> <Directory <em>path</em>><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code><Directory></code> section creates a set of configuration
+directives which applies only to the specified directory <i>and</i> its
+sub-directories.  The section is ended with a matching
+<code></Directory></code>.  Per-directory configuration is implemented
+with a "closest" match algorithm, meaning that the
+<code><Directory></code> section with the closest matching path to the
+actual path of the file/directory in question is used.  Per-directory
+configuration is inherited by all sub-directories until a closer matching
+<code><Directory></code> is found.
+
+<p>
+A trailing slash and wildcard ("<code>/*</code>") can be appended to
+the <em>path</em>, specifying that the configuration section applies <b>only</b>
+to the contents (and sub-contents), <b>not</b> to the actual directory itself.
+Such wildcard matches always take precedence over non-wildcard
+<code><Directory></code> configuration sections.
+<code><Directory></code> sections <b>cannot</b> be nested; they are
+automatically nested at runtime based on their <em>paths</em>.  <em>Paths</em>
+must always be absolute (except inside <code><Anonymous></code> sections),
+and should not reference symbolic links. Path inside of an
+<code><Anonymous></code> section <i>may</i> be relative, indicating that
+they are based on the <code><Anonymous></code> root directory.
+
+<p>
+As of <code>proftpd-1.1.3</code> and later, <code><Directory></code> paths
+that begin with the special character <code>~</code>, and which do not specify
+a username immediately after the <code>~</code> character, are put into a
+special <em>deferred</em> mode.  When <em>deferred</em> mode, the
+<code><Directory></code> section is not merged into the overall server
+configuration at startup time, but instead the merge is <em>deferred</em> until
+the client has authentication, at which time the <code>~</code> character is
+replaced with that authenticated user's home directory.  This allows for a
+<code><Directory></code> section which applies to <i>all</i> users' home
+directories. This feature is not supported within an
+<code><Anonymous></code> section, however.
+
+<p>
+Some examples:
+<pre>
+  <Directory /users/robroy/private>
+    HideNoAccess on
+  </Directory>
+
+  <Directory ~/anonftp>
+    <Limit WRITE>
+      DenyAll
+    </Limit>
+  </Directory>
+</pre>
+
+<p>
+More information on using <code><Directory></code> sections, including
+examples, can be found in the
+<a href="../howto/Directory.html"><code><Directory></code> howto</a>.
+
+<hr>
+<h3><a name="DisplayChdir">DisplayChdir</a></h3>
 <strong>Syntax:</strong> DisplayChdir <em>filename ["on"|"off"]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.3.1rc1 and later
 
@@ -304,10 +1046,10 @@ more information on the variables that can be used in a
 See also: <a href="#DisplayConnect"><code>DisplayConnect</code></a>, <a href="#DisplayQuit"><code>DisplayQuit</code></a>
 
 <hr>
-<h2><a name="DisplayConnect">DisplayConnect</a></h2>
+<h3><a name="DisplayConnect">DisplayConnect</a></h3>
 <strong>Syntax:</strong> DisplayConnect <em>filename</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.0pre2 and later
 
@@ -331,10 +1073,10 @@ See also: <a href="#DisplayChdir"><code>DisplayChdir</code></a>,
 <a href="#DisplayQuit"><code>DisplayQuit</code></a>
 
 <hr>
-<h2><a name="DisplayQuit">DisplayQuit</a></h2>
+<h3><a name="DisplayQuit">DisplayQuit</a></h3>
 <strong>Syntax:</strong> DisplayQuit <em>filename</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.0pre8 and later
 
@@ -362,7 +1104,104 @@ See also: <a href="#DisplayChdir"><code>DisplayChdir</code></a>,
 <a href="#DisplayConnect"><code>DisplayConnect</code></a>
 
 <hr>
-<h2><a name="GroupOwner">GroupOwner</a></h2>
+<h3><a name="FSCachePolicy">FSCachePolicy</a></h3>
+<strong>Syntax:</strong> FSCachePolicy <em>on|off|size count [maxAge secs]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>FSCachePolicy</code> directive configures the internal
+filesystem-related cache, used for performance optimizations on <i>e.g.</i>
+network filesystems.  This directive can be used to disable this internal
+cache, or to tune the caching policy.
+
+<p>
+To disable the cache altogether, use:
+<pre>
+  FSCachePolicy off
+</pre>
+
+<p>
+To configure the maximum number of entries in the cache before eviction happens:
+<pre>
+  FSCachePolicy size 64
+</pre>
+
+<p>
+To configure the maximum age (in seconds) of a cached entry before it is
+evicted:
+<pre>
+  FSCachePolicy maxAge 60
+</pre>
+
+<p>
+The <em>size</em> and <em>maxAge</em> parameters can be combined/set in the
+same directive, <i>e.g.</i>:
+<pre>
+  # Set the maximum cache size at 128, and the max age at 120 seconds
+  FSCachePolicy size 128 maxAge 120
+</pre>
+
+<hr>
+<h3><a name="FSOptions">FSOptions</a></h3>
+<strong>Syntax:</strong> FSOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.6rc3 and later
+
+<p>
+The <code>FSOptions</code> directive configures various optional behavior of
+ProFTPD's filesystem API.  The currently supported options are:
+<ul>
+  <li><code>IgnoreExtendedAttributes</code>
+    <p>
+    When the <code>--enable-xattr</code> configure option is enabled, ProFTPD
+    will support <a href="https://en.wikipedia.org/wiki/Extended_file_attributes"><em>extended attributes</em></a> where possible.  However, this might cause
+    issues with some clients (<i>e.g.</i> some SFTP clients) that do not
+    properly support them.  Use this option to disable ProFTPD's support for
+    extended attributes.
+  </li>
+</ul>
+
+<hr>
+<h3><a name="Global">Global</a></h3>
+<strong>Syntax:</strong> <Global><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.16 and later
+
+<p>
+The <code><Global></code> section is used to create a set of
+configuration directives; this set is then applied universally to both the main
+server configuration <i>and</i> <b>all</b> <code><VirtualHost></code>
+sections.  Most, but not all, other directives can be used inside of a
+<code><Global></code> section.
+
+<p>
+In addition, multiple <code><Global></code> sections can be used in the
+configuration file.  At startup, all <code><Global></code> sections are
+combined, and then merged into each server's configuration.
+<code><Global></code> sections are closed by a matching
+<code></Global></code> directive.
+
+<hr>
+<h3><a name="Group">Group</a></h3>
+<strong>Syntax:</strong> Group <em>name</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>Group</code> directive configures which GID ProFTPD will use when
+running.  See the <a href="#User"><code>User</code></a> directive for details.
+
+<hr>
+<h3><a name="GroupOwner">GroupOwner</a></h3>
 <strong>Syntax:</strong> GroupOwner <em>group-name|"~"</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
@@ -399,8 +1238,53 @@ of the newly created file to be that of the logged-in user, use:
 <p>
 See also: <a href="#UserOwner"><code>UserOwner</code></a>
 
+<p>
+<hr>
+<h3><a name="HideFiles">HideFiles</a></h3>
+<strong>Syntax:</strong> HideFiles <em>[!]regex|"none"]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <code><Directory></code>, .ftpaccess<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.7rc1 and later
+
+<p>
+The <code>HideFiles</code> directive configures a <code><Directory></code>
+section to hide all directory entries, <i>e.g.</i> its files and
+sub-directories, that match the given <em>regex</em>.  These files can still
+be operated on by other FTP commands (<code>DELE</code>, <code>RETR</code>,
+<i>etc</i>), as constrained by any applicable <code><Limit></code>
+sections; this can be modified using the <code>IgnoreHidden</code> directive.
+
+<p>
+Since <code><Directory></code> configurations are inherited by
+sub-directories, the <em>none</em> keyword can be used to disable any
+inherited file hiding within a sub-directory.  This usually occurs through the
+use of a <code>.ftpaccess</code> file.
+
+<p>
+Examples:
+<pre>
+  <Directory />
+    # Hide configuration and passwd files from view
+    HideFiles "(\\.conf|passwd)$"
+
+    # ...or the same regex, without the quotes
+    HideFiles (\.conf|passwd)$
+
+    # Using the ! prefix to "invert" the regular expression matching,
+    # allow <b>only</b> .txt and .html files to be seen
+    HideFiles !(\.txt|\.html)$
+  </Directory>
+</pre>
+
+<p>
+See also: <a href="#HideGroup"><code>HideGroup</code></a>,
+<a href="#HideNoAccess"><code>HideNoAccess</code></a>,
+<a href="#HideUser"><code>HideUser</code></a>
+
+<p>
 <hr>
-<h2><a name="HideGroup">HideGroup</a></h2>
+<h3><a name="HideGroup">HideGroup</a></h3>
 <strong>Syntax:</strong> HideGroup <em>group-name</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <code><Anonymous></code>, <code><Directory></code><br>
@@ -442,7 +1326,7 @@ Examples:
 See also: <a href="#HideUser"><code>HideUser</code></a>, <a href="#HideNoAccess"><code>HideNoAccess</code></a>, <a href="#IgnoreHidden"><code>IgnoreHidden</code></a>
 
 <hr>
-<h2><a name="HideNoAccess">HideNoAccess</a></h2>
+<h3><a name="HideNoAccess">HideNoAccess</a></h3>
 <strong>Syntax:</strong> HideNoAccess <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <code><Anonymous></code>, <code><Directory></code><br>
@@ -466,7 +1350,7 @@ all FTP commands by applying <code>IgnoreHidden</code> in conjunction with
 See also: <a href="#HideGroup"><code>HideGroup</code></a>, <a href="#HideUser"><code>HideUser</code></a>, <a href="#IgnoreHidden"><code>IgnoreHidden</code></a>
 
 <hr>
-<h2><a name="HideUser">HideUser</a></h2>
+<h3><a name="HideUser">HideUser</a></h3>
 <strong>Syntax:</strong> HideUser <em>user-name</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <code><Anonymous></code>, <code><Directory></code><br>
@@ -506,11 +1390,171 @@ Examples:
 <p>
 See also: <a href="#HideGroup"><code>HideGroup</code></a>, <a href="#HideNoAccess"><code>HideNoAccess</code></a>, <a href="#IgnoreHidden"><code>IgnoreHidden</code></a>
 
+<p>
+<hr>
+<h3><a name="IgnoreHidden">IgnoreHidden</a></h3>
+<strong>Syntax:</strong> IgnoreHidden <em>on|off</em><br>
+<strong>Default:</strong> IgnoreHidden off<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>IgnoreHidden</code> directive tells ProFTPD to <em>ignore</em>
+files/directories that are hidden by other directives, such as
+<code>HideFiles</code>, <code>HideUser</code>, and <code>HideGroup</code>.
+
+<p>
+Normally, files marked as "hidden" by <code>HideFiles</code>,
+<code>HideUser</code> or <code>HideGroup</code> <i>can</i> be operated on by
+all FTP commands (assuming Unix file permissions allow access); these files
+simplly do not appear in directory listings.  Additionally, even when normal
+file system permissions <i>deny</i> access, ProFTPD returns a
+"Permission denied" error to the client, indicating that the requested
+file/directory <i>does</i> exist, even if the client cannot use it.  The
+<code>IgnoreHidden</code> directive configures a <code><Limit></code>
+section so as to completely <em>ignore</em> any hidden directory entries for
+the set of FTP commands encompassed by the <code><Limit></code>.  This
+has the effect of returning an error similar to "No such file or directory"
+when the client attempts to use the command upon a hidden directory or file.
+
+<p>
+Example:
+<pre>
+  <Directory />
+    # Hide files/directories owned by root
+    HideUser root
+
+    <Limit DIRS>
+      # Return "No such file or directory" for hidden files/directories
+      IgnoreHidden on
+    </Limit>
+  </Directory>
+</pre>
+
+<p>
+See also: <a href="#HideFiles"><code>HideFiles</code></a>,
+<a href="#HideGroup"><code>HideGroup</code></a>,
+<a href="#HideUser"><code>HideUser</code></a>
+
+<p>
+<hr>
+<h3><a name="IfDefine"><IfDefine></a></h3>
+<strong>Syntax:</strong> <IfDefine <em>[!]label</em>><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <em>any</em><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.6rc1 and later
+
+<p>
+The <code><IfDefine></code> and <code></IfDefine></code> define
+a <i>conditional</i> configuration section.  The directives appearing within
+that section are processed only if the <em>label</em> expression, used by
+<code><IfDefine></code>, is true/exists.  Otherwise, everything within
+the configuration section is skipped.
+
+<p>
+For example, assume you had something like the following in your
+<code>proftpd.conf</code>:
+<pre>
+  <IfDefine USE_TLS>
+    TLSEngine on
+    TLSRequired on
+    ...
+  </IfDefine>
+</pre>
+Then you can enable that TLS functionality using the <code>-D</code>
+command-line option when starting ProFTPD, <i>e.g.</i>:
+<pre>
+  $ /usr/local/sbin/proftpd -DUSE_TLS ...
+</pre>
+
+<p>
+For configuration for which there are multiple conditions, you would use
+multiple nested <code><IfDefine></code> sections, <i>e.g.</i>:
+<pre>
+  <IfDefine USE_TLS>
+    TLSEngine on
+
+    <IfDefine !REQUIRE_TLS>
+      # Require TLS for authentication, but allow clients to downgrade back
+      # to plain TCP after that.
+      TLSRequired auth
+    </IfDefine>
+
+    <IfDefine REQUIRE_TLS>
+      # Require TLS for control and data connections
+      TLSRequired on
+    </IfDefine>
+  </IfDefine>
+</pre>
+
+<p>
+See also: <a href="#Define"><code>Define</code></a>
+
+<p>
+<hr>
+<h3><a name="IfModule"><IfModule></a></h3>
+<strong>Syntax:</strong> <IfModule <em>[!]module-name</em>><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> <em>any</em><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.6rc1 and later
+
+<p>
+The <code><IfModule></code> and <code></IfModule></code> define
+a <i>conditional</i> configuration section.  The directives appearing within
+that section are processed only if the <em>module-name</em> , used by
+<code><IfModule></code>, is present/loaded.  Otherwise, everything within
+the configuration section is skipped.
+
+<p>
+Example:
+<pre>
+  <IfModule mod_load.c>
+    MaxLoad 10 "Acces denied, server too busy"
+  </IfModule>
+</pre>
+
+<p>
+For configuration for which there are multiple modules required, you would use
+multiple nested <code><IfModule></code> sections, <i>e.g.</i>:
+<pre>
+  <IfModule mod_sql.c>
+    SQLEngine on
+
+    <IfModule mod_sql_mysql.c>
+      # Use an SQLConnectInfo using MySQL parameters
+    </IfModule>
+
+    <IfModule !mod_sql_mysql.c>
+      <IfModule mod_sql_sqlite.c>
+        # No mod_sql_mysql, but we do have mod_sql_sqlite available;
+        # use an SQLConnectInfo to a local SQLite database file.
+      </IfModule>
+    </IfModule>
+
+    <IfModule mod_sql_passwd.c>
+      # Try more different password hashes with mod_sql_passwd
+      SQLAuthTypes ...
+    </IfModule>
+
+    <IfModule !mod_sql_passwd.c>
+      # Use only the basic SQLAuthTypes provided by mod_sql
+      SQLAuthTypes Crypt OpenSSL
+    </IfModule>
+  </IfModule>
+</pre>
+
+<p>
+See also: <a href="#Define"><code>Define</code></a>
+
+<p>
 <hr>
-<h2><a name="Include">Include</a></h2>
+<h3><a name="Include">Include</a></h3>
 <strong>Syntax:</strong> Include <em>path|pattern</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Limit></code>, <code><Directory></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.10rc1 and later
 
@@ -529,20 +1573,101 @@ accidentally leave temporary files in a directory that can cause
 <code>proftpd</code> to fail.
 
 <p>
-The <em>path</em> must be an absolute path.
+The <em>path</em> must be an absolute path.
+
+<p>
+Examples:
+<pre>
+  Include /etc/proftpd/conf/tls.conf
+  Include /etc/proftpd/conf/vhosts/*.conf
+</pre>
+
+<p>
+<b>Note</b> that an <code>Include</code> directive appearing inside of a
+<code><Limit></code> section which itself is in a <code>.ftpaccess</code>
+file will be ignored.  <code>Include</code> directives are not allowed
+in <code>.ftpaccess</code> files, even indirectly.
+
+<p>
+<hr>
+<h3><a name="IncludeOptions">IncludeOptions</a></h3>
+<strong>Syntax:</strong> IncludeOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.6rc3 and later
+
+<p>
+The <code>IncludeOptions</code> directive is used to configure various optional
+behavior of <a href="#Include"><code>Include</code></a> directive.  For
+example:
+<pre>
+  IncludeOptions IgnoreTempFiles
+</pre>
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>AllowSymlinks</code><br>
+    <p>
+    When the <code>Include</code> directive encounters symlinks, it will
+    <i>skip</i> them by default; use this option to handle symlinks.
+  </li>
+
+  <p>
+  <li><code>IgnoreTempFiles</code><br>
+    <p>
+    Use this option to have the <code>Include</code> directive automatically
+    <i>skip</i> any files which have extensions identifying them as commonly
+    occurring temporary files.
+  </li>
+
+  <p>
+  <li><code>IgnoreWildcards</code><br>
+    <p>
+    Use this option to have the <code>Include</code> directive automatically
+    <i>reject</i> any paths which include wildcards in the directory names.
+    This can be done, for example, to prevent other <code>Include</code>d files
+    from using wildcards without the administrator's consent.
+  </li>
+</ul>
+
+<p>
+<hr>
+<h3><a name="Limit"><Limit></a></h3>
+<strong>Syntax:</strong> <Limit <em>cmd-list</em>><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code><Limit></code> section is used to place access restrictions on
+one or more FTP commands, within a given configuration section.  Limits flow
+downward, so that a <code><Limit></code> section in the "server
+config" context applies to all <code><Directory></code> and
+<code><Anonymous></code> sections that also reside in that configuration.
+Any number of command parameters can be specified in the <em>cmd-list</em>,
+against which the contents of the <code><Limit></code> section will be
+applied.
 
-Examples:
-<pre>
-  Include /etc/proftpd/conf/tls.conf
-  Include /etc/proftpd/conf/vhosts/*.conf
-</pre>
+<p>
+<code><Limit></code> command restrictions should <b>not</b> be confused
+with file/directory access permission.  While limits can be used to
+<i>restrict</i> a command in a certain directory, they cannot be used to
+<i>override</i> the file permissions; limits <b>cannot</b> <i>grant</i> access
+if the underlying filesystem restricts access.
+
+<p>
+More information on using <code><Limit></code> sections, including
+examples, can be found in the <a href="../howto/Limit.html"><code><Limit></code> howto</a>.
 
 <p>
 <hr>
-<h2><a name="MasqueradeAddress">MasqueradeAddress</a></h2>
+<h3><a name="MasqueradeAddress">MasqueradeAddress</a></h3>
 <strong>Syntax:</strong> MasqueradeAddress <em>ip-address|dns-name|device-name</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> server config, <code><VirtualHost></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.2 and later
 
@@ -571,7 +1696,7 @@ addresses.
 
 <p>
 <hr>
-<h2><a name="MaxCommandRate">MaxCommandRate</a></h2>
+<h3><a name="MaxCommandRate">MaxCommandRate</a></h3>
 <strong>Syntax:</strong> MaxCommandRate <em>count</em> <em>[secs]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -598,7 +1723,7 @@ sets a maximum command rate of 500 commands every 2 seconds.
 
 <p>
 <hr>
-<h2><a name="MaxConnectionRate">MaxConnectionRate</a></h2>
+<h3><a name="MaxConnectionRate">MaxConnectionRate</a></h3>
 <strong>Syntax:</strong> MaxConnectionRate <em>count</em> <em>[interval]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -625,7 +1750,7 @@ sets a maximum connection rate of 500 connections every 2 seconds.
 
 <p>
 <hr>
-<h2><a name="MaxInstances">MaxInstances</a></h2>
+<h3><a name="MaxInstances">MaxInstances</a></h3>
 <strong>Syntax:</strong> MaxInstances <em>count</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -642,17 +1767,126 @@ has no effect when <code>proftpd</code> is configured with "ServerType inetd".
 Each <code>proftpd</code> child process represents a single client connection,
 and thus this directive also controls the maximum number of simultaneous
 connections allowed.  Additional connections beyond the configured limit are
-logged, and silently disconnected.  The <code>MaxInstances</code> directive
-can be used to prevent undesirable denial-of-service attacks (<i>e.g.</i>
-by repeatedly connecting to the FTP control port, a malicious client could try
-to cause <code>proftpd</code> to repeatedly fork new processes, creating a
-"fork-bomb").   By default, no limit is placed on the number of child
-processes that may run at one time; it is <b>highly recommended</b> that a
-maximum number, suitable to your sites traffic, be configured.
+logged, and <b>silently disconnected</b>; the clients will <b>not</b> receive an
+FTP response in this case, but instead will encounter connection-level errors
+such as "Connection reset by peer".  In order to provide a more user-facing
+error message, use the
+<a href="mod_auth.html#MaxClients"><code>MaxClients</code></a> directive,
+set to a value <em>lower</em> than <code>MaxInstances</code>, <i>e.g.</i>:
+<pre>
+  # Set MaxClients lower than MaxInstances, so that clients receive a nicer error message when they are rejected.
+  MaxClients 100
+  MaxInstances 101
+</pre>
+
+<p>
+The <code>MaxInstances</code> directive can be used to prevent undesirable
+denial-of-service attacks (<i>e.g.</i> by repeatedly connecting to the FTP
+control port, a malicious client could try to cause <code>proftpd</code> to
+repeatedly fork new processes, creating a "fork-bomb").  By default, no limit
+is placed on the number of child processes that may run at one time; it is
+<b>highly recommended</b> that a maximum number, suitable to your sites
+traffic, be configured.
+
+<p>
+<hr>
+<h3><a name="MultilineRFC2228">MultilineRFC2228</a></h3>
+<strong>Syntax:</strong> MultilineRFC2228 <em>on|off</em><br>
+<strong>Default:</strong> MultilineRFC2228 off<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.0pre3 and later
+
+<p>
+By default, ProFTPD sends multi-line responses as per
+<a href="https://tools.ietf.org/html/rfc959">RFC 959</a>, <i>i.e.</i>:
+<pre>
+  200-First line
+   More lines...
+  200 Last line
+</pre>
+
+<p>
+<a href="https://tools.ietf.org/html/rfc2228">RFC 2228</a> specifies that "6xy"
+responses will be sent as follows:
+<pre>
+  600-First line
+  600-More lines...
+  600 Last line
+</pre>
+Note that RFC 2228 <b>only</b> specifies this format for response codes
+starting with '6'.
+
+<p>
+Enabling the <code>MultilineRFC2228</code> directive causes <b>all</b>
+multiline responses to be sent in this format, which <i>may</i> be more
+compatible with certain web browsers and clients.  Using this format
+multiline responses is more likely to be compatible with all clients, although
+it is not strictly RFC compliant, and is thus not enabled by default.
+
+<p>
+<hr>
+<h3><a name="Order">Order</a></h3>
+<strong>Syntax:</strong> Order <em>"allow,deny"|"deny,allow"</em><br>
+<strong>Default:</strong> Order allow,deny<br>
+<strong>Context:</strong> <code><Limit></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0p16 and later
+
+<p>
+The <code>Order</code> directive configures the order in which
+<a href="#Allow"><code>Allow</code></a> and
+<a href="#Deny"><code>Deny</code></a> directives are checked inside of a
+<code><Limit></code> configuration section.
+
+<p>
+<code>Allow</code> directives are permissive, and <code>Deny</code> directives
+restrictive, thus the <em>order</em> in which they are examined can
+significantly alter the way access control functions.  If the default setting
+of <em>allow,deny</em> is used, then "allowed" access permissions are checked
+<em>first</em>.  If an <code>Allow</code> directive explicitly allows access to
+the <code><Limit></code> section, access is granted, and any
+<code>Deny</code> directives are never checked.  However, if <code>Allow</code>
+directives do not explicitly permit access, <code>Deny</code> directives are
+checked.  And if any <code>Deny</code> directive applies, access is explicitly
+denied.  Otherwise, access is granted.
+
+<p>
+When <em>deny,allow</em> is used, <code>Deny</code> directive access
+restrictions are checked first.  If any restriction applies, access is denied
+immediately.  If nothing is denied, then <code>Allow</code> permissions are
+checked.  If an <code>Allow</code> directive explicitly permits access, access
+is permitted; otherwise access is implicitly denied.
+
+<p>
+For clarification, the following illustrates the steps used when checking
+<code>Allow</code>/<code>Deny</code> access:
+<ul>
+  <li><em>Order allow,deny</em><br>
+    <ul>
+      <li>Check <code>Allow</code> directives; if one or more apply, <b>allow</b>
+      <li>Check <code>Deny</code> directives; if one or more apply, <b>deny</b>
+      <li>Otherwise, <b>allow</b>
+    </ul>
+  </li>
+
+  <p>
+  <li><em>Order deny,allow</em><br>
+    <ul>
+      <li>Check <code>Deny</code> directives; if one or more apply, <b>deny</b>
+      <li>Check <code>Allow</code> directives; if one or more apply, <b>allow</b>
+      <li>Otherwise, <b>deny</b>
+    </ul>
+  </li>
+</ul>
+
+<p>
+See also: <a href="#Allow"><code>Allow</code></a>,
+<a href="#Deny"><code>Deny</code></a>, <a href="#Limit"><code><Limit></code></a><br>
 
 <p>
 <hr>
-<h2><a name="PassivePorts">PassivePorts</a></h2>
+<h3><a name="PassivePorts">PassivePorts</a></h3>
 <strong>Syntax:</strong> PassivePorts <em>min max</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -697,7 +1931,7 @@ an administrator is: why do you think you need such a small
 
 <p>
 <hr>
-<h2><a name="PathAllowFilter">PathAllowFilter</a></h2>
+<h3><a name="PathAllowFilter">PathAllowFilter</a></h3>
 <strong>Syntax:</strong> PathAllowFilter <em>pattern [flags]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
@@ -743,7 +1977,7 @@ See also: <a href="#PathDenyFilter"><code>PathDenyFilter</code></a>
 
 <p>
 <hr>
-<h2><a name="PathDenyFilter">PathDenyFilter</a></h2>
+<h3><a name="PathDenyFilter">PathDenyFilter</a></h3>
 <strong>Syntax:</strong> PathDenyFilter <em>pattern [flags]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
@@ -784,7 +2018,29 @@ See also: <a href="#PathAllowFilter"><code>PathAllowFilter</code></a>
 
 <p>
 <hr>
-<h2><a name="Port">Port</a></h2>
+<h3><a name="PidFile">PidFile</a></h3>
+<strong>Syntax:</strong> PidFile <em>path</em><br>
+<strong>Default:</strong> PidFile <em>$prefix</em>/var/proftpd.pid<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.0rc2 and later
+
+<p>
+The <code>PidFile</code> directive configures the <em>path</em> to which the
+daemon process records its process ID (PID).  The <em>path</em> must be an
+absolute path, <i>e.g.</i> <code>/var/run/proftpd/proftpd.pid</code>.  The
+<code>PidFile</code> is only used in <a href="#ServerType">standalone</a> mode.
+
+<p>
+It is often useful to be able to send the daemon a signal, so that it closes
+and then reopens its log files (<i>e.g.</i> ExtendedLog, TransferLog), and
+re-reads its configuration files.  This is done by sending the
+<code>SIGHUP</code> signal to the PID contained in the <code>PidFile</code> --
+the PID of the daemon process.
+
+<p>
+<hr>
+<h3><a name="Port">Port</a></h3>
 <strong>Syntax:</strong> Port <em>number</em><br>
 <strong>Default:</strong> Port 21<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code><br>
@@ -816,7 +2072,7 @@ disable/turn off that server:
 
 <p>
 <hr>
-<h2><a name="ProcessTitles">ProcessTitles</a></h2>
+<h3><a name="ProcessTitles">ProcessTitles</a></h3>
 <strong>Syntax:</strong> ProcessTitles <em>terse|verbose</em><br>
 <strong>Default:</strong> ProcessTitles verbose<br>
 <strong>Context:</strong> server config<br>
@@ -859,7 +2115,7 @@ which results in process titles which look like:
 
 <p>
 <hr>
-<h2><a name="Protocols">Protocols</a></h2>
+<h3><a name="Protocols">Protocols</a></h3>
 <strong>Syntax:</strong> Protocols <em>protocol1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -892,7 +2148,35 @@ The currently known/supported protocols include:
 
 <p>
 <hr>
-<h2><a name="ScoreboardFile">ScoreboardFile</a></h2>
+<h3><a name="RegexOptions">RegexOptions</a></h3>
+<strong>Syntax:</strong> RegexOptions <em>[MatchLimit limit] [MatchRecursionLimit limit]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.5rc1 and later
+
+<p>
+The <code>RegexOptions</code> directive configures <em>limits</em> that can
+be set on the handling of regular expressions.  ProFTPD can use regular
+expressions for many things; some malicious clients may attempt <i>resource
+consumption</i> attacks by forcing these regular expressions into very
+memory/CPU-intensive matching.  The <code>RegexOptions</code> directive can
+be used in such cases to enforce lower limits on the regular expression
+handling.
+
+<p>
+The <a href="http://pcre.org/current/doc/html/pcre2api.html"><code>pcreapi</code></a> documentation talks more about what the <em>match limit</em> and <em>match
+recursion limit</em> values do.
+
+<p>
+Note, however, that these limits are only used when
+<a href="http://pcre.org/">PCRE</a> support is enabled (via the
+<code>--enable-pcre</code> built-time option).  If PCRE support is <em>not</em>
+enabled, this directive has no effect.
+
+<p>
+<hr>
+<h3><a name="ScoreboardFile">ScoreboardFile</a></h3>
 <strong>Syntax:</strong> ScoreboardFile <em>path|"none"</em><br>
 <strong>Default:</strong> ScoreboardFile /usr/local/var/proftpd.scoreboard</br>
 <strong>Context:</strong> server config<br>
@@ -928,7 +2212,7 @@ before disabling scoreboarding.
 
 <p>
 <hr>
-<h2><a name="ScoreboardMutex">ScoreboardMutex</a></h2>
+<h3><a name="ScoreboardMutex">ScoreboardMutex</a></h3>
 <strong>Syntax:</strong> ScoreboardMutex <em>path</em><br>
 <strong>Default:</strong> ScoreboardMutex /usr/local/var/proftpd.scoreboard.lck</br>
 <strong>Context:</strong> server config<br>
@@ -949,9 +2233,110 @@ the <code>ScoreboardMutex</code> be located in the same directory as the
 
 <p>
 <hr>
-<h2><a name="ServerIdent">ServerIdent</a></h2>
+<h3><a name="ScoreboardScrub">ScoreboardScrub</a></h3>
+<strong>Syntax:</strong> ScoreboardScrub <em>on|off|secs</em><br>
+<strong>Default:</strong> ScoreboardScrub on<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.3rc1 and later
+
+<p>
+The <code>ScoreboardScrub</code> directive configures the "scrubbing" of the
+<a href="../howto/Scoreboard.html"><code>ScoreboardFile</code></a>.  Scrubbing
+can be turned <em>off</em> entirely (not recommended), left <em>on</em>,
+or configured to run at a custom interval (in seconds).
+
+<p>
+Example:
+<pre>
+  # Disable scoreboard scrubbing
+  ScoreboardScrub off
+
+  # Scrub the scoreboard every 2 minutes
+  ScoreboardScrub 120
+</pre>
+
+<p>
+<hr>
+<h3><a name="ServerAdmin">ServerAdmin</a></h3>
+<strong>Syntax:</strong> ServerAdmin <em>email-address</em><br>
+<strong>Default:</strong> ServerAdmin root at host<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>ServerAdmin</code> directive sets the <em>email address</em> of the
+administrator of the host.
+
+<p>
+Example:
+<pre>
+  ServerAdmin ftp at example.com
+</pre>
+
+<p>
+<hr>
+<h3><a name="ServerAlias">ServerAlias</a></h3>
+<strong>Syntax:</strong> ServerAlias <em>hostname ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>ServerAlias</code> directive is used to configure a <em>hostname</em>
+for the virtual server, such than an FTP client can connect to that virtual
+server using the <code>HOST</code> command.  In effect, you use
+<code>ServerAlias</code> to define the names that you want to support, for
+true name-based virtual hosting.
+
+<p>
+For example, you could define a virtual host using an IP address, and
+explicitly add the <code>HOST</code> names which should be "hosted" (handled)
+by that virtual host configuration, like so:
+<pre>
+  <VirtualHost 1.2.3.4>
+    Port 21
+    ServerAlias *.domain.com
+    ServerAlias example.com
+    ...
+  </VirtualHost>
+</pre>
+So an FTP client which connected to 1.2.3.4:21, and issued:
+<pre>
+  HOST ftp.domain.com
+</pre>
+or:
+<pre>
+  HOST example.com
+</pre>
+would be handled as one would expect.
+
+<p>
+Defining a virtual host using DNS names would automatically handle the DNS
+name as a <code>ServerAlias</code>:
+<pre>
+  <VirtualHost example.com>
+    Port 21
+    ...
+  </VirtualHost>
+</pre>
+would work just like:
+<pre>
+  <VirtualHost 1.2.3.4>
+    Port 21
+    ServerAlias example.com
+    ...
+  </VirtualHost>
+</pre>
+(assuming that "example.com" resolved to 1.2.3.4, of course).
+
+<p>
+<hr>
+<h3><a name="ServerIdent">ServerIdent</a></h3>
 <strong>Syntax:</strong> ServerIdent <em>off|on "identification string"</em><br>
-<strong>Default:</strong> ServerIdent on "ProFTPD [version] Server (server name) [hostname]"<br>
+<strong>Default:</strong> ServerIdent on "ProFTPD Server (server name) [hostname]"<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.0pre2 and later
@@ -971,14 +2356,50 @@ out minimal information will probably want a setting like:
 which won't even reveal the hostname.
 
 <p>
+As of <code>proftpd-1.3.6rc3</code> and later, the default message changed, such
+that the version information is <i>omitted</i>, becoming:
+<pre>
+  "ProFTPD Server (server name) [hostname]"
+</pre>
+
+<p>
 An example of a custom identification string might be:
 <pre>
   ServerIdent on "Welcome to ftp.linux.co.uk"
 </pre>
 
 <p>
+Note that the following variables can be used in the configured
+<code>ServerIdent</code> text:
+<ul>
+  <li><code>%L</code> (<i>server IP address</i>)
+  <li><code>%V</code> (<i>server fully-qualified domain name</i>)
+  <li><code>%v</code> (<i><a href="#ServerName"><code>ServerName</code></a></i>)
+  <li><code>%{version}</code> (<i>ProFTPD version</i>)
+</ul>
+For example:
+<pre>
+  ServerIdent on "Welcome to %v"
+</pre>
+
+<p>
+<hr>
+<h3><a name="ServerName">ServerName</a></h3>
+<strong>Syntax:</strong> ServerName <em>"standalone"|"inetd"</em><br>
+<strong>Default:</strong> ServerName "ProFTPD"
+<strong>Context:</strong> server config, <code><VirtualHost></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>ServerName</code> directive configures the text that will be
+displayed to a client connecting to the server.  This text will be displayed
+to the client <i>e.g.</i> as part of the response for a <code>HELP</code>
+or <code>STAT</code> command.
+
+<p>
 <hr>
-<h2><a name="ServerType">ServerType</a></h2>
+<h3><a name="ServerType">ServerType</a></h3>
 <strong>Syntax:</strong> ServerType <em>"standalone"|"inetd"</em><br>
 <strong>Default:</strong> ServerType standalone<br>
 <strong>Context:</strong> server config<br>
@@ -1004,7 +2425,33 @@ dedicated to processing all requests from the newly connected client.
 
 <p>
 <hr>
-<h2><a name="SocketBindTight">SocketBindTight</a></h2>
+<h3><a name="SetEnv">SetEnv</a></h3>
+<strong>Syntax:</strong> SetEnv <em>name value</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><Virtual></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.10rc1 and later
+
+<p>
+The <code>SetEnv</code> directive is used to set the environment variable
+<em>name</em> to <em>value</em> in session processes.  Note that if
+<code>SetEnv</code> is used in the "server config" configuration
+context, the configured environment value will be set for the ProFTPD daemon
+process as well.
+
+<p>
+Examples:
+<pre>
+  # Set the TZ environment variable
+  SetEnv TZ GMT
+</pre>
+
+<p>
+See also: <a href="#UnsetEnv"><code>UnsetEnv</code></a>
+
+<p>
+<hr>
+<h3><a name="SocketBindTight">SocketBindTight</a></h3>
 <strong>Syntax:</strong> SocketBindTight <em>on|off</em><br>
 <strong>Default:</strong> <em>off</em><br>
 <strong>Context:</strong> server config<br>
@@ -1063,14 +2510,14 @@ On the other hand, if we use:
 then <code>proftpd</code> again creates two sockets.  However one is bound to
 10.0.0.1, port 21 (<i>i.e.</i> "10.0.0.1:21") and the other is bound to
 10.0.0.2, port 2001 (<i>i.e.</i> "10.0.0.2:2001").  Thus these sockets are
-<em>"tightly"</em> bound to the IP addresses.  This means that port 21 can be
+<em>tightly</em> bound to the IP addresses.  This means that port 21 can be
 reused on any address <i>other</i> than 10.0.0.1, and similarly for port 2001
 and 10.0.0.2.
 
 <p>
 One side effect of setting <code>SocketBindTight</code> to <em>on</em> is that
 connections to non-bound addresses will result in a "connection refused"
-message rather than the more common:
+message rather than the more common (<i>assuming</i> no <a href="#DefaultServer"><code>DefaultServer</code></a> directive):
 <pre>
   500 Sorry, no server available to handle request on <i>a.b.c.d.</i>
 </pre>
@@ -1079,8 +2526,11 @@ address/port pair.  This may or may not be aesthetically desirable, depending
 on your circumstances.
 
 <p>
+See also: <a href="#DefaultServer"><code>DefaultServer</code></a>
+
+<p>
 <hr>
-<h2><a name="SocketOptions">SocketOptions</a></h2>
+<h3><a name="SocketOptions">SocketOptions</a></h3>
 <strong>Syntax:</strong> SocketOptions <em>[maxseg <i>byte-count</i>] [rcvbuf <i>byte-count</i>] [sndbuf <i>byte-count</i>] [keepalive "on"|"off"|spec]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code><br>
@@ -1150,7 +2600,7 @@ is equivalent to:
 
 <p>
 <hr>
-<h2><a name="SyslogFacility">SyslogFacility</a></h2>
+<h3><a name="SyslogFacility">SyslogFacility</a></h3>
 <strong>Syntax:</strong> SyslogFacility <em>facility</em><br>
 <strong>Default:</strong> SyslogFacility daemon<br>
 <strong>Context:</strong> server config<br>
@@ -1205,7 +2655,7 @@ See also: <a href="#SyslogLevel"><code>SyslogLevel</code></a>,
 
 <p>
 <hr>
-<h2><a name="SyslogLevel">SyslogLevel</a></h2>
+<h3><a name="SyslogLevel">SyslogLevel</a></h3>
 <strong>Syntax:</strong> SyslogLevel <em>level</em><br>
 <strong>Default:</strong> SyslogLevel debug<br>
 <strong>Context:</strong> server config<br>
@@ -1218,7 +2668,7 @@ recorded via the default Unix syslog logging. The following <em>levels</em>
 are available, in order of decreasing significance:
 
 <p>
-<table border=1>
+<table border=1 summary="Syslog Levels">
   <tr>
     <td><b>Level</b></td>
     <td><b>Description</b></td>
@@ -1285,7 +2735,7 @@ See also: <a href="#SyslogFacility"><code>SyslogFacility</code></a>,
 
 <p>
 <hr>
-<h2><a name="TCPBacklog">TCPBacklog</a></h2>
+<h3><a name="TCPBacklog">TCPBacklog</a></h3>
 <strong>Syntax:</strong> TCPBacklog <em>backlog-size</em><br>
 <strong>Default:</strong> 5<br>
 <strong>Context:</strong> server config<br>
@@ -1328,7 +2778,7 @@ to <a href="http://en.wikipedia.org/wiki/Syn_flood">TCP syn floods</a>.
 Each operating system, then, has different ways of handling incoming and
 pending connections, to attempt to guard against such attacks.  For Linux
 systems, read the <code>tcp(7)</code> man page and specifically about
-<code>tcp_abort_on_overflow</code>, </code>tcp_max_syn_backlog</code>,
+<code>tcp_abort_on_overflow</code>, <code>tcp_max_syn_backlog</code>,
 and <code>tcp_syncookies</code>.  On FreeBSD, read the
 <code>syncookies(4)</code> man page.  And read
 <a href="http://www.sean.de/Solaris/soltune.html#backlog">here</a> for
@@ -1336,7 +2786,20 @@ additional tuning considerations on Solaris.
 
 <p>
 <hr>
-<h2><a name="TimeoutIdle">TimeoutIdle</a></h2>
+<h3><a name="TCPNoDelay">TCPNoDelay</a></h3>
+<strong>Syntax:</strong> TCPNoDelay <em>on|off</em><br>
+<strong>Default:</strong> TCPNoDelay on<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.0 and later
+
+<p>
+The <code>TCPNoDelay</code> directive affects the use of the <a href="https://en.wikipedia.org/wiki/Nagle's_algorithm">Nagle algorithm</a>.  <b>Note</b> that
+most sites will <em>never need this</em>.
+
+<p>
+<hr>
+<h3><a name="TimeoutIdle">TimeoutIdle</a></h3>
 <strong>Syntax:</strong> TimeoutIdle <em>seconds</em><br>
 <strong>Default:</strong> 600 seconds<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -1365,9 +2828,9 @@ See also: <a href="mod_auth.html#TimeoutLogin"><code>TimeoutLogin</code></a>,
 
 <p>
 <hr>
-<h2><a name="TimeoutLinger">TimeoutLinger</a></h2>
+<h3><a name="TimeoutLinger">TimeoutLinger</a></h3>
 <strong>Syntax:</strong> TimeoutLinger <em>seconds</em><br>
-<strong>Default:</strong> 30<br>
+<strong>Default:</strong> 10<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.2.10rc2 and later
@@ -1404,7 +2867,28 @@ their FTP clients.
 
 <p>
 <hr>
-<h2><a name="Trace">Trace</a></h2>
+<h3><a name="TimesGMT">TimesGMT</a></h3>
+<strong>Syntax:</strong> TimesGMT <em>on|off</em><br>
+<strong>Default:</strong> TimesGMT on<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.0 and later
+
+<p>
+The <code>TimesGMT</code> directive configures whether ProFTPD will use
+timestamps in GMT, not local time, for directory listings (via
+<code>LIST</code> and <code>NLST</code> commands) and the <code>MDTM</code>
+command.
+
+<p>
+To configure ProFTPD to use local time, use:
+<pre>
+  TimesGMT off
+</pre>
+
+<p>
+<hr>
+<h3><a name="Trace">Trace</a></h3>
 <strong>Syntax:</strong> Trace <em>channel1:level1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -1444,7 +2928,7 @@ See the <a href="../howto/Tracing.html">Tracing</a> howto for more information.
 
 <p>
 <hr>
-<h2><a name="TraceLog">TraceLog</a></h2>
+<h3><a name="TraceLog">TraceLog</a></h3>
 <strong>Syntax:</strong> TraceLog <em>path</em><br>
 <strong>Default:</strong> None<br> 
 <strong>Context:</strong> server config<br>
@@ -1466,10 +2950,10 @@ See the <a href="../howto/Tracing.html">Tracing</a> howto for more information.
 
 <p>
 <hr>
-<h2><a name="TraceOptions">TraceOptions</a></h2>
+<h3><a name="TraceOptions">TraceOptions</a></h3>
 <strong>Syntax:</strong> TraceOptions <em>opt1 ... optN</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global><br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -1497,18 +2981,18 @@ to disable the option, use a '-' (minus) character prefix.  For example:
 
 <p>
 <hr>
-<h2><a name="TransferLog">TransferLog</a></h2>
+<h3><a name="TransferLog">TransferLog</a></h3>
 <strong>Syntax:</strong> TransferLog <em>path</em>|"none"<br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Anonymous><br>
+<strong>Context:</strong> server config;, <VirtualHost>, <Global>, <Anonymous><br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 1.1.4 and later
 
 <p>
 The <code>TransferLog</code> directive configures the full <em>path</em> to the
-"wu-ftpd style" file transfer log; see the <code>xferlog(5)</code> man page
-for a description of this log file format.  Separate log files can be created
-for each <code><Anonymous></code> and/or <code><VirtualHost></code>.
+"wu-ftpd style" file transfer log; see the <a href="../docs/howto/Logging.html#TransferLog"><code>xferlog(5)</code></a> man page for a description of this log
+file format.  Separate log files can be created for each
+<code><Anonymous></code> and/or <code><VirtualHost></code>.
 Additionally, the special keyword "none" (available in proftpd-1.1.7 and later)
 can be used, which disables wu-ftpd style transfer logging for the context in
 which the directive is used.
@@ -1519,10 +3003,10 @@ See also: <a href="mod_log.html#ExtendedLog"><code>ExtendedLog</code></a>,
 
 <p>
 <hr>
-<h2><a name="Umask">Umask</a></h2>
+<h3><a name="Umask">Umask</a></h3>
 <strong>Syntax:</strong> Umask <em>file-umask [dir-umask]</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <VirtualHost>, <Global>, <Anonymous>, .ftpaccess<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Anonymous>, .ftpaccess<br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 0.99.0 and later
 
@@ -1553,7 +3037,129 @@ umasks in greater detail.
 
 <p>
 <hr>
-<h2><a name="UserOwner">UserOwner</a></h2>
+<h3><a name="UnsetEnv">UnsetEnv</a></h3>
+<strong>Syntax:</strong> UnsetEnv <em>name</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><Virtual></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.2.10rc1 and later
+
+<p>
+The <code>UnsetEnv</code> directive is used to unset/remove the <em>name</em>
+environment variable from the environment for sessions.  Note that if the
+<code>UnsetEnv</code> directive is used in the "server config"
+configuration context, the <em>name</em> variable is removed from the
+environment for the ProFTPD daemon process as well.
+
+<p>
+Examples:
+<pre>
+  # Unset the USER and HOME environment variables for sessions
+  UnsetEnv USER
+  UnsetEnv HOME
+</pre>
+
+<p>
+See also: <a href="#SetEnv"><code>SetEnv</code></a>
+
+<p>
+<hr>
+<h3><a name="UseIPv6">UseIPv6</a></h3>
+<strong>Syntax:</strong> UseIPv6 <em>on|off</em><br>
+<strong>Default:</strong> UseIPv6 on<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.3.1rc1 and later
+
+<p>
+The <code>UseIPv6</code> directive enables/disables the use of IPv6.
+
+<p>
+IPv6 support can also be controlled via command-line options:
+<ul>
+  <li><em><code>-4</code>, <code>--ipv4</code></em><br>
+    <p>
+    Use/support IPv4 functionality only
+  </li>
+
+  <p>
+  <li><em><code>-6</code>, <code>--ipv6</code></em><br>
+    <p>
+    Use/support IPv4 <b>and</b> IPv6 functionality
+  </li>
+</ul>
+
+<p>
+<hr>
+<h3><a name="User">User</a></h3>
+<strong>Syntax:</strong> User <em>name</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <VirtualHost>, <Global>, <Anonymous><br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 0.99.0 and later
+
+<p>
+The <code>User</code> directive configures the UID that ProFTPD will use when
+running.
+
+<p>
+By default, ProFTPD runs as "root"; this is considered undesirable in
+all but the most trusted network configurations.  The <code>User</code>
+directive, used in conjunction with the <a href="#Group"><code>Group</code></a>
+directive, instructs ProFTPD to switch to the specified UID/GID as quickly as
+possible after startup.
+
+<p>
+On some Unix variants, ProFTPD will occasionally switch back to "root"
+in order to accomplish a task which requires superuser access.  Once that task
+is completed, root privileges are relinquished and the server returns to running
+as the specified UID/GID.  When applied to a <code><VirtualHost></code>
+section, ProFTPD will run as the specified UID/GID on connections destined for
+the virtual server's address and port.  If either <code>User</code> or
+<code>Group</code> is applied to an <code><Anonymous></code> section,
+ProFTPD will establish an anonymous login when a client attempts to authenticate
+with that specified <code>User</code> name, as well as permanently switching to
+the corresponding UID/GID after authentication.
+
+<p>
+<hr>
+<h3><a name="UseReverseDNS">UseReverseDNS</a></h3>
+<strong>Syntax:</strong> UseReverseDNS <em>on|off</em><br>
+<strong>Default:</strong> UseReverseDNS on<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_core<br>
+<strong>Compatibility:</strong> 1.1.7 and later
+
+<p>
+The <code>UseReverseDNS</code> directive is used to control whether ProFTPD
+performs a <a href="https://en.wikipedia.org/wiki/Reverse_DNS_lookup">reverse
+DNS lookup</a> on connecting clients, both for control <i>and</i> for data
+connections.  When reverse DNS lookups are enabled, the
+<a href="mod_log.html#LogFormat"><code>LogFormat %h</code></a> variable will
+use the IP address, rather than the remote hostname.
+
+<p>
+Normally, <i>incoming</i> active mode data connections and <i>outgoing</i>
+passive mode data connections have reverse DNS lookups performed on the remote
+host's IP address.  However, when the session is chrooted (<i>e.g.</i> due to
+the <a href="mod_auth.html#DefaultRoot"><code>DefaultRoot</code></a> directive
+or an <code><Anonymous></code> login), the local <code>/etc/hosts</code>
+file cannot be checked, and the only possible resolution is via DNS.  If for
+some reason, DNS is not available or improperly configured <i>for that
+remote host</i>, this can result in ProFTPD blocking/stalling <i>until</i> the
+DNS resolution times out.
+
+<p>
+<b>Note</b> that using:
+<pre>
+  UseReverseDNS on
+</pre>
+<i>can</i> lead to delays when connecting to ProFTPD, due to the time needed
+to perform the forward <i>and</i> reverse DNS resolutions.
+
+<p>
+<hr>
+<h3><a name="UserOwner">UserOwner</a></h3>
 <strong>Syntax:</strong> UserOwner <em>user-name</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> <Anonymous>, <Directory><br>
@@ -1577,10 +3183,10 @@ See also: <a href="#GroupOwner"><code>GroupOwner</code></a>
 
 <p>
 <hr>
-<h2><a name="VirtualHost"><VirtualHost></a></h2>
+<h3><a name="VirtualHost"><VirtualHost></a></h3>
 <strong>Syntax:</strong> <VirtualHost <em>ip-address|dns-name [ip-address|dns-name ...]</em>><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_core<br>
 <strong>Compatibility:</strong> 0.99.0 and later
 
@@ -1688,21 +3294,39 @@ The <code>SocketBindTight</code> directive tells <code>proftpd</code> to
 listen <b>only</b> on that 'localhost' IP address, rather than on all
 addresses.
 
-<p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-30 16:50:10 $</i><br>
+<p><a name="FileZillaNonASCII">
+<font color=red>Question</font>: When I connect to ProFTPD using FileZilla,
+I see FileZilla log the following warning:
+<pre>
+  Status: Server does not support non-ASCII characters.
+</pre>
+even though I used the <code>--enable-nls</code> build option, and my ProFTPD
+supports UTF8.  What is wrong?<br>
+<font color=blue>Answer</font>: FileZilla parses the <code>FEAT</code> response
+to determine whether the FTP server supports the UTF-8 encoding.  However, the
+<i>format</i> of the <code>FEAT</code> response can confuse FileZilla's
+detection code.  For example, if your <code>proftpd.conf</code> uses:
+<pre>
+  MultilineRFC2228 on
+</pre>
+this causes ProFTPD's <code>FEAT</code> response format to be different than
+FileZilla expects, which can lead to the above "does not support non-ASCII
+characters" message.
 
-<br><hr>
+<p>
+The solution is to use:
+<pre>
+  MultilineRFC2228 off
+</pre>
+in your <code>proftpd.conf</code> (or simply remove that directive entirely).
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2014 The ProFTPD Project<br>
+© Copyright 2000-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_ctrls.html b/doc/modules/mod_ctrls.html
index 6f6ea28..0fb6641 100644
--- a/doc/modules/mod_ctrls.html
+++ b/doc/modules/mod_ctrls.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ctrls.html,v 1.6 2013-05-17 15:04:43 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_ctrls.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ctrls</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_ctrls.c</code> and
 <code>mod_ctrls.h</code> files for ProFTPD 1.2, and is not compiled by default.
 Installation instructions are discussed <a href="#Installation">here</a>.
@@ -65,7 +64,7 @@ questions, concerns, or suggestions regarding this module.
 
 <p>
 <hr>
-<h2><a name="ControlsACLs">ControlsACLs</a></h2>
+<h3><a name="ControlsACLs">ControlsACLs</a></h3>
 <strong>Syntax:</strong> ControlsACLs <em>actions|all allow|deny user|group list</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -89,7 +88,7 @@ ACLs for different control actions, and for both users and groups.
 
 <p>
 <hr>
-<h2><a name="ControlsAuthFreshness">ControlsAuthFreshness</a></h2>
+<h3><a name="ControlsAuthFreshness">ControlsAuthFreshness</a></h3>
 <strong>Syntax:</strong> ControlsAuthFreshness <em>seconds</em><br>
 <strong>Default:</strong> ControlsAuthFreshness 10<br>
 <strong>Context:</strong> server config<br>
@@ -103,7 +102,7 @@ is older than the configured age, the connection is denied.
 
 <p>
 <hr>
-<h2><a name="ControlsEngine">ControlsEngine</a></h2>
+<h3><a name="ControlsEngine">ControlsEngine</a></h3>
 <strong>Syntax:</strong> ControlsEngine <em>on|off</em><br>
 <strong>Default:</strong> ControlsEngine on<br>
 <strong>Context:</strong> server config<br>
@@ -117,7 +116,7 @@ requests.
 
 <p>
 <hr>
-<h2><a name="ControlsInterval">ControlsInterval</a></h2>
+<h3><a name="ControlsInterval">ControlsInterval</a></h3>
 <strong>Syntax:</strong> ControlsInterval <em>seconds</em><br>
 <strong>Default:</strong> ControlsInterval 10<br>
 <strong>Context:</strong> server config<br>
@@ -132,7 +131,7 @@ must be a positive number.
 
 <p>
 <hr>
-<h2><a name="ControlsLog">ControlsLog</a></h2>
+<h3><a name="ControlsLog">ControlsLog</a></h3>
 <strong>Syntax:</strong> ControlsLog <em>file</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -146,7 +145,7 @@ configured <em>file</em> must be an absolute path.
 
 <p>
 <hr>
-<h2><a name="ControlsMaxClients">ControlsMaxClients</a></h2>
+<h3><a name="ControlsMaxClients">ControlsMaxClients</a></h3>
 <strong>Syntax:</strong> ControlsMaxClients <em>number</em><br>
 <strong>Default:</strong> ControlsMaxClients 5<br>
 <strong>Context:</strong> server config<br>
@@ -160,7 +159,7 @@ checks the socket.  <em>number</em> must be a positive number.
 
 <p>
 <hr>
-<h2><a name="ControlsSocket">ControlsSocket</a></h2>
+<h3><a name="ControlsSocket">ControlsSocket</a></h3>
 <strong>Syntax:</strong> ControlsSocket <em>file</em><br>
 <strong>Default:</strong> ControlsSocket var/run/proftpd.sock<br>
 <strong>Context:</strong> server config<br>
@@ -175,7 +174,7 @@ option will also need to be used.
 
 <p>
 <hr>
-<h2><a name="ControlsSocketACL">ControlsSocketACL</a></h2>
+<h3><a name="ControlsSocketACL">ControlsSocketACL</a></h3>
 <strong>Syntax:</strong> ControlsSocketACL <em>allow|deny user|group list</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -199,7 +198,7 @@ and one using "group" may be used simultaneously.
 
 <p>
 <hr>
-<h2><a name="ControlsSocketOwner">ControlsSocketOwner</a></h2>
+<h3><a name="ControlsSocketOwner">ControlsSocketOwner</a></h3>
 <strong>Syntax:</strong> ControlsSocketOwner <em>user group</em><br>
 <strong>Default:</strong> ControlsSocketOwner <em>root root</em><br>
 <strong>Context:</strong> server config<br>
@@ -218,7 +217,7 @@ access control.
 
 <p>
 <hr>
-<h2><a name="help"><code>help</code></a></h2>
+<h3><a name="help"><code>help</code></a></h3>
 <strong>Syntax:</strong> ftpdctl help<br>
 <strong>Purpose:</strong> Display a sorted list of active controls and their descriptions
 
@@ -228,7 +227,7 @@ active control actions, and their descriptions.
 
 <p>
 <hr>
-<h2><a name="insctrl"><code>insctrl</code></a></h2>
+<h3><a name="insctrl"><code>insctrl</code></a></h3>
 <strong>Syntax:</strong> ftpdctl insctrl <em>action|all [module]</em><br>
 <strong>Purpose:</strong> Enable control actions
 
@@ -248,7 +247,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="lsctrl"><code>lsctrl</code></a></h2>
+<h3><a name="lsctrl"><code>lsctrl</code></a></h3>
 <strong>Syntax:</strong> ftpdctl lsctrl<br>
 <strong>Purpose:</strong> Display a sorted list of all active control actions
 
@@ -259,7 +258,7 @@ the module that implements that action.
 
 <p>
 <hr>
-<h2><a name="rmctrl"><code>rmctrl</code></a></h2>
+<h3><a name="rmctrl"><code>rmctrl</code></a></h3>
 <strong>Syntax:</strong> ftpdctl rmctrl <em>action|all [module]</em><br>
 <strong>Purpose:</strong> Disable control actions
 
@@ -298,31 +297,40 @@ but portably obtaining them is next to impossible.  On some flavors of Unix
 it simply cannot be done.  Stevens' method is the next best thing right now.
 
 <p>
+<b>Logging</b><br>
+The <code>mod_ctrls</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>ctrls
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace ctrls:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
+<p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_ctrls</code> module is distributed with ProFTPD.  To enable
 use of Controls, use the <code>--enable-ctrls</code> configure option:
 <pre>
-  ./configure --enable-ctrls
-  make
-  make install
+  $ ./configure --enable-ctrls
+  $ make
+  $ make install
 </pre>
 This option causes <code>mod_ctrls</code> to be compiled into
 <code>proftpd</code>.
 
 <p>
 <hr>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-05-17 15:04:43 $</i><br>
-
-<br><hr>
-
 <font size=2><b><i>
-© Copyright 2000-2011 TJ Saunders<br>
+© Copyright 2000-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/modules/mod_delay.html b/doc/modules/mod_delay.html
index 6a32a5e..e6c529e 100644
--- a/doc/modules/mod_delay.html
+++ b/doc/modules/mod_delay.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_delay.html,v 1.7 2009-03-04 00:57:52 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_delay.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_delay</title>
@@ -63,6 +61,7 @@ of the module as it was developed.
 <ul>
   <li><a href="#DelayControlsACLs">DelayControlsACLs</a>
   <li><a href="#DelayEngine">DelayEngine</a>
+  <li><a href="#DelayOnEvent">DelayOnEvent</a>
   <li><a href="#DelayTable">DelayTable</a>
 </ul>
 
@@ -73,7 +72,7 @@ of the module as it was developed.
 </ul>
 
 <hr>
-<h2><a name="DelayControlsACLs">DelayControlsACLs</a></h3>
+<h3><a name="DelayControlsACLs">DelayControlsACLs</a></h3>
 <strong>Syntax:</strong> DelayControlsACLs <em>actions|"all" "allow"|"deny" "user"|"group" list</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -95,11 +94,12 @@ others are denied.  If "deny" is used, then the <em>list</em> of
 allowed.  Multiple <code>DelayControlsACLs</code> directives may be used to
 configure ACLs for different control actions, and for both users and groups.
 
+<p>
 <hr>
-<h2><a name="DelayEngine">DelayEngine</a></h2>
+<h3><a name="DelayEngine">DelayEngine</a></h3>
 <strong>Syntax:</strong> DelayEngine <em>on|off</em><br>
 <strong>Default:</strong> DelayEngine on<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_delay<br>
 <strong>Compatibility:</strong> 1.2.10rc1 and later
 
@@ -116,11 +116,47 @@ Example:
   </IfModule>
 </pre>
 
+<p>
 <hr>
-<h2><a name="DelayTable">DelayTable</a></h2>
-<strong>Syntax:</strong> DelayTable <em>path</em><br>
-<strong>Default:</strong> DelayTable var/proftpd/proftpd.delay<br>
-<strong>Context:</strong> "server config"<br>
+<h3><a name="DelayOnEvent">DelayOnEvent</a></h3>
+<strong>Syntax:</strong> DelayOnEvent <em>event</em> <em>delay-ms</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config<br>
+<strong>Module:</strong> mod_delay<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>DelayOnEvent</code> directive configures an explicit
+<em>delay-ms</em>, in milliseconds, for the given <em>event</em>.  The
+supported values for the <em>event</em> parameter are:
+<ul>
+  <li>USER
+  <li>PASS
+  <li>FailedLogin
+</ul>
+The <em>delay-ms</em> parameter can be a number (in which case the units are
+assumed to be milliseconds), or using a suffix.
+
+<p>
+To help illustrate the usage of <code>DelayOnEvent</code>, here are some
+examples:
+<pre>
+  DelayOnEvent PASS 2000ms
+</pre>
+This configures <code>mod_delay</code> to always add a delay of 2000
+milliseconds for every PASS command.  Alternatively, to add a delay <i>only
+on failed logins</i>, you might use:
+<pre>
+  DelayOnEvent FailedLogin 5s
+</pre>
+which adds a delay of 5 seconds after the login has failed.
+
+<p>
+<hr>
+<h3><a name="DelayTable">DelayTable</a></h3>
+<strong>Syntax:</strong> DelayTable <em>path</em>|<em>"none"</em><br>
+<strong>Default:</strong> DelayTable /var/proftpd/proftpd.delay<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_delay<br>
 <strong>Compatibility:</strong> 1.2.10rc1 and later
 
@@ -138,6 +174,13 @@ Note that timing data is kept across daemon stop/starts.  When new
 stored data.
 
 <p>
+If the <code>DelayTable</code> parameter is <em>"none"</em>, then the
+<code>mod_delay</code> module will <b>not</b> store timing data.  This
+configuration is used, in conjunction with
+<a href="#DelayOnEvent"><code>DelayOnEvent</code></a>, for setting explicit
+delays, rather than learning/adapting the delays dynamically.
+
+<p>
 <hr>
 <h2>Control Actions</h2>
 
@@ -219,6 +262,21 @@ IP address ranges.  For example:
 </pre>
 More information on defining classes can be found <a href="../howto/Classes.html">here</a>.
 
+<p>
+<b>Logging</b><br>
+The <code>mod_delay</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>delay
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace delay:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -247,15 +305,10 @@ delay time for one site will be unacceptably long for another site.
 The <code>mod_delay</code> module is compiled by default.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2009-03-04 00:57:52 $</i><br>
-
 <br><hr>
 
 <font size=2><b><i>
-© Copyright 2004-2009 TJ Saunders<br>
+© Copyright 2004-2014 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
 
@@ -263,4 +316,3 @@ Last Updated: <i>$Date: 2009-03-04 00:57:52 $</i><br>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_dso.html b/doc/modules/mod_dso.html
index e148535..ed36f12 100644
--- a/doc/modules/mod_dso.html
+++ b/doc/modules/mod_dso.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_dso.html,v 1.7 2013-05-09 03:58:40 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_dso.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_dso</title>
@@ -101,10 +99,10 @@ ProFTPD source distribution:
 
 <p>
 <hr>
-<h2><a name="LoadFile">LoadFile</a></h2>
+<h3><a name="LoadFile">LoadFile</a></h3>
 <strong>Syntax:</strong> LoadFile <em>path</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_dso<br>
 <strong>Compatibility:</strong> 1.3.0rc1 and later
 
@@ -127,10 +125,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="LoadModule">LoadModule</a></h2>
+<h3><a name="LoadModule">LoadModule</a></h3>
 <strong>Syntax:</strong> LoadModule <em>name</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_dso<br>
 <strong>Compatibility:</strong> 1.3.0rc1 and later
 
@@ -146,10 +144,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="ModuleControlsACLs">ModuleControlsACLs</a></h2>
+<h3><a name="ModuleControlsACLs">ModuleControlsACLs</a></h3>
 <strong>Syntax:</strong> ModuleControlsACLs <em>actions|all allow|deny user|group list</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_dso<br>
 <strong>Compatibility:</strong> 1.3.0rc1 and later
 
@@ -183,10 +181,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="ModuleOrder">ModuleOrder</a></h2>
+<h3><a name="ModuleOrder">ModuleOrder</a></h3>
 <strong>Syntax:</strong> ModuleOrder <em>...</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_dso<br>
 <strong>Compatibility:</strong> 1.3.0rc1 and later
 
@@ -220,10 +218,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="ModulePath">ModulePath</a></h2>
+<h3><a name="ModulePath">ModulePath</a></h3>
 <strong>Syntax:</strong> ModulePath <em>path</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_dso<br>
 <strong>Compatibility:</strong> 1.3.0rc1 and later
 
@@ -249,7 +247,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="insmod"><code>insmod</code></a></h2>
+<h3><a name="insmod"><code>insmod</code></a></h3>
 <strong>Syntax:</strong> ftpdctl insmod <em>module</em><br>
 <strong>Purpose:</strong> Load a DSO module
 
@@ -269,7 +267,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="lsmod"><code>lsmod</code></a></h2>
+<h3><a name="lsmod"><code>lsmod</code></a></h3>
 <strong>Syntax:</strong> ftpdctl lsmod<br>
 <strong>Purpose:</strong> Display list of all loaded modules
 
@@ -298,7 +296,7 @@ Example:
 
 <p>
 <hr>
-<h2><a name="rmmod"><code>rmmod</code></a></h2>
+<h3><a name="rmmod"><code>rmmod</code></a></h3>
 <strong>Syntax:</strong> ftpdctl rmmod <em>module</em><br>
 <strong>Purpose:</strong> Unload a DSO module
 
@@ -329,13 +327,28 @@ your <code>proftpd</code> has been compiled with Controls support.
 The <code>mod_dso</code> module is distributed with ProFTPD.  To enable use
 of DSO modules, use the <code>--enable-dso</code> configure option:
 <pre>
-  ./configure --enable-dso
-  make
-  make install
+  $ ./configure --enable-dso
+  $ make
+  $ make install
 </pre>
 This option causes <code>mod_dso</code> to be compiled into
 <code>proftpd</code>.
 
+<p>
+<b>Logging</b><br>
+The <code>mod_dso</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>dso
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace dso:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
+
 <p><a name="FAQ">
 <b>Frequently Asked Questions</b><br>
 
@@ -423,8 +436,8 @@ a few such modules with this special handling:
 All of these modules would appear in the <code>`proftpd -l'</code> static
 module list.
 
-<p><a name="DSOLoadingStaticModule">
-<font color=red>Question</font>: If I use <code><a href="../utils/prxs.html">prxs</a></code> to compile a module like <code>mod_ldap</code>, which is already
+<p><a name="DSOLoadingStaticModule"></a>
+<font color=red>Question</font>: If I use <a href="../utils/prxs.html"><code>prxs</code></a> to compile a module like <code>mod_ldap</code>, which is already
 built into my <code>proftpd</code> as a static module, and then I use:
 <pre>
   LoadModule mod_ldap.c
@@ -441,21 +454,35 @@ whether that module is already loaded -- and if so, the module will
 "already loaded".  This means that your <code>mod_ldap</code> shared module
 code would not be loaded, and thus would not override the static module.
 
-<p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-05-09 03:58:40 $</i><br>
-
-<br><hr>
+<p><a name="DSOModulePathWorldWritable">
+<font color=red>Question</font>: I try to start my <code>proftpd</code>
+instance, but it fails to start with this error:
+<pre>
+  Fatal: ModulePath: <i>/usr/lib/proftpd</i> is world-writable on line <i>7</i> of <i>'/etc/proftpd/modules.conf'</i>
+</pre>
+Why?<br>
+<font color=blue>Answer</font>: The <code>mod_dso</code> module, used by
+ProFTPD for DSO/shared modules, ensures that the
+<a href="#ModulePath"><code>ModulePath</code></a> directory is secure.  DSO
+modules are code that are loaded directly into <code>proftpd</code>; loading
+these modules from a world-writable directory means that <b>any</b> system user
+could inject/replace any of those DSO modules with their own code, and get
+<code>proftpd</code> to do whatever they wanted.  Thus <code>mod_dso</code>
+will <b>refuse</b> to use a world-writable directory for loading of modules.
+
+<p>
+To fix this, you simply need to ensure that the <code>ModulePath</code> directory is not world-writable, <i>e.g.</i>:
+<pre>
+  $ chmod o-w /usr/lib/proftpd
+</pre>
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2004-2013 TJ Saunders<br>
+© Copyright 2004-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_facl.html b/doc/modules/mod_facl.html
index 0f130ca..1737496 100644
--- a/doc/modules/mod_facl.html
+++ b/doc/modules/mod_facl.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_facl.html,v 1.1 2006-10-11 16:47:15 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_facl.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_facl</title>
@@ -42,7 +40,7 @@ ProFTPD source distribution:
 </ul>
 
 <hr>
-<h2><a name="FACLEngine">FACLEngine</a></h2>
+<h3><a name="FACLEngine">FACLEngine</a></h3>
 <strong>Syntax:</strong> FACLEngine <em>on|off</em><br>
 <strong>Default:</strong> on<br>
 <strong>Context:</strong> server config<br>
@@ -59,24 +57,31 @@ runtime checking of POSIX ACLs.  Use this directive to disable the module.
 To use the <code>mod_facl</code> module, configure <code>proftpd</code>
 using the following:
 <pre>
-  ./configure --enable-facl --with-modules=mod_facl ...
+  $ ./configure --enable-facl --with-modules=mod_facl ...
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2006-10-11 16:47:15 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_facl</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>facl
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace facl:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2006 TJ Saunders<br>
+© Copyright 2000-2014 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_facts.html b/doc/modules/mod_facts.html
index eec45d0..cc2e072 100644
--- a/doc/modules/mod_facts.html
+++ b/doc/modules/mod_facts.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_facts.html,v 1.13 2013-07-29 16:54:45 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_facts.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_facts</title>
@@ -29,7 +27,8 @@ the <code>mod_facts</code> module implements the <code>MFF</code> and
 
 <p>
 This module is contained in the <code>mod_facts.c</code> file for
-ProFTPD 1.3.<i>x</i>, and is compiled by default.
+ProFTPD 1.3.<i>x</i>, and is compiled by default.  Other installation
+instructions are discussed <a href="#Installation">here</a>.
 
 <p>
 The most current version of <code>mod_facts</code> can be found in the
@@ -45,7 +44,7 @@ ProFTPD source distribution:
 </ul>
 
 <hr>
-<h2><a name="FactsAdvertise">FactsAdvertise</a></h2>
+<h3><a name="FactsAdvertise">FactsAdvertise</a></h3>
 <strong>Syntax:</strong> FactsAdvertise <em>on|off</em><br>
 <strong>Default:</strong> FactsAdvertise on<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -76,7 +75,7 @@ you can disable the advertising of support for those commands using
 </pre>
 
 <hr>
-<h2><a name="FactsOptions">FactsOptions</a></h2>
+<h3><a name="FactsOptions">FactsOptions</a></h3>
 <strong>Syntax:</strong> FactsOptions <em>opt1 ...</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -106,6 +105,32 @@ The currently implemented options are:
     <b>Note</b> that this option first appeared in
     <code>proftpd-1.3.4b</code>.
   </li>
+
+  <p>
+  <li><code>NoAdjustedSymlinks</code><br>
+    <p>
+    By default, <code>mod_facts</code> tries to automatically adjust any
+    symlink destination paths when the FTP session is chrooted, so that
+    the adjusted symlinks work properly <i>e.g.</i> for FTP clients.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc2</code>.
+  </li>
+
+  <p>
+  <li><code>NoNames</code><br>
+    <p>
+    By default, <code>mod_facts</code> will automatically use the newer
+    <code>UNIX.ownername</code> and <code>UNIX.groupname</code> facts in its
+    <code>MLSD</code>/<code>MLST</code> responses.  But some FTP clients may
+    not react well to the presence of these facts.  Use this option to disable
+    to use of these facts.
+
+    <p>
+    <b>Note</b> that this option first appeared in
+    <code>proftpd-1.3.6rc3</code>.
+  </li>
 </ul>
 
 <p><a name="FAQ">
@@ -223,19 +248,49 @@ Alternatively, you can use the <code>HideFiles</code> and
 This latter configuration will apply to <code>LIST</code>, <code>NLST</code>,
 and <code>MLSD</code> equally.
 
+<p><a name="FactsRemove">
+<font color=red>Question</font>: How can I disable the <code>mod_facts</code>
+module entirely?  Some clients (<i>e.g.</i> ncftp) do not appear to implement
+<code>MLSD</code> properly, and do not honor the <code>FEAT</code> response from
+<code>proftpd</code> when <code>FactsAdvertise</code> is configured
+<em>off</em>.<br>
+<font color=blue>Answer</font>: There is no easy way of disabling the
+<code>mod_facts</code> module at present; the
+<a href="#FactsAdvertise"><code>FactsAdvertise</code></a> directive is intended
+to do this in a way compliant with the RFCs.
+
 <p>
-<hr><br>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-07-29 16:54:45 $</i><br>
+That said, you <i>can</i> compile <code>proftpd</code> such that
+<code>mod_facts</code> is built as a DSO/shared module:
+<pre>
+  $ ./configure --enable-dso --with-shared=mod_facts ...
+</pre>
+and then, in your <code>proftpd.conf</code>, you simply omit any
+"LoadModule mod_facts.c" directive, so that that module is never dynamically
+loaded.
 
-<br><hr>
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_facts</code> module is distributed with ProFTPD, and is
+normally compiled as a static module by default.  However, if you wish to
+have <code>mod_facts</code> be built as a shared module, you would use:
+<pre>
+  $ ./configure --enable-dso --with-shared=mod_facts ...
+</pre>
+Then follow the usual steps:
+<pre>
+  $ make
+  $ make install
+</pre>
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2007-2013 TJ Saunders<br>
+© Copyright 2007-2016 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
+<hr>
 
-<hr><br>
 </body>
 </html>
-
diff --git a/doc/modules/mod_ident.html b/doc/modules/mod_ident.html
index 599201a..2528d41 100644
--- a/doc/modules/mod_ident.html
+++ b/doc/modules/mod_ident.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ident.html,v 1.1 2008-01-05 01:22:20 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_ident.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ident</title>
@@ -41,10 +39,10 @@ ProFTPD source distribution:
 
 <p>
 <hr>
-<h2><a name="IdentLookups">IdentLookups</a></h2>
+<h3><a name="IdentLookups">IdentLookups</a></h3>
 <strong>Syntax:</strong> IdentLookups <em>on|off</em><br>
 <strong>Default:</strong> IdentLookups on<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_ident<br>
 <strong>Compatibility:</strong> 1.3.2rc1
 
@@ -53,35 +51,52 @@ The <code>IdentLookups</code> directive enables or disables the module's lookup
 of an "authenticated" user identity using RFC1413.
 
 <p>
+Normally, when a client initially connects to ProFTPD, the
+<code>mod_ident</code> will attempt to use the ident protocol (RFC1413) to
+identify the remote username.  This behavior can be disabled via this directive:
+<pre>
+  IdentLookups off
+</pre>
+<b>Not</b> that this lookup can cause delays (and reports of "slow logins")
+when clients connect to ProFTPD.
+
+<p>
 <hr>
 <h2><a name="Installation">Installation</a></h2>
 The <code>mod_ident</code> module is compiled into <code>proftpd</code> by
 default.  To build a <code>proftpd</code> which does not include the
 <code>mod_ident</code> module, use:
 <pre>
-  ./configure --disable-ident ...
+  $ ./configure --disable-ident ...
 </pre>
 Note that the <code>mod_ident</code> module can also be built as a shared
 module, rather than be statically linked into <code>proftpd</code>:
 <pre>
-  ./configure --enable-dso --with-shared=mod_ident ...
+  $ ./configure --enable-dso --with-shared=mod_ident ...
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2008-01-05 01:22:20 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_ident</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>ident
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace ident:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2008 TJ Saunders<br>
+© Copyright 2008-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_lang.html b/doc/modules/mod_lang.html
index b07ba1c..0b1c30a 100644
--- a/doc/modules/mod_lang.html
+++ b/doc/modules/mod_lang.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_lang.html,v 1.12 2012-10-10 01:26:33 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_lang.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_lang</title>
@@ -30,25 +28,23 @@ be seen <a href="#Usage">here</a>.
 
 <p>
 The most current version of <code>mod_lang</code> can be found in the
-ProFTPD source distribution:
-<pre>
-  <a href="http://www.proftpd.org/">http://www.proftpd.org/</a>
-</pre>
+ProFTPD source distribution.
 
 <h2>Directives</h2>
 <ul>
   <li><a href="#LangDefault">LangDefault</a>
   <li><a href="#LangEngine">LangEngine</a>
+  <li><a href="#LangOptions">LangOptions</a>
   <li><a href="#LangPath">LangPath</a>
   <li><a href="#UseEncoding">UseEncoding</a>
 </ul>
 
 <p>
 <hr>
-<h2><a name="LangDefault">LangDefault</a></h2>
+<h3><a name="LangDefault">LangDefault</a></h3>
 <strong>Syntax:</strong> LangDefault <em>language</em><br>
 <strong>Default:</strong> LangDefault en_US<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_lang<br>
 <strong>Compatibility:</strong> 1.3.1rc1
 
@@ -71,10 +67,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="LangEngine">LangEngine</a></h2>
+<h3><a name="LangEngine">LangEngine</a></h3>
 <strong>Syntax:</strong> LangEngine <em>on|off</em><br>
 <strong>Default:</strong> LangEngine on<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_lang<br>
 <strong>Compatibility:</strong> 1.3.1rc1
 
@@ -93,10 +89,51 @@ and not <code>UseUTF8</code>, which controls the appearance of "UTF8".
 
 <p>
 <hr>
-<h2><a name="LangPath">LangPath</a></h2>
+<h3><a name="LangOptions">LangOptions</a></h3>
+<strong>Syntax:</strong> LangOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_lang<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>LangOptions</code> directive is used to configure various optional
+behavior of <code>mod_lang</code>.
+
+<p>
+Example:
+<pre>
+  LangOptions PreferServerEncoding
+</pre>
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>PreferServerEncoding</code><br>
+    <p>
+    This option will cause <code>mod_lang</code> to refuse any
+    <code>OPTS UTF8</code> commands used by clients; these commands are
+    used to change the server's handling of UTF8 encoded filenames.
+
+    <p>
+    <b>Note</b> that this option replaces the previous "strict" keyword
+    supported by the <code>UseEncoding</code> directive, in older versions
+    of <code>mod_lang</code>.
+
+  <p>
+  <li><code>RequireValidEncoding</code><br>
+    <p>
+    This option will cause <code>proftpd</code> to <b>reject</b> commands
+    on filenames if those filenames, as sent by the client, are not properly
+    encoded in the expected character set.
+</ul>
+
+<p>
+<hr>
+<h3><a name="LangPath">LangPath</a></h3>
 <strong>Syntax:</strong> LangPath <em>path</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config"<br>
+<strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_lang<br>
 <strong>Compatibility:</strong> 1.3.1rc1
 
@@ -124,10 +161,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="UseEncoding">UseEncoding</a></h2>
-<strong>Syntax:</strong> UseEncoding <em>on|off|local-charset client-charset ["strict"]</em><br>
+<h3><a name="UseEncoding">UseEncoding</a></h3>
+<strong>Syntax:</strong> UseEncoding <em>on|off|local-charset client-charset</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_lang<br>
 <strong>Compatibility:</strong> 1.3.2rc1
 
@@ -137,10 +174,7 @@ character sets should be used for encoding.  By default, the
 <code>mod_lang</code> module will automatically discover the local character
 set, and will use UTF-8 for the client character set.  The module will also
 allow the use of UTF-8 encoding to be changed by clients using the
-<code>OPTS UTF8</code> command (as per RFC 2640).  However, if the
-<code>UseEncoding</code> directive is explicitly used to indicate the character
-sets to use (or not use) <b>and</b> the <em>"strict"</em> keyword
-is used, then any OPTS UTF8 commands used by clients will be refused.
+<code>OPTS UTF8</code> command (as per RFC 2640).
 
 <p>
 For example, to disable all use of encoding, use the following in your
@@ -167,7 +201,8 @@ encoding to UTF-8 via the <code>OPTS UTF8</code> command.  If, however, you
 wished to prevent clients from changing the encoding to UTF-8, the above
 configuration would instead look like:
 <pre>
-  UseEncoding koi8-r cp1251 strict
+  LangOptions PreferServerEncoding
+  UseEncoding koi8-r cp1251
 </pre>
 
 <p>
@@ -183,9 +218,9 @@ The <code>mod_lang</code> module is distributed with ProFTPD.  To enable use
 of NLS (Natural Language Support) in your <code>proftpd</code> daemon, use the
 <code>--enable-nls</code> configure option:
 <pre>
-  ./configure --enable-nls
-  make
-  make install
+  $ ./configure --enable-nls
+  $ make
+  $ make install
 </pre>
 This option causes <code>mod_lang</code> to be compiled into
 <code>proftpd</code>.
@@ -204,11 +239,11 @@ mandates that the Telnet control codes be supported in FTP implementations.
 
 <p>
 The <code>mod_lang</code> module, however, can be used to deal with this
-situation.  <b><i>If</i></b> the <a href="#UseEncoding">UseEncoding</code></a>
-directive is used to translate between local and client character sets,
-<i>and</i> the client character set is one of the known Cyrillic character
-sets, then <code>proftpd</code> will disable support of the Telnet control
-codes.
+situation.  <b><i>If</i></b> the
+<a href="#UseEncoding"><code>UseEncoding</code></a> directive is used to
+translate between local and client character sets, <i>and</i> the client
+character set is one of the known Cyrillic character sets, then
+<code>proftpd</code> will disable support of the Telnet control codes.
 
 <p>
 To make a long explanation short, if you want to use Cyrillic characters
@@ -300,19 +335,12 @@ error like this:
 Both of these conditions <b>must</b> be true, otherwise you will see the
 "not a supported language" error.
 
-<p>
-<hr><br>
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2012-10-10 01:26:33 $</i><br>
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2006-2012 TJ Saunders<br>
+© Copyright 2006-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
 
-<hr><br>
-
+<hr>
 </body>
 </html>
-
diff --git a/doc/modules/mod_log.html b/doc/modules/mod_log.html
index b11def7..ee4c995 100644
--- a/doc/modules/mod_log.html
+++ b/doc/modules/mod_log.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_log.html,v 1.17 2014-01-02 16:43:54 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_log.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_log</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_log.c</code> file for
 ProFTPD 1.3.<i>x</i>, and is compiled by default.
 
@@ -27,7 +26,7 @@ ProFTPD 1.3.<i>x</i>, and is compiled by default.
 </ul>
 
 <hr>
-<h2><a name="AllowLogSymlinks">AllowLogSymlinks</a></h2>
+<h3><a name="AllowLogSymlinks">AllowLogSymlinks</a></h3>
 <strong>Syntax:</strong> AllowLogSymlinks <em>on|off</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -52,8 +51,8 @@ warned.</i>
 
 <p>
 <hr>
-<h2><a name="ExtendedLog">ExtendedLog</a></h2>
-<strong>Syntax:</strong> ExtendedLog <em>path [cmd-classes [format-nickname]]</em><br>
+<h3><a name="ExtendedLog">ExtendedLog</a></h3>
+<strong>Syntax:</strong> ExtendedLog <em>path [cmd-classes [format-name]]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
 <strong>Module:</strong> mod_log<br>
@@ -88,7 +87,7 @@ level at which to log the data.  For example:
 <p>
 This table shows the supported command classes:
 <p>
-<table border=1>
+<table border=1 summary="Command Classes">
   <tr>
     <td><b>Command Class</b></td>
     <td><b>FTP Commands</b></td>
@@ -164,7 +163,7 @@ This table shows the supported command classes:
 </table>
 
 <p>
-If a <em>format-nickname</em> parameter is used, <code>ExtendedLog</code> will
+If a <em>format-name</em> parameter is used, <code>ExtendedLog</code> will
 use the named <a href="#LogFormat"><code>LogFormat</code></a>. Otherwise, the
 default format of "%h %l %u %t \"%r\" %s %b" is used.
 
@@ -174,6 +173,12 @@ For example, to log all read and write operations to
 <pre>
   ExtendedLog /var/log/ftp.log READ,WRITE
 </pre>
+and to log all read and write operations to <code>/var/log/ftp.log</code>
+using your own <code>LogFormat</code> named "custom", use:
+<pre>
+  LogFormat custom ...
+  ExtendedLog /var/log/ftp.log READ,WRITE custom
+</pre>
 
 <p>
 See also: <a href="#AllowLogSymlinks"><code>AllowLogSymlinks</code></a>,
@@ -182,8 +187,8 @@ See also: <a href="#AllowLogSymlinks"><code>AllowLogSymlinks</code></a>,
 
 <p>
 <hr>
-<h2><a name="LogFormat">LogFormat</a></h2>
-<strong>Syntax:</strong> LogFormat <em>format-nickname format-string</em><br>
+<h3><a name="LogFormat">LogFormat</a></h3>
+<strong>Syntax:</strong> LogFormat <em>format-name format-string</em><br>
 <strong>Default:</strong> LogFormat default "%h %l %u %t \"%r\" %s %b"<br>
 <strong>Context:</strong> server config<br>
 <strong>Module:</strong> mod_log<br>
@@ -193,7 +198,7 @@ See also: <a href="#AllowLogSymlinks"><code>AllowLogSymlinks</code></a>,
 The <code>LogFormat</code> directive can be used to create a custom logging
 format for use with the <a href="#ExtendedLog"><code>ExtendedLog</code></a>
 directive.  Once created, the format can be referenced by the specified
-<em>format-nickname</em>. The <em>format-string</em> parameter can consist of
+<em>format-name</em>. The <em>format-string</em> parameter can consist of
 any combination of letters, numbers and symbols. The special character '%' is
 used to start a meta sequence/variable (see below). To insert a literal '%'
 character, use "%%".
@@ -210,7 +215,7 @@ The following meta sequences/variables are available and are replaced as
 indicated when logging.
 
 <p>
-<table border=1>
+<table border=1 summary="LogFormat Variables">
   <tr>
     <td><b>Variable</b></td>
     <td><b>Value</b></td>
@@ -283,6 +288,11 @@ indicated when logging.
   </tr>
 
   <tr>
+    <td> <code>%{file-size}</code> </td>
+    <td>Indicates the file size <b>after</b> data transfer, or "-" if not applicable</td>
+  </tr>
+
+  <tr>
     <td> <code>%{gid}</code> </td>
     <td>GID of authenticated user</td>
   </tr>
@@ -369,6 +379,16 @@ indicated when logging.
   </tr>
 
   <tr>
+    <td> <code>%R</code> </td>
+    <td>Response time, in milliseconds</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{remote-port}</code> </td>
+    <td>Remote client port</td>
+  </tr>
+
+  <tr>
     <td> <code>%s</code> </td>
     <td>Numeric FTP response code (status); see <a href="http://www.faqs.org/rfcs/rfc959.html">RFC 959</a> Section 4.2.1</td>
   </tr>
@@ -390,7 +410,7 @@ indicated when logging.
 
   <tr>
     <td> <code>%T</code> </td>
-    <td>Time taken to send/receive file, in seconds</td>
+    <td>Time taken to transfer file, in seconds</td>
   </tr>
 
   <tr>
@@ -399,11 +419,21 @@ indicated when logging.
   </tr>
 
   <tr>
+    <td> <code>%{transfer-millisecs}</code> </td>
+    <td>Time taken to transfer file, in milliseconds</td>
+  </tr>
+
+  <tr>
     <td> <code>%{transfer-status}</code> </td>
     <td>Status of data transfer: "success", "failed", "cancelled", "timeout", or "-"</td>
   </tr>
 
   <tr>
+    <td> <code>%{transfer-type}</code> </td>
+    <td>Data transfer type: "binary" or "ASCII" (if applicable), or "-"</td>
+  </tr>
+
+  <tr>
     <td> <code>%u</code> </td>
     <td>Authenticated local username</td>
   </tr>
@@ -445,7 +475,7 @@ See also: <a href="#ExtendedLog"><code>ExtendedLog</code></a>,
 
 <p>
 <hr>
-<h2><a name="ServerLog">ServerLog</a></h2>
+<h3><a name="ServerLog">ServerLog</a></h3>
 <strong>Syntax:</strong> ServerLog <em>path</em>|"none"<br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -464,7 +494,7 @@ this can be used to override a global <code>ServerLog</code> setting.
 
 <p>
 <hr>
-<h2><a name="SystemLog">SystemLog</a></h2>
+<h3><a name="SystemLog">SystemLog</a></h3>
 <strong>Syntax:</strong> SystemLog <em>path</em>|"none"<br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config<br>
@@ -477,7 +507,8 @@ The <code>SystemLog</code> directive disables <code>proftpd</code>'s use of the
 specified <em>path</em>.  The <em>path</em> should contain an absolute path,
 and should not be to a file in a nonexistent directory, in a world-writable
 directory, or be a symbolic link (unless
-<a href="#AllowLogSymlinks">AllowLogSymlinks</code></a> is set to <em>on</em>).
+<a href="#AllowLogSymlinks"><code>AllowLogSymlinks</code></a> is set to
+<em>on</em>).
 
 <p>
 Use of this directive overrides any facility set by the
@@ -493,19 +524,81 @@ A <em>path</em> value of "none" will disable logging for the entire daemon.
 The <code>mod_log</code> module is compiled by default.
 
 <p>
-<hr><br>
+<hr>
+<h2><a name="Usage">Usage</a></h2>
+<p>
 
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-02 16:43:54 $</i><br>
+<p><a name="FAQ">
+<b>Frequently Asked Questions</b><br>
 
-<br><hr>
+<p>
+<font color=red>Question</font>: I have configured ProFTPD to use DNS names
+in my <code>proftpd.conf</code> using:
+<pre>
+  UseReverseDNS on
+</pre>
+But in my <code>ExtendedLog</code>, I still see IP addresses rather than the
+DNS names I expect to see.  How can that happen?<br>
+<font color=blue>Answer</font>: The
+<a href="#LogFormat"><code>LogFormat</code></a> <code>%h</code> is what is used
+to log DNS names.  The logged value might be an IP address if ProFTPD cannot
+properly verify that the client IP address resolves to a DNS name.
+
+<p>
+"Properly verifying" an IP address, in this case, means resolving the DNS name
+for an IP address <em>and then resolving that DNS name back to its IP
+addresses</em>:
+<pre>
+  $ host 10.1.2.3
+  3.2.1.10.in-addr.arpa domain name pointer host.domain.example.com.
+  $ host host.domain.example.com
+  host.domain.example.com has address 10.4.5.6
+</pre>
+In this example, the IP address 10.1.2.3 does not resolve back to itself via
+DNS, but rather to a <i>different</i> IP address.
+
+<p>
+If the DNS name does not resolve back to the original IP address, then that
+DNS name is <b>not used</b>, as that DNS name is considered "unreliable"; only
+<em>reliable</em> information is logged (and used elsewhere).  Thus ProFTPD
+resorts to logging just the client IP address for the <code>%h</code> variable,
+rather than the DNS name, in these situations.
+
+<p>
+<font color=red>Question</font>: How can I get the reason a client was
+disconnected, for whatever reason, logged to my <code>ExtendedLog</code>?<br>
+<font color=blue>Answer</font>: You can use the <code>%E</code>
+<a href="#LogFormat"><code>LogFormat</code></a> variable for this, <b>in
+conjunction with</b> the <code>EXIT</code> log class.
+
+<p>
+For example, assume you have configured the following:
+<pre>
+  MaxConnectionsPerUser 2
+</pre>
+and you would like your <code>ExtendedLog</code> to record when this limit
+is reached.  To do this, you would use something like the following:
+<pre>
+  LogFormat eos "%a: user=%U disconnect_reason=\"%E\""
+  ExtendedLog /var/log/proftpd/ext.log EXIT eos
+</pre>
+Of course, you can include other logging classes than just <code>EXIT</code>;
+the above is just an example.
+
+<p>
+With the above, when the <code>MaxConnectionsPerUser</code> is reached,
+your log would have a line like:
+<pre>
+  127.0.0.1: user=tj disconnect_reason="Denied by MaxConnectionsPerUser"
+</pre>
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2002-2014<br>
+© Copyright 2002-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
 
-<hr><br>
-
+<hr>
 </body>
 </html>
diff --git a/doc/modules/mod_ls.html b/doc/modules/mod_ls.html
index 2c331e8..99caf7d 100644
--- a/doc/modules/mod_ls.html
+++ b/doc/modules/mod_ls.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_ls.html,v 1.2 2012-02-06 17:54:59 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_ls.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_ls</title>
@@ -29,24 +27,32 @@ The <code>mod_ls</code> module handles the <code>LIST</code>,
 </ul>
 
 <hr>
-<h2><a name="DirFakeGroup">DirFakeGroup</a></h2>
-<strong>Syntax:</strong> DirFakeGroup <em>off|on display-name</em><br>
+<h3><a name="DirFakeGroup">DirFakeGroup</a></h3>
+<strong>Syntax:</strong> DirFakeGroup <em>off|on [display-name]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
 <strong>Module:</strong> mod_ls<br>
 <strong>Compatibility:</strong> All versions
 
 <p>
-The <code>DirFakeGroup</code> directive
+The <code>DirFakeGroup</code> directive can be used to hide the true group
+ownership of files (including directories, FIFOs, <i>etc</i>) in directory
+listings.  If simply turned <em>on</em>, <code>DirFakeGroup</code> will display
+all files as being owned by group "ftp".  Optionally, the <em>display-name</em>
+parameter can be used to specify a group other than "ftp".  A
+<em>display-name</em> of "~" can be used as the parameter, in order to display
+the primary group name of the current user.
 
 <p>
-Examples:
-<pre>
-</pre>
+Both <code>DirFakeGroup</code> and
+<a href="#DirFakeUser"><code>DirFakeUser</code></a> are <b>completely
+cosmetic</b>; the <em>display-names</em> configured do <b>not</b> need to exist
+on the system, and neither directive affects permissions, real ownership or
+access control <em>in any way</em>.
 
 <p>
 <hr>
-<h2><a name="DirFakeMode">DirFakeMode</a></h2>
+<h3><a name="DirFakeMode">DirFakeMode</a></h3>
 <strong>Syntax:</strong> DirFakeMode <em>display-mode</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
@@ -54,7 +60,21 @@ Examples:
 <strong>Compatibility:</strong> All versions
 
 <p>
-The <code>DirFakeMode</code> directive
+The <code>DirFakeMode</code> directive configures the <em>mode</em> (or
+permissions) which will be displayed for <b>all</b> files and directories in
+directory listings.  For each subset of permissions (<i>i.e.</i> user, group,
+other), the "execute" permission for directories is added in listings if the
+"read" permission is specified by this directive.
+
+<p>
+As with <a href="#DirFakeUser"><code>DirFakeUser</code></a>, and
+<a href="#DirFakeGroup"><code>DirFakeGroup</code></a>, the "fake" permissions
+shown in directory listings are <b>cosmetic only</b>; they do not affect real
+permissions or access control <em>in any way</em> on the server.  Note,
+however, that <code>DirFakeMode</code> <i>can</i> affect the real permissions,
+for example, for FTP mirroring tools.  Such tools tend to create a mirror from
+what the tool sees (<i>e.g.</i> <code>DirFakeMode</code> permissions) on the
+source FTP server.
 
 <p>
 Examples:
@@ -65,64 +85,137 @@ Examples:
 
 <p>
 <hr>
-<h2><a name="DirFakeUser">DirFakeUser</a></h2>
-<strong>Syntax:</strong> DirFakeUser <em>off|on display-name</em><br>
+<h3><a name="DirFakeUser">DirFakeUser</a></h3>
+<strong>Syntax:</strong> DirFakeUser <em>off|on [display-name]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
 <strong>Module:</strong> mod_ls<br>
 <strong>Compatibility:</strong> All versions
 
 <p>
-The <code>DirFakeUser</code> directive
+The <code>DirFakeUser</code> directive can be used to hide the true user
+ownership of files (including directories, FIFOs, <i>etc</i>) in directory
+listings.  If simply turned <em>on</em>, <code>DirFakeUser</code> will display
+all files as being owned by user "ftp".  Optionally, the <em>display-name</em>
+parameter can be used to specify a user other than "ftp".  A
+<em>display-name</em> parameter of "~" can be used in order to display the name
+of the current user.
 
 <p>
-Examples:
-<pre>
-</pre>
+Both <a href="#DirFakeGroup"><code>DirFakeGroup</code></a> and
+<code>DirFakeUser</code> are <b>completely cosmetic</b>; the
+<em>display-names</em> specified do <b>not</b> need to exist on the system,
+and neither directive affects permissions, real ownership or access control
+<em>in any way</em>.
 
 <p>
 <hr>
-<h2><a name="ListOptions">ListOptions</a></h2>
-<strong>Syntax:</strong> ListOptions <em>options [strict [maxdepth depth] [maxfiles count] [maxdirs count] [LISTOnly] [NLSTOnly] [NoErrorIfAbsent]</em><br>
+<h3><a name="ListOptions">ListOptions</a></h3>
+<strong>Syntax:</strong> ListOptions <em>options [strict [maxdepth depth] [maxfiles count] [maxdirs count] [LISTOnly] [NLSTOnly] [NoErrorIfAbsent] [NoAdjustedSymlinks] [SortedNLST]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
 <strong>Module:</strong> mod_ls<br>
 <strong>Compatibility:</strong> 1.2.8rc1 and later
 
 <p>
-The <code>ListOptions</code> directive
+The <code>ListOptions</code> directive is used to configure various optional
+behavior of <code>mod_ls</code>.  <b>Note</b>: all of the configured
+<code>ListOptions</code> parameters <b>must</b> appear on the same line in the
+configuration; only the <i>first</i> <code>ListOptions</code> directive that
+appears in the configuration is used.
+
+<p>
+The currently supported <em>flags</em> are:
+<ul>
+  <li><code>LISTOnly</code><br>
+    <p>
+    This <em>flag</em> tells <code>mod_ls</code> to apply the
+    <code>ListOptions</code> configuration only to FTP <code>LIST</code>
+    commands, and not to <i>e.g.</i> <code>NLST</code>/<code>STAT</code>
+    commands.
+  </li>
+
+  <p>
+  <li><code>NLSTOnly</code><br>
+    <p>
+    This <em>flag</em> tells <code>mod_ls</code> to apply the
+    <code>ListOptions</code> configuration only to FTP <code>NLST</code>
+    commands, and not to <i>e.g.</i> <code>LIST</code>/<code>STAT</code>
+    commands.
+  </li>
+
+  <p>
+  <li><code>NoErrorIfAbsent</code><br>
+    <p>
+    This <em>flag</em> tells <code>mod_ls</code> to return the FTP 226
+    response code for <code>LIST</code>/<code>NLST</code> commands for
+    files/paths which do not exist, rather than returning the 450 error
+    code.
+  </li>
+
+  <p>
+  <li><code>NoAdjustedSymlinks</code><br>
+    <p>
+    By default, <code>mod_ls</code> tries to automatically adjust any
+    symlink destination paths when the FTP session is chrooted, so that
+    the adjusted symlinks work properly <i>e.g.</i> for FTP clients.
+
+    <p>
+    <b>Note</b> that this <em>flag</em> first appeared in
+    <code>proftpd-1.3.6rc2</code>.
+  </li>
+
+  <p>
+  <li><code>SortedNLST</code><br>
+    <p>
+    By default, <code>mod_ls</code> returns <code>NLST</code> results
+    in an <em>unordered</em> list, <i>i.e.</i> the sort order used by the
+    underlying filesystem and the <code>readdir(3)</code> library function.
+    Some FTP clients, however, may want/expect to have <code>NLST</code>
+    results sorted alphabetically.  Use this flag to achieve that sorted
+    <code>NLST</code> behavior.
+
+    <p>
+    <b>Note</b> that this <em>flag</em> first appeared in
+    <code>proftpd-1.3.6rc3</code>.
+  </li>
+</ul>
 
 <p>
 See also: <a href="../howto/ListOptions.html">ListOptions</a>
 
 <p>
 <hr>
-<h2><a name="ShowSymlinks">ShowSymlinks</a></h2>
+<h3><a name="ShowSymlinks">ShowSymlinks</a></h3>
 <strong>Syntax:</strong> ShowSymlinks <em>on|off</em><br>
-<strong>Default:</strong> None<br>
+<strong>Default:</strong> <code>ShowSymlinks on</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
 <strong>Module:</strong> mod_ls<br>
 <strong>Compatibility:</strong> All versions
 
 <p>
-The <code>ShowSymlinks</code> directive
-
-<p>
-Examples:
-<pre>
-</pre>
+The <code>ShowSymlinks</code> directive configures whether symbolic links are
+displayed as such in directory listings, or whether they are not displayed
+to the client.  If <code>ShowSymlinks</code> is <em>off</em>, then the linked
+file's permissions and ownership are used in the directory listing.
 
 <p>
 <hr>
-<h2><a name="UseGlobbing">UseGlobbing</a></h2>
+<h3><a name="UseGlobbing">UseGlobbing</a></h3>
 <strong>Syntax:</strong> UseGlobbing <em>on|off</em><br>
-<strong>Default:</strong> None<br>
+<strong>Default:</strong> <code>UseGlobbing on</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
 <strong>Module:</strong> mod_ls<br>
-<strong>Compatibility:</strong> All versions
+<strong>Compatibility:</strong> 1.2.5rc1 and later
+
+<p>
+The <code>UseGlobbing</code> directive controls the use of <code>glob(3)</code>
+functionality, which is needed for supporting wildcard characters such as "*"
+in directory listing requests from FTP clients.
 
 <p>
-The <code>UseGlobbing</code> directive
+The <code>glob(3)</code> functionality in FTP servers has been knowwn to
+cause security issues (see <a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2001-0249">CVE-2001-0249</a>), thus should be disabled when not needed.
 
 <p>
 Examples:
@@ -139,17 +232,11 @@ The <code>mod_ls</code>module is <b>always</b> installed.
 <p>
 <hr><br>
 
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2012-02-06 17:54:59 $</i><br>
-
-<br><hr>
-
 <font size=2><b><i>
-© Copyright 2000-2012 The ProFTPD Project<br>
+© Copyright 2000-2016 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
 
 <hr><br>
 </body>
 </html>
-
diff --git a/doc/modules/mod_memcache.html b/doc/modules/mod_memcache.html
index 9222716..d06fa43 100644
--- a/doc/modules/mod_memcache.html
+++ b/doc/modules/mod_memcache.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_memcache.html,v 1.7 2013-05-07 05:11:40 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_memcache.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_memcache</title>
@@ -31,10 +29,10 @@ that <code>libmemcached</code> version 0.41 or later is <b>required</b>.
 </ul>
 
 <hr>
-<h2><a name="MemcacheEngine">MemcacheEngine</a></h2>
+<h3><a name="MemcacheEngine">MemcacheEngine</a></h3>
 <strong>Syntax:</strong> MemcacheEngine <em>on|off</em><br>
 <strong>Default:</strong> MemcacheEngine off<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_memcache<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -45,10 +43,10 @@ support for the <code>proftpd</code> daemon.
 
 <p>
 <hr>
-<h2><a name="MemcacheLog">MemcacheLog</a></h2>
+<h3><a name="MemcacheLog">MemcacheLog</a></h3>
 <strong>Syntax:</strong> MemcacheLog <em>path|"none"</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_memcache<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -65,10 +63,10 @@ unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
 
 <p>
 <hr>
-<h2><a name="MemcacheOptions">MemcacheOptions</a></h2>
+<h3><a name="MemcacheOptions">MemcacheOptions</a></h3>
 <strong>Syntax:</strong> MemcacheOptions <em>opt1 ... optN</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_memcache<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -80,10 +78,10 @@ The currently supported <code>MemcacheOptions</code> are:
 
 <p>
 <hr>
-<h2><a name="MemcacheReplicas">MemcacheReplicas</a></h2>
+<h3><a name="MemcacheReplicas">MemcacheReplicas</a></h3>
 <strong>Syntax:</strong> MemcacheReplicas <em>count</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_memcache<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -92,10 +90,10 @@ The currently supported <code>MemcacheOptions</code> are:
 
 <p>
 <hr>
-<h2><a name="MemcacheServers">MemcacheServers</a></h2>
+<h3><a name="MemcacheServers">MemcacheServers</a></h3>
 <strong>Syntax:</strong> MemcacheServers <em>servers</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_memcache<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -116,10 +114,10 @@ Alternatively, you can configure a Unix domain socket path using <i>e.g.</i>:
 
 <p>
 <hr>
-<h2><a name="MemcacheTimeouts">MemcacheTimeouts</a></h2>
+<h3><a name="MemcacheTimeouts">MemcacheTimeouts</a></h3>
 <strong>Syntax:</strong> MemcacheTimeouts <em>connect read write</em><br>
 <strong>Default:</strong> None<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_memcache<br>
 <strong>Compatibility:</strong> 1.3.4rc2 and later
 
@@ -133,9 +131,9 @@ The <code>mod_memcache</code> module is distributed with ProFTPD.  To enable
 support and use of the memcache protocol in your <code>proftpd</code> daemon,
 use the <code>--enable-memcache</code> configure option:
 <pre>
-  # ./configure --enable-memcache ...
-  # make
-  # make install
+  $ ./configure --enable-memcache ...
+  $ make
+  $ make install
 </pre>
 This option causes the <code>mod_memcache</code> module to be compiled into
 <code>proftpd</code>.
@@ -144,25 +142,18 @@ This option causes the <code>mod_memcache</code> module to be compiled into
 You may also need to tell <code>configure</code> how to find the
 <code>libmemcached</code> header and library files:
 <pre>
-  # ./configure --enable-memcache \
+  $ ./configure --enable-memcache \
     --with-includes=<i>/path/to/libmemcached/include</i> \
     --with-libraries=<i>/path/to/libmemcached/lib</i>
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-05-07 05:11:40 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2011-2013 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
+<hr>
 
-<hr><br>
 </body>
 </html>
-
diff --git a/doc/modules/mod_redis.html b/doc/modules/mod_redis.html
new file mode 100644
index 0000000..c274d87
--- /dev/null
+++ b/doc/modules/mod_redis.html
@@ -0,0 +1,493 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>ProFTPD module mod_redis</title>
+</head>
+
+<body bgcolor=white>
+
+<hr>
+<center>
+<h2><b>ProFTPD module <code>mod_redis</code></b></h2>
+</center>
+<hr><br>
+
+<p>
+The <code>mod_redis</code> module enables ProFTPD support for caching data in
+<a href="https://redis.io">Redis</a> servers, using the
+<a href="https://github.com/redis/hiredis">hiredis</a> client library.
+
+<h2>Directives</h2>
+<ul>
+  <li><a href="#RedisEngine">RedisEngine</a>
+  <li><a href="#RedisLog">RedisLog</a>
+  <li><a href="#RedisLogOnCommand">RedisLogOnCommand</a>
+  <li><a href="#RedisServer">RedisServer</a>
+  <li><a href="#RedisTimeouts">RedisTimeouts</a>
+</ul>
+
+<hr>
+<h3><a name="RedisEngine">RedisEngine</a></h3>
+<strong>Syntax:</strong> RedisEngine <em>on|off</em><br>
+<strong>Default:</strong> RedisEngine off<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_redis<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>RedisEngine</code> directive enables or disables the
+<code>mod_redis</code> module, and thus the configuration of Redis support for
+the <code>proftpd</code> daemon.
+
+<p>
+<hr>
+<h3><a name="RedisLog">RedisLog</a></h3>
+<strong>Syntax:</strong> RedisLog <em>path|"none"</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_redis<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>RedisLog</code> directive is used to specify a log file for
+<code>mod_redis</code>'s reporting on a per-server basis.  The
+<em>file</em> parameter given must be the full path to the file to use for
+logging.
+
+<p>
+Note that this path must <b>not</b> be to a world-writable directory and,
+unless <code>AllowLogSymlinks</code> is explicitly set to <em>on</em>
+(generally a bad idea), the path must <b>not</b> be a symbolic link.
+
+<p>
+<hr>
+<h3><a name="RedisLogOnCommand">RedisLogOnCommand</a></h3>
+<strong>Syntax:</strong> RedisLogOnCommand <em>commands format-name</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_redis<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>RedisLogOnCommand</code> directive configures the use of Redis for
+<em>logging</em>.  Whenever one of the comma-separated list of <em>commands</em>
+occurs, <code>mod_redis</code> will compose a JSON object, using the
+<a href="mod_log.html#LogFormat"><code>LogFormat</code></a> named by
+<em>format-name</em> as a <i>template</i> for the fields to include in the
+JSON object.  The JSON object of that event will then be appended to a list
+stored in Redis, using <em>format-name</em> as the key name.  Multiple
+<code>RedisLogOnCommand</code> directives can be used, for different log formats
+for different events.
+
+<p>
+More on the use of Redis logging, including a table showing how
+<code>LogFormat</code> variables are mapped to JSON object keys can be found
+<a href="#Logging">here</a>.
+
+<p>
+Example:
+<pre>
+  LogFormat file-transfers "%h %l %u %t \"%r\" %s %b"
+  RedisLogOnCommand APPE,RETR,STOR,STOU file-transfers
+
+  LogFormat sessions "%{iso8601}"
+  RedisLogOnCommand CONNECT,DISCONNECT sessions
+</pre>
+
+<p>
+In addition to specific FTP commands, the <em>commands</em> list can specify
+"ALL", for logging on <b>all</b> commands.  Or it can <i>include</i> the
+"CONNECT" and "DISCONNECT" <i>commands</i>, which can be useful for logging the
+start and end times of a session.  <b>Note</b> that
+<code>RedisLogOnCommand</code> does <b>not</b> currently support the logging
+<i>classes</i> that the <code>ExtendedLog</code> directive supports.
+
+<p>
+<hr>
+<h3><a name="RedisServer">RedisServer</a></h3>
+<strong>Syntax:</strong> RedisServer <em>server [password]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_redis<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>RedisServer</code> directive is used to configure the IP address/port
+of the Redis server that the <code>mod_redis</code> module is to use.  For
+example:
+<pre>
+  RedisServer 1.2.3.4:6379
+</pre>
+or, for an IPv6 address, make sure the IPv6 address is enclosed in square
+brackets:
+<pre>
+  RedisServer [::ffff:1.2.3.4]:6379
+</pre>
+
+<p>
+Alternatively, you can configure a Unix domain socket path using <i>e.g.</i>:
+<pre>
+  RedisServer /var/run/redis.sock
+</pre>
+
+<p>
+An optional <em>password</em> parameter can be provided, for Redis servers
+which are password protected.
+
+<p>
+<hr>
+<h3><a name="RedisTimeouts">RedisTimeouts</a></h3>
+<strong>Syntax:</strong> RedisTimeouts <em>connect-millis io-millis</em><br>
+<strong>Default:</strong> RedisTimeouts 500 500<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_redis<br>
+<strong>Compatibility:</strong> 1.3.6rc5 and later
+
+<p>
+The <code>RedisTimeouts</code> directive configures timeouts to be used
+when communicating with the Redis server.  The <em>connect-millis</em>
+parameter specifies a timeout, in milliseconds, to use when first
+connecting to the Redis server.  The <em>io-millis</em> parameter specifies
+a timeout, in milliseconds, to use both when sending commands to Redis, and
+when reading responses.
+
+<p>
+The default is 500 milliseconds for both timeouts:
+<pre>
+  RedisTimeouts 500 500
+</pre>
+
+<p>
+<hr>
+<h2><a name="Installation">Installation</a></h2>
+The <code>mod_redis</code> module is distributed with ProFTPD.  To enable
+support and use of the Redis protocol in your <code>proftpd</code> daemon,
+use the <code>--enable-redis</code> configure option:
+<pre>
+  $ ./configure --enable-redis ...
+  $ make
+  $ make install
+</pre>
+This option causes the <code>mod_redis</code> module to be compiled into
+<code>proftpd</code>.
+
+<p>
+You may also need to tell <code>configure</code> how to find the
+<code>hiredis</code> header and library files:
+<pre>
+  $ ./configure --enable-redis \
+    --with-includes=<i>/path/to/hiredis/include</i> \
+    --with-libraries=<i>/path/to/hiredis/lib</i>
+</pre>
+
+<p>
+<hr>
+<h2><a name="Usage">Usage</a></h2>
+
+<p>
+Configuring Redis for use by other modules, <i>e.g.</i> <code>mod_ban</code>
+or <code>mod_tls_redis</code>:
+<pre>
+  <IfModule mod_redis.c>
+    RedisEngine on
+    RedisLog /var/log/ftpd/redis.log
+    RedisServer 127.0.0.1:6379
+  </IfModule>
+</pre>
+
+<p>
+This example shows the use of Redis logging for <em>all</em> commands:
+<pre>
+  <IfModule mod_redis.c>
+    RedisEngine on
+    RedisLog /var/log/ftpd/redis.log
+    RedisServer 127.0.0.1:6379
+
+    LogFormat redis "%h %l %u %t \"%r\" %s %b"
+    RedisLogOnCommand ALL redis
+  </IfModule>
+</pre>
+
+<p><a name="Logging"></a>
+<b>Redis Logging</b><br>
+When using Redis logging, the following table shows how <code>mod_redis</code>
+converts a <code>LogFormat</code> variable into the key names in the JSON
+logging objects:
+<table border=1 summary="Redis LogFormat Variables">
+  <tr>
+    <td><b><code>LogFormat</code> Variable</b></td>
+    <td><b>Key</b></td>
+  </tr>
+
+  <tr>
+    <td> <code>%A</code> </td>
+    <td>anon_password</td>
+  </tr>
+
+  <tr>
+    <td> <code>%a</code> </td>
+    <td>remote_ip</td>
+  </tr>
+
+  <tr>
+    <td> <code>%b</code> </td>
+    <td>bytes_sent</td>
+  </tr>
+
+  <tr>
+    <td> <code>%c</code> </td>
+    <td>connection_class</td>
+  </tr>
+
+  <tr>
+    <td> <code>%D</code> </td>
+    <td>dir_path</td>
+  </tr>
+
+  <tr>
+    <td> <code>%d</code> </td>
+    <td>dir_name</td>
+  </tr>
+
+  <tr>
+    <td> <code>%E</code> </td>
+    <td>session_end_reason</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{<em>name</em>}e</code> </td>
+    <td>ENV:<em>name</em></td>
+  </tr>
+
+  <tr>
+    <td> <code>%F</code> </td>
+    <td>transfer_path</td>
+  </tr>
+
+  <tr>
+    <td> <code>%f</code> </td>
+    <td>file</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{file-modified}</code> </td>
+    <td>file_modified</td>
+  </tr>
+
+  <tr>
+    <td> <code>%g</code> </td>
+    <td>group</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{gid}</code> </td>
+    <td>gid</td>
+  </tr>
+
+  <tr>
+    <td> <code>%H</code> </td>
+    <td>server_ip</td>
+  </tr>
+
+  <tr>
+    <td> <code>%h</code> </td>
+    <td>remote_dns</td>
+  </tr>
+
+  <tr>
+    <td> <code>%I</code> </td>
+    <td>session_bytes_rcvd</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{iso8601}</code> </td>
+    <td>timestamp</td>
+  </tr>
+
+  <tr>
+    <td> <code>%J</code> </td>
+    <td>command_params</td>
+  </tr>
+
+  <tr>
+    <td> <code>%L</code> </td>
+    <td>local_ip</td>
+  </tr>
+
+  <tr>
+    <td> <code>%l</code> </td>
+    <td>identd_user</td>
+  </tr>
+
+  <tr>
+    <td> <code>%m</code> </td>
+    <td>command</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{microsecs}</code> </td>
+    <td>microsecs</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{millisecs}</code> </td>
+    <td>millisecs</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{note:<em>name</em>}</code> </td>
+    <td>NOTE:<em>name</em></td>
+  </tr>
+
+  <tr>
+    <td> <code>%O</code> </td>
+    <td>session_bytes_sent</td>
+  </tr>
+
+  <tr>
+    <td> <code>%P</code> </td>
+    <td>pid</td>
+  </tr>
+
+  <tr>
+    <td> <code>%p</code> </td>
+    <td>local_port</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{protocol}</code> </td>
+    <td>protocol</td>
+  </tr>
+
+  <tr>
+    <td> <code>%r</code> </td>
+    <td>raw_command</td>
+  </tr>
+
+  <tr>
+    <td> <code>%S</code> </td>
+    <td>response_msg</td>
+  </tr>
+
+  <tr>
+    <td> <code>%s</code> </td>
+    <td>response_code</td>
+  </tr>
+
+  <tr>
+    <td> <code>%T</code> </td>
+    <td>transfer_secs</td>
+  </tr>
+
+  <tr>
+    <td> <code>%t</code> </td>
+    <td>local_time</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{transfer-failure}</code> </td>
+    <td>transfer_failure</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{transfer-status}</code> </td>
+    <td>transfer_status</td>
+  </tr>
+
+  <tr>
+    <td> <code>%U</code> </td>
+    <td>original_user</td>
+  </tr>
+
+  <tr>
+    <td> <code>%u</code> </td>
+    <td>user</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{uid}</code> </td>
+    <td>uid</td>
+  </tr>
+
+  <tr>
+    <td> <code>%V</code> </td>
+    <td>server_dns</td>
+  </tr>
+
+  <tr>
+    <td> <code>%v</code> </td>
+    <td>server_name</td>
+  </tr>
+
+  <tr>
+    <td> <code>%{version}</code> </td>
+    <td>server_version</td>
+  </tr>
+
+  <tr>
+    <td> <code>%w</code> </td>
+    <td>rename_from</td>
+  </tr>
+</table>
+
+<p>
+In addition to the standard <code>LogFormat</code> variables, the
+<code>mod_redis</code> module also adds a "connecting" key for events
+generated when a client first connects, and a "disconnecting" key for events
+generated when a client disconnects.  These keys can be used for determining
+the start/finish events for a given session.
+
+<p>
+Here is an example of the JSON-formatted records generated, using the above
+example configuration:
+<pre>
+  {"connecting":true,"timestamp":"2013-08-21 23:08:22,171"}
+  {"command":"USER","timestamp":"2013-08-21 23:08:22,278"}
+  {"user":"proftpd","command":"PASS","timestamp":"2013-08-21 23:08:22,305"}
+  {"user":"proftpd","command":"PASV","timestamp":"2013-08-21 23:08:22,317"}
+  {"user":"proftpd","command":"LIST","bytes_sent":432,"transfer_secs":4.211,"timestamp":"2013-08-21 23:08:22,329"}
+  {"user":"proftpd","command":"QUIT","timestamp":"2013-08-21 23:08:22,336"}
+  {"disconnecting":true,"user":"proftpd","timestamp":"2013-08-21 23:08:22,348"}
+</pre>
+Notice that for a given event, not <i>all</i> of the <code>LogFormat</code>
+variables are filled in.  If <code>mod_redis</code> determines that a given
+<code>LogFormat</code> variable has no value for the logged event, it will
+simply omit that variable from the JSON object.
+
+<p>
+Another thing to notice is that the generated JSON object ignores the textual
+delimiters configured by the <code>LogFormat</code> directive; all that
+matters are the <code>LogFormat</code> variables which appear in the directive.
+
+<p><a name="FAQ"></a>
+<b>Frequently Asked Questions</b><br>
+
+<p><a name="SQLLog"></a>
+<font color=red>Question</font>: How can I convert this SQL logging into the
+equivalent Redis logging?
+<pre>
+  SQLNamedQuery upload FREEFORM "INSERT INTO ftplogs ('userid', 'server_ip', 'transfer_date', 'operation', 'protocol', 'client_ip', 'transfer_time', 'bytes_transfer', 'file_hash_type', 'file_hash', 'file_path', 'transfer_status') VALUES ('%u', '%H', NOW(), '%r', '%{protocol}', '%a', '%T', '%b', '%{note:mod_digest.algo}', '%{note:mod_digest.digest}', '%f', '%{transfer-status}')"
+  SQLLog STOR upload
+</pre>
+<font color=blue>Answer</font>: Since the JSON object key names are hardcoded
+in <code>mod_redis</code>, converting the above <code>SQLNamedQuery</code>
+into a suitable/matching <code>LogFormat</code> is the necessary step.  Thus
+for example it might become:
+<pre>
+  LogFormat upload "%u %H %{YYYY-MM-DD HH:MM:SS}t %r %{protocol} %a %T %b %{note:mod_digest.algo} %{note:mod_digest.digest} %f %{transfer-status}"
+  RedisLogOnCommand STOR upload
+</pre>
+<b>Note</b> that <code>LogFormat</code> does not provide a <code>NOW()</code>
+function, unlike many SQL databases, thus the <code>%t</code> variable is
+needed to provide/fill in that timestamp.
+
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
+</body>
+</html>
diff --git a/doc/modules/mod_rlimit.html b/doc/modules/mod_rlimit.html
index 87191f3..70b5268 100644
--- a/doc/modules/mod_rlimit.html
+++ b/doc/modules/mod_rlimit.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_rlimit.html,v 1.4 2014-01-31 17:08:10 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_rlimit.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_rlimit</title>
@@ -48,10 +46,10 @@ ProFTPD source distribution:
 
 <p>
 <hr>
-<h2><a name="RLimitChroot">RLimitChroot</a></h2>
+<h3><a name="RLimitChroot">RLimitChroot</a></h3>
 <strong>Syntax:</strong> RLimitChroot <em>on|off</em><br>
-<strong>Default:</strong> <em>on</em><br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Default:</strong> RLimitChroot on<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_rlimit<br>
 <strong>Compatibility:</strong> 1.3.5rc5
 
@@ -88,11 +86,19 @@ be logged (at debug level 2):
 </pre>
 
 <p>
+The <code>RLimitChroot</code> directive is <b>not</b> intended to <b>prevent</b>
+"Roaring Beast" style attacks entirely; the guarded <code>/etc</code> and
+<code>/lib</code> directories might be created via other means, outside of
+ProFTPD, which would also allow for the attack.  The <code>RLimitChroot</code>
+directive is meant to <em>mitigate</em> (not <em>prevent</em>) the attacks by
+making sure it cannot be done using <i>just</i> ProFTPD.
+
+<p>
 <hr>
-<h2><a name="RLimitCPU">RLimitCPU</a></h2>
+<h3><a name="RLimitCPU">RLimitCPU</a></h3>
 <strong>Syntax:</strong> RLimitCPU <em>[scope] soft-limit|"max" [hard-limit|"max"]</em><br>
-<strong>Default:</strong> System defaults<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Default:</strong> <em>System defaults</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_rlimit<br>
 <strong>Compatibility:</strong> 1.3.5rc2
 
@@ -128,10 +134,10 @@ Example:
 
 <p>
 <hr>
-<h2><a name="RLimitMemory">RLimitMemory</a></h2>
+<h3><a name="RLimitMemory">RLimitMemory</a></h3>
 <strong>Syntax:</strong> RLimitMemory <em>[scope] soft-limit|"max" [hard-limit|"max"]</em><br>
-<strong>Default:</strong> System defaults<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Default:</strong> <em>System defaults</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_rlimit<br>
 <strong>Compatibility:</strong> 1.3.5rc2
 
@@ -165,11 +171,33 @@ Example:
 </pre>
 
 <p>
+<b>Note</b>: If you use <code>RLimitMemory</code>, <i>e.g.</i>:
+<pre>
+  <IfModule mod_rlimit.c>
+    RLimitMemory session 64M
+  </IfModule>
+</pre>
+<b>and</b> you use <a href="../contrib/mod_tls.html"><code>mod_tls</code></a>
+for FTPS transfers:
+<pre>
+  <IfModule mod_tls.c>
+    ...
+  </IfModule>
+</pre>
+then your transfers are likely to fail.  Why?  Because OpenSSL will need to
+allocate memory for the TLS support, in addition to the memory that ProFTPD
+already allocates for data transfers.  Depending on the specific ciphersuites
+negotiated, and the specific memory limit configured, you are very likely to
+hit the <code>RLimitMemory</code> limit.  In short, your
+<code>RLimitMemory</code> might be too low, and not allowing ProFTPD and
+OpenSSL enough memory for the transfer.
+
+<p>
 <hr>
-<h2><a name="RLimitOpenFiles">RLimitOpenFiles</a></h2>
+<h3><a name="RLimitOpenFiles">RLimitOpenFiles</a></h3>
 <strong>Syntax:</strong> RLimitOpenFiles <em>[scope] soft-limit|"max" [hard-limit|"max"]</em><br>
-<strong>Default:</strong> System defaults<br>
-<strong>Context:</strong> "server config", <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Default:</strong> <em>System defaults</em><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
 <strong>Module:</strong> mod_rlimit<br>
 <strong>Compatibility:</strong> 1.3.5rc2
 
@@ -199,7 +227,7 @@ used.
 Example:
 <pre>
   # Limit a given session to 12 open file descriptors
-  RLimitMemory session 12
+  RLimitOpenFiles session 12
 </pre>
 
 <p>
@@ -208,21 +236,31 @@ Example:
 The <code>mod_rlimit</code> module is compiled into <code>proftpd</code> by
 default.
 
-<p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2014-01-31 17:08:10 $</i><br>
+<p><a name="FAQ">FAQ</a>
+<b>Frequently Asked Questions</b><br>
 
-<br><hr>
+<p>
+<font color=red>Question</font>: Why can't I create directories named "lib"
+or "etc" in the root directory?  For example, my FTP client fails like so:
+<code>
+  <font color=blue>Command:	MKD lib</font>
+  <font color=green>Response:	550 lib: Permission denied</font>
+  <font color=blue>Command:	MKD /lib</font>
+  <font color=green>Response:	550 /lib: Permission denied</font>
+</code>
+Although I don't have anything in my <code>proftpd.conf</code> that would block
+these commands, and the filesystem permissions are OK.  Why does this
+happen?<br>
+<font color=blue>Answer</font>: For the answer to this, see the description for
+the <a href="#RLimitChroot"><code>RLimitChroot</code></a> directive.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2013-2014 TJ Saunders<br>
+© Copyright 2013-2017 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/modules/mod_site.html b/doc/modules/mod_site.html
index 6c2a560..b7eddde 100644
--- a/doc/modules/mod_site.html
+++ b/doc/modules/mod_site.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_site.html,v 1.1 2011-02-20 20:06:10 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_site.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_site</title>
@@ -14,6 +12,7 @@
 </center>
 <hr><br>
 
+<p>
 This module is contained in the <code>mod_site.c</code> file for
 ProFTPD 1.3.<i>x</i>, and is compiled by default.
 
@@ -41,19 +40,12 @@ Example configurations:
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2011-02-20 20:06:10 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2002-2011<br>
+© Copyright 2002-2011 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/modules/mod_xfer.html b/doc/modules/mod_xfer.html
index 19179fa..447b17d 100644
--- a/doc/modules/mod_xfer.html
+++ b/doc/modules/mod_xfer.html
@@ -1,6 +1,4 @@
-<!-- $Id: mod_xfer.html,v 1.10 2013-03-13 18:08:28 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/modules/mod_xfer.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD module mod_xfer</title>
@@ -23,6 +21,7 @@ file transfers.
   <li><a href="#AllowOverwrite">AllowOverwrite</a>
   <li><a href="#AllowRetrieveRestart">AllowRetrieveRestart</a>
   <li><a href="#AllowStoreRestart">AllowStoreRestart</a>
+  <li><a href="#DefaultTransferMode">DefaultTransferMode</a>
   <li><a href="#DeleteAbortedStores">DeleteAbortedStores</a>
   <li><a href="#DisplayFileTransfer">DisplayFileTransfer</a>
   <li><a href="#HiddenStores">HiddenStores</a>
@@ -33,6 +32,7 @@ file transfers.
   <li><a href="#StoreUniquePrefix">StoreUniquePrefix</a>
   <li><a href="#TimeoutNoTransfer">TimeoutNoTransfer</a>
   <li><a href="#TimeoutStalled">TimeoutStalled</a>
+  <li><a href="#TransferOptions">TransferOptions</a>
   <li><a href="#TransferPriority">TransferPriority</a>
   <li><a href="#TransferRate">TransferRate</a>
   <li><a href="#UseSendfile">UseSendfile</a>
@@ -40,7 +40,7 @@ file transfers.
 
 <p>
 <hr>
-<h2><a name="AllowOverwrite">AllowOverwrite</a></h2>
+<h3><a name="AllowOverwrite">AllowOverwrite</a></h3>
 <strong>Syntax:</strong> AllowOverwrite <em>on|off</em><br>
 <strong>Default:</strong> <code>AllowOverwrite off</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
@@ -54,7 +54,7 @@ existing files.
 
 <p>
 <hr>
-<h2><a name="AllowRetrieveRestart">AllowRetrieveRestart</a></h2>
+<h3><a name="AllowRetrieveRestart">AllowRetrieveRestart</a></h3>
 <strong>Syntax:</strong> AllowRetrieveRestart <em>on|off</em><br>
 <strong>Default:</strong> <code>AllowRetrieveRestart on</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
@@ -73,7 +73,7 @@ See also: <a href="#AllowStoreRestart"><code>AllowStoreRestart</code></a>
 
 <p>
 <hr>
-<h2><a name="AllowStoreRestart">AllowStoreRestart</a></h2>
+<h3><a name="AllowStoreRestart">AllowStoreRestart</a></h3>
 <strong>Syntax:</strong> AllowStoreRestart <em>on|off</em><br>
 <strong>Default:</strong> <code>AllowStoreRestart on</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
@@ -100,7 +100,24 @@ See also: <a href="#AllowRetrieveRestart"><code>AllowRetrieveRestart</code></a>,
 
 <p>
 <hr>
-<h2><a name="DeleteAbortedStores">DeleteAbortedStores</a></h2>
+<h3><a name="DefaultTransferMode">DefaultTransferMode</a></h3>
+<strong>Syntax:</strong> DefaultTransferMode <em>ascii|binary</em><br>
+<strong>Default:</strong> <code>DefaultTransferMode ascii</code><br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_xfer<br>
+<strong>Compatibility:</strong> 1.2.0pre9 and later
+
+<p>
+The <code>DefaultTransferMode</code> directive sets the <em>default</em>
+transfer mode used for data transfers.  Per RFC 959 requirements, the default
+transfer mode is "ascii", which means that carriage return (<code>CR</code>)
+and line feed (<code>LF</code>) translation will be performed: <code>CRLF</code>
+sequences in <em>uploaded</em> data will be translated to <code>LF</code>,
+and <code>LF</code> translated to <code>CRLF</code> in <em>downloaded</em> data.
+
+<p>
+<hr>
+<h3><a name="DeleteAbortedStores">DeleteAbortedStores</a></h3>
 <strong>Syntax:</strong> DeleteAbortedStores <em>on|off</em><br>
 <strong>Default:</strong> <code>DeleteAbortedStores off</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
@@ -111,16 +128,18 @@ See also: <a href="#AllowRetrieveRestart"><code>AllowRetrieveRestart</code></a>,
 The <code>DeleteAbortedStores</code> directive controls whether ProFTPD
 deletes partially uploaded files if the transfer is stopped via the
 FTP <code>ABOR</code> command (as opposed to a connection failure).
-By default, <code>DeleteAbortedStores</code> is <em>off</em>; however when
-<code>HiddenStores</code> is enabled, then <code>DeleteAbortedStores</code>
-is automatically enabled as well.
+By default, <code>DeleteAbortedStores</code> is <em>off</em>.
+
+<p>
+<b>However</b>, when <code>HiddenStores</code> is enabled, then
+<code>DeleteAbortedStores</code> is automatically enabled as well.
 
 <p>
 See also: <a href="#HiddenStores"><code>HiddenStores</code></a>
 
 <p>
 <hr>
-<h2><a name="HiddenStores">HiddenStores</a></h2>
+<h3><a name="HiddenStores">HiddenStores</a></h3>
 <strong>Syntax:</strong> HiddenStores <em>on|off|prefix [suffix]</em><br>
 <strong>Default:</strong> <code>HiddenStores off</code><br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code><br>
@@ -169,6 +188,17 @@ a suffix:
 prefix as well.
 
 <p>
+In <code>proftpd-1.3.6rc1</code> and later, the <em>prefix</em> and
+<em>suffix</em> values can use the <code>%P</code> variable, which will
+be substituted with the session PID.  This can help to reduce issues
+encountered when an FTP upload fails in such a way that proftpd cannot properly
+clean up the <code>HiddenStores</code> temporary file.  For example:
+<pre>
+  # Use the session PID as part of the name
+  HiddenStores .in. .%P
+</pre>
+
+<p>
 <b><i>Discussion</i></b><br>
 When would you want or need to specify different prefix and suffix values
 for <code>HiddenStores</code>?  You might need this when, for example, your
@@ -188,7 +218,7 @@ See also: <a href="#AllowStoreRestart"><code>AllowStoreRestart</code></a>,
 
 <p>
 <hr>
-<h2><a name="MaxRetrieveFileSize">MaxRetrieveFileSize</a></h2>
+<h3><a name="MaxRetrieveFileSize">MaxRetrieveFileSize</a></h3>
 <strong>Syntax:</strong> MaxRetrieveFileSize <em>[number|"*" units ["user"|"group"|"class" expression]]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
@@ -236,7 +266,7 @@ See also: <a href="#MaxStoreFileSize"><code>MaxStoreFileSize</code></a>
 
 <p>
 <hr>
-<h2><a name="MaxStoreFileSize">MaxStoreFileSize</a></h2>
+<h3><a name="MaxStoreFileSize">MaxStoreFileSize</a></h3>
 <strong>Syntax:</strong> MaxStoreFileSize <em>[number|"*" units ["user"|"group"|"class" expression]]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
@@ -292,7 +322,101 @@ See also: <a href="#MaxRetrieveFileSize"><code>MaxRetrieveFileSize</code></a>
 
 <p>
 <hr>
-<h2><a name="TimeoutNoTransfer">TimeoutNoTransfer</a></h2>
+<h3><a name="MaxTransfersPerHost">MaxTransfersPerHost</a></h3>
+<strong>Syntax:</strong> MaxTransfersPerHost <em>cmd-list count [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
+<strong>Module:</strong> mod_xfer<br>
+<strong>Compatibility:</strong> 1.3.2rc1 and later
+
+<p>
+The <code>MaxTransfersPerHost</code> directive limits the number of data
+transfers happening at the same time <i>from the same host</i>.  The
+<em>cmd-list</em> parameter is a comma-separated list of the data transfer FTP
+commands (APPE, RETR, STOR, and/or STOU) to which the limit applies.  The
+optional <em>message</em> parameter may be used, which will be displayed to a
+client attempting to exceed the maximum value.  If <em>message</em> is
+<i>not</i> supplied, the following message is used by default:
+<pre>
+  "Sorry, the maximum number of data transfers (%m) from your host are currently being used."
+</pre>
+
+<p>
+For example:
+<pre>
+  MaxTransfersPerHost RETR 2
+</pre>
+will result in the following FTP response to a client exceeding the download
+limit:
+<pre>
+  "451 Sorry, the maximum number of data transfers (2) from your host are currently being used."
+</pre>
+And for uploads, you might using something like:
+<pre>
+  MaxTransferPerHost APPE,STOR,STOU 1
+</pre>
+
+<p>
+See also: <a href="#MaxTransfersPerUser"><code>MaxTransfersPerUser</code></a>
+
+<p>
+<hr>
+<h3><a name="MaxTransfersPerUser">MaxTransfersPerUser</a></h3>
+<strong>Syntax:</strong> MaxTransfersPerUser <em>cmd-list count [message]</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
+<strong>Module:</strong> mod_xfer<br>
+<strong>Compatibility:</strong> 1.3.2rc1 and later
+
+<p>
+The <code>MaxTransfersPerUser</code> directive limits the number of data
+transfers happening at the same time <i>for the same user name</i>.  The
+<em>cmd-list</em> parameter is a comma-separated list of the data transfer FTP
+commands (APPE, RETR, STOR, and/or STOU) to which the limit applies.  The
+optional <em>message</em> parameter may be used, which will be displayed to a
+client attempting to exceed the maximum value.  If <em>message</em> is
+<i>not</i> supplied, the following message is used by default:
+<pre>
+  "Sorry, the maximum number of data transfers (%m) from this user are currently being used."
+</pre>
+
+<p>
+For example:
+<pre>
+  MaxTransfersPerUser RETR 2
+</pre>
+will result in the following FTP response to a client exceeding the download
+limit:
+<pre>
+  "451 Sorry, the maximum number of data transfers (2) from this user are currently being used."
+</pre>
+And for uploads, you might using something like:
+<pre>
+  MaxTransferPerUser APPE,STOR,STOU 1
+</pre>
+
+<p>
+See also: <a href="#MaxTransfersPerHost"><code>MaxTransfersPerHost</code></a>
+
+<p>
+<hr>
+<h3><a name="StoreUniquePrefix">StoreUniquePrefix</a></h3>
+<strong>Syntax:</strong> StoreUniquePrefix <em>prefix</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, <code>.ftpaccess</code><br>
+<strong>Module:</strong> mod_xfer<br>
+<strong>Compatibility:</strong> 1.2.6rc1 and later
+
+<p>
+The <code>StoreUniquePrefix</code> directive is used to configure a
+<em>prefix</em> for the generated unique random filenames used for the STOU
+FTP command.  The last six characters of the filename will be random.
+<b>Note</b>: Slash (/) characters are <b>not</b> allowed in the <em>prefix</em>
+value.
+
+<p>
+<hr>
+<h3><a name="TimeoutNoTransfer">TimeoutNoTransfer</a></h3>
 <strong>Syntax:</strong> TimeoutNoTransfer <em>seconds</em><br>
 <strong>Default:</strong> 300 seconds<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -313,7 +437,7 @@ See also: <a href="mod_core.html#TimeoutIdle"><code>TimeoutIdle</code></a>,
 
 <p>
 <hr>
-<h2><a name="TimeoutStalled">TimeoutStalled</a></h2>
+<h3><a name="TimeoutStalled">TimeoutStalled</a></h3>
 <strong>Syntax:</strong> TimeoutStalled <em>seconds</em><br>
 <strong>Default:</strong> 3600 seconds<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
@@ -330,7 +454,37 @@ The maximum allowed <em>seconds</em> value is 65535 (108 minutes).
 
 <p>
 <hr>
-<h2><a name="TransferPriority">TransferPriority</a></h2>
+<h3><a name="TransferOptions">TransferOptions</a></h3>
+<strong>Syntax:</strong> TransferOptions <em>opt1 ...</em><br>
+<strong>Default:</strong> None<br>
+<strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code><br>
+<strong>Module:</strong> mod_xfer<br>
+<strong>Compatibility:</strong> 1.3.6rc1 and later
+
+<p>
+The <code>TransferOptions</code> directive to configure various optional data
+transfer behaviors.
+
+<p>
+The currently implemented options are:
+<ul>
+  <li><code>IgnoreASCII</code><br>
+    <p>
+    This option causes proftpd to silently ignore any client requests to
+    perform ASCII translations via the <code>TYPE</code> command.  That is,
+    FTP clients can request ASCII translations, and proftpd will respond
+    as the client expects, but will <b>not</b> actually perform the translation
+    for either uploads <i>or</i> downloads.  This behavior can be useful in
+    circumstances involving older/mainframe clients and EBCDIC files.
+
+    <p>
+    <b>Note</b> that this option first appeared in 
+    <code>proftpd-1.3.6rc1</code>.
+</ul>
+
+<p>
+<hr>
+<h3><a name="TransferPriority">TransferPriority</a></h3>
 <strong>Syntax:</strong> TransferPriority <em>cmd-list "low"|"medium"|"high"|number</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code><br>
@@ -364,7 +518,7 @@ fine:
 
 <p>
 <hr>
-<h2><a name="TransferRate">TransferRate</a></h2>
+<h3><a name="TransferRate">TransferRate</a></h3>
 <strong>Syntax:</strong> TransferRate <em>cmd-list kbytes-per-sec[:free-bytes]</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
@@ -412,7 +566,7 @@ Here are some examples:
 
 <p>
 <hr>
-<h2><a name="UseSendfile">UseSendfile</a></h2>
+<h3><a name="UseSendfile">UseSendfile</a></h3>
 <strong>Syntax:</strong> UseSendfile <em>on|off|len units|percentage</em><br>
 <strong>Default:</strong> None<br>
 <strong>Context:</strong> server config, <code><VirtualHost></code>, <code><Global></code>, <code><Anonymous></code>, <code><Directory></code>, .ftpaccess<br>
@@ -432,19 +586,27 @@ operations, and buffer allocations.  Read this
 The <code>mod_xfer</code>module is <b>always</b> installed.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2013-03-13 18:08:28 $</i><br>
-
-<br><hr>
+<b>Logging</b><br>
+The <code>mod_xfer</code> module supports <a href="../howto/Tracing.html">trace logging</a>, via the module-specific log channels:
+<ul>
+  <li>xfer
+</ul>
+Thus for trace logging, to aid in debugging, you would use the following in
+your <code>proftpd.conf</code>:
+<pre>
+  TraceLog /path/to/ftpd/trace.log
+  Trace xfer:20
+</pre>
+This trace logging can generate large files; it is intended for debugging use
+only, and should be removed from any production configuration.
 
+<p>
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2013 The ProFTPD Project<br>
+© Copyright 2000-2016 The ProFTPD Project<br>
  All Rights Reserved<br>
 </i></b></font>
+<hr>
 
-<hr><br>
 </body>
 </html>
-
diff --git a/doc/utils/ftpasswd.html b/doc/utils/ftpasswd.html
index b93b256..0a948aa 100644
--- a/doc/utils/ftpasswd.html
+++ b/doc/utils/ftpasswd.html
@@ -1,6 +1,4 @@
-<!-- $Id: ftpasswd.html,v 1.2 2011-03-03 22:17:29 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/utils/ftpasswd.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ftpasswd: tool for ProFTPD's AuthUserFile, AuthGroupFile, UserPassword </title>
@@ -8,12 +6,13 @@
 
 <body bgcolor=white>
 
-<hr><br>
+<hr>
 <center>
 <h2><b><code>ftpasswd</code>: tool for ProFTPD's <code>AuthUserFile</code>, <code>AuthGroupFile</code>, <code>UserPassword</code></b></h2>
 </center>
-<hr><br>
+<hr>
 
+<p>
 This program is used to create and manage files, correctly formatted, suitable
 for use with ProFTPD's <a href="../modules/mod_auth_file.html#AuthUserFile"><code>AuthUserFile</code></a> and <a href="../modules/mod_auth_file.html#AuthGroupFile"><code>AuthGroupFile</code></a> configuration directives.  It can also
 generate password hashes for ProFTPD's <a href="../modules/mod_auth.html#UserPassword"><code>UserPassword</code></a> directive.
@@ -67,20 +66,20 @@ include <code>--gid</code> (defaults to the given <code>--uid</code> argument
 when not provided) and <code>--gecos</code> (not used by <code>proftpd</code>
 at all).  For example:
 <pre>
-  ftpasswd --passwd --name=bob --uid=1001 --home=/home/bob --shell=/bin/false
+  $ ftpasswd --passwd --name=bob --uid=1001 --home=/home/bob --shell=/bin/false
 </pre>
 creates an account for user <code>bob</code>.  To create a file with a name or
 location other than the default (which, for <code>--passwd</code> mode is
 <code>ftpd.passwd</code>), use the <code>--file</code> option.  For example, to create the alternate password file in <code>/usr/local/etc/ftpd/passwd</code>:
 <pre>
-  ftpasswd --passwd --file=/usr/local/etc/ftpd/passwd --name=bob --uid=1001 --home=/home/bob \
+  $ ftpasswd --passwd --file=/usr/local/etc/ftpd/passwd --name=bob --uid=1001 --home=/home/bob \
     --shell=/bin/false
 </pre>
 
 <p>
 For <code>AuthGroupFile</code>s, use <code>--group</code>:
 <pre>
-  ftpasswd --group --name=<i>group-name</i> --gid=<i>group-id</i> --member=<i>user-member1</i>  \
+  $ ftpasswd --group --name=<i>group-name</i> --gid=<i>group-id</i> --member=<i>user-member1</i>  \
     --member=<i>user-member2</i> ... --member=<i>user-memberN</i>
 </pre>
 
@@ -89,7 +88,7 @@ The most common change to these files is made to <code>AuthUserFile</code>s, to
 change a user's password.  The <code>--change-password</code> option was
 provided just for this scenario:
 <pre>
-  ftpasswd --passwd --name=<i>user</i> --change-password
+  $ ftpasswd --passwd --name=<i>user</i> --change-password
 </pre>
 
 <p>
@@ -100,7 +99,7 @@ One could generate a file using <code>--passwd</code> and then extract the
 password hash from the file.  Easier, though, is to use <code>ftpasswd</code>'s
 <code>--hash</code> option:
 <pre>
-  ftpasswd --hash
+  $ ftpasswd --hash
 </pre>
 The password will either be prompted for, or it can be given on standard in
 using <code>--stdin</code>.
@@ -123,7 +122,7 @@ user/group, password matched system password and the
 option is <code>--stdin</code>: this allows scripts to provide a password to
 <code>ftpasswd</code> without prompting for a password.  For example:
 <pre>
-  echo <i>passwd-variable</i> | ftpasswd <i>opts</i> --stdin
+  $ echo <i>passwd-variable</i> | ftpasswd <i>opts</i> --stdin
 </pre>
 Note that the <code>--stdin</code> option does <b>not</b> allow passwords to
 be passed to the script on the command line, but on <code>stdin</code>.  This
@@ -211,6 +210,21 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
                 requests that a new password be given if the entered password
                 is the same as the current password.
 
+    --delete-user
+
+                Remove the entry for the given user name from the file.
+
+    -l          Lock the password of the named account.  This option disables a
+    --lock      password by changing it to a value which matches no possible
+                encrypted value (it adds a '!' at the beginning of the
+                password).
+
+    --not-previous-password
+
+                Double-checks the given password against the previous password
+                for the user, and requests that a new password be given if
+                the entered password is the same as the previous password.
+
     --not-system-password
 
                 Double-checks the given password against the system password
@@ -219,11 +233,19 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
                 helps to enforce different passwords for different types of
                 access.
 
+    --sha256    Use the SHA-256 algorithm for encrypting passwords.
+
+    --sha512    Use the SHA-512 algorithm for encrypting passwords.
+
     --stdin
                 Read the password directly from standard in rather than
                 prompting for it.  This is useful for writing scripts that
                 automate use of ftpasswd.
 
+    -u          Unlock the password of the named account.  This option
+    --unlock    re-enables a password by changing the password back to its
+                previous value (to the value before using the -l option).
+
     --use-cracklib
                 Causes ftpasswd to use Alec Muffet's cracklib routines in
                 order to determine and prevent the use of bad or weak
@@ -242,6 +264,10 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
 
   Options:
 
+    --delete-group
+
+                Remove the entry for the given group name from the file.
+
     --enable-group-passwd
 
                 Prompt for a group password.  This is disabled by default,
@@ -272,6 +298,10 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
                 the specified output-file, an entry will be created for them.
                 Otherwise, the given fields will be updated.
 
+    --sha256    Use the SHA-256 algorithm for encrypting passwords.
+
+    --sha512    Use the SHA-512 algorithm for encrypting passwords.
+
     --stdin
                 Read the password directly from standard in rather than
                 prompting for it.  This is useful for writing scripts that
@@ -300,6 +330,10 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
     --md5       Use the MD5 algorithm for encrypting passwords.  This is the
                 default.
 
+    --sha256    Use the SHA-256 algorithm for encrypting passwords.
+
+    --sha512    Use the SHA-512 algorithm for encrypting passwords.
+
     --stdin
                 Read the password directly from standard in rather than
                 prompting for it.  This is useful for writing scripts that
@@ -315,18 +349,11 @@ usage: ftpasswd [--help] [--hash|--group|--passwd]
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2011-03-03 22:17:29 $</i><br>
-
 <br><hr>
-
 <font size=2><b><i>
-© Copyright 2000-2011 TJ Saunders<br>
+© Copyright 2000-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
 <hr><br>
 
 </body>
diff --git a/doc/utils/ftpcount.html b/doc/utils/ftpcount.html
index fc0f32b..d6a0ea0 100644
--- a/doc/utils/ftpcount.html
+++ b/doc/utils/ftpcount.html
@@ -1,4 +1,4 @@
-
+<!DOCTYPE html>
 <HTML><HEAD><TITLE>Manpage of ftpcount</TITLE>
 </HEAD><BODY>
 
@@ -50,20 +50,24 @@ ProFTPD is written and maintained by a number of people, full credits
 can be found on <A HREF="http://www.proftpd.org/credits.html">http://www.proftpd.org/credits.html</A>
 
 <H2>SEE ALSO</H2>
-
-inetd(8), ftp(1), proftpd(8), ftpwho(1), ftpshut(8)</B>
+inetd(8), ftp(1), proftpd(8), ftpwho(1), ftpshut(8)
 
 <P>
-
 Full documentation on ProFTPD, including configuration and FAQs is available at
 <A HREF="http://www.proftpd.org/.">http://www.proftpd.org/.</A>
-<P>
 
+<P>
 Report bugs at <A HREF="http://bugs.proftpd.org/">http://bugs.proftpd.org</A><BR>
 For help/support, try the ProFTPD mailing lists, detailed on
 <A HREF="http://www.proftpd.org/lists.html">http://www.proftpd.org/lists.html</A>
 
-<P>
-<HR>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </BODY>
 </HTML>
diff --git a/doc/utils/ftpdctl.html b/doc/utils/ftpdctl.html
index 5bac70b..a2283ba 100644
--- a/doc/utils/ftpdctl.html
+++ b/doc/utils/ftpdctl.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <HTML><HEAD><TITLE>Man page of ftpdctl</TITLE>
 </HEAD><BODY>
 
@@ -74,16 +75,13 @@ can be found on
 
 
 <H2>SEE ALSO</H2>
-
-inetd(8), ftp(1), proftpd(8)</b>
+inetd(8), ftp(1), proftpd(8)
 
 <P>
-
 Full documentation on ProFTPD, including configuration and FAQs, is available at
 <B><A HREF="http://www.proftpd.org/">http://www.proftpd.org/</A></B>
 
 <P>
-
 For help/support, try the ProFTPD mailing lists, detailed on
 <B><A HREF="http://www.proftpd.org/lists.html">http://www.proftpd.org/lists.html</A></B>
 
@@ -92,7 +90,13 @@ For help/support, try the ProFTPD mailing lists, detailed on
 Report bugs at
 <B><A HREF="http://bugs.proftpd.org/">http://bugs.proftpd.org/</A></B>
 
-<P>
-<HR>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </BODY>
 </HTML>
diff --git a/doc/utils/ftpmail.html b/doc/utils/ftpmail.html
index 7ac05d2..fa01c7b 100644
--- a/doc/utils/ftpmail.html
+++ b/doc/utils/ftpmail.html
@@ -1,6 +1,4 @@
-<!-- $Id: ftpmail.html,v 1.2 2012-12-04 19:59:36 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/utils/ftpmail.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ftpmail: Automated Email Notifications of Uploads</title>
@@ -68,7 +66,7 @@ starting <code>proftpd</code>.  For example, you might do:
        --recipient='tj at castaglia.org' \
        --smtp-server=mail.domain.com \
        --attach-file \
-       --log=/var/proftpd/log/transfer.log &
+       --log=/var/proftpd/log/transfer.log &
 </pre>
 The key is to make <code>ftpmail</code> run in the background, so that it is
 constantly running.  If the <code>ftpmail</code> process dies, then
@@ -91,7 +89,7 @@ the <code>proftpd</code> daemon was started.
 
 <p>
 <b>Options</b><br>
-The following shows the full list of <code>ftpmail<code> options; this
+The following shows the full list of <code>ftpmail</code> options; this
 can also be obtained by running:
 <pre>
   # ftpmail --help
@@ -194,19 +192,12 @@ Then start instances of <code>ftpmail</code> running, but only for the
 <code>TransferLog</code> files of the domains/virtual servers to be monitored.
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2012-12-04 19:59:36 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
 © Copyright 2008-2012 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/doc/utils/ftpquota.html b/doc/utils/ftpquota.html
index b3233ae..f971d8e 100644
--- a/doc/utils/ftpquota.html
+++ b/doc/utils/ftpquota.html
@@ -1,6 +1,4 @@
-<!-- $Id: ftpquota.html,v 1.2 2011-03-03 22:17:29 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/utils/ftpquota.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ftpquota: tool for ProFTPD mod_quotatab</title>
@@ -209,25 +207,35 @@ usage: ftpquota [options]
  The following options are used to specify specific quota limits:
 
   --Bu                 Specifies the limit of the number of bytes that may be
-  --bytes-upload       uploaded.  Defaults to -1 (unlimited).
+  --bytes-upload       uploaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero will be treated as
+                       "unlimited".
 
   --Bd                 Specifies the limit of the number of bytes that may be
-  --bytes-download     downloaded.  Defaults to -1 (unlimited).
+  --bytes-download     downloaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero will be treated as
+                       "unlimited".
 
   --Bx                 Specifies the limit of the number of bytes that may be
   --bytes-xfer         transferred.  Note that this total includes uploads,
                        downloads, AND directory listings.  Defaults to
-                       -1 (unlimited).
+                       -1 (unlimited).  Note that any value less than or equal
+                       to zero will be treated as "unlimited".
 
   --Fu                 Specifies the limit of the number of files that may be
-  --files-upload       uploaded.  Defaults to -1 (unlimited).
+  --files-upload       uploaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero will be treated as
+                       "unlimited".
 
   --Fd                 Specifies the limit of the number of files that may be
-  --files-download     downloaded.  Defaults to -1 (unlimited).
+  --files-download     downloaded.  Defaults to -1 (unlimited).  Note that any
+                       value less than or equal to zero will be treated as
+                       "unlimited".
 
   --Fx                 Specifies the limit of the number of files that may be
   --files-xfer         transferred, including uploads and downloads.  Defaults
-                       to -1 (unlimited).
+                       to -1 (unlimited).  Note that any value less than or
+                       equal to zero will be treated as "unlimited".
 
   -L                   Specifies the type of limit, "hard" or "soft", of
   --limit-type         the bytes limits.  If "hard", any uploaded files that
@@ -267,20 +275,12 @@ usage: ftpquota [options]
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2011-03-03 22:17:29 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2011 TJ Saunders<br>
+© Copyright 2000-2017 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
-
diff --git a/doc/utils/ftpscrub.html b/doc/utils/ftpscrub.html
index 8d66803..d1d227a 100644
--- a/doc/utils/ftpscrub.html
+++ b/doc/utils/ftpscrub.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <HTML><HEAD><TITLE>Man page of ftpscrub</TITLE>
 </HEAD><BODY>
 <HR>
@@ -82,7 +83,13 @@ For help/support, try the ProFTPD mailing lists, detailed on
 Report bugs at
 <B><A HREF="http://bugs.proftpd.org/">http://bugs.proftpd.org/</A></B>
 
-<P>
-<HR>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </BODY>
 </HTML>
diff --git a/doc/utils/ftpshut.html b/doc/utils/ftpshut.html
index 23b60f2..51ed393 100644
--- a/doc/utils/ftpshut.html
+++ b/doc/utils/ftpshut.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <HTML><HEAD><TITLE>Manpage of ftpshut</TITLE>
 </HEAD><BODY>
 <HR>
@@ -139,7 +140,13 @@ Report bugs at <A HREF="http://bugs.proftpd.org/">http://bugs.proftpd.org/</A><B
 For help/support, try the ProFTPD mailing lists, detailed on
 <A HREF="http://www.proftpd.org/lists.html">http://www.proftpd.org/lists.html</A>
 
-<P>
-<HR>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </BODY>
 </HTML>
diff --git a/doc/utils/ftptop.html b/doc/utils/ftptop.html
index 5166838..f069669 100644
--- a/doc/utils/ftptop.html
+++ b/doc/utils/ftptop.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <HTML><HEAD><TITLE>Man page of ftptop</TITLE>
 </HEAD><BODY>
 
@@ -128,7 +129,13 @@ For help/support, try the ProFTPD mailing lists, detailed on
 Report bugs at
 <B><A HREF="http://bugs.proftpd.org/">http://bugs.proftpd.org/</A></B>
 
-<P>
-<HR>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </BODY>
 </HTML>
diff --git a/doc/utils/ftpwho.html b/doc/utils/ftpwho.html
index 3620f8a..8b2f728 100644
--- a/doc/utils/ftpwho.html
+++ b/doc/utils/ftpwho.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <HTML><HEAD><TITLE>Manpage of ftpwho</TITLE>
 </HEAD><BODY>
 <HR>
@@ -52,7 +53,7 @@ ProFTPD is written and maintained by a number of people, full credits
 can be found on <A HREF="http://www.proftpd.org/credits.html">http://www.proftpd.org/credits.html</A>
 
 <H2>SEE ALSO</H2>
-inetd(8), ftp(1), proftpd(8), ftpcount(1), ftpshut(8)</B>
+inetd(8), ftp(1), proftpd(8), ftpcount(1), ftpshut(8)
 
 <P>
 Full documentation on ProFTPD, including configuration and FAQs is available at
@@ -63,7 +64,13 @@ Report bugs at <A HREF="http://bugs.proftpd.org/">http://bugs.proftpd.org</A><BR
 For help/support, try the ProFTPD mailing lists, detailed on
 <A HREF="http://www.proftpd.org/lists.html">http://www.proftpd.org/lists.html</A>
 
-<P>
-<HR>
+<p>
+<hr>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
+<hr>
+
 </BODY>
 </HTML>
diff --git a/doc/utils/index.html b/doc/utils/index.html
index 518ea7d..887fa15 100644
--- a/doc/utils/index.html
+++ b/doc/utils/index.html
@@ -1,6 +1,4 @@
-<!-- $Id: index.html,v 1.7 2012-12-11 19:41:51 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/utils/index.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>ProFTPD Utilities Documentation</title>
@@ -57,6 +55,7 @@ which accompany the ProFTPD source distribution.
 
   <p>
   <dt>The <a href="ftpshut.html"><code>ftpshut</code></a> utility
+  <dd>
   </dd>
 
   <p>
@@ -93,7 +92,10 @@ all of the directives to see everything that ProFTPD is capable of supporting.
 
 <p>
 <hr>
-Last Updated: <i>$Date: 2012-12-11 19:41:51 $</i><br>
+<font size=2><b><i>
+© Copyright 2017 The ProFTPD Project<br>
+ All Rights Reserved<br>
+</i></b></font>
 <hr>
 
 </body>
diff --git a/doc/utils/prxs.html b/doc/utils/prxs.html
index 24458bc..670d8bf 100644
--- a/doc/utils/prxs.html
+++ b/doc/utils/prxs.html
@@ -1,6 +1,4 @@
-<!-- $Id: prxs.html,v 1.1 2012-02-20 18:56:08 castaglia Exp $ -->
-<!-- $Source: /home/proftpd-core/backup/proftp-cvsroot/proftpd/doc/utils/prxs.html,v $ -->
-
+<!DOCTYPE html>
 <html>
 <head>
 <title>prxs: PRoftpd eXtenSion tool</title>
@@ -60,7 +58,7 @@ describes how <code>prxs</code> works in more details.
 <h2><a name="Options">Options</a></h2>
 The following is the output from running <code>prxs --help</code>:
 <pre>
-usage: prxs <action> <opts> <source files>
+usage: prxs <action> <opts> <source files>
 
 Actions:
 
@@ -106,19 +104,12 @@ To use prxs all in one step, you could do:
 </pre>
 
 <p>
-<hr><br>
-
-Author: <i>$Author: castaglia $</i><br>
-Last Updated: <i>$Date: 2012-02-20 18:56:08 $</i><br>
-
-<br><hr>
-
+<hr>
 <font size=2><b><i>
-© Copyright 2000-2012 TJ Saunders<br>
+© Copyright 2000-2016 TJ Saunders<br>
  All Rights Reserved<br>
 </i></b></font>
-
-<hr><br>
+<hr>
 
 </body>
 </html>
diff --git a/include/ascii.h b/include/ascii.h
index 4fa3673..a5ab77d 100644
--- a/include/ascii.h
+++ b/include/ascii.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2013 The ProFTPD Project team
+ * Copyright (c) 2013-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* ASCII character checks
- * $Id: ascii.h,v 1.1 2013-02-15 22:33:23 castaglia Exp $
- */
+/* ASCII character checks/conversions */
 
 #ifndef PR_ASCII_H
 #define PR_ASCII_H
@@ -41,4 +39,25 @@
 #define PR_ISSPACE(c)		(isascii((int) (c)) && isspace((int) (c)))
 #define PR_ISXDIGIT(c)		(isascii((int) (c)) && isxdigit((int) (c)))
 
+/* For FTP's ASCII conversion rules. */
+void pr_ascii_ftp_reset(void);
+
+/* Converts the given `in' buffer, character by character, writing the data into
+ * the given `out' buffer, converting any CRLF sequences found into LF
+ * sequences.  The amount of data written into the `out' buffer is returned
+ * via the `outlen' argument.
+ * 
+ * Returns the number of "carry over" CRs on success, and -1 on error, setting
+ * errno appropriately.
+ */
+int pr_ascii_ftp_from_crlf(pool *p, char *in, size_t inlen, char **out,
+  size_t *outlen);
+
+/*
+ * Returns the number N on success, and -1 on error, setting errno
+ * appropriately.
+ */
+int pr_ascii_ftp_to_crlf(pool *p, char *in, size_t inlen, char **out,
+  size_t *outlen);
+
 #endif /* PR_ASCII_H */
diff --git a/include/auth.h b/include/auth.h
index 8e68332..256a8fb 100644
--- a/include/auth.h
+++ b/include/auth.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2011 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* ProFTPD Auth API
- * $Id: auth.h,v 1.14 2011-05-23 20:35:35 castaglia Exp $
- */
+/* ProFTPD Auth API */
 
 #ifndef PR_AUTH_H
 #define PR_AUTH_H
@@ -58,6 +56,27 @@
 /* Account has been disabled */
 #define PR_AUTH_DISABLEDPWD		-5
 
+/* Insufficient credentials. */
+#define PR_AUTH_CRED_INSUFFICIENT	-6
+
+/* Unavailable credentials. */
+#define PR_AUTH_CRED_UNAVAIL		-7
+
+/* Failure setting/using credentials. */
+#define PR_AUTH_CRED_ERROR		-8
+
+/* Unavailable credential/authentication service. */
+#define PR_AUTH_INFO_UNAVAIL		-9
+
+/* Max authentication attempts reached. */
+#define PR_AUTH_MAX_ATTEMPTS_EXCEEDED	-10
+
+/* Authentication service initialization failure. */
+#define PR_AUTH_INIT_ERROR		-11
+
+/* New authentication token/credentials needed. */
+#define PR_AUTH_NEW_TOKEN_REQUIRED	-12
+
 void pr_auth_setpwent(pool *);
 void pr_auth_endpwent(pool *);
 void pr_auth_setgrent(pool *);
@@ -83,7 +102,8 @@ int pr_auth_requires_pass(pool *, const char *);
  * configuration for that user.  If the user name is not be handled as
  * an anonymous login, NULL is returned.
  */
-config_rec *pr_auth_get_anon_config(pool *p, char **, char **, char **);
+config_rec *pr_auth_get_anon_config(pool *p, const char **login_user,
+  char **real_user, char **anon_user);
 
 /* Wrapper function around the chroot(2) system call, handles setting of
  * appropriate environment variables if necessary.
@@ -113,16 +133,43 @@ int pr_auth_remove_auth_only_module(const char *);
  */
 int pr_auth_clear_auth_only_modules(void);
 
+/* Clears any cached IDs/names. */
+void pr_auth_cache_clear(void);
+
 /* Enable caching of certain data within the Auth API. */
-int pr_auth_cache_set(int, unsigned int);
+int pr_auth_cache_set(int enable, unsigned int flags);
 #define PR_AUTH_CACHE_FL_UID2NAME	0x00001
 #define PR_AUTH_CACHE_FL_GID2NAME	0x00002
 #define PR_AUTH_CACHE_FL_AUTH_MODULE	0x00004
+#define PR_AUTH_CACHE_FL_NAME2UID	0x00008
+#define PR_AUTH_CACHE_FL_NAME2GID	0x00010
+#define PR_AUTH_CACHE_FL_BAD_UID2NAME	0x00020
+#define PR_AUTH_CACHE_FL_BAD_GID2NAME	0x00040
+#define PR_AUTH_CACHE_FL_BAD_NAME2UID	0x00080
+#define PR_AUTH_CACHE_FL_BAD_NAME2GID	0x00100
+
+/* Default Auth API cache flags/settings. */
+#define PR_AUTH_CACHE_FL_DEFAULT \
+  (PR_AUTH_CACHE_FL_UID2NAME|\
+   PR_AUTH_CACHE_FL_GID2NAME|\
+   PR_AUTH_CACHE_FL_AUTH_MODULE|\
+   PR_AUTH_CACHE_FL_NAME2UID|\
+   PR_AUTH_CACHE_FL_NAME2GID|\
+   PR_AUTH_CACHE_FL_BAD_UID2NAME|\
+   PR_AUTH_CACHE_FL_BAD_GID2NAME|\
+   PR_AUTH_CACHE_FL_BAD_NAME2UID|\
+   PR_AUTH_CACHE_FL_BAD_NAME2GID)
 
 /* Wrapper function for retrieving the user's home directory.  This handles
  * any possible RewriteHome configuration.
  */
-char *pr_auth_get_home(pool *, char *pw_dir);
+const char *pr_auth_get_home(pool *, const char *pw_dir);
+
+/* Policy setting for the maximum allowable password length.  This is
+ * supported for mitigating potential resource consumption attack via the
+ * crypt(3) function.
+ */
+size_t pr_auth_set_max_password_len(pool *p, size_t len);
 
 /* For internal use only. */
 int init_auth(void);
diff --git a/include/bindings.h b/include/bindings.h
index cf63365..e4e1112 100644
--- a/include/bindings.h
+++ b/include/bindings.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,14 @@
  * OpenSSL in the source distribution.
  */
 
-/* ProFTPD bindings support routines.
- * $Id: bindings.h,v 1.14 2012-04-15 18:04:14 castaglia Exp $
- */
-
-#include "conf.h"
-#include "pool.h"
+/* ProFTPD bindings support routines. */
 
 #ifndef PR_BINDINGS_H
 #define PR_BINDINGS_H
 
+#include "conf.h"
+#include "pool.h"
+
 /* NOTE: the is* members could possibly become a bitmasked number */
 
 /* Structure associating an IP address to a server_rec */
@@ -39,7 +37,7 @@ typedef struct ipbind_rec {
   struct ipbind_rec *ib_next;
 
   /* IP address to which this binding is "bound" */
-  pr_netaddr_t *ib_addr;
+  const pr_netaddr_t *ib_addr;
   unsigned int ib_port;
 
   /* Default server to handle requests to this binding.  If namebinds are
@@ -94,14 +92,15 @@ conn_t *pr_ipbind_accept_conn(fd_set *readfds, int *listenfd);
  * arguments. The new binding is added the list maintained by the bindings
  * layer.  Returns 0 on success, -1 on failure.
  */
-int pr_ipbind_create(server_rec *server, pr_netaddr_t *addr, unsigned int port);
+int pr_ipbind_create(server_rec *server, const pr_netaddr_t *addr,
+  unsigned int port);
 
 /* Close all IP bindings associated with the given IP address/port combination.
  * The bindings are then marked as inactive, so that future lookups via
  * pr_ipbind_find() skip these bindings.  Returns 0 on success, -1 on failure
  * (eg no associated bindings found).
  */
-int pr_ipbind_close(pr_netaddr_t *addr, unsigned int port,
+int pr_ipbind_close(const pr_netaddr_t *addr, unsigned int port,
   unsigned char close_namebinds);
 
 /* Close all listenings fds.  This needs to happen just after a process
@@ -119,7 +118,7 @@ int pr_ipbind_add_binds(server_rec *server);
 /* Search the binding list, and return the pr_ipbind_t for the given addr and
  * port.  If requested, skip over inactive bindings while searching.
  */
-pr_ipbind_t *pr_ipbind_find(pr_netaddr_t *addr, unsigned int port,
+pr_ipbind_t *pr_ipbind_find(const pr_netaddr_t *addr, unsigned int port,
   unsigned char skip_inactive);
 
 /* Iterate through the binding list, returning the next ipbind.  Returns NULL
@@ -131,7 +130,7 @@ pr_ipbind_t *pr_ipbind_get(pr_ipbind_t *prev);
 /* Search the binding list, and return the server_rec * that is bound to the
  * given IP address/port combination.
  */
-server_rec *pr_ipbind_get_server(pr_netaddr_t *addr, unsigned int port);
+server_rec *pr_ipbind_get_server(const pr_netaddr_t *addr, unsigned int port);
 
 /* Listens on each file descriptor in the given set, and returns the file
  * descriptor associated with an incoming connection request.  Returns -1
@@ -142,48 +141,49 @@ int pr_ipbind_listen(fd_set *readfds);
 /* Prepares the IP-based binding associated with the given server for listening.
  * Returns 0 on success, -1 on failure.
  */
-int pr_ipbind_open(pr_netaddr_t *addr, unsigned int port, conn_t *listen_conn,
-  unsigned char isdefault, unsigned char islocalhost,
+int pr_ipbind_open(const pr_netaddr_t *addr, unsigned int port,
+  conn_t *listen_conn, unsigned char isdefault, unsigned char islocalhost,
   unsigned char open_namebinds);
 
-conn_t *pr_ipbind_get_listening_conn(server_rec *server, pr_netaddr_t *addr,
-  unsigned int port);
+conn_t *pr_ipbind_get_listening_conn(server_rec *server,
+  const pr_netaddr_t *addr, unsigned int port);
 
-/* Close the pr_namebind_t with the given name.
- */
-int pr_namebind_close(const char *name, pr_netaddr_t *addr, unsigned int port);
+/* Close the pr_namebind_t with the given name. */
+int pr_namebind_close(const char *name, const pr_netaddr_t *addr);
 
 /* Create a pr_namebind_t, similar to a pr_ipbind_t, which maps the name (usu.
- * DNS hostname) to the server_rec.  The given addr and port are used to
- * associate this pr_namebind_t with the given IP address (to which the DNS
- * hostname should resolve).
+ * DNS hostname) to the server_rec.  The given addr is used to associate this
+ * pr_namebind_t with the given IP address (to which the DNS hostname should
+ * resolve).
  */
 int pr_namebind_create(server_rec *server, const char *name,
-  pr_netaddr_t *addr, unsigned int port);
+  const pr_netaddr_t *addr, unsigned int port);
 
 /* Search the Bindings layer, and return the pr_namebind_t associated with
  * the given addr, port, and name.  If requested, skip over inactive
  * bindings while searching.
  */
-pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
+pr_namebind_t *pr_namebind_find(const char *name, const pr_netaddr_t *addr,
   unsigned int port, unsigned char skip_inactive);
 
 /* Find the server_rec associated with the given name.  If none are found,
  * default to the server_rec of the containing pr_ipbind_t.
  */
-server_rec *pr_namebind_get_server(const char *name, pr_netaddr_t *addr,
+server_rec *pr_namebind_get_server(const char *name, const pr_netaddr_t *addr,
   unsigned int port);
 
-/* Opens the pr_namebind_t with the given name.
- */
-int pr_namebind_open(const char *name, pr_netaddr_t *addr, unsigned int port);
+/* Opens the pr_namebind_t with the given name. */
+int pr_namebind_open(const char *name, const pr_netaddr_t *addr);
 
-/* Initialize the Bindings layer.
+/* Provides a count of the number of namebinds associated with this
+ * server_rec.
  */
+unsigned int pr_namebind_count(server_rec *);
+
+/* Initialize the Bindings layer. */
 void init_bindings(void);
 
-/* Free the Bindings layer.
- */
+/* Free the Bindings layer. */
 void free_bindings(void);
 
 /* Macro error-handling wrappers */
diff --git a/include/ccan-json.h b/include/ccan-json.h
new file mode 100644
index 0000000..4b74cb1
--- /dev/null
+++ b/include/ccan-json.h
@@ -0,0 +1,120 @@
+/*
+  Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159 at gmail.com)
+  All rights reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+#ifndef CCAN_JSON_H
+#define CCAN_JSON_H
+
+#include <stddef.h>
+
+typedef enum {
+	JSON_NULL,
+	JSON_BOOL,
+	JSON_STRING,
+	JSON_NUMBER,
+	JSON_ARRAY,
+	JSON_OBJECT,
+} JsonTag;
+
+struct json_node_st
+{
+	/* only if parent is an object or array (NULL otherwise) */
+	struct json_node_st *parent;
+	struct json_node_st *prev, *next;
+	
+	/* only if parent is an object (NULL otherwise) */
+	char *key; /* Must be valid UTF-8. */
+	
+	JsonTag tag;
+	union {
+		/* JSON_BOOL */
+		int bool_;
+		
+		/* JSON_STRING */
+		char *string_; /* Must be valid UTF-8. */
+		
+		/* JSON_NUMBER */
+		double number_;
+		
+		/* JSON_ARRAY */
+		/* JSON_OBJECT */
+		struct {
+			struct json_node_st *head, *tail;
+		} children;
+	};
+};
+
+typedef struct json_node_st JsonNode;
+
+/*** Encoding, decoding, and validation ***/
+
+JsonNode   *json_decode         (const char *json);
+char       *json_encode         (const JsonNode *node);
+char       *json_encode_string  (const char *str);
+char       *json_stringify      (const JsonNode *node, const char *space);
+void        json_delete         (JsonNode *node);
+
+int         json_validate       (const char *json);
+
+/*** Lookup and traversal ***/
+
+JsonNode   *json_find_element   (JsonNode *array, unsigned int index);
+JsonNode   *json_find_member    (JsonNode *object, const char *key);
+
+JsonNode   *json_first_child    (const JsonNode *node);
+
+#define json_foreach(i, object_or_array)            \
+	for ((i) = json_first_child(object_or_array);   \
+		 (i) != NULL;                               \
+		 (i) = (i)->next)
+
+/*** Construction and manipulation ***/
+
+JsonNode *json_mknull(void);
+JsonNode *json_mkbool(int b);
+JsonNode *json_mkstring(const char *s);
+JsonNode *json_mknumber(double n);
+JsonNode *json_mkarray(void);
+JsonNode *json_mkobject(void);
+
+void json_append_element(JsonNode *array, JsonNode *element);
+void json_prepend_element(JsonNode *array, JsonNode *element);
+void json_append_member(JsonNode *object, const char *key, JsonNode *value);
+void json_prepend_member(JsonNode *object, const char *key, JsonNode *value);
+
+void json_remove_from_parent(JsonNode *node);
+
+/*** Error Handling ***/
+
+void json_set_oom(void (*oom)(void));
+
+/*** Debugging ***/
+
+/*
+ * Look for structure and encoding problems in a JsonNode or its descendents.
+ *
+ * If a problem is detected, return false, writing a description of the problem
+ * to errmsg (unless errmsg is NULL).
+ */
+int json_check(const JsonNode *node, char errmsg[256]);
+
+#endif
diff --git a/include/child.h b/include/child.h
index 91e257a..ca95156 100644
--- a/include/child.h
+++ b/include/child.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2011 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Management of child objects
- * $Id: child.h,v 1.3 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Management of child objects */
 
 #ifndef PR_CHILD_H
 #define PR_CHILD_H
diff --git a/include/class.h b/include/class.h
index 8e9ff82..4d49342 100644
--- a/include/class.h
+++ b/include/class.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2011 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Class definitions
- * $Id: class.h,v 1.4 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Class definitions */
 
 #ifndef PR_CLASS_H
 #define PR_CLASS_H
@@ -36,6 +34,7 @@ typedef struct pr_class_t {
   char *cls_name;
   unsigned int cls_satisfy;
   array_header *cls_acls;
+  pr_table_t *cls_notes;
 
   struct pr_class_t *cls_next;
 } pr_class_t;
@@ -46,19 +45,19 @@ typedef struct pr_class_t {
 /* Returns the class object associated with the given name, or NULL if
  * there is no matching class object.
  */
-pr_class_t *pr_class_find(const char *);
+const pr_class_t *pr_class_find(const char *);
 
 /* Iterate through the Class list, returning the next class.  Returns NULL
  * once the end of the list is reached.  If prev is NULL, the iterator
  * restarts at the beginning of the list.
  */
-pr_class_t *pr_class_get(pr_class_t *prev);
+const pr_class_t *pr_class_get(const pr_class_t *prev);
 
 /* Returns the class object for which the given address matches every rule.
  * If multiple classes exist that might match the given address, the first
  * defined class matches.
  */
-pr_class_t *pr_class_match_addr(pr_netaddr_t *);
+const pr_class_t *pr_class_match_addr(const pr_netaddr_t *);
 
 /* Start a new class object, allocated from the given pool, with the given
  * name.
@@ -72,14 +71,16 @@ int pr_class_open(pool *, const char *);
  */
 int pr_class_close(void);
 
-/* Add the given ACL object to the currently opened class object.
- */
-int pr_class_add_acl(pr_netacl_t *);
+/* Add the given ACL object to the currently opened class object. */
+int pr_class_add_acl(const pr_netacl_t *);
 
-/* Set the Satisfy flag on the currently opened class object.
- */
+/* Set the Satisfy flag on the currently opened class object. */
 int pr_class_set_satisfy(int);
 
+/* Set a note on the currently opened class object. */
+int pr_class_add_note(const char *, void *, size_t);
+
+/* For internal use only. */
 void init_class(void);
 
 #endif /* PR_CLASS_H */
diff --git a/include/cmd.h b/include/cmd.h
index 7b490a8..257c6ec 100644
--- a/include/cmd.h
+++ b/include/cmd.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2012 The ProFTPD Project team
+ * Copyright (c) 2009-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,16 +20,15 @@
  * 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.
- *
- * $Id: cmd.h,v 1.7 2012-12-27 22:30:58 castaglia Exp $
  */
 
 #ifndef PR_CMD_H
 #define PR_CMD_H
 
-cmd_rec *pr_cmd_alloc(pool *, int, ...);
-int pr_cmd_clear_cache(cmd_rec *);
-char *pr_cmd_get_displayable_str(cmd_rec *, size_t *);
+cmd_rec *pr_cmd_alloc(pool *p, unsigned int, ...);
+int pr_cmd_clear_cache(cmd_rec *cmd);
+const char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *len);
+int pr_cmd_get_errno(cmd_rec *cmd);
 
 int pr_cmd_cmp(cmd_rec *cmd, int cmd_id);
 int pr_cmd_strcmp(cmd_rec *cmd, const char *cmd_name);
@@ -101,6 +100,8 @@ int pr_cmd_get_id(const char *name_name);
 #define PR_CMD_PROT_ID		55
 #define PR_CMD_MFF_ID		56
 #define PR_CMD_MFMT_ID		57
+#define PR_CMD_HOST_ID		58
+#define PR_CMD_CLNT_ID		59
 
 /* The minimum and maximum command name lengths. */
 #define PR_CMD_MIN_NAMELEN	3
@@ -109,22 +110,28 @@ int pr_cmd_get_id(const char *name_name);
 /* Returns TRUE if the given command is a known HTTP method, FALSE if not
  * a known HTTP method, and -1 if there is an error.
  */
-int pr_cmd_is_http(cmd_rec *c);
+int pr_cmd_is_http(cmd_rec *cmd);
 
 /* Returns TRUE if the given command is a known SMTP method, FALSE if not
  * a known SMTP method, and -1 if there is an error.
  */
-int pr_cmd_is_smtp(cmd_rec *c);
+int pr_cmd_is_smtp(cmd_rec *cmd);
+
+/* Returns TRUE if the given command appears to be an SSH2 request, FALSE
+ * if not, and -1 if there was an error.
+ */
+int pr_cmd_is_ssh2(cmd_rec *cmd);
 
-int pr_cmd_set_name(cmd_rec *, const char *);
+int pr_cmd_set_errno(cmd_rec *cmd, int xerrno);
+int pr_cmd_set_name(cmd_rec *cmd, const char *name);
 
 /* Implemented in main.c */
-int pr_cmd_read(cmd_rec **);
-int pr_cmd_dispatch(cmd_rec *);
-int pr_cmd_dispatch_phase(cmd_rec *, int, int);
+int pr_cmd_read(cmd_rec **cmd);
+int pr_cmd_dispatch(cmd_rec *cmd);
+int pr_cmd_dispatch_phase(cmd_rec *cmd, int, int);
 #define PR_CMD_DISPATCH_FL_SEND_RESPONSE	0x001
 #define PR_CMD_DISPATCH_FL_CLEAR_RESPONSE	0x002
 
-void pr_cmd_set_handler(void (*)(server_rec *, conn_t *));
+void pr_cmd_set_handler(void (*)(server_rec *s, conn_t *conn));
 
 #endif /* PR_CMD_H */
diff --git a/include/compat.h b/include/compat.h
index 9091333..fcb4f68 100644
--- a/include/compat.h
+++ b/include/compat.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2005-2011 The ProFTPD Project team
+ * Copyright (c) 2005-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,61 +22,13 @@
  * OpenSSL in the source distribution.
  */
 
-/* Compatibility
- * $Id: compat.h,v 1.17 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Compatibility macros */
 
 #ifndef PR_COMPAT_H
 #define PR_COMPAT_H
 
 /* Legacy redefines, for compatibility (for a while). */
 
-/* The following macros all first appeared in 1.3.0rc2. */
-#define USE_AUTO_SHADOW		PR_USE_AUTO_SHADOW
-#define USE_CTRLS		PR_USE_CTRLS
-#define USE_CURSES		PR_USE_CURSES
-#define USE_DEVEL		PR_USE_DEVEL
-#define USE_GETADDRINFO		PR_USE_GETADDRINFO
-#define USE_GETNAMEINFO		PR_USE_GETNAMEINFO
-#define USE_IPV6		PR_USE_IPV6
-#define USE_LARGEFILES		PR_USE_LARGEFILES
-
-#define LOG_WRITEABLE_DIR	PR_LOG_WRITABLE_DIR
-#define LOG_SYMLINK		PR_LOG_SYMLINK
-
-#define pr_parse_expression     pr_expr_create
-#define pr_class_and_expression pr_expr_eval_class_and
-#define pr_class_or_expression  pr_expr_eval_class_or
-#define pr_group_and_expression pr_expr_eval_group_and
-#define pr_group_or_expression  pr_expr_eval_group_or
-#define pr_user_and_expression  pr_expr_eval_user_and
-#define pr_user_or_expression   pr_expr_eval_user_or
-
-/* The following macros first appeared in 1.3.1rc1. */
-#define DECLINED		PR_DECLINED
-#define	ERROR			PR_ERROR
-#define	ERROR_INT		PR_ERROR_INT
-#define	ERROR_MSG		PR_ERROR_MSG
-#define HANDLED			PR_HANDLED
-
-/* The following macros first appeared in 1.3.1rc2. */
-#define add_timer               pr_timer_add
-#define remove_timer            pr_timer_remove
-#define reset_timer             pr_timer_reset
-#define timer_sleep             pr_timer_sleep
-
-#define make_named_sub_pool(p, s)	make_sub_pool((p))
-
-/* The following macros first appeared in 1.3.2rc1. */
-#define pr_scoreboard_add_entry		pr_scoreboard_entry_add
-#define pr_scoreboard_del_entry		pr_scoreboard_entry_del
-#define pr_scoreboard_read_entry	pr_scoreboard_entry_read
-#define pr_scoreboard_update_entry	pr_scoreboard_entry_update
-
-/* The following macros first appeared in 1.3.2rc2. */
-#define pr_inet_validate		pr_netaddr_validate_dns_str
-#define call_module			pr_module_call
-
 /* The following macros first appeared in 1.3.3rc1. */
 #define ctrls_check_acl			pr_ctrls_check_acl
 #define ctrls_check_group_acl		pr_ctrls_check_group_acl
@@ -95,4 +47,7 @@
 /* The following macros first appeared in 1.3.4rc2. */
 #define end_login			pr_session_end
 
+/* The following macros first appeared in 1.3.6rc2. */
+#define _sql_make_cmd			sql_make_cmd
+
 #endif /* PR_COMPAT_H */
diff --git a/include/conf.h b/include/conf.h
index 5d1cda1..3c530c5 100644
--- a/include/conf.h
+++ b/include/conf.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2014 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Generic configuration and standard header file includes.
- * $Id: conf.h,v 1.92 2013-09-19 05:54:32 castaglia Exp $
- */
+/* Generic configuration and standard header file includes. */
 
 #ifndef PR_CONF_H
 #define PR_CONF_H
@@ -273,14 +271,6 @@ char *strchr(),*strrchr();
 
 #include "options.h"
 
-/* Solaris 2.5 seems to already have a typedef for 'timer_t', so
- * #define timer_t to something else as a workaround.  However, on AIX,
- * we do NOT want to do this.
- */
-#if defined(HAVE_TIMER_T) && !defined(AIX7)
-# define timer_t p_timer_t
-#endif
-
 /* AIX, when compiled using -D_NO_PROTO, lacks some prototypes without
  * which ProFTPD may do some funny (and not good) things.  Provide the
  * prototypes as necessary here.
@@ -422,10 +412,12 @@ typedef struct {
 #include "str.h"
 #include "ascii.h"
 #include "table.h"
+#include "signals.h"
 #include "proftpd.h"
 #include "support.h"
 #include "str.h"
 #include "sets.h"
+#include "configdb.h"
 #include "dirtree.h"
 #include "expr.h"
 #include "rlimit.h"
@@ -468,7 +460,9 @@ typedef struct {
 #include "pidfile.h"
 #include "env.h"
 #include "pr-syslog.h"
+#include "json.h"
 #include "memcache.h"
+#include "redis.h"
 
 # ifdef HAVE_SETPASSENT
 #  define setpwent()	setpassent(1)
diff --git a/include/configdb.h b/include/configdb.h
new file mode 100644
index 0000000..c95cecb
--- /dev/null
+++ b/include/configdb.h
@@ -0,0 +1,144 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2014-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, Public Flood Software/MacGyver aka Habeeb J. Dihu
+ * 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.
+ */
+
+/* Configuration database API. */
+
+#ifndef PR_CONFIGDB_H
+#define PR_CONFIGDB_H
+
+#include "pool.h"
+#include "sets.h"
+#include "table.h"
+
+typedef struct config_struc config_rec;
+struct server_struc;
+
+struct config_struc {
+  struct config_struc *next, *prev;
+
+  int config_type;
+  unsigned int config_id;
+
+  struct pool_rec *pool;	/* Memory pool for this object */
+  xaset_t *set;			/* The set we are stored in */
+  char *name;
+  unsigned int argc;
+  void **argv;
+
+  long flags;			/* Flags */
+
+  struct server_struc *server;	/* Server this config element is attached to */
+  config_rec *parent;		/* Our parent configuration record */
+  xaset_t *subset;		/* Sub-configuration */
+};
+
+#define CONF_ROOT		(1 << 0) /* No conf record */
+#define CONF_DIR		(1 << 1) /* Per-Dir configuration */
+#define CONF_ANON		(1 << 2) /* Anon. FTP configuration */
+#define CONF_LIMIT		(1 << 3) /* Limits commands available */
+#define CONF_VIRTUAL		(1 << 4) /* Virtual host */
+#define CONF_DYNDIR		(1 << 5) /* .ftpaccess file */
+#define CONF_GLOBAL		(1 << 6) /* "Global" context (applies to main server and ALL virtualhosts */
+#define CONF_CLASS		(1 << 7) /* Class context */
+#define CONF_NAMED		(1 << 8) /* Named virtual host */
+#define CONF_USERDATA		(1 << 14) /* Runtime user data */
+#define CONF_PARAM		(1 << 15) /* config/args pair */
+
+/* config_rec flags */
+#define CF_MERGEDOWN		(1 << 0) /* Merge option down */
+#define CF_MERGEDOWN_MULTI	(1 << 1) /* Merge down, allowing multiple instances */
+#define CF_DYNAMIC		(1 << 2) /* Dynamically added entry */
+#define CF_DEFER		(1 << 3) /* Defer hashing until authentication */
+#define CF_SILENT		(1 << 4) /* Do not print a config dump when merging */
+#define CF_MULTI		(1 << 5) /* Allow multiple instances, but do not merge down */
+
+/* The following macro determines the "highest" level available for
+ * configuration directives.  If a current dir_config is available, it's
+ * subset is used, otherwise anon config or main server
+ */
+
+#define CURRENT_CONF		(session.dir_config ? session.dir_config->subset \
+				 : (session.anon_config ? session.anon_config->subset \
+                                    : main_server ? main_server->conf : NULL))
+#define TOPLEVEL_CONF		(session.anon_config ? session.anon_config->subset : (main_server ? main_server->conf : NULL))
+
+/* Prototypes */
+
+config_rec *add_config_set(xaset_t **, const char *);
+config_rec *add_config(struct server_struc *, const char *);
+config_rec *add_config_param(const char *, unsigned int, ...);
+config_rec *add_config_param_str(const char *, unsigned int, ...);
+config_rec *add_config_param_set(xaset_t **, const char *, unsigned int, ...);
+config_rec *pr_conf_add_server_config_param_str(struct server_struc *,
+  const char *, unsigned int, ...);
+
+/* Flags used when searching for specific config_recs in the in-memory
+ * config database, particularly when 'recurse' is TRUE.
+ */
+#define PR_CONFIG_FIND_FL_SKIP_ANON		0x001
+#define PR_CONFIG_FIND_FL_SKIP_DIR		0x002
+#define PR_CONFIG_FIND_FL_SKIP_LIMIT		0x004
+#define PR_CONFIG_FIND_FL_SKIP_DYNDIR		0x008
+
+config_rec *find_config_next(config_rec *, config_rec *, int,
+  const char *, int);
+config_rec *find_config_next2(config_rec *, config_rec *, int,
+  const char *, int, unsigned long);
+config_rec *find_config(xaset_t *, int, const char *, int);
+config_rec *find_config2(xaset_t *, int, const char *, int, unsigned long);
+void find_config_set_top(config_rec *);
+
+int remove_config(xaset_t *set, const char *name, int recurse);
+
+#define PR_CONFIG_FL_INSERT_HEAD	0x001
+#define PR_CONFIG_FL_PRESERVE_ENTRY	0x002
+config_rec *pr_config_add_set(xaset_t **, const char *, int);
+config_rec *pr_config_add(struct server_struc *, const char *, int);
+int pr_config_remove(xaset_t *set, const char *name, int flags, int recurse);
+
+/* Returns the assigned ID for the provided directive name, or zero
+ * if no ID mapping was found.
+ */
+unsigned int pr_config_get_id(const char *name);
+
+/* Assigns a unique ID for the given configuration directive.  The
+ * mapping of directive to ID is stored in a lookup table, so that
+ * searching of the config database by directive name can be done using
+ * ID comparisons rather than string comparisons.
+ *
+ * Returns the ID assigned for the given directive, or zero if there was an
+ * error.
+ */
+unsigned int pr_config_set_id(const char *name);
+
+void *get_param_ptr(xaset_t *, const char *, int);
+void *get_param_ptr_next(const char *, int);
+
+void pr_config_merge_down(xaset_t *, int);
+void pr_config_dump(void (*)(const char *, ...), xaset_t *, char *);
+
+/* Internal use only. */
+void init_config(void);
+
+#endif /* PR_CONFIGDB_H */
diff --git a/include/ctrls.h b/include/ctrls.h
index 1bbf4af..dcdc24d 100644
--- a/include/ctrls.h
+++ b/include/ctrls.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Controls API definitions
- * $Id: ctrls.h,v 1.9 2013-02-04 06:46:22 castaglia Exp $
- */
+/* Controls API definitions */
 
 #ifndef PR_CTRLS_H
 #define PR_CTRLS_H
diff --git a/include/data.h b/include/data.h
index 4eaf5d9..ac631a8 100644
--- a/include/data.h
+++ b/include/data.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,12 +24,15 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Data connection management prototypes
- * $Id: data.h,v 1.23 2013-01-30 22:37:04 castaglia Exp $
- */
+/* Data connection management prototypes */
+
+#ifndef PR_DATA_H
+#define PR_DATA_H
 
-#ifndef PR_DATACONN_H
-#define PR_DATACONN_H
+/* Toggles whether to actually perform ASCII translation during the data
+ * transfer.
+ */
+int pr_data_ignore_ascii(int);
 
 void pr_data_init(char *, int);
 void pr_data_cleanup(void);
@@ -54,18 +57,20 @@ void pr_data_set_timeout(int, int);
 #ifdef HAVE_SENDFILE
 typedef
 
-#if defined(HAVE_AIX_SENDFILE) || defined(HAVE_HPUX_SENDFILE) || \
+# if defined(HAVE_AIX_SENDFILE) || defined(HAVE_HPUX_SENDFILE) || \
     defined(HAVE_LINUX_SENDFILE) || defined(HAVE_SOLARIS_SENDFILE)
 ssize_t
-#elif defined(HAVE_BSD_SENDFILE) || defined(HAVE_MACOSX_SENDFILE)
+# elif defined(HAVE_BSD_SENDFILE) || defined(HAVE_MACOSX_SENDFILE)
 off_t
-#else
-#error "You have an unknown sendfile implementation."
-#endif
+# else
+#  error "You have an unknown sendfile implementation."
+# endif
 
 pr_sendfile_t;
+#else
+typedef ssize_t pr_sendfile_t;
+#endif /* HAVE_SENDFILE */
 
 pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count);
-#endif /* HAVE_SENDFILE */
 
-#endif /* PR_DATACONN_H */
+#endif /* PR_DATA_H */
diff --git a/include/default_paths.h b/include/default_paths.h
index 1d6a28d..c5198a0 100644
--- a/include/default_paths.h
+++ b/include/default_paths.h
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -26,7 +26,6 @@
 /* ProFTPD default path configuration.  Normally, Makefiles generated
  * by the top-level configuration script define the PR_RUN_DIR and
  * PR_CONFIG_FILE_PATH macros, so the two below are typically not used.
- * $Id: default_paths.h,v 1.12 2011-05-23 20:35:35 castaglia Exp $
  */
 
 #ifndef PROFTPD_PATHS_H
@@ -63,14 +62,18 @@
 /* The location of your `shells' file; a newline delimited list of
  * valid shells on your system.
  */
-#define PR_VALID_SHELL_PATH	"/etc/shells"
+#ifndef PR_VALID_SHELL_PATH
+# define PR_VALID_SHELL_PATH	"/etc/shells"
+#endif
 
 /* Where your log files are kept.  The "wu-ftpd style" xferlog is
  * stored here, as well as "extended" (not yet available) transfer
  * log files.  These can be overridden in the configuration file via
  * "TransferLog" and "ExtendedLog".
  */
-#define PR_XFERLOG_PATH		"/var/log/xferlog"
+#ifndef PR_XFERLOG_PATH
+# define PR_XFERLOG_PATH	"/var/log/xferlog"
+#endif
 
 /* Location of the file that tells proftpd to discontinue servicing
  * requests.
@@ -80,6 +83,8 @@
 /* Location of the file containing users that *cannot* use ftp
  * services (odd, eh?)
  */
-#define PR_FTPUSERS_PATH	"/etc/ftpusers"
+#ifndef PR_FTPUSERS_PATH
+# define PR_FTPUSERS_PATH	"/etc/ftpusers"
+#endif
 
 #endif /* PROFTPD_PATHS_H */
diff --git a/include/dirtree.h b/include/dirtree.h
index 0b0115c..7768308 100644
--- a/include/dirtree.h
+++ b/include/dirtree.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Configuration structure, server, command and associated prototypes.
- * $Id: dirtree.h,v 1.88 2013-07-16 19:06:13 castaglia Exp $
- */
+/* Server, command and associated prototypes. */
 
 #ifndef PR_DIRTREE_H
 #define PR_DIRTREE_H
@@ -34,8 +32,7 @@
 #include "pool.h"
 #include "sets.h"
 #include "table.h"
-
-typedef struct config_struc config_rec;
+#include "configdb.h"
 
 struct conn_struc;
 
@@ -83,10 +80,10 @@ typedef struct server_struc {
   unsigned char tcp_sndbuf_override;
 
   /* Administrator name */
-  char *ServerAdmin;
+  const char *ServerAdmin;
 
   /* Internal address of this server */
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
 
   /* The listener for this server.  Note that this listener, and that
    * pointed to by ipbind->ib_listener (where ipbind->ib_server points to
@@ -115,14 +112,19 @@ typedef struct cmd_struc {
   struct pool_rec *tmp_pool;	/* Temporary pool which only exists
 				 * while the cmd's handler is running
 				 */
-  int argc;
+  unsigned int argc;
 
   char *arg;			/* entire argument (excluding command) */
-  char **argv;
+  void **argv;
+
   char *group;			/* Command grouping */
 
-  int  cmd_class;		/* The command class */
-  int  stash_index;		/* hack to speed up symbol hashing in modules.c */
+  int cmd_class;		/* The command class */
+
+  /* These are used to speed up symbol hashing/lookups in stash.c. */
+  int stash_index;
+  unsigned int stash_hash;
+
   pr_table_t *notes;		/* Private data for passing/retaining between handlers */
 
   int cmd_id;			/* Index into commands list, for faster comparisons */
@@ -136,45 +138,6 @@ typedef struct cmd_struc {
 
 } cmd_rec;
 
-struct config_struc {
-  struct config_struc *next,*prev;
-
-  int config_type;
-  unsigned int config_id;
-
-  struct pool_rec *pool;	/* Memory pool for this object */
-  xaset_t *set;			/* The set we are stored in */
-  char *name;
-  int argc;
-  void **argv;
-
-  long flags;			/* Flags */
-
-  server_rec *server;		/* Server this config element is attached to */
-  config_rec *parent;		/* Our parent configuration record */
-  xaset_t *subset;		/* Sub-configuration */
-};
-
-#define CONF_ROOT		(1 << 0) /* No conf record */
-#define CONF_DIR		(1 << 1) /* Per-Dir configuration */
-#define CONF_ANON		(1 << 2) /* Anon. FTP configuration */
-#define CONF_LIMIT		(1 << 3) /* Limits commands available */
-#define CONF_VIRTUAL		(1 << 4) /* Virtual host */
-#define CONF_DYNDIR		(1 << 5) /* .ftpaccess file */
-#define CONF_GLOBAL		(1 << 6) /* "Global" context (applies to main server and ALL virtualhosts */
-#define CONF_CLASS		(1 << 7) /* Class context */
-#define CONF_NAMED		(1 << 8) /* Named virtual host */
-#define CONF_USERDATA		(1 << 14) /* Runtime user data */
-#define CONF_PARAM		(1 << 15) /* config/args pair */
-
-/* config_rec flags */
-#define CF_MERGEDOWN		(1 << 0) /* Merge option down */
-#define CF_MERGEDOWN_MULTI	(1 << 1) /* Merge down, allowing multiple instances */
-#define CF_DYNAMIC		(1 << 2) /* Dynamically added entry */
-#define CF_DEFER		(1 << 3) /* Defer hashing until authentication */
-#define CF_SILENT		(1 << 4) /* Do not print a config dump when merging */
-#define CF_MULTI		(1 << 5) /* Allow multiple instances, but do not merge down */
-
 /* Operation codes for dir_* funcs */
 #define OP_HIDE			1	/* Op for hiding dirs/files */
 #define OP_COMMAND		2	/* Command operation */
@@ -183,34 +146,24 @@ struct config_struc {
 #define ORDER_ALLOWDENY		0
 #define ORDER_DENYALLOW		1
 
-/* The following macro determines the "highest" level available for
- * configuration directives.  If a current dir_config is available, it's
- * subset is used, otherwise anon config or main server
- */
-
-#define CURRENT_CONF		(session.dir_config ? session.dir_config->subset \
-				 : (session.anon_config ? session.anon_config->subset \
-                                    : main_server->conf))
-#define TOPLEVEL_CONF		(session.anon_config ? session.anon_config->subset : main_server->conf)
-
 extern server_rec		*main_server;
 extern int			tcpBackLog;
 extern int			SocketBindTight;
 extern char			ServerType;
-extern int			ServerMaxInstances;
+extern unsigned long		ServerMaxInstances;
 extern int			ServerUseReverseDNS;
 
 /* These macros are used to help handle configuration in modules */
 #define CONF_ERROR(x, s)	return PR_ERROR_MSG((x),NULL,pstrcat((x)->tmp_pool, \
 				(x)->argv[0],": ",(s),NULL));
 
-#define CHECK_ARGS(x, n)	if((x)->argc-1 < n) \
-				CONF_ERROR(x,"missing arguments")
+#define CHECK_ARGS(x, n)	if ((n) > 0 && (x)->argc > 0 && (x)->argc-1 < (n)) \
+				CONF_ERROR(x,"missing parameters")
 
-#define CHECK_VARARGS(x, n, m)	if((x)->argc - 1 < n || (x)->argc - 1 > m) \
-				CONF_ERROR(x,"missing arguments")
+#define CHECK_VARARGS(x, n, m)	if ((x)->argc - 1 < n || (x)->argc - 1 > m) \
+				CONF_ERROR(x,"missing parameters")
 
-#define CHECK_HASARGS(x, n)	((x)->argc - 1) == n
+#define CHECK_HASARGS(x, n)	((x)->argc - 1) == (n)
 
 #define CHECK_CONF(x,p)		if (!check_context((x),(p))) \
 				CONF_ERROR((x), \
@@ -220,13 +173,13 @@ extern int			ServerUseReverseDNS;
 
 #define CHECK_CMD_ARGS(x, n)	\
   if ((x)->argc != (n)) { \
-    pr_response_add_err(R_501, _("Invalid number of arguments")); \
+    pr_response_add_err(R_501, _("Invalid number of parameters")); \
     return PR_ERROR((x)); \
   }
 
 #define CHECK_CMD_MIN_ARGS(x, n)	\
   if ((x)->argc < (n)) { \
-    pr_response_add_err(R_501, _("Invalid number of arguments")); \
+    pr_response_add_err(R_501, _("Invalid number of parameters")); \
     return PR_ERROR((x)); \
   }
 
@@ -242,43 +195,8 @@ void kludge_enable_umask(void);
 int pr_define_add(const char *, int);
 unsigned char pr_define_exists(const char *);
 
-void init_config(void);
 int fixup_servers(xaset_t *list);
-int parse_config_path(pool *, const char *);
-config_rec *add_config_set(xaset_t **, const char *);
-config_rec *add_config(server_rec *, const char *);
-config_rec *add_config_param(const char *, int, ...);
-config_rec *add_config_param_str(const char *, int, ...);
-config_rec *add_config_param_set(xaset_t **, const char *, int, ...);
-config_rec *pr_conf_add_server_config_param_str(server_rec *, const char *,
-  int, ...);
-
-/* Flags used when searching for specific config_recs in the in-memory
- * config database, particularly when 'recurse' is TRUE.
- */
-#define PR_CONFIG_FIND_FL_SKIP_ANON		0x001
-#define PR_CONFIG_FIND_FL_SKIP_DIR		0x002
-#define PR_CONFIG_FIND_FL_SKIP_LIMIT		0x004
-#define PR_CONFIG_FIND_FL_SKIP_DYNDIR		0x008
-
-config_rec *find_config_next(config_rec *, config_rec *, int,
-  const char *, int);
-config_rec *find_config_next2(config_rec *, config_rec *, int,
-  const char *, int, unsigned long);
-config_rec *find_config(xaset_t *, int, const char *, int);
-config_rec *find_config2(xaset_t *, int, const char *, int, unsigned long);
-void find_config_set_top(config_rec *);
-
-int remove_config(xaset_t *, const char *, int);
-
-#define PR_CONFIG_FL_INSERT_HEAD	0x001
-config_rec *pr_config_add_set(xaset_t **, const char *, int);
-config_rec *pr_config_add(server_rec *, const char *, int);
-
-/* Returns the assigned ID for the provided directive name, or zero
- * if no ID mapping was found.
- */
-unsigned int pr_config_get_id(const char *name);
+xaset_t *get_dir_ctxt(pool *, char *);
 
 /* Returns the buffer size to use for data transfers, regardless of IO
  * direction.
@@ -298,20 +216,6 @@ int pr_config_get_xfer_bufsz2(int);
  */
 int pr_config_get_server_xfer_bufsz(int);
 
-/* Assigns a unique ID for the given configuration directive.  The
- * mapping of directive to ID is stored in a lookup table, so that
- * searching of the config database by directive name can be done using
- * ID comparisons rather than string comparisons.
- *
- * Returns the ID assigned for the given directive, or zero if there was an
- * error.
- */
-unsigned int pr_config_set_id(const char *name);
-
-void *get_param_ptr(xaset_t *, const char *, int);
-void *get_param_ptr_next(const char *, int);
-xaset_t *get_dir_ctxt(pool *, char *);
-
 config_rec *dir_match_path(pool *, char *);
 void build_dyn_config(pool *, const char *, struct stat *, unsigned char);
 unsigned char dir_hide_file(const char *);
@@ -321,16 +225,16 @@ int dir_check(pool *, cmd_rec *, const char *, const char *, int *);
 int dir_check_canon(pool *, cmd_rec *, const char *, const char *, int *);
 int is_dotdir(const char *);
 int login_check_limits(xaset_t *, int, int, int *);
-char *path_subst_uservar(pool *, char **);
 void resolve_anonymous_dirs(xaset_t *);
 void resolve_deferred_dirs(server_rec *);
 void fixup_dirs(server_rec *, int);
 unsigned char check_context(cmd_rec *, int);
 char *get_context_name(cmd_rec *);
 int get_boolean(cmd_rec *, int);
-char *get_full_cmd(cmd_rec *);
+const char *get_full_cmd(cmd_rec *);
 
-void pr_config_dump(void (*)(const char *, ...), xaset_t *, char *);
+/* Internal use only. */
+void init_dirtree(void);
 
 #ifdef PR_USE_DEVEL
 void pr_dirs_dump(void (*)(const char *, ...), xaset_t *, char *);
diff --git a/include/display.h b/include/display.h
index 0fcf2f3..2cf656e 100644
--- a/include/display.h
+++ b/include/display.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2011 The ProFTPD Project team
+ * Copyright (c) 2004-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Display of files
- * $Id: display.h,v 1.7 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Display of files */
 
 #ifndef PR_DISPLAY_H
 #define PR_DISPLAY_H
@@ -46,18 +44,18 @@ struct fh_rec;
 
 /* Used to read the file handle given by fh, located on the filesystem fs, and
  * return the results, with variables expanded, to the client, using the
- * response code given by code.  Returns 0 if the file handle's contents
+ * response code given by resp_code.  Returns 0 if the file handle's contents
  * are displayed without issue, -1 otherwise (with errno set appropriately).
  */
-int pr_display_fh(struct fh_rec *fh, const char *fs, const char *code,
+int pr_display_fh(struct fh_rec *fh, const char *fs, const char *resp_code,
   int flags);
 
 /* Used to read the file given by path, located on the filesystem fs, and
  * return the results, with variables expanded, to the client, using the
- * response code given by code.  Returns 0 if the file is displayed without
+ * response code given by resp_code.  Returns 0 if the file is displayed without
  * issue, -1 otherwise (with errno set appropriately).
  */
-int pr_display_file(const char *path, const char *fs, const char *code,
+int pr_display_file(const char *path, const char *fs, const char *resp_code,
   int flags);
 
 #endif /* PR_DISPLAY_H */
diff --git a/include/encode.h b/include/encode.h
index aedfc06..08473a6 100644
--- a/include/encode.h
+++ b/include/encode.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006-2011 The ProFTPD Project team
+ * Copyright (c) 2006-2014 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* UTF8/charset encoding/decoding
- * $Id: encode.h,v 1.4 2011-05-23 20:35:35 castaglia Exp $
- */
+/* UTF8/charset encoding/decoding */
 
 #ifndef PR_ENCODE_H
 #define PR_ENCODE_H
@@ -49,6 +47,15 @@ void pr_encode_disable_encoding(void);
  */
 int pr_encode_enable_encoding(const char *encoding);
 
+unsigned long pr_encode_get_policy(void);
+int pr_encode_set_policy(unsigned long policy);
+
+/* Determines whether the Encode API will disconnect the client if the
+ * charset conversion fails, i.e. the client is using an illegal/unsupported
+ * encoding.
+ */
+#define PR_ENCODE_POLICY_FL_REQUIRE_VALID_ENCODING		0x001
+
 /* Returns string describing the current charset being used. */
 const char *pr_encode_get_charset(void);
 
diff --git a/include/env.h b/include/env.h
index 27f9fb7..c9e996a 100644
--- a/include/env.h
+++ b/include/env.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2011 The ProFTPD Project team
+ * Copyright (c) 2007-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Environment handling
- * $Id: env.h,v 1.3 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Environment handling */
 
 #ifndef PR_ENV_H
 #define PR_ENV_H
diff --git a/include/event.h b/include/event.h
index 35c5fd8..b2b6290 100644
--- a/include/event.h
+++ b/include/event.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2011 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Event management
- * $Id: event.h,v 1.6 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Event management */
 
 #ifndef PR_EVENT_H
 #define PR_EVENT_H
diff --git a/include/expr.h b/include/expr.h
index fbdb3b5..27b4c78 100644
--- a/include/expr.h
+++ b/include/expr.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Expression API definition
- * $Id: expr.h,v 1.3 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Expression API definition */
 
 #ifndef PR_EXPR_H
 #define PR_EXPR_H
@@ -47,7 +45,7 @@
  * would expect from the API.  Callers of this function MUST take this
  * into account.
  */
-array_header *pr_expr_create(pool *p, int *argc, char **argv);
+array_header *pr_expr_create(pool *p, unsigned int *argc, char **argv);
 
 int pr_expr_eval_class_and(char **);
 int pr_expr_eval_class_or(char **);
diff --git a/include/feat.h b/include/feat.h
index bc289f3..1e8fa6d 100644
--- a/include/feat.h
+++ b/include/feat.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Feature list management
- * $Id: feat.h,v 1.5 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Feature list management */
 
 #ifndef PR_FEAT_H
 #define PR_FEAT_H
diff --git a/include/filter.h b/include/filter.h
index 097245e..7394fb0 100644
--- a/include/filter.h
+++ b/include/filter.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2011 The ProFTPD Project team
+ * Copyright (c) 2009-2014 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@
  * 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.
- *
- * $Id: filter.h,v 1.3 2011-05-23 20:35:35 castaglia Exp $
  */
 
 #ifndef PR_FILTER_H
@@ -38,8 +36,8 @@
  * match a configured PathDenyFilter.
  */
 int pr_filter_allow_path(xaset_t *set, const char *path);
-#define PR_FILTER_ERR_FAILS_ALLOW_FILTER	-1
-#define PR_FILTER_ERR_FAILS_DENY_FILTER		-2
+#define PR_FILTER_ERR_FAILS_ALLOW_FILTER	-2
+#define PR_FILTER_ERR_FAILS_DENY_FILTER		-3
 
 /* Parse the optional flags parameter for PathAllowFilter, PathDenyFilter
  * directive configurations.
diff --git a/include/fsio.h b/include/fsio.h
index 42adc15..d428b00 100644
--- a/include/fsio.h
+++ b/include/fsio.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project
+ * Copyright (c) 2001-2017 The ProFTPD Project
  *
  * 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,15 +24,24 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD virtual/modular filesystem support.
- * $Id: fsio.h,v 1.37 2014-01-31 16:52:33 castaglia Exp $
- */
+/* ProFTPD virtual/modular filesystem support. */
 
 #ifndef PR_FSIO_H
 #define PR_FSIO_H
 
 #include "conf.h"
 
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+#  include <sys/extattr.h>
+# elif defined(HAVE_SYS_XATTR_H)
+#  include <sys/xattr.h>
+#  if defined(HAVE_ATTR_XATTR_H)
+#   include <attr/xattr.h>
+#  endif /* HAVE_ATTR_XATTR_H */
+# endif /* HAVE_SYS_XATTR_H */
+#endif /* PR_USE_XATTR */
+
 /* This is a Tru64-specific hack, to work around some macro funkiness
  * in their /usr/include/sys/mount.h header.
  */
@@ -46,7 +55,7 @@
 #define FSIO_FILE_RENAME	(1 << 2)
 #define FSIO_FILE_UNLINK	(1 << 3)
 #define FSIO_FILE_OPEN		(1 << 4)
-#define FSIO_FILE_CREAT		(1 << 5)
+/* Was FSIO_FILE_CREAT, now unused */
 #define FSIO_FILE_CLOSE		(1 << 6)
 #define FSIO_FILE_READ		(1 << 7)
 #define FSIO_FILE_WRITE		(1 << 8)
@@ -58,10 +67,18 @@
 #define FSIO_FILE_CHOWN		(1 << 14)
 #define FSIO_FILE_ACCESS	(1 << 15)
 #define FSIO_FILE_UTIMES	(1 << 23)
+#define FSIO_FILE_GETXATTR	(1 << 24)
+#define FSIO_FILE_LGETXATTR	(1 << 25)
+#define FSIO_FILE_LISTXATTR	(1 << 26)
+#define FSIO_FILE_LLISTXATTR	(1 << 27)
+#define FSIO_FILE_REMOVEXATTR	(1 << 28)
+#define FSIO_FILE_LREMOVEXATTR	(1 << 29)
+#define FSIO_FILE_SETXATTR	(1 << 30)
+#define FSIO_FILE_LSETXATTR	(1 << 31)
 
 /* Macro that defines the most common file ops */
 #define FSIO_FILE_COMMON	(FSIO_FILE_OPEN|FSIO_FILE_READ|FSIO_FILE_WRITE|\
-                                 FSIO_FILE_CLOSE|FSIO_FILE_CREAT)
+                                 FSIO_FILE_CLOSE)
 
 #define FSIO_DIR_CHROOT		(1 << 16)
 #define FSIO_DIR_CHDIR		(1 << 17)
@@ -110,7 +127,6 @@ struct fs_rec {
   int (*rename)(pr_fs_t *, const char *, const char *);
   int (*unlink)(pr_fs_t *, const char *);
   int (*open)(pr_fh_t *, const char *, int);
-  int (*creat)(pr_fh_t *, const char *, mode_t);
   int (*close)(pr_fh_t *, int);
   int (*read)(pr_fh_t *, int, char *, size_t);
   int (*write)(pr_fh_t *, int, const char *, size_t);
@@ -129,6 +145,25 @@ struct fs_rec {
   int (*faccess)(pr_fh_t *, int, uid_t, gid_t, array_header *);
   int (*utimes)(pr_fs_t *, const char *, struct timeval *);
   int (*futimes)(pr_fh_t *, int, struct timeval *);
+  int (*fsync)(pr_fh_t *, int);
+
+  /* Extended attribute support */
+  ssize_t (*getxattr)(pool *, pr_fs_t *, const char *, const char *, void *,
+    size_t);
+  ssize_t (*lgetxattr)(pool *, pr_fs_t *, const char *, const char *, void *,
+    size_t);
+  ssize_t (*fgetxattr)(pool *, pr_fh_t *, int, const char *, void *, size_t);
+  int (*listxattr)(pool *, pr_fs_t *, const char *, array_header **);
+  int (*llistxattr)(pool *, pr_fs_t *, const char *, array_header **);
+  int (*flistxattr)(pool *, pr_fh_t *, int, array_header **);
+  int (*removexattr)(pool *, pr_fs_t *, const char *, const char *);
+  int (*lremovexattr)(pool *, pr_fs_t *, const char *, const char *);
+  int (*fremovexattr)(pool *, pr_fh_t *, int, const char *);
+  int (*setxattr)(pool *, pr_fs_t *, const char *, const char *, void *,
+    size_t, int);
+  int (*lsetxattr)(pool *, pr_fs_t *, const char *, const char *, void *,
+    size_t, int);
+  int (*fsetxattr)(pool *, pr_fh_t *, int, const char *, void *, size_t, int);
 
   /* For actual operations on the directory (or subdirs)
    * we cast the return from opendir to DIR* in src/fs.c, so
@@ -165,6 +200,12 @@ struct fs_rec {
    * command to complete.
    */
   int allow_xdev_rename;
+
+  /* This flag determines whether the paths handled by this FS handler
+   * are standard, filesystem-based paths, and such use the standard
+   * path separator, glob semantics, etc.
+   */
+  int non_std_path;
 };
 
 struct fh_rec {
@@ -196,40 +237,6 @@ struct fh_rec {
  */
 #define PR_FH_FD(f)	((f)->fh_fd)
 
-#if defined(PR_USE_REGEX) && defined(PR_FS_MATCH)
-typedef struct fs_match_rec pr_fs_match_t;
-struct fs_match_rec {
-
-  pr_fs_match_t *fsm_next, *fsm_prev;
-
-  /* pool for this object's use */
-  pool *fsm_pool;
-
-  /* descriptive tag for this fs regex */
-  char *fsm_name;
-
-  /* mask of the fs operations to which this regex should apply */
-  int fsm_opmask;
-
-  /* string containing the match pattern */
-  char *fsm_pattern;
-
-  /* compiled pattern (regex) */
-  regex_t *fsm_regex;
-
-  /* "trigger" function to be called whenever a path that matches the
-   * compiled regex is given.
-   */
-  int (*trigger)(pr_fh_t *, const char *, int);
-
-  /* NOTE: need some way of keeping track of the pr_fs_t registered by
-   *  an fs_match's trigger function, such that when an fs_match is
-   *  removed, its registered pr_fs_t's are removed as well.
-   */
-  array_header *fsm_fs_objs;
-};
-#endif /* PR_USE_REGEX and PR_FS_MATCH */
-
 int pr_fsio_stat(const char *, struct stat *);
 int pr_fsio_stat_canon(const char *, struct stat *);
 int pr_fsio_fstat(pr_fh_t *, struct stat *);
@@ -251,8 +258,6 @@ int pr_fsio_unlink(const char *);
 int pr_fsio_unlink_canon(const char *);
 pr_fh_t *pr_fsio_open(const char *, int);
 pr_fh_t *pr_fsio_open_canon(const char *, int);
-pr_fh_t *pr_fsio_creat(const char *, mode_t);
-pr_fh_t *pr_fsio_creat_canon(const char *, mode_t);
 int pr_fsio_close(pr_fh_t *);
 int pr_fsio_read(pr_fh_t *, char *, size_t);
 int pr_fsio_write(pr_fh_t *, const char *, size_t);
@@ -274,9 +279,29 @@ int pr_fsio_chroot(const char *);
 int pr_fsio_access(const char *, int, uid_t, gid_t, array_header *);
 int pr_fsio_faccess(pr_fh_t *, int, uid_t, gid_t, array_header *);
 int pr_fsio_utimes(const char *, struct timeval *);
+int pr_fsio_utimes_with_root(const char *, struct timeval *);
 int pr_fsio_futimes(pr_fh_t *, struct timeval *);
+int pr_fsio_fsync(pr_fh_t *fh);
 off_t pr_fsio_lseek(pr_fh_t *, off_t, int);
 
+/* Extended attribute support */
+ssize_t pr_fsio_getxattr(pool *p, const char *, const char *, void *, size_t);
+ssize_t pr_fsio_lgetxattr(pool *, const char *, const char *, void *, size_t);
+ssize_t pr_fsio_fgetxattr(pool *, pr_fh_t *, const char *, void *, size_t);
+int pr_fsio_listxattr(pool *, const char *, array_header **);
+int pr_fsio_llistxattr(pool *, const char *, array_header **);
+int pr_fsio_flistxattr(pool *, pr_fh_t *, array_header **);
+int pr_fsio_removexattr(pool *, const char *, const char *);
+int pr_fsio_lremovexattr(pool *, const char *, const char *);
+int pr_fsio_fremovexattr(pool *, pr_fh_t *, const char *);
+int pr_fsio_setxattr(pool *, const char *, const char *, void *, size_t, int);
+int pr_fsio_lsetxattr(pool *, const char *, const char *, void *, size_t, int);
+int pr_fsio_fsetxattr(pool *, pr_fh_t *, const char *, void *, size_t, int);
+
+/* setxattr flags */
+#define PR_FSIO_XATTR_FL_CREATE		0x001
+#define PR_FSIO_XATTR_FL_REPLACE	0x002
+
 /* Set a flag determining whether we guard against write operations in
  * certain sensitive directories while we are chrooted, e.g. "Roaring Beast"
  * style attacks.
@@ -288,9 +313,16 @@ int pr_fsio_guard_chroot(int);
  */
 int pr_fsio_set_use_mkdtemp(int);
 
+/* Sets a bitmask of various FSIO API options.  Returns the previously
+ * set options.
+ */
+unsigned long pr_fsio_set_options(unsigned long opts);
+#define PR_FSIO_OPT_IGNORE_XATTR		0x00001
+
 /* FS-related functions */
 
-char *pr_fsio_getline(char *, int, pr_fh_t *, unsigned int *);
+char *pr_fsio_getline(char *, size_t, pr_fh_t *, unsigned int *);
+char *pr_fsio_getpipebuf(pool *, int, long *);
 char *pr_fsio_gets(char *, size_t, pr_fh_t *);
 int pr_fsio_puts(const char *, pr_fh_t *);
 int pr_fsio_set_block(pr_fh_t *);
@@ -303,18 +335,44 @@ pr_fs_t *pr_remove_fs(const char *);
 pr_fs_t *pr_unmount_fs(const char *, const char *);
 int pr_unregister_fs(const char *);
 
-#if defined(PR_USE_REGEX) && defined(PR_FS_MATCH)
-pr_fs_match_t *pr_register_fs_match(pool *, const char *, const char *, int);
-void pr_associate_fs(pr_fs_match_t *, pr_fs_t *);
-pr_fs_match_t *pr_create_fs_match(pool *, const char *, const char *, int);
-pr_fs_match_t *pr_get_fs_match(const char *, int);
-pr_fs_match_t *pr_get_next_fs_match(pr_fs_match_t *, const char *, int);
-int pr_insert_fs_match(pr_fs_match_t *);
-int pr_unregister_fs_match(const char *);
-#endif /* PR_USE_REGEX and PR_FS_MATCH */
-
+/* FS Statcache API */
 void pr_fs_clear_cache(void);
-int pr_fs_copy_file(const char *, const char *);
+int pr_fs_clear_cache2(const char *path);
+
+/* Dump the current contents of the statcache via trace logging, to the
+ * "fs.statcache" trace channel.
+ */
+void pr_fs_statcache_dump(void);
+
+/* Clears the entire statcache. */
+void pr_fs_statcache_free(void);
+
+/* Clears the entire statcache and re-creates the memory pool. */
+void pr_fs_statcache_reset(void);
+
+/* Tune the statcache policy: max number of items in the cache at any
+ * one time, the max age (in seconds) for items in the cache, and the policy
+ * flags.
+ *
+ * Note that setting a size of zero, OR setting a max age of zero, effectively
+ * disables the statcache.
+ */
+int pr_fs_statcache_set_policy(unsigned int size, unsigned int max_age,
+  unsigned int flags);
+
+/* Copy a file from the given source path to the destination path. */
+int pr_fs_copy_file(const char *src, const char *dst);
+
+/* Similar to pr_fs_copy_file(), with the addition of an optional progress
+ * callback, invoked during the potentially long-running copy process.
+ *
+ * The callback, when present, will be invoked with the number of bytes
+ * just written to the destination file in that iteration.
+ */
+int pr_fs_copy_file2(const char *src, const char *dst, int flags,
+  void (*progress_cb)(int));
+#define PR_FSIO_COPY_FILE_FL_NO_DELETE_ON_FAILURE	0x0001
+
 int pr_fs_setcwd(const char *);
 const char *pr_fs_getcwd(void);
 const char *pr_fs_getvwd(void);
@@ -323,8 +381,27 @@ int pr_fs_interpolate(const char *, char *, size_t);
 int pr_fs_resolve_partial(const char *, char *, size_t, int);
 int pr_fs_resolve_path(const char *, char *, size_t, int);
 char *pr_fs_decode_path(pool *, const char *);
+
+/* Similar to pr_fs_decode_path(), but allows callers to provide flags.  These
+ * flags can be used, for example, to request that if there are errors during
+ * the decoding, the function NOT hide/mask them, as is done by default, but
+ * convey them to the caller for handling at a higher code layer.
+ */ 
+char *pr_fs_decode_path2(pool *, const char *, int);
+#define FSIO_DECODE_FL_TELL_ERRORS		0x001
+
 char *pr_fs_encode_path(pool *, const char *);
 int pr_fs_use_encoding(int);
+
+/* Split the given path into its individual path components. */
+array_header *pr_fs_split_path(pool *p, const char *path);
+
+/* Given an array of individual path components, join them into a single
+ * path.  The count parameter indicates how many components in the array,
+ * starting from zero, to use.
+ */
+char *pr_fs_join_path(pool *p, array_header *components, size_t count);
+
 int pr_fs_valid_path(const char *);
 void pr_fs_virtual_path(const char *, char *, size_t);
 
@@ -364,12 +441,27 @@ int pr_fs_getsize2(char *, off_t *);
  */
 int pr_fs_fgetsize(int, off_t *);
 
+/* Perform access(2)-like checks on the given struct stat. */
+int pr_fs_have_access(struct stat *st, int mode, uid_t uid, gid_t gid,
+  array_header *suppl_gids);
+
 /* Returns TRUE if the given path is on an NFS-mounted filesystem, FALSE
  * if not on an NFS-mounted filesystem, and -1 if there was an error
  * determining which (with errno set appropriately).
  */
 int pr_fs_is_nfs(const char *path);
 
+/* Provide advice/hints to the OS about what we are going to do with the
+ * given section of the opened file.
+ */
+void pr_fs_fadvise(int fd, off_t offset, off_t len, int advice);
+#define PR_FS_FADVISE_NORMAL		10
+#define PR_FS_FADVISE_RANDOM		11
+#define PR_FS_FADVISE_SEQUENTIAL	12
+#define PR_FS_FADVISE_WILLNEED		13
+#define PR_FS_FADVISE_DONTNEED		14
+#define PR_FS_FADVISE_NOREUSE		15
+
 /* For internal use only. */
 int init_fs(void);
 
diff --git a/include/ftp.h b/include/ftp.h
index 5171a46..856ee60 100644
--- a/include/ftp.h
+++ b/include/ftp.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* FTP commands and responses (may not all be implemented)
- * $Id: ftp.h,v 1.14 2011-05-23 20:35:35 castaglia Exp $
- */
+/* FTP commands and responses (may not all be implemented) */
 
 #ifndef PR_FTP_H
 #define PR_FTP_H
@@ -79,6 +77,8 @@
 #define C_FEAT	"FEAT"		/* Request list of server-supported features */
 #define C_OPTS	"OPTS"		/* Specify options for FTP commands */
 #define C_LANG	"LANG"		/* Request a specific language */
+#define C_HOST	"HOST"		/* Request a named server */
+#define C_CLNT	"CLNT"		/* Client-offered identification */
 
 /* RFC2228 FTP Security commands */
 #define C_ADAT  "ADAT"		/* Authentication/security data */
@@ -93,7 +93,6 @@
 /* Proposed commands */
 #define C_MFF	"MFF"		/* Modify File Fact (RFC3659) */
 #define C_MFMT	"MFMT"		/* Modify File Modify-Type (RFC3659) */
-#define C_HOST	"HOST"		/* Virtual host requested */
 
 #define C_ANY	"*"		/* Special "wildcard" matching command */
 
diff --git a/include/tpl.h b/include/hanson-tpl.h
similarity index 98%
rename from include/tpl.h
rename to include/hanson-tpl.h
index e23953b..ac4bcb1 100755
--- a/include/tpl.h
+++ b/include/hanson-tpl.h
@@ -114,6 +114,7 @@ typedef struct tpl_gather_t {
 typedef int (tpl_gather_cb)(void *img, size_t sz, void *data);
 
 /* Prototypes */
+TPL_API void tpl_fatal(char *fmt, ...);		/* default tpl_hook fatal fcn */
 TPL_API tpl_node *tpl_map(char *fmt,...);       /* define tpl using format */
 TPL_API void tpl_free(tpl_node *r);             /* free a tpl map */
 TPL_API int tpl_pack(tpl_node *r, int i);       /* pack the n'th packable */
diff --git a/include/help.h b/include/help.h
index bd700d4..207ffe3 100644
--- a/include/help.h
+++ b/include/help.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* HELP management
- * $Id: help.h,v 1.2 2011-05-23 20:35:35 castaglia Exp $
- */
+/* HELP management */
 
 #ifndef PR_HELP_H
 #define PR_HELP_H
diff --git a/include/ident.h b/include/ident.h
index d54afd3..ca4b7ca 100644
--- a/include/ident.h
+++ b/include/ident.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001, 2002, 2003 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,8 +22,6 @@
  * 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.
- *
- * $Id: ident.h,v 1.9 2003-02-12 08:49:14 castaglia Exp $
  */
 
 #ifndef PR_IDENT_H
diff --git a/include/inet.h b/include/inet.h
index 72405c8..6ca12ad 100644
--- a/include/inet.h
+++ b/include/inet.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* BSD socket manipulation tools.
- * $Id: inet.h,v 1.40 2013-02-07 15:44:29 castaglia Exp $
- */
+/* BSD socket manipulation tools. */
 
 #ifndef PR_INET_H
 #define PR_INET_H
@@ -112,7 +110,7 @@ typedef struct conn_struc {
   pr_netio_stream_t *instrm, *outstrm;	/* Input/Output streams */
 
   /* Remote address of the connection. */
-  pr_netaddr_t *remote_addr;
+  const pr_netaddr_t *remote_addr;
 
   /* Remote port of the connection. */
   int remote_port;
@@ -121,7 +119,7 @@ typedef struct conn_struc {
   const char *remote_name;
 
   /* Local address of the connection. */
-  pr_netaddr_t *local_addr;
+  const pr_netaddr_t *local_addr;
 
   /* Local port of the connection. */
   int local_port;
@@ -131,7 +129,7 @@ typedef struct conn_struc {
 /* Used for event data for events related to opening of sockets */
 struct socket_ctx {
   server_rec *server;
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   int sockfd;
 };
 
@@ -140,9 +138,9 @@ void pr_inet_clear(void);
 int pr_inet_reverse_dns(pool *, int);
 int pr_inet_getservport(pool *, const char *, const char *);
 pr_netaddr_t *pr_inet_getaddr(pool *, const char *, array_header **);
-conn_t *pr_inet_copy_conn(pool *, conn_t*);
-conn_t *pr_inet_create_conn(pool *, int, pr_netaddr_t *, int, int);
-conn_t *pr_inet_create_conn_portrange(pool *, pr_netaddr_t *, int, int);
+conn_t *pr_inet_copy_conn(pool *, conn_t *);
+conn_t *pr_inet_create_conn(pool *, int, const pr_netaddr_t *, int, int);
+conn_t *pr_inet_create_conn_portrange(pool *, const pr_netaddr_t *, int, int);
 void pr_inet_close(pool *, conn_t *);
 void pr_inet_lingering_abort(pool *, conn_t *, long);
 void pr_inet_lingering_close(pool *, conn_t *, long);
@@ -160,14 +158,14 @@ int pr_inet_listen(pool *p, conn_t *conn, int backlog, int flags);
 
 int pr_inet_resetlisten(pool *, conn_t *);
 int pr_inet_accept_nowait(pool *, conn_t *);
-int pr_inet_connect(pool *, conn_t *, pr_netaddr_t *, int);
-int pr_inet_connect_nowait(pool *, conn_t *, pr_netaddr_t *, int);
+int pr_inet_connect(pool *, conn_t *, const pr_netaddr_t *, int);
+int pr_inet_connect_nowait(pool *, conn_t *, const pr_netaddr_t *, int);
 int pr_inet_get_conn_info(conn_t *, int);
 conn_t *pr_inet_accept(pool *, conn_t *, conn_t *, int, int, unsigned char);
-conn_t *pr_inet_openrw(pool *, conn_t *, pr_netaddr_t *, int, int, int,
+conn_t *pr_inet_openrw(pool *, conn_t *, const pr_netaddr_t *, int, int, int,
   int, int);
-int pr_inet_generate_socket_event(const char *, server_rec *, pr_netaddr_t *,
-  int);
+int pr_inet_generate_socket_event(const char *, server_rec *,
+  const pr_netaddr_t *, int);
 
 void init_inet(void);
 
diff --git a/include/json.h b/include/json.h
new file mode 100644
index 0000000..247e0bc
--- /dev/null
+++ b/include/json.h
@@ -0,0 +1,153 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* JSON API */
+
+#ifndef PR_JSON_H
+#define PR_JSON_H
+
+#include "conf.h"
+
+typedef struct json_list_st pr_json_array_t;
+typedef struct json_obj_st pr_json_object_t;
+
+/* JSON Types */
+
+#define PR_JSON_TYPE_BOOL		1
+#define PR_JSON_TYPE_NUMBER		2
+#define PR_JSON_TYPE_NULL		3
+#define PR_JSON_TYPE_STRING		4
+#define PR_JSON_TYPE_ARRAY		5
+#define PR_JSON_TYPE_OBJECT		6
+
+/* JSON Objects */
+
+pr_json_object_t *pr_json_object_alloc(pool *p);
+
+int pr_json_object_free(pr_json_object_t *json);
+
+pr_json_object_t *pr_json_object_from_text(pool *p, const char *text);
+
+char *pr_json_object_to_text(pool *p, const pr_json_object_t *json,
+  const char *indent);
+
+/* Returns the number of members (keys) in the given object. */
+int pr_json_object_count(const pr_json_object_t *json);
+
+/* Removes the object member under this key. */
+int pr_json_object_remove(pr_json_object_t *json, const char *key);
+
+/* Checks where a member under the given key exists.  Returns TRUE, FALSE,
+ * or -1 is there was some other error.
+ */
+int pr_json_object_exists(const pr_json_object_t *json, const char *key);
+
+int pr_json_object_get_bool(pool *p, const pr_json_object_t *json,
+  const char *key, int *val);
+int pr_json_object_set_bool(pool *p, pr_json_object_t *json, const char *key,
+  int val);
+
+int pr_json_object_get_null(pool *p, const pr_json_object_t *json,
+  const char *key);
+int pr_json_object_set_null(pool *p, pr_json_object_t *json, const char *key);
+
+int pr_json_object_get_number(pool *p, const pr_json_object_t *json,
+  const char *key, double *val);
+int pr_json_object_set_number(pool *p, pr_json_object_t *json, const char *key,
+  double val);
+
+int pr_json_object_get_string(pool *p, const pr_json_object_t *json,
+  const char *key, char **val);
+int pr_json_object_set_string(pool *p, pr_json_object_t *json, const char *key,
+  const char *val);
+
+int pr_json_object_get_array(pool *p, const pr_json_object_t *json,
+  const char *key, pr_json_array_t **val);
+int pr_json_object_set_array(pool *p, pr_json_object_t *json, const char *key,
+  const pr_json_array_t *val);
+
+int pr_json_object_get_object(pool *p, const pr_json_object_t *json,
+  const char *key, pr_json_object_t **val);
+int pr_json_object_set_object(pool *p, pr_json_object_t *json, const char *key,
+  const pr_json_object_t *val);
+
+/* JSON Arrays */
+
+pr_json_array_t *pr_json_array_alloc(pool *p);
+
+int pr_json_array_free(pr_json_array_t *json);
+
+pr_json_array_t *pr_json_array_from_text(pool *p, const char *text);
+
+char *pr_json_array_to_text(pool *p, const pr_json_array_t *json,
+  const char *indent);
+
+/* Returns the number of items in the given array. */
+int pr_json_array_count(const pr_json_array_t *json);
+
+/* Removes the array item under this key. */
+int pr_json_array_remove(pr_json_array_t *json, unsigned int idx);
+
+/* Checks where an item at the given index exists.  Returns TRUE, FALSE,
+ * or -1 is there was some other error.
+ */
+int pr_json_array_exists(const pr_json_array_t *json, unsigned int idx);
+
+int pr_json_array_append_bool(pool *p, pr_json_array_t *json, int val);
+int pr_json_array_get_bool(pool *p, const pr_json_array_t *json,
+  unsigned int idx, int *val);
+
+int pr_json_array_append_null(pool *p, pr_json_array_t *json);
+int pr_json_array_get_null(pool *p, const pr_json_array_t *json,
+  unsigned int idx);
+
+int pr_json_array_append_number(pool *p, pr_json_array_t *json, double val);
+int pr_json_array_get_number(pool *p, const pr_json_array_t *json,
+  unsigned int idx, double *val);
+
+int pr_json_array_append_string(pool *p, pr_json_array_t *json,
+  const char *val);
+int pr_json_array_get_string(pool *p, const pr_json_array_t *json,
+  unsigned int idx, char **val);
+
+int pr_json_array_append_array(pool *p, pr_json_array_t *json,
+  const pr_json_array_t *val);
+int pr_json_array_get_array(pool *p, const pr_json_array_t *json,
+  unsigned int idx, pr_json_array_t **val);
+
+int pr_json_array_append_object(pool *p, pr_json_array_t *json,
+  const pr_json_object_t *val);
+int pr_json_array_get_object(pool *p, const pr_json_array_t *json,
+  unsigned int idx, pr_json_object_t **val);
+
+/* Miscellaneous */
+
+/* Validates that the given text is a valid JSON string. */
+int pr_json_text_validate(pool *p, const char *text);
+
+/* Internal use only. */
+int init_json(void);
+int finish_json(void);
+
+#endif /* PR_JSON_H */
diff --git a/include/lastlog.h b/include/lastlog.h
index de7589a..d5d0e40 100644
--- a/include/lastlog.h
+++ b/include/lastlog.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006 The ProFTPD Project team
+ * Copyright (c) 2006-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Lastlog API
- * $Id: lastlog.h,v 1.1 2006-06-22 20:37:07 castaglia Exp $
- */
+/* Lastlog API */
 
 #ifndef PR_LASTLOG_H
 #define PR_LASTLOG_H
@@ -54,7 +52,7 @@
 #endif
 
 int log_lastlog(uid_t uid, const char *user_name, const char *tty,
-  pr_netaddr_t *remote_addr);
+  const pr_netaddr_t *remote_addr);
 #endif /* PR_USE_LASTLOG */
 
 #endif /* PR_LASTLOG_H */
diff --git a/include/libsupp.h b/include/libsupp.h
index d036eb2..49c69ff 100644
--- a/include/libsupp.h
+++ b/include/libsupp.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * Parts Copyright (C) 1991, 1992, 1993, 1999, 2000 Free Software
  *   Foundation, Inc.
@@ -27,9 +27,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD support library definitions.
- * $Id: libsupp.h,v 1.17 2013-06-22 04:59:35 castaglia Exp $
- */
+/* ProFTPD support library definitions. */
 
 #include <glibc-glob.h>
 
diff --git a/include/log.h b/include/log.h
index af4db26..1badc7b 100644
--- a/include/log.h
+++ b/include/log.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -26,8 +26,6 @@
 
 /* Logging, either to syslog or stderr, as well as debug logging
  * and debug levels.
- *
- * $Id: log.h,v 1.38 2013-10-07 01:29:05 castaglia Exp $
  */
 
 #ifndef PR_LOG_H
@@ -63,7 +61,6 @@
 
 /* Log file modes */
 #define PR_LOG_SYSTEM_MODE	0640
-#define PR_LOG_XFER_MODE	0644
 
 #ifdef PR_USE_LASTLOG
 
@@ -102,8 +99,9 @@
 # endif
 #endif
 
+
 int log_lastlog(uid_t uid, const char *user_name, const char *tty,
-  pr_netaddr_t *remote_addr);
+  const pr_netaddr_t *remote_addr);
 #endif /* PR_USE_LASTLOG */
 
 /* Note: Like lastlog.h, it would be tempting to split out the declaration of
@@ -111,7 +109,7 @@ int log_lastlog(uid_t uid, const char *user_name, const char *tty,
  * wtmp.h file.  But that would collide with the system wtmp.h file on
  * some systems.
  */
-int log_wtmp(const char *, const char *, const char *, pr_netaddr_t *);
+int log_wtmp(const char *, const char *, const char *, const pr_netaddr_t *);
 
 /* file-based logging functions */
 int pr_log_openfile(const char *, int *, mode_t);
@@ -159,7 +157,8 @@ void pr_log_debug(int, const char *, ...)
        ;
 #endif
 
-int  pr_log_setdebuglevel(int);
+int pr_log_setdebuglevel(int);
+int pr_log_setdefaultlevel(int);
 
 void log_stderr(int);
 void log_discard(void);
@@ -218,4 +217,11 @@ int pr_log_event_generate(unsigned int log_type, int log_fd, int log_level,
  */
 int pr_log_event_listening(unsigned int log_type);
 
+/* Log a stacktrace, starting at the location of the calling function.
+ * Note that if fd is less than zero, OR if the given name is null, then the
+ * stacktrace will be logged using pr_log_pri(), otherwise the stacktrace will
+ * be written to the provided file descriptor.
+ */
+void pr_log_stacktrace(int fd, const char *name);
+
 #endif /* PR_LOG_H */
diff --git a/include/mod_log.h b/include/logfmt.h
similarity index 88%
rename from include/mod_log.h
rename to include/logfmt.h
index b398c2d..9a90be1 100644
--- a/include/mod_log.h
+++ b/include/logfmt.h
@@ -1,7 +1,6 @@
 /*
- * ProFTPD: mod_log
- *
- * Copyright (c) 2013 TJ Saunders
+ * ProFTPD: LogFormat
+ * Copyright (c) 2013-2017 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
@@ -21,12 +20,10 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_log.h,v 1.2 2013-11-11 01:34:04 castaglia Exp $
  */
 
-#ifndef MOD_LOG_H
-#define MOD_LOG_H
+#ifndef PR_LOGFMT_H
+#define PR_LOGFMT_H
 
 /* These "meta" sequences represent the parsed LogFormat variables. */
 #define LOGFMT_META_START		0xff
@@ -75,5 +72,11 @@
 #define LOGFMT_META_ISO8601		42
 #define LOGFMT_META_GROUP		43
 #define LOGFMT_META_BASENAME		44
+#define LOGFMT_META_FILE_OFFSET		45
+#define LOGFMT_META_XFER_MS		46
+#define LOGFMT_META_RESPONSE_MS		47
+#define LOGFMT_META_FILE_SIZE		48
+#define LOGFMT_META_XFER_TYPE		49
+#define LOGFMT_META_REMOTE_PORT		50
 
-#endif /* MOD_LOG_H */
+#endif /* PR_LOGFMT_H */
diff --git a/include/memcache.h b/include/memcache.h
index e08a611..2b0a92b 100644
--- a/include/memcache.h
+++ b/include/memcache.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2010-2011 The ProFTPD Project team
+ * Copyright (c) 2010-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,15 +22,12 @@
  * OpenSSL in the source distribution.
  */
 
-/* Memcache support
- * $Id: memcache.h,v 1.12 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Memcache support */
 
 #ifndef PR_MEMCACHE_H
 #define PR_MEMCACHE_H
 
 #include "conf.h"
-#include "tpl.h"
 
 typedef struct mcache_rec pr_memcache_t;
 
@@ -44,6 +41,12 @@ pr_memcache_t *pr_memcache_conn_new(pool *p, module *owner,
   unsigned long flags, uint64_t nreplicas);
 int pr_memcache_conn_close(pr_memcache_t *mcache);
 
+/* Given an existing handle, quit that handle, and clone the internal
+ * structures.  This is to be used by modules which need to get their own
+ * process-specific handle, using a handle inherited from their parent process.
+ */
+int pr_memcache_conn_clone(pool *p, pr_memcache_t *mcache);
+
 /* Set a namespace key prefix, to be used by this connection for all of the
  * operations involving items.  In practice, the key prefix should always
  * be a string which does contain any space characters.
diff --git a/include/mkhome.h b/include/mkhome.h
index 145ce75..d87bac5 100644
--- a/include/mkhome.h
+++ b/include/mkhome.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Home-on-demand support
- * $Id: mkhome.h,v 1.3 2012-09-05 16:40:58 castaglia Exp $
- */
+/* Home-on-demand support */
 
 #ifndef PR_MKHOME_H
 #define PR_MKHOME_H
diff --git a/include/mod_ctrls.h b/include/mod_ctrls.h
index 8186524..14e92a3 100644
--- a/include/mod_ctrls.h
+++ b/include/mod_ctrls.h
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_ctrls -- a module implementing the ftpdctl local socket
  *                       server
- *
- * Copyright (c) 2000-2011 TJ Saunders
+ * Copyright (c) 2000-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,8 +24,6 @@
  *
  * This is mod_ctrls, contrib software for proftpd 1.2 and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_ctrls.h,v 1.5 2011-05-23 20:35:35 castaglia Exp $
  */
 
 #ifndef MOD_CTRLS_H
diff --git a/include/modules.h b/include/modules.h
index 73b6f72..a59e391 100644
--- a/include/modules.h
+++ b/include/modules.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD module definitions.
- * $Id: modules.h,v 1.59 2012-04-15 18:04:14 castaglia Exp $
- */
+/* ProFTPD module definitions. */
 
 #ifndef PR_MODULES_H
 #define PR_MODULES_H
@@ -36,10 +34,10 @@ typedef struct modret_struc	modret_t;
 
 struct modret_struc {
   module *mr_handler_module;		/* which module handled this? */
-  int    mr_error;			/* !0 if error */
-  char   *mr_numeric;			/* numeric error code */
-  char   *mr_message;			/* text message */
-  void	 *data;				/* add'l data -- undefined */
+  int mr_error;				/* !0 if error */
+  const char *mr_numeric;		/* numeric error code */
+  const char *mr_message;		/* text message */
+  void *data;				/* add'l data -- undefined */
 };
 
 /* The following macros are for creating basic modret_t, and can
@@ -68,7 +66,7 @@ struct modret_struc {
 
 typedef struct conftab_rec {
   char *directive;
-  modret_t *(*handler)(cmd_rec*);
+  modret_t *(*handler)(cmd_rec *);
 
   module *m;				/* Reference to owning module
 					 * set when module is initialized
@@ -87,12 +85,14 @@ typedef struct conftab_rec {
 #define CL_MISC				(1 << 5) /* Miscellaneous (RNFR/RNTO, SITE, etc) */
 #define CL_SEC				(1 << 6) /* RFC2228 Security commands */
 #define CL_EXIT				(1 << 7) /* Session exit */
+#define CL_SSH				(1 << 8) /* SSH requests */
+#define CL_SFTP				(1 << 9) /* SFTP requests */
 
 /* Note that CL_ALL explicitly does NOT include CL_EXIT; this is to preserve
  * backward compatible behavior.
  */
 #define CL_ALL				(CL_AUTH|CL_INFO|CL_DIRS|CL_READ| \
-					CL_WRITE|CL_MISC|CL_SEC)
+					CL_WRITE|CL_MISC|CL_SEC|CL_SSH|CL_SFTP)
 
 /* Command handler types for command table */
 #define PRE_CMD				1
@@ -107,11 +107,11 @@ typedef struct cmdtab_rec {
 
   /* See above for cmd types. */
   unsigned char cmd_type;
-  char *command;
+  const char *command;
 
   /* Command group. */
-  char *group;
-  modret_t *(*handler)(cmd_rec*);
+  const char *group;
+  modret_t *(*handler)(cmd_rec *);
 
   /* Does this command require authentication? */
   unsigned char requires_auth;
@@ -126,8 +126,8 @@ typedef struct cmdtab_rec {
 
 typedef struct authtab_rec {
   int auth_flags;			/* future use */
-  char *name;
-  modret_t *(*handler)(cmd_rec*);
+  const char *name;
+  modret_t *(*handler)(cmd_rec *);
 
   module *m;
 } authtable;
@@ -135,10 +135,10 @@ typedef struct authtab_rec {
 #define PR_AUTH_FL_REQUIRED		0x00001
 
 struct module_struc {
-  module *next,*prev;
+  module *next, *prev;
 
   int api_version;			/* API version _not_ module version */
-  char *name;				/* Module name */
+  const char *name;			/* Module name */
 
   struct conftab_rec *conftable;	/* Configuration directive table */
   struct cmdtab_rec *cmdtable;		/* Command table */
@@ -147,7 +147,7 @@ struct module_struc {
   int (*init)(void); 			/* Module initialization */
   int (*sess_init)(void);		/* Session initialization */
 
-  char *module_version;			/* Module version */
+  const char *module_version;		/* Module version */
   void *handle;				/* Module handle */
 
   /* Internal use; high number == higher priority. */
@@ -158,9 +158,10 @@ struct module_struc {
 
 /* Prototypes */
 
-unsigned char command_exists(char *);
+unsigned char command_exists(const char *);
 int modules_init(void);
-void modules_list(int);
+void modules_list(int flags);
+void modules_list2(int (*listf)(const char *, ...), int flags);
 #define PR_MODULES_LIST_FL_SHOW_VERSION		0x00001
 #define PR_MODULES_LIST_FL_SHOW_STATIC		0x00002
 
@@ -185,12 +186,12 @@ void set_auth_check(int (*ck)(cmd_rec *));
 extern int (*cmd_auth_chk)(cmd_rec *);
 
 /* For use from inside module handler functions */
-modret_t *mod_create_ret(cmd_rec *, unsigned char, char *, char *);
+modret_t *mod_create_ret(cmd_rec *, unsigned char, const char *, const char *);
 modret_t *mod_create_error(cmd_rec *, int);
 modret_t *mod_create_data(cmd_rec *, void *);
 
 /* Implemented in mod_core.c */
-int core_chgrp(cmd_rec *, char *, uid_t, gid_t);
-int core_chmod(cmd_rec *, char *, mode_t);
+int core_chgrp(cmd_rec *, const char *, uid_t, gid_t);
+int core_chmod(cmd_rec *, const char *, mode_t);
 
 #endif /* PR_MODULES_H */
diff --git a/include/netacl.h b/include/netacl.h
index fd48169..38164a3 100644
--- a/include/netacl.h
+++ b/include/netacl.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2011 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Network ACL definitions
- * $Id: netacl.h,v 1.5 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Network ACL definitions */
 
 #ifndef PR_NETACL_H
 #define PR_NETACL_H
@@ -49,24 +47,26 @@ typedef enum {
 pr_netacl_t *pr_netacl_create(pool *, char *);
 
 /* Returns a duplicate of the given netacl allocated from the pool. */
-pr_netacl_t *pr_netacl_dup(pool *, pr_netacl_t *);
+pr_netacl_t *pr_netacl_dup(pool *, const pr_netacl_t *);
 
 /* Returns 1 if the given netaddr explicitly matches the ACL, -1 if the
  * netaddr explicitly does not match the ACL (e.g. "none"), and 0 if there is
  * no match.
  */
-int pr_netacl_match(pr_netacl_t *, pr_netaddr_t *);
+int pr_netacl_match(const pr_netacl_t *, const pr_netaddr_t *);
 
 /* Returns TRUE if the given netacl is negated, FALSE if it is not negated,
  * and -1 if there was an error.  If -1 is returned, errno will be set
  * appropriately.
  */
-int pr_netacl_get_negated(pr_netacl_t *);
+int pr_netacl_get_negated(const pr_netacl_t *);
 
 /* Returns the ACL type. */
-pr_netacl_type_t pr_netacl_get_type(pr_netacl_t *);
+pr_netacl_type_t pr_netacl_get_type(const pr_netacl_t *);
 
 /* Returns a string describing the given NetACL. */
-const char *pr_netacl_get_str(pool *, pr_netacl_t *);
+const char *pr_netacl_get_str(pool *p, const pr_netacl_t *acl);
+const char *pr_netacl_get_str2(pool *p, const pr_netacl_t *acl, int flags);
+#define PR_NETACL_FL_STR_NO_DESC	0x0001
 
 #endif /* PR_NETACL_H */
diff --git a/include/netaddr.h b/include/netaddr.h
index 6a23b9d..ec6a06c 100644
--- a/include/netaddr.h
+++ b/include/netaddr.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2013 The ProFTPD Project team
+ * Copyright (c) 2003-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Network address API
- * $Id: netaddr.h,v 1.36 2013-12-23 17:53:42 castaglia Exp $
- */
+/* Network address API */
 
 #ifndef PR_NETADDR_H
 #define PR_NETADDR_H
@@ -215,7 +213,7 @@ int pr_inet_pton(int, const char *, void *);
 pr_netaddr_t *pr_netaddr_alloc(pool *);
 
 /* Duplicate a netaddr using the given pool. */
-pr_netaddr_t *pr_netaddr_dup(pool *, pr_netaddr_t *);
+pr_netaddr_t *pr_netaddr_dup(pool *, const pr_netaddr_t *);
 
 /* Initialize the given netaddr. */
 void pr_netaddr_clear(pr_netaddr_t *);
@@ -230,10 +228,10 @@ void pr_netaddr_clear(pr_netaddr_t *);
  * If there is a failure in resolving the given name to its address(es),
  * NULL will be return, and an error logged.
  */
-pr_netaddr_t *pr_netaddr_get_addr(pool *, const char *, array_header **);
+const pr_netaddr_t *pr_netaddr_get_addr(pool *, const char *, array_header **);
 
 /* Like pr_netaddr_get_addr(), with the ability to specify lookup flags. */
-pr_netaddr_t *pr_netaddr_get_addr2(pool *, const char *, array_header **,
+const pr_netaddr_t *pr_netaddr_get_addr2(pool *, const char *, array_header **,
   unsigned int);
 #define PR_NETADDR_GET_ADDR_FL_INCL_DEVICE	0x001
 #define PR_NETADDR_GET_ADDR_FL_EXCL_DNS		0x002
@@ -264,7 +262,7 @@ int pr_netaddr_ncmp(const pr_netaddr_t *, const pr_netaddr_t *, unsigned int);
  * the netaddr or pattern are NULL.  Otherwise, TRUE is returned if the address
  * is matched by the pattern, or FALSE if is not matched.
  */
-int pr_netaddr_fnmatch(pr_netaddr_t *, const char *, int);
+int pr_netaddr_fnmatch(const pr_netaddr_t *, const char *, int);
 #define PR_NETADDR_MATCH_DNS		0x001
 #define PR_NETADDR_MATCH_IP		0x002
 
@@ -340,18 +338,18 @@ int pr_netaddr_set_reverse_dns(int);
  * lookups have been disabled, the returned string will be the IP address.
  * Returns NULL if there was an error.
  */
-const char *pr_netaddr_get_dnsstr(pr_netaddr_t *);
+const char *pr_netaddr_get_dnsstr(const pr_netaddr_t *);
 
 /* Returns the list of DNS names associated with the given pr_netaddr_t.
  * If DNS lookups have been disabled, an empty list will be returned.
  * NULL is returned if there is an error.
  */
-array_header *pr_netaddr_get_dnsstr_list(pool *, pr_netaddr_t *);
+array_header *pr_netaddr_get_dnsstr_list(pool *, const pr_netaddr_t *);
 
 /* Returns the IP address associated with the given pr_netaddr_t.  Returns
  * NULL if there was an error.
  */
-const char *pr_netaddr_get_ipstr(pr_netaddr_t *);
+const char *pr_netaddr_get_ipstr(const pr_netaddr_t *);
 
 /* Returns the name of the local host, as returned by gethostname(2).  The
  * returned string will be dup'd from the given pool, if any.
@@ -397,10 +395,15 @@ int pr_netaddr_is_v6(const char *);
 int pr_netaddr_is_v4mappedv6(const pr_netaddr_t *);
 
 /* Given an IPv4-mapped IPv6 netaddr, returns an IPv4 netaddr allocated from
- * the given pool.  Returns -1 if the given netaddr is not an IPv4-mapped
+ * the given pool.  Returns NULL if the given netaddr is not an IPv4-mapped
  * IPv6 address.
  */
-pr_netaddr_t *pr_netaddr_v6tov4(pool *p, const pr_netaddr_t *);
+pr_netaddr_t *pr_netaddr_v6tov4(pool *p, const pr_netaddr_t *addr);
+
+/* Given an IPv4 netaddr, return an IPv4-mapped IPv6 netaddr allocated from
+ * the given pool.  Returns NULL if the given netaddr is not an IPv4 address.
+ */
+pr_netaddr_t *pr_netaddr_v4tov6(pool *p, const pr_netaddr_t *addr);
 
 /* Returns TRUE if IPv6 support is enabled, FALSE otherwise. */
 unsigned char pr_netaddr_use_ipv6(void);
@@ -415,14 +418,20 @@ void pr_netaddr_enable_ipv6(void);
  * netaddr information for the sesssion.  DO NOT MODIFY the pointed-to
  * memory!  Returns NULL if no such session information exists.
  */
-pr_netaddr_t *pr_netaddr_get_sess_local_addr(void);
-pr_netaddr_t *pr_netaddr_get_sess_remote_addr(void);
+const pr_netaddr_t *pr_netaddr_get_sess_local_addr(void);
+const pr_netaddr_t *pr_netaddr_get_sess_remote_addr(void);
 const char *pr_netaddr_get_sess_remote_name(void);
 void pr_netaddr_set_sess_addrs(void);
 
-/* Clears the cache of netaddr objects. */
+/* Clears the cache of ALL netaddr objects. */
 void pr_netaddr_clear_cache(void);
 
+/* Clears the cached DNS names, given an IP address string. */
+void pr_netaddr_clear_dnscache(const char *ip_addr);
+
+/* Clears the cached IP addresses, given a DNS name. */
+void pr_netaddr_clear_ipcache(const char *name);
+
 /* Validates the DNS name returned. */
 char *pr_netaddr_validate_dns_str(char *);
 
diff --git a/include/netio.h b/include/netio.h
index e61527e..2a6cc80 100644
--- a/include/netio.h
+++ b/include/netio.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2014 The ProFTPD Project
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Network IO stream layer
- * $Id: netio.h,v 1.18 2014-01-06 06:57:16 castaglia Exp $
- */
+/* Network IO stream layer */
 
 #ifndef PR_NETIO_H
 #define PR_NETIO_H
@@ -186,7 +184,7 @@ int pr_netio_printf_async(pr_netio_stream_t *, char *,...);
 int pr_netio_poll(pr_netio_stream_t *);
 
 /* Read, from the given stream, into the buffer the requested size_t number
- * of bytes.  The int is the minimum number of bytes to read before
+ * of bytes.  The last argument is the minimum number of bytes to read before
  * returning 1 (or greater).
  */
 int pr_netio_read(pr_netio_stream_t *, char *, size_t, int);
@@ -202,6 +200,12 @@ int pr_netio_shutdown(pr_netio_stream_t *, int);
 char *pr_netio_telnet_gets(char *, size_t, pr_netio_stream_t *,
   pr_netio_stream_t *);
 
+/* Similar to pr_netio_telnet_gets(), except that it returns the number of
+ * bytes stored in the given buffer, or -1 if there was an error.
+ */
+int pr_netio_telnet_gets2(char *, size_t, pr_netio_stream_t *,
+  pr_netio_stream_t *);
+
 int pr_netio_write(pr_netio_stream_t *, char *, size_t);
 
 /* This is a bit odd, because io_ functions are opaque, we can't be sure
@@ -219,7 +223,7 @@ void pr_netio_set_poll_interval(pr_netio_stream_t *, unsigned int);
  * default handlers.
  */
 pr_netio_t *pr_alloc_netio(pool *);
-pr_netio_t *pr_alloc_netio2(pool *, module *);
+pr_netio_t *pr_alloc_netio2(pool *, module *, const char *);
 
 /* Register the given NetIO object and all its callbacks for the network
  * I/O layer's use.  If given a NULL argument, it will automatically
@@ -227,15 +231,13 @@ pr_netio_t *pr_alloc_netio2(pool *, module *);
  */
 int pr_register_netio(pr_netio_t *, int);
 
-/* Unregister the NetIO objects indicated by strm_types.
- */
+/* Unregister the NetIO objects indicated by strm_types. */
 int pr_unregister_netio(int);
 
 /* Peek at the NetIO registered for the given stream type. */
 pr_netio_t *pr_get_netio(int);
 
-/* Initialize the network I/O layer.
- */
+/* Initialize the network I/O layer. */
 void init_netio(void);
 
 #endif /* PR_NETIO_H */
diff --git a/include/options.h b/include/options.h
index 5e12abc..9f1aa04 100644
--- a/include/options.h
+++ b/include/options.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2014 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* User configurable defaults and tunable parameters.
- * $Id: options.h,v 1.36 2014-01-25 16:34:09 castaglia Exp $
- */
+/* User configurable defaults and tunable parameters. */
 
 #ifndef PR_OPTIONS_H
 #define PR_OPTIONS_H
@@ -50,8 +48,9 @@
  * burst before the kernel rejects.  This can be configured by the
  * "tcpBackLog" configuration directive, this value is just the default.
  */
-
-#define PR_TUNABLE_DEFAULT_BACKLOG	32
+#ifndef PR_TUNABLE_DEFAULT_BACKLOG
+# define PR_TUNABLE_DEFAULT_BACKLOG	128
+#endif /* PR_TUNABLE_DEFAULT_BACKLOG */
 
 /* The default TCP send/receive buffer sizes, should explicit sizes not
  * be defined at compile time, or should the runtime determination process
@@ -77,7 +76,17 @@
  * miscellaneous tasks.
  */
 #ifndef PR_TUNABLE_BUFFER_SIZE
-# define PR_TUNABLE_BUFFER_SIZE	1024
+# define PR_TUNABLE_BUFFER_SIZE		1024
+#endif
+
+/* There is also a definable buffer size used specifically for parsing
+ * lines of text from the config file: PR_TUNABLE_PARSER_BUFFER_SIZE.
+ *
+ * You should manually set the PR_TUNABLE_PARSER_BUFFER_SIZE only if you
+ * have exceptionally long configuration lines.
+ */
+#ifndef PR_TUNABLE_PARSER_BUFFER_SIZE
+# define PR_TUNABLE_PARSER_BUFFER_SIZE	4096
 #endif
 
 /* There is also a definable buffer size used specifically for data
@@ -133,7 +142,7 @@
  * default linger timeout under 60 seconds.
  */
 #ifndef PR_TUNABLE_TIMEOUTLINGER
-# define PR_TUNABLE_TIMEOUTLINGER	30
+# define PR_TUNABLE_TIMEOUTLINGER	10
 #endif
 
 #ifndef PR_TUNABLE_TIMEOUTLOGIN
@@ -218,6 +227,11 @@
 # define PR_TUNABLE_LOGIN_MAX		256
 #endif
 
+#ifndef PR_TUNABLE_PASSWORD_MAX
+/* Maximum length of a password. */
+# define PR_TUNABLE_PASSWORD_MAX	1024
+#endif
+
 #ifndef PR_TUNABLE_EINTR_RETRY_INTERVAL
 /* Define the time to delay, in seconds, after a system call has been
  * interrupted (errno is EINTR) before retrying that call.
@@ -227,4 +241,17 @@
 # define PR_TUNABLE_EINTR_RETRY_INTERVAL	0.2
 #endif
 
+#ifndef PR_TUNABLE_XFER_LOG_MODE
+# define PR_TUNABLE_XFER_LOG_MODE		0644
+#endif
+
+/* FS Statcache tuning. */
+#ifndef PR_TUNABLE_FS_STATCACHE_SIZE
+# define PR_TUNABLE_FS_STATCACHE_SIZE		32
+#endif
+
+#ifndef PR_TUNABLE_FS_STATCACHE_MAX_AGE
+# define PR_TUNABLE_FS_STATCACHE_MAX_AGE	30
+#endif
+
 #endif /* PR_OPTIONS_H */
diff --git a/include/parser.h b/include/parser.h
index f93e3e3..9db0010 100644
--- a/include/parser.h
+++ b/include/parser.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2011 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Configuration parser
- * $Id: parser.h,v 1.4 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Configuration parser */
 
 #ifndef PR_PARSER_H
 #define PR_PARSER_H
@@ -41,6 +39,12 @@ int pr_parser_prepare(pool *p, xaset_t **parsed_servers);
  */
 int pr_parser_cleanup(void);
 
+/* Called to push a "start-of-context" configuration marker onto the
+ * parser stack.  The name of the configuration context (.e.g "Directory"
+ * or "Anonymous") is provided by the name parameter.
+ */
+config_rec *pr_parser_config_ctxt_open(const char *name);
+
 /* Called to push an "end-of-context" configuration marker onto the
  * parser stack.  If the parser determines that the configuration
  * context being closed is empty, it will remove the entire context from
@@ -49,19 +53,18 @@ int pr_parser_cleanup(void);
  */
 config_rec *pr_parser_config_ctxt_close(int *isempty);
 
+/* Push the config_rec onto the parser stack directly.  This function can
+ * be used, instead of the open/close semantics, for cases where the config_rec
+ * is constructed by means other than file parsing.
+ */
+int pr_parser_config_ctxt_push(config_rec *c);
+
 /* Returns a pointer to the current configuration record on the parser
  * configuration stack.
  */
 config_rec *pr_parser_config_ctxt_get(void);
 
-/* Called to push a "start-of-context" configuration marker onto the
- * parser stack.  The name of the configuration context (.e.g "Directory"
- * or "Anonymous") is provided by the name parameter.
- */
-config_rec *pr_parser_config_ctxt_open(const char *name);
-
-/* Returns the line number of the configuration stream being parsed.
- */
+/* Returns the line number of the configuration stream being parsed. */
 unsigned int pr_parser_get_lineno(void);
 
 /* This is the main function to be used by consumers of the Parser
@@ -85,12 +88,11 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
 #define PR_PARSER_FL_DYNAMIC_CONFIG	0x0001
 
 /* The dispatching of configuration data to the registered configuration
- * handlers is done using a cmd_rec.  This function calls pr_parse_read_line()
- * to obtain the next line of configuration text, then allocates a cmd_rec
- * from the given pool p and populates the struct with data from the
- * line of text.
+ * handlers is done using a cmd_rec.  This function parses the given line of
+ * text, then allocates a cmd_rec from the given pool p and populates the
+ * struct with data from the line of text.
  */
-cmd_rec *pr_parser_parse_line(pool *p);
+cmd_rec *pr_parser_parse_line(pool *p, const char *text, size_t text_len);
 
 /* This convenience function reads the next line from the configuration
  * stream, performing any necessary transformations on the text (e.g.
@@ -104,6 +106,13 @@ cmd_rec *pr_parser_parse_line(pool *p);
  */
 char *pr_parser_read_line(char *buf, size_t bufsz);
 
+/* Called to push a "start-of-server" configuration marker onto the
+ * parser stack.  The name of the server context, usually a string
+ * containing a DNS name or an IP address, is provided by the addrstr
+ * parameter.
+ */
+server_rec *pr_parser_server_ctxt_open(const char *addrstr);
+
 /* Called to push an "end-of-server" configuration record onto the
  * parser stack.  If the parser determines that the server context being
  * closed is empty, it will remove the entire context from the parser stacks:
@@ -111,16 +120,25 @@ char *pr_parser_read_line(char *buf, size_t bufsz);
  */
 server_rec *pr_parser_server_ctxt_close(void);
 
+/* Push the server_rec onto the parser stack directly.  This function can
+ * be used, instead of the open/close semantics, for cases where the server_rec
+ * is constructed by means other than file parsing.
+ */
+int pr_parser_server_ctxt_push(server_rec *s);
+
 /* Returns a pointer to the current server record on the parser server
  * stack.
  */
 server_rec *pr_parser_server_ctxt_get(void);
 
-/* Called to push a "start-of-server" configuration marker onto the
- * parser stack.  The name of the server context, usually a string
- * containing a DNS name or an IP address, is provided by the addrstr
- * parameter.
- */
-server_rec *pr_parser_server_ctxt_open(const char *addrstr);
+/* Configure optional Include behavior. Returns the previously set options. */
+unsigned long pr_parser_set_include_opts(unsigned long opts);
+#define PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS		0x0001
+#define PR_PARSER_INCLUDE_OPT_IGNORE_TMP_FILES		0x0002
+#define PR_PARSER_INCLUDE_OPT_IGNORE_WILDCARDS		0x0004
+
+/* Internal use only */
+int parse_config_path(pool *p, const char *path);
+int parse_config_path2(pool *p, const char *path, unsigned int depth);
 
 #endif /* PR_PARSER_H */
diff --git a/include/pidfile.h b/include/pidfile.h
index b4e7c86..5c0125e 100644
--- a/include/pidfile.h
+++ b/include/pidfile.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2011 The ProFTPD Project team
+ * Copyright (c) 2007-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,15 +22,15 @@
  * OpenSSL in the source distribution.
  */
 
-/* Pidfile handling
- * $Id: pidfile.h,v 1.3 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Pidfile handling */
 
 #ifndef PR_PIDFILE_H
 #define PR_PIDFILE_H
 
 /* For internal use only. */
+const char *pr_pidfile_get(void);
+int pr_pidfile_set(const char *path);
 int pr_pidfile_remove(void);
-void pr_pidfile_write(void);
+int pr_pidfile_write(void);
 
 #endif /* PR_PIDFILE_H */
diff --git a/include/pool.h b/include/pool.h
index 07b2352..974bee0 100644
--- a/include/pool.h
+++ b/include/pool.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -27,7 +27,6 @@
 /* Memory allocation/anti-leak system.  Yes, this *IS* stolen from Apache
  * also.  What can I say?  It makes sense, and it's safe (more overhead
  * though)
- * $Id: pool.h,v 1.28 2012-02-16 00:18:33 castaglia Exp $
  */
 
 #ifndef PR_POOL_H
@@ -73,7 +72,13 @@ typedef struct {
 array_header *make_array(pool *, unsigned int, size_t);
 void clear_array(array_header *);
 void *push_array(array_header *);
-void array_cat(array_header *, const array_header *);
+
+/* Concatenate two array_headers together. */
+void array_cat(array_header *dst, const array_header *src);
+
+/* Similar to array_cat(), except that it provides a return value. */
+int array_cat2(array_header *dst, const array_header *src);
+
 array_header *append_arrays(pool *, const array_header *, const array_header *);
 array_header *copy_array(pool *, const array_header *);
 array_header *copy_array_str(pool *, const array_header *);
@@ -89,7 +94,6 @@ void register_cleanup(pool *, void *, void (*)(void *), void (*)(void *));
 void unregister_cleanup(pool *, void *, void (*)(void *));
 
 /* minimum free bytes in a new block pool */
-
-#define BLOCK_MINFREE	PR_TUNABLE_NEW_POOL_SIZE
+#define BLOCK_MINFREE		PR_TUNABLE_NEW_POOL_SIZE
 
 #endif /* PR_POOL_H */
diff --git a/include/pr-syslog.h b/include/pr-syslog.h
index 88f2653..b078e7d 100644
--- a/include/pr-syslog.h
+++ b/include/pr-syslog.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD internal implemenation of syslog(3) routines
- * $Id: pr-syslog.h,v 1.6 2011-05-23 20:35:35 castaglia Exp $
- */
+/* ProFTPD internal implemenation of syslog(3) routines */
 
 #include "conf.h"
 
@@ -68,7 +66,11 @@
 #elif defined(__hpux)
 # define PR_PATH_LOG	"/dev/log.un"
 #else
-# define PR_PATH_LOG	"/dev/log"
+# if defined(SOLARIS2)
+#  define PR_PATH_LOG	"/dev/conslog"
+# else
+#  define PR_PATH_LOG	"/dev/log"
+# endif /* !Solaris */
 #endif
 
 /* Close desriptor used to write to system logger. */
diff --git a/include/privs.h b/include/privs.h
index 8a10e88..03f5bf2 100644
--- a/include/privs.h
+++ b/include/privs.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,6 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* $Id: privs.h,v 1.34 2012-01-25 07:20:42 castaglia Exp $
- */
-
 #ifndef PR_PRIVS_H
 #define PR_PRIVS_H
 
@@ -69,5 +66,6 @@ int pr_privs_revoke(const char *, int);
 
 /* For internal use only. */
 int init_privs(void);
+int set_nonroot_daemon(int);
 
 #endif /* PR_PRIVS_H */
diff --git a/include/proctitle.h b/include/proctitle.h
index 5486cd1..c46bab7 100644
--- a/include/proctitle.h
+++ b/include/proctitle.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2011 The ProFTPD Project team
+ * Copyright (c) 2007-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Proctitle handling
- * $Id: proctitle.h,v 1.5 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Proctitle handling */
 
 #ifndef PR_PROCTITLE_H
 #define PR_PROCTITLE_H
diff --git a/include/proftpd.h b/include/proftpd.h
index b8858cf..f9e700c 100644
--- a/include/proftpd.h
+++ b/include/proftpd.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* General options
- * $Id: proftpd.h,v 1.75 2012-04-15 18:04:14 castaglia Exp $
- */
+/* General options */
 
 #ifndef PR_PROFTPD_H
 #define PR_PROFTPD_H
@@ -111,7 +109,7 @@ typedef struct {
   uid_t fsuid;				/* Saved file UID */
   gid_t fsgid;				/* Saved file GID */
 
-  char *user,*group;			/* Username/groupname after login */
+  const char *user, *group;		/* Username/groupname after login */
   uid_t login_uid;                      /* UID after login, but before
                                          * session.uid is changed
                                          */
@@ -121,8 +119,8 @@ typedef struct {
 
   pr_table_t *notes;			/* Session notes table */
 
-  pr_class_t *conn_class;		/* Session class */
-  char *proc_prefix;			/* The "prefix" of our process name */
+  const pr_class_t *conn_class;		/* Session class */
+  const char *proc_prefix;		/* The "prefix" of our process name */
 
   int wtmp_log;				/* Are we logging to wtmp? */
   int multiline_rfc2228;		/* Are we using RFC2228-style multiline responses ? */
@@ -132,12 +130,12 @@ typedef struct {
 
   int hide_password;			/* Hide password in logs/ps listing */
 
-  char *chroot_path;			/* Chroot directory */
+  const char *chroot_path;		/* Chroot directory */
 
   struct config_struc *anon_config;	/* Anonymous FTP configuration */
-  char *anon_user;			/* E-mail address sent to us */
+  const char *anon_user;		/* Email address sent to us */
 
-  char *curr_cmd;                       /* Current FTP command */
+  const char *curr_cmd;			/* Current FTP command */
   int curr_cmd_id;			/* Current FTP command ID */
   struct cmd_struc *curr_cmd_rec;       /* Current command */
 
@@ -154,17 +152,17 @@ typedef struct {
 
     int xfer_type;     /* xfer session attributes, default/append/hidden */
     int direction;
-    char *filename;			/* As shown to user */
-    char *path;				/* As used in transfer */
-    char *path_hidden;			/* As used in hidden stor */
+    const char *filename;		/* As shown to user */
+    const char *path;			/* As used in transfer */
+    const char *path_hidden;		/* As used in hidden stor */
 
-    unsigned int bufsize,buflen;
+    unsigned int bufsize, buflen;
 
     struct timeval start_time;		/* Time current transfer started */
     off_t file_size;			/* Total size of file (if known) */
     off_t total_bytes;			/* Total bytes transfered */
 
-    char *bufstart,*buf;
+    char *bufstart, *buf;
   } xfer;
 
   /* Total number of bytes uploaded in this session. */
@@ -205,6 +203,9 @@ typedef struct {
   /* Module which disconnected/ended the session */
   struct module_struc *disconnect_module;
 
+  /* Start/connect time of the session, in milliseconds since epoch. */
+  uint64_t connect_time_ms;
+
 } session_t;
 
 /* Daemon identity values, defined in main.c */
@@ -259,7 +260,7 @@ extern char ServerType;
 #define RECEIVED_SIG_SEGV	0x0008
 #define RECEIVED_SIG_TERMINATE	0x0010
 #define RECEIVED_SIG_XCPU	0x0020
-#define RECEIVED_SIG_TERM_OTHER	0x0040
+#define RECEIVED_SIG_XFSZ	0x0040
 #define RECEIVED_SIG_ABORT	0x0080
 #define RECEIVED_SIG_EVENT	0x0100
 #define RECEIVED_SIG_CHLD	0x0200
@@ -295,7 +296,6 @@ extern char ServerType;
 #endif /* PR_DEVEL_TIMING */
 
 /* Misc Prototypes */
-void pr_signals_handle(void);
 void session_exit(int, void *, int, void *);
 void set_daemon_rlimits(void);
 void set_session_rlimits(void);
diff --git a/include/redis.h b/include/redis.h
new file mode 100644
index 0000000..45684ce
--- /dev/null
+++ b/include/redis.h
@@ -0,0 +1,298 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Redis support */
+
+#ifndef PR_REDIS_H
+#define PR_REDIS_H
+
+#include "conf.h"
+
+typedef struct redis_rec pr_redis_t;
+
+/* Core API for use by modules et al */
+
+/* This function returns the pr_redis_t for the current session; if one
+ * does not exist, it will be allocated.
+ */
+pr_redis_t *pr_redis_conn_get(pool *p);
+pr_redis_t *pr_redis_conn_new(pool *p, module *owner, unsigned long flags);
+int pr_redis_conn_close(pr_redis_t *redis);
+int pr_redis_conn_destroy(pr_redis_t *redis);
+
+/* Set a namespace key prefix, to be used by this connection for all of the
+ * operations involving items.  In practice, the key prefix should always
+ * be a string which does contain any space characters.
+ *
+ * Different modules can use different namespace prefixes for their keys.
+ * Setting NULL for the namespace prefix clears it.
+ */
+int pr_redis_conn_set_namespace(pr_redis_t *redis, module *m,
+  const void *prefix, size_t prefixsz);
+
+/* Authenticate to a password-protected Redis server. */
+int pr_redis_auth(pr_redis_t *redis, const char *password);
+
+/* Issue a custom command to the Redis server; the reply type MUST match the
+ * one specified.  Mostly this is used for testing.
+ */
+int pr_redis_command(pr_redis_t *redis, const array_header *args,
+  int reply_type);
+#define PR_REDIS_REPLY_TYPE_STRING		1
+#define PR_REDIS_REPLY_TYPE_INTEGER		2
+#define PR_REDIS_REPLY_TYPE_NIL			3
+#define PR_REDIS_REPLY_TYPE_ARRAY		4
+#define PR_REDIS_REPLY_TYPE_STATUS		5
+#define PR_REDIS_REPLY_TYPE_ERROR		6
+
+int pr_redis_add(pr_redis_t *redis, module *m, const char *key, void *value,
+  size_t valuesz, time_t expires);
+int pr_redis_decr(pr_redis_t *redis, module *m, const char *key, uint32_t decr,
+  uint64_t *value);
+void *pr_redis_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t *valuesz);
+char *pr_redis_get_str(pool *p, pr_redis_t *redis, module *m, const char *key);
+int pr_redis_incr(pr_redis_t *redis, module *m, const char *key, uint32_t incr,
+  uint64_t *value);
+int pr_redis_remove(pr_redis_t *redis, module *m, const char *key);
+int pr_redis_rename(pr_redis_t *redis, module *m, const char *from,
+  const char *to);
+int pr_redis_set(pr_redis_t *redis, module *m, const char *key, void *value,
+  size_t valuesz, time_t expires);
+
+/* Hash operations */
+int pr_redis_hash_count(pr_redis_t *redis, module *m, const char *key,
+  uint64_t *count);
+int pr_redis_hash_delete(pr_redis_t *redis, module *m, const char *key,
+  const char *field);
+int pr_redis_hash_exists(pr_redis_t *redis, module *m, const char *key,
+  const char *field);
+int pr_redis_hash_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+  const char *field, void **value, size_t *valuesz);
+int pr_redis_hash_getall(pool *p, pr_redis_t *redis, module *m,
+  const char *key, pr_table_t **hash);
+int pr_redis_hash_incr(pr_redis_t *redis, module *m, const char *key,
+  const char *field, int32_t incr, int64_t *value);
+int pr_redis_hash_keys(pool *p, pr_redis_t *redis, module *m, const char *key,
+  array_header **fields);
+int pr_redis_hash_remove(pr_redis_t *redis, module *m, const char *key);
+int pr_redis_hash_set(pr_redis_t *redis, module *m, const char *key,
+  const char *field, void *value, size_t valuesz);
+int pr_redis_hash_setall(pr_redis_t *redis, module *m, const char *key,
+  pr_table_t *hash);
+int pr_redis_hash_values(pool *p, pr_redis_t *redis, module *m,
+  const char *key, array_header **values);
+
+/* List operations */
+int pr_redis_list_append(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_list_count(pr_redis_t *redis, module *m, const char *key,
+  uint64_t *count);
+int pr_redis_list_delete(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_list_exists(pr_redis_t *redis, module *m, const char *key,
+  unsigned int idx);
+int pr_redis_list_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+  unsigned int idx, void **value, size_t *valuesz);
+int pr_redis_list_getall(pool *p, pr_redis_t *redis, module *m,
+  const char *key, array_header **values, array_header **valueszs);
+int pr_redis_list_pop(pool *p, pr_redis_t *redis, module *m, const char *key,
+  void **value, size_t *valuesz, int flags);
+int pr_redis_list_push(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz, int flags);
+int pr_redis_list_remove(pr_redis_t *redis, module *m, const char *key);
+int pr_redis_list_rotate(pool *p, pr_redis_t *redis, module *m, const char *key,
+  void **value, size_t *valuesz);
+int pr_redis_list_set(pr_redis_t *redis, module *m, const char *key,
+  unsigned int idx, void *value, size_t valuesz);
+int pr_redis_list_setall(pr_redis_t *redis, module *m, const char *key,
+  array_header *values, array_header *valueszs);
+
+/* These flags are used for determining whether the list operation occurs
+ * to the LEFT or the RIGHT side of the list, e.g. LPUSH vs RPUSH.
+ */
+#define PR_REDIS_LIST_FL_LEFT		1
+#define PR_REDIS_LIST_FL_RIGHT		2
+
+/* Set operations */
+int pr_redis_set_add(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_set_count(pr_redis_t *redis, module *m, const char *key,
+  uint64_t *count);
+int pr_redis_set_delete(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_set_exists(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_set_getall(pool *p, pr_redis_t *redis, module *m, const char *key,
+  array_header **values, array_header **valueszs);
+int pr_redis_set_remove(pr_redis_t *redis, module *m, const char *key);
+int pr_redis_set_setall(pr_redis_t *redis, module *m, const char *key,
+  array_header *values, array_header *valueszs);
+
+/* Sorted Set operations */
+int pr_redis_sorted_set_add(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz, float score);
+int pr_redis_sorted_set_count(pr_redis_t *redis, module *m, const char *key,
+  uint64_t *count);
+int pr_redis_sorted_set_delete(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_sorted_set_exists(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz);
+int pr_redis_sorted_set_getn(pool *p, pr_redis_t *redis, module *m,
+  const char *key, unsigned int offset, unsigned int len,
+  array_header **values, array_header **valueszs, int flags);
+
+/* These flags are used for determining whether the sorted set items are
+ * obtained in ascending (ASC) or descending (DESC) order.
+ */
+#define PR_REDIS_SORTED_SET_FL_ASC		1
+#define PR_REDIS_SORTED_SET_FL_DESC		2
+
+int pr_redis_sorted_set_incr(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz, float incr, float *score);
+int pr_redis_sorted_set_remove(pr_redis_t *redis, module *m, const char *key);
+int pr_redis_sorted_set_score(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz, float *score);
+int pr_redis_sorted_set_set(pr_redis_t *redis, module *m, const char *key,
+  void *value, size_t valuesz, float score);
+int pr_redis_sorted_set_setall(pr_redis_t *redis, module *m, const char *key,
+  array_header *values, array_header *valueszs, array_header *scores);
+
+/* Variants of the above, where the key values are arbitrary bits rather than
+ * being assumed to be strings.
+ */
+int pr_redis_kadd(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+  void *value, size_t valuesz, time_t expires);
+int pr_redis_kdecr(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+  uint32_t decr, uint64_t *value);
+void *pr_redis_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, size_t *valuesz);
+char *pr_redis_kget_str(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t keysz);
+int pr_redis_kincr(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+  uint32_t incr, uint64_t *value);
+int pr_redis_kremove(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz);
+int pr_redis_krename(pr_redis_t *redis, module *m, const char *from,
+  size_t fromsz, const char *to, size_t tosz);
+int pr_redis_kset(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+  void *value, size_t valuesz, time_t expires);
+
+int pr_redis_hash_kcount(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, uint64_t *count);
+int pr_redis_hash_kdelete(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, const char *field, size_t fieldsz);
+int pr_redis_hash_kexists(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, const char *field, size_t fieldsz);
+int pr_redis_hash_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, const char *field, size_t fieldsz, void **value,
+  size_t *valuesz);
+int pr_redis_hash_kgetall(pool *p, pr_redis_t *redis, module *m,
+  const char *key, size_t keysz, pr_table_t **hash);
+int pr_redis_hash_kincr(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, const char *field, size_t fieldsz, int32_t incr,
+  int64_t *value);
+int pr_redis_hash_kkeys(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, array_header **fields);
+int pr_redis_hash_kremove(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz);
+int pr_redis_hash_kset(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, const char *field, size_t fieldsz, void *value, size_t valuesz);
+int pr_redis_hash_ksetall(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, pr_table_t *hash);
+int pr_redis_hash_kvalues(pool *p, pr_redis_t *redis, module *m,
+  const char *key, size_t keysz, array_header **values);
+
+int pr_redis_list_kappend(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_list_kcount(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, uint64_t *count);
+int pr_redis_list_kdelete(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_list_kexists(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, unsigned int idx);
+int pr_redis_list_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, unsigned int idx, void **value, size_t *valuesz);
+int pr_redis_list_kgetall(pool *p, pr_redis_t *redis, module *m,
+  const char *key, size_t keysz, array_header **values,
+  array_header **valueszs);
+int pr_redis_list_kpop(pool *p, pr_redis_t *redis, module *m,
+  const char *key, size_t keysz, void **value, size_t *valuesz, int flags);
+int pr_redis_list_kpush(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz, int flags);
+int pr_redis_list_kremove(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz);
+int pr_redis_list_krotate(pool *p, pr_redis_t *redis, module *m,
+  const char *key, size_t keysz, void **value, size_t *valuesz);
+int pr_redis_list_kset(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, unsigned int idx, void *value, size_t valuesz);
+int pr_redis_list_ksetall(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, array_header *values, array_header *valueszs);
+
+int pr_redis_set_kadd(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_set_kcount(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, uint64_t *count);
+int pr_redis_set_kdelete(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_set_kexists(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_set_kgetall(pool *p, pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, array_header **values, array_header **valueszs);
+int pr_redis_set_kremove(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz);
+int pr_redis_set_ksetall(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, array_header *values, array_header *valueszs);
+
+int pr_redis_sorted_set_kadd(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz, float score);
+int pr_redis_sorted_set_kcount(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, uint64_t *count);
+int pr_redis_sorted_set_kdelete(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_sorted_set_kexists(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz);
+int pr_redis_sorted_set_kgetn(pool *p, pr_redis_t *redis, module *m,
+  const char *key, size_t keysz, unsigned int offset, unsigned int len,
+  array_header **values, array_header **valueszs, int flags);
+int pr_redis_sorted_set_kincr(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz, float incr, float *score);
+int pr_redis_sorted_set_kremove(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz);
+int pr_redis_sorted_set_kscore(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz, float *score);
+int pr_redis_sorted_set_kset(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, void *value, size_t valuesz, float score);
+int pr_redis_sorted_set_ksetall(pr_redis_t *redis, module *m, const char *key,
+  size_t keysz, array_header *values, array_header *valueszs,
+  array_header *scores);
+
+/* For internal use only */
+int redis_set_server(const char *server, int port, const char *password);
+int redis_set_timeouts(unsigned long connect_millis, unsigned long io_millis);
+
+int redis_clear(void);
+int redis_init(void);
+
+#endif /* PR_REDIS_H */
diff --git a/include/regexp.h b/include/regexp.h
index f8c6e62..30db332 100644
--- a/include/regexp.h
+++ b/include/regexp.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Regular expression management
- * $Id: regexp.h,v 1.9 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Regular expression management */
 
 #ifndef PR_REGEXP_H
 #define PR_REGEXP_H
diff --git a/include/response.h b/include/response.h
index 22aa4aa..65e17bf 100644
--- a/include/response.h
+++ b/include/response.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Command response routines
- * $Id: response.h,v 1.9 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Command response routines */
 
 #ifndef PR_RESPONSE_H
 #define PR_RESPONSE_H
@@ -33,8 +31,8 @@
 
 typedef struct resp_struc {
   struct resp_struc *next;
-  char *num;
-  char *msg;
+  const char *num;
+  const char *msg;
 } pr_response_t;
 
 /* Utilize gcc's __attribute__ pragma for signalling that it should perform
@@ -63,7 +61,8 @@ void pr_response_flush(pr_response_t **);
  * sent/added for flushing to the client.  The strings for the values are
  * allocated out of the given pool.
  */
-int pr_response_get_last(pool *, char **resp_code, char **response_msg);
+int pr_response_get_last(pool *, const char **resp_code,
+  const char **response_msg);
 
 void pr_response_send(const char *, const char *, ...)
 #ifdef __GNUC__
diff --git a/include/rlimit.h b/include/rlimit.h
index d8ae3a4..c09217c 100644
--- a/include/rlimit.h
+++ b/include/rlimit.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2013 The ProFTPD Project team
+ * Copyright (c) 2013-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Resource limits
- * $Id: rlimit.h,v 1.3 2013-02-06 07:34:54 castaglia Exp $
- */
+/* Resource limits */
 
 #ifndef PR_RLIMIT_H
 #define PR_RLIMIT_H
diff --git a/include/scoreboard.h b/include/scoreboard.h
index 57f700f..cfa9a39 100644
--- a/include/scoreboard.h
+++ b/include/scoreboard.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2012 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* Scoreboard routines
- * $Id: scoreboard.h,v 1.22 2012-01-27 01:02:58 castaglia Exp $
- */
+/* Scoreboard routines */
 
 #ifndef PR_SCOREBOARD_H
 #define PR_SCOREBOARD_H
@@ -130,6 +128,7 @@ typedef struct {
 
 const char *pr_get_scoreboard(void);
 const char *pr_get_scoreboard_mutex(void);
+int pr_lock_scoreboard(int, int);
 int pr_set_scoreboard(const char *);
 int pr_set_scoreboard_mutex(const char *);
 
@@ -150,5 +149,6 @@ pr_scoreboard_entry_t *pr_scoreboard_entry_read(void);
 const char *pr_scoreboard_entry_get(int);
 int pr_scoreboard_entry_kill(pr_scoreboard_entry_t *, int);
 int pr_scoreboard_entry_update(pid_t, ...);
+int pr_scoreboard_entry_lock(int, int);
 
 #endif /* PR_SCOREBOARD_H */
diff --git a/include/session.h b/include/session.h
index 8b24f3e..63a3ebf 100644
--- a/include/session.h
+++ b/include/session.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2012 The ProFTPD Project team
+ * Copyright (c) 2009-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,15 +20,12 @@
  * 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.
- *
- * $Id: session.h,v 1.8 2012-04-15 18:04:14 castaglia Exp $
  */
 
 #ifndef PR_SESSION_H
 #define PR_SESSION_H
 
-/* List of disconnect/end-of-session reason codes.
- */
+/* List of disconnect/end-of-session reason codes. */
 
 /* Unknown/unspecified reason for disconnection */
 #define PR_SESS_DISCONNECT_UNSPECIFIED		0
@@ -75,11 +72,14 @@
 /* Disconnected due to wrong protocol used (e.g. HTTP/SMTP). */
 #define PR_SESS_DISCONNECT_BAD_PROTOCOL		14
 
+/* Disconnected due to segfault. */
+#define PR_SESS_DISCONNECT_SEGFAULT		15
+
 /* Returns a string describing the reason the client was disconnected or
  * the session ended.  If a pointer to a char * was provided, any extra
  * disconnect details will be provided.
  */
-const char *pr_session_get_disconnect_reason(char **details);
+const char *pr_session_get_disconnect_reason(const char **details);
 
 /* Returns the current protocol name in use.
  *
@@ -102,6 +102,7 @@ void pr_session_disconnect(module *m, int reason_code, const char *details);
 void pr_session_end(int flags);
 #define PR_SESS_END_FL_NOEXIT		0x01
 #define PR_SESS_END_FL_SYNTAX_CHECK	0x02
+#define PR_SESS_END_FL_ERROR		0x04
 
 /* Returns a so-called "tty name" suitable for use via PAM, and in WtmpLog
  * logging.
diff --git a/include/sets.h b/include/sets.h
index ad94034..fca5e2c 100644
--- a/include/sets.h
+++ b/include/sets.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,6 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* $Id: sets.h,v 1.11 2011-12-21 04:16:58 castaglia Exp $
- */
-
 #ifndef PR_SETS_H
 #define PR_SETS_H
 
diff --git a/src/version.c b/include/signals.h
similarity index 66%
copy from src/version.c
copy to include/signals.h
index 1b0216a..33b73de 100644
--- a/src/version.c
+++ b/include/signals.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008 The ProFTPD Project team
+ * Copyright (c) 2014 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,26 +16,28 @@
  * along with this program; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
  *
- * As a special exemption, The ProFTPD Project team and other respective
+ * As a special exemption, the ProFTPD Project team and other respective
  * copyright holders give permission to link this program with OpenSSL, and
  * distribute the resulting executable, without including the source code for
  * OpenSSL in the source distribution.
  */
 
-/* Versioning
- * $Id: version.c,v 1.2 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Signal handling API. */
+
+#ifndef PR_SIGNALS_H
+#define PR_SIGNALS_H
+
+#include "config.h"
 
-#include "conf.h"
+void pr_signals_block(void);
+void pr_signals_unblock(void);
+void pr_signals_handle(void);
 
-unsigned long pr_version_get_module_api_number(void) {
-  return PR_MODULE_API_VERSION;
-}
+/* Signal handling functions. */
+RETSIGTYPE pr_signals_handle_disconnect(int);
+RETSIGTYPE pr_signals_handle_event(int);
 
-unsigned long pr_version_get_number(void) {
-  return PROFTPD_VERSION_NUMBER;
-}
+/* Internal use only. */
+int init_signals(void);
 
-const char *pr_version_get_str(void) {
-  return PROFTPD_VERSION_TEXT;
-}
+#endif /* PR_SIGNALS_H */
diff --git a/include/stash.h b/include/stash.h
index c389b2f..c8ba7e2 100644
--- a/include/stash.h
+++ b/include/stash.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2010-2012 The ProFTPD Project team
+ * Copyright (c) 2010-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* ProFTPD symbol table hash ("stash")
- * $Id: stash.h,v 1.3 2012-04-24 23:27:38 castaglia Exp $
- */
+/* ProFTPD symbol table hash ("stash") */
 
 #ifndef PR_STASH_H
 #define PR_STASH_H
@@ -36,9 +34,22 @@ typedef enum {
   PR_SYM_HOOK
 } pr_stash_type_t;
 
-int pr_stash_add_symbol(pr_stash_type_t, void *);
-void *pr_stash_get_symbol(pr_stash_type_t, const char *, void *, int *);
-int pr_stash_remove_symbol(pr_stash_type_t, const char *, module *);
+int pr_stash_add_symbol(pr_stash_type_t stash_type, void *sym);
+void *pr_stash_get_symbol(pr_stash_type_t stash_type, const char *name,
+  void *prev_sym, int *);
+void *pr_stash_get_symbol2(pr_stash_type_t stash_type, const char *name,
+  void *prev_sym, int *, unsigned int *);
+int pr_stash_remove_symbol(pr_stash_type_t stash_type, const char *name,
+  module *m);
+
+/* These functions are similar to pr_stash_remove_symbol(), except that they
+ * allow for providing type-specific criteria.
+ */
+int pr_stash_remove_conf(const char *directive_name, module *m);
+int pr_stash_remove_cmd(const char *cmd_name, module *m,
+  unsigned char cmd_type, const char *cmd_group, int cmd_class);
+int pr_stash_remove_auth(const char *api_name, module *m);
+int pr_stash_remove_hook(const char *hook_name, module *m);
 
 void pr_stash_dump(void (*)(const char *, ...));
 
diff --git a/include/str.h b/include/str.h
index 149912f..316a32a 100644
--- a/include/str.h
+++ b/include/str.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* String manipulation functions
- * $Id: str.h,v 1.11 2013-11-24 00:45:29 castaglia Exp $
- */
+/* String manipulation functions */
 
 #ifndef PR_STR_H
 #define PR_STR_H
@@ -32,8 +30,14 @@
 /* Default maximum number of replacements that will do in a given string. */
 #define PR_STR_MAX_REPLACEMENTS                 8
 
+/* Per RFC959, directory responses for MKD and PWD should be "dir_name" (with
+ * quotes).  For directories that CONTAIN quotes, the additional quotes must
+ * be duplicated.
+ */
+const char *quote_dir(pool *p, char *dir);
+
 char *sstrcat(char *, const char *, size_t);
-char *sreplace(pool *, char *, ...);
+const char *sreplace(pool *, const char *, ...);
 
 char *pdircat(pool *, ...);
 char *pstrcat(pool *, ...);
@@ -52,10 +56,13 @@ char *pstrndup(pool *, const char *, size_t);
 int pr_strnrstr(const char *s, size_t slen, const char *suffix,
   size_t suffixlen, int flags);
 
+/* Returns a quoted version of the given string. */
+const char *pr_str_quote(pool *p, const char *str);
+
 /* Newer version of sreplace(), with more control and better error reporting. */
-char *pr_str_replace(pool *, unsigned int, char *, ...);
-char *pr_str_strip(pool *, char *);
-char *pr_str_strip_end(char *, char *);
+const char *pr_str_replace(pool *, unsigned int, const char *, ...);
+const char *pr_str_strip(pool *, const char *);
+char *pr_str_strip_end(char *, const char *);
 int pr_str_get_nbytes(const char *, const char *, off_t *);
 char *pr_str_get_word(char **, int);
 
@@ -67,6 +74,50 @@ char *pr_str_get_word(char **, int);
  */
 int pr_str_get_duration(const char *str, int *duration);
 
+/* Encode the given buffer of binary data as a hex string.  The flags indicate
+ * whether to use uppercase or lowercase hex values; the default is to use
+ * lowercase values.
+ *
+ * Returns NULL on error, or the successfully encoded string, allocated out of
+ * the given pool, on success.
+ */
+char *pr_str_bin2hex(pool *p, const unsigned char *buf, size_t len, int flags);
+#define PR_STR_FL_HEX_USE_UC			0x0001
+#define PR_STR_FL_HEX_USE_LC			0x0002
+
+/* Decodes the given buffer of hex-encoded data into binary data. */
+unsigned char *pr_str_hex2bin(pool *p, const unsigned char *hex, size_t hex_len,
+  size_t *len);
+
+/* Obtain the Levenshtein distance between the two strings.  The various
+ * operations (swap, substitution, insertion, deletion) can be weighted.
+ */
+int pr_str_levenshtein(pool *p, const char *a, const char *b,
+  int swap_cost, int subst_cost, int insert_cost, int del_cost, int flags);
+
+/* Given a string and a list of possibly similar candidates, return an
+ * array of the candidates, sorted in order of Levenshtein distance (ascending).
+ * A maximum edit distance can be used to return the most relevant subset of
+ * the candidates; if a max distance of zero is used, the default max distance
+ * value will be used.
+ */
+array_header *pr_str_get_similars(pool *p, const char *s,
+  array_header *candidates, int max_distance, int flags);
+#define PR_STR_DEFAULT_MAX_EDIT_DISTANCE		7
+
+/* Given a string delimited by a character (such as comma or pipe), return
+ * an array of each item.
+ */
+array_header *pr_str_text_to_array(pool *p, const char *text, char delimiter);
+
+/* Converts a string to a uid_t/gid_t, respectively. */
+int pr_str2uid(const char *, uid_t *);
+int pr_str2gid(const char *, gid_t *);
+
+/* Converts a uid_t/gid_t to a string, respectively */
+const char *pr_uid2str(pool *, uid_t);
+const char *pr_gid2str(pool *, gid_t);
+
 #define PR_STR_FL_PRESERVE_COMMENTS		0x0001
 #define PR_STR_FL_PRESERVE_WHITESPACE		0x0002
 #define PR_STR_FL_IGNORE_CASE			0x0004
diff --git a/include/support.h b/include/support.h
index a63144a..b75b27f 100644
--- a/include/support.h
+++ b/include/support.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,13 +24,12 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Non-specific support functions.
- * $Id: support.h,v 1.39 2011-11-19 02:40:12 castaglia Exp $
- */
+/* Non-specific support functions. */
 
 #ifndef PR_SUPPORT_H
 #define PR_SUPPORT_H
 
+#include <sys/time.h>
 #include <time.h>
 
 #if defined(NAME_MAX)
@@ -60,29 +59,44 @@ int getopt_long(int, char * const [], const char *, const struct option *,
 # endif /* !HAVE_GETOPT_LONG */
 #endif /* !HAVE_GETOPT */
 
-void pr_signals_block(void);
-void pr_signals_unblock(void);
-
 char *dir_interpolate(pool *, const char *);
 char *dir_abs_path(pool *, const char *, int);
+
+/* Performs chroot-aware handling of symlinks. */
+int dir_readlink(pool *, const char *, char *, size_t, int);
+#define PR_DIR_READLINK_FL_HANDLE_REL_PATH		0x0001
+
 char *dir_realpath(pool *, const char *);
 char *dir_canonical_path(pool *, const char *);
 char *dir_canonical_vpath(pool *, const char *);
 char *dir_best_path(pool *, const char *);
 
+/* Schedulables. */
 void schedule(void (*f)(void *, void *, void *, void *), int, void *, void *,
   void *, void *);
 void run_schedule(void);
+void restart_daemon(void *, void *, void *, void *);
+void shutdown_end_session(void *, void *, void *, void *);
 
-size_t get_name_max(char *, int);
+long get_name_max(char *path, int fd);
 
 mode_t file_mode(const char *);
+mode_t file_mode2(pool *, const char *);
+
+mode_t symlink_mode(const char *);
+mode_t symlink_mode2(pool *, const char *);
+
 int file_exists(const char *);
+int file_exists2(pool *, const char *);
+
 int dir_exists(const char *);
+int dir_exists2(pool *, const char *);
+
 int exists(const char *);
+int exists2(pool *, const char *);
 
 char *safe_token(char **);
-int check_shutmsg(time_t *, time_t *, time_t *, char *, size_t);
+int check_shutmsg(const char *, time_t *, time_t *, time_t *, char *, size_t);
 
 void pr_memscrub(void *, size_t);
 
@@ -92,4 +106,12 @@ struct tm *pr_localtime(pool *, const time_t *);
 const char *pr_strtime(time_t);
 const char *pr_strtime2(time_t, int);
 
+int pr_gettimeofday_millis(uint64_t *);
+int pr_timeval2millis(struct timeval *, uint64_t *);
+
+/* Resolve/substitute any "%u" variables in the path.  Returns the resolved
+ * path, or NULL if there was an error.
+ */
+const char *path_subst_uservar(pool *p, const char **path);
+
 #endif /* PR_SUPPORT_H */
diff --git a/include/table.h b/include/table.h
index 9954482..d75331a 100644
--- a/include/table.h
+++ b/include/table.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2012 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Table management
- * $Id: table.h,v 1.12 2012-01-26 17:55:07 castaglia Exp $
- */
+/* Table management */
 
 #ifndef PR_TABLE_H
 #define PR_TABLE_H
@@ -33,7 +31,7 @@
 
 typedef struct tab_key {
   struct tab_key *next;
-  void *key_data;
+  const void *key_data;
   size_t key_datasz;
   unsigned int hash;
   unsigned nents;
@@ -44,7 +42,7 @@ typedef struct tab_entry {
   struct tab_entry *next, *prev;
   unsigned int idx;
   pr_table_key_t *key;
-  void *value_data;
+  const void *value_data;
   size_t value_datasz;
 
 } pr_table_entry_t;
@@ -56,15 +54,15 @@ typedef struct table_rec pr_table_t;
  * If value_datasz is 0, value_data is assumed to be a NUL-terminated string
  * and strlen() is called on it.
  */
-int pr_table_add(pr_table_t *tab, const char *key_data, void *value_data,
+int pr_table_add(pr_table_t *tab, const char *key_data, const void *value_data,
   size_t value_datasz);
 
 /* Add an entry in the table under the given key, making a duplicate of
  * the given value from the table's pool.  If value_datasz is 0, value_data
  * is assumed to be a NUL-terminated string and strlen() is called on it.
  */
-int pr_table_add_dup(pr_table_t *tab, const char *key_data, void *value_data,
-  size_t value_datasz);
+int pr_table_add_dup(pr_table_t *tab, const char *key_data,
+  const void *value_data, size_t value_datasz);
 
 /* Allocates a new table from the given pool.  flags can be used to
  * determine the table behavior, e.g. will it allow multiple entries under
@@ -91,8 +89,8 @@ int pr_table_count(pr_table_t *tab);
  * to halt the iteration, unless PR_TABLE_DO_FL_ALL is used.
  */
 int pr_table_do(pr_table_t *tab, int cb(const void *key_data,
-  size_t key_datasz, void *value_data, size_t value_datasz, void *user_data),
-  void *user_data, int flags);
+  size_t key_datasz, const void *value_data, size_t value_datasz,
+  void *user_data), void *user_data, int flags);
 #define PR_TABLE_DO_FL_ALL			0x0010
 
 /* Remove all entries from the table, emptying it.
@@ -114,19 +112,19 @@ int pr_table_free(pr_table_t *tab);
  * entry in the table for the given key.  If value_datasz is not NULL,
  * the size of the returned value will be stored in it.
  */
-void *pr_table_get(pr_table_t *tab, const char *key_data,
+const void *pr_table_get(pr_table_t *tab, const char *key_data,
   size_t *value_datasz);
 
 /* Retrieve the next key, for iterating over the entire table.  Returns
  * NULL when the end of the table has been reached.
  */
-void *pr_table_next(pr_table_t *tab);
+const void *pr_table_next(pr_table_t *tab);
 
 /* Returns the value stored under the given key, and removes that entry from
  * the table.  If value_datasz is not NULL, the size of the returned value
  * will be stored in it.
  */
-void *pr_table_remove(pr_table_t *tab, const char *key_data,
+const void *pr_table_remove(pr_table_t *tab, const char *key_data,
   size_t *value_datasz);
 
 /* Rewind to the start of the table before iterating using pr_table_next().
@@ -139,8 +137,8 @@ int pr_table_rewind(pr_table_t *tab);
  * multiple times in order to set all entries under that key; call
  * pr_table_exists() to find the number of entries to change.
  */
-int pr_table_set(pr_table_t *tab, const char *key_data, void *value_data,
-  size_t value_datasz);
+int pr_table_set(pr_table_t *tab, const char *key_data,
+  const void *value_data, size_t value_datasz);
 
 /* Change some of the characteristics of an allocated table tab via
  * the control cmd.  pr_table_ctl() can only be called on an empty table.
@@ -249,7 +247,7 @@ void pr_table_dump(void (*)(const char *, ...), pr_table_t *tab);
  * function must provide the size of the given value_data.
  */
 int pr_table_kadd(pr_table_t *tab, const void *key_data, size_t key_datasz,
-  void *value_data, size_t value_datasz);
+  const void *value_data, size_t value_datasz);
 
 /* Same as pr_table_exists(), except that the key data to use is treated as
  * an opaque memory region of size key_datasz.  This function should be
@@ -257,18 +255,23 @@ int pr_table_kadd(pr_table_t *tab, const void *key_data, size_t key_datasz,
  */
 int pr_table_kexists(pr_table_t *tab, const void *key_data, size_t key_datasz);
 
+/* Same as pr_table_next(), except that the size of the key is also provided.
+ * This function should be used if the lookup key is not a string.
+ */
+const void *pr_table_knext(pr_table_t *tab, size_t *key_datasz);
+
 /* Same as pr_table_get(), except that the key data to use is treated as
  * an opaque memory region of size key_datasz.  This function should be
  * used if the lookup key is not a string.
  */
-void *pr_table_kget(pr_table_t *tab, const void *key_data,
+const void *pr_table_kget(pr_table_t *tab, const void *key_data,
   size_t key_datasz, size_t *value_datasz);
 
 /* Same as pr_table_remove(), except that the key data to use is treated as
  * an opaque memory region of size key_datasz.  This function should be
  * used if the lookup key is not a string.
  */
-void *pr_table_kremove(pr_table_t *tab, const void *key_data,
+const void *pr_table_kremove(pr_table_t *tab, const void *key_data,
   size_t key_datasz, size_t *value_datasz);
 
 /* Same as pr_table_set(), except that the key data to use is treated as
@@ -276,7 +279,7 @@ void *pr_table_kremove(pr_table_t *tab, const void *key_data,
  * used if the lookup key is not a string.
  */
 int pr_table_kset(pr_table_t *tab, const void *key_data, size_t key_datasz,
-  void *value_data, size_t value_datasz);
+  const void *value_data, size_t value_datasz);
 
 /* Similar to pr_table_alloc(), except that the number of chains can
  * be explicitly configured.
diff --git a/include/throttle.h b/include/throttle.h
index b5b52a1..b74e6b9 100644
--- a/include/throttle.h
+++ b/include/throttle.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* Transfer rate/throttling functions
- * $Id: throttle.h,v 1.2 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Transfer rate/throttling functions */
 
 #ifndef PR_THROTTLE_H
 #define PR_THROTTLE_H
diff --git a/include/timers.h b/include/timers.h
index 8ba6afc..016264f 100644
--- a/include/timers.h
+++ b/include/timers.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,8 +22,6 @@
  * 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.
- *
- * $Id: timers.h,v 1.18 2011-05-23 20:35:35 castaglia Exp $
  */
 
 #ifndef PR_TIMERS_H
diff --git a/include/trace.h b/include/trace.h
index c2d7005..f05dd0e 100644
--- a/include/trace.h
+++ b/include/trace.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006-2013 The ProFTPD Project team
+ * Copyright (c) 2006-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* Trace API
- * $Id: trace.h,v 1.8 2013-01-31 20:35:54 castaglia Exp $
- */
+/* Trace API */
 
 #ifndef PR_TRACE_H
 #define PR_TRACE_H
diff --git a/include/utf8.h b/include/utf8.h
index e2c5ac4..7bf1d2e 100644
--- a/include/utf8.h
+++ b/include/utf8.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006 The ProFTPD Project team
+ * Copyright (c) 2006-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* UTF8 encoding/decoding
- * $Id: utf8.h,v 1.1 2006-05-25 16:55:34 castaglia Exp $
- */
+/* UTF8 encoding/decoding */
 
 #ifndef PR_UTF8_H
 #define PR_UTF8_H
diff --git a/include/var.h b/include/var.h
index 8777599..e518204 100644
--- a/include/var.h
+++ b/include/var.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2011 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Variables API definition
- * $Id: var.h,v 1.4 2011-05-23 20:35:35 castaglia Exp $
- */
+/* Variables API definition */
 
 #ifndef PR_VAR_H
 #define PR_VAR_H
diff --git a/include/version.h b/include/version.h
index f06055a..8e08eff 100644
--- a/include/version.h
+++ b/include/version.h
@@ -1,8 +1,8 @@
 #include "buildstamp.h"
 
 /* Application version (in various forms) */
-#define PROFTPD_VERSION_NUMBER		0x0001030511
-#define PROFTPD_VERSION_TEXT		"1.3.5e"
+#define PROFTPD_VERSION_NUMBER		0x0001030605
+#define PROFTPD_VERSION_TEXT		"1.3.6"
 
 /* Module API version */
 #define PR_MODULE_API_VERSION		0x20
@@ -12,4 +12,4 @@ unsigned long pr_version_get_number(void);
 const char *pr_version_get_str(void);
 
 /* PR_STATUS is reported by --version-status -- don't ask why */
-#define PR_STATUS          		"(maint)"
+#define PR_STATUS          		"(stable)"
diff --git a/include/xferlog.h b/include/xferlog.h
index e30dd6f..d00c1c5 100644
--- a/include/xferlog.h
+++ b/include/xferlog.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2011 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,14 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* TransferLog routines
- * $Id: xferlog.h,v 1.3 2011-05-23 20:35:35 castaglia Exp $
- */
+/* TransferLog routines */
 
 #ifndef PR_XFERLOG_H
 #define PR_XFERLOG_H
 
 int xferlog_open(const char *);
 void xferlog_close(void);
-int xferlog_write(long, const char *, off_t, char *, char, char, char, char *,
-  char, const char *);
+int xferlog_write(long, const char *, off_t, const char *, char, char, char,
+  const char *, char, const char *);
 
 #endif /* PR_XFERLOG_H */
diff --git a/lib/Makefile.in b/lib/Makefile.in
index 8667ffe..b8b9e81 100644
--- a/lib/Makefile.in
+++ b/lib/Makefile.in
@@ -35,12 +35,13 @@ lib: libsupp.a $(LIB_DEPS)
 install:
 
 clean:
-	rm -f *.o libsupp.a
+	$(RM) -f *.o libsupp.a
 	test -z $(LIB_DEPS) || (cd libltdl/ && $(MAKE) clean)
 
 depend:
 	$(MAKEDEPEND) $(CPPFLAGS) *.c 2>/dev/null
 	$(MAKEDEPEND) $(CPPFLAGS) -fMakefile.in *.c 2>/dev/null
 
-distclean:
+distclean: clean
+	-$(RM) *.gcda *.gcno
 	test -z $(LIB_DEPS) || (cd libltdl/ && $(MAKE) distclean)
diff --git a/lib/ccan-json.c b/lib/ccan-json.c
new file mode 100644
index 0000000..f8250fe
--- /dev/null
+++ b/lib/ccan-json.c
@@ -0,0 +1,1404 @@
+/*
+  Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159 at gmail.com)
+  All rights reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+#include "ccan-json.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* ProFTPD: stdbool.h equivalents */
+#ifndef TRUE
+# define TRUE	1
+#endif
+
+#ifndef FALSE
+# define FALSE	0
+#endif
+
+/* Out-of-memory error handling. */
+static void default_oom(void)
+{
+	fprintf(stderr, "%s", "Out of memory!\n");
+	exit(EXIT_FAILURE);
+}
+
+static void (*json_oom)(void) = default_oom;
+
+/* Sadly, strdup is not portable. */
+static char *json_strdup(const char *str)
+{
+	char *ret = (char*) malloc(strlen(str) + 1);
+	if (ret == NULL) {
+		json_oom();
+		return NULL;
+	}
+	strcpy(ret, str);
+	return ret;
+}
+
+/* String buffer */
+
+typedef struct
+{
+	char *cur;
+	char *end;
+	char *start;
+} SB;
+
+static void sb_init(SB *sb)
+{
+	sb->start = (char*) malloc(17);
+	if (sb->start == NULL)
+		json_oom();
+	sb->cur = sb->start;
+	sb->end = sb->start + 16;
+}
+
+/* sb and need may be evaluated multiple times. */
+#define sb_need(sb, need) do {                  \
+		if ((sb)->end - (sb)->cur < (need))     \
+			sb_grow(sb, need);                  \
+	} while (0)
+
+static void sb_grow(SB *sb, int need)
+{
+	size_t length = sb->cur - sb->start;
+	size_t alloc = sb->end - sb->start;
+	
+	do {
+		alloc *= 2;
+	} while (alloc < length + need);
+	
+	sb->start = (char*) realloc(sb->start, alloc + 1);
+	if (sb->start == NULL)
+		json_oom();
+	sb->cur = sb->start + length;
+	sb->end = sb->start + alloc;
+}
+
+static void sb_put(SB *sb, const char *bytes, int count)
+{
+	sb_need(sb, count);
+	memcpy(sb->cur, bytes, count);
+	sb->cur += count;
+}
+
+#define sb_putc(sb, c) do {         \
+		if ((sb)->cur >= (sb)->end) \
+			sb_grow(sb, 1);         \
+		*(sb)->cur++ = (c);         \
+	} while (0)
+
+static void sb_puts(SB *sb, const char *str)
+{
+	sb_put(sb, str, strlen(str));
+}
+
+static char *sb_finish(SB *sb)
+{
+	*sb->cur = 0;
+	assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start));
+	return sb->start;
+}
+
+static void sb_free(SB *sb)
+{
+	free(sb->start);
+}
+
+/*
+ * Unicode helper functions
+ *
+ * These are taken from the ccan/charset module and customized a bit.
+ * Putting them here means the compiler can (choose to) inline them,
+ * and it keeps ccan/json from having a dependency.
+ */
+
+#if 0
+/* PROFTPD NOTE: This has been commented out, since it will cause
+ * problems on platforms where `uchar_t' is already defined.  Instead,
+ * all ocurrences of `uchar_t' in the original code have been replaced
+ * with `uint32_t'.
+ */
+
+/*
+ * Type for Unicode codepoints.
+ * We need our own because wchar_t might be 16 bits.
+ */
+typedef uint32_t uchar_t;
+#endif
+
+/*
+ * Validate a single UTF-8 character starting at @s.
+ * The string must be null-terminated.
+ *
+ * If it's valid, return its length (1 thru 4).
+ * If it's invalid or clipped, return 0.
+ *
+ * This function implements the syntax given in RFC3629, which is
+ * the same as that given in The Unicode Standard, Version 6.0.
+ *
+ * It has the following properties:
+ *
+ *  * All codepoints U+0000..U+10FFFF may be encoded,
+ *    except for U+D800..U+DFFF, which are reserved
+ *    for UTF-16 surrogate pair encoding.
+ *  * UTF-8 byte sequences longer than 4 bytes are not permitted,
+ *    as they exceed the range of Unicode.
+ *  * The sixty-six Unicode "non-characters" are permitted
+ *    (namely, U+FDD0..U+FDEF, U+xxFFFE, and U+xxFFFF).
+ */
+static int utf8_validate_cz(const char *s)
+{
+	unsigned char c = *s++;
+	
+	if (c <= 0x7F) {        /* 00..7F */
+		return 1;
+	} else if (c <= 0xC1) { /* 80..C1 */
+		/* Disallow overlong 2-byte sequence. */
+		return 0;
+	} else if (c <= 0xDF) { /* C2..DF */
+		/* Make sure subsequent byte is in the range 0x80..0xBF. */
+		if (((unsigned char)*s++ & 0xC0) != 0x80)
+			return 0;
+		
+		return 2;
+	} else if (c <= 0xEF) { /* E0..EF */
+		/* Disallow overlong 3-byte sequence. */
+		if (c == 0xE0 && (unsigned char)*s < 0xA0)
+			return 0;
+		
+		/* Disallow U+D800..U+DFFF. */
+		if (c == 0xED && (unsigned char)*s > 0x9F)
+			return 0;
+		
+		/* Make sure subsequent bytes are in the range 0x80..0xBF. */
+		if (((unsigned char)*s++ & 0xC0) != 0x80)
+			return 0;
+		if (((unsigned char)*s++ & 0xC0) != 0x80)
+			return 0;
+		
+		return 3;
+	} else if (c <= 0xF4) { /* F0..F4 */
+		/* Disallow overlong 4-byte sequence. */
+		if (c == 0xF0 && (unsigned char)*s < 0x90)
+			return 0;
+		
+		/* Disallow codepoints beyond U+10FFFF. */
+		if (c == 0xF4 && (unsigned char)*s > 0x8F)
+			return 0;
+		
+		/* Make sure subsequent bytes are in the range 0x80..0xBF. */
+		if (((unsigned char)*s++ & 0xC0) != 0x80)
+			return 0;
+		if (((unsigned char)*s++ & 0xC0) != 0x80)
+			return 0;
+		if (((unsigned char)*s++ & 0xC0) != 0x80)
+			return 0;
+		
+		return 4;
+	} else {                /* F5..FF */
+		return 0;
+	}
+}
+
+/* Validate a null-terminated UTF-8 string. */
+static int utf8_validate(const char *s)
+{
+	int len;
+	
+	for (; *s != 0; s += len) {
+		len = utf8_validate_cz(s);
+		if (len == 0)
+			return FALSE;
+	}
+	
+	return TRUE;
+}
+
+/*
+ * Read a single UTF-8 character starting at @s,
+ * returning the length, in bytes, of the character read.
+ *
+ * This function assumes input is valid UTF-8,
+ * and that there are enough characters in front of @s.
+ */
+static int utf8_read_char(const char *s, uint32_t *out)
+{
+	const unsigned char *c = (const unsigned char*) s;
+	
+	assert(utf8_validate_cz(s));
+
+	if (c[0] <= 0x7F) {
+		/* 00..7F */
+		*out = c[0];
+		return 1;
+	} else if (c[0] <= 0xDF) {
+		/* C2..DF (unless input is invalid) */
+		*out = ((uint32_t)c[0] & 0x1F) << 6 |
+		       ((uint32_t)c[1] & 0x3F);
+		return 2;
+	} else if (c[0] <= 0xEF) {
+		/* E0..EF */
+		*out = ((uint32_t)c[0] &  0xF) << 12 |
+		       ((uint32_t)c[1] & 0x3F) << 6  |
+		       ((uint32_t)c[2] & 0x3F);
+		return 3;
+	} else {
+		/* F0..F4 (unless input is invalid) */
+		*out = ((uint32_t)c[0] &  0x7) << 18 |
+		       ((uint32_t)c[1] & 0x3F) << 12 |
+		       ((uint32_t)c[2] & 0x3F) << 6  |
+		       ((uint32_t)c[3] & 0x3F);
+		return 4;
+	}
+}
+
+/*
+ * Write a single UTF-8 character to @s,
+ * returning the length, in bytes, of the character written.
+ *
+ * @unicode must be U+0000..U+10FFFF, but not U+D800..U+DFFF.
+ *
+ * This function will write up to 4 bytes to @out.
+ */
+static int utf8_write_char(uint32_t unicode, char *out)
+{
+	unsigned char *o = (unsigned char*) out;
+	
+	assert(unicode <= 0x10FFFF && !(unicode >= 0xD800 && unicode <= 0xDFFF));
+
+	if (unicode <= 0x7F) {
+		/* U+0000..U+007F */
+		*o++ = unicode;
+		return 1;
+	} else if (unicode <= 0x7FF) {
+		/* U+0080..U+07FF */
+		*o++ = 0xC0 | unicode >> 6;
+		*o++ = 0x80 | (unicode & 0x3F);
+		return 2;
+	} else if (unicode <= 0xFFFF) {
+		/* U+0800..U+FFFF */
+		*o++ = 0xE0 | unicode >> 12;
+		*o++ = 0x80 | (unicode >> 6 & 0x3F);
+		*o++ = 0x80 | (unicode & 0x3F);
+		return 3;
+	} else {
+		/* U+10000..U+10FFFF */
+		*o++ = 0xF0 | unicode >> 18;
+		*o++ = 0x80 | (unicode >> 12 & 0x3F);
+		*o++ = 0x80 | (unicode >> 6 & 0x3F);
+		*o++ = 0x80 | (unicode & 0x3F);
+		return 4;
+	}
+}
+
+/*
+ * Compute the Unicode codepoint of a UTF-16 surrogate pair.
+ *
+ * @uc should be 0xD800..0xDBFF, and @lc should be 0xDC00..0xDFFF.
+ * If they aren't, this function returns false.
+ */
+static int from_surrogate_pair(uint16_t uc, uint16_t lc, uint32_t *unicode)
+{
+	if (uc >= 0xD800 && uc <= 0xDBFF && lc >= 0xDC00 && lc <= 0xDFFF) {
+		*unicode = 0x10000 + ((((uint32_t)uc & 0x3FF) << 10) | (lc & 0x3FF));
+		return TRUE;
+	} else {
+		return FALSE;
+	}
+}
+
+/*
+ * Construct a UTF-16 surrogate pair given a Unicode codepoint.
+ *
+ * @unicode must be U+10000..U+10FFFF.
+ */
+static void to_surrogate_pair(uint32_t unicode, uint16_t *uc, uint16_t *lc)
+{
+	uint32_t n;
+	
+	assert(unicode >= 0x10000 && unicode <= 0x10FFFF);
+	
+	n = unicode - 0x10000;
+	*uc = ((n >> 10) & 0x3FF) | 0xD800;
+	*lc = (n & 0x3FF) | 0xDC00;
+}
+
+#define is_space(c) ((c) == '\t' || (c) == '\n' || (c) == '\r' || (c) == ' ')
+#define is_digit(c) ((c) >= '0' && (c) <= '9')
+
+static int parse_value      (const char **sp, JsonNode        **out);
+static int parse_string     (const char **sp, char            **out);
+static int parse_number     (const char **sp, double           *out);
+static int parse_array      (const char **sp, JsonNode        **out);
+static int parse_object     (const char **sp, JsonNode        **out);
+static int parse_hex16      (const char **sp, uint16_t         *out);
+
+static int expect_literal   (const char **sp, const char *str);
+static void skip_space      (const char **sp);
+
+static void emit_value              (SB *out, const JsonNode *node);
+static void emit_value_indented     (SB *out, const JsonNode *node, const char *space, int indent_level);
+static void emit_string             (SB *out, const char *str);
+static void emit_number             (SB *out, double num);
+static void emit_array              (SB *out, const JsonNode *array);
+static void emit_array_indented     (SB *out, const JsonNode *array, const char *space, int indent_level);
+static void emit_object             (SB *out, const JsonNode *object);
+static void emit_object_indented    (SB *out, const JsonNode *object, const char *space, int indent_level);
+
+static int write_hex16(char *out, uint16_t val);
+
+static JsonNode *mknode(JsonTag tag);
+static void append_node(JsonNode *parent, JsonNode *child);
+static void prepend_node(JsonNode *parent, JsonNode *child);
+static void append_member(JsonNode *object, char *key, JsonNode *value);
+
+/* Assertion-friendly validity checks */
+static int tag_is_valid(unsigned int tag);
+static int number_is_valid(const char *num);
+
+void json_set_oom(void (*oom)(void)) {
+	json_oom = (oom != NULL ? oom : default_oom);
+}
+
+JsonNode *json_decode(const char *json)
+{
+	const char *s = json;
+	JsonNode *ret;
+	
+	skip_space(&s);
+	if (!parse_value(&s, &ret))
+		return NULL;
+	
+	skip_space(&s);
+	if (*s != 0) {
+		json_delete(ret);
+		return NULL;
+	}
+	
+	return ret;
+}
+
+char *json_encode(const JsonNode *node)
+{
+	return json_stringify(node, NULL);
+}
+
+char *json_encode_string(const char *str)
+{
+	SB sb;
+	sb_init(&sb);
+	
+	emit_string(&sb, str);
+	
+	return sb_finish(&sb);
+}
+
+char *json_stringify(const JsonNode *node, const char *space)
+{
+	SB sb;
+	sb_init(&sb);
+	
+	if (space != NULL)
+		emit_value_indented(&sb, node, space, 0);
+	else
+		emit_value(&sb, node);
+	
+	return sb_finish(&sb);
+}
+
+void json_delete(JsonNode *node)
+{
+	if (node != NULL) {
+		json_remove_from_parent(node);
+		
+		switch (node->tag) {
+			case JSON_STRING:
+				free(node->string_);
+				break;
+			case JSON_ARRAY:
+			case JSON_OBJECT:
+			{
+				JsonNode *child, *next;
+				for (child = node->children.head; child != NULL; child = next) {
+					next = child->next;
+					json_delete(child);
+				}
+				break;
+			}
+			default:;
+		}
+		
+		free(node);
+	}
+}
+
+int json_validate(const char *json)
+{
+	const char *s = json;
+	
+	skip_space(&s);
+	if (!parse_value(&s, NULL))
+		return FALSE;
+	
+	skip_space(&s);
+	if (*s != 0)
+		return FALSE;
+	
+	return TRUE;
+}
+
+JsonNode *json_find_element(JsonNode *array, unsigned int idx)
+{
+	JsonNode *element;
+	unsigned int i = 0;
+	
+	if (array == NULL || array->tag != JSON_ARRAY)
+		return NULL;
+	
+	json_foreach(element, array) {
+		if (i == idx)
+			return element;
+		i++;
+	}
+	
+	return NULL;
+}
+
+JsonNode *json_find_member(JsonNode *object, const char *name)
+{
+	JsonNode *member;
+	
+	if (object == NULL || object->tag != JSON_OBJECT)
+		return NULL;
+	
+	json_foreach(member, object)
+		if (strcmp(member->key, name) == 0)
+			return member;
+	
+	return NULL;
+}
+
+JsonNode *json_first_child(const JsonNode *node)
+{
+	if (node != NULL && (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT))
+		return node->children.head;
+	return NULL;
+}
+
+static JsonNode *mknode(JsonTag tag)
+{
+	JsonNode *ret = (JsonNode*) calloc(1, sizeof(JsonNode));
+	if (ret == NULL) {
+		json_oom();
+		return NULL;
+	}
+	ret->tag = tag;
+	return ret;
+}
+
+JsonNode *json_mknull(void)
+{
+	return mknode(JSON_NULL);
+}
+
+JsonNode *json_mkbool(int b)
+{
+	JsonNode *ret = mknode(JSON_BOOL);
+	ret->bool_ = b;
+	return ret;
+}
+
+static JsonNode *mkstring(char *s)
+{
+	JsonNode *ret = mknode(JSON_STRING);
+	ret->string_ = s;
+	return ret;
+}
+
+JsonNode *json_mkstring(const char *s)
+{
+	return mkstring(json_strdup(s));
+}
+
+JsonNode *json_mknumber(double n)
+{
+	JsonNode *node = mknode(JSON_NUMBER);
+	node->number_ = n;
+	return node;
+}
+
+JsonNode *json_mkarray(void)
+{
+	return mknode(JSON_ARRAY);
+}
+
+JsonNode *json_mkobject(void)
+{
+	return mknode(JSON_OBJECT);
+}
+
+static void append_node(JsonNode *parent, JsonNode *child)
+{
+	child->parent = parent;
+	child->prev = parent->children.tail;
+	child->next = NULL;
+	
+	if (parent->children.tail != NULL)
+		parent->children.tail->next = child;
+	else
+		parent->children.head = child;
+	parent->children.tail = child;
+}
+
+static void prepend_node(JsonNode *parent, JsonNode *child)
+{
+	child->parent = parent;
+	child->prev = NULL;
+	child->next = parent->children.head;
+	
+	if (parent->children.head != NULL)
+		parent->children.head->prev = child;
+	else
+		parent->children.tail = child;
+	parent->children.head = child;
+}
+
+static void append_member(JsonNode *object, char *key, JsonNode *value)
+{
+	value->key = key;
+	append_node(object, value);
+}
+
+void json_append_element(JsonNode *array, JsonNode *element)
+{
+	assert(array->tag == JSON_ARRAY);
+	assert(element->parent == NULL);
+	
+	append_node(array, element);
+}
+
+void json_prepend_element(JsonNode *array, JsonNode *element)
+{
+	assert(array->tag == JSON_ARRAY);
+	assert(element->parent == NULL);
+	
+	prepend_node(array, element);
+}
+
+void json_append_member(JsonNode *object, const char *key, JsonNode *value)
+{
+	assert(object->tag == JSON_OBJECT);
+	assert(value->parent == NULL);
+	
+	append_member(object, json_strdup(key), value);
+}
+
+void json_prepend_member(JsonNode *object, const char *key, JsonNode *value)
+{
+	assert(object->tag == JSON_OBJECT);
+	assert(value->parent == NULL);
+	
+	value->key = json_strdup(key);
+	prepend_node(object, value);
+}
+
+void json_remove_from_parent(JsonNode *node)
+{
+	JsonNode *parent = node->parent;
+	
+	if (parent != NULL) {
+		if (node->prev != NULL)
+			node->prev->next = node->next;
+		else
+			parent->children.head = node->next;
+		if (node->next != NULL)
+			node->next->prev = node->prev;
+		else
+			parent->children.tail = node->prev;
+		
+		free(node->key);
+		
+		node->parent = NULL;
+		node->prev = node->next = NULL;
+		node->key = NULL;
+	}
+}
+
+static int parse_value(const char **sp, JsonNode **out)
+{
+	const char *s = *sp;
+	
+	switch (*s) {
+		case 'n':
+			if (expect_literal(&s, "null")) {
+				if (out)
+					*out = json_mknull();
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		
+		case 'f':
+			if (expect_literal(&s, "false")) {
+				if (out)
+					*out = json_mkbool(FALSE);
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		
+		case 't':
+			if (expect_literal(&s, "true")) {
+				if (out)
+					*out = json_mkbool(TRUE);
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		
+		case '"': {
+			char *str;
+			if (parse_string(&s, out ? &str : NULL)) {
+				if (out)
+					*out = mkstring(str);
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		}
+		
+		case '[':
+			if (parse_array(&s, out)) {
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		
+		case '{':
+			if (parse_object(&s, out)) {
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		
+		default: {
+			double num;
+			if (parse_number(&s, out ? &num : NULL)) {
+				if (out)
+					*out = json_mknumber(num);
+				*sp = s;
+				return TRUE;
+			}
+			return FALSE;
+		}
+	}
+}
+
+static int parse_array(const char **sp, JsonNode **out)
+{
+	const char *s = *sp;
+	JsonNode *ret = out ? json_mkarray() : NULL;
+	JsonNode *element;
+	
+	if (*s++ != '[')
+		goto failure;
+	skip_space(&s);
+	
+	if (*s == ']') {
+		s++;
+		goto success;
+	}
+	
+	for (;;) {
+		if (!parse_value(&s, out ? &element : NULL))
+			goto failure;
+		skip_space(&s);
+		
+		if (out)
+			json_append_element(ret, element);
+		
+		if (*s == ']') {
+			s++;
+			goto success;
+		}
+		
+		if (*s++ != ',')
+			goto failure;
+		skip_space(&s);
+	}
+	
+success:
+	*sp = s;
+	if (out)
+		*out = ret;
+	return TRUE;
+
+failure:
+	json_delete(ret);
+	return FALSE;
+}
+
+static int parse_object(const char **sp, JsonNode **out)
+{
+	const char *s = *sp;
+	JsonNode *ret = out ? json_mkobject() : NULL;
+	char *key;
+	JsonNode *value;
+	
+	if (*s++ != '{')
+		goto failure;
+	skip_space(&s);
+	
+	if (*s == '}') {
+		s++;
+		goto success;
+	}
+	
+	for (;;) {
+		if (!parse_string(&s, out ? &key : NULL))
+			goto failure;
+		skip_space(&s);
+		
+		if (*s++ != ':')
+			goto failure_free_key;
+		skip_space(&s);
+		
+		if (!parse_value(&s, out ? &value : NULL))
+			goto failure_free_key;
+		skip_space(&s);
+		
+		if (out)
+			append_member(ret, key, value);
+		
+		if (*s == '}') {
+			s++;
+			goto success;
+		}
+		
+		if (*s++ != ',')
+			goto failure;
+		skip_space(&s);
+	}
+	
+success:
+	*sp = s;
+	if (out)
+		*out = ret;
+	return TRUE;
+
+failure_free_key:
+	if (out)
+		free(key);
+failure:
+	json_delete(ret);
+	return FALSE;
+}
+
+int parse_string(const char **sp, char **out)
+{
+	const char *s = *sp;
+	SB sb;
+	char throwaway_buffer[4];
+		/* enough space for a UTF-8 character */
+	char *b;
+	
+	if (*s++ != '"')
+		return FALSE;
+	
+	if (out) {
+		sb_init(&sb);
+		sb_need(&sb, 4);
+		b = sb.cur;
+	} else {
+		b = throwaway_buffer;
+	}
+	
+	while (*s != '"') {
+		unsigned char c = *s++;
+		
+		/* Parse next character, and write it to b. */
+		if (c == '\\') {
+			c = *s++;
+			switch (c) {
+				case '"':
+				case '\\':
+				case '/':
+					*b++ = c;
+					break;
+				case 'b':
+					*b++ = '\b';
+					break;
+				case 'f':
+					*b++ = '\f';
+					break;
+				case 'n':
+					*b++ = '\n';
+					break;
+				case 'r':
+					*b++ = '\r';
+					break;
+				case 't':
+					*b++ = '\t';
+					break;
+				case 'u':
+				{
+					uint16_t uc, lc;
+					uint32_t unicode;
+					
+					if (!parse_hex16(&s, &uc))
+						goto failed;
+					
+					if (uc >= 0xD800 && uc <= 0xDFFF) {
+						/* Handle UTF-16 surrogate pair. */
+						if (*s++ != '\\' || *s++ != 'u' || !parse_hex16(&s, &lc))
+							goto failed; /* Incomplete surrogate pair. */
+						if (!from_surrogate_pair(uc, lc, &unicode))
+							goto failed; /* Invalid surrogate pair. */
+					} else if (uc == 0) {
+						/* Disallow "\u0000". */
+						goto failed;
+					} else {
+						unicode = uc;
+					}
+					
+					b += utf8_write_char(unicode, b);
+					break;
+				}
+				default:
+					/* Invalid escape */
+					goto failed;
+			}
+		} else if (c <= 0x1F) {
+			/* Control characters are not allowed in string literals. */
+			goto failed;
+		} else {
+			/* Validate and echo a UTF-8 character. */
+			int len;
+			
+			s--;
+			len = utf8_validate_cz(s);
+			if (len == 0)
+				goto failed; /* Invalid UTF-8 character. */
+			
+			while (len--)
+				*b++ = *s++;
+		}
+		
+		/*
+		 * Update sb to know about the new bytes,
+		 * and set up b to write another character.
+		 */
+		if (out) {
+			sb.cur = b;
+			sb_need(&sb, 4);
+			b = sb.cur;
+		} else {
+			b = throwaway_buffer;
+		}
+	}
+	s++;
+	
+	if (out)
+		*out = sb_finish(&sb);
+	*sp = s;
+	return TRUE;
+
+failed:
+	if (out)
+		sb_free(&sb);
+	return FALSE;
+}
+
+/*
+ * The JSON spec says that a number shall follow this precise pattern
+ * (spaces and quotes added for readability):
+ *	 '-'? (0 | [1-9][0-9]*) ('.' [0-9]+)? ([Ee] [+-]? [0-9]+)?
+ *
+ * However, some JSON parsers are more liberal.  For instance, PHP accepts
+ * '.5' and '1.'.  JSON.parse accepts '+3'.
+ *
+ * This function takes the strict approach.
+ */
+int parse_number(const char **sp, double *out)
+{
+	const char *s = *sp;
+
+	/* '-'? */
+	if (*s == '-')
+		s++;
+
+	/* (0 | [1-9][0-9]*) */
+	if (*s == '0') {
+		s++;
+	} else {
+		if (!is_digit(*s))
+			return FALSE;
+		do {
+			s++;
+		} while (is_digit(*s));
+	}
+
+	/* ('.' [0-9]+)? */
+	if (*s == '.') {
+		s++;
+		if (!is_digit(*s))
+			return FALSE;
+		do {
+			s++;
+		} while (is_digit(*s));
+	}
+
+	/* ([Ee] [+-]? [0-9]+)? */
+	if (*s == 'E' || *s == 'e') {
+		s++;
+		if (*s == '+' || *s == '-')
+			s++;
+		if (!is_digit(*s))
+			return FALSE;
+		do {
+			s++;
+		} while (is_digit(*s));
+	}
+
+	if (out)
+		*out = strtod(*sp, NULL);
+
+	*sp = s;
+	return TRUE;
+}
+
+static void skip_space(const char **sp)
+{
+	const char *s = *sp;
+	while (is_space(*s))
+		s++;
+	*sp = s;
+}
+
+static void emit_value(SB *out, const JsonNode *node)
+{
+	assert(tag_is_valid(node->tag));
+	switch (node->tag) {
+		case JSON_NULL:
+			sb_puts(out, "null");
+			break;
+		case JSON_BOOL:
+			sb_puts(out, node->bool_ ? "true" : "false");
+			break;
+		case JSON_STRING:
+			emit_string(out, node->string_);
+			break;
+		case JSON_NUMBER:
+			emit_number(out, node->number_);
+			break;
+		case JSON_ARRAY:
+			emit_array(out, node);
+			break;
+		case JSON_OBJECT:
+			emit_object(out, node);
+			break;
+		default:
+			assert(FALSE);
+	}
+}
+
+void emit_value_indented(SB *out, const JsonNode *node, const char *space, int indent_level)
+{
+	assert(tag_is_valid(node->tag));
+	switch (node->tag) {
+		case JSON_NULL:
+			sb_puts(out, "null");
+			break;
+		case JSON_BOOL:
+			sb_puts(out, node->bool_ ? "true" : "false");
+			break;
+		case JSON_STRING:
+			emit_string(out, node->string_);
+			break;
+		case JSON_NUMBER:
+			emit_number(out, node->number_);
+			break;
+		case JSON_ARRAY:
+			emit_array_indented(out, node, space, indent_level);
+			break;
+		case JSON_OBJECT:
+			emit_object_indented(out, node, space, indent_level);
+			break;
+		default:
+			assert(FALSE);
+	}
+}
+
+static void emit_array(SB *out, const JsonNode *array)
+{
+	const JsonNode *element;
+	
+	sb_putc(out, '[');
+	json_foreach(element, array) {
+		emit_value(out, element);
+		if (element->next != NULL)
+			sb_putc(out, ',');
+	}
+	sb_putc(out, ']');
+}
+
+static void emit_array_indented(SB *out, const JsonNode *array, const char *space, int indent_level)
+{
+	const JsonNode *element = array->children.head;
+	int i;
+	
+	if (element == NULL) {
+		sb_puts(out, "[]");
+		return;
+	}
+	
+	sb_puts(out, "[\n");
+	while (element != NULL) {
+		for (i = 0; i < indent_level + 1; i++)
+			sb_puts(out, space);
+		emit_value_indented(out, element, space, indent_level + 1);
+		
+		element = element->next;
+		sb_puts(out, element != NULL ? ",\n" : "\n");
+	}
+	for (i = 0; i < indent_level; i++)
+		sb_puts(out, space);
+	sb_putc(out, ']');
+}
+
+static void emit_object(SB *out, const JsonNode *object)
+{
+	const JsonNode *member;
+	
+	sb_putc(out, '{');
+	json_foreach(member, object) {
+		emit_string(out, member->key);
+		sb_putc(out, ':');
+		emit_value(out, member);
+		if (member->next != NULL)
+			sb_putc(out, ',');
+	}
+	sb_putc(out, '}');
+}
+
+static void emit_object_indented(SB *out, const JsonNode *object, const char *space, int indent_level)
+{
+	const JsonNode *member = object->children.head;
+	int i;
+	
+	if (member == NULL) {
+		sb_puts(out, "{}");
+		return;
+	}
+	
+	sb_puts(out, "{\n");
+	while (member != NULL) {
+		for (i = 0; i < indent_level + 1; i++)
+			sb_puts(out, space);
+		emit_string(out, member->key);
+		sb_puts(out, ": ");
+		emit_value_indented(out, member, space, indent_level + 1);
+		
+		member = member->next;
+		sb_puts(out, member != NULL ? ",\n" : "\n");
+	}
+	for (i = 0; i < indent_level; i++)
+		sb_puts(out, space);
+	sb_putc(out, '}');
+}
+
+void emit_string(SB *out, const char *str)
+{
+	const char *s = str;
+	char *b;
+	
+	assert(utf8_validate(str));
+	
+	/*
+	 * 14 bytes is enough space to write up to two
+	 * \uXXXX escapes and two quotation marks.
+	 */
+	sb_need(out, 14);
+	b = out->cur;
+	
+	*b++ = '"';
+	while (*s != 0) {
+		unsigned char c = *s++;
+		
+		/* Encode the next character, and write it to b. */
+		switch (c) {
+			case '"':
+				*b++ = '\\';
+				*b++ = '"';
+				break;
+			case '\\':
+				*b++ = '\\';
+				*b++ = '\\';
+				break;
+			case '\b':
+				*b++ = '\\';
+				*b++ = 'b';
+				break;
+			case '\f':
+				*b++ = '\\';
+				*b++ = 'f';
+				break;
+			case '\n':
+				*b++ = '\\';
+				*b++ = 'n';
+				break;
+			case '\r':
+				*b++ = '\\';
+				*b++ = 'r';
+				break;
+			case '\t':
+				*b++ = '\\';
+				*b++ = 't';
+				break;
+			default: {
+				int len;
+				
+				s--;
+				len = utf8_validate_cz(s);
+				
+				if (len == 0) {
+					/*
+					 * Handle invalid UTF-8 character gracefully in production
+					 * by writing a replacement character (U+FFFD)
+					 * and skipping a single byte.
+					 *
+					 * This should never happen when assertions are enabled
+					 * due to the assertion at the beginning of this function.
+					 */
+					assert(FALSE);
+					*b++ = 0xEF;
+					*b++ = 0xBF;
+					*b++ = 0xBD;
+					s++;
+				} else if (c < 0x1F) {
+					/* Encode using \u.... */
+					uint32_t unicode;
+					
+					s += utf8_read_char(s, &unicode);
+					
+					if (unicode <= 0xFFFF) {
+						*b++ = '\\';
+						*b++ = 'u';
+						b += write_hex16(b, unicode);
+					} else {
+						/* Produce a surrogate pair. */
+						uint16_t uc, lc;
+						assert(unicode <= 0x10FFFF);
+						to_surrogate_pair(unicode, &uc, &lc);
+						*b++ = '\\';
+						*b++ = 'u';
+						b += write_hex16(b, uc);
+						*b++ = '\\';
+						*b++ = 'u';
+						b += write_hex16(b, lc);
+					}
+				} else {
+					/* Write the character directly. */
+					while (len--)
+						*b++ = *s++;
+				}
+				
+				break;
+			}
+		}
+	
+		/*
+		 * Update *out to know about the new bytes,
+		 * and set up b to write another encoded character.
+		 */
+		out->cur = b;
+		sb_need(out, 14);
+		b = out->cur;
+	}
+	*b++ = '"';
+	
+	out->cur = b;
+}
+
+static void emit_number(SB *out, double num)
+{
+	/*
+	 * This isn't exactly how JavaScript renders numbers,
+	 * but it should produce valid JSON for reasonable numbers
+	 * preserve precision well enough, and avoid some oddities
+	 * like 0.3 -> 0.299999999999999988898 .
+	 */
+	char buf[64];
+	sprintf(buf, "%.16g", num);
+	
+	if (number_is_valid(buf))
+		sb_puts(out, buf);
+	else
+		sb_puts(out, "null");
+}
+
+static int tag_is_valid(unsigned int tag)
+{
+	return (/* tag >= JSON_NULL && */ tag <= JSON_OBJECT);
+}
+
+static int number_is_valid(const char *num)
+{
+	return (parse_number(&num, NULL) && *num == '\0');
+}
+
+static int expect_literal(const char **sp, const char *str)
+{
+	const char *s = *sp;
+	
+	while (*str != '\0')
+		if (*s++ != *str++)
+			return FALSE;
+	
+	*sp = s;
+	return TRUE;
+}
+
+/*
+ * Parses exactly 4 hex characters (capital or lowercase).
+ * Fails if any input chars are not [0-9A-Fa-f].
+ */
+static int parse_hex16(const char **sp, uint16_t *out)
+{
+	const char *s = *sp;
+	uint16_t ret = 0;
+	uint16_t i;
+	uint16_t tmp;
+	char c;
+
+	for (i = 0; i < 4; i++) {
+		c = *s++;
+		if (c >= '0' && c <= '9')
+			tmp = c - '0';
+		else if (c >= 'A' && c <= 'F')
+			tmp = c - 'A' + 10;
+		else if (c >= 'a' && c <= 'f')
+			tmp = c - 'a' + 10;
+		else
+			return FALSE;
+
+		ret <<= 4;
+		ret += tmp;
+	}
+	
+	if (out)
+		*out = ret;
+	*sp = s;
+	return TRUE;
+}
+
+/*
+ * Encodes a 16-bit number into hexadecimal,
+ * writing exactly 4 hex chars.
+ */
+static int write_hex16(char *out, uint16_t val)
+{
+	const char *hex = "0123456789ABCDEF";
+	
+	*out++ = hex[(val >> 12) & 0xF];
+	*out++ = hex[(val >> 8)  & 0xF];
+	*out++ = hex[(val >> 4)  & 0xF];
+	*out++ = hex[ val        & 0xF];
+	
+	return 4;
+}
+
+int json_check(const JsonNode *node, char errmsg[256])
+{
+	#define problem(...) do { \
+			if (errmsg != NULL) \
+				snprintf(errmsg, 256, __VA_ARGS__); \
+			return FALSE; \
+		} while (0)
+	
+	if (node->key != NULL && !utf8_validate(node->key))
+		problem("key contains invalid UTF-8");
+	
+	if (!tag_is_valid(node->tag))
+		problem("tag is invalid (%u)", node->tag);
+	
+	if (node->tag == JSON_BOOL) {
+		if (node->bool_ != FALSE && node->bool_ != TRUE)
+			problem("bool_ is neither false (%d) nor true (%d)", (int)FALSE, (int)TRUE);
+	} else if (node->tag == JSON_STRING) {
+		if (node->string_ == NULL)
+			problem("string_ is NULL");
+		if (!utf8_validate(node->string_))
+			problem("string_ contains invalid UTF-8");
+	} else if (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT) {
+		JsonNode *head = node->children.head;
+		JsonNode *tail = node->children.tail;
+		
+		if (head == NULL || tail == NULL) {
+			if (head != NULL)
+				problem("tail is NULL, but head is not");
+			if (tail != NULL)
+				problem("head is NULL, but tail is not");
+		} else {
+			JsonNode *child;
+			JsonNode *last = NULL;
+			
+			if (head->prev != NULL)
+				problem("First child's prev pointer is not NULL");
+			
+			for (child = head; child != NULL; last = child, child = child->next) {
+				if (child == node)
+					problem("node is its own child");
+				if (child->next == child)
+					problem("child->next == child (cycle)");
+				if (child->next == head)
+					problem("child->next == head (cycle)");
+				
+				if (child->parent != node)
+					problem("child does not point back to parent");
+				if (child->next != NULL && child->next->prev != child)
+					problem("child->next does not point back to child");
+				
+				if (node->tag == JSON_ARRAY && child->key != NULL)
+					problem("Array element's key is not NULL");
+				if (node->tag == JSON_OBJECT && child->key == NULL)
+					problem("Object member's key is NULL");
+				
+				if (!json_check(child, errmsg))
+					return FALSE;
+			}
+			
+			if (last != tail)
+				problem("tail does not match pointer found by starting at head and following next links");
+		}
+	}
+	
+	return TRUE;
+	
+	#undef problem
+}
diff --git a/lib/glibc-glob.c b/lib/glibc-glob.c
index 43b02bc..6f676e2 100644
--- a/lib/glibc-glob.c
+++ b/lib/glibc-glob.c
@@ -1367,10 +1367,10 @@ glob_in_dir (const char *pattern, const char *directory, int flags,
 	     "*a/".  */
 	  names = (struct globlink *) __alloca (sizeof (struct globlink));
 	  names->name = (char *) malloc (1);
+	  names->next = NULL;
 	  if (names->name == NULL)
 	    goto memory_error;
 	  names->name[0] = '\0';
-	  names->next = NULL;
 	  nfound = 1;
 	  meta = 0;
 	}
diff --git a/lib/tpl.c b/lib/hanson-tpl.c
similarity index 99%
rename from lib/tpl.c
rename to lib/hanson-tpl.c
index 5d10abf..1ee5c7c 100755
--- a/lib/tpl.c
+++ b/lib/hanson-tpl.c
@@ -57,7 +57,7 @@ typedef unsigned __int64 uint64_t;
 #include <sys/mman.h>   /* mmap */
 #endif
 
-#include "tpl.h"
+#include "hanson-tpl.h"
 
 #define TPL_GATHER_BUFLEN 8192
 #define TPL_MAGIC "tpl"
@@ -182,7 +182,6 @@ static int tpl_mmap_output_file(char *filename, size_t sz, void **text_out);
 static int tpl_cpu_bigendian(void);
 static int tpl_needs_endian_swap(void *);
 static void tpl_byteswap(void *word, int len);
-static void tpl_fatal(char *fmt, ...);
 static int tpl_serlen(tpl_node *r, tpl_node *n, void *dv, size_t *serlen);
 static int tpl_unpackA0(tpl_node *r);
 static int tpl_oops(const char *fmt, ...);
@@ -322,7 +321,7 @@ static tpl_node *tpl_map_va(char *fmt, va_list ap) {
     tpl_pound_data *pd;
     int *fxlens, num_fxlens, pound_num, pound_prod, applies_to_struct;
     int contig_fxlens[10]; /* temp space for contiguous fxlens */
-    int num_contig_fxlens, i, j;
+    unsigned int num_contig_fxlens, i, j;
     ptrdiff_t inter_elt_len=0; /* padded element length of contiguous structs in array */
 
 
@@ -423,9 +422,10 @@ static tpl_node *tpl_map_va(char *fmt, va_list ap) {
                   }
                   if (num_contig_fxlens >= (sizeof(contig_fxlens)/sizeof(contig_fxlens[0]))) {
                     tpl_hook.fatal("contiguous # exceeds hardcoded limit\n");
+                  } else {
+                    contig_fxlens[num_contig_fxlens++] = pound_num;
+                    pound_prod *= pound_num;
                   }
-                  contig_fxlens[num_contig_fxlens++] = pound_num;
-                  pound_prod *= pound_num;
                 }
                 /* increment c to skip contiguous # so its points to last one */
                 c = peek-1;
@@ -1013,6 +1013,7 @@ TPL_API int tpl_dump(tpl_node *r, int mode, ...) {
                 bufv += rc;
             } else if (rc == -1) {
                 if (errno == EINTR || errno == EAGAIN) continue;
+                va_end(ap);
                 tpl_hook.oops("error writing to fd %d: %s\n", fd, strerror(errno));
                 free(buf);
                 return -1;
@@ -1025,6 +1026,7 @@ TPL_API int tpl_dump(tpl_node *r, int mode, ...) {
           pa_addr = (void*)va_arg(ap, void*);
           pa_sz = va_arg(ap, size_t);
           if (pa_sz < sz) {
+              va_end(ap);
               tpl_hook.oops("tpl_dump: buffer too small, need %d bytes\n", sz);
               return -1;
           }
@@ -1247,7 +1249,7 @@ static int tpl_needs_endian_swap(void *d) {
 }
 
 static size_t tpl_size_for(char c) {
-  int i;
+  register size_t i;
   for(i=0; i < sizeof(tpl_types)/sizeof(tpl_types[0]); i++) {
     if (tpl_types[i].c == c) return tpl_types[i].sz;
   }
@@ -1471,6 +1473,7 @@ TPL_API int tpl_load(tpl_node *r, int mode, ...) {
     } else if (mode & TPL_FD) {
         fd = va_arg(ap,int);
     } else {
+        va_end(ap);
         tpl_hook.oops("unsupported tpl_load mode %d\n", mode);
         return -1;
     }
@@ -1730,8 +1733,8 @@ static int tpl_mmap_output_file(char *filename, size_t sz, void **text_out) {
     }
     if (ftruncate(fd,sz) == -1) {
         tpl_hook.oops("ftruncate failed: %s\n", strerror(errno));
-        munmap( text, sz );
-        close(fd);
+        (void) munmap( text, sz );
+        (void) close(fd);
         return -1;
     }
     *text_out = text;
@@ -2151,12 +2154,13 @@ static void tpl_byteswap(void *word, int len) {
     }
 }
 
-static void tpl_fatal(char *fmt, ...) {
+void tpl_fatal(char *fmt, ...) {
     va_list ap;
     char exit_msg[100];
 
+    memset(exit_msg, '\0', sizeof(exit_msg));
     va_start(ap,fmt);
-    vsnprintf(exit_msg, 100, fmt, ap);
+    vsnprintf(exit_msg, sizeof(exit_msg)-1, fmt, ap);
     va_end(ap);
 
     tpl_hook.oops("%s", exit_msg);
@@ -2255,7 +2259,7 @@ static int tpl_gather_blocking(int fd, void **img, size_t *sz) {
     do { 
         rc = read(fd,&((*(char**)img)[i]),tpllen-i);
         i += (rc>0) ? rc : 0;
-    } while ((rc==-1 && (errno==EINTR||errno==EAGAIN)) || (rc>0 && i<tpllen));
+    } while ((rc==-1 && (errno==EINTR||errno==EAGAIN)) || (rc>0 && (uint32_t)i<tpllen));
 
     if (rc<0) {
         tpl_hook.oops("tpl_gather_fd_blocking failed: %s\n", strerror(errno));
@@ -2265,7 +2269,7 @@ static int tpl_gather_blocking(int fd, void **img, size_t *sz) {
         /* tpl_hook.oops("tpl_gather_fd_blocking: eof\n"); */
         tpl_hook.free(*img);
         return 0;
-    } else if (i != tpllen) {
+    } else if ((uint32_t)i != tpllen) {
         tpl_hook.oops("internal error\n");
         tpl_hook.free(*img);
         return -1;
diff --git a/lib/libcap/_makenames.c b/lib/libcap/_makenames.c
index 48933ec..ddbaf05 100644
--- a/lib/libcap/_makenames.c
+++ b/lib/libcap/_makenames.c
@@ -1,6 +1,4 @@
 /*
- * $Id: _makenames.c,v 1.1 2003-01-03 02:16:17 jwm Exp $
- *
  * Copyright (c) 1997-8 Andrew G. Morgan <morgan at linux.kernel.org>
  *
  * This is a file to make the capability <-> string mappings for
diff --git a/lib/libcap/cap_alloc.c b/lib/libcap/cap_alloc.c
index 8beee74..c5962f0 100644
--- a/lib/libcap/cap_alloc.c
+++ b/lib/libcap/cap_alloc.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_alloc.c,v 1.3 2008-08-06 17:00:41 castaglia Exp $
- *
  * Copyright (c) 1997-8 Andrew G Morgan <morgan at linux.kernel.org>
  *
  * See end of file for Log.
diff --git a/lib/libcap/cap_extint.c b/lib/libcap/cap_extint.c
index 1dae41e..75ce508 100644
--- a/lib/libcap/cap_extint.c
+++ b/lib/libcap/cap_extint.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_extint.c,v 1.1 2003-01-03 02:16:17 jwm Exp $
- *
  * Copyright (c) 1997-8 Andrew G Morgan <morgan at linux.kernel.org>
  *
  * See end of file for Log.
diff --git a/lib/libcap/cap_file.c b/lib/libcap/cap_file.c
index 348644a..65522f4 100644
--- a/lib/libcap/cap_file.c
+++ b/lib/libcap/cap_file.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_file.c,v 1.1 2003-01-03 02:16:17 jwm Exp $
- *
  * Copyright (c) 1997 Andrew G Morgan <morgan at linux.kernel.org>
  *
  * See end of file for Log.
diff --git a/lib/libcap/cap_flag.c b/lib/libcap/cap_flag.c
index 2a1d02a..f78dc05 100644
--- a/lib/libcap/cap_flag.c
+++ b/lib/libcap/cap_flag.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_flag.c,v 1.1 2003-01-03 02:16:17 jwm Exp $
- *
  * Copyright (c) 1997-8 Andrew G. Morgan <morgan at linux.kernel.org>
  *
  * See end of file for Log.
diff --git a/lib/libcap/cap_proc.c b/lib/libcap/cap_proc.c
index 6837232..73a02d5 100644
--- a/lib/libcap/cap_proc.c
+++ b/lib/libcap/cap_proc.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_proc.c,v 1.2 2008-08-06 17:00:41 castaglia Exp $
- *
  * Copyright (c) 1997-8 Andrew G Morgan <morgan at linux.kernel.org>
  *
  * See end of file for Log.
diff --git a/lib/libcap/cap_sys.c b/lib/libcap/cap_sys.c
index 437531d..78e64dd 100644
--- a/lib/libcap/cap_sys.c
+++ b/lib/libcap/cap_sys.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_sys.c,v 1.2 2005-01-25 19:30:55 castaglia Exp $
- *
  * Copyright (c) 1997-8 Andrew G. Morgan   <morgan at linux.kernel.org>
  *
  * This file contains the system calls for getting and setting
diff --git a/lib/libcap/cap_text.c b/lib/libcap/cap_text.c
index 32a30ed..11f4e48 100644
--- a/lib/libcap/cap_text.c
+++ b/lib/libcap/cap_text.c
@@ -1,6 +1,4 @@
 /*
- * $Id: cap_text.c,v 1.2 2003-05-15 00:49:13 castaglia Exp $
- *
  * Copyright (c) 1997-8 Andrew G Morgan <morgan at linux.kernel.org>
  * Copyright (c) 1997 Andrew Main <zefram at dcs.warwick.ac.uk>
  *
diff --git a/lib/libcap/libcap.h b/lib/libcap/libcap.h
index ae588c8..5751651 100644
--- a/lib/libcap/libcap.h
+++ b/lib/libcap/libcap.h
@@ -1,6 +1,4 @@
 /*
- * $Id: libcap.h,v 1.5 2008-08-23 02:49:48 castaglia Exp $
- *
  * Copyright (c) 1997 Andrew G Morgan <morgan at linux.kernel.org>
  *
  * See end of file for Log.
diff --git a/lib/libltdl/ltdl.c b/lib/libltdl/ltdl.c
index 726b2d8..49311c3 100644
--- a/lib/libltdl/ltdl.c
+++ b/lib/libltdl/ltdl.c
@@ -2072,6 +2072,7 @@ lt_dlpath_insertdir (char **ppath, char *before, const char *dir)
   char  *canonical	= 0;
   char  *argz		= 0;
   size_t argz_len	= 0;
+  char  *pdir           = 0;
 
   assert (ppath);
   assert (dir && *dir);
@@ -2090,7 +2091,7 @@ lt_dlpath_insertdir (char **ppath, char *before, const char *dir)
       assert (!before);		/* BEFORE cannot be set without PPATH.  */
       assert (dir);		/* Without DIR, don't call this function!  */
 
-      *ppath = lt__strdup (dir);
+      *ppath = pdir = lt__strdup (dir);
       if (*ppath == 0)
 	++errors;
 
@@ -2130,6 +2131,7 @@ lt_dlpath_insertdir (char **ppath, char *before, const char *dir)
  cleanup:
   FREE (argz);
   FREE (canonical);
+  FREE (pdir);
 
   return errors;
 }
diff --git a/lib/pr-syslog.c b/lib/pr-syslog.c
index 7358228..244a7d5 100644
--- a/lib/pr-syslog.c
+++ b/lib/pr-syslog.c
@@ -27,9 +27,6 @@
  * SUCH DAMAGE.
  */
 
-/* $Id: pr-syslog.c,v 1.26 2012-01-25 07:03:24 castaglia Exp $
- */
-
 #include "conf.h"
 
 #if defined(SOLARIS2) || defined(IRIX6) || defined(SYSV5UNIXWARE7)
@@ -109,7 +106,7 @@ static void pr_vsyslog(int sockfd, int pri, register const char *fmt,
   time_t now;
   static char logbuf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
   size_t buflen = 0;
-  int saved_errno = errno;
+  int len = 0, saved_errno = errno;
 
 #ifdef HAVE_DEV_LOG_STREAMS
   struct strbuf ctl, dat;
@@ -126,21 +123,24 @@ static void pr_vsyslog(int sockfd, int pri, register const char *fmt,
   memset(logbuf, '\0', sizeof(logbuf));
 
   /* Check for invalid bits. */
-  if (pri & ~(LOG_PRIMASK|LOG_FACMASK))
+  if (pri & ~(LOG_PRIMASK|LOG_FACMASK)) {
     pri &= LOG_PRIMASK|LOG_FACMASK;
+  }
 
   /* Check priority against setlogmask values. */
-  if ((LOG_MASK(pri & LOG_PRIMASK) & log_mask) == 0)
+  if ((LOG_MASK(pri & LOG_PRIMASK) & log_mask) == 0) {
     return;
+  }
 
   /* Set default facility if none specified. */
-  if ((pri & LOG_FACMASK) == 0)
+  if ((pri & LOG_FACMASK) == 0) {
     pri |= log_facility;
+  }
 
 #ifndef HAVE_DEV_LOG_STREAMS
-  snprintf(logbuf, sizeof(logbuf), "<%d>", pri);
+  len = snprintf(logbuf, sizeof(logbuf), "<%d>", pri);
   logbuf[sizeof(logbuf)-1] = '\0';
-  buflen = strlen(logbuf);
+  buflen += len;
 
 # ifdef HAVE_TZNAME
   /* Preserve the old tzname setting. */
@@ -164,37 +164,41 @@ static void pr_vsyslog(int sockfd, int pri, register const char *fmt,
   /* Skip past the leading "day of week" prefix. */
   timestr += 4;
 
-  snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, "%.15s ", timestr);
+  len = snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, "%.15s ", timestr);
   logbuf[sizeof(logbuf)-1] = '\0';
-  buflen = strlen(logbuf);
+  buflen += len;
 #endif
 
   time(&now);
 
-  if (log_ident == NULL)
+  if (log_ident == NULL) {
 #ifdef HAVE___PROGNAME
     log_ident = __progname;
 #else
     log_ident = "proftpd";
 #endif /* HAVE___PROGNAME */
+  }
 
-  if (buflen < sizeof(logbuf) && log_ident != NULL) {
-    snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, "%s", log_ident);
+  if (buflen < sizeof(logbuf) &&
+      log_ident != NULL) {
+    len = snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, "%s", log_ident);
     logbuf[sizeof(logbuf)-1] = '\0';
-    buflen = strlen(logbuf);
+    buflen += len;
   }
 
-  if (buflen < sizeof(logbuf)-1 && (log_opts & LOG_PID)) {
-    snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen,
-             "[%d]", (int)getpid());
+  if (buflen < sizeof(logbuf)-1 &&
+      (log_opts & LOG_PID)) {
+    len = snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, "[%d]",
+      (int) getpid());
     logbuf[sizeof(logbuf)-1] = '\0';
-    buflen = strlen(logbuf);
+    buflen += len;
   }
 
-  if (buflen < sizeof(logbuf)-1 && log_ident != NULL) {
-    snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, ": ");
+  if (buflen < sizeof(logbuf)-1 &&
+      log_ident != NULL) {
+    len = snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, ": ");
     logbuf[sizeof(logbuf)-1] = '\0';
-    buflen = strlen(logbuf);
+    buflen += len;
   }
 
 #if defined(SOLARIS2_9) || defined(SOLARIS2_10)
@@ -234,10 +238,10 @@ static void pr_vsyslog(int sockfd, int pri, register const char *fmt,
       }
     }
 
-    snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, "[ID %lu %s.%s] ",
-      (unsigned long) msgid, facility_name, level_name);
+    len = snprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen,
+      "[ID %lu %s.%s] ", (unsigned long) msgid, facility_name, level_name);
     logbuf[sizeof(logbuf)-1] = '\0';
-    buflen = strlen(logbuf);
+    buflen += len;
   }
 #endif /* Solaris 9 or 10 */
 
@@ -246,9 +250,9 @@ static void pr_vsyslog(int sockfd, int pri, register const char *fmt,
 
   /* We have the header.  Print the user's format into the buffer.  */
   if (buflen < sizeof(logbuf)) {
-    vsnprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, fmt, ap);
+    len = vsnprintf(&(logbuf[buflen]), sizeof(logbuf) - buflen, fmt, ap);
     logbuf[sizeof(logbuf)-1] = '\0';
-    buflen = strlen(logbuf);
+    buflen += len;
   }
 
   /* Always make sure the buffer is NUL-terminated
@@ -258,15 +262,25 @@ static void pr_vsyslog(int sockfd, int pri, register const char *fmt,
   /* If we have a SOCK_STREAM connection, also send ASCII NUL as a record
    * terminator.
    */
-  if (sock_type == SOCK_STREAM)
+  if (sock_type == SOCK_STREAM) {
     ++buflen;
+  }
+
+  /* If we have exceeded the capacity of the buffer, we're done here. */
+  if (buflen >= sizeof(logbuf)) {
+    return;
+  }
 
 #ifndef HAVE_DEV_LOG_STREAMS
-  send(sockfd, logbuf, buflen, 0);
+  if (sockfd >= 0 &&
+      send(sockfd, logbuf, buflen, 0) < 0) {
+    fprintf(stderr, "error sending log message '%s' to socket fd %d: %s\n",
+      logbuf, sockfd, strerror(errno));
+  }
 #else
 
-  /* Prepare the structs for use by putmsg(). As /dev/log is a STREAMS
-   * device on Solaris (and possibly other platforms?), putmsg() is
+  /* Prepare the structs for use by putmsg(). As /dev/log (or /dev/conslog)
+   * is a STREAMS device on Solaris (and possibly other platforms?), putmsg() is
    * used so that syslog facility and level are properly honored; write()
    * does not seem to work as desired.
    */
@@ -323,7 +337,7 @@ int pr_openlog(const char *ident, int opts, int facility) {
           return -1;
         }
 
-        fcntl(sockfd, F_SETFD, 1);
+        (void) fcntl(sockfd, F_SETFD, 1);
       }
     }
 
@@ -348,21 +362,9 @@ int pr_openlog(const char *ident, int opts, int facility) {
 #else
   sockfd = open(PR_PATH_LOG, O_WRONLY);
 
-# ifdef SOLARIS2
-  /* Workaround for a /dev/log bug (SunSolve bug #4817079) on Solaris. */
-  if (sockfd >= 0) {
-    struct strioctl ic;
-
-    ic.ic_cmd = I_ERRLOG;
-    ic.ic_timout = 0;
-    ic.ic_len = 0;
-    ic.ic_dp = NULL;
-
-    if (ioctl(sockfd, I_STR, &ic) < 0)
-      fprintf(stderr, "error setting I_ERRLOG on " PR_PATH_LOG ": %s\n",
-        strerror(errno));
+  if (sockfd < 0) {
+    fprintf(stderr, "error opening '%s': %s\n", PR_PATH_LOG, strerror(errno));
   }
-# endif /* SOLARIS2 */
 #endif
 
   return sockfd;
diff --git a/lib/pr_fnmatch.c b/lib/pr_fnmatch.c
index a1cb101..8e46a22 100644
--- a/lib/pr_fnmatch.c
+++ b/lib/pr_fnmatch.c
@@ -250,7 +250,11 @@ __wcschrnul (const wchar_t *s, wint_t c)
 # endif
 # define STRLEN(S) strlen (S)
 # define STRCAT(D, S) strcat (D, S)
-# define MEMPCPY(D, S, N) __mempcpy (D, S, N)
+# ifdef HAVE_MEMPCPY
+#  define MEMPCPY(D, S, N) mempcpy (D, S, N)
+# else
+#  define MEMPCPY(D, S, N) __mempcpy (D, S, N)
+# endif
 # define MEMCHR(S, C, N) memchr (S, C, N)
 # define STRCOLL(S1, S2) strcoll (S1, S2)
 # include "pr_fnmatch_loop.c"
diff --git a/lib/pr_fnmatch_loop.c b/lib/pr_fnmatch_loop.c
index 80ac9d0..5138e7b 100644
--- a/lib/pr_fnmatch_loop.c
+++ b/lib/pr_fnmatch_loop.c
@@ -72,7 +72,7 @@ FCT (pattern, string, string_end, no_leading_period, flags, ends)
 {
   register const CHAR *p = pattern, *n = string;
   register UCHAR c;
-  int is_seqval;
+  int is_seqval = 0;
 #ifdef _LIBC
 # if WIDE_CHAR_VERSION
   const char *collseq = (const char *)
diff --git a/lib/sstrncpy.c b/lib/sstrncpy.c
index 71fee6a..c37f0ba 100644
--- a/lib/sstrncpy.c
+++ b/lib/sstrncpy.c
@@ -40,12 +40,11 @@
 #include "libsupp.h"
 
 /* "safe" strncpy, saves room for \0 at end of dest, and refuses to copy
- * more than "n" bytes.
+ * more than "n" bytes.  Returns the number of bytes copied, or -1 if there
+ * was an error.
  */
 int sstrncpy(char *dst, const char *src, size_t n) {
-#ifndef HAVE_STRLCPY
-  register char *d = dst;
-#endif /* HAVE_STRLCPY */
+  register char *d;
   int res = 0;
 
   if (dst == NULL) {
@@ -67,15 +66,7 @@ int sstrncpy(char *dst, const char *src, size_t n) {
     return n;
   }
 
-#ifdef HAVE_STRLCPY
-  strlcpy(dst, src, n);
-
-  /* We want the returned length to be the number of bytes copied as
-   * requested by the caller, not the total length of the src string.
-   */
-  res = n;
-
-#else
+  d = dst;
   if (src && *src) {
     for (; *src && n > 1; n--) {
       *d++ = *src++;
@@ -84,8 +75,5 @@ int sstrncpy(char *dst, const char *src, size_t n) {
   }
 
   *d = '\0';
-#endif /* HAVE_STRLCPY */
-
   return res;
 }
-
diff --git a/locale/Makefile.in b/locale/Makefile.in
index aa0798a..93557e5 100644
--- a/locale/Makefile.in
+++ b/locale/Makefile.in
@@ -18,7 +18,7 @@ PACKAGE=proftpd
 #
 #   `find ../ -name "*.[ch]" -print | grep -v tests\/ | egrep "contrib|include|modules|src" | sort > files.txt'
 #
-# and is then checked into CVS.
+# and is then checked into git.
 FILES=files.txt
 
 LANGS=bg_BG en_US es_ES fr_FR it_IT ja_JP ko_KR ru_RU zh_CN zh_TW $(EXTRA_LANGS)
diff --git a/locale/files.txt b/locale/files.txt
index 978a76b..b221d4e 100644
--- a/locale/files.txt
+++ b/locale/files.txt
@@ -1,7 +1,24 @@
+../contrib/dist/coverity/modeling.c
+../contrib/mod_auth_otp/auth-otp.c
+../contrib/mod_auth_otp/base32.c
+../contrib/mod_auth_otp/base32.h
+../contrib/mod_auth_otp/crypto.c
+../contrib/mod_auth_otp/crypto.h
+../contrib/mod_auth_otp/db.c
+../contrib/mod_auth_otp/db.h
+../contrib/mod_auth_otp/mod_auth_otp.c
+../contrib/mod_auth_otp/otp.c
+../contrib/mod_auth_otp/otp.h
+../contrib/mod_auth_otp/t/api/base32.c
+../contrib/mod_auth_otp/t/api/otp.c
+../contrib/mod_auth_otp/t/api/stubs.c
+../contrib/mod_auth_otp/t/api/tests.c
+../contrib/mod_auth_otp/t/api/tests.h
 ../contrib/mod_ban.c
 ../contrib/mod_copy.c
 ../contrib/mod_ctrls_admin.c
 ../contrib/mod_deflate.c
+../contrib/mod_digest.c
 ../contrib/mod_dnsbl/mod_dnsbl.c
 ../contrib/mod_dynmasq.c
 ../contrib/mod_exec.c
@@ -14,8 +31,8 @@
 ../contrib/mod_log_forensic.c
 ../contrib/mod_qos.c
 ../contrib/mod_quotatab.c
-../contrib/mod_quotatab_file.c
 ../contrib/mod_quotatab.h
+../contrib/mod_quotatab_file.c
 ../contrib/mod_quotatab_ldap.c
 ../contrib/mod_quotatab_radius.c
 ../contrib/mod_quotatab_sql.c
@@ -25,12 +42,12 @@
 ../contrib/mod_rewrite.c
 ../contrib/mod_sftp/agent.c
 ../contrib/mod_sftp/agent.h
-../contrib/mod_sftp/auth.c
-../contrib/mod_sftp/auth.h
 ../contrib/mod_sftp/auth-hostbased.c
 ../contrib/mod_sftp/auth-kbdint.c
 ../contrib/mod_sftp/auth-password.c
 ../contrib/mod_sftp/auth-publickey.c
+../contrib/mod_sftp/auth.c
+../contrib/mod_sftp/auth.h
 ../contrib/mod_sftp/blacklist.c
 ../contrib/mod_sftp/blacklist.h
 ../contrib/mod_sftp/channel.c
@@ -68,7 +85,6 @@
 ../contrib/mod_sftp/msg.h
 ../contrib/mod_sftp/packet.c
 ../contrib/mod_sftp/packet.h
-../contrib/mod_sftp_pam.c
 ../contrib/mod_sftp/rfc4716.c
 ../contrib/mod_sftp/rfc4716.h
 ../contrib/mod_sftp/scp.c
@@ -77,7 +93,6 @@
 ../contrib/mod_sftp/service.h
 ../contrib/mod_sftp/session.c
 ../contrib/mod_sftp/session.h
-../contrib/mod_sftp_sql.c
 ../contrib/mod_sftp/ssh2.h
 ../contrib/mod_sftp/tap.c
 ../contrib/mod_sftp/tap.h
@@ -85,6 +100,8 @@
 ../contrib/mod_sftp/umac.h
 ../contrib/mod_sftp/utf8.c
 ../contrib/mod_sftp/utf8.h
+../contrib/mod_sftp_pam.c
+../contrib/mod_sftp_sql.c
 ../contrib/mod_shaper.c
 ../contrib/mod_site_misc.c
 ../contrib/mod_snmp/agentx.h
@@ -105,8 +122,6 @@
 ../contrib/mod_snmp/pdu.h
 ../contrib/mod_snmp/smi.c
 ../contrib/mod_snmp/smi.h
-../contrib/mod_snmp/stacktrace.c
-../contrib/mod_snmp/stacktrace.h
 ../contrib/mod_snmp/uptime.c
 ../contrib/mod_snmp/uptime.h
 ../contrib/mod_sql.c
@@ -116,23 +131,30 @@
 ../contrib/mod_sql_passwd.c
 ../contrib/mod_sql_postgres.c
 ../contrib/mod_sql_sqlite.c
+../contrib/mod_statcache.c
 ../contrib/mod_tls.c
 ../contrib/mod_tls.h
+../contrib/mod_tls_fscache.c
 ../contrib/mod_tls_memcache.c
+../contrib/mod_tls_redis.c
 ../contrib/mod_tls_shmcache.c
 ../contrib/mod_unique_id.c
-../contrib/mod_wrap2_file.c
+../contrib/mod_wrap.c
 ../contrib/mod_wrap2/mod_wrap2.c
+../contrib/mod_wrap2_file.c
+../contrib/mod_wrap2_redis.c
 ../contrib/mod_wrap2_sql.c
-../contrib/mod_wrap.c
 ../include/ascii.h
 ../include/auth.h
 ../include/bindings.h
+../include/buildstamp.h
+../include/ccan-json.h
 ../include/child.h
 ../include/class.h
 ../include/cmd.h
 ../include/compat.h
 ../include/conf.h
+../include/configdb.h
 ../include/ctrls.h
 ../include/data.h
 ../include/default_paths.h
@@ -147,14 +169,18 @@
 ../include/fsio.h
 ../include/ftp.h
 ../include/glibc-glob.h
+../include/hanson-tpl.h
 ../include/help.h
+../include/ident.h
 ../include/inet.h
+../include/json.h
+../include/lastlog.h
 ../include/libsupp.h
 ../include/log.h
+../include/logfmt.h
 ../include/memcache.h
 ../include/mkhome.h
 ../include/mod_ctrls.h
-../include/mod_log.h
 ../include/modules.h
 ../include/netacl.h
 ../include/netaddr.h
@@ -163,24 +189,26 @@
 ../include/parser.h
 ../include/pidfile.h
 ../include/pool.h
+../include/pr-syslog.h
 ../include/privs.h
 ../include/proctitle.h
 ../include/proftpd.h
-../include/pr-syslog.h
+../include/redis.h
 ../include/regexp.h
 ../include/response.h
 ../include/rlimit.h
 ../include/scoreboard.h
 ../include/session.h
 ../include/sets.h
+../include/signals.h
 ../include/stash.h
 ../include/str.h
 ../include/support.h
 ../include/table.h
 ../include/throttle.h
 ../include/timers.h
-../include/tpl.h
 ../include/trace.h
+../include/utf8.h
 ../include/var.h
 ../include/version.h
 ../include/xferlog.h
@@ -201,14 +229,18 @@
 ../modules/mod_log.c
 ../modules/mod_ls.c
 ../modules/mod_memcache.c
+../modules/mod_redis.c
 ../modules/mod_rlimit.c
 ../modules/mod_site.c
 ../modules/mod_xfer.c
+../modules/module_glue.c
+../src/ascii.c
 ../src/auth.c
 ../src/bindings.c
 ../src/child.c
 ../src/class.c
 ../src/cmd.c
+../src/configdb.c
 ../src/ctrls.c
 ../src/data.c
 ../src/dirtree.c
@@ -222,7 +254,9 @@
 ../src/fsio.c
 ../src/ftpdctl.c
 ../src/help.c
+../src/ident.c
 ../src/inet.c
+../src/json.c
 ../src/lastlog.c
 ../src/log.c
 ../src/main.c
@@ -237,12 +271,14 @@
 ../src/pool.c
 ../src/privs.c
 ../src/proctitle.c
+../src/redis.c
 ../src/regexp.c
 ../src/response.c
 ../src/rlimit.c
 ../src/scoreboard.c
 ../src/session.c
 ../src/sets.c
+../src/signals.c
 ../src/stash.c
 ../src/str.c
 ../src/support.c
@@ -250,6 +286,7 @@
 ../src/throttle.c
 ../src/timers.c
 ../src/trace.c
+../src/utf8.c
 ../src/var.c
 ../src/version.c
 ../src/wtmp.c
diff --git a/modules/Makefile.in b/modules/Makefile.in
index 11e8971..3c26e78 100644
--- a/modules/Makefile.in
+++ b/modules/Makefile.in
@@ -71,3 +71,6 @@ depend:
 	$(RM) module_glue.c
 	$(MAKEDEPEND) $(CPPFLAGS) *.c 2>/dev/null
 	$(MAKEDEPEND) $(CPPFLAGS) -fMakefile.in *.c 2>/dev/null
+
+distclean: clean
+	-$(RM) *.gcda *.gcno
diff --git a/modules/mod_auth.c b/modules/mod_auth.c
index 4102159..d93c630 100644
--- a/modules/mod_auth.c
+++ b/modules/mod_auth.c
@@ -29,6 +29,14 @@
 #include "conf.h"
 #include "privs.h"
 
+#ifdef HAVE_USERSEC_H
+# include <usersec.h>
+#endif
+
+#ifdef HAVE_SYS_AUDIT_H
+# include <sys/audit.h>
+#endif
+
 extern pid_t mpid;
 
 module auth_module;
@@ -40,14 +48,21 @@ static unsigned char lastlog = FALSE;
 static unsigned char mkhome = FALSE;
 static unsigned char authenticated_without_pass = FALSE;
 static int TimeoutLogin = PR_TUNABLE_TIMEOUTLOGIN;
-static int logged_in = 0;
-static int auth_tries = 0;
+static int logged_in = FALSE;
+static int auth_anon_allow_robots = FALSE;
+static int auth_anon_allow_robots_enabled = FALSE;
+static int auth_client_connected = FALSE;
+static unsigned int auth_tries = 0;
 static char *auth_pass_resp_code = R_230;
 static pr_fh_t *displaylogin_fh = NULL;
 static int TimeoutSession = 0;
 
+static int saw_first_user_cmd = FALSE;
+static const char *timing_channel = "timing";
+
+static int auth_count_scoreboard(cmd_rec *, const char *);
 static int auth_scan_scoreboard(void);
-static int auth_count_scoreboard(cmd_rec *, char *);
+static int auth_sess_init(void);
 
 /* auth_cmd_chk_cb() is hooked into the main server's auth_hook function,
  * so that we can deny all commands until authentication is complete.
@@ -87,7 +102,7 @@ static int auth_login_timeout_cb(CALLBACK_FRAME) {
    * TimeoutLogin has been exceeded to the log here, in addition to the
    * scheduled session exit message.
    */
-  pr_log_pri(PR_LOG_NOTICE, "%s", "Login timeout exceeded, disconnected");
+  pr_log_pri(PR_LOG_INFO, "%s", "Login timeout exceeded, disconnected");
   pr_event_generate("core.timeout-login", NULL);
 
   pr_session_disconnect(&auth_module, PR_SESS_DISCONNECT_TIMEOUT,
@@ -103,7 +118,7 @@ static int auth_session_timeout_cb(CALLBACK_FRAME) {
     _("Session Timeout (%d seconds): closing control connection"),
     TimeoutSession);
 
-  pr_log_pri(PR_LOG_NOTICE, "%s", "FTP session timed out, disconnected");
+  pr_log_pri(PR_LOG_INFO, "%s", "FTP session timed out, disconnected");
   pr_session_disconnect(&auth_module, PR_SESS_DISCONNECT_TIMEOUT,
     "TimeoutSession");
 
@@ -115,14 +130,71 @@ static int auth_session_timeout_cb(CALLBACK_FRAME) {
  */
 
 static void auth_exit_ev(const void *event_data, void *user_data) {
+  pr_auth_cache_clear();
+
   /* Close the scoreboard descriptor that we opened. */
   (void) pr_close_scoreboard(FALSE);
 }
 
+static void auth_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&auth_module, "core.exit", auth_exit_ev);
+  pr_event_unregister(&auth_module, "core.session-reinit", auth_sess_reinit_ev);
+
+  pr_timer_remove(PR_TIMER_LOGIN, &auth_module);
+
+  /* Reset the CreateHome setting. */
+  mkhome = FALSE;
+
+  /* Reset any MaxPasswordSize setting. */
+  (void) pr_auth_set_max_password_len(session.pool, 0);
+
+#if defined(PR_USE_LASTLOG)
+  lastlog = FALSE;
+#endif /* PR_USE_LASTLOG */
+  mkhome = FALSE;
+
+  res = auth_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&auth_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
+/* Initialization functions
+ */
+
+static int auth_init(void) {
+  /* Add the commands handled by this module to the HELP list. */ 
+  pr_help_add(C_USER, _("<sp> username"), TRUE);
+  pr_help_add(C_PASS, _("<sp> password"), TRUE);
+  pr_help_add(C_ACCT, _("is not implemented"), FALSE);
+  pr_help_add(C_REIN, _("is not implemented"), FALSE);
+
+  /* By default, enable auth checking */
+  set_auth_check(auth_cmd_chk_cb);
+
+  return 0;
+}
+
 static int auth_sess_init(void) {
   config_rec *c = NULL;
   unsigned char *tmp = NULL;
-  int res = 0;
+
+  pr_event_register(&auth_module, "core.session-reinit", auth_sess_reinit_ev,
+    NULL);
+
+  /* Check for any MaxPasswordSize. */
+  c = find_config(main_server->conf, CONF_PARAM, "MaxPasswordSize", FALSE);
+  if (c != NULL) {
+    size_t len;
+
+    len = *((size_t *) c->argv[0]);
+    (void) pr_auth_set_max_password_len(session.pool, len);
+  }
 
   /* Check for a server-specific TimeoutLogin */
   c = find_config(main_server->conf, CONF_PARAM, "TimeoutLogin", FALSE);
@@ -137,52 +209,60 @@ static int auth_sess_init(void) {
       auth_login_timeout_cb, "TimeoutLogin");
   }
 
-  PRIVS_ROOT
-  res = pr_open_scoreboard(O_RDWR);
-  PRIVS_RELINQUISH
+  if (auth_client_connected == FALSE) {
+    int res = 0;
 
-  if (res < 0) {
-    switch (res) {
-      case PR_SCORE_ERR_BAD_MAGIC:
-        pr_log_debug(DEBUG0, "error opening scoreboard: bad/corrupted file");
-        break;
+    PRIVS_ROOT
+    res = pr_open_scoreboard(O_RDWR);
+    PRIVS_RELINQUISH
 
-      case PR_SCORE_ERR_OLDER_VERSION:
-        pr_log_debug(DEBUG0, "error opening scoreboard: bad version (too old)");
-        break;
+    if (res < 0) {
+      switch (res) {
+        case PR_SCORE_ERR_BAD_MAGIC:
+          pr_log_debug(DEBUG0, "error opening scoreboard: bad/corrupted file");
+          break;
 
-      case PR_SCORE_ERR_NEWER_VERSION:
-        pr_log_debug(DEBUG0, "error opening scoreboard: bad version (too new)");
-        break;
+        case PR_SCORE_ERR_OLDER_VERSION:
+          pr_log_debug(DEBUG0,
+            "error opening scoreboard: bad version (too old)");
+          break;
 
-      default:
-        pr_log_debug(DEBUG0, "error opening scoreboard: %s", strerror(errno));
-        break;
+        case PR_SCORE_ERR_NEWER_VERSION:
+          pr_log_debug(DEBUG0,
+            "error opening scoreboard: bad version (too new)");
+          break;
+
+        default:
+          pr_log_debug(DEBUG0, "error opening scoreboard: %s", strerror(errno));
+          break;
+      }
     }
   }
 
   pr_event_register(&auth_module, "core.exit", auth_exit_ev, NULL);
 
-  /* Create an entry in the scoreboard for this session, if we don't already
-   * have one.
-   */
-  if (pr_scoreboard_entry_get(PR_SCORE_CLIENT_ADDR) == NULL) {
-    if (pr_scoreboard_entry_add() < 0) {
-      pr_log_pri(PR_LOG_NOTICE, "notice: unable to add scoreboard entry: %s",
-        strerror(errno));
-    }
+  if (auth_client_connected == FALSE) {
+    /* Create an entry in the scoreboard for this session, if we don't already
+     * have one.
+     */
+    if (pr_scoreboard_entry_get(PR_SCORE_CLIENT_ADDR) == NULL) {
+      if (pr_scoreboard_entry_add() < 0) {
+        pr_log_pri(PR_LOG_NOTICE, "notice: unable to add scoreboard entry: %s",
+          strerror(errno));
+      }
 
-    pr_scoreboard_entry_update(session.pid,
-      PR_SCORE_USER, "(none)",
-      PR_SCORE_SERVER_PORT, main_server->ServerPort,
-      PR_SCORE_SERVER_ADDR, session.c->local_addr, session.c->local_port,
-      PR_SCORE_SERVER_LABEL, main_server->ServerName,
-      PR_SCORE_CLIENT_ADDR, session.c->remote_addr,
-      PR_SCORE_CLIENT_NAME, session.c->remote_name,
-      PR_SCORE_CLASS, session.conn_class ? session.conn_class->cls_name : "",
-      PR_SCORE_PROTOCOL, "ftp",
-      PR_SCORE_BEGIN_SESSION, time(NULL),
-      NULL);
+      pr_scoreboard_entry_update(session.pid,
+        PR_SCORE_USER, "(none)",
+        PR_SCORE_SERVER_PORT, main_server->ServerPort,
+        PR_SCORE_SERVER_ADDR, session.c->local_addr, session.c->local_port,
+        PR_SCORE_SERVER_LABEL, main_server->ServerName,
+        PR_SCORE_CLIENT_ADDR, session.c->remote_addr,
+        PR_SCORE_CLIENT_NAME, session.c->remote_name,
+        PR_SCORE_CLASS, session.conn_class ? session.conn_class->cls_name : "",
+        PR_SCORE_PROTOCOL, "ftp",
+        PR_SCORE_BEGIN_SESSION, time(NULL),
+        NULL);
+    }
 
   } else {
     /* We're probably handling a HOST comand, and the server changed; just
@@ -221,31 +301,19 @@ static int auth_sess_init(void) {
    */
   auth_scan_scoreboard();
 
+  auth_client_connected = TRUE;
   return 0;
 }
 
-static int auth_init(void) {
-
-  /* Add the commands handled by this module to the HELP list. */ 
-  pr_help_add(C_USER, _("<sp> username"), TRUE);
-  pr_help_add(C_PASS, _("<sp> password"), TRUE);
-  pr_help_add(C_ACCT, _("is not implemented"), FALSE);
-  pr_help_add(C_REIN, _("is not implemented"), FALSE);
-
-  /* By default, enable auth checking */
-  set_auth_check(auth_cmd_chk_cb);
-
-  return 0;
-}
-
-static int _do_auth(pool *p, xaset_t *conf, char *u, char *pw) {
+static int do_auth(pool *p, xaset_t *conf, const char *u, char *pw) {
   char *cpw = NULL;
   config_rec *c;
 
-  if (conf) {
+  if (conf != NULL) {
     c = find_config(conf, CONF_PARAM, "UserPassword", FALSE);
+    while (c != NULL) {
+      pr_signals_handle();
 
-    while (c) {
       if (strcmp(c->argv[0], u) == 0) {
         cpw = (char *) c->argv[1];
         break;
@@ -255,7 +323,7 @@ static int _do_auth(pool *p, xaset_t *conf, char *u, char *pw) {
     }
   }
 
-  if (cpw) {
+  if (cpw != NULL) {
     if (pr_auth_getpwnam(p, u) == NULL) {
       int xerrno = errno;
 
@@ -276,38 +344,35 @@ static int _do_auth(pool *p, xaset_t *conf, char *u, char *pw) {
 /* Command handlers
  */
 
-MODRET auth_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-
-    /* Remove the TimeoutLogin timer. */
-    pr_timer_remove(PR_TIMER_LOGIN, &auth_module);
-
-    pr_event_unregister(&auth_module, "core.exit", auth_exit_ev);
+static void login_failed(pool *p, const char *user) {
+#ifdef HAVE_LOGINFAILED
+  const char *host, *sess_ttyname;
+  int res, xerrno;
 
-    /* Reset the CreateHome setting. */
-    mkhome = FALSE;
+  host = pr_netaddr_get_dnsstr(session.c->remote_addr);
+  sess_ttyname = pr_session_get_ttyname(p);
 
-#ifdef PR_USE_LASTLOG
-    /* Reset the UseLastLog setting. */
-    lastlog = FALSE;
-#endif /* PR_USE_LASTLOG */
+  PRIVS_ROOT
+  res = loginfailed((char *) user, (char *) host, (char *) sess_ttyname,
+    AUDIT_FAIL);
+  xerrno = errno;
+  PRIVS_RELINQUISH
 
-    res = auth_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&auth_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
+  if (res < 0) {
+    pr_trace_msg("auth", 3, "AIX loginfailed() error for user '%s', "
+      "host '%s', tty '%s', reason %d: %s", user, host, sess_ttyname,
+      AUDIT_FAIL, strerror(errno));
   }
-
-  return PR_DECLINED(cmd);
+#endif /* HAVE_LOGINFAILED */
 }
 
 MODRET auth_err_pass(cmd_rec *cmd) {
+  const char *user;
+
+  user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
+  if (user != NULL) {
+    login_failed(cmd->tmp_pool, user);
+  }
 
   /* Remove the stashed original USER name here in a LOG_CMD_ERR handler, so
    * that other modules, who may want to lookup the original USER parameter on
@@ -322,7 +387,6 @@ MODRET auth_err_pass(cmd_rec *cmd) {
 }
 
 MODRET auth_log_pass(cmd_rec *cmd) {
-  size_t passwd_len;
 
   /* Only log, to the syslog, that the login has succeeded here, where we
    * know that the login has definitely succeeded.
@@ -331,6 +395,8 @@ MODRET auth_log_pass(cmd_rec *cmd) {
     (session.anon_config != NULL) ? "ANON" : C_USER, session.user);
 
   if (cmd->arg != NULL) {
+    size_t passwd_len;
+
     /* And scrub the memory holding the password sent by the client, for
      * safety/security.
      */
@@ -341,9 +407,39 @@ MODRET auth_log_pass(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
+static void login_succeeded(pool *p, const char *user) {
+#ifdef HAVE_LOGINSUCCESS
+  const char *host, *sess_ttyname;
+  char *msg = NULL;
+  int res, xerrno;
+
+  host = pr_netaddr_get_dnsstr(session.c->remote_addr);
+  sess_ttyname = pr_session_get_ttyname(p);
+
+  PRIVS_ROOT
+  res = loginsuccess((char *) user, (char *) host, (char *) sess_ttyname, &msg);
+  xerrno = errno;
+  PRIVS_RELINQUISH
+
+  if (res == 0) {
+    if (msg != NULL) {
+      pr_trace_msg("auth", 14, "AIX loginsuccess() report: %s", msg);
+    }
+
+  } else {
+    pr_trace_msg("auth", 3, "AIX loginsuccess() error for user '%s', "
+      "host '%s', tty '%s': %s", user, host, sess_ttyname, strerror(errno));
+  }
+
+  if (msg != NULL) {
+    free(msg);
+  }
+#endif /* HAVE_LOGINSUCCESS */
+}
+
 MODRET auth_post_pass(cmd_rec *cmd) {
   config_rec *c = NULL;
-  char *grantmsg = NULL, *user;
+  const char *grantmsg = NULL, *user;
   unsigned int ctxt_precedence = 0;
   unsigned char have_user_timeout, have_group_timeout, have_class_timeout,
     have_all_timeout, *root_revoke = NULL, *authenticated;
@@ -365,7 +461,7 @@ MODRET auth_post_pass(cmd_rec *cmd) {
      * user/group/class-specific sections.
      */
     c = find_config(main_server->conf, CONF_PARAM, "Protocols", FALSE);
-    if (c) {
+    if (c != NULL) {
       register unsigned int i;
       array_header *protocols;
       char **elts;
@@ -566,6 +662,8 @@ MODRET auth_post_pass(cmd_rec *cmd) {
      pr_response_add(auth_pass_resp_code, "%s", grantmsg);
   }
 
+  login_succeeded(cmd->tmp_pool, user);
+
   /* A RootRevoke value of 0 indicates 'false', 1 indicates 'true', and
    * 2 indicates 'NonCompliantActiveTransfer'.  We will drop root privs for any
    * RootRevoke value greater than 0.
@@ -596,80 +694,94 @@ MODRET auth_post_pass(cmd_rec *cmd) {
     pr_log_debug(DEBUG0, "RootRevoke in effect, dropped root privs");
   }
 
+  c = find_config(TOPLEVEL_CONF, CONF_PARAM, "AnonAllowRobots", FALSE);
+  if (c != NULL) {
+    auth_anon_allow_robots = *((int *) c->argv[0]);
+  }
+
   return PR_DECLINED(cmd);
 }
 
-/* Handle group based authentication, only checked if pw
- * based fails
- */
-
-static config_rec *_auth_group(pool *p, char *user, char **group,
-                               char **ournamep, char **anonnamep, char *pass)
-{
+/* Handle group based authentication, only checked if pw based fails. */
+static config_rec *auth_group(pool *p, const char *user, char **group,
+    char **ournamep, char **anonnamep, char *pass) {
   config_rec *c;
-  char *ourname = NULL,*anonname = NULL;
+  char *ourname = NULL, *anonname = NULL;
   char **grmem;
   struct group *grp;
 
-  ourname = (char*)get_param_ptr(main_server->conf,"UserName",FALSE);
-  if (ournamep && ourname)
+  ourname = get_param_ptr(main_server->conf, "UserName", FALSE);
+  if (ournamep != NULL &&
+      ourname != NULL) {
     *ournamep = ourname;
+  }
 
   c = find_config(main_server->conf, CONF_PARAM, "GroupPassword", TRUE);
-
   if (c) do {
     grp = pr_auth_getgrnam(p, c->argv[0]);
-
-    if (!grp)
+    if (grp == NULL) {
       continue;
+    }
 
-    for (grmem = grp->gr_mem; *grmem; grmem++)
+    for (grmem = grp->gr_mem; *grmem; grmem++) {
       if (strcmp(*grmem, user) == 0) {
-        if (pr_auth_check(p, c->argv[1], user, pass) == 0)
+        if (pr_auth_check(p, c->argv[1], user, pass) == 0) {
           break;
+        }
       }
+    }
 
     if (*grmem) {
-      if (group)
+      if (group != NULL) {
         *group = c->argv[0];
+      }
 
-      if (c->parent)
+      if (c->parent != NULL) {
         c = c->parent;
+      }
+
+      if (c->config_type == CONF_ANON) {
+        anonname = get_param_ptr(c->subset, "UserName", FALSE);
+      }
 
-      if (c->config_type == CONF_ANON)
-        anonname = (char*)get_param_ptr(c->subset,"UserName",FALSE);
-      if (anonnamep)
+      if (anonnamep != NULL) {
         *anonnamep = anonname;
-      if (anonnamep && !anonname && ourname)
+      }
+
+      if (anonnamep != NULL &&
+          !anonname &&
+          ourname != NULL) {
         *anonnamep = ourname;
+      }
 
       break;
     }
-  } while((c = find_config_next(c,c->next,CONF_PARAM,"GroupPassword",TRUE)) != NULL);
+
+  } while((c = find_config_next(c, c->next, CONF_PARAM, "GroupPassword",
+     TRUE)) != NULL);
 
   return c;
 }
 
-/* Determine any applicable chdirs
- */
-
-static char *get_default_chdir(pool *p, xaset_t *conf) {
+/* Determine any applicable chdirs. */
+static const char *get_default_chdir(pool *p, xaset_t *conf) {
   config_rec *c;
-  char *dir = NULL;
-  int ret;
+  const char *dir = NULL;
 
   c = find_config(conf, CONF_PARAM, "DefaultChdir", FALSE);
+  while (c != NULL) {
+    int res;
+
+    pr_signals_handle();
 
-  while (c) {
     /* Check the groups acl */
     if (c->argc < 2) {
       dir = c->argv[0];
       break;
     }
 
-    ret = pr_expr_eval_group_and(((char **) c->argv)+1);
-
-    if (ret) {
+    res = pr_expr_eval_group_and(((char **) c->argv)+1);
+    if (res) {
       dir = c->argv[0];
       break;
     }
@@ -678,12 +790,16 @@ static char *get_default_chdir(pool *p, xaset_t *conf) {
   }
 
   /* If the directory is relative, concatenate w/ session.cwd. */
-  if (dir && *dir != '/' && *dir != '~')
+  if (dir != NULL &&
+      *dir != '/' &&
+      *dir != '~') {
     dir = pdircat(p, session.cwd, dir, NULL);
+  }
 
   /* Check for any expandable variables. */
-  if (dir)
+  if (dir != NULL) {
     dir = path_subst_uservar(p, &dir);
+  }
 
   return dir;
 }
@@ -697,7 +813,7 @@ static int is_symlink_path(pool *p, const char *path, size_t pathlen) {
     return 0;
   }
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   res = pr_fsio_lstat(path, &st);
   if (res < 0) {
     xerrno = errno;
@@ -748,13 +864,13 @@ static int is_symlink_path(pool *p, const char *path, size_t pathlen) {
 }
 
 /* Determine if the user (non-anon) needs a default root dir other than /. */
-static int get_default_root(pool *p, int allow_symlinks, char **root) {
+static int get_default_root(pool *p, int allow_symlinks, const char **root) {
   config_rec *c = NULL;
-  char *dir = NULL;
+  const char *dir = NULL;
   int res;
 
   c = find_config(main_server->conf, CONF_PARAM, "DefaultRoot", FALSE);
-  while (c) {
+  while (c != NULL) {
     pr_signals_handle();
 
     /* Check the groups acl */
@@ -772,8 +888,8 @@ static int get_default_root(pool *p, int allow_symlinks, char **root) {
     c = find_config_next(c, c->next, CONF_PARAM, "DefaultRoot", FALSE);
   }
 
-  if (dir) {
-    char *new_dir;
+  if (dir != NULL) {
+    const char *new_dir;
 
     /* Check for any expandable variables. */
     new_dir = path_subst_uservar(p, &dir);
@@ -797,7 +913,7 @@ static int get_default_root(pool *p, int allow_symlinks, char **root) {
          * symlinks, which is what we do NOT want to do here.
          */
 
-        path = dir;
+        path = pstrdup(p, dir);
         if (*path != '/') {
           if (*path == '~') {
             if (pr_fs_interpolate(dir, target_path,
@@ -838,6 +954,8 @@ static int get_default_root(pool *p, int allow_symlinks, char **root) {
        * root.
        */
 
+      pr_fs_clear_cache2(dir);
+
       PRIVS_USER
       realdir = dir_realpath(p, dir);
       xerrno = errno;
@@ -883,27 +1001,34 @@ static struct passwd *passwd_dup(pool *p, struct passwd *pw) {
 }
 
 static void ensure_open_passwd(pool *p) {
-  /* Make sure pass/group is open.
-   */
+  /* Make sure pass/group is open. */
   pr_auth_setpwent(p);
   pr_auth_setgrent(p);
 
   /* On some unices the following is necessary to ensure the files
-   * are open.  (BSDI 3.1)
+   * are open (BSDI 3.1)
    */
   pr_auth_getpwent(p);
   pr_auth_getgrent(p);
+
+  /* Per Debian bug report:
+   *   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=717235
+   * we might want to do another set{pw,gr}ent(), to play better with
+   * some NSS modules.
+   */
+  pr_auth_setpwent(p);
+  pr_auth_setgrent(p);
 }
 
 /* Next function (the biggie) handles all authentication, setting
  * up chroot() jail, etc.
  */
-static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
+static int setup_env(pool *p, cmd_rec *cmd, const char *user, char *pass) {
   struct passwd *pw;
   config_rec *c, *tmpc;
-  char *origuser, *ourname = NULL, *anonname = NULL, *anongroup = NULL, *ugroup = NULL;
-  char *defaulttransfermode, *defroot = NULL,*defchdir = NULL,*xferlog = NULL;
-  const char *sess_ttyname;
+  const char *defchdir = NULL, *defroot = NULL, *origuser, *sess_ttyname;
+  char *ourname = NULL, *anonname = NULL, *anongroup = NULL, *ugroup = NULL;
+  char *xferlog = NULL;
   int aclp, i, res = 0, allow_chroot_symlinks = TRUE, showsymlinks;
   unsigned char *wtmp_log = NULL, *anon_require_passwd = NULL;
 
@@ -913,11 +1038,11 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
 
   origuser = user;
   c = pr_auth_get_anon_config(p, &user, &ourname, &anonname);
-
-  if (c)
+  if (c != NULL) {
     session.anon_config = c;
+  }
 
-  if (!user) {
+  if (user == NULL) {
     pr_log_auth(PR_LOG_NOTICE, "USER %s: user is not a UserAlias from %s [%s] "
       "to %s:%i", origuser, session.c->remote_name,
       pr_netaddr_get_ipstr(session.c->remote_addr),
@@ -983,7 +1108,7 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
   session.login_gid = pw->pw_gid;
 
   /* Check for any expandable variables in session.cwd. */
-  pw->pw_dir = path_subst_uservar(p, &pw->pw_dir);
+  pw->pw_dir = (char *) path_subst_uservar(p, (const char **) &pw->pw_dir);
 
   /* Before we check for supplemental groups, check to see if the locally
    * resolved name of the user, returned via auth_getpwnam(), is different
@@ -994,7 +1119,7 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
    * and session.groups lists are stale, and clear them out.
    */
   if (strcmp(pw->pw_name, user) != 0) {
-    pr_log_debug(DEBUG10, "local user name '%s' differs from client-sent "
+    pr_trace_msg("auth", 10, "local user name '%s' differs from client-sent "
       "user name '%s', clearing cached group data", pw->pw_name, user);
     session.gids = NULL;
     session.groups = NULL;
@@ -1011,7 +1136,7 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
      */
      res = pr_auth_getgroups(p, pw->pw_name, &session.gids, &session.groups);
      if (res < 1) {
-       pr_log_debug(DEBUG2, "no supplemental groups found for user '%s'",
+       pr_log_debug(DEBUG5, "no supplemental groups found for user '%s'",
          pw->pw_name);
      }
   }
@@ -1025,30 +1150,40 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
   /* If c != NULL from this point on, we have an anonymous login */
   aclp = login_check_limits(main_server->conf, FALSE, TRUE, &i);
 
-  if (c) {
+  if (c != NULL) {
     anongroup = get_param_ptr(c->subset, "GroupName", FALSE);
-    if (!anongroup)
+    if (anongroup == NULL) {
       anongroup = get_param_ptr(main_server->conf, "GroupName",FALSE);
+    }
 
+#ifdef PR_USE_REGEX
     /* Check for configured AnonRejectPasswords regex here, and fail the login
      * if the given password matches the regex.
      */
-#ifdef PR_USE_REGEX
-    if ((tmpc = find_config(c->subset, CONF_PARAM, "AnonRejectPasswords",
-        FALSE)) != NULL) {
-      int re_res;
-      pr_regex_t *pw_regex = (pr_regex_t *) tmpc->argv[0];
-
-      if (pw_regex && pass &&
-          ((re_res = pr_regexp_exec(pw_regex, pass, 0, NULL, 0, 0, 0)) == 0)) {
-        char errstr[200] = {'\0'};
-
-        pr_regexp_error(re_res, pw_regex, errstr, sizeof(errstr));
-        pr_log_auth(PR_LOG_NOTICE, "ANON %s: AnonRejectPasswords denies login",
-          origuser);
+    tmpc = find_config(c->subset, CONF_PARAM, "AnonRejectPasswords", FALSE);
+    if (tmpc != NULL) {
+      int re_notmatch;
+      pr_regex_t *pw_regex;
+
+      pw_regex = (pr_regex_t *) tmpc->argv[0];
+      re_notmatch = *((int *) tmpc->argv[1]);
+
+      if (pw_regex != NULL &&
+          pass != NULL) {
+        int re_res;
+
+        re_res = pr_regexp_exec(pw_regex, pass, 0, NULL, 0, 0, 0);
+        if (re_res == 0 ||
+            (re_res != 0 && re_notmatch == TRUE)) {
+          char errstr[200] = {'\0'};
+
+          pr_regexp_error(re_res, pw_regex, errstr, sizeof(errstr));
+          pr_log_auth(PR_LOG_NOTICE,
+            "ANON %s: AnonRejectPasswords denies login", origuser);
  
-        pr_event_generate("mod_auth.anon-reject-passwords", session.c);
-        goto auth_failure;
+          pr_event_generate("mod_auth.anon-reject-passwords", session.c);
+          goto auth_failure;
+        }
       }
     }
 #endif
@@ -1067,21 +1202,23 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
     goto auth_failure;
   }
 
-  if (c) {
+  if (c != NULL) {
     anon_require_passwd = get_param_ptr(c->subset, "AnonRequirePassword",
       FALSE);
   }
 
-  if (!c ||
-      (anon_require_passwd && *anon_require_passwd == TRUE)) {
+  if (c == NULL ||
+      (anon_require_passwd != NULL &&
+       *anon_require_passwd == TRUE)) {
     int auth_code;
-    char *user_name = user;
+    const char *user_name = user;
 
-    if (c &&
-        origuser &&
+    if (c != NULL &&
+        origuser != NULL &&
         strcasecmp(user, origuser) != 0) {
-      unsigned char *auth_using_alias = get_param_ptr(c->subset,
-        "AuthUsingAlias", FALSE);
+      unsigned char *auth_using_alias;
+
+      auth_using_alias = get_param_ptr(c->subset, "AuthUsingAlias", FALSE);
 
       /* If 'AuthUsingAlias' set and we're logging in under an alias,
        * then auth using that alias.
@@ -1097,10 +1234,10 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
 
     /* It is possible for the user to have already been authenticated during
      * the handling of the USER command, as by an RFC2228 mechanism.  If
-     * that had happened, we won't need to call _do_auth() here.
+     * that had happened, we won't need to call do_auth() here.
      */
     if (!authenticated_without_pass) {
-      auth_code = _do_auth(p, c ? c->subset : main_server->conf, user_name,
+      auth_code = do_auth(p, c ? c->subset : main_server->conf, user_name,
         pass);
 
     } else {
@@ -1114,7 +1251,7 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
        * passes
        */
 
-      c = _auth_group(p, user, &anongroup, &ourname, &anonname, pass);
+      c = auth_group(p, user, &anongroup, &ourname, &anonname, pass);
       if (c != NULL) {
         if (c->config_type != CONF_ANON) {
           c = NULL;
@@ -1162,6 +1299,43 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
           user);
         goto auth_failure;
 
+      case PR_AUTH_CRED_INSUFFICIENT:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): Insufficient credentials", user);
+        goto auth_failure;
+
+      case PR_AUTH_CRED_UNAVAIL:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): Unavailable credentials", user);
+        goto auth_failure;
+
+      case PR_AUTH_CRED_ERROR:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): Failure setting credentials", user);
+        goto auth_failure;
+
+      case PR_AUTH_INFO_UNAVAIL:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): Unavailable authentication service", user);
+        goto auth_failure;
+
+      case PR_AUTH_MAX_ATTEMPTS_EXCEEDED:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): Max authentication service attempts reached",
+          user);
+        goto auth_failure;
+
+      case PR_AUTH_INIT_ERROR:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): Failed initializing authentication service",
+          user);
+        goto auth_failure;
+
+      case PR_AUTH_NEW_TOKEN_REQUIRED:
+        pr_log_auth(PR_LOG_NOTICE,
+          "USER %s (Login failed): New authentication token required", user);
+        goto auth_failure;
+
       default:
         break;
     };
@@ -1199,7 +1373,8 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
   if (c) {
     struct group *grp = NULL;
     unsigned char *add_userdir = NULL;
-    char *u, *chroot_dir;
+    const char *u;
+    char *chroot_dir;
 
     u = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
     add_userdir = get_param_ptr(c->subset, "UserDirRoot", FALSE);
@@ -1215,8 +1390,10 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
     PRIVS_ROOT
     res = set_groups(p, pw->pw_gid, session.gids);
     if (res < 0) {
-      pr_log_pri(PR_LOG_WARNING, "error: unable to set groups: %s",
-        strerror(errno));
+      if (errno != ENOSYS) {
+        pr_log_pri(PR_LOG_WARNING, "error: unable to set groups: %s",
+          strerror(errno));
+      }
     }
 
 #ifndef PR_DEVEL_COREDUMP
@@ -1280,7 +1457,7 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
           chroot_path[chroot_pathlen-1] = '\0';
         }
 
-        pr_fs_clear_cache();
+        pr_fs_clear_cache2(chroot_path);
         res = pr_fsio_lstat(chroot_path, &st);
         if (res < 0) {
           int xerrno = errno;
@@ -1335,8 +1512,10 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
     PRIVS_ROOT
     res = set_groups(p, daemon_gid, daemon_gids);
     if (res < 0) {
-      pr_log_pri(PR_LOG_ERR, "error: unable to set groups: %s",
-        strerror(errno));
+      if (errno != ENOSYS) {
+        pr_log_pri(PR_LOG_ERR, "error: unable to set groups: %s",
+          strerror(errno));
+      }
     }
 
 #ifndef PR_DEVEL_COREDUMP
@@ -1439,9 +1618,9 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
 
   /* Get default chdir (if any) */
   defchdir = get_default_chdir(p, (c ? c->subset : main_server->conf));
-
-  if (defchdir)
+  if (defchdir != NULL) {
     sstrncpy(session.cwd, defchdir, sizeof(session.cwd));
+  }
 
   /* Check limits again to make sure deny/allow directives still permit
    * access.
@@ -1543,8 +1722,10 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
 
   res = set_groups(p, pw->pw_gid, session.gids);
   if (res < 0) {
-    pr_log_pri(PR_LOG_ERR, "error: unable to set groups: %s",
-      strerror(errno));
+    if (errno != ENOSYS) {
+      pr_log_pri(PR_LOG_ERR, "error: unable to set groups: %s",
+        strerror(errno));
+    }
   }
 
   PRIVS_RELINQUISH
@@ -1734,18 +1915,6 @@ static int setup_env(pool *p, cmd_rec *cmd, char *user, char *pass) {
    */
   /* pr_auth_endpwent(p); */
 
-  /* Default transfer mode is ASCII */
-  defaulttransfermode = (char *) get_param_ptr(main_server->conf,
-    "DefaultTransferMode", FALSE);
-
-  if (defaulttransfermode &&
-      strcasecmp(defaulttransfermode, "binary") == 0) {
-    session.sf_flags &= (SF_ALL^SF_ASCII);
-
-  } else {
-    session.sf_flags |= SF_ASCII;
-  }
-
   /* Authentication complete, user logged in, now kill the login
    * timer.
    */
@@ -1927,7 +2096,7 @@ static int have_client_limits(cmd_rec *cmd) {
   return FALSE;
 }
 
-static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
+static int auth_count_scoreboard(cmd_rec *cmd, const char *user) {
   char *key;
   void *v;
   pr_scoreboard_entry_t *score = NULL;
@@ -1945,10 +2114,10 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
   /* Determine how many users are currently connected. */
 
   /* We use this call to get the possibly-changed user name. */
-  (void) pr_auth_get_anon_config(cmd->tmp_pool, &user, NULL, NULL);
+  c = pr_auth_get_anon_config(cmd->tmp_pool, &user, NULL, NULL);
 
   /* Gather our statistics. */
-  if (user) {
+  if (user != NULL) {
     char curr_server_addr[80] = {'\0'};
 
     snprintf(curr_server_addr, sizeof(curr_server_addr), "%s:%d",
@@ -1968,19 +2137,23 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
       /* Make sure it matches our current server. */
       if (strcmp(score->sce_server_addr, curr_server_addr) == 0) {
 
-        if ((c && c->config_type == CONF_ANON &&
-            !strcmp(score->sce_user, user)) || !c) {
+        if ((c != NULL && c->config_type == CONF_ANON &&
+            !strcmp(score->sce_user, user)) || c == NULL) {
 
           /* This small hack makes sure that cur is incremented properly
            * when dealing with anonymous logins (the timing of anonymous
            * login updates to the scoreboard makes this...odd).
            */
-          if (c && c->config_type == CONF_ANON && cur == 0)
+          if (c != NULL &&
+              c->config_type == CONF_ANON &&
+              cur == 0) {
               cur = 1;
+          }
 
           /* Only count authenticated clients, as per the documentation. */
-          if (strncmp(score->sce_user, "(none)", 7) == 0)
+          if (strncmp(score->sce_user, "(none)", 7) == 0) {
             continue;
+          }
 
           cur++;
 
@@ -1994,8 +2167,11 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
              * when dealing with anonymous logins (the timing of anonymous
              * login updates to the scoreboard makes this...odd).
              */
-            if (c && c->config_type == CONF_ANON && hcur == 0)
+            if (c != NULL &&
+                c->config_type == CONF_ANON &&
+                hcur == 0) {
               hcur = 1;
+            }
 
             hcur++;
           }
@@ -2005,8 +2181,9 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
             usersessions++;
 
             /* Count up unique hosts. */
-            if (!same_host)
+            if (!same_host) {
               hostsperuser++;
+            }
           }
         }
 
@@ -2064,8 +2241,9 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
       continue;
     }
 
-    if (maxc->argc > 2)
+    if (maxc->argc > 2) {
       maxstr = maxc->argv[2];
+    }
 
     if (*max &&
         ccur > *max) {
@@ -2095,8 +2273,9 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
       "are already connected.";
     unsigned int *max = maxc->argv[0];
 
-    if (maxc->argc > 1)
+    if (maxc->argc > 1) {
       maxstr = maxc->argv[1];
+    }
 
     if (*max && hcur > *max) {
       char maxn[20] = {'\0'};
@@ -2122,8 +2301,9 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
       "are already connected.";
     unsigned int *max = maxc->argv[0];
 
-    if (maxc->argc > 1)
+    if (maxc->argc > 1) {
       maxstr = maxc->argv[1];
+    }
 
     if (*max && usersessions > *max) {
       char maxn[20] = {'\0'};
@@ -2148,8 +2328,9 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
       "already connected.";
     unsigned int *max = maxc->argv[0];
 
-    if (maxc->argc > 1)
+    if (maxc->argc > 1) {
       maxstr = maxc->argv[1];
+    }
 
     if (*max && cur > *max) {
       char maxn[20] = {'\0'};
@@ -2173,8 +2354,9 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
       "already connected.";
     unsigned int *max = maxc->argv[0];
 
-    if (maxc->argc > 1)
+    if (maxc->argc > 1) {
       maxstr = maxc->argv[1];
+    }
 
     if (*max && hostsperuser > *max) {
       char maxn[20] = {'\0'};
@@ -2186,7 +2368,7 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
         NULL));
       (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
 
-      pr_log_auth(PR_LOG_NOTICE, "Connection refused (MaxHostsPerHost %u)",
+      pr_log_auth(PR_LOG_NOTICE, "Connection refused (MaxHostsPerUser %u)",
         *max);
       pr_session_disconnect(&auth_module, PR_SESS_DISCONNECT_CONFIG_ACL,
         "Denied by MaxHostsPerUser");
@@ -2198,8 +2380,23 @@ static int auth_count_scoreboard(cmd_rec *cmd, char *user) {
 
 MODRET auth_pre_user(cmd_rec *cmd) {
 
-  if (logged_in)
+  if (saw_first_user_cmd == FALSE) {
+    if (pr_trace_get_level(timing_channel)) {
+      unsigned long elapsed_ms;
+      uint64_t finish_ms;
+
+      pr_gettimeofday_millis(&finish_ms);
+      elapsed_ms = (unsigned long) (finish_ms - session.connect_time_ms);
+
+      pr_trace_msg(timing_channel, 4, "Time before first USER: %lu ms",
+        elapsed_ms);
+    }
+    saw_first_user_cmd = TRUE;
+  }
+
+  if (logged_in) {
     return PR_DECLINED(cmd);
+  }
 
   /* Close the passwd and group databases, because libc won't let us see new
    * entries to these files without this (only in PersistentPasswd mode).
@@ -2212,6 +2409,9 @@ MODRET auth_pre_user(cmd_rec *cmd) {
     pr_log_pri(PR_LOG_NOTICE, "USER %s (Login failed): "
       "maximum USER length exceeded", cmd->arg);
     pr_response_add_err(R_501, _("Login incorrect."));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -2221,7 +2421,7 @@ MODRET auth_pre_user(cmd_rec *cmd) {
 MODRET auth_user(cmd_rec *cmd) {
   int nopass = FALSE;
   config_rec *c;
-  char *denymsg = NULL, *user, *origuser;
+  const char *denymsg = NULL, *user, *origuser;
   int failnopwprompt = 0, aclp, i;
   unsigned char *anon_require_passwd = NULL, *login_passwd_prompt = NULL;
 
@@ -2389,7 +2589,8 @@ MODRET auth_user(cmd_rec *cmd) {
     pr_cmd_dispatch(fakecmd);
 
   } else {
-    pr_response_add(R_331, _("Password required for %s"), cmd->argv[1]);
+    pr_response_add(R_331, _("Password required for %s"),
+      (char *) cmd->argv[1]);
   }
 
   return PR_HANDLED(cmd);
@@ -2397,11 +2598,74 @@ MODRET auth_user(cmd_rec *cmd) {
 
 /* Close the passwd and group databases, similar to auth_pre_user(). */
 MODRET auth_pre_pass(cmd_rec *cmd) {
+  const char *user;
   char *displaylogin;
 
   pr_auth_endpwent(cmd->tmp_pool);
   pr_auth_endgrent(cmd->tmp_pool);
 
+  /* Handle cases where PASS might be sent before USER. */
+  user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
+  if (user != NULL) {
+    config_rec *c;
+
+    c = find_config(main_server->conf, CONF_PARAM, "AllowEmptyPasswords",
+      FALSE);
+    if (c == NULL) {
+      const char *anon_user;
+      config_rec *anon_config;
+
+      /* Since we have not authenticated yet, we cannot use the TOPLEVEL_CONF
+       * macro to handle <Anonymous> sections.  So we do it manually.
+       */
+      anon_user = pstrdup(cmd->tmp_pool, user);
+      anon_config = pr_auth_get_anon_config(cmd->tmp_pool, &anon_user, NULL,
+        NULL);
+      if (anon_config != NULL) {
+        c = find_config(anon_config->subset, CONF_PARAM, "AllowEmptyPasswords",
+          FALSE);
+      }
+    }
+ 
+    if (c != NULL) {
+      int allow_empty_passwords;
+
+      allow_empty_passwords = *((int *) c->argv[0]);
+      if (allow_empty_passwords == FALSE) {
+        size_t passwd_len = 0;
+ 
+        if (cmd->argc > 1) {
+          if (cmd->arg != NULL) {
+            passwd_len = strlen(cmd->arg);
+          }
+        }
+
+        /* Make sure to NOT enforce 'AllowEmptyPasswords off' if e.g.
+         * the AllowDotLogin TLSOption is in effect.
+         */
+        if (cmd->argc == 1 ||
+            passwd_len == 0) {
+
+          if (session.auth_mech == NULL ||
+              strcmp(session.auth_mech, "mod_tls.c") != 0) {
+            pr_log_debug(DEBUG5,
+              "Refusing empty password from user '%s' (AllowEmptyPasswords "
+              "false)", user);
+            pr_log_auth(PR_LOG_NOTICE,
+              "Refusing empty password from user '%s'", user);
+
+            pr_event_generate("mod_auth.empty-password", user);
+            pr_response_add_err(R_501, _("Login incorrect."));
+            return PR_ERROR(cmd);
+          }
+
+          pr_log_debug(DEBUG9, "%s", "'AllowEmptyPasswords off' in effect, "
+            "BUT client authenticated via the AllowDotLogin TLSOption");
+        }
+      }
+    }
+  }
+
   /* Look for a DisplayLogin file which has an absolute path.  If we find one,
    * open a filehandle, such that that file can be displayed even if the
    * session is chrooted.  DisplayLogin files with relative paths will be
@@ -2441,14 +2705,15 @@ MODRET auth_pre_pass(cmd_rec *cmd) {
 }
 
 MODRET auth_pass(cmd_rec *cmd) {
-  char *user = NULL;
+  const char *user = NULL;
   int res = 0;
 
-  if (logged_in)
+  if (logged_in) {
     return PR_ERROR_MSG(cmd, R_503, _("You are already logged in"));
+  }
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-  if (!user) {
+  if (user == NULL) {
     (void) pr_table_remove(session.notes, "mod_auth.orig-user", NULL);
     (void) pr_table_remove(session.notes, "mod_auth.anon-passwd", NULL);
 
@@ -2480,7 +2745,20 @@ MODRET auth_pass(cmd_rec *cmd) {
       }
     }
 
-    logged_in = 1;
+    logged_in = TRUE;
+
+    if (pr_trace_get_level(timing_channel)) {
+      unsigned long elapsed_ms;
+      uint64_t finish_ms;
+
+      pr_gettimeofday_millis(&finish_ms);
+      elapsed_ms = (unsigned long) (finish_ms - session.connect_time_ms);
+
+      pr_trace_msg(timing_channel, 4,
+        "Time before successful login (via '%s'): %lu ms", session.auth_mech,
+        elapsed_ms);
+    }
+
     return PR_HANDLED(cmd);
   }
 
@@ -2488,7 +2766,7 @@ MODRET auth_pass(cmd_rec *cmd) {
 
   if (res == 0) {
     unsigned int max_logins, *max = NULL;
-    char *denymsg = NULL;
+    const char *denymsg = NULL;
 
     /* check for AccessDenyMsg */
     if ((denymsg = get_param_ptr((session.anon_config ?
@@ -2543,6 +2821,230 @@ MODRET auth_rein(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* FSIO callbacks for providing a fake robots.txt file, for the AnonAllowRobots
+ * functionality.
+ */
+
+#define AUTH_ROBOTS_TXT			"User-agent: *\nDisallow: /\n"
+#define AUTH_ROBOTS_TXT_FD		6742
+
+static int robots_fsio_stat(pr_fs_t *fs, const char *path, struct stat *st) {
+  st->st_dev = (dev_t) 0;
+  st->st_ino = (ino_t) 0;
+  st->st_mode = (S_IFREG|S_IRUSR|S_IRGRP|S_IROTH);
+  st->st_nlink = 0;
+  st->st_uid = (uid_t) 0;
+  st->st_gid = (gid_t) 0;
+  st->st_atime = 0;
+  st->st_mtime = 0;
+  st->st_ctime = 0;
+  st->st_size = strlen(AUTH_ROBOTS_TXT);
+  st->st_blksize = 1024;
+  st->st_blocks = 1;
+
+  return 0;
+}
+
+static int robots_fsio_fstat(pr_fh_t *fh, int fd, struct stat *st) {
+  if (fd != AUTH_ROBOTS_TXT_FD) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return robots_fsio_stat(NULL, NULL, st);
+}
+
+static int robots_fsio_lstat(pr_fs_t *fs, const char *path, struct stat *st) {
+  return robots_fsio_stat(fs, path, st);
+}
+
+static int robots_fsio_unlink(pr_fs_t *fs, const char *path) {
+  return 0;
+}
+
+static int robots_fsio_open(pr_fh_t *fh, const char *path, int flags) {
+  if (flags != O_RDONLY) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return AUTH_ROBOTS_TXT_FD;
+}
+
+static int robots_fsio_close(pr_fh_t *fh, int fd) {
+  if (fd != AUTH_ROBOTS_TXT_FD) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int robots_fsio_read(pr_fh_t *fh, int fd, char *buf, size_t bufsz) {
+  size_t robots_len;
+
+  if (fd != AUTH_ROBOTS_TXT_FD) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  robots_len = strlen(AUTH_ROBOTS_TXT);
+
+  if (bufsz < robots_len) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  memcpy(buf, AUTH_ROBOTS_TXT, robots_len);
+  return (int) robots_len;
+}
+
+static int robots_fsio_write(pr_fh_t *fh, int fd, const char *buf,
+    size_t bufsz) {
+  if (fd != AUTH_ROBOTS_TXT_FD) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return (int) bufsz;
+}
+
+static int robots_fsio_access(pr_fs_t *fs, const char *path, int mode,
+    uid_t uid, gid_t gid, array_header *suppl_gids) {
+  if (mode != R_OK) {
+    errno = EACCES;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int robots_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
+
+  if (fh->fh_fd != AUTH_ROBOTS_TXT_FD) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (mode != R_OK) {
+    errno = EACCES;
+    return -1;
+  }
+
+  return 0;
+}
+
+MODRET auth_pre_retr(cmd_rec *cmd) {
+  const char *path;
+  pr_fs_t *curr_fs = NULL;
+  struct stat st;
+
+  /* Only apply this for <Anonymous> logins. */
+  if (session.anon_config == NULL) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (auth_anon_allow_robots == TRUE) {
+    return PR_DECLINED(cmd);
+  }
+
+  auth_anon_allow_robots_enabled = FALSE;
+
+  path = dir_canonical_path(cmd->tmp_pool, cmd->arg);
+  if (strcasecmp(path, "/robots.txt") != 0) {
+    return PR_DECLINED(cmd);
+  }
+
+  /* If a previous REST command, with a non-zero value, has been sent, then
+   * do nothing.  Ugh.
+   */
+  if (session.restart_pos > 0) {
+    pr_log_debug(DEBUG10, "'AnonAllowRobots off' in effect, but cannot "
+      "support resumed download (REST %" PR_LU " previously sent by client)",
+      (pr_off_t) session.restart_pos);
+    return PR_DECLINED(cmd);
+  }
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    /* There's an existing REAL "robots.txt" file on disk; use that, and
+     * preserve the principle of least surprise.
+     */
+    pr_log_debug(DEBUG10, "'AnonAllowRobots off' in effect, but have "
+      "real 'robots.txt' file on disk; using that");
+    return PR_DECLINED(cmd);
+  }
+
+  curr_fs = pr_get_fs(path, NULL);
+  if (curr_fs != NULL) {
+    pr_fs_t *robots_fs;
+
+    robots_fs = pr_register_fs(cmd->pool, "robots", path);
+    if (robots_fs == NULL) {
+      pr_log_debug(DEBUG8, "'AnonAllowRobots off' in effect, but failed to "
+        "register FS: %s", strerror(errno));
+      return PR_DECLINED(cmd);
+    }
+
+    /* Use enough of our own custom FSIO callbacks to be able to provide
+     * a fake "robots.txt" file.
+     */
+    robots_fs->stat = robots_fsio_stat;
+    robots_fs->fstat = robots_fsio_fstat;
+    robots_fs->lstat = robots_fsio_lstat;
+    robots_fs->unlink = robots_fsio_unlink;
+    robots_fs->open = robots_fsio_open;
+    robots_fs->close = robots_fsio_close;
+    robots_fs->read = robots_fsio_read;
+    robots_fs->write = robots_fsio_write;
+    robots_fs->access = robots_fsio_access;
+    robots_fs->faccess = robots_fsio_faccess;
+
+    /* For all other FSIO callbacks, use the underlying FS. */
+    robots_fs->rename = curr_fs->rename;
+    robots_fs->lseek = curr_fs->lseek;
+    robots_fs->link = curr_fs->link;
+    robots_fs->readlink = curr_fs->readlink;
+    robots_fs->symlink = curr_fs->symlink;
+    robots_fs->ftruncate = curr_fs->ftruncate;
+    robots_fs->truncate = curr_fs->truncate;
+    robots_fs->chmod = curr_fs->chmod;
+    robots_fs->fchmod = curr_fs->fchmod;
+    robots_fs->chown = curr_fs->chown;
+    robots_fs->fchown = curr_fs->fchown;
+    robots_fs->lchown = curr_fs->lchown;
+    robots_fs->utimes = curr_fs->utimes;
+    robots_fs->futimes = curr_fs->futimes;
+    robots_fs->fsync = curr_fs->fsync;
+
+    pr_fs_clear_cache2(path);
+    auth_anon_allow_robots_enabled = TRUE;
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET auth_post_retr(cmd_rec *cmd) {
+  if (auth_anon_allow_robots == TRUE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (auth_anon_allow_robots_enabled == TRUE) {
+    int res;
+
+    res = pr_unregister_fs("/robots.txt");
+    if (res < 0) {
+      pr_log_debug(DEBUG9, "error removing 'robots' FS for '/robots.txt': %s",
+        strerror(errno));
+    }
+
+    auth_anon_allow_robots_enabled = FALSE;
+  }
+
+  return PR_DECLINED(cmd);
+}
+
 /* Configuration handlers
  */
 
@@ -2572,20 +3074,61 @@ MODRET set_accessgrantmsg(cmd_rec *cmd) {
 
 /* usage: AllowChrootSymlinks on|off */
 MODRET set_allowchrootsymlinks(cmd_rec *cmd) {
-  int b = -1;
+  int allow_chroot_symlinks = -1;
   config_rec *c = NULL;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  b = get_boolean(cmd, 1);
-  if (b == -1) {
+  allow_chroot_symlinks = get_boolean(cmd, 1);
+  if (allow_chroot_symlinks == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = allow_chroot_symlinks;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AllowEmptyPasswords on|off */
+MODRET set_allowemptypasswords(cmd_rec *cmd) {
+  int allow_empty_passwords = -1;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
+
+  allow_empty_passwords = get_boolean(cmd, 1);
+  if (allow_empty_passwords == -1) {
     CONF_ERROR(cmd, "expected Boolean parameter");
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(int));
-  *((int *) c->argv[0]) = b;
+  *((int *) c->argv[0]) = allow_empty_passwords;
+  c->flags |= CF_MERGEDOWN;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: AnonAllowRobots on|off */
+MODRET set_anonallowrobots(cmd_rec *cmd) {
+  int allow_robots = -1;
+  config_rec *c;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ANON);
+
+  allow_robots = get_boolean(cmd, 1);
+  if (allow_robots == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = allow_robots;
 
   return PR_HANDLED(cmd);
 }
@@ -2608,17 +3151,52 @@ MODRET set_anonrequirepassword(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: AnonRejectPasswords pattern [flags] */
 MODRET set_anonrejectpasswords(cmd_rec *cmd) {
 #ifdef PR_USE_REGEX
+  config_rec *c;
   pr_regex_t *pre = NULL;
-  int res;
+  int notmatch = FALSE, regex_flags = REG_EXTENDED|REG_NOSUB, res = 0;
+  char *pattern = NULL;
+
+  if (cmd->argc-1 < 1 ||
+      cmd->argc-1 > 2) {
+    CONF_ERROR(cmd, "bad number of parameters");
+  }
 
-  CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ANON);
 
+  /* Make sure that, if present, the flags parameter is correctly formatted. */
+  if (cmd->argc-1 == 2) {
+    int flags = 0;
+
+    /* We need to parse the flags parameter here, to see if any flags which
+     * affect the compilation of the regex (e.g. NC) are present.
+     */
+
+    flags = pr_filter_parse_flags(cmd->tmp_pool, cmd->argv[2]);
+    if (flags < 0) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+        ": badly formatted flags parameter: '", cmd->argv[2], "'", NULL));
+    }
+
+    if (flags == 0) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+        ": unknown flags '", cmd->argv[2], "'", NULL));
+    }
+
+    regex_flags |= flags;
+  }
+
   pre = pr_regexp_alloc(&auth_module);
 
-  res = pr_regexp_compile(pre, cmd->argv[1], REG_EXTENDED|REG_NOSUB);
+  pattern = cmd->argv[1];
+  if (*pattern == '!') {
+    notmatch = TRUE;
+    pattern++;
+  }
+
+  res = pr_regexp_compile(pre, pattern, regex_flags);
   if (res != 0) {
     char errstr[200] = {'\0'};
 
@@ -2629,7 +3207,9 @@ MODRET set_anonrejectpasswords(cmd_rec *cmd) {
       cmd->argv[1], "': ", errstr, NULL));
   }
 
-  (void) add_config_param(cmd->argv[0], 1, (void *) pre);
+  c = add_config_param(cmd->argv[0], 2, pre, NULL);
+  c->argv[1] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[1]) = notmatch;
   return PR_HANDLED(cmd);
 
 #else
@@ -2774,12 +3354,9 @@ MODRET set_createhome(cmd_rec *cmd) {
 
         /* Check for a "~" parameter. */
         if (strncmp(cmd->argv[i+1], "~", 2) != 0) {
-          char *tmp = NULL;
           uid_t uid;
 
-          uid = strtol(cmd->argv[++i], &tmp, 10);
-
-          if (tmp && *tmp) {
+          if (pr_str2uid(cmd->argv[++i], &uid) < 0) { 
             CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "bad UID parameter: '",
               cmd->argv[i], "'", NULL));
           }
@@ -2798,12 +3375,9 @@ MODRET set_createhome(cmd_rec *cmd) {
 
         /* Check for a "~" parameter. */
         if (strncmp(cmd->argv[i+1], "~", 2) != 0) {
-          char *tmp = NULL;
           gid_t gid;
 
-          gid = strtol(cmd->argv[++i], &tmp, 10);
-
-          if (tmp && *tmp) {
+          if (pr_str2gid(cmd->argv[++i], &gid) < 0) {
             CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "bad GID parameter: '",
               cmd->argv[i], "'", NULL));
           }
@@ -2873,40 +3447,44 @@ MODRET set_createhome(cmd_rec *cmd) {
 
 MODRET add_defaultroot(cmd_rec *cmd) {
   config_rec *c;
-  char *dir,**argv;
-  int argc;
+  char *dir;
+  unsigned int argc;
+  void **argv;
   array_header *acl = NULL;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (cmd->argc < 2)
-    CONF_ERROR(cmd,"syntax: DefaultRoot <directory> [<group-expression>]");
+  if (cmd->argc < 2) {
+    CONF_ERROR(cmd, "syntax: DefaultRoot <directory> [<group-expression>]");
+  }
 
-  argv = cmd->argv;
   argc = cmd->argc - 2;
+  argv = cmd->argv;
 
   dir = *++argv;
 
   /* dir must be / or ~. */
   if (*dir != '/' &&
-      *dir != '~')
+      *dir != '~') {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "(", dir, ") absolute pathname "
       "required", NULL));
+  }
 
-  if (strchr(dir, '*'))
+  if (strchr(dir, '*')) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "(", dir, ") wildcards not allowed "
       "in pathname", NULL));
+  }
 
-  if (*(dir + strlen(dir) - 1) != '/')
+  if (*(dir + strlen(dir) - 1) != '/') {
     dir = pstrcat(cmd->tmp_pool, dir, "/", NULL);
+  }
 
-  acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
-
+  acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
   c = add_config_param(cmd->argv[0], 0);
 
   c->argc = argc + 1;
-  c->argv = pcalloc(c->pool, (argc + 2) * sizeof(char *));
-  argv = (char **) c->argv;
+  c->argv = pcalloc(c->pool, (argc + 2) * sizeof(void *));
+  argv = c->argv;
   *argv++ = pstrdup(c->pool, dir);
 
   if (argc && acl)
@@ -2921,41 +3499,45 @@ MODRET add_defaultroot(cmd_rec *cmd) {
 
 MODRET add_defaultchdir(cmd_rec *cmd) {
   config_rec *c;
-  char *dir,**argv;
-  int argc;
+  char *dir;
+  unsigned int argc;
+  void **argv;
   array_header *acl = NULL;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
 
-  if (cmd->argc < 2)
+  if (cmd->argc < 2) {
     CONF_ERROR(cmd, "syntax: DefaultChdir <directory> [<group-expression>]");
+  }
 
-  argv = cmd->argv;
   argc = cmd->argc - 2;
+  argv = cmd->argv;
 
   dir = *++argv;
 
-  if (strchr(dir, '*'))
+  if (strchr(dir, '*')) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "(", dir, ") wildcards not allowed "
       "in pathname", NULL));
+  }
 
-  if (*(dir + strlen(dir) - 1) != '/')
+  if (*(dir + strlen(dir) - 1) != '/') {
     dir = pstrcat(cmd->tmp_pool, dir, "/", NULL);
+  }
 
-  acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
-
+  acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
   c = add_config_param(cmd->argv[0], 0);
 
   c->argc = argc + 1;
-  c->argv = pcalloc(c->pool, (argc + 2) * sizeof(char *));
-  argv = (char **) c->argv;
+  c->argv = pcalloc(c->pool, (argc + 2) * sizeof(void *));
+  argv = c->argv;
   *argv++ = pstrdup(c->pool, dir);
 
-  if (argc && acl)
+  if (argc && acl) {
     while(argc--) {
       *argv++ = pstrdup(c->pool, *((char **) acl->elts));
       acl->elts = ((char **) acl->elts) + 1;
     }
+  }
 
   *argv = NULL;
 
@@ -3259,6 +3841,50 @@ MODRET set_maxloginattempts(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: MaxPasswordSize len */
+MODRET set_maxpasswordsize(cmd_rec *cmd) {
+  config_rec *c;
+  size_t password_len;
+  char *len, *ptr = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  len = cmd->argv[1];
+  if (*len == '-') {
+    CONF_ERROR(cmd, "badly formatted parameter");
+  }
+
+  password_len = strtoul(len, &ptr, 10);
+  if (ptr && *ptr) {
+    CONF_ERROR(cmd, "badly formatted parameter");
+  }
+
+/* XXX Applies to the following modules, which use crypt(3):
+ *
+ *  mod_ldap (ldap_auth_check; "check" authtab)
+ *    ldap_auth_auth ("auth" authtab) calls pr_auth_check()
+ *  mod_sql (sql_auth_crypt, via SQLAuthTypes; cmd_check "check" authtab dispatches here)
+ *    cmd_auth ("auth" authtab) calls pr_auth_check()
+ *  mod_auth_file (authfile_chkpass, "check" authtab)
+ *    authfile_auth ("auth" authtab) calls pr_auth_check()
+ *  mod_auth_unix (pw_check, "check" authtab)
+ *    pw_auth ("auth" authtab) calls pr_auth_check()
+ *
+ *  mod_sftp uses pr_auth_authenticate(), which will dispatch into above
+ *
+ *  mod_radius does NOT use either -- up to RADIUS server policy?
+ *
+ * Is there a common code path that all of the above go through?
+ */
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(size_t));
+  *((size_t *) c->argv[0]) = password_len;
+
+  return PR_HANDLED(cmd);
+}
+
 MODRET set_requirevalidshell(cmd_rec *cmd) {
   int bool = -1;
   config_rec *c = NULL;
@@ -3373,8 +3999,10 @@ MODRET set_timeoutsession(cmd_rec *cmd) {
      cmd->server->config_type : CONF_ROOT);
 
   /* this directive must have either 1 or 3 arguments */
-  if (cmd->argc-1 != 1 && cmd->argc-1 != 3)
-    CONF_ERROR(cmd, "missing arguments");
+  if (cmd->argc-1 != 1 &&
+      cmd->argc-1 != 3) {
+    CONF_ERROR(cmd, "missing parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
 
@@ -3425,24 +4053,27 @@ MODRET set_timeoutsession(cmd_rec *cmd) {
 
   } else if (cmd->argc-1 == 3) {
     array_header *acl = NULL;
-    int argc = cmd->argc - 3;
-    char **argv = cmd->argv + 2;
+    unsigned int argc;
+    void **argv;
 
-    acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
+    argc = cmd->argc - 3;
+    argv = cmd->argv + 2;
+
+    acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
 
     c = add_config_param(cmd->argv[0], 0);
     c->argc = argc + 2;
 
-    /* add 3 to argc for the argv of the config_rec: one for the
+    /* Add 3 to argc for the argv of the config_rec: one for the
      * seconds value, one for the precedence, one for the classifier,
-     * and one for the terminating NULL
+     * and one for the terminating NULL.
      */
-    c->argv = pcalloc(c->pool, ((argc + 4) * sizeof(char *)));
+    c->argv = pcalloc(c->pool, ((argc + 4) * sizeof(void *)));
 
-    /* capture the config_rec's argv pointer for doing the by-hand
-     * population
+    /* Capture the config_rec's argv pointer for doing the by-hand
+     * population.
      */
-    argv = (char **) c->argv;
+    argv = c->argv;
 
     /* Copy in the seconds. */
     *argv = pcalloc(c->pool, sizeof(int));
@@ -3520,15 +4151,20 @@ MODRET set_uselastlog(cmd_rec *cmd) {
 /* usage: UserAlias alias real-user */
 MODRET set_useralias(cmd_rec *cmd) {
   config_rec *c = NULL;
+  char *alias, *real_user;
 
   CHECK_ARGS(cmd, 2);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
 
   /* Make sure that the given names differ. */
-  if (strcmp(cmd->argv[1], cmd->argv[2]) == 0)
+  alias = cmd->argv[1];
+  real_user = cmd->argv[2];
+
+  if (strcmp(alias, real_user) == 0) {
     CONF_ERROR(cmd, "alias and real user names must differ");
+  }
 
-  c = add_config_param_str(cmd->argv[0], 2, cmd->argv[1], cmd->argv[2]);
+  c = add_config_param_str(cmd->argv[0], 2, alias, real_user);
 
   /* Note: only merge this directive down if it is not appearing in an
    * <Anonymous> context.
@@ -3570,6 +4206,32 @@ MODRET set_userpassword(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: WtmpLog on|off */
+MODRET set_wtmplog(cmd_rec *cmd) {
+  int use_wtmp = -1;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
+
+  if (strcasecmp(cmd->argv[1], "NONE") == 0) {
+    use_wtmp = FALSE;
+
+  } else {
+    use_wtmp = get_boolean(cmd, 1);
+    if (use_wtmp == -1) {
+      CONF_ERROR(cmd, "expected Boolean parameter");
+    }
+  }
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned char));
+  *((unsigned char *) c->argv[0]) = use_wtmp;
+  c->flags |= CF_MERGEDOWN;
+
+  return PR_HANDLED(cmd);
+}
+
 /* Module API tables
  */
 
@@ -3577,6 +4239,8 @@ static conftable auth_conftab[] = {
   { "AccessDenyMsg",		set_accessdenymsg,		NULL },
   { "AccessGrantMsg",		set_accessgrantmsg,		NULL },
   { "AllowChrootSymlinks",	set_allowchrootsymlinks,	NULL },
+  { "AllowEmptyPasswords",	set_allowemptypasswords,	NULL },
+  { "AnonAllowRobots",		set_anonallowrobots,		NULL },
   { "AnonRequirePassword",	set_anonrequirepassword,	NULL },
   { "AnonRejectPasswords",	set_anonrejectpasswords,	NULL },
   { "AuthAliasOnly",		set_authaliasonly,		NULL },
@@ -3594,6 +4258,7 @@ static conftable auth_conftab[] = {
   { "MaxConnectionsPerHost",	set_maxconnectsperhost,		NULL },
   { "MaxHostsPerUser",		set_maxhostsperuser,		NULL },
   { "MaxLoginAttempts",		set_maxloginattempts,		NULL },
+  { "MaxPasswordSize",		set_maxpasswordsize,		NULL },
   { "RequireValidShell",	set_requirevalidshell,		NULL },
   { "RewriteHome",		set_rewritehome,		NULL },
   { "RootLogin",		set_rootlogin,			NULL },
@@ -3605,6 +4270,8 @@ static conftable auth_conftab[] = {
   { "UserAlias",		set_useralias,			NULL },
   { "UserDirRoot",		set_userdirroot,		NULL },
   { "UserPassword",		set_userpassword,		NULL },
+  { "WtmpLog",			set_wtmplog,			NULL },
+
   { NULL,			NULL,				NULL }
 };
 
@@ -3618,7 +4285,12 @@ static cmdtable auth_cmdtab[] = {
   { LOG_CMD_ERR,C_PASS,	G_NONE,	auth_err_pass,  FALSE,  FALSE },
   { CMD,	C_ACCT,	G_NONE,	auth_acct,	FALSE,	FALSE,	CL_AUTH },
   { CMD,	C_REIN,	G_NONE,	auth_rein,	FALSE,	FALSE,	CL_AUTH },
-  { POST_CMD,	C_HOST,	G_NONE,	auth_post_host,	FALSE,	FALSE },
+
+  /* For the automatic robots.txt handling */
+  { PRE_CMD,	C_RETR,	G_NONE,	auth_pre_retr,	FALSE,	FALSE },
+  { POST_CMD,	C_RETR,	G_NONE,	auth_post_retr,	FALSE,	FALSE },
+  { POST_CMD_ERR,C_RETR,G_NONE,	auth_post_retr,	FALSE,	FALSE },
+
   { 0, NULL }
 };
 
diff --git a/modules/mod_auth_file.c b/modules/mod_auth_file.c
index a5cc663..948c6ee 100644
--- a/modules/mod_auth_file.c
+++ b/modules/mod_auth_file.c
@@ -1,8 +1,7 @@
 /*
  * ProFTPD: mod_auth_file - file-based authentication module that supports
  *                          restrictions on the file contents
- *
- * Copyright (c) 2002-2015 The ProFTPD Project team
+ * Copyright (c) 2002-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -78,7 +77,7 @@ typedef struct file_rec {
 
 } authfile_file_t;
 
-/* List of server-specific Authiles */
+/* List of server-specific AuthFiles */
 static authfile_file_t *af_user_file = NULL;
 static authfile_file_t *af_group_file = NULL;
 
@@ -86,10 +85,10 @@ static int handle_empty_salt = FALSE;
 
 static int authfile_sess_init(void);
 
-static int af_setpwent(void);
-static int af_setgrent(void);
+static int af_setpwent(pool *);
+static int af_setgrent(pool *);
 
-static const char *trace_channel = "authfile";
+static const char *trace_channel = "auth.file";
 
 /* Support routines.  Move the passwd/group functions out of lib/ into here. */
 
@@ -175,6 +174,9 @@ static int af_check_file(pool *p, const char *name, const char *path,
        * we need to make sure that we get an absolute path (Bug#4145).
        */
       path = dir_abs_path(p, buf, FALSE);
+      if (path != NULL) {
+        orig_path = path;
+      }
     }
 
     res = stat(orig_path, &st);
@@ -232,7 +234,7 @@ static int af_check_file(pool *p, const char *name, const char *path,
   /* Check the parent directory of this file.  If the parent directory
    * is world-writable, that too is insecure.
    */
-  res = af_check_parent_dir(p, name, path);
+  res = af_check_parent_dir(p, name, orig_path);
   if (res < 0) {
     return -1;
   }
@@ -361,6 +363,8 @@ static char **af_getgrmems(char *s) {
   int nmembers = 0;
 
   while (s && *s && nmembers < MAXMEMBERS) {
+    pr_signals_handle();
+
     members[nmembers++] = s;
     while (*s && *s != ',') {
       s++;
@@ -433,8 +437,8 @@ static struct group *af_getgrp(const char *buf, unsigned int lineno) {
 }
 #endif /* !HAVE_FGETGRENT */
 
-static int af_allow_grent(struct group *grp) {
-  if (!af_group_file) {
+static int af_allow_grent(pool *p, struct group *grp) {
+  if (af_group_file == NULL) {
     errno = EPERM;
     return -1;
   }
@@ -444,18 +448,18 @@ static int af_allow_grent(struct group *grp) {
 
     if (grp->gr_gid < af_group_file->af_min_id.gid) {
       pr_log_debug(DEBUG3, MOD_AUTH_FILE_VERSION ": skipping group '%s': "
-        "GID %lu below the minimum allowed (%lu)", grp->gr_name,
-        (unsigned long) grp->gr_gid,
-        (unsigned long) af_group_file->af_min_id.gid);
+        "GID %s below the minimum allowed (%s)", grp->gr_name,
+        pr_gid2str(p, grp->gr_gid),
+        pr_gid2str(p, af_group_file->af_min_id.gid));
       errno = EINVAL;
       return -1;
     }
 
     if (grp->gr_gid > af_group_file->af_max_id.gid) {
       pr_log_debug(DEBUG3, MOD_AUTH_FILE_VERSION ": skipping group '%s': "
-        "GID %lu above the maximum allowed (%lu)", grp->gr_name,
-        (unsigned long) grp->gr_gid,
-        (unsigned long) af_group_file->af_max_id.gid);
+        "GID %s above the maximum allowed (%s)", grp->gr_name,
+        pr_gid2str(p, grp->gr_gid),
+        pr_gid2str(p, af_group_file->af_max_id.gid));
       errno = EINVAL;
       return -1;
     }
@@ -494,7 +498,7 @@ static void af_endgrent(void) {
   return;
 }
 
-static struct group *af_getgrent(void) {
+static struct group *af_getgrent(pool *p) {
   struct group *grp = NULL, *res = NULL;
 
   if (!af_group_file ||
@@ -548,7 +552,7 @@ static struct group *af_getgrent(void) {
       break;
     }
 
-    if (af_allow_grent(grp) < 0) {
+    if (af_allow_grent(p, grp) < 0) {
       continue;
     }
 
@@ -559,16 +563,17 @@ static struct group *af_getgrent(void) {
   return res;
 }
 
-static struct group *af_getgrnam(const char *name) {
+static struct group *af_getgrnam(pool *p, const char *name) {
   struct group *grp = NULL;
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(p) < 0) {
     return NULL;
   }
 
-  while ((grp = af_getgrent()) != NULL) {
-    if (strcmp(name, grp->gr_name) == 0) {
+  while ((grp = af_getgrent(p)) != NULL) {
+    pr_signals_handle();
 
+    if (strcmp(name, grp->gr_name) == 0) {
       /* Found the requested group */
       break;
     }
@@ -577,16 +582,17 @@ static struct group *af_getgrnam(const char *name) {
   return grp;
 }
 
-static struct group *af_getgrgid(gid_t gid) {
+static struct group *af_getgrgid(pool *p, gid_t gid) {
   struct group *grp = NULL;
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(p) < 0) {
     return NULL;
   }
 
-  while ((grp = af_getgrent()) != NULL) {
-    if (grp->gr_gid == gid) {
+  while ((grp = af_getgrent(p)) != NULL) {
+    pr_signals_handle();
 
+    if (grp->gr_gid == gid) {
       /* Found the requested GID */
       break;
     }
@@ -595,7 +601,7 @@ static struct group *af_getgrgid(gid_t gid) {
   return grp;
 }
 
-static int af_setgrent(void) {
+static int af_setgrent(pool *p) {
 
   if (af_group_file != NULL) {
     if (af_group_file->af_file != NULL) {
@@ -617,10 +623,10 @@ static int af_setgrent(void) {
         if (pr_fsio_stat(af_group_file->af_path, &st) == 0) {
           pr_log_pri(PR_LOG_WARNING,
             "error: unable to open AuthGroupFile file '%s' (file owned by "
-            "UID %lu, GID %lu, perms %04o, accessed by UID %lu, GID %lu): %s",
-            af_group_file->af_path, (unsigned long) st.st_uid,
-            (unsigned long) st.st_gid, st.st_mode & ~S_IFMT,
-            (unsigned long) geteuid(), (unsigned long) getegid(),
+            "UID %s, GID %s, perms %04o, accessed by UID %s, GID %s): %s",
+            af_group_file->af_path, pr_uid2str(p, st.st_uid),
+            pr_gid2str(p, st.st_gid), st.st_mode & ~S_IFMT,
+            pr_uid2str(p, geteuid()), pr_gid2str(p, getegid()),
             strerror(xerrno));
 
         } else {
@@ -633,6 +639,11 @@ static int af_setgrent(void) {
         return -1;
       }
 
+      /* As the file may contain sensitive data, we do not want it lingering
+       * around in stdio buffers.
+       */
+      (void) setvbuf(af_group_file->af_file, NULL, _IONBF, 0);
+
       if (fcntl(fileno(af_group_file->af_file), F_SETFD, FD_CLOEXEC) < 0) {
         pr_log_pri(PR_LOG_WARNING, MOD_AUTH_FILE_VERSION
           ": unable to set CLOEXEC on AuthGroupFile %s (fd %d): %s",
@@ -651,8 +662,8 @@ static int af_setgrent(void) {
   return -1;
 }
 
-static int af_allow_pwent(struct passwd *pwd) {
-  if (!af_user_file) {
+static int af_allow_pwent(pool *p, struct passwd *pwd) {
+  if (af_user_file == NULL) {
     errno = EPERM;
     return -1;
   }
@@ -662,18 +673,18 @@ static int af_allow_pwent(struct passwd *pwd) {
 
     if (pwd->pw_uid < af_user_file->af_min_id.uid) {
       pr_log_debug(DEBUG3, MOD_AUTH_FILE_VERSION ": skipping user '%s': "
-        "UID %lu below the minimum allowed (%lu)", pwd->pw_name,
-        (unsigned long) pwd->pw_uid,
-        (unsigned long) af_user_file->af_min_id.uid);
+        "UID %s below the minimum allowed (%s)", pwd->pw_name,
+        pr_uid2str(p, pwd->pw_uid),
+        pr_uid2str(p, af_user_file->af_min_id.uid));
       errno = EINVAL;
       return -1;
     }
 
     if (pwd->pw_uid > af_user_file->af_max_id.gid) {
       pr_log_debug(DEBUG3, MOD_AUTH_FILE_VERSION ": skipping user '%s': "
-        "UID %lu above the maximum allowed (%lu)", pwd->pw_name,
-        (unsigned long) pwd->pw_uid,
-        (unsigned long) af_user_file->af_max_id.uid);
+        "UID %s above the maximum allowed (%s)", pwd->pw_name,
+        pr_uid2str(p, pwd->pw_uid),
+        pr_uid2str(p, af_user_file->af_max_id.uid));
       errno = EINVAL;
       return -1;
     }
@@ -729,7 +740,7 @@ static void af_endpwent(void) {
   return;
 }
 
-static struct passwd *af_getpwent(void) {
+static struct passwd *af_getpwent(pool *p) {
   struct passwd *pwd = NULL, *res = NULL;
 
   if (af_user_file == NULL ||
@@ -773,7 +784,7 @@ static struct passwd *af_getpwent(void) {
       break;
     }
 
-    if (af_allow_pwent(pwd) < 0) {
+    if (af_allow_pwent(p, pwd) < 0) {
 #ifndef HAVE_FGETPWENT
       memset(buf, '\0', sizeof(buf));
 #endif
@@ -787,18 +798,17 @@ static struct passwd *af_getpwent(void) {
   return res;
 }
 
-static struct passwd *af_getpwnam(const char *name) {
+static struct passwd *af_getpwnam(pool *p, const char *name) {
   struct passwd *pwd = NULL;
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(p) < 0) {
     return NULL;
   }
 
-  while ((pwd = af_getpwent()) != NULL) {
+  while ((pwd = af_getpwent(p)) != NULL) {
     pr_signals_handle();
 
     if (strcmp(name, pwd->pw_name) == 0) {
-
       /* Found the requested user */
       break;
     }
@@ -807,21 +817,22 @@ static struct passwd *af_getpwnam(const char *name) {
   return pwd;
 }
 
-static char *af_getpwpass(const char *name) {
-  struct passwd *pwd = af_getpwnam(name);
+static char *af_getpwpass(pool *p, const char *name) {
+  struct passwd *pwd = af_getpwnam(p, name);
   return pwd ? pwd->pw_passwd : NULL;
 }
 
-static struct passwd *af_getpwuid(uid_t uid) {
+static struct passwd *af_getpwuid(pool *p, uid_t uid) {
   struct passwd *pwd = NULL;
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(p) < 0) {
     return NULL;
   }
 
-  while ((pwd = af_getpwent()) != NULL) {
-    if (pwd->pw_uid == uid) {
+  while ((pwd = af_getpwent(p)) != NULL) {
+    pr_signals_handle();
 
+    if (pwd->pw_uid == uid) {
       /* Found the requested UID */
       break;
     }
@@ -830,7 +841,7 @@ static struct passwd *af_getpwuid(uid_t uid) {
   return pwd;
 }
 
-static int af_setpwent(void) {
+static int af_setpwent(pool *p) {
 
   if (af_user_file != NULL) {
     if (af_user_file->af_file != NULL) {
@@ -852,10 +863,10 @@ static int af_setpwent(void) {
         if (pr_fsio_stat(af_user_file->af_path, &st) == 0) {
           pr_log_pri(PR_LOG_WARNING,
             "error: unable to open AuthUserFile file '%s' (file owned by "
-            "UID %lu, GID %lu, perms %04o, accessed by UID %lu, GID %lu): %s",
-            af_user_file->af_path, (unsigned long) st.st_uid,
-            (unsigned long) st.st_gid, st.st_mode & ~S_IFMT,
-            (unsigned long) geteuid(), (unsigned long) getegid(),
+            "UID %s, GID %s, perms %04o, accessed by UID %s, GID %s): %s",
+            af_user_file->af_path, pr_uid2str(p, st.st_uid),
+            pr_gid2str(p, st.st_gid), st.st_mode & ~S_IFMT,
+            pr_uid2str(p, geteuid()), pr_gid2str(p, getegid()),
             strerror(xerrno));
 
         } else {
@@ -868,6 +879,11 @@ static int af_setpwent(void) {
         return -1;
       }
 
+      /* As the file may contain sensitive data, we do not want it lingering
+       * around in stdio buffers.
+       */
+      (void) setvbuf(af_user_file->af_file, NULL, _IONBF, 0);
+
       if (fcntl(fileno(af_user_file->af_file), F_SETFD, FD_CLOEXEC) < 0) {
         pr_log_pri(PR_LOG_WARNING, MOD_AUTH_FILE_VERSION
           ": unable to set CLOEXEC on AuthUserFile %s (fd %d): %s",
@@ -897,7 +913,7 @@ MODRET authfile_endpwent(cmd_rec *cmd) {
 MODRET authfile_getpwent(cmd_rec *cmd) {
   struct passwd *pwd = NULL;
 
-  pwd = af_getpwent();
+  pwd = af_getpwent(cmd->tmp_pool);
 
   return pwd ? mod_create_data(cmd, pwd) : PR_DECLINED(cmd);
 }
@@ -906,14 +922,15 @@ MODRET authfile_getpwnam(cmd_rec *cmd) {
   struct passwd *pwd = NULL;
   const char *name = cmd->argv[0];
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
   /* Ugly -- we iterate through the file.  Time-consuming. */
-  while ((pwd = af_getpwent()) != NULL) {
-    if (strcmp(name, pwd->pw_name) == 0) {
+  while ((pwd = af_getpwent(cmd->tmp_pool)) != NULL) {
+    pr_signals_handle();
 
+    if (strcmp(name, pwd->pw_name) == 0) {
       /* Found the requested name */
       break;
     }
@@ -926,11 +943,11 @@ MODRET authfile_getpwuid(cmd_rec *cmd) {
   struct passwd *pwd = NULL;
   uid_t uid = *((uid_t *) cmd->argv[0]);
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  pwd = af_getpwuid(uid);
+  pwd = af_getpwuid(cmd->tmp_pool, uid);
 
   return pwd ? mod_create_data(cmd, pwd) : PR_DECLINED(cmd);
 }
@@ -938,17 +955,17 @@ MODRET authfile_getpwuid(cmd_rec *cmd) {
 MODRET authfile_name2uid(cmd_rec *cmd) {
   struct passwd *pwd = NULL;
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  pwd = af_getpwnam(cmd->argv[0]);
+  pwd = af_getpwnam(cmd->tmp_pool, cmd->argv[0]);
 
   return pwd ? mod_create_data(cmd, (void *) &pwd->pw_uid) : PR_DECLINED(cmd);
 }
 
 MODRET authfile_setpwent(cmd_rec *cmd) {
-  if (af_setpwent() == 0) {
+  if (af_setpwent(cmd->tmp_pool) == 0) {
     return PR_DECLINED(cmd);
   }
 
@@ -958,11 +975,11 @@ MODRET authfile_setpwent(cmd_rec *cmd) {
 MODRET authfile_uid2name(cmd_rec *cmd) {
   struct passwd *pwd = NULL;
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  pwd = af_getpwuid(*((uid_t *) cmd->argv[0]));
+  pwd = af_getpwuid(cmd->tmp_pool, *((uid_t *) cmd->argv[0]));
 
   return pwd ? mod_create_data(cmd, pwd->pw_name) : PR_DECLINED(cmd);
 }
@@ -975,7 +992,7 @@ MODRET authfile_endgrent(cmd_rec *cmd) {
 MODRET authfile_getgrent(cmd_rec *cmd) {
   struct group *grp = NULL;
 
-  grp = af_getgrent();
+  grp = af_getgrent(cmd->tmp_pool);
 
   return grp ? mod_create_data(cmd, grp) : PR_DECLINED(cmd);
 }
@@ -984,11 +1001,11 @@ MODRET authfile_getgrgid(cmd_rec *cmd) {
   struct group *grp = NULL;
   gid_t gid = *((gid_t *) cmd->argv[0]);
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  grp = af_getgrgid(gid);
+  grp = af_getgrgid(cmd->tmp_pool, gid);
 
   return grp ? mod_create_data(cmd, grp) : PR_DECLINED(cmd);
 }
@@ -997,13 +1014,14 @@ MODRET authfile_getgrnam(cmd_rec *cmd) {
   struct group *grp = NULL;
   const char *name = cmd->argv[0];
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  while ((grp = af_getgrent()) != NULL) {
-    if (strcmp(name, grp->gr_name) == 0) {
+  while ((grp = af_getgrent(cmd->tmp_pool)) != NULL) {
+    pr_signals_handle();
 
+    if (strcmp(name, grp->gr_name) == 0) {
       /* Found the name requested */
       break;
     }
@@ -1022,23 +1040,25 @@ MODRET authfile_getgroups(cmd_rec *cmd) {
     return PR_DECLINED(cmd);
   }
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
   /* Check for NULLs */
-  if (cmd->argv[1])
+  if (cmd->argv[1]) {
     gids = (array_header *) cmd->argv[1];
+  }
 
-  if (cmd->argv[2])
+  if (cmd->argv[2]) {
     groups = (array_header *) cmd->argv[2];
+  }
 
   /* Retrieve the necessary info. */
-  pwd = af_getpwnam(name);
+  pwd = af_getpwnam(cmd->tmp_pool, name);
   if (pwd == NULL) {
     return PR_DECLINED(cmd);
   }
@@ -1049,16 +1069,16 @@ MODRET authfile_getgroups(cmd_rec *cmd) {
   }
 
   if (groups &&
-      (grp = af_getgrgid(pwd->pw_gid)) != NULL) {
+      (grp = af_getgrgid(cmd->tmp_pool, pwd->pw_gid)) != NULL) {
     *((char **) push_array(groups)) = pstrdup(session.pool, grp->gr_name);
   }
 
-  af_setgrent();
+  (void) af_setgrent(cmd->tmp_pool);
 
   /* This is where things get slow, expensive, and ugly.  Loop through
    * everything, checking to make sure we haven't already added it.
    */
-  while ((grp = af_getgrent()) != NULL &&
+  while ((grp = af_getgrent(cmd->tmp_pool)) != NULL &&
       grp->gr_mem) {
     char **gr_mems = NULL;
 
@@ -1071,11 +1091,13 @@ MODRET authfile_getgroups(cmd_rec *cmd) {
       if (strcmp(*gr_mems, pwd->pw_name) == 0) {
 
         /* ...add the GID and name */
-        if (gids)
+        if (gids) {
           *((gid_t *) push_array(gids)) = grp->gr_gid;
+        }
 
-        if (groups)
+        if (groups) {
           *((char **) push_array(groups)) = pstrdup(session.pool, grp->gr_name);
+        }
       }
     }
   }
@@ -1093,11 +1115,11 @@ MODRET authfile_getgroups(cmd_rec *cmd) {
 MODRET authfile_gid2name(cmd_rec *cmd) {
   struct group *grp = NULL;
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  grp = af_getgrgid(*((gid_t *) cmd->argv[0]));
+  grp = af_getgrgid(cmd->tmp_pool, *((gid_t *) cmd->argv[0]));
 
   return grp ? mod_create_data(cmd, grp->gr_name) : PR_DECLINED(cmd);
 }
@@ -1105,17 +1127,17 @@ MODRET authfile_gid2name(cmd_rec *cmd) {
 MODRET authfile_name2gid(cmd_rec *cmd) {
   struct group *grp = NULL;
 
-  if (af_setgrent() < 0) {
+  if (af_setgrent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
-  grp = af_getgrnam(cmd->argv[0]);
+  grp = af_getgrnam(cmd->tmp_pool, cmd->argv[0]);
 
   return grp ? mod_create_data(cmd, (void *) &grp->gr_gid) : PR_DECLINED(cmd);
 }
 
 MODRET authfile_setgrent(cmd_rec *cmd) {
-  if (af_setgrent() == 0) {
+  if (af_setgrent(cmd->tmp_pool) == 0) {
     return PR_DECLINED(cmd);
   }
 
@@ -1126,12 +1148,12 @@ MODRET authfile_auth(cmd_rec *cmd) {
   char *tmp = NULL, *cleartxt_pass = NULL;
   const char *name = cmd->argv[0];
 
-  if (af_setpwent() < 0) {
+  if (af_setpwent(cmd->tmp_pool) < 0) {
     return PR_DECLINED(cmd);
   }
 
   /* Lookup the cleartxt password for this user. */
-  tmp = af_getpwpass(name);
+  tmp = af_getpwpass(cmd->tmp_pool, name);
   if (tmp == NULL) {
 
     /* For now, return DECLINED.  Ideally, we could stash an auth module
@@ -1151,25 +1173,117 @@ MODRET authfile_auth(cmd_rec *cmd) {
 
   cleartxt_pass = pstrdup(cmd->tmp_pool, tmp);
 
-  if (pr_auth_check(cmd->tmp_pool, cleartxt_pass, name, cmd->argv[1]))
+  if (pr_auth_check(cmd->tmp_pool, cleartxt_pass, name, cmd->argv[1])) {
     return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+  }
 
   session.auth_mech = "mod_auth_file.c";
   return PR_HANDLED(cmd);
 }
 
+/* Per Bug#4171, if we see EINVAL (or EPERM, as documented in same man pages),
+ * check the /proc/sys/crypto/fips_enabled setting and the salt string, to see
+ * if an unsupported algorithm in FIPS mode, e.g. DES or MD5, was used to
+ * generate this salt string.
+ *
+ * There's not much we can do at this point other than log a message for the
+ * admin that this is the case, and let them know how to fix things (if they
+ * can).  Ultimately this breakage comes from those kind folks distributing
+ * glibc.  Sigh.
+ */
+static void check_unsupported_algo(const char *user,
+    const char *ciphertxt_pass, size_t ciphertxt_passlen) {
+  FILE *fp = NULL;
+  char fips_enabled[256];
+  size_t len = 0, sz = 0;
+
+  /* First, read in /proc/sys/crypto/fips_enabled. */
+  fp = fopen("/proc/sys/crypto/fips_enabled", "r");
+  if (fp == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "unable to open /proc/sys/crypto/fips_enabled: %s", strerror(errno));
+    return;
+  }
+
+  memset(fips_enabled, '\0', sizeof(fips_enabled));
+  sz = sizeof(fips_enabled)-1;
+  len = fread(fips_enabled, 1, sz, fp);
+  if (len == 0) {
+    if (feof(fp)) {
+      /* An empty /proc/sys/crypto/fips_enabled?  Weird. */
+      pr_trace_msg(trace_channel, 4,
+        "/proc/sys/crypto/fips_enabled is unexpectedly empty!");
+
+    } else if (ferror(fp)) {
+      pr_trace_msg(trace_channel, 4,
+        "error reading /proc/sys/crypto/fips_enabled: %s", strerror(errno));
+    }
+
+    fclose(fp);
+    return;
+  }
+
+  fclose(fp);
+
+  /* Trim any newline. */
+  if (fips_enabled[len-1] == '\n') {
+    fips_enabled[len-1] = '\0';
+  }
+
+  if (strcmp(fips_enabled, "0") != 0) {
+    /* FIPS mode enabled on this system.  If our salt string doesn't start
+     * with a '$', it uses DES; if it starts wit '$1$', it uses MD5.  Either
+     * way, on a FIPS-enabled system, those algorithms aren't supported.
+     */
+    if (ciphertxt_pass[0] != '$') {
+      /* DES */
+      pr_log_pri(PR_LOG_ERR, MOD_AUTH_FILE_VERSION
+        ": AuthUserFile entry for user '%s' uses DES, which is not supported "
+        "on a FIPS-enabled system (see /proc/sys/crypto/fips_enabled)", user);
+      pr_log_pri(PR_LOG_ERR, MOD_AUTH_FILE_VERSION
+        ": recommend updating user '%s' entry to use SHA256/SHA512 "
+        "(using ftpasswd --sha256/--sha512)", user);
+
+    } else if (ciphertxt_passlen >= 3 &&
+               strncmp(ciphertxt_pass, "$1$", 3) == 0) {
+      /* MD5 */
+      pr_log_pri(PR_LOG_ERR, MOD_AUTH_FILE_VERSION
+        ": AuthUserFile entry for user '%s' uses MD5, which is not supported "
+        "on a FIPS-enabled system (see /proc/sys/crypto/fips_enabled)", user);
+      pr_log_pri(PR_LOG_ERR, MOD_AUTH_FILE_VERSION
+        ": recommend updating user '%s' entry to use SHA256/SHA512 "
+        "(using ftpasswd --sha256/--sha512)", user);
+
+    } else {
+      pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION
+        ": possible illegal salt characters in AuthUserFile entry "
+        "for user '%s'?", user);
+    }
+
+  } else {
+    /* The only other time crypt(3) would return EINVAL/EPERM, on a system
+     * with procfs, is if the salt characters were illegal.  Right?
+     */
+    pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION
+      ": possible illegal salt characters in AuthUserFile entry for "
+      "user '%s'?", user);
+  }
+}
+
 MODRET authfile_chkpass(cmd_rec *cmd) {
   const char *ciphertxt_pass = cmd->argv[0];
   const char *cleartxt_pass = cmd->argv[2];
   char *crypted_pass = NULL;
+  size_t ciphertxt_passlen = 0;
+  int xerrno;
 
-  if (!ciphertxt_pass) {
+  if (ciphertxt_pass == NULL) {
     pr_log_debug(DEBUG2, MOD_AUTH_FILE_VERSION
       ": missing ciphertext password for comparison");
     return PR_DECLINED(cmd);
   }
 
-  if (!cleartxt_pass) {
+  if (cleartxt_pass == NULL) {
     pr_log_debug(DEBUG2, MOD_AUTH_FILE_VERSION
       ": missing client-provided password for comparison");
     return PR_DECLINED(cmd);
@@ -1180,18 +1294,32 @@ MODRET authfile_chkpass(cmd_rec *cmd) {
    * Otherwise, it could be checking a password retrieved by some other
    * auth module.
    */
-  if (!af_user_file)
+  if (af_user_file == NULL) {
     return PR_DECLINED(cmd);
+  }
 
   crypted_pass = crypt(cleartxt_pass, ciphertxt_pass);
+  xerrno = errno;
+
+  ciphertxt_passlen = strlen(ciphertxt_pass);
   if (handle_empty_salt == TRUE &&
-      strlen(ciphertxt_pass) == 0) {
+      ciphertxt_passlen == 0) {
     crypted_pass = "";
   }
 
   if (crypted_pass == NULL) {
+    const char *user;
+
+    user = cmd->argv[1];
     pr_log_debug(DEBUG0, MOD_AUTH_FILE_VERSION
-      ": error using crypt(3): %s", strerror(errno));
+      ": error using crypt(3) for user '%s': %s", user, strerror(xerrno));
+
+    if (ciphertxt_passlen > 0 &&
+        (xerrno == EINVAL ||
+         xerrno == EPERM)) {
+      check_unsupported_algo(user, ciphertxt_pass, ciphertxt_passlen);
+    }
+
     return PR_DECLINED(cmd);
   }
 
@@ -1211,6 +1339,7 @@ MODRET set_authgroupfile(cmd_rec *cmd) {
   config_rec *c = NULL;
   authfile_file_t *file = NULL;
   int flags = 0;
+  char *path;
 
 #ifdef PR_USE_REGEX
   if (cmd->argc-1 < 1 ||
@@ -1224,10 +1353,11 @@ MODRET set_authgroupfile(cmd_rec *cmd) {
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (*(cmd->argv[1]) != '/') {
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-      "unable to use relative path for ", cmd->argv[0], " '",
-      cmd->argv[1], "'.", NULL));
+      "unable to use relative path for ", (char *) cmd->argv[0], " '",
+      path, "'.", NULL));
   }
 
   /* Make sure the configured file has the correct permissions.  Note that
@@ -1237,13 +1367,13 @@ MODRET set_authgroupfile(cmd_rec *cmd) {
   flags = PR_AUTH_FILE_FL_ALLOW_WORLD_READABLE;
   if (af_check_file(cmd->tmp_pool, cmd->argv[0], cmd->argv[1], flags) < 0) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-      "unable to use ", cmd->argv[1], ": ", strerror(errno), NULL));
+      "unable to use ", path, ": ", strerror(errno), NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
 
   file = pcalloc(c->pool, sizeof(authfile_file_t));
-  file->af_path = pstrdup(c->pool, cmd->argv[1]);
+  file->af_path = pstrdup(c->pool, path);
   c->argv[0] = (void *) file;
 
   /* Check for restrictions */
@@ -1326,38 +1456,12 @@ MODRET set_authgroupfile(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* Command handlers
- */
-
-MODRET authfile_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-
-    af_user_file = NULL;
-    af_group_file = NULL;
-
-    res = authfile_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&auth_file_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
-  }
-
-  return PR_DECLINED(cmd);
-}
-
-/* Configuration handlers
- */
-
 /* usage: AuthUserFile path [home <regexp>] [id <min-max>] [name <regex>] */
 MODRET set_authuserfile(cmd_rec *cmd) {
   config_rec *c = NULL;
   authfile_file_t *file = NULL;
   int flags = 0;
+  char *path;
 
 #ifdef PR_USE_REGEX
   if (cmd->argc-1 < 1 ||
@@ -1371,10 +1475,11 @@ MODRET set_authuserfile(cmd_rec *cmd) {
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
-  if (*(cmd->argv[1]) != '/') {
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-      "unable to use relative path for ", cmd->argv[0], " '",
-      cmd->argv[1], "'.", NULL));
+      "unable to use relative path for ", (char *) cmd->argv[0], " '",
+      path, "'.", NULL));
   }
 
   /* Make sure the configured file has the correct permissions.  Note that
@@ -1384,13 +1489,13 @@ MODRET set_authuserfile(cmd_rec *cmd) {
   flags = 0;
   if (af_check_file(cmd->tmp_pool, cmd->argv[0], cmd->argv[1], flags) < 0) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-      "unable to use ", cmd->argv[1], ": ", strerror(errno), NULL));
+      "unable to use ", path, ": ", strerror(errno), NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
 
   file = pcalloc(c->pool, sizeof(authfile_file_t));
-  file->af_path = pstrdup(c->pool, cmd->argv[1]);
+  file->af_path = pstrdup(c->pool, path);
   c->argv[0] = (void *) file;
 
   /* Check for restrictions */
@@ -1502,6 +1607,27 @@ MODRET set_authuserfile(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void authfile_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&auth_file_module, "core.session-reinit",
+    authfile_sess_reinit_ev);
+
+  af_user_file = NULL;
+  af_group_file = NULL;
+
+  res = authfile_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&auth_file_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
@@ -1538,6 +1664,9 @@ static int authfile_init(void) {
 static int authfile_sess_init(void) {
   config_rec *c = NULL;
 
+  pr_event_register(&auth_file_module, "core.session-reinit",
+    authfile_sess_reinit_ev, NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "AuthUserFile", FALSE);
   if (c) {
     af_user_file = c->argv[0];
@@ -1560,11 +1689,6 @@ static conftable authfile_conftab[] = {
   { NULL }
 };
 
-static cmdtable authfile_cmdtab[] = {
-  { POST_CMD,	C_HOST,	G_NONE,	authfile_post_host,	FALSE, FALSE },
-  { 0, NULL }
-};
-
 static authtable authfile_authtab[] = {
 
   /* User information callbacks */
@@ -1607,7 +1731,7 @@ module auth_file_module = {
   authfile_conftab,
 
   /* Module command handler table */
-  authfile_cmdtab,
+  NULL,
 
   /* Module authentication handler table */
   authfile_authtab,
diff --git a/modules/mod_auth_pam.c b/modules/mod_auth_pam.c
index 2447e86..565330c 100644
--- a/modules/mod_auth_pam.c
+++ b/modules/mod_auth_pam.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_auth_pam -- Support for PAM-style authentication.
  * Copyright (c) 1998, 1999, 2000 Habeeb J. Dihu aka
  *   MacGyver <macgyver at tos.net>, All Rights Reserved.
- * Copyright 2000-2013 The ProFTPD Project
+ * Copyright 2000-2016 The ProFTPD Project
  *
  * 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
@@ -29,13 +29,8 @@
  * This module should work equally well under all Linux distributions (which
  * have PAM support), as well as Solaris 2.5 and above.
  *
- * If you have any problems, questions, comments, or suggestions regarding
- * this module, please feel free to contact Habeeb J. Dihu aka MacGyver
- * <macgyver at tos.net>.
- *
- * -- DO NOT MODIFY THE TWO LINES BELOW --
+ * -- DO NOT MODIFY THE LINES BELOW --
  * $Libraries: -lpam$
- * $Id: mod_auth_pam.c,v 1.30 2013-10-13 17:34:01 castaglia Exp $
  */
 
 #include "conf.h"
@@ -81,7 +76,7 @@ static int		pam_conv_error		= 0;
 static unsigned long auth_pam_opts = 0UL;
 #define AUTH_PAM_OPT_NO_TTY	0x0001
 
-static const char *trace_channel = "pam";
+static const char *trace_channel = "auth.pam";
 
 /* On non-Solaris systems, the struct pam_message argument is declared
  * const, but on Solaris, it isn't.  To avoid compiler warnings about
@@ -95,16 +90,18 @@ static const char *trace_channel = "pam";
 
 static int pam_exchange(int num_msg, PR_PAM_CONST struct pam_message **msg,
     struct pam_response **resp, void *appdata_ptr) {
-  register unsigned int i = 0, j = 0;
+  register int i = 0, j = 0;
   struct pam_response *response = NULL;
 
-  if (num_msg <= 0)
+  if (num_msg <= 0) {
     return PAM_CONV_ERR;
+  }
 
   response = calloc(num_msg, sizeof(struct pam_response));
 
-  if (response == NULL)
+  if (response == NULL) {
     return PAM_CONV_ERR;
+  }
 
   for (i = 0; i < num_msg; i++) {
     response[i].resp_retcode = 0; /* PAM_SUCCESS; */
@@ -177,12 +174,12 @@ static struct pam_conv pam_conv = {
 };
 
 static void auth_pam_exit_ev(const void *event_data, void *user_data) {
-  int pam_error = 0, disable_id_switching;
+  int res = 0, disable_id_switching;
 
-  /* Sanity check.
-   */
-  if (pamh == NULL)
+  /* Sanity check. */
+  if (pamh == NULL) {
     return;
+  }
 
   /* We need privileges to be able to write to things like lastlog and
    * friends.
@@ -204,26 +201,30 @@ static void auth_pam_exit_ev(const void *event_data, void *user_data) {
    * instance of PAM authentication.
    */
 #ifdef PAM_CRED_DELETE
-  pam_error = pam_setcred(pamh, PAM_CRED_DELETE);
+  res = pam_setcred(pamh, PAM_CRED_DELETE);
 #else
-  pam_error = pam_setcred(pamh, PAM_DELETE_CRED);
+  res = pam_setcred(pamh, PAM_DELETE_CRED);
 #endif /* !PAM_CRED_DELETE */
-  if (pam_error != PAM_SUCCESS) {
+  if (res != PAM_SUCCESS) {
     pr_trace_msg(trace_channel, 1,
-      "error setting PAM_DELETE_CRED credential: %s",
-      pam_strerror(pamh, pam_error));
+      "error setting PAM_DELETE_CRED credential: %s", pam_strerror(pamh, res));
   }
 
-  pam_error = pam_close_session(pamh, PAM_SILENT);
-  if (pam_error != PAM_SUCCESS) {
+  pr_trace_msg(trace_channel, 17, "closing PAM session");
+  res = pam_close_session(pamh, PAM_SILENT);
+  if (res != PAM_SUCCESS) {
     pr_trace_msg(trace_channel, 1, "error closing PAM session: %s",
-      pam_strerror(pamh, pam_error));
+      pam_strerror(pamh, res));
+  }
+
+  pr_trace_msg(trace_channel, 17, "freeing PAM handle");
+  res = pam_end(pamh, 0);
+  if (res != PAM_SUCCESS) {
+    pr_trace_msg(trace_channel, 1, "error freeing PAM handle: %s",
+      pam_strerror(pamh, res));
   }
 
-#ifndef SOLARIS2
-  pam_end(pamh, 0);
   pamh = NULL;
-#endif
 
   PRIVS_RELINQUISH
   pr_signals_unblock();
@@ -237,11 +238,10 @@ static void auth_pam_exit_ev(const void *event_data, void *user_data) {
     pam_user = NULL;
     pam_user_len = 0;
   }
-
 }
 
 MODRET pam_auth(cmd_rec *cmd) {
-  int pam_error = 0, retval = PR_AUTH_ERROR, success = 0;
+  int res = 0, retval = PR_AUTH_ERROR, success = 0;
   config_rec *c = NULL;
   unsigned char *auth_pam = NULL, pam_authoritative = FALSE;
   char ttyentry[32];
@@ -266,15 +266,16 @@ MODRET pam_auth(cmd_rec *cmd) {
     pam_authoritative = TRUE;
   }
 
-  /* Just in case...
-   */
-  if (cmd->argc != 2)
+  /* Just in case... */
+  if (cmd->argc != 2) {
     return pam_authoritative ? PR_ERROR(cmd) : PR_DECLINED(cmd);
+  }
 
-  /* Allocate our entries...we free these up at the end of the authentication.
-   */
-  if ((pam_user_len = strlen(cmd->argv[0]) + 1) > (PAM_MAX_MSG_SIZE + 1))
+  /* Allocate our entries; we free these up at the end of the authentication. */
+  pam_user_len = strlen(cmd->argv[0]) + 1;
+  if (pam_user_len > (PAM_MAX_MSG_SIZE + 1)) {
     pam_user_len = PAM_MAX_MSG_SIZE + 1;
+  }
 
 #ifdef MAXLOGNAME
   /* Some platforms' PAM libraries do not handle login strings that
@@ -282,22 +283,25 @@ MODRET pam_auth(cmd_rec *cmd) {
    */
   if (pam_user_len > MAXLOGNAME) {
     pr_log_pri(PR_LOG_NOTICE,
-      "PAM(%s): Name exceeds maximum login length (%u)", cmd->argv[0],
+      "PAM(%s): Name exceeds maximum login length (%u)", (char *) cmd->argv[0],
       MAXLOGNAME);
     pr_trace_msg(trace_channel, 1,
       "user name '%s' exceeds maximum login length %u, declining",
-      cmd->argv[0], MAXLOGNAME);
+      (char *) cmd->argv[0], MAXLOGNAME);
     return PR_DECLINED(cmd);
   }
 #endif
   pam_user = malloc(pam_user_len);
-  if (pam_user == NULL)
+  if (pam_user == NULL) {
     return pam_authoritative ? PR_ERROR(cmd) : PR_DECLINED(cmd);
+  }
 
   sstrncpy(pam_user, cmd->argv[0], pam_user_len);
 
-  if ((pam_pass_len = strlen(cmd->argv[1]) + 1) > (PAM_MAX_MSG_SIZE + 1))
+  pam_pass_len = strlen(cmd->argv[1]) + 1;
+  if (pam_pass_len > (PAM_MAX_MSG_SIZE + 1)) {
     pam_pass_len = PAM_MAX_MSG_SIZE + 1;
+  }
  
   pam_pass = malloc(pam_pass_len);
   if (pam_pass == NULL) {
@@ -352,18 +356,33 @@ MODRET pam_auth(cmd_rec *cmd) {
    * pam_open_session()
    * pam_setcred()
    */
-  pam_error = pam_start(pamconfig, pam_user, &pam_conv, &pamh);
-  if (pam_error != PAM_SUCCESS)
+  pr_trace_msg(trace_channel, 17, "initializing PAM handle");
+  res = pam_start(pamconfig, pam_user, &pam_conv, &pamh);
+  if (res != PAM_SUCCESS) {
     goto done;
+  }
 
-  pam_set_item(pamh, PAM_RUSER, pam_user);
+  pr_trace_msg(trace_channel, 9, "setting PAM_RUSER to '%s'", pam_user);
+  res = pam_set_item(pamh, PAM_RUSER, pam_user);
+  if (res != PAM_SUCCESS) {
+    pr_trace_msg(trace_channel, 1, "pam_set_item() error for PAM_RUSER: %s",
+      pam_strerror(pamh, res));
+  }
 
-  /* Set our host environment for PAM modules that check host information.
-   */
-  if (session.c != NULL)
-    pam_set_item(pamh, PAM_RHOST, session.c->remote_name);
-  else
-    pam_set_item(pamh, PAM_RHOST, "IHaveNoIdeaHowIGotHere");
+  /* Set our host environment for PAM modules that check host information. */
+  if (session.c != NULL) {
+    pr_trace_msg(trace_channel, 9,
+      "setting PAM_RHOST to '%s'", session.c->remote_name);
+    res = pam_set_item(pamh, PAM_RHOST, session.c->remote_name);
+
+  } else {
+    res = pam_set_item(pamh, PAM_RHOST, "IHaveNoIdeaHowIGotHere");
+  }
+
+  if (res != PAM_SUCCESS) {
+    pr_trace_msg(trace_channel, 1, "pam_set_item() error for PAM_RHOST: %s",
+      pam_strerror(pamh, res));
+  }
 
   if (!(auth_pam_opts & AUTH_PAM_OPT_NO_TTY)) {
     memset(ttyentry, '\0', sizeof(ttyentry));
@@ -372,27 +391,47 @@ MODRET pam_auth(cmd_rec *cmd) {
     ttyentry[sizeof(ttyentry)-1] = '\0';
 
     pr_trace_msg(trace_channel, 9, "setting PAM_TTY to '%s'", ttyentry);
-    pam_set_item(pamh, PAM_TTY, ttyentry);
+    res = pam_set_item(pamh, PAM_TTY, ttyentry);
+    if (res != PAM_SUCCESS) {
+      pr_trace_msg(trace_channel, 1, "pam_set_item() error for PAM_TTY: %s",
+        pam_strerror(pamh, res));
+    }
   }
 
-  /* Authenticate, and get any credentials as needed.
-   */
-  pam_error = pam_authenticate(pamh, PAM_SILENT);
-
-  if (pam_error != PAM_SUCCESS) {
-    switch (pam_error) {
+  /* Authenticate, and get any credentials as needed. */
+  res = pam_authenticate(pamh, PAM_SILENT);
+  if (res != PAM_SUCCESS) {
+    switch (res) {
       case PAM_USER_UNKNOWN:
         retval = PR_AUTH_NOPWD;
         break;
 
+#ifdef PAM_CRED_INSUFFICIENT
+      case PAM_CRED_INSUFFICIENT:
+        retval = PR_AUTH_CRED_INSUFFICIENT;
+        break;
+#endif /* PAM_CRED_INSUFFICIENT */
+
+#ifdef PAM_AUTHINFO_UNAVAIL
+      case PAM_AUTHINFO_UNAVAIL:
+        retval = PR_AUTH_INFO_UNAVAIL;
+        break;
+#endif /* PAM_AUTHINFO_UNAVAIL */
+
+#ifdef PAM_MAXTRIES
+      case PAM_MAXTRIES:
+        retval = PR_AUTH_MAX_ATTEMPTS_EXCEEDED;
+        break;
+#endif /* PAM_MAXTRIES */
+
       default:
         retval = PR_AUTH_BADPWD;
         break;
     }
 
     pr_trace_msg(trace_channel, 1,
-      "authentication error (%d) for user '%s': %s", pam_error, cmd->argv[0],
-      pam_strerror(pamh, pam_error));
+      "authentication error (%d) for user '%s': %s", res, (char *) cmd->argv[0],
+      pam_strerror(pamh, res));
     goto done;
   }
 
@@ -401,10 +440,9 @@ MODRET pam_auth(cmd_rec *cmd) {
     goto done;
   }
 
-  pam_error = pam_acct_mgmt(pamh, PAM_SILENT);
-
-  if (pam_error != PAM_SUCCESS) {
-    switch (pam_error) {
+  res = pam_acct_mgmt(pamh, PAM_SILENT);
+  if (res != PAM_SUCCESS) {
+    switch (res) {
 #ifdef PAM_AUTHTOKEN_REQD
       case PAM_AUTHTOKEN_REQD:
         pr_trace_msg(trace_channel, 8,
@@ -413,6 +451,14 @@ MODRET pam_auth(cmd_rec *cmd) {
         break;
 #endif /* PAM_AUTHTOKEN_REQD */
 
+#ifdef PAM_NEW_AUTHTOKEN_REQD
+      case PAM_NEW_AUTHTOKEN_REQD:
+        pr_trace_msg(trace_channel, 8,
+          "account mgmt error: PAM_NEW_AUTHTOKEN_REQD");
+        retval = PR_AUTH_NEW_TOKEN_REQUIRED;
+        break;
+#endif /* PAM_NEW_AUTHTOKEN_REQD */
+
       case PAM_ACCT_EXPIRED:
         pr_trace_msg(trace_channel, 8, "account mgmt error: PAM_ACCT_EXPIRED");
         retval = PR_AUTH_DISABLEDPWD;
@@ -432,41 +478,61 @@ MODRET pam_auth(cmd_rec *cmd) {
 
       default:
         pr_trace_msg(trace_channel, 8, "account mgmt error: (unknown) [%d]",
-          pam_error);
+          res);
         retval = PR_AUTH_BADPWD;
         break;
     }
 
     pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_PAM_VERSION
-      ": PAM(%s): %s", cmd->argv[0], pam_strerror(pamh, pam_error));
+      ": PAM(%s): %s", (char *) cmd->argv[0], pam_strerror(pamh, res));
     goto done;
   }
 
   /* Open the session. */
-  pam_error = pam_open_session(pamh, PAM_SILENT);
+  pr_trace_msg(trace_channel, 17, "opening PAM session");
+  res = pam_open_session(pamh, PAM_SILENT);
+  if (res != PAM_SUCCESS) {
+    pr_trace_msg(trace_channel, 1,
+      "pam_open_session() failed: %s", pam_strerror(pamh, res));
 
-  if (pam_error != PAM_SUCCESS) {
-    switch (pam_error) {
+    switch (res) {
       case PAM_SESSION_ERR:
+        retval = PR_AUTH_INIT_ERROR;
+        break;
+
       default:
         retval = PR_AUTH_DISABLEDPWD;
         break;
     }
 
     pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_PAM_VERSION
-      ": PAM(%s): %s", cmd->argv[0], pam_strerror(pamh, pam_error));
+      ": PAM(%s): %s", (char *) cmd->argv[0], pam_strerror(pamh, res));
     goto done;
   }
 
   /* Finally, establish credentials. */
 #ifdef PAM_CRED_ESTABLISH
-  pam_error = pam_setcred(pamh, PAM_CRED_ESTABLISH);
+  res = pam_setcred(pamh, PAM_CRED_ESTABLISH);
 #else
-  pam_error = pam_setcred(pamh, PAM_ESTABLISH_CRED);
+  res = pam_setcred(pamh, PAM_ESTABLISH_CRED);
 #endif /* !PAM_CRED_ESTABLISH */
 
-  if (pam_error != PAM_SUCCESS) {
-    switch (pam_error) {
+  if (res != PAM_SUCCESS) {
+    switch (res) {
+#ifdef PAM_CRED_UNAVAIL
+      case PAM_CRED_UNAVAIL:
+        pr_trace_msg(trace_channel, 8, "credentials error: PAM_CRED_UNAVAIL");
+        retval = PR_AUTH_CRED_UNAVAIL;
+        break;
+#endif /* PAM_CRED_UNAVAIL */
+
+#ifdef PAM_CRED_ERR
+      case PAM_CRED_ERR:
+        pr_trace_msg(trace_channel, 8, "credentials error: PAM_CRED_ERR");
+        retval = PR_AUTH_CRED_ERROR;
+        break;
+#endif /* PAM_CRED_ERR */
+
       case PAM_CRED_EXPIRED:
         pr_trace_msg(trace_channel, 8, "credentials error: PAM_CRED_EXPIRED");
         retval = PR_AUTH_AGEPWD;
@@ -479,31 +545,21 @@ MODRET pam_auth(cmd_rec *cmd) {
 
       default:
         pr_trace_msg(trace_channel, 8, "credentials error: (unknown) [%d]",
-          pam_error);
+          res);
         retval = PR_AUTH_BADPWD;
         break;
     }
 
     pr_log_pri(PR_LOG_NOTICE, MOD_AUTH_PAM_VERSION
-      ": PAM(%s): %s", cmd->argv[0], pam_strerror(pamh, pam_error));
+      ": PAM(%s): %s", (char *) cmd->argv[0], pam_strerror(pamh, res));
     goto done;
   }
 
   success++;
 
  done:
-
   /* And we're done.  Clean up and relinquish our root privs.  */
 
-#if defined(SOLARIS2) || defined(HPUX10) || defined(HPUX11)
-  if (success)
-    pam_error = pam_close_session(pamh, 0);
-
-  if (pamh)
-    pam_end(pamh, pam_error);
-  pamh = NULL;
-#endif
-
   if (pam_pass != NULL) {
     pr_memscrub(pam_pass, pam_pass_len);
     free(pam_pass);
@@ -523,12 +579,11 @@ MODRET pam_auth(cmd_rec *cmd) {
     }
 
     return pam_authoritative ? PR_ERROR_INT(cmd, retval) : PR_DECLINED(cmd);
-
-  } else {
-    session.auth_mech = "mod_auth_pam.c";
-    pr_event_register(&auth_pam_module, "core.exit", auth_pam_exit_ev, NULL);
-    return PR_HANDLED(cmd);
   }
+
+  session.auth_mech = "mod_auth_pam.c";
+  pr_event_register(&auth_pam_module, "core.exit", auth_pam_exit_ev, NULL);
+  return PR_HANDLED(cmd);
 }
 
 /* Configuration handlers
@@ -579,7 +634,7 @@ MODRET set_authpamoptions(cmd_rec *cmd) {
 
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown AuthPAMOption: '",
-        cmd->argv[i], "'", NULL));
+        (char *) cmd->argv[i], "'", NULL));
     }
   }
 
diff --git a/modules/mod_auth_unix.c b/modules/mod_auth_unix.c
index 8a4c074..788b4c5 100644
--- a/modules/mod_auth_unix.c
+++ b/modules/mod_auth_unix.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Unix authentication module for ProFTPD
- * $Id: mod_auth_unix.c,v 1.61 2013-10-13 17:34:01 castaglia Exp $
- */
+/* Unix authentication module for ProFTPD */
 
 #include "conf.h"
 
@@ -97,21 +95,20 @@ extern void cygwin_set_impersonation_token (const HANDLE);
 
 #include "privs.h"
 
-static const char *pwdfname = "/etc/passwd";
-static const char *grpfname = "/etc/group";
-
 #ifdef HAVE__PW_STAYOPEN
 extern int _pw_stayopen;
 #endif
 
 module auth_unix_module;
 
-static const char *trace_channel = "auth";
-
+static const char *pwdfname = "/etc/passwd";
 static FILE *pwdf = NULL;
+
+static const char *grpfname = "/etc/group";
 static FILE *grpf = NULL;
 
 static int unix_persistent_passwd = FALSE;
+static const char *trace_channel = "auth.unix";
 
 #undef PASSWD
 #define PASSWD		pwdfname
@@ -131,6 +128,8 @@ static int unix_persistent_passwd = FALSE;
 #define AUTH_UNIX_OPT_AIX_NO_RLOGIN		0x0001
 #define AUTH_UNIX_OPT_NO_GETGROUPLIST		0x0002
 #define AUTH_UNIX_OPT_MAGIC_TOKEN_CHROOT	0x0004
+#define AUTH_UNIX_OPT_NO_INITGROUPS		0x0008
+#define AUTH_UNIX_OPT_AIX_NO_AUTHENTICATE	0x0010
 
 static unsigned long auth_unix_opts = 0UL;
 
@@ -139,7 +138,7 @@ static void auth_unix_exit_ev(const void *, void *);
 static int auth_unix_sess_init(void);
 
 static void p_setpwent(void) {
-  if (pwdf) {
+  if (pwdf != NULL) {
     rewind(pwdf);
 
   } else {
@@ -152,14 +151,14 @@ static void p_setpwent(void) {
 }
 
 static void p_endpwent(void) {
-  if (pwdf) {
+  if (pwdf != NULL) {
     fclose(pwdf);
     pwdf = NULL;
   }
 }
 
 static RETSETGRENTTYPE p_setgrent(void) {
-  if (grpf) {
+  if (grpf != NULL) {
     rewind(grpf);
 
   } else {
@@ -176,44 +175,47 @@ static RETSETGRENTTYPE p_setgrent(void) {
 }
 
 static void p_endgrent(void) {
-  if (grpf) {
+  if (grpf != NULL) {
     fclose(grpf);
     grpf = NULL;
   }
 }
 
 static struct passwd *p_getpwent(void) {
-  if (!pwdf)
+  if (pwdf == NULL) {
     p_setpwent();
+  }
 
-  if (!pwdf)
+  if (pwdf == NULL) {
     return NULL;
+  }
 
   return fgetpwent(pwdf);
 }
 
 static struct group *p_getgrent(void) {
-  struct group *gr = NULL;
-
-  if (!grpf)
+  if (grpf == NULL) {
     p_setgrent();
+  }
 
-  if (!grpf)
+  if (grpf == NULL) {
     return NULL;
+  }
 
-  gr = fgetgrent(grpf);
-
-  return gr;
+  return fgetgrent(grpf);
 }
 
 static struct passwd *p_getpwnam(const char *name) {
   struct passwd *pw = NULL;
+  size_t name_len;
 
   p_setpwent();
+  name_len = strlen(name);
+
   while ((pw = p_getpwent()) != NULL) {
     pr_signals_handle();
 
-    if (strcmp(name, pw->pw_name) == 0) {
+    if (strncmp(name, pw->pw_name, name_len + 1) == 0) {
       break;
     }
   }
@@ -228,8 +230,9 @@ static struct passwd *p_getpwuid(uid_t uid) {
   while ((pw = p_getpwent()) != NULL) {
     pr_signals_handle();
 
-    if (pw->pw_uid == uid)
+    if (pw->pw_uid == uid) {
       break;
+    }
   }
 
   return pw;
@@ -237,13 +240,17 @@ static struct passwd *p_getpwuid(uid_t uid) {
 
 static struct group *p_getgrnam(const char *name) {
   struct group *gr = NULL;
+  size_t name_len;
 
   p_setgrent();
+  name_len = strlen(name);
+
   while ((gr = p_getgrent()) != NULL) {
     pr_signals_handle();
 
-    if (strcmp(name, gr->gr_name) == 0)
+    if (strncmp(name, gr->gr_name, name_len + 1) == 0) {
       break;
+    }
   }
 
   return gr;
@@ -256,8 +263,9 @@ static struct group *p_getgrgid(gid_t gid) {
   while ((gr = p_getgrent()) != NULL) {
     pr_signals_handle();
 
-    if (gr->gr_gid == gid)
+    if (gr->gr_gid == gid) {
       break;
+    }
   }
 
   return gr;
@@ -478,7 +486,7 @@ MODRET pw_getgrgid(cmd_rec *cmd) {
 }
 
 #ifdef PR_USE_SHADOW
-static char *_get_pw_info(pool *p, const char *u, time_t *lstchg, time_t *min,
+static char *get_pwd_info(pool *p, const char *u, time_t *lstchg, time_t *min,
     time_t *max, time_t *warn, time_t *inact, time_t *expire) {
   struct spwd *sp;
   char *cpw = NULL;
@@ -586,7 +594,7 @@ static char *_get_pw_info(pool *p, const char *u, time_t *lstchg, time_t *min,
 
 #else /* PR_USE_SHADOW */
 
-static char *_get_pw_info(pool *p, const char *u, time_t *lstchg, time_t *min,
+static char *get_pwd_info(pool *p, const char *u, time_t *lstchg, time_t *min,
     time_t *max, time_t *warn, time_t *inact, time_t *expire) {
   char *cpw = NULL;
 #if defined(HAVE_GETPRPWENT) || defined(COMSEC)
@@ -705,6 +713,7 @@ static char *_get_pw_info(pool *p, const char *u, time_t *lstchg, time_t *min,
  */
 
 MODRET pw_auth(cmd_rec *cmd) {
+  int res;
   time_t now;
   char *cpw;
   time_t lstchg = -1, max = -1, inact = -1, disable = -1;
@@ -713,15 +722,15 @@ MODRET pw_auth(cmd_rec *cmd) {
   name = cmd->argv[0];
   time(&now);
 
-  cpw = _get_pw_info(cmd->tmp_pool, name, &lstchg, NULL, &max, NULL, &inact,
+  cpw = get_pwd_info(cmd->tmp_pool, name, &lstchg, NULL, &max, NULL, &inact,
     &disable);
-
-  if (!cpw) {
+  if (cpw == NULL) {
     return PR_DECLINED(cmd);
   }
 
-  if (pr_auth_check(cmd->tmp_pool, cpw, cmd->argv[0], cmd->argv[1])) {
-    return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+  res = pr_auth_check(cmd->tmp_pool, cpw, cmd->argv[0], cmd->argv[1]);
+  if (res < PR_AUTH_OK) {
+    return PR_ERROR_INT(cmd, res);
   }
 
   if (lstchg > (time_t) 0 &&
@@ -742,65 +751,67 @@ MODRET pw_auth(cmd_rec *cmd) {
 }
 
 MODRET pw_authz(cmd_rec *cmd) {
-
-#ifdef HAVE_LOGINRESTRICTIONS
-  int code = 0, mode = S_RLOGIN;
-  char *reason = NULL;
-#endif
-
   /* XXX Any other implementations here? */
 
 #ifdef HAVE_LOGINRESTRICTIONS
+  if (!(auth_unix_opts & AUTH_UNIX_OPT_AIX_NO_RLOGIN)) {
+    int res, xerrno, code = 0;
+    char *user = NULL, *reason = NULL;
 
-  if (auth_unix_opts & AUTH_UNIX_OPT_AIX_NO_RLOGIN) {
-    mode = 0;
-  }
+    user = cmd->argv[0];
 
-  /* Check for account login restrictions and such using AIX-specific
-   * functions.
-   */
-  PRIVS_ROOT
-  if (loginrestrictions(cmd->argv[0], mode, NULL, &reason) != 0) {
+    /* Check for account login restrictions and such using AIX-specific
+     * functions.
+     */
+    PRIVS_ROOT
+    res = loginrestrictions(user, S_RLOGIN, NULL, &reason);
+    xerrno = errno;
     PRIVS_RELINQUISH
 
-    if (reason &&
-        *reason) {
-      pr_log_auth(LOG_WARNING, "login restricted for user '%s': %.100s",
-        cmd->argv[0], reason);
-    }
-
-    pr_log_debug(DEBUG2, "AIX loginrestrictions() failed for user '%s': %s",
-      cmd->argv[0], strerror(errno));
-
-    return PR_ERROR_INT(cmd, PR_AUTH_DISABLEDPWD);
-  }
+    if (res != 0) {
+      if (reason != NULL &&
+          *reason) {
+        pr_trace_msg(trace_channel, 9,
+          "AIX loginrestrictions() failed for user '%s': %.100s", user, reason);
+        pr_log_auth(LOG_WARNING, "login restricted for user '%s': %.100s",
+          user, reason);
+      }
 
-  code = passwdexpired(cmd->argv[0], &reason);
-  PRIVS_RELINQUISH
+      pr_log_auth(LOG_NOTICE,
+        "AIX loginrestrictions() failed for user '%s': %s", user,
+        strerror(xerrno));
 
-  switch (code) {
-    case 0:
-      /* Password not expired for user */
-      break;
+      return PR_ERROR_INT(cmd, PR_AUTH_DISABLEDPWD);
+    }
 
-    case 1:
-      /* Password expired and needs to be changed */
-      pr_log_auth(LOG_WARNING, "password expired for user '%s': %.100s",
-        cmd->argv[0], reason);
-      return PR_ERROR_INT(cmd, PR_AUTH_AGEPWD);
+    PRIVS_ROOT
+    code = passwdexpired(user, &reason);
+    PRIVS_RELINQUISH
 
-    case 2:
-      /* Password expired, requires sysadmin to change it */
-      pr_log_auth(LOG_WARNING,
-        "password expired for user '%s', requires sysadmin intervention: "
-        "%.100s", cmd->argv[0], reason);
-      return PR_ERROR_INT(cmd, PR_AUTH_AGEPWD);
+    switch (code) {
+      case 0:
+        /* Password not expired for user */
+        break;
 
-    default:
-      /* Other error */
-      pr_log_auth(LOG_WARNING, "AIX passwdexpired() failed for user '%s': "
-        "%.100s", cmd->argv[0], reason);
-      return PR_ERROR_INT(cmd, PR_AUTH_DISABLEDPWD);
+      case 1:
+        /* Password expired and needs to be changed */
+        pr_log_auth(LOG_WARNING, "password expired for user '%s': %.100s",
+          cmd->argv[0], reason);
+        return PR_ERROR_INT(cmd, PR_AUTH_AGEPWD);
+
+      case 2:
+        /* Password expired, requires sysadmin to change it */
+        pr_log_auth(LOG_WARNING,
+          "password expired for user '%s', requires sysadmin intervention: "
+          "%.100s", user, reason);
+        return PR_ERROR_INT(cmd, PR_AUTH_AGEPWD);
+
+      default:
+        /* Other error */
+        pr_log_auth(LOG_WARNING, "AIX passwdexpired() failed for user '%s': "
+          "%.100s", user, reason);
+        return PR_ERROR_INT(cmd, PR_AUTH_DISABLEDPWD);
+    }
   }
 #endif /* !HAVE_LOGINRESTRICTIONS */
 
@@ -930,12 +941,56 @@ MODRET pw_check(cmd_rec *cmd) {
   } else {
 # endif /* CYGWIN */
 
+#ifdef HAVE_AUTHENTICATE
+  if (!(auth_unix_opts & AUTH_UNIX_OPT_AIX_NO_AUTHENTICATE)) {
+    int res, xerrno, reenter = 0;
+    char *user, *passwd, *msg = NULL;
+
+    user = cmd->argv[1];
+    passwd = cmd->argv[2];
+
+    pr_trace_msg(trace_channel, 9, "calling AIX authenticate() for user '%s'",
+      user);
+
+    PRIVS_ROOT
+    do {
+      res = authenticate(user, passwd, &reenter, &msg);
+      xerrno = errno;
+
+      pr_trace_msg(trace_channel, 9,
+        "AIX authenticate result: %d (msg '%.100s')", res, msg);
+
+    } while (reenter != 0);
+    PRIVS_RELINQUISH
+
+    /* AIX indicates failure with a return value of 1. */
+    if (res != 0) {
+      pr_log_auth(LOG_WARNING,
+       "AIX authenticate failed for user '%s': %.100s", user, msg);
+
+      if (xerrno == ENOENT) {
+        return PR_ERROR_INT(cmd, PR_AUTH_NOPWD);
+      }
+
+      return PR_ERROR_INT(cmd, PR_AUTH_DISABLEDPWD);
+    }
+  }
+#endif /* HAVE_AUTHENTICATE */
+
   /* Call pw_authz here, to make sure the user is authorized to login. */
 
-  if (cmd2 == NULL)
+  if (cmd2 == NULL) {
     cmd2 = pr_cmd_alloc(cmd->tmp_pool, 1, cmd->argv[1]);
+  }
 
   mr = pw_authz(cmd2);
+  if (MODRET_ISERROR(mr)) {
+    int err_code;
+
+    err_code = MODRET_ERROR(mr);
+    return PR_ERROR_INT(cmd, err_code);
+  }
+
   if (MODRET_ISDECLINED(mr)) {
     return PR_DECLINED(cmd);
   }
@@ -1035,45 +1090,104 @@ MODRET pw_name2gid(cmd_rec *cmd) {
   return gr ? mod_create_data(cmd, (void *) &gr->gr_gid) : PR_DECLINED(cmd);
 }
 
-/* cmd->argv[0] = name
- * cmd->argv[1] = (array_header **) group_ids
- * cmd->argv[2] = (array_header **) group_names
- */
+static int get_groups_by_getgrset(const char *user, gid_t primary_gid,
+    array_header *gids, array_header *groups,
+    struct group *(*my_getgrgid)(gid_t)) {
+  int res;
+#ifdef HAVE_GETGRSET
+  gid_t group_ids[NGROUPS_MAX];
+  unsigned int ngroups = 0;
+  register unsigned int i;
+  char *grgid, *grouplist, *ptr;
 
-MODRET pw_getgroups(cmd_rec *cmd) {
-  struct passwd *pw = NULL;
-  struct group *gr = NULL;
-  array_header *gids = NULL, *groups = NULL;
-  char *name = NULL;
-  int use_getgrouplist = FALSE;
+  pr_trace_msg("auth", 4,
+    "using getgrset(3) to look up group membership");
 
-  /* Function pointers for which lookup functions to use */
-  struct passwd *(*my_getpwnam)(const char *) = NULL;
-  struct group *(*my_getgrgid)(gid_t) = NULL;
-  struct group *(*my_getgrent)(void) = NULL;
-  RETSETGRENTTYPE (*my_setgrent)(void) = NULL;
+  grouplist = getgrset(user);
+  if (grouplist == NULL) {
+    int xerrno = errno;
 
-  /* Play function pointer games */
-  if (unix_persistent_passwd) {
-    my_getpwnam = p_getpwnam;
-    my_getgrgid = p_getgrgid;
-    my_getgrent = p_getgrent;
-    my_setgrent = p_setgrent;
+    pr_log_pri(PR_LOG_WARNING, "getgrset(3) error: %s", strerror(xerrno));
 
-  } else {
-    my_getpwnam = getpwnam;
-    my_getgrgid = getgrgid;
-    my_getgrent = getgrent;
-    my_setgrent = setgrent;
+    errno = xerrno;
+    return -1;
+  }
+
+  ptr = grouplist;
+  memset(group_ids, 0, sizeof(group_ids));
+
+  /* The getgrset(3) function returns a string which is a comma-delimited
+   * list of group IDs.
+   */
+  grgid = strsep(&grouplist, ",");
+  while (grgid != NULL) {
+    gid_t gid;
+
+    pr_signals_handle();
+
+    if (ngroups >= sizeof(group_ids)) {
+      /* Reached capacity of the group_ids array. */
+      break;
+    }
+
+    pr_str2gid(grgid, &gid);
+
+    /* Skip the primary group. */
+    if (gid == primary_gid) {
+      grgid = strsep(&grouplist, ",");
+      continue;
+    }
+
+    group_ids[ngroups] = gid;
+    ngroups++;
+
+    grgid = strsep(&grouplist, ",");
+  }
+
+  for (i = 0; i < ngroups; i++) {
+    struct group *gr;
+
+    gr = my_getgrgid(group_ids[i]);
+    if (gr != NULL) {
+      if (gids != NULL &&
+          primary_gid != gr->gr_gid) {
+        *((gid_t *) push_array(gids)) = gr->gr_gid;
+      }
+
+      if (groups != NULL &&
+          primary_gid != gr->gr_gid) {
+        *((char **) push_array(groups)) = pstrdup(session.pool,
+          gr->gr_name);
+      }
+    }
   }
 
+  free(ptr);
+  res = 0;
+
+#else
+  errno = ENOSYS;
+  res = -1;
+#endif /* HAVE_GETGRSET */
+
+  return res;
+}
+
+static int get_groups_by_getgrouplist(const char *user, gid_t primary_gid,
+    array_header *gids, array_header *groups,
+    struct group *(*my_getgrgid)(gid_t)) {
+  int res;
 #ifdef HAVE_GETGROUPLIST
+  int use_getgrouplist = TRUE;
+  gid_t group_ids[NGROUPS_MAX];
+  int ngroups = NGROUPS_MAX;
+  register int i;
+
   /* Determine whether to use getgrouplist(3), if available.  Older glibc
    * versions (i.e. 2.2.4 and older) had buggy getgrouplist() implementations
    * which allowed for buffer overflows (see CVS-2003-0689); do not use
    * getgrouplist() on such glibc versions.
    */
-  use_getgrouplist = TRUE;
 
 # if defined(__GLIBC__) && \
      defined(__GLIBC_MINOR__) && \
@@ -1081,198 +1195,285 @@ MODRET pw_getgroups(cmd_rec *cmd) {
      __GLIBC_MINOR__ < 3
   use_getgrouplist = FALSE;
 # endif
-#endif /* !HAVE_GETGROUPLIST */
 
-  /* Use of getgrouplist(3) might have been disabled via the "noGetgrouplist"
+  /* Use of getgrouplist(3) might have been disabled via the "NoGetgrouplist"
    * AuthUnixOption as well.
    */
   if (auth_unix_opts & AUTH_UNIX_OPT_NO_GETGROUPLIST) {
     use_getgrouplist = FALSE;
   }
 
-  name = (char *) cmd->argv[0];
+  if (use_getgrouplist == FALSE) {
+    errno = ENOSYS;
+    return -1;
+  }
 
-  /* Check for NULL values. */
-  if (cmd->argv[1])
-    gids = (array_header *) cmd->argv[1];
+  pr_trace_msg("auth", 4,
+    "using getgrouplist(3) to look up group membership");
 
-  if (cmd->argv[2])
-    groups = (array_header *) cmd->argv[2];
+  memset(group_ids, 0, sizeof(group_ids));
+  if (getgrouplist(user, primary_gid, group_ids, &ngroups) < 0) {
+    int xerrno = errno;
 
-  /* Retrieve the necessary info. */
-  if (!name || !(pw = my_getpwnam(name)))
-    return PR_DECLINED(cmd);
+    pr_log_pri(PR_LOG_WARNING, "getgrouplist(3) error: %s", strerror(xerrno));
 
-  /* Populate the first group ID and name. */
-  if (gids)
-    *((gid_t *) push_array(gids)) = pw->pw_gid;
+    errno = xerrno;
+    return -1;
+  }
 
-  if (groups && (gr = my_getgrgid(pw->pw_gid)) != NULL)
-    *((char **) push_array(groups)) = pstrdup(session.pool, gr->gr_name);
+  for (i = 0; i < ngroups; i++) {
+    struct group *gr;
 
-  my_setgrent();
+    gr = my_getgrgid(group_ids[i]);
+    if (gr != NULL) {
+      if (gids != NULL &&
+          primary_gid != gr->gr_gid) {
+        *((gid_t *) push_array(gids)) = gr->gr_gid;
+      }
 
-  if (use_getgrouplist) {
-#ifdef HAVE_GETGROUPLIST
-    gid_t group_ids[NGROUPS_MAX];
-    int ngroups = NGROUPS_MAX;
-    register unsigned int i;
+      if (groups != NULL &&
+          primary_gid != gr->gr_gid) {
+        *((char **) push_array(groups)) = pstrdup(session.pool,
+          gr->gr_name);
+      }
+    }
+  }
 
-    pr_trace_msg("auth", 4,
-      "using getgrouplist(3) to look up group membership");
+  res = 0;
+#else
+  errno = ENOSYS;
+  res = -1;
+#endif /* HAVE_GETGROUPLIST */
 
-    memset(group_ids, 0, sizeof(group_ids));
-    if (getgrouplist(pw->pw_name, pw->pw_gid, group_ids, &ngroups) < 0) {
-      int xerrno = errno;
+  return res;
+}
 
-      pr_log_pri(PR_LOG_WARNING, "getgrouplist error: %s", strerror(xerrno));
+static int get_groups_by_getgrent(const char *user, gid_t primary_gid,
+    array_header *gids, array_header *groups,
+    struct group *(*my_getgrent)(void)) {
+  struct group *gr;
+  size_t user_len;
 
-      errno = xerrno;
-      return PR_DECLINED(cmd);
-    }
+  /* This is where things get slow, expensive, and ugly.  Loop through
+   * everything, checking to make sure we haven't already added it.
+   */
+  user_len = strlen(user);
+  while ((gr = my_getgrent()) != NULL &&
+         gr->gr_mem != NULL) {
+    char **gr_member = NULL;
+
+    pr_signals_handle();
+
+    /* Loop through each member name listed */
+    for (gr_member = gr->gr_mem; *gr_member; gr_member++) {
 
-    for (i = 0; i < ngroups; i++) {
-      gr = my_getgrgid(group_ids[i]);
-      if (gr) {
-        if (gids && pw->pw_gid != gr->gr_gid)
+     /* If it matches the given username... */
+      if (strncmp(*gr_member, user, user_len + 1) == 0) {
+
+        if (gids != NULL &&
+            primary_gid != gr->gr_gid) {
           *((gid_t *) push_array(gids)) = gr->gr_gid;
+        }
 
-        if (groups && pw->pw_gid != gr->gr_gid) {
+        if (groups != NULL &&
+            primary_gid != gr->gr_gid) {
           *((char **) push_array(groups)) = pstrdup(session.pool,
             gr->gr_name);
         }
       }
     }
-#endif /* !HAVE_GETGROUPLIST */
-
-  } else {
-#ifdef HAVE_GETGRSET
-    gid_t group_ids[NGROUPS_MAX];
-    unsigned int ngroups = 0;
-    register unsigned int i;
-    char *grgid, *grouplist, *ptr;
+  }
 
-    pr_trace_msg("auth", 4,
-      "using getgrset(3) to look up group membership");
+  return 0;
+}
 
-    grouplist = getgrset(pw->pw_name);
-    if (grouplist == NULL) {
-      int xerrno = errno;
+static int get_groups_by_initgroups(const char *user, gid_t primary_gid,
+    array_header *gids, array_header *groups,
+    struct group *(*my_getgrgid)(gid_t)) {
+  int res;
+#if defined(HAVE_INITGROUPS) && defined(HAVE_GETGROUPS)
+  gid_t group_ids[NGROUPS_MAX+1];
+  int ngroups, use_initgroups = TRUE, xerrno;
+  register int i;
+
+  /* On Mac OSX, the getgroups(2) man page has this unsettling tidbit:
+   *
+   *  Calling initgroups(3) to opt-in for supplementary groups will cause
+   *  getgroups() to return a single entry, the GID that was passed to
+   *  initgroups(3).
+   *
+   * But in our case, we WANT all of those groups.  Thus on Mac OSX, we
+   * will skip the use of initgroups(3) in favor of other mechanisms
+   * (e.g. getgrouplist(3)).
+   */
+# if defined(DARWIN10) || \
+     defined(DARWIN11) || \
+     defined(DARWIN12) || \
+     defined(DARWIN13) || \
+     defined(DARWIN14) || \
+     defined(DARWIN15)
+  use_initgroups = FALSE;
+# endif /* Mac OSX */
+
+  /* Use of initgroups(3) might have been disabled via the "NoInitgroups"
+   * AuthUnixOption as well.
+   */
+  if (auth_unix_opts & AUTH_UNIX_OPT_NO_INITGROUPS) {
+    use_initgroups = FALSE;
+  }
 
-      pr_log_pri(PR_LOG_WARNING, "getgrset error: %s", strerror(xerrno));
+  /* If we are not root, then initgroups(3) will most likely fail. */
+  if (geteuid() != PR_ROOT_UID) {
+    use_initgroups = FALSE;
+  }
 
-      errno = xerrno;
-      return PR_DECLINED(cmd);
-    }
+  if (use_initgroups == FALSE) {
+    errno = ENOSYS;
+    return -1;
+  }
 
-    ptr = grouplist;
-    memset(group_ids, 0, sizeof(group_ids));
+  pr_trace_msg("auth", 4,
+    "using initgroups(3) to look up group membership");
 
-    /* The getgrset(3) function returns a string which is a comma-delimited
-     * list of group IDs.
-     */
-    grgid = strsep(&grouplist, ",");
-    while (grgid) {
-      long gid;
+  PRIVS_ROOT
+  res = initgroups(user, primary_gid);
+  xerrno = errno;
+  PRIVS_RELINQUISH
 
-      pr_signals_handle();
+  if (res < 0) {
+    pr_log_pri(PR_LOG_WARNING, "initgroups(3) error: %s", strerror(xerrno));
 
-      if (ngroups >= sizeof(group_ids)) {
-        /* Reached capacity of the group_ids array. */
-        break;
-      }
+    errno = xerrno;
+    return -1;
+  }
 
-      /* XXX Should we use strtoul(3) or even strtoull(3) here? */
-      gid = strtol(grgid, NULL, 10);
+  ngroups = getgroups(NGROUPS_MAX+1, group_ids);
+  if (ngroups < 0) {
+    xerrno = errno;
 
-      /* Skip the primary group. */
-      if ((gid_t) gid == pw->pw_gid) {
-        grgid = strsep(&grouplist, ",");
-        continue;
-      }
+    pr_log_pri(PR_LOG_WARNING, "getgroups(2) error: %s", strerror(xerrno));
 
-      group_ids[ngroups] = (gid_t) gid;
-      ngroups++;
+    errno = xerrno;
+    return -1;
+  }
 
-      grgid = strsep(&grouplist, ",");
-    }
+  for (i = 0; i < ngroups; i++) {
+    struct group *gr;
 
-    for (i = 0; i < ngroups; i++) {
-      gr = my_getgrgid(group_ids[i]);
-      if (gr) {
-        if (gids && pw->pw_gid != gr->gr_gid)
-          *((gid_t *) push_array(gids)) = gr->gr_gid;
+    gr = my_getgrgid(group_ids[i]);
+    if (gr != NULL) {
+      if (gids != NULL &&
+          primary_gid != gr->gr_gid) {
+        *((gid_t *) push_array(gids)) = gr->gr_gid;
+      }
 
-        if (groups && pw->pw_gid != gr->gr_gid) {
-          *((char **) push_array(groups)) = pstrdup(session.pool,
-            gr->gr_name);
-        }
+      if (groups != NULL &&
+          primary_gid != gr->gr_gid) {
+        *((char **) push_array(groups)) = pstrdup(session.pool,
+          gr->gr_name);
       }
     }
+  }
+
+  res = 0;
 
-    free(ptr);
 #else
-    char **gr_member = NULL;
-    size_t pw_namelen;
+  errno = ENOSYS;
+  res = -1;
+#endif /* HAVE_INITGROUPS and HAVE_GETGROUPS */
 
-    /* This is where things get slow, expensive, and ugly.  Loop through
-     * everything, checking to make sure we haven't already added it.
-     */
-    pw_namelen = strlen(pw->pw_name);
-    while ((gr = my_getgrent()) != NULL && gr->gr_mem) {
-      pr_signals_handle();
+  return res;
+}
 
-      /* Loop through each member name listed */
-      for (gr_member = gr->gr_mem; *gr_member; gr_member++) {
+/* cmd->argv[0] = name
+ * cmd->argv[1] = (array_header **) group_ids
+ * cmd->argv[2] = (array_header **) group_names
+ */
+MODRET pw_getgroups(cmd_rec *cmd) {
+  int res;
+  struct passwd *pw = NULL;
+  struct group *gr = NULL;
+  array_header *gids = NULL, *groups = NULL;
+  const char *name = NULL;
 
-        /* If it matches the given username... */
-        if (strncmp(*gr_member, pw->pw_name, pw_namelen + 1) == 0) {
+  /* Function pointers for which lookup functions to use */
+  struct passwd *(*my_getpwnam)(const char *) = NULL;
+  struct group *(*my_getgrgid)(gid_t) = NULL;
+  struct group *(*my_getgrent)(void) = NULL;
+  RETSETGRENTTYPE (*my_setgrent)(void) = NULL;
 
-          /* ...add the GID and name */
-          if (gids)
-            *((gid_t *) push_array(gids)) = gr->gr_gid;
+  /* Play function pointer games */
+  if (unix_persistent_passwd) {
+    my_getpwnam = p_getpwnam;
+    my_getgrgid = p_getgrgid;
+    my_getgrent = p_getgrent;
+    my_setgrent = p_setgrent;
 
-          if (groups && pw->pw_gid != gr->gr_gid) {
-            *((char **) push_array(groups)) = pstrdup(session.pool,
-              gr->gr_name);
-          }
-        }
-      }
-    }
-#endif /* !HAVE_GETGRSET */
+  } else {
+    my_getpwnam = getpwnam;
+    my_getgrgid = getgrgid;
+    my_getgrent = getgrent;
+    my_setgrent = setgrent;
   }
 
-  if (gids &&
-      gids->nelts > 0) {
-    return mod_create_data(cmd, (void *) &gids->nelts);
+  name = cmd->argv[0];
 
-  } else if (groups &&
-             groups->nelts > 0) {
-    return mod_create_data(cmd, (void *) &groups->nelts);
+  if (cmd->argv[1] != NULL) {
+    gids = (array_header *) cmd->argv[1];
   }
 
-  return PR_DECLINED(cmd);
-}
+  if (cmd->argv[2] != NULL) {
+    groups = (array_header *) cmd->argv[2];
+  }
 
-/* Command handlers
- */
+  /* Retrieve the necessary info. */
+  if (name == NULL ||
+      !(pw = my_getpwnam(name))) {
+    return PR_DECLINED(cmd);
+  }
 
-MODRET auth_unix_post_host(cmd_rec *cmd) {
+  /* Populate the first group ID and name. */
+  if (gids != NULL) {
+    *((gid_t *) push_array(gids)) = pw->pw_gid;
+  }
 
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
+  if (groups != NULL &&
+      (gr = my_getgrgid(pw->pw_gid)) != NULL) {
+    *((char **) push_array(groups)) = pstrdup(session.pool, gr->gr_name);
+  }
 
-    pr_event_unregister(&auth_unix_module, "core.exit", auth_unix_exit_ev);
-    auth_unix_opts = 0UL;
+  my_setgrent();
 
-    res = auth_unix_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&auth_unix_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
+  /* Myriad are the ways of obtaining the group membership of a user. */
+
+  res = get_groups_by_initgroups(name, pw->pw_gid, gids, groups, my_getgrgid);
+  if (res < 0 &&
+      errno == ENOSYS) {
+    res = get_groups_by_getgrouplist(name, pw->pw_gid, gids, groups,
+      my_getgrgid);
+  }
+
+  if (res < 0 &&
+      errno == ENOSYS) {
+    res = get_groups_by_getgrset(name, pw->pw_gid, gids, groups, my_getgrgid);
+  }
+
+  if (res < 0 &&
+      errno == ENOSYS) {
+    res = get_groups_by_getgrent(name, pw->pw_gid, gids, groups, my_getgrent);
+  }
+
+  if (res < 0) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (gids != NULL &&
+      gids->nelts > 0) {
+    return mod_create_data(cmd, (void *) &gids->nelts);
+
+  } else if (groups != NULL &&
+             groups->nelts > 0) {
+    return mod_create_data(cmd, (void *) &groups->nelts);
   }
 
   return PR_DECLINED(cmd);
@@ -1296,15 +1497,21 @@ MODRET set_authunixoptions(cmd_rec *cmd) {
   c = add_config_param(cmd->argv[0], 1, NULL);
 
   for (i = 1; i < cmd->argc; i++) {
-    if (strcmp(cmd->argv[i], "aixNoRLogin") == 0) {
+    if (strcasecmp(cmd->argv[i], "AIXNoRLogin") == 0) {
       opts |= AUTH_UNIX_OPT_AIX_NO_RLOGIN;
 
-    } else if (strcmp(cmd->argv[i], "noGetgrouplist") == 0) {
+    } else if (strcasecmp(cmd->argv[i], "NoGetgrouplist") == 0) {
       opts |= AUTH_UNIX_OPT_NO_GETGROUPLIST;
 
-    } else if (strcmp(cmd->argv[i], "magicTokenChroot") == 0) {
+    } else if (strcasecmp(cmd->argv[i], "NoInitgroups") == 0) {
+      opts |= AUTH_UNIX_OPT_NO_INITGROUPS;
+
+    } else if (strcasecmp(cmd->argv[i], "MagicTokenChroot") == 0) {
       opts |= AUTH_UNIX_OPT_MAGIC_TOKEN_CHROOT;
 
+    } else if (strcasecmp(cmd->argv[i], "AIXNoAuthenticate") == 0) {
+      opts |= AUTH_UNIX_OPT_AIX_NO_AUTHENTICATE;
+
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown AuthUnixOption '",
         cmd->argv[i], "'", NULL));
@@ -1342,8 +1549,24 @@ MODRET set_persistentpasswd(cmd_rec *cmd) {
 static void auth_unix_exit_ev(const void *event_data, void *user_data) {
   pr_auth_endpwent(session.pool);
   pr_auth_endgrent(session.pool);
+}
+
+static void auth_unix_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
 
-  return;
+  pr_event_unregister(&auth_unix_module, "core.exit", auth_unix_exit_ev);
+  pr_event_unregister(&auth_unix_module, "core.session-reinit",
+    auth_unix_sess_reinit_ev);
+  auth_unix_opts = 0UL;
+  unix_persistent_passwd = FALSE;
+
+  res = auth_unix_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&auth_unix_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
 }
 
 /* Initialization routines
@@ -1362,6 +1585,8 @@ static int auth_unix_sess_init(void) {
   config_rec *c;
 
   pr_event_register(&auth_unix_module, "core.exit", auth_unix_exit_ev, NULL);
+  pr_event_register(&auth_unix_module, "core.session-reinit",
+    auth_unix_sess_reinit_ev, NULL);
 
   c = find_config(main_server->conf, CONF_PARAM, "AuthUnixOptions", FALSE);
   if (c) {
@@ -1385,11 +1610,6 @@ static conftable auth_unix_conftab[] = {
   { NULL,			NULL,				NULL }
 };
 
-static cmdtable auth_unix_cmdtab[] = {
-  { POST_CMD,	C_HOST,	G_NONE,	auth_unix_post_host,	FALSE, FALSE },
-  { 0, NULL }
-};
-
 static authtable auth_unix_authtab[] = {
   { 0,  "setpwent",	pw_setpwent },
   { 0,  "endpwent",	pw_endpwent },
@@ -1425,7 +1645,7 @@ module auth_unix_module = {
   auth_unix_conftab,
 
   /* Module command handler table */
-  auth_unix_cmdtab,
+  NULL,
 
   /* Module authentication handler table */
   auth_unix_authtab,
diff --git a/modules/mod_cap.c b/modules/mod_cap.c
index 0ca6c50..9dc2b3a 100644
--- a/modules/mod_cap.c
+++ b/modules/mod_cap.c
@@ -29,10 +29,9 @@
  * recommended for security-consious admins. See README.capabilities for more
  * information.
  *
- * -- DO NOT MODIFY THE TWO LINES BELOW --
+ * ----- DO NOT MODIFY THE TWO LINES BELOW -----
  * $Libraries: -L$(top_srcdir)/lib/libcap -lcap$
  * $Directories: $(top_srcdir)/lib/libcap$
- * $Id: mod_cap.c,v 1.35 2013-10-13 17:34:01 castaglia Exp $
  */
 
 #include <stdio.h>
@@ -109,7 +108,7 @@ static void lp_debug(void) {
   }
 
   pr_log_debug(DEBUG1, MOD_CAP_VERSION ": capabilities '%s'", res);
-  cap_free(res);
+  (void) cap_free(res);
 
   if (cap_free(caps) < 0) {
     pr_log_pri(PR_LOG_NOTICE, MOD_CAP_VERSION
@@ -191,8 +190,9 @@ MODRET set_caps(cmd_rec *cmd) {
   config_rec *c = NULL;
   register unsigned int i = 0;
 
-  if (cmd->argc - 1 < 1)
+  if (cmd->argc - 1 < 1) {
     CONF_ERROR(cmd, "need at least one parameter");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
@@ -200,40 +200,50 @@ MODRET set_caps(cmd_rec *cmd) {
   flags |= (CAP_USE_CHOWN|CAP_USE_SETUID);
 
   for (i = 1; i < cmd->argc; i++) {
-    char *cp = cmd->argv[i];
-    cp++;
+    char *cap, *ptr;
+
+    cap = ptr = cmd->argv[i];
+    ptr++;
 
-    if (*cmd->argv[i] != '+' && *cmd->argv[i] != '-')
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": bad option: '",
-        cmd->argv[i], "'", NULL));
+    if (*cap != '+' &&
+        *cap != '-') {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": bad option: '", cap, "'",
+        NULL));
+    }
 
-    if (strcasecmp(cp, "CAP_CHOWN") == 0) {
-      if (*cmd->argv[i] == '-')
+    if (strcasecmp(ptr, "CAP_CHOWN") == 0) {
+      if (*cap == '-') {
         flags &= ~CAP_USE_CHOWN;
+      }
 
-    } else if (strcasecmp(cp, "CAP_DAC_OVERRIDE") == 0) {
-      if (*cmd->argv[i] == '+')
+    } else if (strcasecmp(ptr, "CAP_DAC_OVERRIDE") == 0) {
+      if (*cap == '+') {
         flags |= CAP_USE_DAC_OVERRIDE;
+      }
 
-    } else if (strcasecmp(cp, "CAP_DAC_READ_SEARCH") == 0) {
-      if (*cmd->argv[i] == '+')
+    } else if (strcasecmp(ptr, "CAP_DAC_READ_SEARCH") == 0) {
+      if (*cap == '+') {
         flags |= CAP_USE_DAC_READ_SEARCH;
+      }
 
-    } else if (strcasecmp(cp, "CAP_FOWNER") == 0) {
-      if (*cmd->argv[i] == '+')
+    } else if (strcasecmp(ptr, "CAP_FOWNER") == 0) {
+      if (*cap == '+') {
         flags |= CAP_USE_FOWNER;
+      }
 
-    } else if (strcasecmp(cp, "CAP_FSETID") == 0) {
-      if (*cmd->argv[i] == '+')
+    } else if (strcasecmp(ptr, "CAP_FSETID") == 0) {
+      if (*cap == '+') {
         flags |= CAP_USE_FSETID;
+      }
 
-    } else if (strcasecmp(cp, "CAP_SETUID") == 0) {
-      if (*cmd->argv[i] == '-')
+    } else if (strcasecmp(ptr, "CAP_SETUID") == 0) {
+      if (*cap == '-') {
         flags &= ~CAP_USE_SETUID;
+      }
 
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown capability: '",
-        cp, "'", NULL));
+        ptr, "'", NULL));
     }
   }
 
@@ -270,28 +280,6 @@ MODRET set_capengine(cmd_rec *cmd) {
 /* Command handlers
  */
 
-MODRET cap_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-
-    have_capabilities = FALSE;
-    use_capabilities = TRUE;
-    cap_flags = 0;
-
-    res = cap_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&cap_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
-  }
-
-  return PR_DECLINED(cmd);
-}
-
 /* The POST_CMD handler for "PASS" is only called after PASS has
  * successfully completed, which means authentication is successful,
  * so we can "tweak" our root access down to almost nothing.
@@ -370,8 +358,9 @@ MODRET cap_post_pass(cmd_rec *cmd) {
      */
     if (strncmp(proto, "ssh2", 5) != 0 ||
         xerrno != EPERM) {
-      pr_log_pri(PR_LOG_ERR, MOD_CAP_VERSION ": setreuid(%lu, %lu) failed: %s",
-        (unsigned long) session.uid, (unsigned long) PR_ROOT_UID,
+      pr_log_pri(PR_LOG_ERR, MOD_CAP_VERSION ": setreuid(%s, %s) failed: %s",
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_uid2str(cmd->tmp_pool, PR_ROOT_UID),
         strerror(xerrno));
     }
 
@@ -386,8 +375,9 @@ MODRET cap_post_pass(cmd_rec *cmd) {
    */
 
   res = lp_init_cap();
-  if (res != -1)
+  if (res != -1) {
     res = lp_add_cap(CAP_NET_BIND_SERVICE, CAP_PERMITTED);
+  }
 
   /* Add the CAP_CHOWN capability, unless explicitly configured not to. */
   if (res != -1 &&
@@ -461,8 +451,10 @@ MODRET cap_post_pass(cmd_rec *cmd) {
   }
 
   if (setreuid(dst_uid, session.uid) == -1) {
-    pr_log_pri(PR_LOG_ERR, MOD_CAP_VERSION ": setreuid(%lu, %lu) failed: %s",
-      (unsigned long) dst_uid, (unsigned long) session.uid, strerror(errno));
+    pr_log_pri(PR_LOG_ERR, MOD_CAP_VERSION ": setreuid(%s, %s) failed: %s",
+      pr_uid2str(cmd->tmp_pool, dst_uid),
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      strerror(errno));
     lp_free_cap();
     pr_signals_unblock();
     pr_session_disconnect(&cap_module, PR_SESS_DISCONNECT_BY_APPLICATION, NULL);
@@ -470,17 +462,20 @@ MODRET cap_post_pass(cmd_rec *cmd) {
   pr_signals_unblock();
 
   pr_log_debug(DEBUG9, MOD_CAP_VERSION
-    ": uid = %lu, euid = %lu, gid = %lu, egid = %lu",
-    (unsigned long) getuid(), (unsigned long) geteuid(),
-    (unsigned long) getgid(), (unsigned long) getegid());
+    ": uid = %s, euid = %s, gid = %s, egid = %s",
+    pr_uid2str(cmd->tmp_pool, getuid()),
+    pr_uid2str(cmd->tmp_pool, geteuid()),
+    pr_gid2str(cmd->tmp_pool, getgid()),
+    pr_gid2str(cmd->tmp_pool, getegid()));
 
   /* Now our only capabilities consist of CAP_NET_BIND_SERVICE (and other
    * configured caps), however in order to actually be able to bind to
    * low-numbered ports, we need the capability to be in the effective set.
    */
 
-  if (res != -1)
+  if (res != -1) {
     res = lp_add_cap(CAP_NET_BIND_SERVICE, CAP_EFFECTIVE);
+  }
 
   /* Add the CAP_CHOWN capability, unless explicitly configured not to. */
   if (res != -1 &&
@@ -543,10 +538,34 @@ MODRET cap_post_pass(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void cap_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&cap_module, "core.session-reinit", cap_sess_reinit_ev);
+
+  have_capabilities = FALSE;
+  use_capabilities = TRUE;
+  cap_flags = 0;
+
+  res = cap_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&cap_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization routines
  */
 
 static int cap_sess_init(void) {
+  pr_event_register(&cap_module, "core.session-reinit", cap_sess_reinit_ev,
+    NULL);
+
   /* Check to see if the lowering of capabilities has been disabled in the
    * configuration file.
    */
@@ -656,7 +675,6 @@ static conftable cap_conftab[] = {
 };
 
 static cmdtable cap_cmdtab[] = {
-  { POST_CMD,	C_HOST,	G_NONE,	cap_post_host,	FALSE, FALSE },
   { POST_CMD,	C_PASS,	G_NONE,	cap_post_pass,	FALSE, FALSE },
   { 0, NULL }
 };
diff --git a/modules/mod_core.c b/modules/mod_core.c
index 080a97c..6c1c147 100644
--- a/modules/mod_core.c
+++ b/modules/mod_core.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -60,6 +60,7 @@ static const char *trace_log = NULL;
 #endif /* PR_USE_TRACE */
 
 /* Necessary prototypes. */
+static void core_exit_ev(const void *, void *);
 static int core_sess_init(void);
 static void reset_server_auth_order(void);
 
@@ -121,9 +122,16 @@ static unsigned long core_exceeded_cmd_rate(cmd_rec *cmd) {
 }
 
 static int core_idle_timeout_cb(CALLBACK_FRAME) {
+  int timeout;
+
+  timeout = pr_data_get_timeout(PR_DATA_TIMEOUT_IDLE);
+
   /* We don't want to quit in the middle of a transfer */
   if (session.sf_flags & SF_XFER) { 
- 
+    pr_trace_msg("timer", 4,
+      "TimeoutIdle (%d %s) reached, but data transfer in progress, ignoring",
+      timeout, timeout != 1 ? "seconds" : "second"); 
+
     /* Restart the timer. */
     return 1; 
   }
@@ -131,8 +139,7 @@ static int core_idle_timeout_cb(CALLBACK_FRAME) {
   pr_event_generate("core.timeout-idle", NULL);
  
   pr_response_send_async(R_421,
-    _("Idle timeout (%d seconds): closing control connection"),
-    pr_data_get_timeout(PR_DATA_TIMEOUT_IDLE));
+    _("Idle timeout (%d seconds): closing control connection"), timeout);
 
   pr_timer_remove(PR_TIMER_LOGIN, ANY_MODULE);
   pr_timer_remove(PR_TIMER_NOXFER, ANY_MODULE);
@@ -187,28 +194,32 @@ static int core_scrub_scoreboard_cb(CALLBACK_FRAME) {
 MODRET start_ifdefine(cmd_rec *cmd) {
   unsigned int ifdefine_ctx_count = 1;
   unsigned char not_define = FALSE, defined = FALSE;
-  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'}, *config_line = NULL;
+  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'}, *config_line = NULL, *ptr;
 
   CHECK_ARGS(cmd, 1);
 
-  if (*(cmd->argv[1]) == '!') {
+  ptr = cmd->argv[1];
+  if (*ptr == '!') {
     not_define = TRUE;
-    (cmd->argv[1])++;
+    ptr++;
   }
 
-  defined = pr_define_exists(cmd->argv[1]);
+  defined = pr_define_exists(ptr);
 
   /* Return now if we don't need to consume the <IfDefine> section
    * configuration lines.
    */
-  if ((!not_define && defined) || (not_define && !defined)) {
-    pr_log_debug(DEBUG3, "%s: using '%s%s' section at line %u", cmd->argv[0],
-      not_define ? "!" : "", cmd->argv[1], pr_parser_get_lineno());
+  if ((!not_define && defined) ||
+      (not_define && !defined)) {
+    pr_log_debug(DEBUG3, "%s: using '%s%s' section at line %u",
+      (char *) cmd->argv[0], not_define ? "!" : "", (char *) cmd->argv[1],
+      pr_parser_get_lineno());
     return PR_HANDLED(cmd);
+  }
 
-  } else
-    pr_log_debug(DEBUG3, "%s: skipping '%s%s' section at line %u", cmd->argv[0],
-      not_define ? "!" : "", cmd->argv[1], pr_parser_get_lineno());
+  pr_log_debug(DEBUG3, "%s: skipping '%s%s' section at line %u",
+    (char *) cmd->argv[0], not_define ? "!" : "", (char *) cmd->argv[1],
+    pr_parser_get_lineno());
 
   /* Rather than communicating with parse_config_file() via some global
    * variable/flag the need to skip configuration lines, if the requested
@@ -219,17 +230,19 @@ MODRET start_ifdefine(cmd_rec *cmd) {
   while (ifdefine_ctx_count && (config_line = pr_parser_read_line(buf,
       sizeof(buf))) != NULL) {
 
-    if (strncasecmp(config_line, "<IfDefine", 9) == 0)
+    if (strncasecmp(config_line, "<IfDefine", 9) == 0) {
       ifdefine_ctx_count++;
 
-    if (strcasecmp(config_line, "</IfDefine>") == 0)
+    } else if (strcasecmp(config_line, "</IfDefine>") == 0) {
       ifdefine_ctx_count--;
+    }
   }
 
   /* If there are still unclosed <IfDefine> sections, signal an error.
    */
-  if (ifdefine_ctx_count)
+  if (ifdefine_ctx_count) {
     CONF_ERROR(cmd, "unclosed <IfDefine> context");
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -245,30 +258,33 @@ MODRET end_ifdefine(cmd_rec *cmd) {
 MODRET start_ifmodule(cmd_rec *cmd) {
   unsigned int ifmodule_ctx_count = 1;
   unsigned char not_module = FALSE, found_module = FALSE;
-  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'}, *config_line = NULL;
+  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'}, *config_line = NULL, *ptr;
 
   CHECK_ARGS(cmd, 1);
 
-  if (*(cmd->argv[1]) == '!') {
+  ptr = cmd->argv[1];
+  if (*ptr == '!') {
     not_module = TRUE;
-    (cmd->argv[1])++;
+    ptr++;
   }
 
-  found_module = pr_module_exists(cmd->argv[1]);
+  found_module = pr_module_exists(ptr);
 
   /* Return now if we don't need to consume the <IfModule> section
    * configuration lines.
    */
-  if ((!not_module && found_module) || (not_module && !found_module)) {
-    pr_log_debug(DEBUG3, "%s: using '%s%s' section at line %u", cmd->argv[0],
-      not_module ? "!" : "", cmd->argv[1], pr_parser_get_lineno());
+  if ((!not_module && found_module) ||
+      (not_module && !found_module)) {
+    pr_log_debug(DEBUG3, "%s: using '%s%s' section at line %u",
+      (char *) cmd->argv[0], not_module ? "!" : "", (char *) cmd->argv[1],
+      pr_parser_get_lineno());
     return PR_HANDLED(cmd);
-
-  } else {
-    pr_log_debug(DEBUG3, "%s: skipping '%s%s' section at line %u", cmd->argv[0],
-      not_module ? "!" : "", cmd->argv[1], pr_parser_get_lineno());
   }
 
+  pr_log_debug(DEBUG3, "%s: skipping '%s%s' section at line %u",
+    (char *) cmd->argv[0], not_module ? "!" : "", (char *) cmd->argv[1],
+    pr_parser_get_lineno());
+
   /* Rather than communicating with parse_config_file() via some global
    * variable/flag the need to skip configuration lines, if the requested
    * module condition is not TRUE, read in the lines here (effectively
@@ -330,40 +346,89 @@ MODRET set_define(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-MODRET add_include(cmd_rec *cmd) {
-  int res;
+/* usage: Include path|pattern */
+MODRET set_include(cmd_rec *cmd) {
+  int allowed_ctxs, parent_ctx, res, xerrno;
 
   CHECK_ARGS(cmd, 1);
-  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_ANON|CONF_GLOBAL|CONF_DIR);
+
+  /* If we are not currently in a .ftpaccess context, then we allow Include
+   * in a <Limit> section.  Otherwise, a .ftpaccess file could contain a
+   * <Limit>, and that <Limit> could include e.g. itself, leading to a loop.
+   */
+
+  allowed_ctxs = CONF_ROOT|CONF_VIRTUAL|CONF_ANON|CONF_GLOBAL|CONF_DIR;
+
+  parent_ctx = CONF_ROOT;
+  if (cmd->config != NULL &&
+      cmd->config->parent != NULL) {
+    parent_ctx = cmd->config->parent->config_type;
+  }
+
+  if (parent_ctx != CONF_DYNDIR) {
+    allowed_ctxs |= CONF_LIMIT;
+  }
+
+  CHECK_CONF(cmd, allowed_ctxs);
 
   /* Make sure the given path is a valid path. */
 
   PRIVS_ROOT
   res = pr_fs_valid_path(cmd->argv[1]);
+  PRIVS_RELINQUISH
 
   if (res < 0) {
-    PRIVS_RELINQUISH
-
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
       "unable to use path for configuration file '", cmd->argv[1], "'", NULL));
   }
 
-  if (parse_config_path(cmd->tmp_pool, cmd->argv[1]) == -1) {
-    int xerrno = errno;
+  PRIVS_ROOT
+  res = parse_config_path(cmd->tmp_pool, cmd->argv[1]);
+  xerrno = errno;
+  PRIVS_RELINQUISH
 
+  if (res < 0) {
     if (xerrno != EINVAL) {
       pr_log_pri(PR_LOG_WARNING, "warning: unable to include '%s': %s",
-        cmd->argv[1], strerror(xerrno));
+        (char *) cmd->argv[1], strerror(xerrno));
 
     } else {
-      PRIVS_RELINQUISH
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error including '",
+        (char *) cmd->argv[1], "': ", strerror(xerrno), NULL));
+    }
+  }
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: IncludeOptions opt1 ... */
+MODRET set_includeoptions(cmd_rec *cmd) {
+  register unsigned int i;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "AllowSymlinks") == 0) {
+      opts |= PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS;
 
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error including '", cmd->argv[1],
-        "': ", strerror(xerrno), NULL));
+    } else if (strcmp(cmd->argv[i], "IgnoreTempFiles") == 0) {
+      opts |= PR_PARSER_INCLUDE_OPT_IGNORE_TMP_FILES;
+
+    } else if (strcmp(cmd->argv[i], "IgnoreWildcards") == 0) {
+      opts |= PR_PARSER_INCLUDE_OPT_IGNORE_WILDCARDS;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown IncludeOption '",
+        cmd->argv[i], "'", NULL));
     }
   }
-  PRIVS_RELINQUISH
 
+  (void) pr_parser_set_include_opts(opts);
   return PR_HANDLED(cmd);
 }
 
@@ -393,46 +458,56 @@ MODRET set_debuglevel(cmd_rec *cmd) {
 }
 
 MODRET set_defaultaddress(cmd_rec *cmd) {
-  pr_netaddr_t *main_addr = NULL;
+  const char *name, *main_ipstr;
+  const pr_netaddr_t *main_addr = NULL;
   array_header *addrs = NULL;
   unsigned int addr_flags = PR_NETADDR_GET_ADDR_FL_INCL_DEVICE;
 
-  if (cmd->argc-1 < 1)
+  if (cmd->argc-1 < 1) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_ROOT);
 
-  main_addr = pr_netaddr_get_addr2(main_server->pool, cmd->argv[1], &addrs,
-    addr_flags);
+  name = cmd->argv[1];
+  main_addr = pr_netaddr_get_addr2(main_server->pool, name, &addrs, addr_flags);
   if (main_addr == NULL) {
-    return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
-      (cmd->argv)[0], ": unable to resolve \"", cmd->argv[1], "\"",
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to resolve '", name, "'",
       NULL));
   }
 
   /* If the given name is a DNS name, automatically add a ServerAlias
    * directive.
    */
-  if (pr_netaddr_is_v4(cmd->argv[1]) == FALSE &&
-      pr_netaddr_is_v6(cmd->argv[1]) == FALSE) {
-    add_config_param_str("ServerAlias", 1, cmd->argv[1]);
+  if (pr_netaddr_is_v4(name) == FALSE &&
+      pr_netaddr_is_v6(name) == FALSE) {
+    add_config_param_str("ServerAlias", 1, name);
   }
 
-  main_server->ServerAddress = pr_netaddr_get_ipstr(main_addr);
+  main_server->ServerAddress = main_ipstr = pr_netaddr_get_ipstr(main_addr);
   main_server->addr = main_addr;
 
-  if (addrs) {
+  if (addrs != NULL) {
     register unsigned int i;
     pr_netaddr_t **elts = addrs->elts;
 
     /* For every additional address, implicitly add a bind record. */
     for (i = 0; i < addrs->nelts; i++) {
-      const char *ipstr = pr_netaddr_get_ipstr(elts[i]);
+      const char *ipstr;
+
+      ipstr = pr_netaddr_get_ipstr(elts[i]);
+
+      /* Skip duplicate addresses. */
+      if (strcmp(main_ipstr, ipstr) == 0) {
+        continue;
+      }
 
 #ifdef PR_USE_IPV6
       if (pr_netaddr_use_ipv6()) {
-        char *ipbuf = pcalloc(cmd->tmp_pool, INET6_ADDRSTRLEN + 1);
-        if (pr_netaddr_get_family(elts[i]) == AF_INET) {
+        char *ipbuf;
 
+        ipbuf = pcalloc(cmd->tmp_pool, INET6_ADDRSTRLEN + 1);
+        if (pr_netaddr_get_family(elts[i]) == AF_INET) {
           /* Create the bind record using the IPv4-mapped IPv6 version of
            * this address.
            */
@@ -455,7 +530,8 @@ MODRET set_defaultaddress(cmd_rec *cmd) {
     char *addrs_str = (char *) pr_netaddr_get_ipstr(main_addr);
 
     for (i = 2; i < cmd->argc; i++) {
-      pr_netaddr_t *addr;
+      const char *addr_ipstr;
+      const pr_netaddr_t *addr;
       addrs = NULL;
 
       addr = pr_netaddr_get_addr2(cmd->tmp_pool, cmd->argv[i], &addrs,
@@ -465,7 +541,8 @@ MODRET set_defaultaddress(cmd_rec *cmd) {
           cmd->argv[i], "': ", strerror(errno), NULL));
       }
 
-      add_config_param_str("_bind_", 1, pr_netaddr_get_ipstr(addr));
+      addr_ipstr = pr_netaddr_get_ipstr(addr);
+      add_config_param_str("_bind_", 1, addr_ipstr);
 
       /* If the given name is a DNS name, automatically add a ServerAlias
        * directive.
@@ -475,16 +552,24 @@ MODRET set_defaultaddress(cmd_rec *cmd) {
         add_config_param_str("ServerAlias", 1, cmd->argv[i]);
       }
 
-      addrs_str = pstrcat(cmd->tmp_pool, addrs_str, ", ",
-        pr_netaddr_get_ipstr(addr), NULL);
+      addrs_str = pstrcat(cmd->tmp_pool, addrs_str, ", ", addr_ipstr, NULL);
 
-      if (addrs) {
+      if (addrs != NULL) {
         register unsigned int j;
         pr_netaddr_t **elts = addrs->elts;
 
         /* For every additional address, implicitly add a bind record. */
         for (j = 0; j < addrs->nelts; j++) {
-          add_config_param_str("_bind_", 1, pr_netaddr_get_ipstr(elts[j]));
+          const char *ipstr;
+
+          ipstr = pr_netaddr_get_ipstr(elts[j]);
+
+          /* Skip duplicate addresses. */
+          if (strcmp(addr_ipstr, ipstr) == 0) {
+            continue;
+          }
+
+          add_config_param_str("_bind_", 1, ipstr);
         }
       }
     }
@@ -492,8 +577,7 @@ MODRET set_defaultaddress(cmd_rec *cmd) {
     pr_log_debug(DEBUG3, "setting default addresses to %s", addrs_str);
 
   } else {
-    pr_log_debug(DEBUG3, "setting default addresses to %s",
-      pr_netaddr_get_ipstr(main_addr));
+    pr_log_debug(DEBUG3, "setting default address to %s", main_ipstr);
   }
 
   return PR_HANDLED(cmd);
@@ -534,7 +618,7 @@ MODRET set_setenv(cmd_rec *cmd) {
   add_config_param_str(cmd->argv[0], 2, cmd->argv[1], cmd->argv[2]);
 
   /* In addition, if this is the "server config" context, set the
-   * environ variable now.  If there was a <Daemon> context, that would
+   * environment variable now.  If there was a <Daemon> context, that would
    * be a more appropriate place for configuring parse-time environ
    * variables.
    */
@@ -544,8 +628,8 @@ MODRET set_setenv(cmd_rec *cmd) {
 
   if (ctxt_type == CONF_ROOT) {
     if (pr_env_set(cmd->server->pool, cmd->argv[1], cmd->argv[2]) < 0) {
-      pr_log_debug(DEBUG1, "%s: unable to set environ variable '%s': %s",
-        cmd->argv[0], cmd->argv[1], strerror(errno));
+      pr_log_debug(DEBUG1, "%s: unable to set environment variable '%s': %s",
+        (char *) cmd->argv[0], (char *) cmd->argv[1], strerror(errno));
 
     } else {
       core_handle_locale_env(cmd->argv[1]);
@@ -567,30 +651,6 @@ MODRET add_transferlog(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-MODRET set_wtmplog(cmd_rec *cmd) {
-  int bool = -1;
-  config_rec *c = NULL;
-
-  CHECK_ARGS(cmd, 1);
-  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
-
-  if (strcasecmp(cmd->argv[1], "NONE") == 0)
-    bool = 0;
-  else
-    bool = get_boolean(cmd, 1);
-
-  if (bool != -1) {
-    c = add_config_param(cmd->argv[0], 1, NULL);
-    c->argv[0] = pcalloc(c->pool, sizeof(unsigned char));
-    *((unsigned char *) c->argv[0]) = bool;
-    c->flags |= CF_MERGEDOWN;
-
-  } else
-    CONF_ERROR(cmd, "expected boolean argument, or \"NONE\"");
-
-  return PR_HANDLED(cmd);
-}
-
 MODRET set_serveradmin(cmd_rec *cmd) {
   server_rec *s = cmd->server;
 
@@ -747,7 +807,11 @@ MODRET set_pidfile(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  if (pr_pidfile_set(cmd->argv[1]) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to set PidFile '",
+      cmd->argv[1], "': ", strerror(errno), NULL));
+  }
+
   return PR_HANDLED(cmd);
 }
 
@@ -759,20 +823,20 @@ MODRET set_sysloglevel(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   level = pr_log_str2sysloglevel(cmd->argv[1]);
-  if (level < 0)
+  if (level < 0) {
     CONF_ERROR(cmd, "SyslogLevel requires level keyword: one of "
       "emerg/alert/crit/error/warn/notice/info/debug");
+  }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
-  c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
-  *((unsigned int *) c->argv[0]) = level;
+  c->argv[0] = pcalloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = level;
 
   return PR_HANDLED(cmd);
 }
 
 /* usage: ServerAlias hostname [hostname ...] */
 MODRET set_serveralias(cmd_rec *cmd) {
-#ifdef PR_USE_HOST
   register unsigned int i;
 
   if (cmd->argc < 2) {
@@ -786,9 +850,6 @@ MODRET set_serveralias(cmd_rec *cmd) {
   }
 
   return PR_HANDLED(cmd);
-#else
-  CONF_ERROR(cmd, "not yet implemented");
-#endif /* PR_USE_HOST */
 }
 
 /* usage: ServerIdent off|on [name] */
@@ -853,20 +914,34 @@ MODRET set_defaultserver(cmd_rec *cmd) {
 
 MODRET set_masqueradeaddress(cmd_rec *cmd) {
   config_rec *c = NULL;
-  pr_netaddr_t *masq_addr = NULL;
+  const char *name;
+  size_t namelen;
+  const pr_netaddr_t *masq_addr = NULL;
   unsigned int addr_flags = PR_NETADDR_GET_ADDR_FL_INCL_DEVICE;
 
   CHECK_ARGS(cmd, 1);
-  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   /* We can only masquerade as one address, so we don't need to know if the
    * given name might map to multiple addresses.
    */
-  masq_addr = pr_netaddr_get_addr2(cmd->server->pool, cmd->argv[1], NULL,
-    addr_flags);
+  name = cmd->argv[1];
+  namelen = strlen(name);
+  if (namelen == 0) {
+    /* Guard against empty names here. */
+    CONF_ERROR(cmd, "missing required name parameter");
+  }
+
+  masq_addr = pr_netaddr_get_addr2(cmd->server->pool, name, NULL, addr_flags);
   if (masq_addr == NULL) {
-    return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool, cmd->argv[0],
-      ": unable to resolve \"", cmd->argv[1], "\"", NULL));
+    /* If the requested name cannot be resolved because it is not known AT
+     * THIS TIME, then do not fail to start the server.  We will simply try
+     * again later (Bug#4104).
+     */
+    if (errno != ENOENT) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to resolve '", name, "'",
+        NULL));
+    }
   }
 
   c = add_config_param(cmd->argv[0], 2, (void *) masq_addr, NULL);
@@ -876,23 +951,25 @@ MODRET set_masqueradeaddress(cmd_rec *cmd) {
 }
 
 MODRET set_maxinstances(cmd_rec *cmd) {
-  int max;
+  long max_instances;
   char *endp;
 
-  CHECK_ARGS(cmd,1);
-  CHECK_CONF(cmd,CONF_ROOT);
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT);
 
-  if (strcasecmp(cmd->argv[1], "none") == 0)
-    max = 0;
+  if (strcasecmp(cmd->argv[1], "none") == 0) {
+    max_instances = 0UL;
 
-  else {
-    max = (int)strtol(cmd->argv[1], &endp, 10);
+  } else {
+    max_instances = strtol(cmd->argv[1], &endp, 10);
 
-    if ((endp && *endp) || max < 1)
+    if ((endp && *endp) ||
+        max_instances < 1) {
       CONF_ERROR(cmd, "argument must be 'none' or a number greater than 0");
+    }
   }
 
-  ServerMaxInstances = max;
+  ServerMaxInstances = max_instances;
   return PR_HANDLED(cmd);
 }
 
@@ -1149,7 +1226,9 @@ MODRET set_socketoptions(cmd_rec *cmd) {
         cmd->server->tcp_keepalive->keepalive_intvl = intvl;
 #else
         cmd->server->tcp_keepalive->keepalive_enabled = TRUE;
-        pr_log_debug(DEBUG0, "%s: platform does not support fine-grained TCP keepalive control, using \"keepalive on\"", cmd->argv[0]);
+        pr_log_debug(DEBUG0,
+          "%s: platform does not support fine-grained TCP keepalive control, "
+          "using \"keepalive on\"", (char *) cmd->argv[0]);
 #endif /* No TCP_KEEPIDLE, TCP_KEEPCNT, or TCP_KEEPINTVL */
 
       } else {
@@ -1263,7 +1342,7 @@ MODRET set_user(cmd_rec *cmd) {
 
 MODRET add_from(cmd_rec *cmd) {
   int cargc;
-  char **cargv;
+  void **cargv;
 
   CHECK_CONF(cmd, CONF_CLASS);
 
@@ -1271,20 +1350,21 @@ MODRET add_from(cmd_rec *cmd) {
   cargv = cmd->argv;
 
   while (cargc && *(cargv + 1)) {
-    if (strcasecmp("all", *(cargv + 1)) == 0 ||
-        strcasecmp("none", *(cargv + 1)) == 0) {
-      pr_netacl_t *acl = pr_netacl_create(cmd->tmp_pool, *(cargv + 1));
+    if (strcasecmp("all", *(((char **) cargv) + 1)) == 0 ||
+        strcasecmp("none", *(((char **) cargv) + 1)) == 0) {
+      pr_netacl_t *acl = pr_netacl_create(cmd->tmp_pool,
+        *(((char **) cargv) + 1));
       if (acl == NULL) {
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "bad ACL definition '",
-          *(cargv + 1), "': ", strerror(errno), NULL));
+          *(((char **) cargv) + 1), "': ", strerror(errno), NULL));
       }
 
-      pr_trace_msg("netacl", 9, "'%s' parsed into netacl '%s'", *(cargv + 1),
-        pr_netacl_get_str(cmd->tmp_pool, acl));
+      pr_trace_msg("netacl", 9, "'%s' parsed into netacl '%s'",
+        *(((char **) cargv) + 1), pr_netacl_get_str(cmd->tmp_pool, acl));
 
       if (pr_class_add_acl(acl) < 0) {
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error adding rule '",
-          *(cargv + 1), "': ", strerror(errno), NULL));
+          *(((char **) cargv) + 1), "': ", strerror(errno), NULL));
       }
 
       cargc = 0;
@@ -1295,8 +1375,9 @@ MODRET add_from(cmd_rec *cmd) {
 
   /* Parse each parameter into a netacl. */
   while (cargc-- && *(++cargv)) {
-    char *ent = NULL;
-    char *str = pstrdup(cmd->tmp_pool, *cargv);
+    char *ent = NULL, *str;
+
+    str = pstrdup(cmd->tmp_pool, *((char **) cargv));
 
     while ((ent = pr_str_get_token(&str, ",")) != NULL) {
       if (*ent) {
@@ -1311,7 +1392,7 @@ MODRET add_from(cmd_rec *cmd) {
         acl = pr_netacl_create(cmd->tmp_pool, ent);
         if (acl == NULL) {
           CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "bad ACL definition '",
-            *(cargv + 1), "': ", strerror(errno), NULL));
+            *(((char **) cargv) + 1), "': ", strerror(errno), NULL));
         }
 
         pr_trace_msg("netacl", 9, "'%s' parsed into netacl '%s'", ent,
@@ -1328,6 +1409,104 @@ MODRET add_from(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: FSCachePolicy on|off|size {count} [maxAge {age}] */
+MODRET set_fscachepolicy(cmd_rec *cmd) {
+  register unsigned int i;
+  config_rec *c;
+
+  if (cmd->argc != 2 &&
+      cmd->argc != 5) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  if (cmd->argc == 2) {
+    int engine;
+
+    engine = get_boolean(cmd, 1);
+    if (engine == -1) {
+      CONF_ERROR(cmd, "expected Boolean parameter");
+    }
+
+    c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+    c->argv[0] = palloc(c->pool, sizeof(int));
+    *((int *) c->argv[0]) = engine;
+    c->argv[1] = palloc(c->pool, sizeof(unsigned int));
+    *((unsigned int *) c->argv[1]) = PR_TUNABLE_FS_STATCACHE_SIZE;
+    c->argv[2] = palloc(c->pool, sizeof(unsigned int));
+    *((unsigned int *) c->argv[2]) = PR_TUNABLE_FS_STATCACHE_MAX_AGE;
+
+    return PR_HANDLED(cmd);
+  }
+
+  c = add_config_param_str(cmd->argv[0], 3, NULL, NULL, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = TRUE;
+  c->argv[1] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[1]) = PR_TUNABLE_FS_STATCACHE_SIZE;
+  c->argv[2] = palloc(c->pool, sizeof(unsigned int));
+  *((unsigned int *) c->argv[2]) = PR_TUNABLE_FS_STATCACHE_MAX_AGE;
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strncasecmp(cmd->argv[i], "size", 5) == 0) {
+      int size;
+
+      size = atoi(cmd->argv[i++]);
+      if (size < 1) {
+        CONF_ERROR(cmd, "size parameter must be greater than 1");
+      }
+
+      *((unsigned int *) c->argv[1]) = size;
+
+    } else if (strncasecmp(cmd->argv[i], "maxAge", 7) == 0) {
+      int max_age;
+
+      max_age = atoi(cmd->argv[i++]);
+      if (max_age < 1) {
+        CONF_ERROR(cmd, "maxAge parameter must be greater than 1");
+      }
+
+      *((unsigned int *) c->argv[2]) = max_age;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown FSCachePolicy: ",
+        cmd->argv[i], NULL));
+    }
+  }
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: FSOptions opt1 opt2 ... */
+MODRET set_fsoptions(cmd_rec *cmd) {
+  register unsigned int i;
+  config_rec *c;
+
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "IgnoreExtendedAttributes") == 0) {
+      opts |= PR_FSIO_OPT_IGNORE_XATTR;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown FSOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
 MODRET set_group(cmd_rec *cmd) {
   struct group *grp = NULL;
 
@@ -1415,7 +1594,7 @@ MODRET set_trace(cmd_rec *cmd) {
 
     c = add_config_param(cmd->argv[0], 0);
     c->argc = cmd->argc - 2;
-    c->argv = pcalloc(c->pool, ((c->argc + 1) * sizeof(char *))); 
+    c->argv = pcalloc(c->pool, ((c->argc + 1) * sizeof(void *))); 
 
     for (i = idx; i < cmd->argc; i++) {
       char *ptr;
@@ -1449,8 +1628,14 @@ MODRET set_tracelog(cmd_rec *cmd) {
 
   trace_log = pstrdup(cmd->server->pool, cmd->argv[1]);
   if (pr_trace_set_file(trace_log) < 0) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error using TraceLog '",
-      trace_log, "': ", strerror(errno), NULL));
+    if (errno == EPERM) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error using TraceLog '",
+        trace_log, "': directory is symlink or is world-writable", NULL));
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error using TraceLog '",
+        trace_log, "': ", strerror(errno), NULL));
+    }
   }
 
   return PR_HANDLED(cmd);
@@ -1530,7 +1715,7 @@ MODRET set_traceoptions(cmd_rec *cmd) {
      */
     if (pr_trace_set_options(trace_opts) < 0) {
       pr_log_debug(DEBUG6, "%s: error setting TraceOptions (%lu): %s",
-        cmd->argv[0], trace_opts, strerror(errno));
+        (char *) cmd->argv[0], trace_opts, strerror(errno));
     }
   }
 
@@ -1593,8 +1778,8 @@ MODRET set_unsetenv(cmd_rec *cmd) {
   add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); 
 
   /* In addition, if this is the "server config" context, unset the
-   * environ variable now.  If there was a <Daemon> context, that would
-   * be a more appropriate place for configuring parse-time environ
+   * environment variable now.  If there was a <Daemon> context, that would
+   * be a more appropriate place for configuring parse-time environment
    * variables.
    */
   ctxt_type = (cmd->config && cmd->config->config_type != CONF_PARAM ?
@@ -1603,8 +1788,8 @@ MODRET set_unsetenv(cmd_rec *cmd) {
 
   if (ctxt_type == CONF_ROOT) {
     if (pr_env_unset(cmd->server->pool, cmd->argv[1]) < 0) {
-      pr_log_debug(DEBUG1, "%s: unable to unset environ variable '%s': %s",
-        cmd->argv[0], cmd->argv[1], strerror(errno));
+      pr_log_debug(DEBUG1, "%s: unable to unset environment variable '%s': %s",
+        (char *) cmd->argv[0], (char *) cmd->argv[1], strerror(errno));
 
     } else {
       core_handle_locale_env(cmd->argv[1]);
@@ -1845,8 +2030,8 @@ MODRET set_regex(cmd_rec *cmd, char *param, char *type) {
     regex_flags |= flags;
   }
 
-  pr_log_debug(DEBUG4, "%s: compiling %s regex '%s'", cmd->argv[0], type,
-    cmd->argv[1]);
+  pr_log_debug(DEBUG4, "%s: compiling %s regex '%s'", (char *) cmd->argv[0],
+    type, (char *) cmd->argv[1]);
   pre = pr_regexp_alloc(&core_module);
 
   res = pr_regexp_compile(pre, cmd->argv[1], regex_flags);
@@ -1856,8 +2041,8 @@ MODRET set_regex(cmd_rec *cmd, char *param, char *type) {
     pr_regexp_error(res, pre, errstr, sizeof(errstr));
     pr_regexp_free(NULL, pre);
 
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1], "' failed regex "
-      "compilation: ", errstr, NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", (char *) cmd->argv[1],
+      "' failed regex compilation: ", errstr, NULL));
   }
 
   c = add_config_param(param, 1, pre);
@@ -1907,7 +2092,8 @@ MODRET set_allowdenyfilter(cmd_rec *cmd) {
     regex_flags |= flags;
   }
 
-  pr_log_debug(DEBUG4, "%s: compiling regex '%s'", cmd->argv[0], cmd->argv[1]);
+  pr_log_debug(DEBUG4, "%s: compiling regex '%s'", (char *) cmd->argv[0],
+    (char *) cmd->argv[1]);
   pre = pr_regexp_alloc(&core_module);
 
   res = pr_regexp_compile(pre, cmd->argv[1], regex_flags);
@@ -1917,8 +2103,8 @@ MODRET set_allowdenyfilter(cmd_rec *cmd) {
     pr_regexp_error(res, pre, errstr, sizeof(errstr));
     pr_regexp_free(NULL, pre);
 
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1], "' failed regex "
-      "compilation: ", errstr, NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", (char *) cmd->argv[1],
+      "' failed regex compilation: ", errstr, NULL));
   }
 
   c = add_config_param(cmd->argv[0], 1, pre);
@@ -2059,8 +2245,9 @@ MODRET add_directory(cmd_rec *cmd) {
   if (*dir != '/' &&
       *dir != '~' &&
       (!cmd->config ||
-       cmd->config->config_type != CONF_ANON))
+       cmd->config->config_type != CONF_ANON)) {
     CONF_ERROR(cmd, "relative path not allowed in non-<Anonymous> sections");
+  }
 
   /* If in anonymous mode, and path is relative, just cat anon root
    * and relative path.
@@ -2072,8 +2259,9 @@ MODRET add_directory(cmd_rec *cmd) {
       cmd->config->config_type == CONF_ANON &&
       *dir != '/' &&
       *dir != '~') {
-    if (strncmp(dir, "*", 2) != 0)
+    if (strncmp(dir, "*", 2) != 0) {
       dir = pdircat(cmd->tmp_pool, "/", dir, NULL);
+    }
     rootdir = cmd->config->name;
 
   } else {
@@ -2121,11 +2309,11 @@ MODRET add_directory(cmd_rec *cmd) {
 
   if (!(c->flags & CF_DEFER)) {
     pr_log_debug(DEBUG2, "<Directory %s>: adding section for resolved "
-      "path '%s'", cmd->argv[1], dir);
+      "path '%s'", (char *) cmd->argv[1], dir);
 
   } else {
     pr_log_debug(DEBUG2,
-      "<Directory %s>: deferring resolution of path", cmd->argv[1]);
+      "<Directory %s>: deferring resolution of path", (char *) cmd->argv[1]);
   }
 
   return PR_HANDLED(cmd);
@@ -2137,6 +2325,7 @@ MODRET set_hidefiles(cmd_rec *cmd) {
   config_rec *c = NULL;
   unsigned int precedence = 0;
   unsigned char negated = FALSE, none = FALSE;
+  char *ptr;
 
   int ctxt = (cmd->config && cmd->config->config_type != CONF_PARAM ?
     cmd->config->config_type : cmd->server->config_type ?
@@ -2161,15 +2350,16 @@ MODRET set_hidefiles(cmd_rec *cmd) {
   }
 
   /* Check for a leading '!' prefix, signifying regex negation */
-  if (*cmd->argv[1] == '!') {
+  ptr = cmd->argv[1];
+  if (*ptr == '!') {
     negated = TRUE;
-    cmd->argv[1] = cmd->argv[1] + 1;
+    ptr++;
 
   } else {
     /* Check for a "none" argument, which is used to nullify inherited
      * HideFiles configurations from parent directories.
      */
-    if (strcasecmp(cmd->argv[1], "none") == 0) {
+    if (strcasecmp(ptr, "none") == 0) {
       none = TRUE;
     }
   }
@@ -2179,14 +2369,14 @@ MODRET set_hidefiles(cmd_rec *cmd) {
 
     pre = pr_regexp_alloc(&core_module);
   
-    res = pr_regexp_compile(pre, cmd->argv[1], REG_EXTENDED|REG_NOSUB);
+    res = pr_regexp_compile(pre, ptr, REG_EXTENDED|REG_NOSUB);
     if (res != 0) {
       char errstr[200] = {'\0'};
 
       pr_regexp_error(res, pre, errstr, sizeof(errstr));
       pr_regexp_free(NULL, pre);
 
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1],
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", ptr,
         "' failed regex compilation: ", errstr, NULL));
     }
   }
@@ -2219,10 +2409,16 @@ MODRET set_hidefiles(cmd_rec *cmd) {
 
   } else if (cmd->argc-1 == 3) {
     array_header *acl = NULL;
-    int argc = cmd->argc - 3;
-    char **argv = cmd->argv + 2;
+    unsigned int argc = cmd->argc - 3;
+    void **argv;
+
+    argv = &(cmd->argv[2]);
 
-    acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
+    acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
+    if (acl == NULL) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error creating expression: ",
+        strerror(errno), NULL));
+    }
 
     c = add_config_param(cmd->argv[0], 0);
     c->argc = argc + 4;
@@ -2231,12 +2427,12 @@ MODRET set_hidefiles(cmd_rec *cmd) {
      * regexp, one for the 'negated' value, one for the precedence,
      * one for the classifier, and one for the terminating NULL
      */
-    c->argv = pcalloc(c->pool, ((argc + 5) * sizeof(char *)));
+    c->argv = pcalloc(c->pool, ((argc + 5) * sizeof(void *)));
 
     /* Capture the config_rec's argv pointer for doing the by-hand
      * population.
      */
-    argv = (char **) c->argv;
+    argv = c->argv;
 
     /* Copy in the regexp. */
     *argv = pcalloc(c->pool, sizeof(pr_regex_t *));
@@ -2255,7 +2451,7 @@ MODRET set_hidefiles(cmd_rec *cmd) {
 
     /* now, copy in the expression arguments */
     if (argc && acl) {
-      while (argc--) {
+      while (argc-- > 0) {
         *argv++ = pstrdup(c->pool, *((char **) acl->elts));
         acl->elts = ((char **) acl->elts) + 1;
       }
@@ -2418,13 +2614,17 @@ MODRET set_allowoverride(cmd_rec *cmd) {
 MODRET end_directory(cmd_rec *cmd) {
   int empty_ctxt = FALSE;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_DIR);
 
   pr_parser_config_ctxt_close(&empty_ctxt);
 
-  if (empty_ctxt)
-    pr_log_debug(DEBUG3, "%s: ignoring empty section", cmd->argv[0]);
+  if (empty_ctxt) {
+    pr_log_debug(DEBUG3, "%s: ignoring empty section", (char *) cmd->argv[0]);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -2465,13 +2665,17 @@ MODRET add_anonymous(cmd_rec *cmd) {
 MODRET end_anonymous(cmd_rec *cmd) {
   int empty_ctxt = FALSE;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_ANON);
 
   pr_parser_config_ctxt_close(&empty_ctxt);
 
-  if (empty_ctxt)
-    pr_log_debug(DEBUG3, "%s: ignoring empty section", cmd->argv[0]);
+  if (empty_ctxt) {
+    pr_log_debug(DEBUG3, "%s: ignoring empty section", (char *) cmd->argv[0]);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -2480,15 +2684,19 @@ MODRET add_class(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  if (pr_class_open(main_server->pool, cmd->argv[1]) < 0)
+  if (pr_class_open(main_server->pool, cmd->argv[1]) < 0) {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error creating <Class ",
       cmd->argv[1], ">: ", strerror(errno), NULL));
+  }
 
   return PR_HANDLED(cmd);
 }
 
 MODRET end_class(cmd_rec *cmd) {
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_CLASS);
 
   if (pr_class_close() < 0) {
@@ -2501,8 +2709,10 @@ MODRET end_class(cmd_rec *cmd) {
 MODRET add_global(cmd_rec *cmd) {
   config_rec *c = NULL;
 
-  if (cmd->argc-1 != 0)
+  if (cmd->argc-1 != 0) {
     CONF_ERROR(cmd, "Too many parameters");
+  }
+
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL);
 
   c = pr_parser_config_ctxt_open(cmd->argv[0]);
@@ -2514,13 +2724,17 @@ MODRET add_global(cmd_rec *cmd) {
 MODRET end_global(cmd_rec *cmd) {
   int empty_ctxt = FALSE;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_GLOBAL);
 
   pr_parser_config_ctxt_close(&empty_ctxt);
 
-  if (empty_ctxt)
-    pr_log_debug(DEBUG3, "%s: ignoring empty section", cmd->argv[0]);
+  if (empty_ctxt) {
+    pr_log_debug(DEBUG3, "%s: ignoring empty section", (char *) cmd->argv[0]);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -2531,11 +2745,13 @@ MODRET add_limit(cmd_rec *cmd) {
   int cargc, have_cdup = FALSE, have_xcup = FALSE, have_mkd = FALSE,
     have_xmkd = FALSE, have_pwd = FALSE, have_xpwd = FALSE, have_rmd = FALSE,
     have_xrmd = FALSE;
-  char **cargv, **elts;
+  void **cargv, **elts;
   array_header *list;
 
-  if (cmd->argc < 2)
+  if (cmd->argc < 2) {
     CONF_ERROR(cmd, "directive requires one or more commands");
+  }
+
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_DIR|CONF_ANON|CONF_DYNDIR|CONF_GLOBAL);
 
   c = pr_parser_config_ctxt_open("Limit");
@@ -2546,9 +2762,9 @@ MODRET add_limit(cmd_rec *cmd) {
   list = make_array(c->pool, c->argc + 1, sizeof(void *));
 
   while (cargc-- && *(++cargv)) {
-    char *ent = NULL;
-    char *str = pstrdup(cmd->tmp_pool, *cargv);
+    char *ent = NULL, *str;
 
+    str = pstrdup(cmd->tmp_pool, *((char **) cargv));
     while ((ent = pr_str_get_token(&str, ",")) != NULL) {
       pr_signals_handle();
 
@@ -2569,7 +2785,7 @@ MODRET add_limit(cmd_rec *cmd) {
    * the counterpart (see Bug#3077).
    */
 
-  elts = (char **) list->elts;
+  elts = list->elts;
   for (i = 0; i < list->nelts; i++) {
     if (strcasecmp(elts[i], C_CDUP) == 0) {
       have_cdup = TRUE;
@@ -2636,24 +2852,35 @@ MODRET add_limit(cmd_rec *cmd) {
 }
 
 MODRET set_order(cmd_rec *cmd) {
-  int order = -1,argc = cmd->argc;
-  char *arg = "",**argv = cmd->argv+1;
+  int order = -1;
+  char *arg = "";
   config_rec *c = NULL;
 
+  if (cmd->argc != 2 &&
+      cmd->argc != 3) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
   CHECK_CONF(cmd, CONF_LIMIT);
 
-  while (--argc && *argv)
-    arg = pstrcat(cmd->tmp_pool, arg, *argv++, NULL);
+  if (cmd->argc == 2) {
+    arg = cmd->argv[1];
 
-  if (strcasecmp(arg, "allow,deny") == 0)
+  } else {
+    /* Concatenate our parameters. */
+    arg = pstrcat(cmd->tmp_pool, arg, (char *) cmd->argv[1],
+      (char *) cmd->argv[2], NULL);
+  }
+
+  if (strcasecmp(arg, "allow,deny") == 0) {
     order = ORDER_ALLOWDENY;
 
-  else if (strcasecmp(arg, "deny,allow") == 0)
+  } else if (strcasecmp(arg, "deny,allow") == 0) {
     order = ORDER_DENYALLOW;
 
-  else
+  } else {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", arg, "': invalid argument",
       NULL));
+  }
 
   c = add_config_param(cmd->argv[0], 1, NULL);
   c->argv[0] = pcalloc(c->pool, sizeof(int));
@@ -2664,14 +2891,16 @@ MODRET set_order(cmd_rec *cmd) {
 
 MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
   config_rec *c;
-  char **argv;
-  int argc, eval_type;
-  array_header *acl;
+  void **argv;
+  unsigned int argc;
+  int eval_type;
+  array_header *acl = NULL;
  
   CHECK_CONF(cmd, CONF_LIMIT);
 
-  if (cmd->argc < 2)
+  if (cmd->argc < 2) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   /* For AllowClass/DenyClass and AllowUser/DenyUser, the default expression
    * type is "or".
@@ -2694,7 +2923,7 @@ MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
     if (strcasecmp(cmd->argv[1], "AND") == 0) {
       eval_type = PR_EXPR_EVAL_AND;
       argc = cmd->argc-2;
-      argv = cmd->argv+1;
+      argv = cmd->argv;
 
     } else if (strcasecmp(cmd->argv[1], "OR") == 0) {
       eval_type = PR_EXPR_EVAL_OR;
@@ -2706,8 +2935,9 @@ MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
       pr_regex_t *pre;
       int res;
 
-      if (cmd->argc != 3)
+      if (cmd->argc != 3) {
         CONF_ERROR(cmd, "wrong number of parameters");
+      }
 
       pre = pr_regexp_alloc(&core_module);
 
@@ -2718,8 +2948,8 @@ MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
         pr_regexp_error(res, pre, errstr, sizeof(errstr));
         pr_regexp_free(NULL, pre);
 
-        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[2], "' failed "
-          "regex compilation: ", errstr, NULL));
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", (char *) cmd->argv[2],
+          "' failed regex compilation: ", errstr, NULL));
       }
 
       c = add_config_param(cmd->argv[0], 2, NULL, NULL);
@@ -2729,7 +2959,6 @@ MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
       c->flags |= CF_MERGEDOWN_MULTI;
 
       return PR_HANDLED(cmd);
-
 #else
       CONF_ERROR(cmd, "The 'regex' parameter cannot be used on this system, "
         "as you do not have POSIX compliant regex support");
@@ -2745,23 +2974,27 @@ MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
     argv = cmd->argv;
   }
 
-  acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
+  acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
+  if (acl == NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error creating expression: ",
+      strerror(errno), NULL));
+  }
 
   c = add_config_param(cmd->argv[0], 0);
 
   c->argc = acl->nelts + 1;
-  c->argv = pcalloc(c->pool, (c->argc + 1) * sizeof(char *));
+  c->argv = pcalloc(c->pool, (c->argc + 1) * sizeof(void *));
 
   c->argv[0] = pcalloc(c->pool, sizeof(unsigned char));
   *((unsigned char *) c->argv[0]) = eval_type;
 
-  argv = (char **) c->argv + 1;
+  argv = &(c->argv[1]);
 
-  if (acl) {
-    while (acl->nelts--) {
-      *argv++ = pstrdup(c->pool, *((char **) acl->elts));
-      acl->elts = ((char **) acl->elts) + 1;
-    }
+  while (acl->nelts-- > 0) {
+    pr_signals_handle();
+
+    *argv++ = pstrdup(c->pool, *((char **) acl->elts));
+    acl->elts = ((char **) acl->elts) + 1;
   }
 
   *argv = NULL;
@@ -2772,7 +3005,7 @@ MODRET set_allowdenyusergroupclass(cmd_rec *cmd) {
 
 MODRET set_allowdeny(cmd_rec *cmd) {
   int argc;
-  char **argv;
+  void **argv;
   pr_netacl_t **aclargv;
   array_header *list;
   config_rec *c;
@@ -2791,13 +3024,13 @@ MODRET set_allowdeny(cmd_rec *cmd) {
    */
 
   while (argc && *(argv+1)) {
-    if (strcasecmp("from", *(argv+1)) == 0) {
+    if (strcasecmp("from", *(((char **) argv) + 1)) == 0) {
       argv++;
       argc--;
       continue;
 
-    } else if (strcasecmp("!all", *(argv+1)) == 0 ||
-               strcasecmp("!none", *(argv+1)) == 0) {
+    } else if (strcasecmp("!all", *(((char **) argv) + 1)) == 0 ||
+               strcasecmp("!none", *(((char **) argv) + 1)) == 0) {
       CONF_ERROR(cmd, "the ! negation operator cannot be used with ALL/NONE");
 
     } else if (strcasecmp("all", *(argv+1)) == 0 ||
@@ -2861,7 +3094,10 @@ MODRET set_allowdeny(cmd_rec *cmd) {
 MODRET set_denyall(cmd_rec *cmd) {
   config_rec *c = NULL;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_LIMIT|CONF_ANON|CONF_DIR|CONF_DYNDIR);
 
   c = add_config_param(cmd->argv[0], 1, NULL);
@@ -2874,7 +3110,10 @@ MODRET set_denyall(cmd_rec *cmd) {
 MODRET set_allowall(cmd_rec *cmd) {
   config_rec *c = NULL;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_LIMIT|CONF_ANON|CONF_DIR|CONF_DYNDIR);
 
   c = add_config_param(cmd->argv[0], 1, NULL);
@@ -2899,9 +3138,9 @@ MODRET set_authorder(cmd_rec *cmd) {
   c = add_config_param(cmd->argv[0], 1, NULL);
   module_list = make_array(c->pool, 0, sizeof(char *));
 
-  for (i = 1; i < cmd->argc; i++)
+  for (i = 1; i < cmd->argc; i++) {
     *((char **) push_array(module_list)) = pstrdup(c->pool, cmd->argv[i]);
-
+  }
   c->argv[0] = (void *) module_list;
 
   return PR_HANDLED(cmd);
@@ -2910,13 +3149,17 @@ MODRET set_authorder(cmd_rec *cmd) {
 MODRET end_limit(cmd_rec *cmd) {
   int empty_ctxt = FALSE;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_LIMIT);
 
   pr_parser_config_ctxt_close(&empty_ctxt);
 
-  if (empty_ctxt)
-    pr_log_debug(DEBUG3, "%s: ignoring empty section", cmd->argv[0]);
+  if (empty_ctxt) {
+    pr_log_debug(DEBUG3, "%s: ignoring empty section", (char *) cmd->argv[0]);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -2989,16 +3232,19 @@ MODRET set_displayquit(cmd_rec *cmd) {
 }
 
 MODRET add_virtualhost(cmd_rec *cmd) {
+  const char *name, *addr_ipstr;
   server_rec *s = NULL;
-  pr_netaddr_t *addr = NULL;
+  const pr_netaddr_t *addr = NULL;
   array_header *addrs = NULL;
   unsigned int addr_flags = PR_NETADDR_GET_ADDR_FL_INCL_DEVICE;
 
-  if (cmd->argc-1 < 1)
+  if (cmd->argc-1 < 1) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
   CHECK_CONF(cmd, CONF_ROOT);
 
-  s = pr_parser_server_ctxt_open(cmd->argv[1]);
+  name = cmd->argv[1];
+  s = pr_parser_server_ctxt_open(name);
   if (s == NULL) {
     CONF_ERROR(cmd, "unable to create virtual server configuration");
   }
@@ -3009,27 +3255,38 @@ MODRET add_virtualhost(cmd_rec *cmd) {
    * are server_recs for each one.
    */
 
-  addr = pr_netaddr_get_addr2(cmd->tmp_pool, cmd->argv[1], &addrs, addr_flags);
+  addr = pr_netaddr_get_addr2(cmd->tmp_pool, name, &addrs, addr_flags);
   if (addr == NULL) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error resolving '", cmd->argv[1],
-      "': ", strerror(errno), NULL));
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error resolving '", name, "': ",
+      strerror(errno), NULL));
   }
 
   /* If the given name is a DNS name, automatically add a ServerAlias
    * directive.
    */
-  if (pr_netaddr_is_v4(cmd->argv[1]) == FALSE &&
-      pr_netaddr_is_v6(cmd->argv[1]) == FALSE) {
-    add_config_param_str("ServerAlias", 1, cmd->argv[1]);
+  if (pr_netaddr_is_v4(name) == FALSE &&
+      pr_netaddr_is_v6(name) == FALSE) {
+    add_config_param_str("ServerAlias", 1, name);
   }
 
-  if (addrs) {
+  addr_ipstr = pr_netaddr_get_ipstr(addr);
+
+  if (addrs != NULL) {
     register unsigned int i;
     pr_netaddr_t **elts = addrs->elts;
 
     /* For every additional address, implicitly add a bind record. */
     for (i = 0; i < addrs->nelts; i++) {
-      add_config_param_str("_bind_", 1, pr_netaddr_get_ipstr(elts[i]));
+      const char *ipstr;
+
+      ipstr = pr_netaddr_get_ipstr(elts[i]);
+
+      /* Skip duplicate addresses. */
+      if (strcmp(addr_ipstr, ipstr) == 0) {
+        continue;
+      }
+
+      add_config_param_str("_bind_", 1, ipstr);
     }
   }
 
@@ -3043,30 +3300,40 @@ MODRET add_virtualhost(cmd_rec *cmd) {
     for (i = 2; i < cmd->argc; i++) {
       addrs = NULL;
 
-      addr = pr_netaddr_get_addr2(cmd->tmp_pool, cmd->argv[i], &addrs,
-        addr_flags);
+      name = cmd->argv[i];
+      addr = pr_netaddr_get_addr2(cmd->tmp_pool, name, &addrs, addr_flags);
       if (addr == NULL) {
-        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error resolving '",
-          cmd->argv[i], "': ", strerror(errno), NULL));
+        CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error resolving '", name, "': ",
+          strerror(errno), NULL));
       }
 
       /* If the given name is a DNS name, automatically add a ServerAlias
        * directive.
        */
-      if (pr_netaddr_is_v4(cmd->argv[i]) == FALSE &&
-          pr_netaddr_is_v6(cmd->argv[i]) == FALSE) {
-        add_config_param_str("ServerAlias", 1, cmd->argv[i]);
+      if (pr_netaddr_is_v4(name) == FALSE &&
+          pr_netaddr_is_v6(name) == FALSE) {
+        add_config_param_str("ServerAlias", 1, name);
       }
 
-      add_config_param_str("_bind_", 1, pr_netaddr_get_ipstr(addr));
+      addr_ipstr = pr_netaddr_get_ipstr(addr);
+      add_config_param_str("_bind_", 1, addr_ipstr);
 
-      if (addrs) {
+      if (addrs != NULL) {
         register unsigned int j;
         pr_netaddr_t **elts = addrs->elts;
 
         /* For every additional address, implicitly add a bind record. */
         for (j = 0; j < addrs->nelts; j++) {
-          add_config_param_str("_bind_", 1, pr_netaddr_get_ipstr(elts[j]));
+          const char *ipstr;
+
+          ipstr = pr_netaddr_get_ipstr(elts[j]);
+
+          /* Skip duplicate addresses. */
+          if (strcmp(addr_ipstr, ipstr) == 0) {
+            continue;
+          }
+
+          add_config_param_str("_bind_", 1, ipstr);
         }
       }
     }
@@ -3077,11 +3344,14 @@ MODRET add_virtualhost(cmd_rec *cmd) {
 
 MODRET end_virtualhost(cmd_rec *cmd) {
   server_rec *s = NULL, *next_s = NULL;
-  pr_netaddr_t *addr = NULL;
+  const pr_netaddr_t *addr = NULL;
   const char *address = NULL;
   unsigned int addr_flags = PR_NETADDR_GET_ADDR_FL_INCL_DEVICE;
 
-  CHECK_ARGS(cmd, 0);
+  if (cmd->argc > 1) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
   CHECK_CONF(cmd, CONF_VIRTUAL);
 
   if (cmd->server->ServerAddress) {
@@ -3113,7 +3383,7 @@ MODRET end_virtualhost(cmd_rec *cmd) {
        */
       if (s != cmd->server) {
         const char *serv_addrstr = NULL;
-        pr_netaddr_t *serv_addr = NULL;
+        const pr_netaddr_t *serv_addr = NULL;
 
         if (s->addr) {
           serv_addr = s->addr;
@@ -3179,9 +3449,11 @@ MODRET regex_filters(cmd_rec *cmd) {
   if (allow_regex != NULL &&
       cmd->arg != NULL &&
       pr_regexp_exec(allow_regex, cmd->arg, 0, NULL, 0, 0, 0) != 0) {
-    pr_log_debug(DEBUG2, "'%s %s' denied by AllowFilter", cmd->argv[0],
+    pr_log_debug(DEBUG2, "'%s %s' denied by AllowFilter", (char *) cmd->argv[0],
       cmd->arg);
     pr_response_add_err(R_550, _("%s: Forbidden command argument"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EACCES);
     errno = EACCES;
     return PR_ERROR(cmd);
   }
@@ -3191,9 +3463,11 @@ MODRET regex_filters(cmd_rec *cmd) {
   if (deny_regex != NULL &&
       cmd->arg != NULL &&
       pr_regexp_exec(deny_regex, cmd->arg, 0, NULL, 0, 0, 0) == 0) {
-    pr_log_debug(DEBUG2, "'%s %s' denied by DenyFilter", cmd->argv[0],
+    pr_log_debug(DEBUG2, "'%s %s' denied by DenyFilter", (char *) cmd->argv[0],
       cmd->arg);
     pr_response_add_err(R_550, _("%s: Forbidden command argument"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EACCES);
     errno = EACCES;
     return PR_ERROR(cmd);
   }
@@ -3204,10 +3478,7 @@ MODRET regex_filters(cmd_rec *cmd) {
 
 MODRET core_pre_any(cmd_rec *cmd) {
   unsigned long cmd_delay = 0;
-  char *rnfr_path = NULL;
-
-  /* Make sure any FS caches are clear before each command. */
-  pr_fs_clear_cache();
+  const char *rnfr_path = NULL;
 
   /* Check for an exceeded MaxCommandRate. */
   cmd_delay = core_exceeded_cmd_rate(cmd);
@@ -3270,8 +3541,11 @@ MODRET core_pre_any(cmd_rec *cmd) {
       if (reject_cmd) {
         pr_log_debug(DEBUG3,
           "RNFR followed immediately by %s rather than RNTO, rejecting command",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         pr_response_add_err(R_501, _("Bad sequence of commands"));
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
     }
@@ -3293,11 +3567,20 @@ MODRET core_quit(cmd_rec *cmd) {
     displayquit_fh = NULL;
 
   } else {
-    char *display = get_param_ptr(TOPLEVEL_CONF, "DisplayQuit", FALSE); 
+    char *display;
+
+    display = get_param_ptr(TOPLEVEL_CONF, "DisplayQuit", FALSE);
     if (display) {
       if (pr_display_file(display, NULL, R_221, flags) < 0) {
+        int xerrno = errno;
+
         pr_log_debug(DEBUG6, "unable to display DisplayQuit file '%s': %s",
-          display, strerror(errno));
+          display, strerror(xerrno));
+
+        if (xerrno == ENOENT) {
+          /* No file found?  Send our normal fairwell, then. */
+          pr_response_send(R_221, "%s", _("Goodbye."));
+        }
       }
 
     } else {
@@ -3324,28 +3607,22 @@ MODRET core_log_quit(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* Per RFC959, directory responses for MKD and PWD should be
- * "dir_name" (w/ quote).  For directories that CONTAIN quotes,
- * the add'l quotes must be duplicated.
- */
-static const char *quote_dir(cmd_rec *cmd, char *dir) {
-  return sreplace(cmd->tmp_pool, dir, "\"", "\"\"", NULL);
-}
-
 MODRET core_pwd(cmd_rec *cmd) {
   CHECK_CMD_ARGS(cmd, 1);
 
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.vwd, NULL)) {
     int xerrno = EACCES;
 
-    pr_response_add_err(R_550, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   pr_response_add(R_257, _("\"%s\" is the current directory"),
-    quote_dir(cmd, pr_fs_encode_path(cmd->tmp_pool, session.vwd)));
+    quote_dir(cmd->tmp_pool, pr_fs_encode_path(cmd->tmp_pool, session.vwd)));
 
   return PR_HANDLED(cmd);
 }
@@ -3354,11 +3631,13 @@ MODRET core_pasv(cmd_rec *cmd) {
   unsigned int port = 0;
   char *addrstr = NULL, *tmp = NULL;
   config_rec *c = NULL;
-  pr_netaddr_t *bind_addr;
+  const pr_netaddr_t *bind_addr;
   const char *proto;
 
   if (session.sf_flags & SF_EPSV_ALL) {
     pr_response_add_err(R_500, _("Illegal PASV command, EPSV ALL in effect"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3372,8 +3651,10 @@ MODRET core_pasv(cmd_rec *cmd) {
     int xerrno = EPERM;
 
     pr_log_debug(DEBUG8, "PASV denied by <Limit> configuration");
-    pr_response_add_err(R_501, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -3398,8 +3679,10 @@ MODRET core_pasv(cmd_rec *cmd) {
         pr_log_debug(DEBUG0,
           "Unable to handle PASV for IPv6 address '%s', rejecting command",
           pr_netaddr_get_ipstr(session.c->local_addr));
-        pr_response_add_err(R_501, "%s: %s", cmd->argv[0], strerror(xerrno));
+        pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[0],
+          strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
@@ -3441,10 +3724,12 @@ MODRET core_pasv(cmd_rec *cmd) {
   }
 
   if (session.d == NULL) {
-    pr_response_add_err(R_425, _("Unable to build data connection: "
-      "Internal error"));
-    errno = EINVAL;
-    return PR_ERROR(cmd);
+    pr_response_add_err(R_425,
+      _("Unable to build data connection: Internal error"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
+    return PR_ERROR(cmd);
   }
 
   /* Make sure that necessary socket options are set on the socket prior
@@ -3459,8 +3744,10 @@ MODRET core_pasv(cmd_rec *cmd) {
   if (pr_inet_listen(session.pool, session.d, 1, 0) < 0) {
     int xerrno = errno;
 
-    pr_response_add_err(R_425, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_425, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -3490,25 +3777,32 @@ MODRET core_pasv(cmd_rec *cmd) {
       c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress",
         FALSE);
       if (c != NULL) {
-        addrstr = (char *) pr_netaddr_get_ipstr(c->argv[0]);
+        if (c->argv[0] != NULL) {
+          addrstr = (char *) pr_netaddr_get_ipstr(c->argv[0]);
+        }
       }
     }
 
   } else {
     c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
     if (c != NULL) {
-      addrstr = (char *) pr_netaddr_get_ipstr(c->argv[0]);
+      if (c->argv[0] != NULL) {
+        addrstr = (char *) pr_netaddr_get_ipstr(c->argv[0]);
+      }
     }
   }
 
   /* Fixup the address string for the PASV response. */
   tmp = strrchr(addrstr, ':');
-  if (tmp)
+  if (tmp) {
     addrstr = tmp + 1;
+  }
 
-  for (tmp = addrstr; *tmp; tmp++)
-    if (*tmp == '.')
+  for (tmp = addrstr; *tmp; tmp++) {
+    if (*tmp == '.') {
       *tmp = ',';
+    }
+  }
 
   pr_log_debug(DEBUG1, "Entering Passive Mode (%s,%u,%u).", addrstr,
     (port >> 8) & 255, port & 255);
@@ -3523,7 +3817,8 @@ MODRET core_pasv(cmd_rec *cmd) {
 }
 
 MODRET core_port(cmd_rec *cmd) {
-  pr_netaddr_t *listen_addr = NULL, *port_addr = NULL;
+  const pr_netaddr_t *listen_addr = NULL, *port_addr = NULL;
+  char *port_info;
 #ifdef PR_USE_IPV6
   char buf[INET6_ADDRSTRLEN] = {'\0'};
 #else
@@ -3537,6 +3832,8 @@ MODRET core_port(cmd_rec *cmd) {
 
   if (session.sf_flags & SF_EPSV_ALL) {
     pr_response_add_err(R_500, _("Illegal PORT command, EPSV ALL in effect"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3550,8 +3847,10 @@ MODRET core_port(cmd_rec *cmd) {
     int xerrno = EPERM;
 
     pr_log_debug(DEBUG8, "PORT denied by <Limit> configuration");
-    pr_response_add_err(R_501, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -3571,15 +3870,20 @@ MODRET core_port(cmd_rec *cmd) {
     pr_log_debug(DEBUG0, "RootRevoke in effect, unable to bind to local "
       "port %d for active transfer", session.c->local_port-1);
     pr_response_add_err(R_500, _("Unable to service PORT commands"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   /* Format is h1,h2,h3,h4,p1,p2 (ASCII in network order) */
-  if (sscanf(cmd->argv[1], "%u,%u,%u,%u,%u,%u", &h1, &h2, &h3, &h4, &p1,
+  port_info = cmd->argv[1];
+  if (sscanf(port_info, "%u,%u,%u,%u,%u,%u", &h1, &h2, &h3, &h4, &p1,
       &p2) != 6) {
-    pr_log_debug(DEBUG2, "PORT '%s' is not syntactically valid", cmd->argv[1]);
+    pr_log_debug(DEBUG2, "PORT '%s' is not syntactically valid", port_info);
     pr_response_add_err(R_501, _("Illegal PORT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3588,6 +3892,8 @@ MODRET core_port(cmd_rec *cmd) {
       (h1|h2|h3|h4) == 0 || (p1|p2) == 0) {
     pr_log_debug(DEBUG2, "PORT '%s' has invalid value(s)", cmd->arg);
     pr_response_add_err(R_501, _("Illegal PORT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3612,6 +3918,8 @@ MODRET core_port(cmd_rec *cmd) {
     pr_log_debug(DEBUG1, "error getting sockaddr for '%s': %s", buf,
       strerror(errno)); 
     pr_response_add_err(R_501, _("Illegal PORT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3633,14 +3941,18 @@ MODRET core_port(cmd_rec *cmd) {
       c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress",
         FALSE);
       if (c != NULL) {
-        listen_addr = c->argv[0];
+        if (c->argv[0] != NULL) {
+          listen_addr = c->argv[0];
+        }
       }
     }
 
   } else {
     c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
     if (c != NULL) {
-      listen_addr = c->argv[0];
+      if (c->argv[0] != NULL) {
+        listen_addr = c->argv[0];
+      }
     }
   }
  
@@ -3668,7 +3980,7 @@ MODRET core_port(cmd_rec *cmd) {
 
   if (allow_foreign_addr == NULL ||
       *allow_foreign_addr == FALSE) {
-    pr_netaddr_t *remote_addr = session.c->remote_addr;
+    const pr_netaddr_t *remote_addr = session.c->remote_addr;
 
 #ifdef PR_USE_IPV6
     if (pr_netaddr_use_ipv6()) {
@@ -3680,6 +3992,8 @@ MODRET core_port(cmd_rec *cmd) {
         pr_log_pri(PR_LOG_WARNING,
           "Refused PORT %s (IPv4/IPv6 address mismatch)", cmd->arg);
         pr_response_add_err(R_500, _("Illegal PORT command"));
+
+        pr_cmd_set_errno(cmd, EPERM);
         errno = EPERM;
         return PR_ERROR(cmd);
       }
@@ -3690,6 +4004,8 @@ MODRET core_port(cmd_rec *cmd) {
       pr_log_pri(PR_LOG_WARNING, "Refused PORT %s (address mismatch)",
         cmd->arg);
       pr_response_add_err(R_500, _("Illegal PORT command"));
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
     }
@@ -3706,6 +4022,8 @@ MODRET core_port(cmd_rec *cmd) {
       "Refused PORT %s (port %d below 1024, possible bounce attack)", cmd->arg,
       port);
     pr_response_add_err(R_500, _("Illegal PORT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3727,7 +4045,8 @@ MODRET core_port(cmd_rec *cmd) {
 }
 
 MODRET core_eprt(cmd_rec *cmd) {
-  pr_netaddr_t na, *listen_addr = NULL;
+  const pr_netaddr_t *listen_addr = NULL;
+  pr_netaddr_t na;
   int family = 0;
   unsigned short port = 0;
   unsigned char *allow_foreign_addr = NULL, *root_revoke = NULL;
@@ -3738,6 +4057,8 @@ MODRET core_eprt(cmd_rec *cmd) {
 
   if (session.sf_flags & SF_EPSV_ALL) {
     pr_response_add_err(R_500, _("Illegal EPRT command, EPSV ALL in effect"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3751,9 +4072,11 @@ MODRET core_eprt(cmd_rec *cmd) {
     int xerrno = EPERM;
 
     pr_log_debug(DEBUG8, "EPRT denied by <Limit> configuration");
-    pr_response_add_err(R_501, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
-    errno = EPERM;
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
@@ -3775,6 +4098,8 @@ MODRET core_eprt(cmd_rec *cmd) {
     pr_log_debug(DEBUG0, "RootRevoke in effect, unable to bind to local "
       "port %d for active transfer", session.c->local_port-1);
     pr_response_add_err(R_500, _("Unable to service EPRT commands"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3812,6 +4137,8 @@ MODRET core_eprt(cmd_rec *cmd) {
 #else
       pr_response_add_err(R_522, _("Network protocol not supported, use (1)"));
 #endif /* PR_USE_IPV6 */
+
+      pr_cmd_set_errno(cmd, EINVAL);
       errno = EINVAL;
       return PR_ERROR(cmd);
   }
@@ -3829,14 +4156,19 @@ MODRET core_eprt(cmd_rec *cmd) {
 
   } else {
     pr_response_add_err(R_501, _("Illegal EPRT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   tmp = strchr(argstr, delim);
   if (tmp == NULL) {
-    pr_log_debug(DEBUG3, "badly formatted EPRT argument: '%s'", cmd->argv[1]);
+    pr_log_debug(DEBUG3, "badly formatted EPRT argument: '%s'",
+      (char *) cmd->argv[1]);
     pr_response_add_err(R_501, _("Illegal EPRT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3863,6 +4195,8 @@ MODRET core_eprt(cmd_rec *cmd) {
         pr_log_debug(DEBUG2, "error converting IPv4 address '%s': %s",
           argstr, strerror(errno));
         pr_response_add_err(R_501, _("Illegal EPRT command"));
+
+        pr_cmd_set_errno(cmd, EPERM);
         errno = EPERM;
         return PR_ERROR(cmd);
       }
@@ -3880,6 +4214,8 @@ MODRET core_eprt(cmd_rec *cmd) {
         pr_log_debug(DEBUG2, "error converting IPv6 address '%s': %s",
           argstr, strerror(errno));
         pr_response_add_err(R_501, _("Illegal EPRT command"));
+
+        pr_cmd_set_errno(cmd, EPERM);
         errno = EPERM;
         return PR_ERROR(cmd);
       }
@@ -3900,8 +4236,11 @@ MODRET core_eprt(cmd_rec *cmd) {
    * parameter.
    */
   if (*argstr != delim) {
-    pr_log_debug(DEBUG3, "badly formatted EPRT argument: '%s'", cmd->argv[1]);
+    pr_log_debug(DEBUG3, "badly formatted EPRT argument: '%s'",
+      (char *) cmd->argv[1]);
     pr_response_add_err(R_501, _("Illegal EPRT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -3923,14 +4262,18 @@ MODRET core_eprt(cmd_rec *cmd) {
       c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress",
         FALSE);
       if (c != NULL) {
-        listen_addr = c->argv[0];
+        if (c->argv[0] != NULL) {
+          listen_addr = c->argv[0];
+        }
       }
     }
 
   } else {
     c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
     if (c != NULL) {
-      listen_addr = c->argv[0];
+      if (c->argv[0] != NULL) {
+        listen_addr = c->argv[0];
+      }
     }
   }
 
@@ -3963,6 +4306,8 @@ MODRET core_eprt(cmd_rec *cmd) {
       pr_log_pri(PR_LOG_WARNING, "Refused EPRT %s (address mismatch)",
         cmd->arg);
       pr_response_add_err(R_500, _("Illegal EPRT command"));
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
     }
@@ -3979,6 +4324,8 @@ MODRET core_eprt(cmd_rec *cmd) {
       "Refused EPRT %s (port %d below 1024, possible bounce attack)", cmd->arg,
       port);
     pr_response_add_err(R_500, _("Illegal EPRT command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -4019,7 +4366,7 @@ MODRET core_epsv(cmd_rec *cmd) {
   int family = 0;
   int epsv_min_port = 1024, epsv_max_port = 65535;
   config_rec *c = NULL;
-  pr_netaddr_t *bind_addr;
+  const pr_netaddr_t *bind_addr;
 
   CHECK_CMD_MIN_ARGS(cmd, 1);
 
@@ -4030,18 +4377,23 @@ MODRET core_epsv(cmd_rec *cmd) {
     int xerrno = EPERM;
 
     pr_log_debug(DEBUG8, "EPSV denied by <Limit> configuration");
-    pr_response_add_err(R_501, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  if (cmd->argc-1 == 1)
+  if (cmd->argc-1 == 1) {
     arg = pstrdup(cmd->tmp_pool, cmd->argv[1]);
+  }
 
   if (arg && strcasecmp(arg, "all") == 0) {
     session.sf_flags |= SF_EPSV_ALL;
     pr_response_add(R_200, _("EPSV ALL command successful"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_HANDLED(cmd);
   }
@@ -4055,7 +4407,9 @@ MODRET core_epsv(cmd_rec *cmd) {
 
     if (endp && *endp) {
       pr_response_add_err(R_501, _("%s: unknown network protocol"),
-        cmd->argv[0]);
+        (char *) cmd->argv[0]);
+
+      pr_cmd_set_errno(cmd, EINVAL);
       errno = EINVAL;
       return PR_ERROR(cmd);
     }
@@ -4104,6 +4458,8 @@ MODRET core_epsv(cmd_rec *cmd) {
 #else
       pr_response_add_err(R_522, _("Network protocol not supported, use (1)"));
 #endif /* PR_USE_IPV6 */
+
+      pr_cmd_set_errno(cmd, EINVAL);
       errno = EINVAL;
       return PR_ERROR(cmd);
   }
@@ -4158,6 +4514,8 @@ MODRET core_epsv(cmd_rec *cmd) {
   if (session.d == NULL) {
     pr_response_add_err(R_425,
       _("Unable to build data connection: Internal error"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
@@ -4174,8 +4532,10 @@ MODRET core_epsv(cmd_rec *cmd) {
   if (pr_inet_listen(session.pool, session.d, 1, 0) < 0) {
     int xerrno = errno;
 
-    pr_response_add_err(R_425, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_425, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -4220,8 +4580,9 @@ MODRET core_help(cmd_rec *cmd) {
   } else {
     char *cp;
 
-    for (cp = cmd->argv[1]; *cp; cp++)
+    for (cp = cmd->argv[1]; *cp; cp++) {
       *cp = toupper(*cp);
+    }
 
     if (strcasecmp(cmd->argv[1], C_SITE) == 0) {
       return pr_module_call(&site_module, site_dispatch, cmd);
@@ -4231,7 +4592,10 @@ MODRET core_help(cmd_rec *cmd) {
       return PR_HANDLED(cmd);
     }
 
-    pr_response_add_err(R_502, _("Unknown command '%s'"), cmd->argv[1]);
+    pr_response_add_err(R_502, _("Unknown command '%s'"),
+      (char *) cmd->argv[1]);
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
@@ -4240,7 +4604,6 @@ MODRET core_help(cmd_rec *cmd) {
 }
 
 MODRET core_host(cmd_rec *cmd) {
-#ifdef PR_USE_HOST
   const char *local_ipstr;
   char *host;
   size_t hostlen;
@@ -4248,7 +4611,9 @@ MODRET core_host(cmd_rec *cmd) {
   int found_ipv6 = FALSE;
 
   if (cmd->argc != 2) {
-    pr_response_add_err(R_500, _("'%s' not understood"), cmd->argv[0]);
+    pr_response_add_err(R_500, _("'%s' not understood"), (char *) cmd->argv[0]);
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
@@ -4256,35 +4621,50 @@ MODRET core_host(cmd_rec *cmd) {
   if (session.user != NULL) {
     pr_log_debug(DEBUG0,
       "HOST '%s' command received after login, refusing HOST command",
-      cmd->argv[1]);
+      (char *) cmd->argv[1]);
 
     /* Per HOST spec, HOST after successful USER/PASS is not allowed. */
     pr_response_add_err(R_503, _("Bad sequence of commands"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
 
-  /* XXX Need checking of a <Limit> for HOST commands, so that HOST can be
-   * denied via configuration if need be.
-   */
+  if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.cwd, NULL)) {
+    int xerrno = EACCES;
+
+    pr_response_add_err(R_504, "%s: %s", (char *) cmd->argv[1],
+      strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
-  /* XXX Should there be a limit on the number of HOST commands that a client
+  /* Should there be a limit on the number of HOST commands that a client
    * can send?
+   *
+   * In practice, this will be limited by the TimeoutLogin time interval;
+   * a client can send as many HOST commands as it wishes, as long as it
+   * successfully authenticates in that time.
    */
 
-#if 0
   /* If the user has already authenticated or negotiated a RFC2228 mechanism,
    * then the HOST command is too late.
    */
-  if (session.rfc2228_mech != NULL) {
+  if (session.rfc2228_mech != NULL &&
+      pr_table_get(session.notes, "mod_tls.sni", NULL) == NULL) {
     pr_log_debug(DEBUG0, "HOST '%s' command received after client has "
-      "requested RFC2228 protection, refusing HOST command", cmd->argv[1]);
+      "requested RFC2228 protection (%s), refusing HOST command",
+      (char *) cmd->argv[1], session.rfc2228_mech);
 
     pr_response_add_err(R_503, _("Bad sequence of commands"));
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
-#endif
 
   host = cmd->argv[1];
   hostlen = strlen(host);
@@ -4298,6 +4678,8 @@ MODRET core_host(cmd_rec *cmd) {
       if (host[hostlen-1] != ']') {
         pr_response_add_err(R_501, _("%s: Invalid IPv6 address provided"),
           host);
+
+        pr_cmd_set_errno(cmd, EINVAL);
         errno = EINVAL;
         return PR_ERROR(cmd);
       }
@@ -4311,7 +4693,9 @@ MODRET core_host(cmd_rec *cmd) {
           "refusing HOST command", host);
 
         pr_response_add_err(R_501, _("%s: Invalid IPv6 address provided"),
-          cmd->argv[1]);
+          (char *) cmd->argv[1]);
+
+        pr_cmd_set_errno(cmd, EINVAL);
         errno = EINVAL;
         return PR_ERROR(cmd);
       }
@@ -4321,6 +4705,8 @@ MODRET core_host(cmd_rec *cmd) {
     } else {
       pr_response_add_err(R_501, _("%s: Invalid hostname provided"),
         host);
+
+      pr_cmd_set_errno(cmd, EINVAL);
       errno = EINVAL;
       return PR_ERROR(cmd);
     }
@@ -4337,11 +4723,20 @@ MODRET core_host(cmd_rec *cmd) {
         pr_log_debug(DEBUG0, "HOST '%s' requested, but client connected to "
           "IPv4 address '%s', refusing HOST command", host, local_ipstr);
         pr_response_add_err(R_504, _("%s: Unknown hostname provided"),
-          cmd->argv[1]);
+          (char *) cmd->argv[1]);
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
     }
 
+    (void) pr_table_remove(session.notes, "mod_core.host", NULL);
+    if (pr_table_add_dup(session.notes, "mod_core.host", host, 0) < 0) {
+      pr_trace_msg("command", 3,
+        "error stashing 'mod_core.host' in session.notes: %s", strerror(errno));
+    }
+
     /* No need to send the banner information again, since we didn't actually
      * change the virtual host used by the client.
      */
@@ -4361,6 +4756,8 @@ MODRET core_host(cmd_rec *cmd) {
 
         pr_response_add_err(R_501, _("%s: Invalid IPv6 address provided"),
           host);
+
+        pr_cmd_set_errno(cmd, EINVAL);
         errno = EINVAL;
         return PR_ERROR(cmd);
       }
@@ -4372,11 +4769,20 @@ MODRET core_host(cmd_rec *cmd) {
         pr_log_debug(DEBUG0, "HOST '%s' requested, but client connected to "
           "IPv6 address '%s', refusing HOST command", host, local_ipstr);
         pr_response_add_err(R_504, _("%s: Unknown hostname provided"),
-          cmd->argv[1]);
+          (char *) cmd->argv[1]);
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
     }
 
+    (void) pr_table_remove(session.notes, "mod_core.host", NULL);
+    if (pr_table_add_dup(session.notes, "mod_core.host", host, 0) < 0) {
+      pr_trace_msg("command", 3,
+        "error stashing 'mod_core.host' in session.notes: %s", strerror(errno));
+    }
+
     /* No need to send the banner information again, since we didn't actually
      * change the virtual host used by the client.
      */
@@ -4391,22 +4797,55 @@ MODRET core_host(cmd_rec *cmd) {
   if (strchr(host, ':') != NULL) {
     /* Hostnames cannot contain colon characters. */
     pr_response_add_err(R_501, _("%s: Invalid hostname provided"), host);
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   named_server = pr_namebind_get_server(host, main_server->addr,
-    main_server->ServerPort);
+    session.c->local_port);
   if (named_server == NULL) {
     pr_log_debug(DEBUG0, "Unknown host '%s' requested on %s#%d, "
       "refusing HOST command", host, local_ipstr, main_server->ServerPort);
 
     pr_response_add_err(R_504, _("%s: Unknown hostname provided"),
-      cmd->argv[1]);
+      (char *) cmd->argv[1]);
+
+    pr_cmd_set_errno(cmd, ENOENT);
     errno = ENOENT;
     return PR_ERROR(cmd);
   }
 
+  if (session.rfc2228_mech != NULL &&
+      strncmp(session.rfc2228_mech, "TLS", 4) == 0) {
+    const char *sni = NULL;
+
+    /* If the TLS client used the SNI extension, ensure that the SNI name
+     * matches the HOST name, per RFC 7151, Section 3.2.2.  Otherwise, we
+     * reject the HOST command.
+     */
+    sni = pr_table_get(session.notes, "mod_tls.sni", NULL);
+    if (sni != NULL) {
+      if (strcasecmp(sni, host) != 0) {
+        pr_log_debug(DEBUG0, "HOST '%s' requested, but client connected via "
+          "TLS to SNI '%s', refusing HOST command", host, sni);
+        pr_response_add_err(R_504, _("%s: Unknown hostname provided"),
+          (char *) cmd->argv[1]);
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
+        return PR_ERROR(cmd);
+      }
+    }
+  }
+
+  (void) pr_table_remove(session.notes, "mod_core.host", NULL);
+  if (pr_table_add_dup(session.notes, "mod_core.host", host, 0) < 0) {
+    pr_trace_msg("command", 3,
+      "error stashing 'mod_core.host' in session.notes: %s", strerror(errno));
+  }
+
   if (named_server != main_server) {
     /* Set a session flag indicating that the main_server pointer changed. */
     pr_log_debug(DEBUG0,
@@ -4414,6 +4853,8 @@ MODRET core_host(cmd_rec *cmd) {
       named_server->ServerName, host);
     session.prev_server = main_server;
     main_server = named_server;
+
+    pr_event_generate("core.session-reinit", named_server);
   }
 
   /* XXX Ultimately, if HOST is successful, we change the main_server pointer
@@ -4425,6 +4866,63 @@ MODRET core_host(cmd_rec *cmd) {
    * AuthOrder, timeouts, etc etc.  (Unfortunately, POST_CMD handlers cannot
    * fail the given command; for modules which then need to end the
    * connection, they'll need to use pr_session_disconnect().)
+   *
+   * Modules implementing post_host handlers:
+   *   mod_core
+   *
+   * Modules implementing 'sess-reinit' event handlers:
+   *   mod_auth
+   *   mod_auth_file
+   *   mod_auth_unix
+   *   mod_ban
+   *   mod_cap
+   *   mod_copy
+   *   mod_deflate
+   *   mod_delay
+   *   mod_dnsbl
+   *   mod_exec
+   *   mod_facts
+   *   mod_ident
+   *   mod_ldap
+   *   mod_log
+   *   mod_log_forensic
+   *   mod_memcache
+   *   mod_qos
+   *   mod_quotatab
+   *   mod_radius
+   *   mod_rewrite
+   *   mod_site_misc
+   *   mod_sql
+   *   mod_sql_passwd
+   *   mod_tls
+   *   mod_wrap
+   *   mod_wrap2
+   *   mod_xfer
+   *
+   * Modules that MIGHT need a session-reinit listener:
+   *   mod_ratio
+   *   mod_snmp
+   *
+   * Modules that DO NOT NEED a session-reinit listener:
+   *   mod_auth_pam
+   *   mod_ctrls_admin
+   *   mod_dynmasq
+   *   mod_ifsession
+   *   mod_ifversion
+   *   mod_load
+   *   mod_readme
+   *   mod_sftp (HOST command is FTP only)
+   *   mod_sftp_pam
+   *   mod_sftp_sql
+   *   mod_shaper
+   *   mod_sql_mysql
+   *   mod_sql_postgres
+   *   mod_sql_odbc
+   *   mod_sql_sqlite
+   *   mod_tls_fscache
+   *   mod_tls_memcache
+   *   mod_tls_shmcache
+   *   mod_unique_id
    */
 
   /* XXX Will this function need to use pr_response_add(), rather than
@@ -4436,9 +4934,6 @@ MODRET core_host(cmd_rec *cmd) {
 
   pr_session_send_banner(main_server, 0);
   return PR_HANDLED(cmd);
-#else
-  return PR_DECLINED(cmd);
-#endif /* PR_USE_HOST */
 }
 
 MODRET core_post_host(cmd_rec *cmd) {
@@ -4450,8 +4945,11 @@ MODRET core_post_host(cmd_rec *cmd) {
     int res;
     config_rec *c;
 
+    /* Reset the FS options */
+    (void) pr_fsio_set_options(0UL);
+
     /* Remove the TimeoutIdle timer. */
-    (void) pr_timer_remove(PR_TIMER_IDLE, NULL);
+    (void) pr_timer_remove(PR_TIMER_IDLE, ANY_MODULE);
 
     /* Restore the original TimeoutLinger value. */
     pr_data_set_linger(PR_TUNABLE_TIMEOUTLINGER);
@@ -4468,7 +4966,7 @@ MODRET core_post_host(cmd_rec *cmd) {
       pr_signals_handle();
 
       if (pr_env_unset(session.pool, c->argv[0]) < 0) {
-        pr_log_debug(DEBUG0, "unable to unset environ variable '%s': %s",
+        pr_log_debug(DEBUG0, "unable to unset environment variable '%s': %s",
           (char *) c->argv[0], strerror(errno));
       }
 
@@ -4514,17 +5012,22 @@ MODRET core_post_host(cmd_rec *cmd) {
   return PR_DECLINED(cmd);
 }
 
+MODRET core_clnt(cmd_rec *cmd) {
+  pr_response_add(R_200, _("OK"));
+  return PR_HANDLED(cmd);
+}
+
 MODRET core_syst(cmd_rec *cmd) {
   pr_response_add(R_215, "UNIX Type: L8");
   return PR_HANDLED(cmd);
 }
 
-int core_chgrp(cmd_rec *cmd, char *dir, uid_t uid, gid_t gid) {
+int core_chgrp(cmd_rec *cmd, const char *path, uid_t uid, gid_t gid) {
   char *cmd_name;
 
   cmd_name = cmd->argv[0];
   pr_cmd_set_name(cmd, "SITE_CHGRP");
-  if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, dir, NULL)) {
+  if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
     pr_log_debug(DEBUG7, "SITE CHGRP command denied by <Limit> config");
     pr_cmd_set_name(cmd, cmd_name);
 
@@ -4533,15 +5036,15 @@ int core_chgrp(cmd_rec *cmd, char *dir, uid_t uid, gid_t gid) {
   }
   pr_cmd_set_name(cmd, cmd_name);
 
-  return pr_fsio_lchown(dir, uid, gid);
+  return pr_fsio_lchown(path, uid, gid);
 }
 
-int core_chmod(cmd_rec *cmd, char *dir, mode_t mode) {
+int core_chmod(cmd_rec *cmd, const char *path, mode_t mode) {
   char *cmd_name;
 
   cmd_name = cmd->argv[0];
   pr_cmd_set_name(cmd, "SITE_CHMOD");
-  if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, dir, NULL)) {
+  if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
     pr_log_debug(DEBUG7, "SITE CHMOD command denied by <Limit> config");
     pr_cmd_set_name(cmd, cmd_name);
 
@@ -4550,27 +5053,43 @@ int core_chmod(cmd_rec *cmd, char *dir, mode_t mode) {
   }
   pr_cmd_set_name(cmd, cmd_name);
 
-  return pr_fsio_chmod(dir,mode);
+  return pr_fsio_chmod(path, mode);
 }
 
-MODRET _chdir(cmd_rec *cmd, char *ndir) {
-  char *dir, *odir, *cdir;
+MODRET core_chdir(cmd_rec *cmd, char *ndir) {
+  char *dir, *orig_dir, *cdir;
+  int xerrno = 0;
   config_rec *c = NULL, *cdpath;
-  unsigned char show_symlinks = TRUE, *tmp = NULL;
+  unsigned char show_symlinks = TRUE, *ptr = NULL;
+  struct stat st;
+
+  orig_dir = ndir;
+
+  pr_fs_clear_cache2(ndir);
+  if (pr_fsio_lstat(ndir, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char buf[PR_TUNABLE_PATH_MAX];
+      int len;
 
-  odir = ndir;
-  pr_fs_clear_cache();
+      len = dir_readlink(cmd->tmp_pool, ndir, buf, sizeof(buf)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        buf[len] = '\0';
+        ndir = pstrdup(cmd->tmp_pool, buf);
+      }
+    }
+  }
 
-  tmp = get_param_ptr(TOPLEVEL_CONF, "ShowSymlinks", FALSE);
-  if (tmp != NULL)
-    show_symlinks = *tmp;
+  ptr = get_param_ptr(TOPLEVEL_CONF, "ShowSymlinks", FALSE);
+  if (ptr != NULL) {
+    show_symlinks = *ptr;
+  }
 
   if (show_symlinks) {
     int use_cdpath = FALSE;
 
     dir = dir_realpath(cmd->tmp_pool, ndir);
-
-    if (!dir) {
+    if (dir == NULL) {
       use_cdpath = TRUE;
     }
 
@@ -4579,19 +5098,21 @@ MODRET _chdir(cmd_rec *cmd, char *ndir) {
 
       allowed_access = dir_check_full(cmd->tmp_pool, cmd, cmd->group, dir,
         NULL);
-      if (!allowed_access)
+      if (!allowed_access) {
         use_cdpath = TRUE;
+      }
     }
 
-    if (!use_cdpath &&
+    if (use_cdpath == FALSE &&
         pr_fsio_chdir(dir, 0) < 0) {
+      xerrno = errno;
       use_cdpath = TRUE;
     }
 
     if (use_cdpath) {
       for (cdpath = find_config(main_server->conf, CONF_PARAM, "CDPath", TRUE);
-          cdpath != NULL; cdpath =
-            find_config_next(cdpath,cdpath->next,CONF_PARAM,"CDPath",TRUE)) {
+          cdpath != NULL;
+          cdpath = find_config_next(cdpath, cdpath->next, CONF_PARAM, "CDPath", TRUE)) {
         cdir = palloc(cmd->tmp_pool, strlen(cdpath->argv[0]) + strlen(ndir) + 2);
         snprintf(cdir, strlen(cdpath->argv[0]) + strlen(ndir) + 2,
                  "%s%s%s", (char *) cdpath->argv[0],
@@ -4606,11 +5127,14 @@ MODRET _chdir(cmd_rec *cmd, char *ndir) {
         }
       }
 
-      if (!cdpath) {
-        int xerrno = errno;
+      if (cdpath == FALSE) {
+        if (xerrno == 0) {
+          xerrno = errno;
+        }
 
-        pr_response_add_err(R_550, "%s: %s", odir, strerror(xerrno));
+        pr_response_add_err(R_550, "%s: %s", orig_dir, strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
@@ -4642,9 +5166,9 @@ MODRET _chdir(cmd_rec *cmd, char *ndir) {
     }            
 
     if (use_cdpath) {
-      for (cdpath = find_config(main_server->conf,CONF_PARAM,"CDPath",TRUE);
-          cdpath != NULL; cdpath =
-            find_config_next(cdpath,cdpath->next,CONF_PARAM,"CDPath",TRUE)) {
+      for (cdpath = find_config(main_server->conf, CONF_PARAM, "CDPath", TRUE);
+          cdpath != NULL;
+          cdpath = find_config_next(cdpath, cdpath->next, CONF_PARAM, "CDPath", TRUE)) {
         cdir = palloc(cmd->tmp_pool, strlen(cdpath->argv[0]) + strlen(ndir) + 2);
         snprintf(cdir, strlen(cdpath->argv[0]) + strlen(ndir) + 2,
                  "%s%s%s", (char *) cdpath->argv[0],
@@ -4660,11 +5184,14 @@ MODRET _chdir(cmd_rec *cmd, char *ndir) {
         }
       }
 
-      if (!cdpath) {
-        int xerrno = errno;
+      if (cdpath == NULL) {
+        if (xerrno == 0) {
+          xerrno = errno;
+        }
 
-        pr_response_add_err(R_550, "%s: %s", odir, strerror(xerrno));
+        pr_response_add_err(R_550, "%s: %s", orig_dir, strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
@@ -4683,18 +5210,17 @@ MODRET _chdir(cmd_rec *cmd, char *ndir) {
       FALSE);
   }
 
-  if (!c &&
-      session.anon_config) {
+  if (c == NULL &&
+      session.anon_config != NULL) {
     c = find_config(session.anon_config->subset, CONF_PARAM, "DisplayChdir",
       FALSE);
   }
 
-  if (!c) {
+  if (c == NULL) {
     c = find_config(cmd->server->conf, CONF_PARAM, "DisplayChdir", FALSE);
   }
 
-  if (c) {
-    struct stat st;
+  if (c != NULL) {
     time_t prev = 0;
 
     char *display = c->argv[0];
@@ -4735,17 +5261,33 @@ MODRET _chdir(cmd_rec *cmd, char *ndir) {
     }
   }
 
-  pr_response_add(R_250, _("%s command successful"), cmd->argv[0]);
+  pr_response_add(R_250, _("%s command successful"), (char *) cmd->argv[0]);
   return PR_HANDLED(cmd);
 }
 
 MODRET core_rmd(cmd_rec *cmd) {
   int res;
-  char *dir;
+  char *decoded_path, *dir;
+  struct stat st;
 
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
-  dir = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  dir = decoded_path;
 
   res = pr_filter_allow_path(CURRENT_CONF, dir);
   switch (res) {
@@ -4753,29 +5295,55 @@ MODRET core_rmd(cmd_rec *cmd) {
       break;
 
     case PR_FILTER_ERR_FAILS_ALLOW_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-        dir);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+        (char *) cmd->argv[0], dir);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd); 
  
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-        dir);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+        (char *) cmd->argv[0], dir);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
   }
 
-  /* If told to rmdir a symlink to a directory, don't; you can't rmdir a
-   * symlink, you delete it.
-   */
-  dir = dir_canonical_path(cmd->tmp_pool, dir);
+  pr_fs_clear_cache2(dir);
+  if (pr_fsio_lstat(dir, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char buf[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(buf, '\0', sizeof(buf));
+      len = dir_readlink(cmd->tmp_pool, dir, buf, sizeof(buf)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        buf[len] = '\0';
+        dir = pstrdup(cmd->tmp_pool, buf);
+
+      } else {
+        dir = dir_canonical_path(cmd->tmp_pool, dir);
+      }
+
+    } else {
+      dir = dir_canonical_path(cmd->tmp_pool, dir);
+    }
+
+  } else {
+    dir = dir_canonical_path(cmd->tmp_pool, dir);
+  }
+
   if (dir == NULL) {
     int xerrno = EINVAL;
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -4785,6 +5353,7 @@ MODRET core_rmd(cmd_rec *cmd) {
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -4792,24 +5361,25 @@ MODRET core_rmd(cmd_rec *cmd) {
   if (pr_fsio_rmdir(dir) < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error removing directory '%s': %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, dir,
-      strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error removing directory '%s': %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), dir, strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  pr_response_add(R_250, _("%s command successful"), cmd->argv[0]);
+  pr_response_add(R_250, _("%s command successful"), (char *) cmd->argv[0]);
   return PR_HANDLED(cmd);
 }
 
 MODRET core_mkd(cmd_rec *cmd) {
   int res;
-  char *dir;
+  char *decoded_path, *dir;
 
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
@@ -4818,11 +5388,28 @@ MODRET core_mkd(cmd_rec *cmd) {
    */
   if (strchr(cmd->arg, '*')) {
     pr_response_add_err(R_550, _("%s: Invalid directory name"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  dir = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  dir = decoded_path;
 
   res = pr_filter_allow_path(CURRENT_CONF, dir);
   switch (res) {
@@ -4830,16 +5417,20 @@ MODRET core_mkd(cmd_rec *cmd) {
       break;
 
     case PR_FILTER_ERR_FAILS_ALLOW_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-        dir);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+        (char *) cmd->argv[0], dir);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd); 
  
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-        dir);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+        (char *) cmd->argv[0], dir);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
   }
@@ -4850,6 +5441,7 @@ MODRET core_mkd(cmd_rec *cmd) {
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -4857,9 +5449,11 @@ MODRET core_mkd(cmd_rec *cmd) {
   if (!dir_check_canon(cmd->tmp_pool, cmd, cmd->group, dir, NULL)) {
     int xerrno = EACCES;
 
-    pr_log_debug(DEBUG8, "%s command denied by <Limit> config", cmd->argv[0]);
+    pr_log_debug(DEBUG8, "%s command denied by <Limit> config",
+      (char *) cmd->argv[0]);
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -4868,46 +5462,96 @@ MODRET core_mkd(cmd_rec *cmd) {
       session.fsgid) < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error making directory '%s': %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, dir,
-      strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error making directory '%s': %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), dir, strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
  
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   pr_response_add(R_257, _("\"%s\" - Directory successfully created"),
-    quote_dir(cmd, dir));
+    quote_dir(cmd->tmp_pool, dir));
 
   return PR_HANDLED(cmd);
 }
 
 MODRET core_cwd(cmd_rec *cmd) {
-  char *dir;
+  char *decoded_path;
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
-  dir = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
-  return _chdir(cmd, dir);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  return core_chdir(cmd, decoded_path);
 }
 
 MODRET core_cdup(cmd_rec *cmd) {
   CHECK_CMD_ARGS(cmd, 1);
-  return _chdir(cmd, "..");
+  return core_chdir(cmd, "..");
 }
 
-/* Returns the modification time of a file, as per RFC3659.
- */
+/* Returns the modification time of a file, as per RFC3659. */
 MODRET core_mdtm(cmd_rec *cmd) {
-  char *path;
+  char *decoded_path, *path;
   struct stat st;
 
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
-  path = dir_realpath(cmd->tmp_pool,
-    pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  path = decoded_path;
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char buf[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(buf, '\0', sizeof(buf));
+      len = dir_readlink(cmd->tmp_pool, path, buf, sizeof(buf)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        buf[len] = '\0';
+        path = pstrdup(cmd->tmp_pool, buf);
+
+      } else {
+        path = dir_realpath(cmd->tmp_pool, decoded_path);
+      }
+
+    } else {
+      path = dir_realpath(cmd->tmp_pool, decoded_path);
+    }
+  }
 
   if (!path ||
       !dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL) ||
@@ -4916,12 +5560,15 @@ MODRET core_mdtm(cmd_rec *cmd) {
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
 
   } else {
     if (!S_ISREG(st.st_mode)) {
       pr_response_add_err(R_550, _("%s: not a plain file"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EINVAL);
       errno = EINVAL;
       return PR_ERROR(cmd);
 
@@ -4932,7 +5579,7 @@ MODRET core_mdtm(cmd_rec *cmd) {
       memset(buf, '\0', sizeof(buf));
 
       tm = pr_gmtime(cmd->tmp_pool, &st.st_mtime);
-      if (tm) {
+      if (tm != NULL) {
         snprintf(buf, sizeof(buf), "%04d%02d%02d%02d%02d%02d",
           tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour,
           tm->tm_min, tm->tm_sec);
@@ -4949,37 +5596,88 @@ MODRET core_mdtm(cmd_rec *cmd) {
 }
 
 MODRET core_size(cmd_rec *cmd) {
-  char *path;
+  char *decoded_path, *path;
   struct stat st;
 
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
+  /* The PR_ALLOW_ASCII_MODE_SIZE macro should ONLY be defined at compile time,
+   * e.g. using:
+   *
+   *  $ ./configure CPPFLAGS=-DPR_ALLOW_ASCII_MODE_SIZE ...
+   *
+   * Define this macro if you want proftpd to handle a SIZE command while in
+   * ASCII mode.  Note, however, that ProFTPD will NOT properly calculate
+   * CRLF sequences EVEN if this macro is defined: ProFTPD will always return
+   * the number of bytes on disk for the requested file, even if the number of
+   * bytes transferred when that file is downloaded is different.  Thus this
+   * behavior will not comply with RFC 3659, Section 4.  Caveat emptor.
+   */
+#ifndef PR_ALLOW_ASCII_MODE_SIZE
   /* Refuse the command if we're in ASCII mode. */
   if (session.sf_flags & SF_ASCII) {
-    pr_log_debug(DEBUG5, "%s not allowed in ASCII mode", cmd->argv[0]);
-    pr_response_add_err(R_550, _("%s not allowed in ASCII mode"), cmd->argv[0]);
+    pr_log_debug(DEBUG5, "%s not allowed in ASCII mode", (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, _("%s not allowed in ASCII mode"),
+      (char *) cmd->argv[0]);
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
+#endif /* PR_ALLOW_ASCII_MODE_SIZE */
 
-  path = dir_realpath(cmd->tmp_pool,
-    pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
 
-  pr_fs_clear_cache();
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
 
-  if (!path ||
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  pr_fs_clear_cache2(decoded_path);
+  if (pr_fsio_lstat(decoded_path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char buf[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(buf, '\0', sizeof(buf));
+      len = dir_readlink(cmd->tmp_pool, decoded_path, buf, sizeof(buf)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        buf[len] = '\0';
+        decoded_path = pstrdup(cmd->tmp_pool, buf);
+      }
+    }
+  }
+
+  path = dir_realpath(cmd->tmp_pool, decoded_path);
+  if (path != NULL) {
+    pr_fs_clear_cache2(path);
+  }
+
+  if (path == NULL ||
       !dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL) ||
       pr_fsio_stat(path, &st) == -1) {
     int xerrno = errno;
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
 
   } else {
     if (!S_ISREG(st.st_mode)) {
       pr_response_add_err(R_550, _("%s: not a regular file"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EINVAL);
       errno = EINVAL;
       return PR_ERROR(cmd);
 
@@ -4993,12 +5691,27 @@ MODRET core_size(cmd_rec *cmd) {
 
 MODRET core_dele(cmd_rec *cmd) {
   int res;
-  char *path, *fullpath;
+  char *decoded_path, *path, *fullpath;
   struct stat st;
 
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
-  path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  path = decoded_path;
 
   res = pr_filter_allow_path(CURRENT_CONF, path);
   switch (res) {
@@ -5006,16 +5719,20 @@ MODRET core_dele(cmd_rec *cmd) {
       break;
 
     case PR_FILTER_ERR_FAILS_ALLOW_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd); 
  
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
   }
@@ -5027,6 +5744,7 @@ MODRET core_dele(cmd_rec *cmd) {
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5037,6 +5755,7 @@ MODRET core_dele(cmd_rec *cmd) {
     pr_log_debug(DEBUG7, "deleting '%s' denied by <Limit> configuration", path);
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5046,13 +5765,14 @@ MODRET core_dele(cmd_rec *cmd) {
    * so we need to use lstat(), not stat(), lest we log the wrong size.
    */
   memset(&st, 0, sizeof(st));
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   if (pr_fsio_lstat(path, &st) < 0) {
     int xerrno = errno;
 
     pr_log_debug(DEBUG3, "unable to lstat '%s': %s", path, strerror(xerrno));
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5064,14 +5784,15 @@ MODRET core_dele(cmd_rec *cmd) {
   if (S_ISDIR(st.st_mode)) {
     int xerrno = EISDIR;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error deleting '%s': %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, path,
-      strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error deleting '%s': %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), path, strerror(xerrno));
 
     pr_log_debug(DEBUG3, "error deleting '%s': %s", path, strerror(xerrno));
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5080,14 +5801,15 @@ MODRET core_dele(cmd_rec *cmd) {
   if (pr_fsio_unlink(path) < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error deleting '%s': %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid, path,
-      strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error deleting '%s': %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), path, strerror(xerrno));
 
     pr_log_debug(DEBUG3, "error deleting '%s': %s", path, strerror(xerrno));
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5105,13 +5827,13 @@ MODRET core_dele(cmd_rec *cmd) {
       "_");
   }
 
-  pr_response_add(R_250, _("%s command successful"), cmd->argv[0]);
+  pr_response_add(R_250, _("%s command successful"), (char *) cmd->argv[0]);
   return PR_HANDLED(cmd);
 }
 
 MODRET core_rnto(cmd_rec *cmd) {
   int res;
-  char *path;
+  char *decoded_path, *path;
   unsigned char *allow_overwrite = NULL;
   struct stat st;
 
@@ -5124,11 +5846,28 @@ MODRET core_rnto(cmd_rec *cmd) {
     }
 
     pr_response_add_err(R_503, _("Bad sequence of commands"));
-    errno = EINVAL;
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
+    return PR_ERROR(cmd);
+  }
+
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  path = decoded_path;
 
   res = pr_filter_allow_path(CURRENT_CONF, path);
   switch (res) {
@@ -5136,16 +5875,20 @@ MODRET core_rnto(cmd_rec *cmd) {
       break;
 
     case PR_FILTER_ERR_FAILS_ALLOW_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd); 
  
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
   }
@@ -5157,11 +5900,13 @@ MODRET core_rnto(cmd_rec *cmd) {
   /* Deny the rename if AllowOverwrites are not allowed, and the destination
    * rename file already exists.
    */
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   if ((!allow_overwrite || *allow_overwrite == FALSE) &&
       pr_fsio_stat(path, &st) == 0) {
     pr_log_debug(DEBUG6, "AllowOverwrite denied permission for %s", path);
     pr_response_add_err(R_550, _("%s: Rename permission denied"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EACCES);
     errno = EACCES;
     return PR_ERROR(cmd);
   }
@@ -5171,19 +5916,6 @@ MODRET core_rnto(cmd_rec *cmd) {
       pr_fsio_rename(session.xfer.path, path) == -1) {
     int xerrno = errno;
 
-    if (xerrno != EXDEV) {
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error renaming '%s' to '%s': %s", cmd->argv[0], session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
-        session.xfer.path, path, strerror(xerrno));
-
-      pr_response_add_err(R_550, _("Rename %s: %s"), cmd->arg,
-        strerror(xerrno));
-
-      errno = xerrno;
-      return PR_ERROR(cmd);
-    }
-
     if (xerrno == EISDIR) {
       /* In this case, the client has requested that a directory be renamed
        * across mount points.  The pr_fs_copy_file() function can't handle
@@ -5194,10 +5926,11 @@ MODRET core_rnto(cmd_rec *cmd) {
        * client.
        */
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
         "error copying '%s' to '%s': %s (previous error was '%s')",
-        cmd->argv[0], session.user, (unsigned long) session.uid,
-        (unsigned long) session.gid, session.xfer.path, path,
+        (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), session.xfer.path, path,
         strerror(xerrno), strerror(EXDEV));
 
       pr_log_debug(DEBUG4,
@@ -5212,6 +5945,22 @@ MODRET core_rnto(cmd_rec *cmd) {
       pr_response_add_err(R_550, _("Rename %s: %s"), cmd->arg,
         strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    if (xerrno != EXDEV) {
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error renaming '%s' to '%s': %s", (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), session.xfer.path, path,
+        strerror(xerrno));
+
+      pr_response_add_err(R_550, _("Rename %s: %s"), cmd->arg,
+        strerror(xerrno));
+
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -5222,14 +5971,16 @@ MODRET core_rnto(cmd_rec *cmd) {
     if (pr_fs_copy_file(session.xfer.path, path) < 0) {
       xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error copying '%s' to '%s': %s", cmd->argv[0], session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
-        session.xfer.path, path, strerror(xerrno));
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error copying '%s' to '%s': %s", (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), session.xfer.path, path,
+        strerror(xerrno));
 
       pr_response_add_err(R_550, _("Rename %s: %s"), cmd->arg,
         strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -5260,11 +6011,26 @@ MODRET core_rnto_cleanup(cmd_rec *cmd) {
 
 MODRET core_rnfr(cmd_rec *cmd) {
   int res;
-  char *path;
+  char *decoded_path, *path;
 
   CHECK_CMD_MIN_ARGS(cmd, 2);
 
-  path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  path = decoded_path;
 
   res = pr_filter_allow_path(CURRENT_CONF, path);
   switch (res) {
@@ -5272,16 +6038,20 @@ MODRET core_rnfr(cmd_rec *cmd) {
       break;
 
     case PR_FILTER_ERR_FAILS_ALLOW_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd); 
  
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
   }
@@ -5289,13 +6059,14 @@ MODRET core_rnfr(cmd_rec *cmd) {
   /* Allow renaming a symlink, even a dangling one. */
   path = dir_canonical_path(cmd->tmp_pool, path);
 
-  if (!path ||
+  if (path == NULL ||
       !dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL) ||
-      !exists(path)) {
+      !exists2(cmd->tmp_pool, path)) {
     int xerrno = errno;
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5314,9 +6085,8 @@ MODRET core_rnfr(cmd_rec *cmd) {
   pr_table_add(session.notes, "mod_core.rnfr-path",
     pstrdup(session.xfer.p, session.xfer.path), 0);
 
-  pr_response_add(R_350, _("File or directory exists, ready for "
-    "destination name"));
-
+  pr_response_add(R_350,
+    _("File or directory exists, ready for destination name"));
   return PR_HANDLED(cmd);
 }
 
@@ -5325,43 +6095,52 @@ MODRET core_noop(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+static int feat_cmp(const void *a, const void *b) {
+  return strcasecmp(*((const char **) a), *((const char **) b));
+}
+
 MODRET core_feat(cmd_rec *cmd) {
+  register unsigned int i;
   const char *feat = NULL;
+  array_header *feats = NULL;
+
   CHECK_CMD_ARGS(cmd, 1);
 
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.vwd, NULL)) {
     int xerrno = EPERM;
 
     pr_log_debug(DEBUG3, "%s command denied by <Limit> configuration",
-      cmd->argv[0]);
-    pr_response_add_err(R_550, "%s: %s", cmd->argv[0], strerror(xerrno));
+      (char *) cmd->argv[0]);
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   feat = pr_feat_get();
-  if (feat) {
-    feat = pstrcat(cmd->tmp_pool, _("Features:"), "\r\n ", feat, NULL);
-    while (TRUE) {
-      const char *next;
+  if (feat == NULL) {
+    pr_response_add(R_211, _("No features supported"));
+    return PR_HANDLED(cmd);
+  }
 
-      pr_signals_handle();
+  feats = make_array(cmd->tmp_pool, 0, sizeof(char **));
 
-      next = pr_feat_get_next();
-      if (next == NULL) {
-        break;
-      }
-
-      feat = pstrcat(cmd->tmp_pool, feat, "\r\n ", next, NULL);
-    }
+  while (feat != NULL) {
+    pr_signals_handle();
+    *((char **) push_array(feats)) = pstrdup(cmd->tmp_pool, feat);
+    feat = pr_feat_get_next();
+  }
 
-    pr_response_add(R_211, "%s", feat);
-    pr_response_add(R_DUP, _("End"));
+  /* Sort the features, for a prettier output. */
+  qsort(feats->elts, feats->nelts, sizeof(char *), feat_cmp);
 
-  } else {
-    pr_response_add(R_211, _("No features supported"));
+  pr_response_add(R_211, "%s", _("Features:"));
+  for (i = 0; i < feats->nelts; i++) {
+    pr_response_add(R_DUP, "%s", ((const char **) feats->elts)[i]);
   }
+  pr_response_add(R_DUP, _("End"));
 
   return PR_HANDLED(cmd);
 }
@@ -5382,8 +6161,10 @@ MODRET core_opts(cmd_rec *cmd) {
 
     pr_log_debug(DEBUG2,
       "OPTS command with too many parameters (%d), rejecting", cmd->argc-1);
-    pr_response_add_err(R_550, "%s: %s", cmd->argv[0], strerror(xerrno));
+    pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[0],
+      strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5396,10 +6177,11 @@ MODRET core_opts(cmd_rec *cmd) {
     int xerrno = EACCES;
 
     pr_log_debug(DEBUG7, "OPTS %s denied by <Limit> configuration",
-      cmd->argv[1]);
-    pr_response_add_err(R_550, "%s %s: %s", cmd->argv[0], cmd->argv[1],
-      strerror(xerrno));
+      (char *) cmd->argv[1]);
+    pr_response_add_err(R_550, "%s %s: %s", (char *) cmd->argv[0],
+      (char *) cmd->argv[1], strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -5413,8 +6195,9 @@ MODRET core_opts(cmd_rec *cmd) {
   subcmd->arg = arg;
 
   res = pr_cmd_dispatch(subcmd);
-  if (res < 0)
+  if (res < 0) {
     return PR_ERROR(cmd);
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -5438,7 +6221,7 @@ MODRET core_post_pass(cmd_rec *cmd) {
       pr_timer_remove(PR_TIMER_IDLE, &core_module);
 
       if (timeout > 0) {
-        pr_timer_add(timeout, PR_TIMER_IDLE, NULL, core_idle_timeout_cb,
+        pr_timer_add(timeout, PR_TIMER_IDLE, &core_module, core_idle_timeout_cb,
           "TimeoutIdle");
       }
     }
@@ -5454,8 +6237,16 @@ MODRET core_post_pass(cmd_rec *cmd) {
       char *channel, *ptr;
       int min_level, max_level, res;
 
-      ptr = strchr(c->argv[i], ':');
+      pr_signals_handle();
+
       channel = c->argv[i];
+      ptr = strchr(channel, ':');
+      if (ptr == NULL) {
+        pr_log_debug(DEBUG6, "skipping badly formatted '%s' setting",
+          channel);
+        continue;
+      }
+
       *ptr = '\0';
 
       res = pr_trace_parse_levels(ptr + 1, &min_level, &max_level);
@@ -5498,6 +6289,33 @@ MODRET core_post_pass(cmd_rec *cmd) {
     core_max_cmd_ts = 0;
   }
 
+  /* Configure the statcache to start caching for the authenticated session. */
+  pr_fs_statcache_reset();
+  c = find_config(main_server->conf, CONF_PARAM, "FSCachePolicy", FALSE);
+  if (c != NULL) {
+    int engine;
+    unsigned int size, max_age;
+
+    engine = *((int *) c->argv[0]);
+    size = *((unsigned int *) c->argv[1]);
+    max_age = *((unsigned int *) c->argv[2]);
+
+    if (engine) {
+      pr_fs_statcache_set_policy(size, max_age, 0);
+
+    } else {
+      pr_fs_statcache_set_policy(0, 0, 0);
+    }
+
+  } else {
+    /* Set the default statcache policy. */
+    pr_fs_statcache_set_policy(PR_TUNABLE_FS_STATCACHE_SIZE,
+      PR_TUNABLE_FS_STATCACHE_MAX_AGE, 0);
+  }
+
+  /* Register an exit handler here, for clearing the statcache. */
+  pr_event_register(&core_module, "core.exit", core_exit_ev, NULL);
+
   /* Note: we MUST return HANDLED here, not DECLINED, to indicate that at
    * least one POST_CMD handler of the PASS command succeeded.  Since
    * mod_core is always the last module to which commands are dispatched,
@@ -5510,19 +6328,6 @@ MODRET core_post_pass(cmd_rec *cmd) {
 /* Configuration directive handlers
  */
 
-MODRET set_defaulttransfermode(cmd_rec *cmd) {
-  CHECK_ARGS(cmd, 1);
-  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
-
-  if (strcasecmp(cmd->argv[1], "ascii") != 0 &&
-      strcasecmp(cmd->argv[1], "binary") != 0)
-    CONF_ERROR(cmd, "parameter must be 'ascii' or 'binary'");
-
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
-
-  return PR_HANDLED(cmd);
-}
-
 MODRET set_deferwelcome(cmd_rec *cmd) {
   int bool = -1;
   config_rec *c = NULL;
@@ -5531,8 +6336,9 @@ MODRET set_deferwelcome(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   bool = get_boolean(cmd, 1);
-  if (bool == -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 char));
@@ -5577,12 +6383,17 @@ static const char *core_get_xfer_bytes_str(void *data, size_t datasz) {
 /* Event handlers
  */
 
+static void core_exit_ev(const void *event_data, void *user_data) {
+  pr_fs_statcache_free();
+}
+
 static void core_restart_ev(const void *event_data, void *user_data) {
+  pr_fs_statcache_reset();
   pr_scoreboard_scrub();
 
 #ifdef PR_USE_TRACE
   if (trace_log) {
-    pr_trace_set_levels(PR_TRACE_DEFAULT_CHANNEL, -1, -1);
+    (void) pr_trace_set_levels(PR_TRACE_DEFAULT_CHANNEL, -1, -1);
     pr_trace_set_file(NULL);
     trace_log = NULL;
   }
@@ -5657,9 +6468,8 @@ static int core_init(void) {
   pr_help_add(C_NOOP, _("(no operation)"), TRUE);
   pr_help_add(C_FEAT, _("(returns feature list)"), TRUE);
   pr_help_add(C_OPTS, _("<sp> command [<sp> options]"), TRUE);
-#ifdef PR_USE_HOST
   pr_help_add(C_HOST, _("<cp> hostname"), TRUE);
-#endif /* PR_USE_HOST */
+  pr_help_add(C_CLNT, _("<cp> client-info"), TRUE);
   pr_help_add(C_AUTH, _("<sp> base64-data"), FALSE);
   pr_help_add(C_CCC, _("(clears protection level)"), FALSE);
   pr_help_add(C_CONF, _("<sp> base64-data"), FALSE);
@@ -5671,14 +6481,13 @@ static int core_init(void) {
   /* Add the additional features implemented by this module into the
    * list, to be displayed in response to a FEAT command.
    */
+  pr_feat_add(C_CLNT);
   pr_feat_add(C_EPRT);
   pr_feat_add(C_EPSV);
   pr_feat_add(C_MDTM);
   pr_feat_add("REST STREAM");
   pr_feat_add(C_SIZE);
-#ifdef PR_USE_HOST
   pr_feat_add(C_HOST);
-#endif /* PR_USE_HOST */
 
   pr_event_register(&core_module, "core.restart", core_restart_ev, NULL);
   pr_event_register(&core_module, "core.startup", core_startup_ev, NULL);
@@ -5731,7 +6540,7 @@ static void set_server_auth_order(void) {
   c = find_config(main_server->conf, CONF_PARAM, "AuthOrder", FALSE);
   if (c != NULL) {
     array_header *module_list = (array_header *) c->argv[0];
-    int modulec = 0;
+    unsigned int modulec = 0;
     char **modulev = NULL;
     register unsigned int i = 0;
 
@@ -5806,11 +6615,12 @@ static int core_sess_init(void) {
   char *displayquit = NULL;
   config_rec *c = NULL;
   unsigned int *debug_level = NULL;
+  unsigned long fs_opts = 0UL;
 
   init_auth();
 
   c = find_config(main_server->conf, CONF_PARAM, "MultilineRFC2228", FALSE);
-  if (c) {
+  if (c != NULL) {
     session.multiline_rfc2228 = *((int *) c->argv[0]);
   }
 
@@ -5824,8 +6634,8 @@ static int core_sess_init(void) {
 
   timeout_idle = pr_data_get_timeout(PR_DATA_TIMEOUT_IDLE);
   if (timeout_idle) {
-    pr_timer_add(timeout_idle, PR_TIMER_IDLE, NULL, core_idle_timeout_cb,
-      "TimeoutIdle");
+    pr_timer_add(timeout_idle, PR_TIMER_IDLE, &core_module,
+      core_idle_timeout_cb, "TimeoutIdle");
   }
 
   /* Check for a server-specific TimeoutLinger */
@@ -5839,8 +6649,23 @@ static int core_sess_init(void) {
  
   /* Check for a configured DebugLevel. */
   debug_level = get_param_ptr(main_server->conf, "DebugLevel", FALSE);
-  if (debug_level != NULL)
+  if (debug_level != NULL) {
     pr_log_setdebuglevel(*debug_level);
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "FSOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    fs_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "FSOptions", FALSE);
+  }
+
+  (void) pr_fsio_set_options(fs_opts);
 
   /* Check for any server-specific RegexOptions */
   c = find_config(main_server->conf, CONF_PARAM, "RegexOptions", FALSE);
@@ -5862,7 +6687,7 @@ static int core_sess_init(void) {
 
   while (c) {
     if (pr_env_set(session.pool, c->argv[0], c->argv[1]) < 0) {
-      pr_log_debug(DEBUG1, "unable to set environ variable '%s': %s",
+      pr_log_debug(DEBUG1, "unable to set environment variable '%s': %s",
         (char *) c->argv[0], strerror(errno));
 
     } else {
@@ -5877,7 +6702,7 @@ static int core_sess_init(void) {
 
   while (c) {
     if (pr_env_unset(session.pool, c->argv[0]) < 0) {
-      pr_log_debug(DEBUG1, "unable to unset environ variable '%s': %s",
+      pr_log_debug(DEBUG1, "unable to unset environment variable '%s': %s",
         (char *) c->argv[0], strerror(errno));
 
     } else {
@@ -5899,8 +6724,17 @@ static int core_sess_init(void) {
       char *channel, *ptr;
       int min_level, max_level, res;
 
-      ptr = strchr(c->argv[i], ':');
+      pr_signals_handle();
+
       channel = c->argv[i];
+
+      ptr = strchr(channel, ':');
+      if (ptr == NULL) {
+        pr_log_debug(DEBUG6, "skipping badly formatted '%s' setting",
+          channel);
+        continue;
+      }
+
       *ptr = '\0';
 
       res = pr_trace_parse_levels(ptr + 1, &min_level, &max_level);
@@ -6093,7 +6927,6 @@ static conftable core_conftab[] = {
   { "DebugLevel",		set_debuglevel,			NULL },
   { "DefaultAddress",		set_defaultaddress,		NULL },
   { "DefaultServer",		set_defaultserver,		NULL },
-  { "DefaultTransferMode",	set_defaulttransfermode,	NULL },
   { "DeferWelcome",		set_deferwelcome,		NULL },
   { "Define",			set_define,			NULL },
   { "Deny",			set_allowdeny,			NULL },
@@ -6106,6 +6939,8 @@ static conftable core_conftab[] = {
   { "DisplayConnect",		set_displayconnect,		NULL },
   { "DisplayQuit",		set_displayquit,		NULL },
   { "From",			add_from,			NULL },
+  { "FSCachePolicy",		set_fscachepolicy,		NULL },
+  { "FSOptions",		set_fsoptions,			NULL },
   { "Group",			set_group, 			NULL },
   { "GroupOwner",		add_groupowner,			NULL },
   { "HideFiles",		set_hidefiles,			NULL },
@@ -6113,7 +6948,8 @@ static conftable core_conftab[] = {
   { "HideNoAccess",		set_hidenoaccess,		NULL },
   { "HideUser",			set_hideuser,			NULL },
   { "IgnoreHidden",		set_ignorehidden,		NULL },
-  { "Include",			add_include,	 		NULL },
+  { "Include",			set_include,	 		NULL },
+  { "IncludeOptions",		set_includeoptions, 		NULL },
   { "MasqueradeAddress",	set_masqueradeaddress,		NULL },
   { "MaxCommandRate",		set_maxcommandrate,		NULL },
   { "MaxConnectionRate",	set_maxconnrate,		NULL },
@@ -6155,9 +6991,8 @@ static conftable core_conftab[] = {
   { "UseReverseDNS",		set_usereversedns,		NULL },
   { "User",			set_user,			NULL },
   { "UserOwner",		add_userowner,			NULL },
-  { "WtmpLog",			set_wtmplog,			NULL },
-  { "tcpBackLog",		set_tcpbacklog,			NULL },
-  { "tcpNoDelay",		set_tcpnodelay,			NULL },
+  { "TCPBackLog",		set_tcpbacklog,			NULL },
+  { "TCPNoDelay",		set_tcpnodelay,			NULL },
 
   { NULL, NULL, NULL }
 };
@@ -6196,9 +7031,12 @@ static cmdtable core_cmdtab[] = {
   { CMD, C_NOOP, G_NONE,  core_noop,	FALSE,	FALSE,  CL_MISC },
   { CMD, C_FEAT, G_NONE,  core_feat,	FALSE,	FALSE,  CL_INFO },
   { CMD, C_OPTS, G_NONE,  core_opts,    FALSE,	FALSE,	CL_MISC },
+  { CMD, C_HOST, G_NONE,  core_host,    FALSE,	FALSE,	CL_MISC },
   { POST_CMD, C_PASS, G_NONE, core_post_pass, FALSE, FALSE },
   { CMD, C_HOST, G_NONE,  core_host,	FALSE,	FALSE,	CL_AUTH },
   { POST_CMD, C_HOST, G_NONE, core_post_host, FALSE, FALSE },
+  { CMD, C_CLNT, G_NONE,  core_clnt,	FALSE,	FALSE,	CL_INFO },
+
   { 0, NULL }
 };
 
diff --git a/modules/mod_ctrls.c b/modules/mod_ctrls.c
index 1c7a8ea..25ea723 100644
--- a/modules/mod_ctrls.c
+++ b/modules/mod_ctrls.c
@@ -3,7 +3,7 @@
  *          server, as well as several utility functions for other Controls
  *          modules
  *
- * Copyright (c) 2000-2013 TJ Saunders
+ * Copyright (c) 2000-2016 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,10 +24,8 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * This is mod_ctrls, contrib software for proftpd 1.2 and above.
+ * This is mod_ctrls, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_ctrls.c,v 1.59 2013-12-05 00:11:01 castaglia Exp $
  */
 
 #include "conf.h"
@@ -256,6 +254,8 @@ static void ctrls_cls_read(void) {
   pr_ctrls_cl_t *cl = cl_list;
 
   while (cl) {
+    pr_signals_handle();
+
     if (pr_ctrls_recv_request(cl) < 0) {
 
       if (errno == EOF) {
@@ -264,18 +264,21 @@ static void ctrls_cls_read(void) {
       } else if (errno == EINVAL) {
 
         /* Unsupported action requested */
-        if (!cl->cl_flags)
+        if (!cl->cl_flags) {
           cl->cl_flags = PR_CTRLS_CL_NOACTION;
+        }
 
         pr_ctrls_log(MOD_CTRLS_VERSION,
           "recvd from %s/%s client: (invalid action)", cl->cl_user,
           cl->cl_group);
 
-      } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      } else if (errno == EAGAIN ||
+                 errno == EWOULDBLOCK) {
 
         /* Malicious/blocked client */
-        if (!cl->cl_flags)
+        if (!cl->cl_flags) {
           cl->cl_flags = PR_CTRLS_CL_BLOCKED;
+        }
 
       } else {
         pr_ctrls_log(MOD_CTRLS_VERSION,
@@ -289,18 +292,20 @@ static void ctrls_cls_read(void) {
       /* Request successfully read.  Flag this client as being in such a
        * state.
        */
-      if (!cl->cl_flags)
+      if (!cl->cl_flags) {
         cl->cl_flags = PR_CTRLS_CL_HAVEREQ;
+      }
 
       if (ctrl->ctrls_cb_args) {
-        int reqargc = ctrl->ctrls_cb_args->nelts;
+        unsigned int reqargc = ctrl->ctrls_cb_args->nelts;
         char **reqargv = ctrl->ctrls_cb_args->elts;
 
         /* Reconstruct the original request string from the client for
          * logging.
          */
-        while (reqargc--)
+        while (reqargc--) {
           request = pstrcat(cl->cl_pool, request, " ", *reqargv++, NULL);
+        }
 
         pr_ctrls_log(MOD_CTRLS_VERSION,
           "recvd from %s/%s client: '%s'", cl->cl_user, cl->cl_group,
@@ -326,6 +331,8 @@ static int ctrls_cls_write(void) {
      */
     pr_ctrls_cl_t *tmpcl = cl->cl_next;
 
+    pr_signals_handle();
+
     /* This client has something to hear */
     if (cl->cl_flags == PR_CTRLS_CL_NOACCESS) {
       char *msg = "access denied";
@@ -370,51 +377,48 @@ static int ctrls_cls_write(void) {
 
     } else if (cl->cl_flags == PR_CTRLS_CL_HAVEREQ) {
 
-      if (cl->cl_ctrls->nelts > 0) {
-        register int i = 0;
+      if (cl->cl_ctrls != NULL &&
+          cl->cl_ctrls->nelts > 0) {
+        register unsigned int i = 0;
         pr_ctrls_t **ctrlv = NULL;
 
         ctrlv = (pr_ctrls_t **) cl->cl_ctrls->elts;
 
-        if (cl->cl_ctrls) {
-          for (i = 0; i < cl->cl_ctrls->nelts; i++) {
-            if ((ctrlv[i])->ctrls_cb_retval < 1) {
+        for (i = 0; i < cl->cl_ctrls->nelts; i++) {
+          if ((ctrlv[i])->ctrls_cb_retval < 1) {
 
-              /* Make sure the callback(s) added responses */
-              if ((ctrlv[i])->ctrls_cb_resps) {
-                if (pr_ctrls_send_msg(cl->cl_fd, (ctrlv[i])->ctrls_cb_retval,
-                    (ctrlv[i])->ctrls_cb_resps->nelts,
-                    (char **) (ctrlv[i])->ctrls_cb_resps->elts) < 0) {
-                  pr_ctrls_log(MOD_CTRLS_VERSION,
-                    "error: unable to send response to %s/%s "
-                    "client: %s", cl->cl_user, cl->cl_group, strerror(errno));
+            /* Make sure the callback(s) added responses */
+            if ((ctrlv[i])->ctrls_cb_resps) {
+              if (pr_ctrls_send_msg(cl->cl_fd, (ctrlv[i])->ctrls_cb_retval,
+                  (ctrlv[i])->ctrls_cb_resps->nelts,
+                  (char **) (ctrlv[i])->ctrls_cb_resps->elts) < 0) {
+                pr_ctrls_log(MOD_CTRLS_VERSION,
+                  "error: unable to send response to %s/%s "
+                  "client: %s", cl->cl_user, cl->cl_group, strerror(errno));
 
-                } else {
+              } else {
+                /* For logging/accounting purposes */
+                register unsigned int j = 0;
+                int respval = (ctrlv[i])->ctrls_cb_retval;
+                unsigned int respargc = (ctrlv[i])->ctrls_cb_resps->nelts;
+                char **respargv = (ctrlv[i])->ctrls_cb_resps->elts;
 
-                  /* For logging/accounting purposes */
-                  register int j = 0;
-                  int respval = (ctrlv[i])->ctrls_cb_retval;
-                  int respargc = (ctrlv[i])->ctrls_cb_resps->nelts;
-                  char **respargv = (ctrlv[i])->ctrls_cb_resps->elts;
+                pr_ctrls_log(MOD_CTRLS_VERSION,
+                  "sent to %s/%s client: return value: %d",
+                  cl->cl_user, cl->cl_group, respval);
 
+                for (j = 0; j < respargc; j++) {
                   pr_ctrls_log(MOD_CTRLS_VERSION,
-                    "sent to %s/%s client: return value: %d",
-                    cl->cl_user, cl->cl_group, respval);
-
-                  for (j = 0; j < respargc; j++) {
-                    pr_ctrls_log(MOD_CTRLS_VERSION,
-                      "sent to %s/%s client: '%s'", cl->cl_user, cl->cl_group,
-                      respargv[j]);
-                  }
+                    "sent to %s/%s client: '%s'", cl->cl_user, cl->cl_group,
+                    respargv[j]);
                 }
-
-              } else {
-
-                /* No responses added by callbacks */
-                pr_ctrls_log(MOD_CTRLS_VERSION,
-                  "notice: no responses given for %s/%s client: "
-                  "check controls handlers", cl->cl_user, cl->cl_group);
               }
+
+            } else {
+              /* No responses added by callbacks */
+              pr_ctrls_log(MOD_CTRLS_VERSION,
+                "notice: no responses given for %s/%s client: "
+                "check controls handlers", cl->cl_user, cl->cl_group);
             }
           }
         }
@@ -451,9 +455,10 @@ static int ctrls_listen(const char *sock_file, int flags) {
     int xerrno = errno;
 
     pr_signals_unblock();
+    pr_log_pri(PR_LOG_NOTICE, MOD_CTRLS_VERSION
+      ": error: unable to create local socket: %s", strerror(xerrno));
+
     errno = xerrno;
-    pr_ctrls_log(MOD_CTRLS_VERSION,
-      "error: unable to create local socket: %s", strerror(errno));
     return -1;
   }
 
@@ -489,7 +494,6 @@ static int ctrls_listen(const char *sock_file, int flags) {
 
     (void) close(sockfd);
     errno = xerrno;
-
     return -1;
   }
 
@@ -518,8 +522,8 @@ static int ctrls_listen(const char *sock_file, int flags) {
     (void) close(sockfd);
 
     errno = xerrno;
-    pr_ctrls_log(MOD_CTRLS_VERSION,
-      "error: unable to bind to local socket: %s", strerror(xerrno));
+    pr_log_pri(PR_LOG_NOTICE, MOD_CTRLS_VERSION
+      ": error: unable to bind to local socket: %s", strerror(xerrno));
     pr_trace_msg(trace_channel, 1, "unable to bind to local socket: %s",
       strerror(xerrno));
 
@@ -535,8 +539,8 @@ static int ctrls_listen(const char *sock_file, int flags) {
     (void) close(sockfd);
 
     errno = xerrno;
-    pr_ctrls_log(MOD_CTRLS_VERSION,
-      "error: unable to listen on local socket '%s': %s", sock.sun_path,
+    pr_log_pri(PR_LOG_NOTICE, MOD_CTRLS_VERSION
+      ": error: unable to listen on local socket '%s': %s", sock.sun_path,
       strerror(xerrno));
     pr_trace_msg(trace_channel, 1, "unable to listen on local socket '%s': %s",
       sock.sun_path, strerror(xerrno));
@@ -548,9 +552,10 @@ static int ctrls_listen(const char *sock_file, int flags) {
 #if !defined(SO_PEERCRED) && !defined(HAVE_GETPEEREID) && \
     !defined(HAVE_GETPEERUCRED) && defined(LOCAL_CREDS)
   /* Set the LOCAL_CREDS socket option. */
-  if (setsockopt(sockfd, 0, LOCAL_CREDS, &opt, optlen) < 0)
-    pr_ctrls_log(MOD_CTRLS_VERSION, "error enabling LOCAL_CREDS: %s",
+  if (setsockopt(sockfd, 0, LOCAL_CREDS, &opt, optlen) < 0) {
+    pr_log_debug(DEBUG0, MOD_CTRLS_VERSION ": error enabling LOCAL_CREDS: %s",
       strerror(errno));
+  }
 #endif /* !LOCAL_CREDS */
 
   /* Change the permissions on the socket, so that users can connect */
@@ -561,8 +566,8 @@ static int ctrls_listen(const char *sock_file, int flags) {
     (void) close(sockfd);
 
     errno = xerrno;
-    pr_ctrls_log(MOD_CTRLS_VERSION,
-      "error: unable to chmod local socket: %s", strerror(xerrno));
+    pr_log_pri(PR_LOG_NOTICE, MOD_CTRLS_VERSION
+      ": error: unable to chmod local socket: %s", strerror(xerrno));
     pr_trace_msg(trace_channel, 1, "unable to chmod local socket: %s",
       strerror(xerrno));
 
@@ -727,7 +732,8 @@ static int ctrls_timer_cb(CALLBACK_FRAME) {
     PRIVS_ROOT
     if (chown(ctrls_sock_file, ctrls_sock_uid, ctrls_sock_gid) < 0) {
       pr_log_pri(PR_LOG_NOTICE, MOD_CTRLS_VERSION
-        ": unable to chown local socket: %s", strerror(errno));
+        ": unable to chown local socket %s: %s", ctrls_sock_file,
+        strerror(errno));
     }
     PRIVS_RELINQUISH
 
@@ -981,11 +987,11 @@ MODRET set_ctrlsauthfreshness(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT);
 
   freshness = atoi(cmd->argv[1]);
-  if (freshness <= 0)
+  if (freshness <= 0) {
     CONF_ERROR(cmd, "must be a positive number");
+  }
 
   ctrls_cl_freshness = freshness;
-
   return PR_HANDLED(cmd);
 }
 
@@ -996,8 +1002,9 @@ MODRET set_ctrlsengine(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT);
 
   bool = get_boolean(cmd, 1);
-  if (bool == -1)
+  if (bool == -1) {
     CONF_ERROR(cmd, "expected Boolean parameter");
+  }
 
   ctrls_engine = bool;
   return PR_HANDLED(cmd);
@@ -1011,8 +1018,9 @@ MODRET set_ctrlsinterval(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT);
 
   nsecs = atoi(cmd->argv[1]);
-  if (nsecs <= 0)
+  if (nsecs <= 0) {
     CONF_ERROR(cmd, "must be a positive number");
+  }
 
   /* Remove the existing timer, and re-install it with this new interval. */
   ctrls_interval = nsecs;
@@ -1034,13 +1042,15 @@ MODRET set_ctrlslog(cmd_rec *cmd) {
 
   res = ctrls_openlog();
   if (res < 0) {
-    if (res == -1)
+    if (res == -1) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to open '",
-        cmd->argv[1], "': ", strerror(errno), NULL));
+        (char *) cmd->argv[1], "': ", strerror(errno), NULL));
+    }
 
-    if (res == -2)
+    if (res == -2) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
         "unable to log to a world-writable directory", NULL));
+    }
   }
 
   return PR_HANDLED(cmd);
@@ -1054,8 +1064,9 @@ MODRET set_ctrlsmaxclients(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT);
 
   nclients = atoi(cmd->argv[1]);
-  if (nclients <= 0)
+  if (nclients <= 0) {
     CONF_ERROR(cmd, "must be a positive number");
+  }
 
   cl_maxlistlen = nclients;
   return PR_HANDLED(cmd);
@@ -1063,11 +1074,15 @@ MODRET set_ctrlsmaxclients(cmd_rec *cmd) {
 
 /* Default: var/run/proftpd.sock */
 MODRET set_ctrlssocket(cmd_rec *cmd) {
+  char *path;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  if (*cmd->argv[1] != '/')
+  path = cmd->argv[1];
+  if (*path != '/') {
     CONF_ERROR(cmd, "must be an absolute path");
+  }
 
   /* Close the socket. */
   if (ctrls_sockfd >= 0) {
@@ -1079,8 +1094,8 @@ MODRET set_ctrlssocket(cmd_rec *cmd) {
   }
 
   /* Change the path. */
-  if (strcmp(cmd->argv[1], ctrls_sock_file) != 0) {
-    ctrls_sock_file = pstrdup(ctrls_pool, cmd->argv[1]);
+  if (strcmp(path, ctrls_sock_file) != 0) {
+    ctrls_sock_file = pstrdup(ctrls_pool, path);
   }
 
   return PR_HANDLED(cmd);
@@ -1125,29 +1140,33 @@ MODRET set_ctrlssocketowner(cmd_rec *cmd) {
 
   uid = pr_auth_name2uid(cmd->tmp_pool, cmd->argv[1]);
   if (uid == (uid_t) -1) {
-    if (errno != EINVAL)
-      pr_log_debug(DEBUG0, "%s: %s has UID of -1", cmd->argv[0],
-        cmd->argv[1]);
+    if (errno != EINVAL) {
+      pr_log_debug(DEBUG0, "%s: %s has UID of -1", (char *) cmd->argv[0],
+        (char *) cmd->argv[1]);
 
-    else
-      pr_log_debug(DEBUG0, "%s: no such user '%s'", cmd->argv[0],
-        cmd->argv[1]);
+    } else {
+      pr_log_debug(DEBUG0, "%s: no such user '%s'", (char *) cmd->argv[0],
+        (char *) cmd->argv[1]);
+    }
 
-  } else
+  } else {
     ctrls_sock_uid = uid;
+  }
 
   gid = pr_auth_name2gid(cmd->tmp_pool, cmd->argv[2]);
   if (gid == (gid_t) -1) {
-    if (errno != EINVAL)
-      pr_log_debug(DEBUG0, "%s: %s has GID of -1", cmd->argv[0],
-        cmd->argv[2]);
+    if (errno != EINVAL) {
+      pr_log_debug(DEBUG0, "%s: %s has GID of -1", (char *) cmd->argv[0],
+        (char *) cmd->argv[2]);
 
-    else
-      pr_log_debug(DEBUG0, "%s: no such group '%s'", cmd->argv[0],
-        cmd->argv[2]);
+    } else {
+      pr_log_debug(DEBUG0, "%s: no such group '%s'", (char *) cmd->argv[0],
+        (char *) cmd->argv[2]);
+    }
 
-  } else
+  } else {
     ctrls_sock_gid = gid;
+  }
 
   return PR_HANDLED(cmd);
 }
@@ -1164,12 +1183,14 @@ static void ctrls_shutdown_ev(const void *event_data, void *user_data) {
     pr_ctrls_cl_t *cl = NULL;
 
     for (cl = cl_list; cl; cl = cl->cl_next) {
-      close(cl->cl_fd);
-      cl->cl_fd = -1;
+      if (cl->cl_fd >= 0) {
+        (void) close(cl->cl_fd);
+        cl->cl_fd = -1;
+      }
     }
   }
 
-  close(ctrls_sockfd);
+  (void) close(ctrls_sockfd);
   ctrls_sockfd = -1;
 
   /* Remove the local socket path as well */
@@ -1205,8 +1226,10 @@ static void ctrls_restart_ev(const void *event_data, void *user_data) {
     pr_ctrls_cl_t *cl = NULL;
 
     for (cl = cl_list; cl; cl = cl->cl_next) {
-      close(cl->cl_fd);
-      cl->cl_fd = -1;
+      if (cl->cl_fd >= 0) {
+        (void) close(cl->cl_fd);
+        cl->cl_fd = -1;
+      }
     }
   }
 
diff --git a/modules/mod_delay.c b/modules/mod_delay.c
index 9f11b3e..4924548 100644
--- a/modules/mod_delay.c
+++ b/modules/mod_delay.c
@@ -2,7 +2,7 @@
  * ProFTPD: mod_delay -- a module for adding arbitrary delays to the FTP
  *                       session lifecycle
  *
- * Copyright (c) 2004-2014 TJ Saunders
+ * Copyright (c) 2004-2016 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
@@ -23,10 +23,8 @@
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
  *
- * This is mod_delay, contrib software for proftpd 1.2.10 and above.
+ * This is mod_delay, contrib software for proftpd 1.3.x and above.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_delay.c,v 1.73 2014-02-09 20:42:23 castaglia Exp $
  */
 
 #include "conf.h"
@@ -117,6 +115,7 @@ struct delay_rec {
 };
 
 struct {
+  int dt_enabled;
   const char *dt_path;
   int dt_fd;
   size_t dt_size;
@@ -127,9 +126,20 @@ struct {
 static unsigned int delay_engine = TRUE;
 static unsigned int delay_nuser = 0;
 static unsigned int delay_npass = 0;
+static unsigned long delay_user_delayed = 0L;
+static unsigned long delay_pass_delayed = 0L;
 static pool *delay_pool = NULL;
 static struct timeval delay_tv;
 
+/* DelayOnEvent events */
+#define DELAY_EVENT_USER_CMD		1
+#define DELAY_EVENT_PASS_CMD		2
+#define DELAY_EVENT_FAILED_LOGIN	3
+
+static unsigned long delay_failed_login_min_delay = 0UL;
+static unsigned long delay_pass_min_delay = 0UL;
+static unsigned long delay_user_min_delay = 0UL;
+
 static int delay_sess_init(void);
 static void delay_table_reset(void);
 
@@ -143,7 +153,7 @@ static const char *trace_channel = "delay";
 static long delay_select_k(unsigned long k, array_header *values) {
   unsigned long l, ir, tmp = 0;
   long *elts = (long *) values->elts;
-  int nelts = values->nelts;
+  unsigned int nelts = values->nelts;
 
   /* This is from "Numeric Recipes in C", Ch. 8.5, as the select()
    * algorithm, an in-place sorting algorithm for finding the Kth
@@ -155,41 +165,50 @@ static long delay_select_k(unsigned long k, array_header *values) {
   ir = values->nelts - 1;
 
   while (TRUE) {
+    pr_signals_handle();
+
     if (ir <= l+1) {
       if (ir == l+1 &&
-          elts[ir] < elts[l])
+          elts[ir] < elts[l]) {
         delay_swap(elts[l], elts[ir]);
+      }
 
       return elts[k];
 
     } else {
-      unsigned long i, j;
+      unsigned int i, j;
       long p;
       unsigned long mid = (l + ir) >> 1;
 
       delay_swap(elts[mid], elts[l+1]);
-      if (elts[l] > elts[ir])
+      if (elts[l] > elts[ir]) {
         delay_swap(elts[l], elts[ir]);
+      }
 
-      if (elts[l+1] > elts[ir])
+      if (elts[l+1] > elts[ir]) {
         delay_swap(elts[l+1], elts[ir]);
+      }
 
-      if (elts[l] > elts[l+1])
+      if (elts[l] > elts[l+1]) {
         delay_swap(elts[l], elts[l+1]);
+      }
 
       i = l + 1;
       j = ir;
       p = elts[l+1];
 
       while (TRUE) {
+        pr_signals_handle();
+
         do i++;
           while (i < nelts && elts[i] < p);
 
         do j--;
-          while (j >= 0 && elts[j] > p);
+          while (elts[j] > p);
 
-        if (j < i)
+        if (j < i) {
           break;
+        }
 
         delay_swap(elts[i], elts[j]);
       }
@@ -197,15 +216,18 @@ static long delay_select_k(unsigned long k, array_header *values) {
       elts[l+1] = elts[j];
       elts[j] = p;
 
-      if (p >= k)
+      if ((unsigned long) p >= k) {
         ir = j - 1;
+      }
 
-      if (p <= k)
+      if ((unsigned long) p <= k) {
         l = i;
+      }
 
       if (l >= (nelts - 1) ||
-          ir >= nelts)
+          ir >= nelts) {
         break;
+      }
     }
   }
 
@@ -284,8 +306,9 @@ static long delay_get_median(pool *p, unsigned int rownum, const char *protocol,
   return median;
 }
 
-static void delay_mask_signals(unsigned char block) {
+static int delay_mask_signals(unsigned char block) {
   static sigset_t mask_sigset;
+  int res = -1;
 
   if (block) {
     sigemptyset(&mask_sigset);
@@ -302,38 +325,33 @@ static void delay_mask_signals(unsigned char block) {
 #endif
     sigaddset(&mask_sigset, SIGHUP);
 
-    sigprocmask(SIG_BLOCK, &mask_sigset, NULL);
+    res = sigprocmask(SIG_BLOCK, &mask_sigset, NULL);
 
   } else {
-    sigprocmask(SIG_UNBLOCK, &mask_sigset, NULL);
+    res = sigprocmask(SIG_UNBLOCK, &mask_sigset, NULL);
   }
+
+  return res;
 }
 
 static void delay_signals_block(void) {
-  delay_mask_signals(TRUE);
+  if (delay_mask_signals(TRUE) < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "error blocking signals: %s", strerror(errno));
+  }   
 }
 
 static void delay_signals_unblock(void) {
-  delay_mask_signals(FALSE);
+  if (delay_mask_signals(FALSE) < 0) {
+    pr_trace_msg(trace_channel, 1,
+      "error unblocking signals: %s", strerror(errno));
+  }
 }
 
-static void delay_delay(long interval) {
+static unsigned long delay_delay(unsigned long interval) {
   struct timeval tv;
-  long rand_usec;
   int res, xerrno;
 
-  /* Add an additional delay of a random number of usecs, with a 
-   * maximum of half of the given interval.
-   */
-  rand_usec = ((interval / 2.0) * rand()) / RAND_MAX;
-  pr_trace_msg(trace_channel, 8, "additional random delay of %ld usecs added",
-    (long int) rand_usec);
-  interval += rand_usec;
-
-  if (interval > DELAY_MAX_DELAY_USECS) {
-    interval = DELAY_MAX_DELAY_USECS;
-  }
-
   tv.tv_sec = interval / 1000000;
   tv.tv_usec = interval % 1000000;
 
@@ -351,6 +369,128 @@ static void delay_delay(long interval) {
     /* If we were interrupted, handle the interrupting signal. */
     pr_signals_handle();
   }
+
+  return interval;
+}
+
+static unsigned long delay_delay_with_jitter(long interval) {
+  long rand_usec;
+
+  /* Add an additional delay of a random number of usecs, with a
+   * maximum of half of the given interval.
+   */
+  rand_usec = ((interval / 2.0) * rand()) / RAND_MAX;
+  pr_trace_msg(trace_channel, 8, "additional random delay of %ld usecs added",
+    (long int) rand_usec);
+  interval += rand_usec;
+
+  if (interval > DELAY_MAX_DELAY_USECS) {
+    interval = DELAY_MAX_DELAY_USECS;
+  }
+
+  return delay_delay(interval);
+}
+
+/* Similar to the pr_str_get_duration() function, but parses millisecond
+ * values, not seconds.
+ */
+static int delay_str_get_duration_ms(const char *str, long *duration) {
+  unsigned int mins, secs;
+  long msecs;
+  int flags = PR_STR_FL_IGNORE_CASE, has_suffix = FALSE;
+  size_t len;
+  char *ptr = NULL;
+
+  if (str == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (sscanf(str, "%2u:%2u.%4lu", &mins, &secs, &msecs) == 3) {
+    if (mins > INT_MAX ||
+        secs > INT_MAX ||
+        msecs > INT_MAX) {
+      errno = ERANGE;
+      return -1;
+    }
+
+    if (duration != NULL) {
+      *duration = (mins * 60 * 1000) + (secs * 1000) + msecs;
+    }
+
+    return 0;
+  }
+
+  len = strlen(str);
+  if (len == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  has_suffix = pr_strnrstr(str, len, "ms", 2, flags);
+  if (has_suffix == TRUE) {
+    /* Parse millisecs */
+
+    if (sscanf(str, "%ld", &msecs) == 1) {
+      if (msecs > INT_MAX) {
+        errno = ERANGE;
+        return -1;
+      }
+
+      if (duration != NULL) {
+        *duration = msecs;
+      }
+
+      return 0;
+    }
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  has_suffix = pr_strnrstr(str, len, "s", 1, flags);
+  if (has_suffix == FALSE) {
+    has_suffix = pr_strnrstr(str, len, "sec", 3, flags);
+  }
+  if (has_suffix == TRUE) {
+    /* Parse seconds */
+
+    if (sscanf(str, "%u", &secs) == 1) {
+      if (secs > INT_MAX) {
+        errno = ERANGE;
+        return -1;
+      }
+
+      if (duration != NULL) {
+        *duration = (secs * 1000);
+      }
+
+      return 0;
+    }
+
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Use strtol(3) here, check for trailing garbage, etc. */
+  msecs = strtol(str, &ptr, 10);
+  if (ptr && *ptr) {
+    /* Not a bare number, but a string with non-numeric characters. */
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (msecs < 0 ||
+      msecs > INT_MAX) {
+    errno = ERANGE;
+    return -1;
+  }
+
+  if (duration != NULL) {
+    *duration = msecs;
+  }
+
+  return 0;
 }
 
 /* There are two rows (USER and PASS) for each server ID (SID).
@@ -575,7 +715,10 @@ static int delay_table_init(void) {
     lock.l_type = F_UNLCK;
 
     pr_trace_msg(trace_channel, 8, "unlocking DelayTable '%s'", fh->fh_path);
-    fcntl(fh->fh_fd, F_SETLK, &lock);
+    if (fcntl(fh->fh_fd, F_SETLK, &lock) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking fd %d: %s", fh->fh_fd, strerror(errno));
+    }
   }
 
   delay_tab.dt_fd = fh->fh_fd;
@@ -724,7 +867,10 @@ static int delay_table_init(void) {
     lock.l_type = F_UNLCK;
 
     pr_trace_msg(trace_channel, 8, "unlocking DelayTable '%s'", fh->fh_path);
-    fcntl(fh->fh_fd, F_SETLK, &lock);
+    if (fcntl(fh->fh_fd, F_SETLK, &lock) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "error unlocking fd %d: %s", fh->fh_fd, strerror(errno));
+    }
   }
 
   /* Done */
@@ -1192,7 +1338,10 @@ static int delay_handle_reset(pr_ctrls_t *ctrl, int reqargc,
   }
 
   lock.l_type = F_UNLCK;
-  fcntl(fh->fh_fd, F_SETLK, &lock);
+  if (fcntl(fh->fh_fd, F_SETLK, &lock) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error unlocking fd %d: %s", fh->fh_fd, strerror(errno));
+  }
 
   if (pr_fsio_close(fh) < 0) {
     pr_ctrls_add_response(ctrl,
@@ -1208,6 +1357,11 @@ static int delay_handle_reset(pr_ctrls_t *ctrl, int reqargc,
 static int delay_handle_delay(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
 
+  if (delay_tab.dt_enabled == FALSE) {
+    pr_ctrls_add_response(ctrl, "delay: DelayTable disabled");
+    return -1;
+  }
+
   if (reqargc == 0 ||
       reqargv == NULL) {
     pr_ctrls_add_response(ctrl, "delay: missing required parameters");
@@ -1293,21 +1447,145 @@ MODRET set_delayengine(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: DelayTable path */
+/* usage: DelayOnEvent event delay-millis */
+MODRET set_delayonevent(cmd_rec *cmd) {
+  config_rec *c;
+  long delay_ms = -1;
+  int event;
+
+  CHECK_ARGS(cmd, 2);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  if (strcmp(cmd->argv[1], "USER") == 0) {
+    event = DELAY_EVENT_USER_CMD;
+
+  } else if (strcmp(cmd->argv[1], "PASS") == 0) {
+    event = DELAY_EVENT_PASS_CMD;
+
+  } else if (strcmp(cmd->argv[1], "FailedLogin") == 0) {
+    event = DELAY_EVENT_FAILED_LOGIN;
+
+  } else {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown/unsupported event: ",
+      cmd->argv[1], NULL));
+  }
+
+  if (delay_str_get_duration_ms(cmd->argv[2], &delay_ms) < 0) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing delay parameter '",
+      cmd->argv[2], "': ", strerror(errno), NULL));
+  }
+
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[0]) = event;
+  c->argv[1] = palloc(c->pool, sizeof(unsigned long));
+
+  /* Note: Even though we parsed the delay parameter in millisec, we
+   * need to use microsecs internally, as that is the implemented interface.
+   */
+  *((unsigned long *) c->argv[1]) = (delay_ms * 1000);
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: DelayTable path|"none" */
 MODRET set_delaytable(cmd_rec *cmd) {
+  const char *table = NULL;
+
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT);
 
-  if (pr_fs_valid_path(cmd->argv[1]) < 0)
-    CONF_ERROR(cmd, "must be an absolute path");
+  if (pr_fs_valid_path(cmd->argv[1]) < 0) {
+    if (strcasecmp(cmd->argv[1], "none") != 0) {
+      CONF_ERROR(cmd, "must be an absolute path");
+    }
 
-  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  } else {
+    table = cmd->argv[1];
+  }
+
+  add_config_param_str(cmd->argv[0], 1, table);
   return PR_HANDLED(cmd);
 }
 
 /* Command handlers
  */
 
+MODRET delay_log_pass(cmd_rec *cmd) {
+  if (delay_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (delay_pass_min_delay > 0) {
+    unsigned long interval = 0L;
+
+    if (delay_pass_delayed < delay_pass_min_delay) {
+      interval = delay_pass_min_delay - delay_pass_delayed;
+    }
+
+    if (interval > 0) {
+      pr_trace_msg(trace_channel, 9,
+        "enforcing minimum PASS delay (%lu usec), adding %ld usec delay",
+        delay_pass_min_delay, interval);
+      delay_delay(interval);
+    }
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET delay_log_pass_err(cmd_rec *cmd) {
+  if (delay_engine == FALSE) { 
+    return PR_DECLINED(cmd);
+  }
+  
+  if (delay_failed_login_min_delay > 0 ||
+      delay_pass_min_delay > 0) {
+    unsigned long interval = 0L, min_delay;
+
+    min_delay = delay_failed_login_min_delay;
+    if (delay_pass_min_delay > min_delay) {
+      min_delay = delay_pass_min_delay;
+    }
+      
+    if (delay_pass_delayed < min_delay) {
+      interval = min_delay - delay_pass_delayed;
+    }
+
+    if (interval > 0) {
+      pr_trace_msg(trace_channel, 9,
+        "enforcing minimum failed login delay (%lu usec), adding %ld usec "
+        "delay", delay_failed_login_min_delay, interval);
+      delay_delay(interval);
+    }
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET delay_log_user(cmd_rec *cmd) {
+  if (delay_engine == FALSE) { 
+    return PR_DECLINED(cmd);
+  }
+
+  if (delay_user_min_delay > 0) {
+    long interval = 0L;
+
+    if (delay_user_delayed < delay_user_min_delay) {
+      interval = delay_user_min_delay - delay_user_delayed;
+    }
+
+    if (interval > 0) {
+      pr_trace_msg(trace_channel, 9,
+        "enforcing minimum USER delay (%lu usec), adding %ld usec delay",
+        delay_user_min_delay, interval);
+      delay_delay(interval);
+    }
+  }
+
+  return PR_DECLINED(cmd);
+}
+
 MODRET delay_post_pass(cmd_rec *cmd) {
   struct timeval tv;
   unsigned int rownum;
@@ -1315,7 +1593,8 @@ MODRET delay_post_pass(cmd_rec *cmd) {
   const char *proto;
   unsigned char *authenticated;
 
-  if (delay_engine == FALSE) {
+  if (delay_engine == FALSE ||
+      delay_tab.dt_enabled == FALSE) {
     return PR_DECLINED(cmd);
   }
 
@@ -1399,7 +1678,7 @@ MODRET delay_post_pass(cmd_rec *cmd) {
       pr_trace_msg(trace_channel, 9,
         "interval (%ld usecs) less than selected median (%ld usecs), delaying",
         interval, median);
-      delay_delay(median - interval);
+      delay_pass_delayed = delay_delay_with_jitter(median - interval);
     }
 
   } else {
@@ -1411,35 +1690,14 @@ MODRET delay_post_pass(cmd_rec *cmd) {
 }
 
 MODRET delay_pre_pass(cmd_rec *cmd) {
-  if (!delay_engine)
+  if (delay_engine == FALSE) {
     return PR_DECLINED(cmd);
-
-  gettimeofday(&delay_tv, NULL);
-  return PR_DECLINED(cmd);
-}
-
-MODRET delay_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-
-    delay_engine = TRUE;
-
-    if (delay_tab.dt_fd > 0) {
-      close(delay_tab.dt_fd);
-      delay_tab.dt_fd = -1;
-    }
-
-    res = delay_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&delay_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
   }
 
+  /* Always reset the USER delayed value. */
+  delay_pass_delayed = 0L;
+
+  gettimeofday(&delay_tv, NULL);
   return PR_DECLINED(cmd);
 }
 
@@ -1450,7 +1708,8 @@ MODRET delay_post_user(cmd_rec *cmd) {
   const char *proto;
   unsigned char *authenticated;
 
-  if (delay_engine == FALSE) {
+  if (delay_engine == FALSE ||
+      delay_tab.dt_enabled == FALSE) {
     return PR_DECLINED(cmd);
   }
 
@@ -1536,7 +1795,7 @@ MODRET delay_post_user(cmd_rec *cmd) {
       pr_trace_msg(trace_channel, 9,
         "interval (%ld usecs) less than selected median (%ld usecs), delaying",
         interval, median);
-      delay_delay(median - interval);
+      delay_user_delayed = delay_delay_with_jitter(median - interval);
     }
 
   } else {
@@ -1548,8 +1807,12 @@ MODRET delay_post_user(cmd_rec *cmd) {
 }
 
 MODRET delay_pre_user(cmd_rec *cmd) {
-  if (!delay_engine)
+  if (delay_engine == FALSE) {
     return PR_DECLINED(cmd);
+  }
+
+  /* Always reset the USER delayed value. */
+  delay_user_delayed = 0L;
 
   gettimeofday(&delay_tv, NULL);
   return PR_DECLINED(cmd);
@@ -1585,12 +1848,23 @@ static void delay_postparse_ev(const void *event_data, void *user_data) {
     return;
   }
 
+  delay_tab.dt_enabled = FALSE;
+
   c = find_config(main_server->conf, CONF_PARAM, "DelayTable", FALSE);
   if (c != NULL) {
-    delay_tab.dt_path = c->argv[0];
+    const char *table = NULL;
+
+    table = c->argv[0];
+    if (table != NULL) {
+      delay_tab.dt_enabled = TRUE;
+      delay_tab.dt_path = table;
+    }
+  }
+
+  if (delay_tab.dt_enabled) {
+    (void) delay_table_init();
   }
 
-  (void) delay_table_init();
   return;
 }
 
@@ -1601,6 +1875,7 @@ static void delay_restart_ev(const void *event_data, void *user_data) {
 
   delay_tab.dt_path = PR_RUN_DIR "/proftpd.delay";
   delay_tab.dt_data = NULL;
+  delay_tab.dt_enabled = TRUE;
 
   if (delay_pool) {
     destroy_pool(delay_pool);
@@ -1619,14 +1894,41 @@ static void delay_restart_ev(const void *event_data, void *user_data) {
   return;
 }
 
+static void delay_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&delay_module, "core.session-reinit",
+    delay_sess_reinit_ev);
+
+  delay_engine = TRUE;
+
+  if (delay_tab.dt_fd > 0) {
+    close(delay_tab.dt_fd);
+    delay_tab.dt_fd = -1;
+  }
+
+  delay_nuser = 0;
+  delay_npass = 0;
+
+  res = delay_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&delay_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 static void delay_shutdown_ev(const void *event_data, void *user_data) {
-  pr_fh_t *fh;
-  char *data;
-  size_t datalen;
+  pr_fh_t *fh = NULL;
+  char *data = NULL;
+  size_t datalen = 0;
   int xerrno = 0;
 
-  if (!delay_engine)
+  if (delay_engine == FALSE ||
+      delay_tab.dt_enabled == FALSE) {
     return;
+  }
 
   /* Write out the DelayTable to the filesystem, thus updating the
    * file metadata.
@@ -1665,7 +1967,10 @@ static void delay_shutdown_ev(const void *event_data, void *user_data) {
 
   datalen = delay_tab.dt_size;
   data = palloc(delay_pool, datalen);
-  memcpy(data, delay_tab.dt_data, datalen);
+  if (data != NULL &&
+      datalen > 0) {
+    memcpy(data, delay_tab.dt_data, datalen);
+  }
 
   if (delay_table_unload(TRUE) < 0) {
     pr_log_pri(PR_LOG_WARNING, MOD_DELAY_VERSION
@@ -1673,10 +1978,13 @@ static void delay_shutdown_ev(const void *event_data, void *user_data) {
       delay_tab.dt_path, strerror(errno));
   }
 
-  if (pr_fsio_write(fh, data, datalen) < 0) {
-    pr_log_pri(PR_LOG_WARNING, MOD_DELAY_VERSION
-      ": error updating DelayTable '%s': %s", delay_tab.dt_path,
-      strerror(errno));
+  if (data != NULL &&
+      datalen > 0) {
+    if (pr_fsio_write(fh, data, datalen) < 0) {
+      pr_log_pri(PR_LOG_WARNING, MOD_DELAY_VERSION
+        ": error updating DelayTable '%s': %s", delay_tab.dt_path,
+        strerror(errno));
+    }
   }
 
   delay_tab.dt_fd = -1;
@@ -1729,7 +2037,10 @@ static int delay_init(void) {
 static int delay_sess_init(void) {
   pr_fh_t *fh;
   config_rec *c;
-  int xerrno = errno;
+  int xerrno;
+
+  pr_event_register(&delay_module, "core.session-reinit", delay_sess_reinit_ev,
+    NULL);
 
   if (delay_engine == FALSE) {
     return 0;
@@ -1748,6 +2059,45 @@ static int delay_sess_init(void) {
     return 0;
   }
 
+  c = find_config(main_server->conf, CONF_PARAM, "DelayOnEvent", FALSE);
+  while (c != NULL) {
+    int event;
+    unsigned long delay_usec;
+
+    pr_signals_handle();
+
+    event = *((int *) c->argv[0]);
+    delay_usec = *((unsigned long *) c->argv[1]);
+
+    switch (event) {
+      case DELAY_EVENT_USER_CMD:
+        delay_user_min_delay = delay_usec;
+        break;
+
+      case DELAY_EVENT_PASS_CMD:
+        delay_pass_min_delay = delay_usec;
+        break;
+
+      case DELAY_EVENT_FAILED_LOGIN:
+        delay_failed_login_min_delay = delay_usec;
+        break;
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "DelayOnEvent", FALSE);
+  }
+
+  if (delay_tab.dt_enabled == FALSE) {
+    /* If the DelayTable has been disabled (but not DelayEngine), AND there
+     * are not DelayOnEvent rules set, log a warning since mod_delay will not
+     * be doing much; it's probably an unintentional misconfiguration.
+     */
+
+    pr_log_debug(DEBUG0, MOD_DELAY_VERSION
+      ": no DelayOnEvent rules configured with \"DelayTable none\" in effect, "
+      "disabling module");
+    return 0;
+  }
+
   delay_nuser = 0;
   delay_npass = 0;
 
@@ -1802,6 +2152,7 @@ static ctrls_acttab_t delay_acttab[] = {
 static conftable delay_conftab[] = {
   { "DelayControlsACLs",set_delayctrlsacls,	NULL },
   { "DelayEngine",	set_delayengine,	NULL },
+  { "DelayOnEvent",	set_delayonevent,	NULL },
   { "DelayTable",	set_delaytable,		NULL },
   { NULL }
 };
@@ -1813,7 +2164,10 @@ static cmdtable delay_cmdtab[] = {
   { PRE_CMD,		C_USER,	G_NONE,	delay_pre_user,		FALSE, FALSE },
   { POST_CMD,		C_USER,	G_NONE,	delay_post_user,	FALSE, FALSE },
   { POST_CMD_ERR,	C_USER,	G_NONE,	delay_post_user,	FALSE, FALSE },
-  { POST_CMD,		C_HOST,	G_NONE, delay_post_host,	FALSE, FALSE },
+  { LOG_CMD,		C_USER,	G_NONE,	delay_log_user,		FALSE, FALSE },
+  { LOG_CMD_ERR,	C_USER,	G_NONE,	delay_log_user,		FALSE, FALSE },
+  { LOG_CMD,		C_PASS,	G_NONE,	delay_log_pass,		FALSE, FALSE },
+  { LOG_CMD_ERR,	C_PASS,	G_NONE,	delay_log_pass_err,	FALSE, FALSE },
   { 0, NULL }
 };
 
diff --git a/modules/mod_dso.c b/modules/mod_dso.c
index 70710cc..8da6757 100644
--- a/modules/mod_dso.c
+++ b/modules/mod_dso.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_dso -- support for loading/unloading modules at run-time
- *
- * Copyright (c) 2004-2013 TJ Saunders <tj at castaglia.org>
+ * Copyright (c) 2004-2016 TJ Saunders <tj at castaglia.org>
  *
  * 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,8 +23,6 @@
  *
  * This is mod_dso, contrib software for proftpd 1.3.x.
  * For more information contact TJ Saunders <tj at castaglia.org>.
- *
- * $Id: mod_dso.c,v 1.33 2013-10-13 23:46:43 castaglia Exp $
  */
 
 #include "conf.h"
@@ -203,10 +200,9 @@ static int dso_load_module(char *name) {
 
     *ptr = '.';
     pr_log_debug(DEBUG1, MOD_DSO_VERSION
-      ": unable to find module symbol '%s' in '%s'", symbol_name,
-        mh ? name : "self");
-    pr_trace_msg(trace_channel, 1, "unable to find module symbol '%s' in '%s'",
-      symbol_name, mh ? name : "self");
+      ": unable to find module symbol '%s' in 'self'", symbol_name);
+    pr_trace_msg(trace_channel, 1,
+      "unable to find module symbol '%s' in 'self'", symbol_name);
 
     lt_dlclose(mh);
     mh = NULL;
@@ -338,7 +334,7 @@ static int dso_unload_module_by_name(const char *name) {
 
 static int dso_handle_insmod(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i;
+  register int i;
 
   /* Check the ACL. */
   if (!pr_ctrls_check_acl(ctrl, dso_acttab, "insmod")) {
@@ -416,7 +412,7 @@ static int dso_handle_lsmod(pr_ctrls_t *ctrl, int reqargc,
 
 static int dso_handle_rmmod(pr_ctrls_t *ctrl, int reqargc,
     char **reqargv) {
-  register unsigned int i;
+  register int i;
 
   /* Check the ACL. */
   if (!pr_ctrls_check_acl(ctrl, dso_acttab, "rmmod")) {
@@ -576,7 +572,7 @@ MODRET set_moduleorder(cmd_rec *cmd) {
     }
   }
 
-  pr_log_debug(DEBUG4, "%s: reordering modules", cmd->argv[0]);
+  pr_log_debug(DEBUG4, "%s: reordering modules", (char *) cmd->argv[0]);
   for (i = 1; i < cmd->argc; i++) {
     m = pr_module_get(cmd->argv[i]);
 
@@ -598,7 +594,7 @@ MODRET set_moduleorder(cmd_rec *cmd) {
 
     if (pr_module_unload(m) < 0) {
       pr_log_debug(DEBUG0, "%s: error unloading module 'mod_%s.c': %s",
-        cmd->argv[0], m->name, strerror(errno));
+        (char *) cmd->argv[0], m->name, strerror(errno));
     }
 
     m = mn;
@@ -607,12 +603,12 @@ MODRET set_moduleorder(cmd_rec *cmd) {
   for (m = module_list; m; m = m->next) {
     if (pr_module_load(m) < 0) {
       pr_log_debug(DEBUG0, "%s: error loading module 'mod_%s.c': %s",
-        cmd->argv[0], m->name, strerror(errno));
+        (char *) cmd->argv[0], m->name, strerror(errno));
       exit(1);
     }
   }
 
-  pr_log_pri(PR_LOG_NOTICE, "%s: module order is now:", cmd->argv[0]);
+  pr_log_pri(PR_LOG_NOTICE, "%s: module order is now:", (char *) cmd->argv[0]);
   for (m = loaded_modules; m; m = m->next) {
     pr_log_pri(PR_LOG_NOTICE, " mod_%s.c", m->name);
   }
@@ -677,6 +673,7 @@ static void dso_restart_ev(const void *event_data, void *user_data) {
   /* Re-register the control handlers */
   for (i = 0; dso_acttab[i].act_action; i++) {
     pool *sub_pool = make_sub_pool(dso_pool);
+    pr_pool_tag(sub_pool, "DSO control action pool");
 
     /* Allocate and initialize the ACL for this control. */
     dso_acttab[i].act_acl = pcalloc(sub_pool, sizeof(ctrls_acl_t));
@@ -756,6 +753,7 @@ static int dso_init(void) {
   /* Register ctrls handlers. */
   for (i = 0; dso_acttab[i].act_action; i++) {
     pool *sub_pool = make_sub_pool(dso_pool);
+    pr_pool_tag(sub_pool, "DSO control action pool");
 
     /* Allocate and initialize the ACL for this control. */
     dso_acttab[i].act_acl = pcalloc(sub_pool, sizeof(ctrls_acl_t));
diff --git a/modules/mod_facl.c b/modules/mod_facl.c
index 40e5bc1..aa9a291 100644
--- a/modules/mod_facl.c
+++ b/modules/mod_facl.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2016 The ProFTPD Project team
+ * Copyright (c) 2004-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,11 +22,12 @@
  * for OpenSSL in the source distribution.
  */
 
-/* POSIX ACL checking code (aka POSIX.1e hell) */
+/* POSIX ACL checking code (aka POSIX.1e hell)
+ */
 
 #include "conf.h"
 
-#define MOD_FACL_VERSION		"mod_facl/0.4"
+#define MOD_FACL_VERSION		"mod_facl/0.6"
 
 /* Make sure the version of proftpd is as necessary. */
 #if PROFTPD_VERSION_NUMBER < 0x0001030101
@@ -61,111 +62,52 @@ static int is_errno_eperm(int xerrno) {
   return 0;
 }
 
-/* Copied directory from src/fsio.c, since these functions are not
- * accessible outside of that file.
- */
-static int sys_access(pr_fs_t *fs, const char *path, int mode, uid_t uid,
+static int facl_access(pr_fs_t *fs, const char *path, int mode, uid_t uid,
     gid_t gid, array_header *suppl_gids) {
-  mode_t mask;
   struct stat st;
 
-  pr_fs_clear_cache();
-  if (pr_fsio_stat(path, &st) < 0)
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
     return -1;
-
-  /* Root always succeeds. */
-  if (uid == PR_ROOT_UID)
-    return 0;
-
-  /* Initialize mask to reflect the permission bits that are applicable for
-   * the given user. mask contains the user-bits if the user ID equals the
-   * ID of the file owner. mask contains the group bits if the group ID
-   * belongs to the group of the file. mask will always contain the other
-   * bits of the permission bits.
-   */
-  mask = S_IROTH|S_IWOTH|S_IXOTH;
-
-  if (st.st_uid == uid)
-    mask |= S_IRUSR|S_IWUSR|S_IXUSR;
-
-  /* Check the current group, as well as all supplementary groups.
-   * Fortunately, we have this information cached, so accessing it is
-   * almost free.
-   */
-  if (st.st_gid == gid) {
-    mask |= S_IRGRP|S_IWGRP|S_IXGRP;
-
-  } else {
-    if (suppl_gids) {
-      register unsigned int i = 0;
-
-      for (i = 0; i < suppl_gids->nelts; i++) {
-        if (st.st_gid == ((gid_t *) suppl_gids->elts)[i]) {
-          mask |= S_IRGRP|S_IWGRP|S_IXGRP;
-          break;
-        }
-      }
-    }
-  }
-
-  mask &= st.st_mode;
-
-  /* Perform requested access checks. */
-  if (mode & R_OK) {
-    if (!(mask & (S_IRUSR|S_IRGRP|S_IROTH))) {
-      errno = EACCES;
-      return -1;
-    }
-  }
-
-  if (mode & W_OK) {
-    if (!(mask & (S_IWUSR|S_IWGRP|S_IWOTH))) {
-      errno = EACCES;
-      return -1;
-    }
   }
 
-  if (mode & X_OK) {
-    if (!(mask & (S_IXUSR|S_IXGRP|S_IXOTH))) {
-      errno = EACCES;
-      return -1;
-    }
-  }
-
-  /* F_OK already checked by checking the return value of stat. */
-  return 0;
+  return pr_fs_have_access(&st, mode, uid, gid, suppl_gids);
 }
 
-static int sys_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
+static int facl_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
     array_header *suppl_gids) {
-  return sys_access(fh->fh_fs, fh->fh_path, mode, uid, gid, suppl_gids);
+  return facl_access(fh->fh_fs, fh->fh_path, mode, uid, gid, suppl_gids);
 }
 
-#if defined(HAVE_BSD_POSIX_ACL) || defined(HAVE_LINUX_POSIX_ACL)
+#if defined(HAVE_BSD_POSIX_ACL) || \
+    defined(HAVE_LINUX_POSIX_ACL)
 static acl_perm_t get_facl_perm_for_mode(int mode) {
   acl_perm_t res;
 
   memset(&res, 0, sizeof(acl_perm_t));
 
-  if (mode & R_OK)
+  if (mode & R_OK) {
     res |= ACL_READ;
+  }
 
-  if (mode & W_OK)
+  if (mode & W_OK) {
     res |= ACL_WRITE;
+  }
 
-  if (mode & X_OK)
+  if (mode & X_OK) {
     res |= ACL_EXECUTE;
+  }
 
   return res;
 }
-#endif
 
-static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
-    struct stat *st, uid_t uid, gid_t gid, array_header *suppl_gids) {
-# if defined(HAVE_BSD_POSIX_ACL) || defined(HAVE_LINUX_POSIX_ACL)
+static int check_bsd_facl(pool *p, const char *path, int mode, void *acl,
+    int nents, struct stat *st, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
   register unsigned int i;
   int have_access_entry = FALSE, res = -1;
   pool *acl_pool;
+  char *acl_text;
   acl_t facl = acl;
   acl_entry_t ae;
   acl_tag_t ae_type;
@@ -176,6 +118,13 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
   array_header *acl_groups;
   array_header *acl_users;
 
+  acl_text = acl_to_text(facl, NULL);
+  if (acl_text != NULL) {
+    pr_trace_msg(trace_channel, 8,
+      "checking path '%s', ACL '%s'", path, acl_text);
+    acl_free(acl_text);
+  }
+
   /* Iterate through all of the ACL entries, sorting them for later
    * checking.
    */
@@ -202,6 +151,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
   acl_users = make_array(acl_pool, 1, sizeof(acl_entry_t));
 
   while (res > 0) {
+    pr_signals_handle();
+
     if (acl_get_tag_type(ae, &ae_type) < 0) {
       pr_trace_msg(trace_channel, 5,
         "error retrieving type of ACL entry for '%s': %s", path,
@@ -247,8 +198,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
     ae_type = ACL_USER_OBJ;
     have_access_entry = TRUE;
 
-    pr_trace_msg(trace_channel, 10, "user ID %lu matches ACL owner user ID",
-      (unsigned long) uid);
+    pr_trace_msg(trace_channel, 10, "user ID %s matches ACL owner user ID",
+      pr_uid2str(NULL, uid));
   }
 
   /* 2. If not matched above, and if the given user ID matches one of the
@@ -267,7 +218,7 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
       have_access_entry = TRUE;
 
       pr_trace_msg(trace_channel, 10,
-        "user ID %lu matches ACL allowed users list", (unsigned long) uid);
+        "user ID %s matches ACL allowed users list", pr_uid2str(NULL, uid));
 
       break;
     }
@@ -295,19 +246,21 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
 #  elif defined(HAVE_LINUX_POSIX_ACL)
     ret = acl_get_perm(perms, get_facl_perm_for_mode(mode));
 #  endif
-
     if (ret == 1) {
       ae = acl_group_entry;
       ae_type = ACL_GROUP_OBJ;
       have_access_entry = TRUE;
 
       pr_trace_msg(trace_channel, 10,
-        "primary group ID %lu matches ACL owner group ID",
-        (unsigned long) gid);
+        "primary group ID %s matches ACL owner group ID",
+        pr_gid2str(NULL, gid));
 
     } else if (ret < 0) {
+      int xerrno = errno;
+
       pr_trace_msg(trace_channel, 5,
-        "error checking permissions in permission set: %s", strerror(errno));
+        "error checking permissions in permission set: %s", strerror(xerrno));
+      errno = xerrno;
     }
   }
 
@@ -332,22 +285,24 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
 #  elif defined(HAVE_LINUX_POSIX_ACL)
         ret = acl_get_perm(perms, get_facl_perm_for_mode(mode));
 #  endif
-
         if (ret == 1) {
           ae = acl_group_entry;
           ae_type = ACL_GROUP_OBJ;
           have_access_entry = TRUE;
 
           pr_trace_msg(trace_channel, 10,
-            "supplemental group ID %lu matches ACL owner group ID",
-            (unsigned long) suppl_gid);
+            "supplemental group ID %s matches ACL owner group ID",
+            pr_gid2str(NULL, suppl_gid));
 
           break;
 
         } else if (ret < 0) {
+          int xerrno = errno;
+
           pr_trace_msg(trace_channel, 5,
             "error checking permissions in permission set: %s",
-            strerror(errno));
+            strerror(xerrno));
+          errno = xerrno;
         }
       }
     }
@@ -377,21 +332,23 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
 #  elif defined(HAVE_LINUX_POSIX_ACL)
       ret = acl_get_perm(perms, get_facl_perm_for_mode(mode));
 #  endif
-
       if (ret == 1) {
         ae = e;
         ae_type = ACL_GROUP;
         have_access_entry = TRUE;
 
         pr_trace_msg(trace_channel, 10,
-          "primary group ID %lu matches ACL allowed groups list",
-          (unsigned long) gid);
+          "primary group ID %s matches ACL allowed groups list",
+          pr_gid2str(NULL, gid));
 
         break;
 
       } else if (ret < 0) {
+        int xerrno = errno;
+
         pr_trace_msg(trace_channel, 5,
           "error checking permissions in permission set: %s", strerror(errno));
+        errno = xerrno;
       }
     }
 
@@ -418,22 +375,24 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
 #  elif defined(HAVE_LINUX_POSIX_ACL)
           ret = acl_get_perm(perms, get_facl_perm_for_mode(mode));
 #  endif
-
           if (ret == 1) {
             ae = e;
             ae_type = ACL_GROUP;
             have_access_entry = TRUE;
 
             pr_trace_msg(trace_channel, 10,
-              "supplemental group ID %lu matches ACL allowed groups list",
-              (unsigned long) suppl_gid);
+              "supplemental group ID %s matches ACL allowed groups list",
+              pr_gid2str(NULL, suppl_gid));
 
             break;
 
           } else if (ret < 0) {
+            int xerrno = errno;
+
             pr_trace_msg(trace_channel, 5,
               "error checking permissions in permission set: %s",
-              strerror(errno));
+              strerror(xerrno));
+            errno = xerrno;
           }
         }
       }
@@ -485,13 +444,15 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
 #  elif defined(HAVE_LINUX_POSIX_ACL)
       ret = acl_get_perm(perms, get_facl_perm_for_mode(mode));
 #  endif
-
-     if (ret == 1) {
+      if (ret == 1) {
         res = 0;
 
       } else if (ret < 0) {
+        int xerrno = errno;
+
         pr_trace_msg(trace_channel, 5,
-          "error checking permissions in permission set: %s", strerror(errno));
+          "error checking permissions in permission set: %s", strerror(xerrno));
+        errno = xerrno;
       }
 
       break;
@@ -534,15 +495,21 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
 
       } else {
         if (ret1 < 0) {
+          int xerrno = errno;
+
           pr_trace_msg(trace_channel, 5,
             "error checking permissions in entry permission set: %s",
-            strerror(errno));
+            strerror(xerrno));
+          errno = xerrno;
         }
 
         if (ret2 < 0) {
+          int xerrno = errno;
+
           pr_trace_msg(trace_channel, 5,
             "error checking permissions in mask permission set: %s",
-            strerror(errno));
+            strerror(xerrno));
+          errno = xerrno;
         }
       }
 
@@ -553,15 +520,126 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
   destroy_pool(acl_pool);
 
   if (res < 0) {
-    errno = EACCES;
     pr_trace_msg(trace_channel, 3,
-      "returning EACCES for path '%s', user ID %lu", path,
-      (unsigned long) uid);
+      "returning EACCES for path '%s', user ID %s", path,
+      pr_uid2str(NULL, uid));
+    errno = EACCES;
   }
 
   return res;
+}
+#endif /* BSD/Linux POSIX ACL */
 
-# elif defined(HAVE_SOLARIS_POSIX_ACL)
+#if defined(HAVE_MACOSX_POSIX_ACL)
+static acl_perm_t get_facl_perm_for_mode(int mode) {
+  acl_perm_t res;
+
+  memset(&res, 0, sizeof(acl_perm_t));
+
+  if (mode & R_OK) {
+    res |= ACL_READ_DATA;
+  }
+
+  if (mode & W_OK) {
+    res |= ACL_WRITE_DATA;
+  }
+
+  if (mode & X_OK) {
+    res |= ACL_EXECUTE;
+  }
+
+  return res;
+}
+
+static int check_macosx_facl(pool *p, const char *path, int mode, void *acl,
+    int nents, struct stat *st, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
+  int have_access = FALSE, res = -1;
+  char *acl_text;
+  acl_t facl = acl;
+  acl_entry_t ae;
+
+  acl_text = acl_to_text(facl, NULL);
+  if (acl_text != NULL) {
+    pr_trace_msg(trace_channel, 8,
+      "checking path '%s', ACL '%s'", path, acl_text);
+    acl_free(acl_text);
+  }
+
+  /* Iterate through all of the ACL entries, sorting them for later
+   * checking.
+   */
+  res = acl_get_entry(facl, ACL_FIRST_ENTRY, &ae);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 8,
+      "unable to retrieve first ACL entry for '%s': [%d] %s", path,
+      errno, strerror(errno));
+    errno = EACCES;
+    return -1;
+  }
+
+  if (res == 0) {
+    pr_trace_msg(trace_channel, 3, "ill-formed ACL for '%s' has no entries",
+      path);
+    errno = EACCES;
+    return -1;
+  }
+
+  while (res > 0) {
+    acl_tag_t ae_type;
+
+    pr_signals_handle();
+
+    if (acl_get_tag_type(ae, &ae_type) < 0) {
+      pr_trace_msg(trace_channel, 5,
+        "error retrieving type of ACL entry for '%s': %s", path,
+        strerror(errno));
+      res = acl_get_entry(facl, ACL_NEXT_ENTRY, &ae);
+      continue;
+    }
+
+    if (ae_type & ACL_TYPE_EXTENDED) {
+      int ret;
+
+      acl_permset_t perms;
+      if (acl_get_permset(ae, &perms) < 0) {
+        pr_trace_msg(trace_channel, 5, "error retrieving permission set: %s",
+          strerror(errno));
+      }
+
+      ret = acl_get_perm_np(perms, get_facl_perm_for_mode(mode));
+      if (ret == 1) {
+        have_access = TRUE;
+        break;
+
+      } else if (ret < 0) {
+        int xerrno = errno;
+
+        pr_trace_msg(trace_channel, 5,
+          "error checking permissions in permission set: %s", strerror(xerrno));
+        errno = xerrno;
+      }
+    }
+
+    res = acl_get_entry(facl, ACL_NEXT_ENTRY, &ae);
+  }
+
+  if (!have_access) {
+    pr_trace_msg(trace_channel, 3,
+      "returning EACCES for path '%s', user ID %s", path,
+      pr_uid2str(NULL, uid));
+    errno = EACCES;
+    return -1;
+  }
+
+  return 0;
+}
+#endif /* MacOSX POSIX ACL */
+
+#if defined(HAVE_SOLARIS_POSIX_ACL)
+static int check_solaris_facl(pool *p, const char *path, int mode, void *acl,
+    int nents, struct stat *st, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
   register unsigned int i;
   int have_access_entry = FALSE, have_mask_entry = FALSE, idx, res = -1;
   pool *acl_pool;
@@ -680,8 +758,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
     ae_type = USER_OBJ;
     have_access_entry = TRUE;
 
-    pr_trace_msg(trace_channel, 10, "user ID %lu matches ACL owner user ID",
-      (unsigned long) uid);
+    pr_trace_msg(trace_channel, 10, "user ID %s matches ACL owner user ID",
+      pr_uid2str(NULL, uid));
   }
 
   /* 2. If not matched above, and f the given user ID matches one of the
@@ -701,7 +779,7 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
       have_access_entry = TRUE;
 
       pr_trace_msg(trace_channel, 10,
-        "user ID %lu matches ACL allowed users list", (unsigned long) uid);
+        "user ID %s matches ACL allowed users list", pr_uid2str(NULL, uid));
 
       break;
     }
@@ -723,8 +801,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
       have_access_entry = TRUE;
 
       pr_trace_msg(trace_channel, 10,
-        "primary group ID %lu matches ACL owner group ID",
-        (unsigned long) gid);
+        "primary group ID %s matches ACL owner group ID",
+        pr_gid2str(NULL, gid));
     }
   }
 
@@ -742,8 +820,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
           have_access_entry = TRUE;
 
           pr_trace_msg(trace_channel, 10,
-            "supplemental group ID %lu matches ACL owner group ID",
-            (unsigned long) suppl_gid);
+            "supplemental group ID %s matches ACL owner group ID",
+            pr_gid2str(NULL, suppl_gid));
 
           break;
         }
@@ -770,8 +848,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
         have_access_entry = TRUE;
 
         pr_trace_msg(trace_channel, 10,
-          "primary group ID %lu matches ACL allowed groups list",
-          (unsigned long) gid);
+          "primary group ID %s matches ACL allowed groups list",
+          pr_gid2str(NULL, gid));
 
         break;
       }
@@ -793,8 +871,8 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
             have_access_entry = TRUE;
 
             pr_trace_msg(trace_channel, 10,
-              "supplemental group ID %lu matches ACL allowed groups list",
-              (unsigned long) suppl_gid);
+              "supplemental group ID %s matches ACL allowed groups list",
+              pr_gid2str(NULL, suppl_gid));
 
             break;
           }
@@ -835,21 +913,24 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
   switch (ae_type) {
     case USER_OBJ:
     case OTHER_OBJ:
-      if (ae.a_perm & mode)
+      if (ae.a_perm & mode) {
         res = 0;
+      }
       break;
 
     default: 
       if (have_mask_entry) {
         if ((ae.a_perm & mode) &&
-            (acl_mask_entry.a_perm & mode))
+            (acl_mask_entry.a_perm & mode)) {
           res = 0;
+        }
 
       } else {
 
         /* If there is no mask entry, then access should be granted. */
-        if (ae.a_perm & mode)
+        if (ae.a_perm & mode) {
           res = 0;
+        }
       }
 
       break;
@@ -858,14 +939,32 @@ static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
   destroy_pool(acl_pool);
 
   if (res < 0) {
-    errno = EACCES;
     pr_trace_msg(trace_channel, 3,
-      "returning EACCES for path '%s', user ID %lu", path,
-      (unsigned long) uid);
+      "returning EACCES for path '%s', user ID %s", path,
+      pr_uid2str(NULL, uid));
+    errno = EACCES;
   }
 
   return res;
-# endif /* HAVE_SOLARIS_POSIX_ACL */
+}
+#endif /* Solaris POSIX ACL */
+
+static int check_facl(pool *p, const char *path, int mode, void *acl, int nents,
+    struct stat *st, uid_t uid, gid_t gid, array_header *suppl_gids) {
+  int res = -1;
+
+# if defined(HAVE_BSD_POSIX_ACL) || \
+     defined(HAVE_LINUX_POSIX_ACL)
+  res = check_bsd_facl(p, path, mode, acl, nents, st, uid, gid, suppl_gids);
+
+# elif defined(HAVE_MACOSX_POSIX_ACL)
+  res = check_macosx_facl(p, path, mode, acl, nents, st, uid, gid, suppl_gids);
+
+# elif defined(HAVE_SOLARIS_POSIX_ACL)
+  res = check_solaris_facl(p, path, mode, acl, nents, st, uid, gid, suppl_gids);
+# endif
+
+  return res;
 }
 
 # if defined(PR_USE_FACL)
@@ -880,15 +979,17 @@ static int facl_fsio_access(pr_fs_t *fs, const char *path, int mode,
   void *acls;
   pool *tmp_pool = NULL;
 
-  pr_fs_clear_cache();
-  if (pr_fsio_stat(path, &st) < 0)
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
     return -1;
+  }
 
   /* Look up the acl for this path. */
-# if defined(HAVE_BSD_POSIX_ACL) || defined(HAVE_LINUX_POSIX_ACL)
+# if defined(HAVE_BSD_POSIX_ACL) || \
+     defined(HAVE_LINUX_POSIX_ACL) || \
+     defined(HAVE_MACOSX_POSIX_ACL)
   acls = acl_get_file(path, ACL_TYPE_ACCESS);
-
-  if (!acls) {
+  if (acls == NULL) {
     xerrno = errno;
 
     pr_trace_msg(trace_channel, 5, "unable to retrieve ACL for '%s': [%d] %s",
@@ -898,12 +999,7 @@ static int facl_fsio_access(pr_fs_t *fs, const char *path, int mode,
       pr_trace_msg(trace_channel, 3, "ACL retrieval operation not supported "
         "for '%s', falling back to normal access check", path);
 
-      /* Fall back to the custom access() function defined in src/fsio.
-       * Since that sys_access() function there is not public, we have
-       * to duplicate the code.  For now, that is, until a more clean
-       * arrangement can be found.
-       */
-      if (sys_access(fs, path, mode, uid, gid, suppl_gids) < 0) {
+      if (facl_access(fs, path, mode, uid, gid, suppl_gids) < 0) {
         xerrno = errno;
 
         pr_trace_msg(trace_channel, 6, "normal access check for '%s' "
@@ -933,14 +1029,14 @@ static int facl_fsio_access(pr_fs_t *fs, const char *path, int mode,
       pr_trace_msg(trace_channel, 3, "ACL retrieval operation not supported "
         "for '%s', falling back to normal access check", path);
 
-      if (sys_access(fs, path, mode, uid, gid, suppl_gids) < 0) {
+      if (facl_access(fs, path, mode, uid, gid, suppl_gids) < 0) {
         xerrno = errno;
 
         pr_trace_msg(trace_channel, 6, "normal access check for '%s' "
           "failed: %s", path, strerror(xerrno));
         errno = xerrno;
         return -1;
-      }
+      }   
 
       return 0;
     }
@@ -974,14 +1070,14 @@ static int facl_fsio_access(pr_fs_t *fs, const char *path, int mode,
       pr_trace_msg(trace_channel, 3, "ACL retrieval operation not supported "
         "for '%s', falling back to normal access check", path);
 
-      if (sys_access(fs, path, mode, uid, gid, suppl_gids) < 0) {
+      if (facl_access(fs, path, mode, uid, gid, suppl_gids) < 0) {
         xerrno = errno;
 
         pr_trace_msg(trace_channel, 6, "normal access check for '%s' "
           "failed: %s", path, strerror(xerrno));
         errno = xerrno;
         return -1;
-      }
+      }   
 
       return 0;
     }
@@ -1005,8 +1101,8 @@ static int facl_fsio_access(pr_fs_t *fs, const char *path, int mode,
      defined(HAVE_MACOSX_POSIX_ACL)
   acl_free(acls);
 # endif
-
   destroy_pool(tmp_pool);
+
   errno = xerrno;
   return res;
 }
@@ -1018,15 +1114,16 @@ static int facl_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
   void *acls;
   pool *tmp_pool = NULL;
 
-  pr_fs_clear_cache();
-  if (pr_fsio_fstat(fh, &st) < 0)
+  if (pr_fsio_fstat(fh, &st) < 0) {
     return -1;
+  }
 
   /* Look up the acl for this fd. */
-# if defined(HAVE_BSD_POSIX_ACL) || defined(HAVE_LINUX_POSIX_ACL)
+# if defined(HAVE_BSD_POSIX_ACL) || \
+     defined(HAVE_LINUX_POSIX_ACL) || \
+     defined(HAVE_MACOSX_POSIX_ACL)
   acls = acl_get_fd(PR_FH_FD(fh));
-
-  if (!acls) {
+  if (acls == NULL) {
     xerrno = errno;
 
     pr_trace_msg(trace_channel, 10,
@@ -1037,19 +1134,14 @@ static int facl_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
       pr_trace_msg(trace_channel, 3, "ACL retrieval operation not supported "
         "for '%s', falling back to normal access check", fh->fh_path);
 
-      /* Fall back to the custom faccess() function defined in src/fsio.
-       * Since that sys_faccess() function there is not public, we have
-       * to duplicate the code.  For now, that is, until a more clean
-       * arrangement can be found.
-       */
-      if (sys_faccess(fh, mode, uid, gid, suppl_gids) < 0) {
+      if (facl_faccess(fh, mode, uid, gid, suppl_gids) < 0) {
         xerrno = errno;
 
         pr_trace_msg(trace_channel, 6, "normal access check for '%s' "
           "failed: %s", fh->fh_path, strerror(xerrno));
         errno = xerrno;
         return -1;
-      }
+      }   
 
       return 0;
     }
@@ -1072,7 +1164,7 @@ static int facl_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
       pr_trace_msg(trace_channel, 3, "ACL retrieval operation not supported "
         "for '%s', falling back to normal access check", fh->fh_path);
 
-      if (sys_faccess(fh, mode, uid, gid, suppl_gids) < 0) {
+      if (facl_faccess(fh, mode, uid, gid, suppl_gids) < 0) {
         xerrno = errno;
 
         pr_trace_msg(trace_channel, 6, "normal access check for '%s' "
@@ -1110,7 +1202,7 @@ static int facl_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
       pr_trace_msg(trace_channel, 3, "ACL retrieval operation not supported "
         "for '%s', falling back to normal access check", fh->fh_path);
 
-      if (sys_faccess(fh, mode, uid, gid, suppl_gids) < 0) {
+      if (facl_faccess(fh, mode, uid, gid, suppl_gids) < 0) {
         xerrno = errno;
 
         pr_trace_msg(trace_channel, 6, "normal access check for '%s' "
@@ -1147,10 +1239,30 @@ static int facl_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
   return res;
 }
 # endif /* !PR_USE_FACL */
-
 #endif /* HAVE_POSIX_ACL */
 
-#if defined(PR_SHARED_MODULE)
+/* Configuration handlers
+ */
+
+/* usage: FACLEngine on|off */
+MODRET set_faclengine(cmd_rec *cmd) {
+  int engine = -1;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT);
+
+  engine = get_boolean(cmd, 1);
+  if (engine == -1) {
+    CONF_ERROR(cmd, "expected Boolean parameter");
+  }
+
+  facl_engine = engine;
+  return PR_HANDLED(cmd);
+}
+
+#if defined(PR_SHARED_MODULE) && \
+    defined(PR_USE_FACL) && \
+    defined(HAVE_POSIX_ACL)
 static void facl_mod_unload_ev(const void *event_data, void *user_data) {
   if (strcmp("mod_facl.c", (const char *) event_data) == 0) {
     pr_event_unregister(&facl_module, NULL, NULL);
@@ -1163,7 +1275,8 @@ static void facl_mod_unload_ev(const void *event_data, void *user_data) {
 #endif /* !PR_SHARED_MODULE */
 
 static void facl_postparse_ev(const void *event_data, void *user_data) {
-#if defined(PR_USE_FACL) && defined(HAVE_POSIX_ACL)
+#if defined(PR_USE_FACL) && \
+    defined(HAVE_POSIX_ACL)
   pr_fs_t *fs;
 #endif /* PR_USE_FACL and HAVE_POSIX_ACL */
 
@@ -1171,7 +1284,8 @@ static void facl_postparse_ev(const void *event_data, void *user_data) {
     return;
   }
 
-#if defined(PR_USE_FACL) && defined(HAVE_POSIX_ACL)
+#if defined(PR_USE_FACL) && \
+    defined(HAVE_POSIX_ACL)
   fs = pr_register_fs(permanent_pool, "facl", "/");
   if (fs == NULL) {
     int xerrno = errno;
@@ -1193,7 +1307,8 @@ static void facl_postparse_ev(const void *event_data, void *user_data) {
  */
 
 static int facl_init(void) {
-#if defined(PR_USE_FACL) && defined(HAVE_POSIX_ACL)
+#if defined(PR_USE_FACL) && \
+    defined(HAVE_POSIX_ACL)
 # if defined(PR_SHARED_MODULE)
   pr_event_register(&facl_module, "core.module-unload", facl_mod_unload_ev,
     NULL);
@@ -1204,24 +1319,6 @@ static int facl_init(void) {
   return 0;
 }
 
-/* Configuration handlers
- */
-
-/* usage: FACLEngine on|off */
-MODRET set_faclengine(cmd_rec *cmd) {
-  int bool = -1;
-
-  CHECK_ARGS(cmd, 1);
-  CHECK_CONF(cmd, CONF_ROOT);
-
-  bool = get_boolean(cmd, 1);
-  if (bool == -1)
-    CONF_ERROR(cmd, "expected Boolean parameter");
-
-  facl_engine = bool;
-  return PR_HANDLED(cmd);
-}
-
 /* Module Tables
  */
 
@@ -1258,4 +1355,3 @@ module facl_module = {
   /* Module version */
   MOD_FACL_VERSION
 };
-
diff --git a/modules/mod_facts.c b/modules/mod_facts.c
index 4570f67..7c2634a 100644
--- a/modules/mod_facts.c
+++ b/modules/mod_facts.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD: mod_facts -- a module for handling "facts" [RFC3659]
- * Copyright (c) 2007-2016 The ProFTPD Project
+ * Copyright (c) 2007-2017 The ProFTPD Project
  *
  * 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
@@ -25,7 +25,7 @@
 #include "conf.h"
 #include "privs.h"
 
-#define MOD_FACTS_VERSION		"mod_facts/0.4"
+#define MOD_FACTS_VERSION		"mod_facts/0.6"
 
 #if PROFTPD_VERSION_NUMBER < 0x0001030101
 # error "ProFTPD 1.3.1rc1 or later required"
@@ -42,20 +42,28 @@ static unsigned long facts_opts = 0;
 #define FACTS_OPT_SHOW_UNIX_GROUP	0x00020
 #define FACTS_OPT_SHOW_UNIX_MODE	0x00040
 #define FACTS_OPT_SHOW_UNIX_OWNER	0x00080
+#define FACTS_OPT_SHOW_MEDIA_TYPE	0x00100
+#define FACTS_OPT_SHOW_UNIX_OWNER_NAME	0x00200
+#define FACTS_OPT_SHOW_UNIX_GROUP_NAME	0x00400
 
 static unsigned long facts_mlinfo_opts = 0;
 #define FACTS_MLINFO_FL_SHOW_SYMLINKS			0x00001
 #define FACTS_MLINFO_FL_SHOW_SYMLINKS_USE_SLINK		0x00002
 #define FACTS_MLINFO_FL_NO_CDIR				0x00004
 #define FACTS_MLINFO_FL_APPEND_CRLF			0x00008
+#define FACTS_MLINFO_FL_NO_ADJUSTED_SYMLINKS		0x00010
+#define FACTS_MLINFO_FL_NO_NAMES			0x00020
 
 struct mlinfo {
   pool *pool;
   struct stat st;
   struct tm *tm;
+  const char *user;
+  const char *group;
   const char *type;
   const char *perm;
   const char *path;
+  const char *real_path;
 };
 
 /* Necessary prototypes */
@@ -71,7 +79,8 @@ static int facts_filters_allow_path(cmd_rec *cmd, const char *path) {
   if (pre != NULL &&
       pr_regexp_exec(pre, path, 0, NULL, 0, 0, 0) != 0) {
     pr_log_debug(DEBUG2, MOD_FACTS_VERSION
-      ": %s denied by PathAllowFilter on '%s'", cmd->argv[0], cmd->arg);
+      ": %s denied by PathAllowFilter on '%s'", (char *) cmd->argv[0],
+      cmd->arg);
     return -1;
   }
 
@@ -79,7 +88,7 @@ static int facts_filters_allow_path(cmd_rec *cmd, const char *path) {
   if (pre != NULL &&
       pr_regexp_exec(pre, path, 0, NULL, 0, 0, 0) == 0) {
     pr_log_debug(DEBUG2, MOD_FACTS_VERSION
-      ": %s denied by PathDenyFilter on '%s'", cmd->argv[0], cmd->arg);
+      ": %s denied by PathDenyFilter on '%s'", (char *) cmd->argv[0], cmd->arg);
     return -1;
   }
 #endif
@@ -202,8 +211,31 @@ static time_t facts_mktime(unsigned int year, unsigned int month,
   return res;
 }
 
+static const char *facts_mime_type(struct mlinfo *info) {
+  cmdtable *cmdtab;
+  cmd_rec *cmd;
+  modret_t *res;
+
+  cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "mime_type", NULL, NULL, NULL);
+  if (cmdtab == NULL) {
+    errno = EPERM;
+    return NULL;
+  }
+
+  cmd = pr_cmd_alloc(info->pool, 1, info->real_path);
+  res = pr_module_call(cmdtab->m, cmdtab->handler, cmd);
+  if (MODRET_ISHANDLED(res) &&
+      MODRET_HASDATA(res)) {
+    return res->data;
+  }
+
+  errno = ENOENT;
+  return NULL;
+}
+
 static size_t facts_mlinfo_fmt(struct mlinfo *info, char *buf, size_t bufsz,
     int flags) {
+  int len;
   char *ptr;
   size_t buflen = 0;
 
@@ -212,70 +244,104 @@ static size_t facts_mlinfo_fmt(struct mlinfo *info, char *buf, size_t bufsz,
   ptr = buf;
 
   if (facts_opts & FACTS_OPT_SHOW_MODIFY) {
-    snprintf(ptr, bufsz, "modify=%04d%02d%02d%02d%02d%02d;",
-      info->tm->tm_year+1900, info->tm->tm_mon+1, info->tm->tm_mday,
-      info->tm->tm_hour, info->tm->tm_min, info->tm->tm_sec);
-    buflen = strlen(buf);
+    if (info->tm != NULL) {
+      len = snprintf(ptr, bufsz, "modify=%04d%02d%02d%02d%02d%02d;",
+        info->tm->tm_year+1900, info->tm->tm_mon+1, info->tm->tm_mday,
+        info->tm->tm_hour, info->tm->tm_min, info->tm->tm_sec);
+
+    } else {
+      len = 0;
+    }
+
+    buflen += len;
     ptr = buf + buflen;
   }
 
   if (facts_opts & FACTS_OPT_SHOW_PERM) {
-    snprintf(ptr, bufsz - buflen, "perm=%s;", info->perm);
-    buflen = strlen(buf);
+    len = snprintf(ptr, bufsz - buflen, "perm=%s;", info->perm);
+    buflen += len;
     ptr = buf + buflen;
   }
 
   if (!S_ISDIR(info->st.st_mode) &&
       (facts_opts & FACTS_OPT_SHOW_SIZE)) {
-    snprintf(ptr, bufsz - buflen, "size=%" PR_LU ";",
+    len = snprintf(ptr, bufsz - buflen, "size=%" PR_LU ";",
       (pr_off_t) info->st.st_size);
-    buflen = strlen(buf);
+    buflen += len;
     ptr = buf + buflen;
   }
 
   if (facts_opts & FACTS_OPT_SHOW_TYPE) {
-    snprintf(ptr, bufsz - buflen, "type=%s;", info->type);
-    buflen = strlen(buf);
+    len = snprintf(ptr, bufsz - buflen, "type=%s;", info->type);
+    buflen += len;
     ptr = buf + buflen;
   }
 
   if (facts_opts & FACTS_OPT_SHOW_UNIQUE) {
-    snprintf(ptr, bufsz - buflen, "unique=%lXU%lX;",
+    len = snprintf(ptr, bufsz - buflen, "unique=%lXU%lX;",
       (unsigned long) info->st.st_dev, (unsigned long) info->st.st_ino);
-    buflen = strlen(buf);
+    buflen += len;
     ptr = buf + buflen;
   }
 
   if (facts_opts & FACTS_OPT_SHOW_UNIX_GROUP) {
-    snprintf(ptr, bufsz - buflen, "UNIX.group=%lu;",
-      (unsigned long) info->st.st_gid);
-    buflen = strlen(buf);
+    len = snprintf(ptr, bufsz - buflen, "UNIX.group=%s;",
+      pr_gid2str(NULL, info->st.st_gid));
+    buflen += len;
     ptr = buf + buflen;
   }
 
+  if (!(facts_mlinfo_opts & FACTS_MLINFO_FL_NO_NAMES)) {
+    if (facts_opts & FACTS_OPT_SHOW_UNIX_GROUP_NAME) {
+      len = snprintf(ptr, bufsz - buflen, "UNIX.groupname=%s;", info->group);
+      buflen += len;
+      ptr = buf + buflen;
+    }
+  }
+
   if (facts_opts & FACTS_OPT_SHOW_UNIX_MODE) {
-    snprintf(ptr, bufsz - buflen, "UNIX.mode=0%o;",
+    len = snprintf(ptr, bufsz - buflen, "UNIX.mode=0%o;",
       (unsigned int) info->st.st_mode & 07777);
-    buflen = strlen(buf);
+    buflen += len;
     ptr = buf + buflen;
   }
 
   if (facts_opts & FACTS_OPT_SHOW_UNIX_OWNER) {
-    snprintf(ptr, bufsz - buflen, "UNIX.owner=%lu;",
-      (unsigned long) info->st.st_uid);
-    buflen = strlen(buf);
+    len = snprintf(ptr, bufsz - buflen, "UNIX.owner=%s;",
+      pr_uid2str(NULL, info->st.st_uid));
+    buflen += len;
     ptr = buf + buflen;
   }
 
+  if (!(facts_mlinfo_opts & FACTS_MLINFO_FL_NO_NAMES)) {
+    if (facts_opts & FACTS_OPT_SHOW_UNIX_OWNER_NAME) {
+      len = snprintf(ptr, bufsz - buflen, "UNIX.ownername=%s;", info->user);
+      buflen += len;
+      ptr = buf + buflen;
+    }
+  }
+
+  if (facts_opts & FACTS_OPT_SHOW_MEDIA_TYPE) {
+    const char *mime_type;
+
+    mime_type = facts_mime_type(info);
+    if (mime_type != NULL) {
+      len = snprintf(ptr, bufsz - buflen, "media-type=%s;",
+        mime_type);
+      buflen += len;
+      ptr = buf + buflen;
+    }
+  }
+
   if (flags & FACTS_MLINFO_FL_APPEND_CRLF) {
-    snprintf(ptr, bufsz - buflen, " %s\r\n", info->path);
+    len = snprintf(ptr, bufsz - buflen, " %s\r\n", info->path);
 
   } else {
-    snprintf(ptr, bufsz - buflen, " %s", info->path);
+    len = snprintf(ptr, bufsz - buflen, " %s", info->path);
   }
 
   buf[bufsz-1] = '\0';
-  buflen = strlen(buf);
+  buflen += len;
 
   return buflen;
 }
@@ -357,10 +423,12 @@ static void facts_mlinfobuf_flush(void) {
 }
 
 static int facts_mlinfo_get(struct mlinfo *info, const char *path,
-    const char *dent_name, int flags, uid_t uid, gid_t gid, mode_t *mode) {
+    const char *dent_name, int flags, const char *user, uid_t uid,
+    const char *group, gid_t gid, mode_t *mode) {
   char *perm = "";
   int res;
 
+  pr_fs_clear_cache2(path);
   res = pr_fsio_lstat(path, &(info->st));
   if (res < 0) {
     int xerrno = errno;
@@ -372,10 +440,24 @@ static int facts_mlinfo_get(struct mlinfo *info, const char *path,
     return -1;
   }
 
+  if (user != NULL) {
+    info->user = pstrdup(info->pool, user);
+
+  } else {
+    info->user = session.user;
+  }
+
   if (uid != (uid_t) -1) {
     info->st.st_uid = uid;
   }
 
+  if (group != NULL) {
+    info->group = pstrdup(info->pool, group);
+
+  } else {
+    info->group = session.group;
+  }
+
   if (gid != (gid_t) -1) {
     info->st.st_gid = gid;
   }
@@ -386,19 +468,47 @@ static int facts_mlinfo_get(struct mlinfo *info, const char *path,
 #ifdef S_ISLNK
     if (S_ISLNK(info->st.st_mode)) {
       struct stat target_st;
+      const char *dst_path;
+      char *link_path;
+      size_t link_pathsz;
+      int len;
 
       /* Now we need to use stat(2) on the path (versus lstat(2)) to get the
        * info for the target, and copy its st_dev and st_ino values to our
        * stat in order to ensure that the unique fact values are the same.
+       *
+       * If we are chrooted, however, then the stat(2) on the symlink will
+       * almost certainly fail, especially if the destination path is an
+       * absolute path.
        */
 
-      pr_fs_clear_cache();
-      res = pr_fsio_stat(path, &target_st);
+      link_pathsz = PR_TUNABLE_PATH_MAX;
+      link_path = pcalloc(info->pool, link_pathsz);
+      len = dir_readlink(info->pool, path, link_path, link_pathsz-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0 &&
+          (size_t) len < link_pathsz) {
+        char *best_path;
+
+        best_path = dir_best_path(info->pool, link_path);
+        if (best_path != NULL) {
+          dst_path = best_path;
+
+        } else {
+          dst_path = link_path;
+        }
+
+      } else {
+        dst_path = path;
+      }
+
+      pr_fs_clear_cache2(dst_path);
+      res = pr_fsio_stat(dst_path, &target_st);
       if (res < 0) {
         int xerrno = errno;
 
         pr_log_debug(DEBUG4, MOD_FACTS_VERSION ": error stat'ing '%s': %s",
-          path, strerror(xerrno));
+          dst_path, strerror(xerrno));
 
         errno = xerrno;
         return -1;
@@ -432,7 +542,14 @@ static int facts_mlinfo_get(struct mlinfo *info, const char *path,
           char target[PR_TUNABLE_PATH_MAX+1];
           int targetlen;
 
-          targetlen = pr_fsio_readlink(path, target, sizeof(target)-1);
+          if (flags & FACTS_MLINFO_FL_NO_ADJUSTED_SYMLINKS) {
+            targetlen = pr_fsio_readlink(path, target, sizeof(target)-1);
+
+          } else {
+            sstrncpy(target, dst_path, sizeof(target)-1);
+            targetlen = len;
+          }
+
           if (targetlen < 0) { 
             int xerrno = errno;
 
@@ -443,7 +560,7 @@ static int facts_mlinfo_get(struct mlinfo *info, const char *path,
             return -1;
           }
 
-          if (targetlen >= sizeof(target)-1) {
+          if ((size_t) targetlen >= sizeof(target)-1) {
             targetlen = sizeof(target)-1;
           }
 
@@ -485,6 +602,9 @@ static int facts_mlinfo_get(struct mlinfo *info, const char *path,
        */
 
       perm = pstrcat(info->pool, perm, "adfr", NULL);
+
+    } else {
+      perm = pstrcat(info->pool, perm, "dfr", NULL);
     }
 
     if (pr_fsio_access(path, W_OK, session.uid, session.gid,
@@ -536,6 +656,7 @@ static int facts_mlinfo_get(struct mlinfo *info, const char *path,
     info->st.st_mode = *mode;
   }
 
+  info->real_path = pstrdup(info->pool, path);
   return 0;
 }
 
@@ -599,6 +720,16 @@ static void facts_mlst_feat_add(pool *p) {
     feat_str = pstrcat(p, feat_str, ";", NULL);
   }
 
+  if (!(facts_mlinfo_opts & FACTS_MLINFO_FL_NO_NAMES)) {
+    feat_str = pstrcat(p, feat_str, "UNIX.groupname", NULL);
+    if (facts_opts & FACTS_OPT_SHOW_UNIX_GROUP_NAME) {
+      feat_str = pstrcat(p, feat_str, "*;", NULL);
+
+    } else {
+      feat_str = pstrcat(p, feat_str, ";", NULL);
+    }
+  }
+
   feat_str = pstrcat(p, feat_str, "UNIX.mode", NULL);
   if (facts_opts & FACTS_OPT_SHOW_UNIX_MODE) {
     feat_str = pstrcat(p, feat_str, "*;", NULL);
@@ -615,6 +746,24 @@ static void facts_mlst_feat_add(pool *p) {
     feat_str = pstrcat(p, feat_str, ";", NULL);
   }
 
+  if (!(facts_mlinfo_opts & FACTS_MLINFO_FL_NO_NAMES)) {
+    feat_str = pstrcat(p, feat_str, "UNIX.ownername", NULL);
+    if (facts_opts & FACTS_OPT_SHOW_UNIX_OWNER_NAME) {
+      feat_str = pstrcat(p, feat_str, "*;", NULL);
+
+    } else {
+      feat_str = pstrcat(p, feat_str, ";", NULL);
+    }
+  }
+
+  /* Note: we only show the 'media-type' fact IFF mod_mime is present AND
+   * is enabled via MIMEEngine.
+   */
+  if (pr_module_exists("mod_mime.c") == TRUE &&
+      (facts_opts & FACTS_OPT_SHOW_MEDIA_TYPE)) {
+    feat_str = pstrcat(p, feat_str, "media-type*;", NULL);
+  }
+
   feat_str = pstrcat(p, "MLST ", feat_str, NULL);
   pr_feat_add(feat_str);
 }
@@ -640,7 +789,7 @@ static void facts_mlst_feat_remove(void) {
 
 static int facts_modify_mtime(pool *p, const char *path, char *timestamp) {
   char c, *ptr;
-  unsigned int year, month, day, hour, min, sec;
+  int year, month, day, hour, min, sec;
   struct timeval tvs[2];
   int res;
 
@@ -730,81 +879,14 @@ static int facts_modify_mtime(pool *p, const char *path, char *timestamp) {
   tvs[0].tv_sec = tvs[1].tv_sec = facts_mktime(year, month, day, hour, min,
     sec);
 
-  res = pr_fsio_utimes(path, tvs);
+  res = pr_fsio_utimes_with_root(path, tvs);
   if (res < 0) {
     int xerrno = errno;
 
-    if (xerrno == EPERM) {
-      struct stat st;
-      int matching_gid = FALSE;
-
-      /* If the utimes(2) call failed because the process UID doesn't
-       * match the file UID, then check to see if the GIDs match (and that
-       * the file has group write permissions).
-       *
-       * This can be alleviated in two ways: a) if mod_cap is present,
-       * enable the CAP_FOWNER capability for the session, or b) use root
-       * privs.
-       */
-      pr_fs_clear_cache();
-      if (pr_fsio_stat(path, &st) < 0) {
-        errno = xerrno;
-        return -1;
-      }
-
-      /* Be sure to check the primary and all the supplemental groups to
-       * which this session belongs.
-       */
-      if (st.st_gid == session.gid) {
-        matching_gid = TRUE;
-
-      } else {
-        register unsigned int i;
-        gid_t *gids;
-
-        gids = session.gids->elts;
-        for (i = 0; i < session.gids->nelts; i++) {
-          if (st.st_gid == gids[i]) {
-            matching_gid = TRUE;
-            break;
-          }
-        }
-      }
-
-      if (matching_gid == TRUE &&
-          (st.st_mode & S_IWGRP)) {
-        int merrno = 0;
-
-        /* Try the utimes(2) call again, this time with root privs. */
-
-        pr_signals_block();
-        PRIVS_ROOT
-        res = pr_fsio_utimes(path, tvs);
-        if (res < 0) {
-          merrno = errno;
-        }
-        PRIVS_RELINQUISH
-        pr_signals_unblock();
-
-        if (res == 0)
-          return 0;
-
-        pr_log_debug(DEBUG2, MOD_FACTS_VERSION
-          ": error modifying modify fact for '%s': %s", path,
-          strerror(merrno));
-        errno = xerrno;
-        return -1;
-      }
-
-      errno = xerrno;
-      return -1;
-
-    } else {
-      pr_log_debug(DEBUG2, MOD_FACTS_VERSION
-        ": error modifying modify fact for '%s': %s", path, strerror(xerrno));
-      errno = xerrno;
-      return -1;
-    }
+    pr_log_debug(DEBUG2, MOD_FACTS_VERSION
+      ": error modifying modify fact for '%s': %s", path, strerror(xerrno));
+    errno = xerrno;
+    return -1;
   }
 
   return 0;
@@ -865,7 +947,10 @@ MODRET facts_mff(cmd_rec *cmd) {
   char *facts, *ptr;
 
   if (cmd->argc < 3) {
-    pr_response_add_err(R_501, _("Invalid number of arguments"));
+    pr_response_add_err(R_501, _("Invalid number of parameters"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -878,34 +963,68 @@ MODRET facts_mff(cmd_rec *cmd) {
   ptr = strchr(cmd->arg, ' ');
   if (ptr == NULL) {
     pr_response_add_err(R_501, _("Invalid command syntax"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   path = pstrdup(cmd->tmp_pool, ptr + 1);
 
-  decoded_path = pr_fs_decode_path(cmd->tmp_pool, path);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      path);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
   canon_path = dir_canonical_path(cmd->tmp_pool, decoded_path);
   if (canon_path == NULL) {
-    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(EINVAL));
+    int xerrno = EINVAL;
+
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, canon_path, NULL)) {
     pr_log_debug(DEBUG4, MOD_FACTS_VERSION ": %s command denied by <Limit>",
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
     pr_response_add_err(R_550, _("Unable to handle command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   if (facts_filters_allow_path(cmd, decoded_path) < 0) {
-    pr_response_add_err(R_550, "%s: %s", path, strerror(EACCES));
+    int xerrno = EACCES;
+
+    pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   ptr = strchr(facts, ';');
   if (ptr == NULL) {
-    pr_response_add_err(R_550, "%s: %s", facts, strerror(EINVAL));
+    int xerrno = EINVAL;
+
+    pr_response_add_err(R_550, "%s: %s", facts, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
@@ -920,15 +1039,26 @@ MODRET facts_mff(cmd_rec *cmd) {
       char *timestamp, *ptr2;
 
       ptr2 = strchr(facts, '=');
-      if (!ptr2) {
-        pr_response_add_err(R_501, "%s: %s", cmd->argv[1], strerror(EINVAL));
+      if (ptr2 == NULL) {
+        int xerrno = EINVAL;
+
+        pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[1],
+          strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
 
       timestamp = ptr2 + 1;
 
       if (strlen(timestamp) < 14) {
-        pr_response_add_err(R_501, "%s: %s", timestamp, strerror(EINVAL));
+        int xerrno = EINVAL;
+
+        pr_response_add_err(R_501, "%s: %s", timestamp, strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
 
@@ -936,7 +1066,7 @@ MODRET facts_mff(cmd_rec *cmd) {
       if (ptr2) {
         pr_log_debug(DEBUG7, MOD_FACTS_VERSION
           ": %s: ignoring unsupported timestamp precision in '%s'",
-          cmd->argv[0], timestamp);
+          (char *) cmd->argv[0], timestamp);
         *ptr2 = '\0';
       }
 
@@ -946,6 +1076,7 @@ MODRET facts_mff(cmd_rec *cmd) {
         pr_response_add_err(xerrno == ENOENT ? R_550 : R_501, "%s: %s", path,
           strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
@@ -956,17 +1087,28 @@ MODRET facts_mff(cmd_rec *cmd) {
       char *group, *ptr2;
 
       ptr2 = strchr(facts, '=');
-      if (!ptr2) {
+      if (ptr2 == NULL) {
+        int xerrno = EINVAL;
+
         *ptr = ';';
-        pr_response_add_err(R_501, "%s: %s", cmd->argv[1], strerror(EINVAL));
+        pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[1],
+          strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
 
       group = ptr2 + 1;
 
       if (facts_modify_unix_group(cmd->tmp_pool, decoded_path, group) < 0) {
-        pr_response_add_err(errno == ENOENT ? R_550 : R_501, "%s: %s", path,
-          strerror(errno));
+        int xerrno = errno;
+
+        pr_response_add_err(xerrno == ENOENT ? R_550 : R_501, "%s: %s", path,
+          strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
 
@@ -976,17 +1118,28 @@ MODRET facts_mff(cmd_rec *cmd) {
       char *mode_str, *ptr2;
 
       ptr2 = strchr(facts, '=');
-      if (!ptr2) {
+      if (ptr2 == NULL) {
+        int xerrno = errno;
+
         *ptr = ';';
-        pr_response_add_err(R_501, "%s: %s", cmd->argv[1], strerror(EINVAL));
+        pr_response_add_err(R_501, "%s: %s", (char *) cmd->argv[1],
+          strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
 
       mode_str = ptr2 + 1;
 
       if (facts_modify_unix_mode(cmd->tmp_pool, decoded_path, mode_str) < 0) {
-        pr_response_add_err(errno == ENOENT ? R_550 : R_501, "%s: %s", path,
-          strerror(errno));
+        int xerrno = errno;
+
+        pr_response_add_err(xerrno == ENOENT ? R_550 : R_501, "%s: %s", path,
+          strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
 
@@ -996,10 +1149,13 @@ MODRET facts_mff(cmd_rec *cmd) {
        */
       pr_log_debug(DEBUG5, MOD_FACTS_VERSION
         ": %s: fact '%s' unsupported for modification, denying request",
-        cmd->argv[0], facts);
+        (char *) cmd->argv[0], facts);
       pr_response_add_err(R_504, _("Cannot modify fact '%s'"), facts);
 
       *ptr = ';';
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
@@ -1012,7 +1168,7 @@ MODRET facts_mff(cmd_rec *cmd) {
    * were successfully modified are to be included in the response, for
    * possible client parsing.  This means that the list is NOT localisable.
    */
-  pr_response_add(R_213, "%s %s", cmd->argv[1], path);
+  pr_response_add(R_213, "%s %s", (char *) cmd->argv[1], path);
   return PR_HANDLED(cmd);
 }
 
@@ -1022,7 +1178,10 @@ MODRET facts_mfmt(cmd_rec *cmd) {
   int res;
 
   if (cmd->argc < 3) {
-    pr_response_add_err(R_501, _("Invalid number of arguments"));
+    pr_response_add_err(R_501, _("Invalid number of parameters"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -1035,41 +1194,75 @@ MODRET facts_mfmt(cmd_rec *cmd) {
   ptr = strchr(cmd->arg, ' ');
   if (ptr == NULL) {
     pr_response_add_err(R_501, _("Invalid command syntax"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   path = pstrdup(cmd->tmp_pool, ptr + 1);
 
-  decoded_path = pr_fs_decode_path(cmd->tmp_pool, path);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      path);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
 
   canon_path = dir_canonical_path(cmd->tmp_pool, decoded_path);
   if (canon_path == NULL) {
-    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(EINVAL));
+    int xerrno = EINVAL;
+
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, canon_path, NULL)) {
     pr_log_debug(DEBUG4, MOD_FACTS_VERSION ": %s command denied by <Limit>",
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
     pr_response_add_err(R_550, _("Unable to handle command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   if (facts_filters_allow_path(cmd, decoded_path) < 0) {
-    pr_response_add_err(R_550, "%s: %s", path, strerror(EACCES));
+    int xerrno = EACCES;
+
+    pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   if (strlen(timestamp) < 14) {
-    pr_response_add_err(R_501, "%s: %s", timestamp, strerror(EINVAL));
+    int xerrno = EINVAL;
+
+    pr_response_add_err(R_501, "%s: %s", timestamp, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   ptr = strchr(timestamp, '.');
   if (ptr) {
     pr_log_debug(DEBUG7, MOD_FACTS_VERSION
-      ": %s: ignoring unsupported timestamp precision in '%s'", cmd->argv[0],
-      timestamp);
+      ": %s: ignoring unsupported timestamp precision in '%s'",
+      (char *) cmd->argv[0], timestamp);
     *ptr = '\0';
   }
 
@@ -1084,6 +1277,7 @@ MODRET facts_mfmt(cmd_rec *cmd) {
     pr_response_add_err(xerrno == ENOENT ? R_550 : R_501, "%s: %s", path,
       strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -1106,6 +1300,7 @@ MODRET facts_mfmt(cmd_rec *cmd) {
 
 MODRET facts_mlsd(cmd_rec *cmd) {
   const char *path, *decoded_path, *best_path;
+  const char *fake_user = NULL, *fake_group = NULL;
   config_rec *c;
   uid_t fake_uid = -1;
   gid_t fake_gid = -1;
@@ -1118,7 +1313,21 @@ MODRET facts_mlsd(cmd_rec *cmd) {
 
   if (cmd->argc != 1) {
     path = pstrdup(cmd->tmp_pool, cmd->arg);
-    decoded_path = pr_fs_decode_path(cmd->tmp_pool, path);
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+        strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), path);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
 
   } else {
     decoded_path = path = pr_fs_getcwd();
@@ -1126,8 +1335,11 @@ MODRET facts_mlsd(cmd_rec *cmd) {
 
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, (char *) decoded_path, NULL)) {
     pr_log_debug(DEBUG4, MOD_FACTS_VERSION ": %s command denied by <Limit>",
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
     pr_response_add_err(R_550, _("Unable to handle command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -1145,14 +1357,20 @@ MODRET facts_mlsd(cmd_rec *cmd) {
     int xerrno = errno;
 
     pr_log_debug(DEBUG4, MOD_FACTS_VERSION ": unable to stat '%s' (%s), "
-      "denying %s", decoded_path, strerror(xerrno), cmd->argv[0]);
+      "denying %s", decoded_path, strerror(xerrno), (char *) cmd->argv[0]);
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
   if (!S_ISDIR(info.st.st_mode)) {
     pr_response_add_err(R_550, _("'%s' is not a directory"), path);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -1185,8 +1403,6 @@ MODRET facts_mlsd(cmd_rec *cmd) {
     "DirFakeUser", FALSE);
   if (c) {
     if (c->argc > 0) {
-      const char *fake_user = NULL;
-
       fake_user = c->argv[0];
       if (fake_user != NULL &&
           strncmp(fake_user, "~", 2) != 0) {
@@ -1194,11 +1410,13 @@ MODRET facts_mlsd(cmd_rec *cmd) {
 
       } else {
         fake_uid = session.uid;
+        fake_user = session.user;
       }
 
     } else {
       /* Handle the "DirFakeUser off" case (Bug#3715). */
       fake_uid = (uid_t) -1;
+      fake_user = NULL;
     }
   }
 
@@ -1206,8 +1424,6 @@ MODRET facts_mlsd(cmd_rec *cmd) {
     "DirFakeGroup", FALSE);
   if (c) {
     if (c->argc > 0) {
-      const char *fake_group = NULL;
-
       fake_group = c->argv[0];
       if (fake_group != NULL &&
           strncmp(fake_group, "~", 2) != 0) {
@@ -1215,11 +1431,13 @@ MODRET facts_mlsd(cmd_rec *cmd) {
 
       } else {
         fake_gid = session.gid;
+        fake_group = session.group;
       }
 
     } else {
       /* Handle the "DirFakeGroup off" case (Bug#3715). */
       fake_gid = (gid_t) -1;
+      fake_group = NULL;
     }
   }
 
@@ -1227,13 +1445,15 @@ MODRET facts_mlsd(cmd_rec *cmd) {
   if (dirh == NULL) {
     int xerrno = errno;
 
-    pr_trace_msg("fileperms", 1, "MLSD, user '%s' (UID %lu, GID %lu): "
+    pr_trace_msg("fileperms", 1, "MLSD, user '%s' (UID %s, GID %s): "
       "error opening directory '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid),
       best_path, strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", path, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -1247,12 +1467,12 @@ MODRET facts_mlsd(cmd_rec *cmd) {
     pr_response_add_err(R_550, "%s: %s", (char *) cmd->argv[0],
       strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
   session.sf_flags |= SF_ASCII_OVERRIDE;
 
-  pr_fs_clear_cache();
   facts_mlinfobuf_init();
 
   while ((dent = pr_fsio_readdir(dirh)) != NULL) {
@@ -1289,9 +1509,8 @@ MODRET facts_mlsd(cmd_rec *cmd) {
 
     info.pool = make_sub_pool(cmd->tmp_pool);
     pr_pool_tag(info.pool, "MLSD facts pool");
-
     if (facts_mlinfo_get(&info, rel_path, dent->d_name, flags,
-        fake_uid, fake_gid, fake_mode) < 0) {
+        fake_user, fake_uid, fake_group, fake_gid, fake_mode) < 0) {
       pr_log_debug(DEBUG3, MOD_FACTS_VERSION
         ": MLSD: unable to get info for '%s': %s", abs_path, strerror(errno));
       continue;
@@ -1352,11 +1571,26 @@ MODRET facts_mlst(cmd_rec *cmd) {
   mode_t *fake_mode = NULL;
   unsigned char *ptr;
   const char *path, *decoded_path;
+  const char *fake_user = NULL, *fake_group = NULL;
   struct mlinfo info;
 
   if (cmd->argc != 1) {
     path = pstrdup(cmd->tmp_pool, cmd->arg);
-    decoded_path = pr_fs_decode_path(cmd->tmp_pool, path);
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, path,
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", path,
+        strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("%s: Illegal character sequence in filename"), path);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
 
   } else {
     decoded_path = path = pr_fs_getcwd();
@@ -1365,8 +1599,11 @@ MODRET facts_mlst(cmd_rec *cmd) {
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, (char *) decoded_path,
       &hidden)) {
     pr_log_debug(DEBUG4, MOD_FACTS_VERSION ": %s command denied by <Limit>",
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
     pr_response_add_err(R_550, _("Unable to handle command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -1398,8 +1635,6 @@ MODRET facts_mlst(cmd_rec *cmd) {
     CONF_PARAM, "DirFakeUser", FALSE);
   if (c) {
     if (c->argc > 0) {
-      const char *fake_user;
-
       fake_user = c->argv[0];
       if (fake_user != NULL &&
           strncmp(fake_user, "~", 2) != 0) {
@@ -1407,11 +1642,13 @@ MODRET facts_mlst(cmd_rec *cmd) {
 
       } else {
         fake_uid = session.uid;
+        fake_user = session.user;
       }
 
     } else {
       /* Handle the "DirFakeUser off" case (Bug#3715). */
-      fake_uid = session.uid;
+      fake_uid = (uid_t) -1;
+      fake_user = NULL;
     }
   }
 
@@ -1419,8 +1656,6 @@ MODRET facts_mlst(cmd_rec *cmd) {
     CONF_PARAM, "DirFakeGroup", FALSE);
   if (c) {
     if (c->argc > 0) {
-      const char *fake_group;
-
       fake_group = c->argv[0];
       if (fake_group != NULL &&
           strncmp(fake_group, "~", 2) != 0) {
@@ -1428,11 +1663,13 @@ MODRET facts_mlst(cmd_rec *cmd) {
 
       } else {
         fake_gid = session.gid;
+        fake_group = session.group;
       }
 
     } else {
       /* Handle the "DirFakeGroup off" case (Bug#3715). */
-      fake_gid = session.gid;
+      fake_gid = (gid_t) -1;
+      fake_group = NULL;
     }
   }
 
@@ -1445,10 +1682,13 @@ MODRET facts_mlst(cmd_rec *cmd) {
    */
   flags |= FACTS_MLINFO_FL_NO_CDIR;
 
-  pr_fs_clear_cache();
-  if (facts_mlinfo_get(&info, decoded_path, decoded_path, flags, fake_uid,
-      fake_gid, fake_mode) < 0) {
+  pr_fs_clear_cache2(decoded_path);
+  if (facts_mlinfo_get(&info, decoded_path, decoded_path, flags,
+      fake_user, fake_uid, fake_group, fake_gid, fake_mode) < 0) {
     pr_response_add_err(R_550, _("'%s' cannot be listed"), path);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -1492,12 +1732,16 @@ MODRET facts_opts_mlst(cmd_rec *cmd) {
 
   /* Convert underscores to spaces in the method name, for prettier logging. */
   for (i = 0; method[i]; i++) {
-    if (method[i] == '_')
+    if (method[i] == '_') {
       method[i] = ' ';
+    }
   }
 
   if (cmd->argc > 2) {
     pr_response_add_err(R_501, _("'%s' not understood"), method);
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -1511,7 +1755,7 @@ MODRET facts_opts_mlst(cmd_rec *cmd) {
     /* This response is mandated by RFC3659, therefore it is not
      * localisable.
      */
-    pr_response_add(R_200, "%s", method);
+    pr_response_add(R_200, "%s", "MLST OPTS");
     return PR_HANDLED(cmd);
   }
 
@@ -1554,6 +1798,10 @@ MODRET facts_opts_mlst(cmd_rec *cmd) {
       facts_opts |= FACTS_OPT_SHOW_UNIX_GROUP;
       resp_str = pstrcat(cmd->tmp_pool, resp_str, "UNIX.group;", NULL);
 
+    } else if (strcasecmp(facts, "UNIX.groupname") == 0) {
+      facts_opts |= FACTS_OPT_SHOW_UNIX_GROUP_NAME;
+      resp_str = pstrcat(cmd->tmp_pool, resp_str, "UNIX.groupname;", NULL);
+
     } else if (strcasecmp(facts, "UNIX.mode") == 0) {
       facts_opts |= FACTS_OPT_SHOW_UNIX_MODE;
       resp_str = pstrcat(cmd->tmp_pool, resp_str, "UNIX.mode;", NULL);
@@ -1562,6 +1810,14 @@ MODRET facts_opts_mlst(cmd_rec *cmd) {
       facts_opts |= FACTS_OPT_SHOW_UNIX_OWNER;
       resp_str = pstrcat(cmd->tmp_pool, resp_str, "UNIX.owner;", NULL);
 
+    } else if (strcasecmp(facts, "UNIX.ownername") == 0) {
+      facts_opts |= FACTS_OPT_SHOW_UNIX_OWNER_NAME;
+      resp_str = pstrcat(cmd->tmp_pool, resp_str, "UNIX.ownername;", NULL);
+
+    } else if (strcasecmp(facts, "media-type") == 0) {
+      facts_opts |= FACTS_OPT_SHOW_MEDIA_TYPE;
+      resp_str = pstrcat(cmd->tmp_pool, resp_str, "media-type;", NULL);
+
     } else {
       pr_log_debug(DEBUG3, MOD_FACTS_VERSION
         ": %s: client requested unsupported fact '%s'", method, facts);
@@ -1575,31 +1831,10 @@ MODRET facts_opts_mlst(cmd_rec *cmd) {
   facts_mlst_feat_add(cmd->tmp_pool);
 
   /* This response is mandated by RFC3659, therefore it is not localisable. */
-  pr_response_add(R_200, "%s %s", method, resp_str);
+  pr_response_add(R_200, "MLST OPTS %s", resp_str);
   return PR_HANDLED(cmd);
 }
 
-MODRET facts_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-
-    facts_opts = 0;
-    facts_mlinfo_opts = 0;
-
-    res = facts_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&facts_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
-  }
-
-  return PR_DECLINED(cmd);
-}
-
 /* Configuration handlers
  */
 
@@ -1637,9 +1872,15 @@ MODRET set_factsoptions(cmd_rec *cmd) {
   c = add_config_param(cmd->argv[0], 1, NULL);
 
   for (i = 1; i < cmd->argc; i++) {
-    if (strncmp(cmd->argv[i], "UseSlink", 9) == 0) {
+    if (strcmp(cmd->argv[i], "UseSlink") == 0) {
       opts |= FACTS_MLINFO_FL_SHOW_SYMLINKS_USE_SLINK;
 
+    } else if (strcmp(cmd->argv[i], "NoAdjustedSymlinks") == 0) {
+      opts |= FACTS_MLINFO_FL_NO_ADJUSTED_SYMLINKS;
+
+    } else if (strcmp(cmd->argv[i], "NoNames") == 0) {
+      opts |= FACTS_MLINFO_FL_NO_NAMES;
+
     } else {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown FactsOption '",
         cmd->argv[i], "'", NULL));
@@ -1652,6 +1893,32 @@ MODRET set_factsoptions(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void facts_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&facts_module, "core.session-reinit",
+    facts_sess_reinit_ev);
+
+  facts_opts = 0;
+  facts_mlinfo_opts = 0;
+
+  pr_feat_remove("MFF modify;UNIX.group;UNIX.mode;");
+  pr_feat_remove("MFMT");
+  pr_feat_remove("TVFS");
+  facts_mlst_feat_remove();
+
+  res = facts_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&facts_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization functions
  */
 
@@ -1666,6 +1933,9 @@ static int facts_sess_init(void) {
   config_rec *c;
   int advertise = TRUE;
 
+  pr_event_register(&facts_module, "core.session-reinit",
+    facts_sess_reinit_ev, NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "FactsAdvertise", FALSE);
   if (c) {
     advertise = *((int *) c->argv[0]);
@@ -1688,12 +1958,27 @@ static int facts_sess_init(void) {
   }
 
   facts_opts = FACTS_OPT_SHOW_MODIFY|FACTS_OPT_SHOW_PERM|FACTS_OPT_SHOW_SIZE|
-    FACTS_OPT_SHOW_TYPE|FACTS_OPT_SHOW_UNIQUE|FACTS_OPT_SHOW_UNIX_GROUP|
-    FACTS_OPT_SHOW_UNIX_MODE|FACTS_OPT_SHOW_UNIX_OWNER;
+    FACTS_OPT_SHOW_TYPE|FACTS_OPT_SHOW_UNIQUE|
+    FACTS_OPT_SHOW_UNIX_GROUP|FACTS_OPT_SHOW_UNIX_GROUP_NAME|
+    FACTS_OPT_SHOW_UNIX_MODE|FACTS_OPT_SHOW_UNIX_OWNER|
+    FACTS_OPT_SHOW_UNIX_OWNER_NAME;
+
+  if (pr_module_exists("mod_mime.c") == TRUE) {
+    /* Check to see if MIMEEngine is enabled.  Yes, this is slightly
+     * naughty, looking at some other module's configuration directives,
+     * but for compliance with RFC 3659, specifically for implementing the
+     * "media-type" fact for MLSx commands, we need to do this.
+     */
+    c = find_config(main_server->conf, CONF_PARAM, "MIMEEngine", FALSE);
+    if (c != NULL) {
+      int engine;
 
-  /* XXX The media-type fact could be supported if mod_mimetype was available
-   * and used.
-   */
+      engine = *((int *) c->argv[0]);
+      if (engine == TRUE) {
+        facts_opts |= FACTS_OPT_SHOW_MEDIA_TYPE;
+      }
+    }
+  }
 
   pr_feat_add("MFF modify;UNIX.group;UNIX.mode;");
   pr_feat_add("MFMT");
@@ -1721,7 +2006,6 @@ static cmdtable facts_cmdtab[] = {
   { LOG_CMD_ERR,C_MLSD,		G_NONE,	facts_mlsd_cleanup, FALSE, FALSE },
   { CMD,	C_MLST,		G_DIRS,	facts_mlst, TRUE, FALSE, CL_DIRS },
   { CMD,	C_OPTS "_MLST", G_NONE, facts_opts_mlst, FALSE, FALSE },
-  { POST_CMD,	C_HOST,		G_NONE,	facts_post_host, FALSE, FALSE },
   { 0, NULL }
 };
 
diff --git a/modules/mod_ident.c b/modules/mod_ident.c
index c54d09c..51a107b 100644
--- a/modules/mod_ident.c
+++ b/modules/mod_ident.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_ident -- a module for performing identd lookups [RFC1413]
- *
- * Copyright (c) 2008-2013 The ProFTPD Project
+ * Copyright (c) 2008-2016 The ProFTPD Project
  *
  * 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
@@ -21,8 +20,6 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_ident.c,v 1.11 2013-02-15 22:50:54 castaglia Exp $
  */
 
 #include "conf.h"
@@ -64,13 +61,13 @@ static int ident_timeout_cb(CALLBACK_FRAME) {
 static char *ident_lookup(pool *p, conn_t *conn) {
   conn_t *ident_conn = NULL, *ident_io = NULL;
   char buf[256], *ident = NULL;
-  int timerno, res = 0;
-  int ident_port = pr_inet_getservport(p, "ident", "tcp");
-  pr_netaddr_t *bind_addr;
+  int ident_port, timerno, res = 0;
+  const pr_netaddr_t *bind_addr;
 
   ident_nstrm = NULL;
   ident_timeout_triggered = FALSE;
   
+  ident_port = pr_inet_getservport(p, "ident", "tcp");
   if (ident_port == -1) {
     return NULL;
   }
@@ -292,29 +289,6 @@ static char *ident_lookup(pool *p, conn_t *conn) {
   return pstrdup(p, ident);
 }
 
-/* Command handlers
- */
-
-MODRET ident_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-
-    ident_engine = FALSE;
-
-    res = ident_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&ident_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
-  }
-
-  return PR_DECLINED(cmd);
-}
-
 /* Configuration handlers
  */
 
@@ -337,6 +311,26 @@ MODRET set_identlookups(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* Event listeners
+ */
+
+static void ident_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&ident_module, "core.session-reinit",
+    ident_sess_reinit_ev);
+
+  ident_engine = FALSE;
+
+  res = ident_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&ident_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization functions
  */
 
@@ -345,6 +339,9 @@ static int ident_sess_init(void) {
   config_rec *c;
   char *ident = NULL;
 
+  pr_event_register(&ident_module, "core.session-reinit", ident_sess_reinit_ev,
+    NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "IdentLookups", FALSE);
   if (c != NULL) {
     ident_engine = *((int *) c->argv[0]);
@@ -364,6 +361,7 @@ static int ident_sess_init(void) {
   }
 
   tmp_pool = make_sub_pool(session.pool);
+  pr_pool_tag(tmp_pool, "IdentLookup pool");
 
   /* Perform the RFC1413 lookup */
   pr_log_debug(DEBUG6, MOD_IDENT_VERSION ": performing ident lookup");
@@ -401,11 +399,6 @@ static conftable ident_conftab[] = {
   { NULL }
 };
 
-static cmdtable ident_cmdtab[] = {
-  { POST_CMD,	C_HOST,	G_NONE,	ident_post_host,	FALSE,	FALSE },
-  { 0, NULL }
-};
-
 module ident_module = {
   NULL, NULL,
 
@@ -419,7 +412,7 @@ module ident_module = {
   ident_conftab,
 
   /* Module command handler table */
-  ident_cmdtab,
+  NULL,
 
   /* Module authentication handler table */
   NULL,
diff --git a/modules/mod_lang.c b/modules/mod_lang.c
index 485d01f..eee2960 100644
--- a/modules/mod_lang.c
+++ b/modules/mod_lang.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_lang -- a module for handling the LANG command [RFC2640]
- *
- * Copyright (c) 2006-2013 The ProFTPD Project
+ * Copyright (c) 2006-2017 The ProFTPD Project
  *
  * 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
@@ -21,13 +20,11 @@
  * give permission to link this program with OpenSSL, and distribute the
  * resulting executable, without including the source code for OpenSSL in the
  * source distribution.
- *
- * $Id: mod_lang.c,v 1.41 2013-10-13 17:34:01 castaglia Exp $
  */
 
 #include "conf.h"
 
-#define MOD_LANG_VERSION		"mod_lang/1.0"
+#define MOD_LANG_VERSION		"mod_lang/1.1"
 
 #if PROFTPD_VERSION_NUMBER < 0x0001030101
 # error "ProFTPD 1.3.1rc1 or later required"
@@ -53,6 +50,13 @@ static array_header *lang_list = NULL;
 static pr_table_t *lang_aliases = NULL;
 static const char *lang_path = PR_LOCALE_DIR;
 
+static unsigned long lang_opts = 0UL;
+#define LANG_OPT_PREFER_SERVER_ENCODING		0x0001
+#define LANG_OPT_REQUIRE_VALID_ENCODING		0x0002
+
+static int lang_use_encoding = -1;
+static const char *lang_local_charset = NULL, *lang_client_charset = NULL;
+
 /* Support routines
  */
 
@@ -156,14 +160,14 @@ static int lang_set_lang(pool *p, const char *lang) {
   char *curr_lang;
 
   if (lang_aliases != NULL) {
-    void *v;
+    const void *v;
 
     /* Check to see if the given lang has an alias that has been determined
      * to be acceptable.
      */
 
     v = pr_table_get(lang_aliases, lang, NULL);
-    if (v) {
+    if (v != NULL) {
       pr_log_debug(DEBUG9, MOD_LANG_VERSION ": '%s' is an alias for '%s'",
         lang, (const char *) v);
       lang = v;
@@ -237,20 +241,20 @@ static int lang_supported(pool *p, const char *lang) {
   char *lang_dup, **langs;
   int ok = FALSE;
 
-  if (!lang_list) {
+  if (lang_list == NULL) {
     errno = EPERM;
     return -1;
   }
 
   if (lang_aliases != NULL) {
-    void *v;
+    const void *v;
     
     /* Check to see if the given lang has an alias that has been determined
      * to be acceptable.
      */
 
     v = pr_table_get(lang_aliases, lang, NULL);
-    if (v) {
+    if (v != NULL) {
       pr_log_debug(DEBUG9, MOD_LANG_VERSION ": using '%s' as alias for '%s'",
         (const char *) v, lang);
       lang = v;
@@ -285,6 +289,72 @@ static int lang_supported(pool *p, const char *lang) {
   return 0;
 }
 
+/* Lookup/handle any UseEncoding configuration. */
+static int process_encoding_config(int *utf8_client_encoding) {
+  config_rec *c;
+  int strict_encoding;
+
+  c = find_config(main_server->conf, CONF_PARAM, "UseEncoding", FALSE);
+  if (c == NULL) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  if (c->argc == 1) {
+    lang_use_encoding = *((int *) c->argv[0]);
+    if (lang_use_encoding == TRUE) {
+      pr_fs_use_encoding(TRUE);
+
+    } else {
+      pr_encode_disable_encoding();
+      pr_fs_use_encoding(FALSE);
+    }
+
+    return 0;
+  }
+
+  lang_local_charset = c->argv[0];
+  lang_client_charset = c->argv[1];
+  strict_encoding = *((int *) c->argv[2]);
+
+  if (strict_encoding == TRUE) {
+    /* Fold the UseEncoding "strict" keyword functionality into LangOptions;
+     * we want to move people that way anyway.
+     */
+    lang_opts |= LANG_OPT_PREFER_SERVER_ENCODING;
+  }
+
+  if (strcasecmp(lang_client_charset, "UTF8") == 0 ||
+      strcasecmp(lang_client_charset, "UTF-8") == 0) {
+    if (utf8_client_encoding) {
+      *utf8_client_encoding = TRUE;
+    }
+  }
+
+  if (pr_encode_set_charset_encoding(lang_local_charset,
+      lang_client_charset) < 0) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
+      ": error setting local charset '%s', client charset '%s': %s",
+      lang_local_charset, lang_client_charset, strerror(errno));
+    pr_fs_use_encoding(FALSE);
+
+  } else {
+    pr_log_debug(DEBUG3, MOD_LANG_VERSION ": using local charset '%s', "
+      "client charset '%s' for path encoding", lang_local_charset,
+      lang_client_charset);
+    pr_fs_use_encoding(TRUE);
+
+    /* Make sure that gettext() uses the specified charset as well. */
+    if (bind_textdomain_codeset("proftpd", lang_client_charset) == NULL) {
+      pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
+        ": error setting client charset '%s' for localised messages: %s",
+        lang_client_charset, strerror(errno));
+    }
+  }
+
+  return 0;
+}
+
 /* Configuration handlers
  */
 
@@ -317,6 +387,38 @@ MODRET set_langengine(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: LangOptions opt1 opt2 ... */
+MODRET set_langoptions(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0)
+    CONF_ERROR(cmd, "wrong number of parameters");
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcmp(cmd->argv[i], "PreferServerEncoding") == 0) {
+      opts |= LANG_OPT_PREFER_SERVER_ENCODING;
+
+    } else if (strcmp(cmd->argv[i], "RequireValidEncoding") == 0) {
+      opts |= LANG_OPT_REQUIRE_VALID_ENCODING;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown LangOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
 /* usage: LangPath path */
 MODRET set_langpath(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
@@ -338,16 +440,16 @@ MODRET set_useencoding(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   if (cmd->argc == 2) {
-    int b = -1;
+    int use_encoding = -1;
 
-    b = get_boolean(cmd, 1);
-    if (b == -1) {
+    use_encoding = get_boolean(cmd, 1);
+    if (use_encoding == -1) {
       CONF_ERROR(cmd, "expected Boolean parameter");
     }
 
     c = add_config_param(cmd->argv[0], 1, NULL);
     c->argv[0] = pcalloc(c->pool, sizeof(int));
-    *((int *) c->argv[0]) = b;
+    *((int *) c->argv[0]) = use_encoding;
 
   } else if (cmd->argc == 3 ||
              cmd->argc == 4) {
@@ -392,6 +494,9 @@ MODRET lang_lang(cmd_rec *cmd) {
   if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.cwd, NULL)) {
     pr_log_debug(DEBUG4, MOD_LANG_VERSION ": LANG command denied by <Limit>");
     pr_response_add_err(R_500, _("Unable to handle command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -409,11 +514,17 @@ MODRET lang_lang(cmd_rec *cmd) {
     pr_log_debug(DEBUG7, MOD_LANG_VERSION ": assuming language files are "
       "unavailable after login, denying LANG command");
     pr_response_add_err(R_500, _("Unable to handle command"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   if (cmd->argc > 2) {
-    pr_response_add_err(R_501, _("Invalid number of arguments"));
+    pr_response_add_err(R_501, _("Invalid number of parameters"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -434,18 +545,22 @@ MODRET lang_lang(cmd_rec *cmd) {
 
   if (lang_supported(cmd->tmp_pool, cmd->argv[1]) < 0) {
     pr_log_debug(DEBUG3, MOD_LANG_VERSION ": language '%s' unsupported: %s",
-      cmd->argv[1], strerror(errno));
-    pr_response_add_err(R_504, _("Language %s not supported"), cmd->argv[1]);
+      (char *) cmd->argv[1], strerror(errno));
+    pr_response_add_err(R_504, _("Language %s not supported"),
+      (char *) cmd->argv[1]);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   pr_log_debug(DEBUG7, MOD_LANG_VERSION
-    ": setting to client-requested language '%s'", cmd->argv[1]);
+    ": setting to client-requested language '%s'", (char *) cmd->argv[1]);
 
   if (lang_set_lang(cmd->tmp_pool, cmd->argv[1]) < 0) {
     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
-      ": unable to use client-requested language '%s': %s", cmd->argv[1],
-      strerror(errno));
+      ": unable to use client-requested language '%s': %s",
+      (char *) cmd->argv[1], strerror(errno));
     pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
       ": using LangDefault '%s' instead", lang_default);
 
@@ -472,9 +587,14 @@ MODRET lang_lang(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+MODRET lang_post_pass(cmd_rec *cmd) {
+  (void) process_encoding_config(NULL);
+  return PR_DECLINED(cmd);
+}
+
 MODRET lang_utf8(cmd_rec *cmd) {
   register unsigned int i;
-  int b;
+  int use_utf8;
   const char *curr_encoding;
   char *method;
 
@@ -484,159 +604,149 @@ MODRET lang_utf8(cmd_rec *cmd) {
    * logging.
    */
   for (i = 0; method[i]; i++) {
-    if (method[i] == '_')
+    if (method[i] == '_') {
       method[i] = ' ';
+    }
   }
 
   if (cmd->argc != 2) {
     pr_response_add_err(R_501, _("'%s' not understood"), method);
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  b = get_boolean(cmd, 1);
-  if (b < 0) {
+  use_utf8 = get_boolean(cmd, 1);
+  if (use_utf8 < 0) {
     pr_response_add_err(R_501, _("'%s' not understood"), method);
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   curr_encoding = pr_encode_get_encoding();
   if (curr_encoding != NULL) {
     pr_log_debug(DEBUG9, MOD_LANG_VERSION
-      ": Handling OPTS UTF8 %s (current encoding is '%s')", cmd->argv[1],
-      curr_encoding);
+      ": Handling OPTS UTF8 %s (current encoding is '%s')",
+      (char *) cmd->argv[1], curr_encoding);
 
   } else {
     pr_log_debug(DEBUG9, MOD_LANG_VERSION
-      ": Handling OPTS UTF8 %s (encoding currently disabled)", cmd->argv[1]);
+      ": Handling OPTS UTF8 %s (encoding currently disabled)",
+      (char *) cmd->argv[1]);
   }
 
   if (pr_encode_is_utf8(curr_encoding) == TRUE) {
-    if (b) {
+    if (use_utf8) {
       /* Client requested that we use UTF8, and we already are.  Nothing
        * more needs to be done.
        */
       pr_response_add(R_200, _("UTF8 set to on"));
 
     } else {
-      config_rec *c;
-
-      /* Client requested that we NOT use UTF8, and we are.  Need to disable
-       * encoding, then, unless the UseEncoding setting dictates that we
-       * must.
+      /* Client requested that we NOT use UTF8 (i.e. "OPTS UTF8 off"), and
+       * we are.  Need to disable encoding, then, unless the
+       * LangOptions/UseEncoding settings dictate that we MUST use UTF8.
        */
 
-      c = find_config(main_server->conf, CONF_PARAM, "UseEncoding", FALSE);
-      if (c) {
-        if (c->argc == 1) {
-          int use_encoding;
-
-          use_encoding = *((int *) c->argv[0]);
-          if (use_encoding) {
-            /* We have explicit UseEncoding instructions; we cannot change
-             * the encoding use as requested by the client.
-             */
-            pr_log_debug(DEBUG5, MOD_LANG_VERSION
-              ": unable to accept 'OPTS UTF8 off' due to UseEncoding "
-              "directive in config file");
-            pr_response_add_err(R_451, _("Unable to accept %s"), method);
-            return PR_ERROR(cmd);
-          }
-
-        } else {
-          char *local_charset, *client_charset;
-          int strict_charsets;
-
-          /* UseEncoding configured with specific charsets, and the client
-           * requested that we turn UTF8 support off.  Easy enough; just
-           * (re)set the encoding to use the configured charsets.
-           */
+      if (lang_use_encoding == TRUE) {
+        /* We have explicit instructions; we cannot change the encoding use as
+         * requested by the client.
+         */
+        pr_log_debug(DEBUG5, MOD_LANG_VERSION
+          ": unable to accept 'OPTS UTF8 off' due to LangOptions/UseEncoding "
+          "directive in config file");
+        pr_response_add_err(R_451, _("Unable to accept %s"), method);
 
-          local_charset = c->argv[0];
-          client_charset = c->argv[1];
-          strict_charsets = *((int *) c->argv[2]);
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
+        return PR_ERROR(cmd);
 
-          if (strict_charsets == TRUE) {
-            /* We have explicit UseEncoding instructions; we cannot change
-             * the encoding use as requested by the client.
-             */
-            pr_log_debug(DEBUG5, MOD_LANG_VERSION
-              ": unable to accept 'OPTS UTF8 off' due to UseEncoding "
-              "'strict' keyword in config file");
-            pr_response_add_err(R_451, _("Unable to accept %s"), method);
-            return PR_ERROR(cmd);
-          }
+      } else if (lang_local_charset != NULL &&
+                 lang_client_charset != NULL) {
 
-          if (pr_encode_set_charset_encoding(local_charset,
-              client_charset) < 0) {
+        /* UseEncoding configured with specific charsets, and the client
+         * requested that we turn UTF8 support off.  Easy enough; just
+         * (re)set the encoding to use the configured charsets.
+         */
 
-            pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
-              ": error setting local charset '%s', client charset '%s': %s",
-              local_charset, client_charset, strerror(errno));
-            pr_fs_use_encoding(FALSE);
+        if (lang_opts & LANG_OPT_PREFER_SERVER_ENCODING) {
+          /* We have explicit instructions; we cannot change the encoding
+           * use as requested by the client.
+           */
+          pr_log_debug(DEBUG5, MOD_LANG_VERSION
+            ": unable to accept 'OPTS UTF8 off' due to "
+            "LangOptions/UseEncoding directive in config file");
+          pr_response_add_err(R_451, _("Unable to accept %s"), method);
+
+          pr_cmd_set_errno(cmd, EPERM);
+          errno = EPERM;
+          return PR_ERROR(cmd);
+        }
 
-            pr_response_add_err(R_451, _("Unable to accept %s"), method);
-            return PR_ERROR(cmd);
-          }
+        if (pr_encode_set_charset_encoding(lang_local_charset,
+            lang_client_charset) < 0) {
 
-          pr_log_debug(DEBUG3, MOD_LANG_VERSION ": using local charset '%s', "
-            "client charset '%s' for path encoding", local_charset,
-            client_charset);
-          pr_fs_use_encoding(TRUE);
-          pr_response_add(R_200, _("UTF8 set to off"));
-          return PR_HANDLED(cmd);
+          pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
+            ": error setting local charset '%s', client charset '%s': %s",
+            lang_local_charset, lang_client_charset, strerror(errno));
+          pr_fs_use_encoding(FALSE);
+          pr_response_add_err(R_451, _("Unable to accept %s"), method);
+
+          pr_cmd_set_errno(cmd, EPERM);
+          errno = EPERM;
+          return PR_ERROR(cmd);
         }
-     }
 
-     pr_log_debug(DEBUG5, MOD_LANG_VERSION
-       ": disabling use of UTF8 encoding as per client's request");
+        pr_log_debug(DEBUG3, MOD_LANG_VERSION ": using local charset '%s', "
+          "client charset '%s' for path encoding", lang_local_charset,
+          lang_client_charset);
+        pr_fs_use_encoding(TRUE);
+        pr_response_add(R_200, _("UTF8 set to off"));
+        return PR_HANDLED(cmd);
+      }
+
+      pr_log_debug(DEBUG5, MOD_LANG_VERSION
+        ": disabling use of UTF8 encoding as per client's request");
 
       /* No explicit UseEncoding instructions; we can turn off encoding. */
       pr_encode_disable_encoding();
       pr_fs_use_encoding(FALSE);
-
       pr_response_add(R_200, _("UTF8 set to off"));
     }
 
   } else {
-    if (b) {
-      config_rec *c;
-
-      /* Client requested that we use UTF8, and we currently are not.
-       * Enable UTF8 encoding, unless the UseEncoding setting dictates that
-       * we cannot.
+    if (use_utf8) {
+      /* Client requested that we use UTF8 (i.e. "OPTS UTF8 on"), and we
+       * currently are not.  Enable UTF8 encoding, unless the
+       * LangOptions/UseEncoding setting dictates that we cannot.
        */
 
-      c = find_config(main_server->conf, CONF_PARAM, "UseEncoding", FALSE);
-      if (c) {
-        if (c->argc == 1) {
-          int use_encoding;
-
-          use_encoding = *((int *) c->argv[0]);
-          if (use_encoding == FALSE) {
-            /* We have explicit UseEncoding instructions. */
-            pr_log_debug(DEBUG5, MOD_LANG_VERSION
-              ": unable to accept 'OPTS UTF8 on' due to UseEncoding "
-              "directive in config file");
-            pr_response_add_err(R_451, _("Unable to accept %s"), method);
-            return PR_ERROR(cmd);
-          }
+      if (lang_use_encoding == FALSE) {
+        /* We have explicit instructions. */
+        pr_log_debug(DEBUG5, MOD_LANG_VERSION
+          ": unable to accept 'OPTS UTF8 on' due to LangOptions/UseEncoding "
+          "directive in config file");
+        pr_response_add_err(R_451, _("Unable to accept %s"), method);
 
-        } else {
-          int strict_charsets;
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
+        return PR_ERROR(cmd);
 
-          strict_charsets = *((int *) c->argv[2]);
+      } else if (lang_opts & LANG_OPT_PREFER_SERVER_ENCODING) {
+        /* We have explicit instructions; we cannot change the encoding use
+         * as requested by the client.
+         */
+        pr_log_debug(DEBUG5, MOD_LANG_VERSION
+          ": unable to accept 'OPTS UTF8 on' due to LangOptions/UseEncoding "
+          "directive in config file");
+        pr_response_add_err(R_451, _("Unable to accept %s"), method);
 
-          if (strict_charsets == TRUE) {
-            /* We have explicit UseEncoding instructions; we cannot change
-             * the encoding use as requested by the client.
-             */
-            pr_log_debug(DEBUG5, MOD_LANG_VERSION
-              ": unable to accept 'OPTS UTF8 on' due to UseEncoding "
-              "'strict' keyword in config file");
-            pr_response_add_err(R_451, _("Unable to accept %s"), method);
-            return PR_ERROR(cmd);
-          }
-        }
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
+        return PR_ERROR(cmd);
       } 
 
       pr_log_debug(DEBUG5, MOD_LANG_VERSION
@@ -647,6 +757,9 @@ MODRET lang_utf8(cmd_rec *cmd) {
         pr_log_debug(DEBUG3, MOD_LANG_VERSION
           ": error enabling UTF8 encoding: %s", strerror(errno));
         pr_response_add_err(R_451, _("Unable to accept %s"), method);
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
       }
 
@@ -672,9 +785,7 @@ static void lang_postparse_ev(const void *event_data, void *user_data) {
   config_rec *c;
   DIR *dirh;
   server_rec *s;
-#ifdef HAVE_LIBINTL_H
   const char *locale_path = NULL;
-#endif
 
   c = find_config(main_server->conf, CONF_PARAM, "LangEngine", FALSE);
   if (c) {
@@ -844,6 +955,7 @@ static void lang_postparse_ev(const void *event_data, void *user_data) {
 
 static void lang_restart_ev(const void *event_data, void *user_data) {
   destroy_pool(lang_pool);
+  lang_curr = LANG_DEFAULT_LANG;
   lang_list = NULL;
   lang_aliases = NULL;
 
@@ -866,17 +978,19 @@ static int lang_init(void) {
 
 static int lang_sess_init(void) {
   config_rec *c;
-  int strict_encoding = FALSE, utf8_client_encoding = FALSE;
+  int res, utf8_client_encoding = FALSE;
 
   c = find_config(main_server->conf, CONF_PARAM, "LangEngine", FALSE);
-  if (c)
+  if (c != NULL) {
     lang_engine = *((int *) c->argv[0]);
+  }
 
-  if (!lang_engine)
+  if (lang_engine == FALSE) {
     return 0;
+  }
 
   c = find_config(main_server->conf, CONF_PARAM, "LangDefault", FALSE);
-  if (c) {
+  if (c != NULL) {
     char *lang;
 
     lang = c->argv[0];
@@ -905,7 +1019,7 @@ static int lang_sess_init(void) {
      * could not be found for some reason), we should still have an entry for
      * the current language.
      */
-    if (!lang_list) {
+    if (lang_list == NULL) {
       lang_list = make_array(lang_pool, 1, sizeof(char *));
     }
 
@@ -914,63 +1028,39 @@ static int lang_sess_init(void) {
     } 
   }
 
-  c = find_config(main_server->conf, CONF_PARAM, "UseEncoding", FALSE);
-  if (c) {
-    if (c->argc == 1) {
-      int use_encoding;
-
-      use_encoding = *((int *) c->argv[0]);
-      if (use_encoding) {
-        pr_fs_use_encoding(TRUE);
+  c = find_config(main_server->conf, CONF_PARAM, "LangOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
 
-      } else {
-        pr_encode_disable_encoding();
-        pr_fs_use_encoding(FALSE);
-      }
+    pr_signals_handle();
 
-    } else {
-      char *local_charset, *client_charset;
+    opts = *((unsigned long *) c->argv[0]);
+    lang_opts |= opts;
 
-      local_charset = c->argv[0];
-      client_charset = c->argv[1];
-      strict_encoding = *((int *) c->argv[2]);
+    c = find_config_next(c, c->next, CONF_PARAM, "LangOptions", FALSE);
+  }
 
-      if (strcasecmp(client_charset, "UTF8") == 0 ||
-          strcasecmp(client_charset, "UTF-8") == 0) {
-        utf8_client_encoding = TRUE;
-      }
+  if (lang_opts & LANG_OPT_REQUIRE_VALID_ENCODING) {
+    unsigned long encoding_policy;
 
-      if (pr_encode_set_charset_encoding(local_charset, client_charset) < 0) {
-        pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
-          ": error setting local charset '%s', client charset '%s': %s",
-          local_charset, client_charset, strerror(errno));
-        pr_fs_use_encoding(FALSE);
+    encoding_policy = pr_encode_get_policy();
+    encoding_policy |= PR_ENCODE_POLICY_FL_REQUIRE_VALID_ENCODING;
 
-      } else {
-        pr_log_debug(DEBUG3, MOD_LANG_VERSION ": using local charset '%s', "
-          "client charset '%s' for path encoding", local_charset,
-          client_charset);
-        pr_fs_use_encoding(TRUE);
-
-        /* Make sure that gettext() uses the specified charset as well. */
-        if (bind_textdomain_codeset("proftpd", client_charset) == NULL) {
-          pr_log_pri(PR_LOG_NOTICE, MOD_LANG_VERSION
-            ": error setting client charset '%s' for localised messages: %s",
-            client_charset, strerror(errno));
-        }
-      }
-    }
+    pr_encode_set_policy(encoding_policy);
+  }
 
-  } else {
+  res = process_encoding_config(&utf8_client_encoding);
+  if (res < 0 &&
+      errno == ENOENT) {
     /* Default is to use UTF8. */
     pr_fs_use_encoding(TRUE);
   }
 
-  /* If strict encoding is not required, OR if the encoding configured
-   * explicitly requests UTF8 from the client, then we can list UTF8 in
-   * the FEAT response. 
+  /* If the PreferServerEncoding LangOption is not set, OR if the encoding
+   * configured explicitly requests UTF8 from the client, then we can list
+   * UTF8 in the FEAT response. 
    */
-  if (strict_encoding == FALSE ||
+  if (!(lang_opts & LANG_OPT_PREFER_SERVER_ENCODING) ||
       utf8_client_encoding == TRUE) {
     pr_feat_add("UTF8");
   }
@@ -989,6 +1079,7 @@ static int lang_sess_init(void) {
 static conftable lang_conftab[] = {
   { "LangDefault",	set_langdefault,	NULL },
   { "LangEngine",	set_langengine,		NULL },
+  { "LangOptions",	set_langoptions,	NULL },
   { "LangPath",		set_langpath,		NULL },
   { "UseEncoding",	set_useencoding,	NULL },
   { NULL }
@@ -997,6 +1088,7 @@ static conftable lang_conftab[] = {
 static cmdtable lang_cmdtab[] = {
   { CMD,	C_LANG,			G_NONE,	lang_lang,	FALSE,	FALSE },
   { CMD,	C_OPTS "_UTF8",		G_NONE,	lang_utf8,	FALSE,	FALSE },
+  { POST_CMD,	C_PASS,			G_NONE,	lang_post_pass,	FALSE,	FALSE },
   { 0, NULL }
 };
 
diff --git a/modules/mod_log.c b/modules/mod_log.c
index 5200b40..4f7ee0e 100644
--- a/modules/mod_log.c
+++ b/modules/mod_log.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -28,7 +28,7 @@
 
 #include "conf.h"
 #include "privs.h"
-#include "mod_log.h"
+#include "logfmt.h"
 
 module log_module;
 
@@ -57,7 +57,8 @@ struct logfile_struc {
 
   logformat_t		*lf_format;
 
-  int			lf_classes;
+  int			lf_incl_classes;
+  int			lf_excl_classes;
 
   /* Pointer to the "owning" configuration */
   config_rec		*lf_conf;
@@ -98,12 +99,13 @@ static xaset_t *log_set = NULL;
    %l			- Remote logname (from identd)
    %m			- Request (command) method (RETR, etc)
    %O                   - Total number of "raw" bytes written out to network
-   %P			- Process ID of child serving request
+   %P                   - Process ID of child serving request
    %p			- Port of server serving request
+   %R                   - Response time for command/request, in milliseconds
    %r			- Full request (command)
    %s			- Response code (status)
    %S                   - Response string
-   %T			- Time taken to serve request, in seconds
+   %T			- Time taken to transfer file, in seconds
    %t			- Time
    %{format}t		- Formatted time (strftime(3) format)
    %U                   - Original username sent by client
@@ -113,6 +115,8 @@ static xaset_t *log_set = NULL;
    %w                   - RNFR path ("whence" a rename comes, i.e. the source)
    %{file-modified}     - Indicates whether a file is being modified
                           (i.e. already exists) or not.
+   %{file-offset}       - Contains the offset at which the file is read/written
+   %{file-size}         - Contains the file size at the end of the transfer
    %{iso8601}           - ISO-8601 timestamp: YYYY-MM-dd HH:mm:ss,SSS
                             for example: "1999-11-27 15:49:37,459"
    %{microsecs}         - 6 digits of microseconds of current time
@@ -120,13 +124,16 @@ static xaset_t *log_set = NULL;
    %{protocol}          - Current protocol (e.g. "ftp", "sftp", etc)
    %{uid}               - UID of logged-in user
    %{gid}               - Primary GID of logged-in user
-   %{transfer-status}   - "success", "failed", "cancelled", "timeout", or "-"
    %{transfer-failure}  - reason, or "-"
+   %{transfer-millisecs}- Time taken to transfer file, in milliseconds
+   %{transfer-status}   - "success", "failed", "cancelled", "timeout", or "-"
+   %{transfer-type}     - "binary" or "ASCII"
    %{version}           - ProFTPD version
 */
 
 /* Necessary prototypes */
 static int log_sess_init(void);
+static void log_xfer_stalled_ev(const void *, void *);
 
 static void add_meta(unsigned char **s, unsigned char meta, int args, ...) {
   int arglen;
@@ -197,6 +204,18 @@ static void logformat(const char *directive, char *nickname, char *fmts) {
           continue;
         }
 
+        if (strncmp(tmp, "{file-offset}", 13) == 0) {
+          add_meta(&outs, LOGFMT_META_FILE_OFFSET, 0);
+          tmp += 13;
+          continue;
+        }
+
+        if (strncmp(tmp, "{file-size}", 11) == 0) {
+          add_meta(&outs, LOGFMT_META_FILE_SIZE, 0);
+          tmp += 11;
+          continue;
+        }
+
         if (strncmp(tmp, "{gid}", 5) == 0) {
           add_meta(&outs, LOGFMT_META_GID, 0);
           tmp += 5;
@@ -227,12 +246,24 @@ static void logformat(const char *directive, char *nickname, char *fmts) {
           continue;
         }
 
+        if (strncmp(tmp, "{remote-port}", 13) == 0) {
+          add_meta(&outs, LOGFMT_META_REMOTE_PORT, 0);
+          tmp += 13;
+          continue;
+        }
+
         if (strncmp(tmp, "{uid}", 5) == 0) {
           add_meta(&outs, LOGFMT_META_UID, 0);
           tmp += 5;
           continue;
         }
 
+        if (strncmp(tmp, "{transfer-millisecs}", 20) == 0) {
+          add_meta(&outs, LOGFMT_META_XFER_MS, 0);
+          tmp += 20;
+          continue;
+        }
+
         if (strncmp(tmp, "{transfer-failure}", 18) == 0) {
           add_meta(&outs, LOGFMT_META_XFER_FAILURE, 0);
           tmp += 18;
@@ -245,6 +276,12 @@ static void logformat(const char *directive, char *nickname, char *fmts) {
           continue;
         }
 
+        if (strncmp(tmp, "{transfer-type}", 15) == 0) {
+          add_meta(&outs, LOGFMT_META_XFER_TYPE, 0);
+          tmp += 15;
+          continue;
+        }
+
         if (strncmp(tmp, "{version}", 9) == 0) {
           add_meta(&outs, LOGFMT_META_VERSION, 0);
           tmp += 9;
@@ -369,6 +406,10 @@ static void logformat(const char *directive, char *nickname, char *fmts) {
             add_meta(&outs, LOGFMT_META_COMMAND, 0);
             break;
 
+          case 'R':
+            add_meta(&outs, LOGFMT_META_RESPONSE_MS, 0);
+            break;
+
           case 's':
             add_meta(&outs, LOGFMT_META_RESPONSE_CODE, 0);
             break;
@@ -432,9 +473,8 @@ static void logformat(const char *directive, char *nickname, char *fmts) {
   lf->lf_format = palloc(log_pool, outs - format);
   memcpy(lf->lf_format, format, outs - format);
 
-  if (format_set == NULL) {
+  if (!format_set)
     format_set = xaset_create(log_pool, NULL);
-  }
 
   xaset_insert_end(format_set, (xasetmember_t *) lf);
   formats = (logformat_t *) format_set->xas_list;
@@ -459,15 +499,21 @@ MODRET set_logformat(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 2);
   CHECK_CONF(cmd, CONF_ROOT);
 
+  if (strlen(cmd->argv[1]) == 0) {
+    CONF_ERROR(cmd, "missing required nickname parameter");
+  }
+
   logformat(cmd->argv[0], cmd->argv[1], cmd->argv[2]);
   return PR_HANDLED(cmd);
 }
 
-static int parse_classes(char *s) {
-  int classes = CL_NONE;
+static int parse_classes(char *s, int *incl_classes, int *excl_classes) {
+  int incl = CL_NONE, excl = CL_NONE;
   char *nextp = NULL;
 
   do {
+    int exclude = FALSE;
+
     pr_signals_handle();
 
     nextp = strchr(s, ',');
@@ -481,42 +527,123 @@ static int parse_classes(char *s) {
       }
     }
 
-    if (strcasecmp(s, "NONE") == 0) {
-      classes = CL_NONE;
-      break;
+    if (*s == '!') {
+      exclude = TRUE;
+      s++;
     }
 
-    if (strcasecmp(s, "ALL") == 0) {
-      classes = CL_ALL;
-      break;
+    if (strcasecmp(s, "NONE") == 0) {
+      if (exclude) {
+        incl = CL_ALL;
+        excl = CL_NONE;
+
+      } else {
+        incl = CL_NONE;
+      }
+
+    } else if (strcasecmp(s, "ALL") == 0) {
+      if (exclude) {
+        incl = CL_NONE;
+        excl = CL_ALL;
+
+      } else {
+        incl = CL_ALL;
+      }
 
     } else if (strcasecmp(s, "AUTH") == 0) {
-      classes |= CL_AUTH;
+      if (exclude) {
+        incl &= ~CL_AUTH;
+        excl |= CL_AUTH;
+
+      } else {
+        incl |= CL_AUTH;
+      }
 
     } else if (strcasecmp(s, "INFO") == 0) {
-      classes |= CL_INFO;
+      if (exclude) {
+        incl &= ~CL_INFO;
+        excl |= CL_INFO;
+
+      } else {
+        incl |= CL_INFO;
+      }
 
     } else if (strcasecmp(s, "DIRS") == 0) {
-      classes |= CL_DIRS;
+      if (exclude) {
+        incl &= ~CL_DIRS;
+        excl |= CL_DIRS;
+
+      } else {
+        incl |= CL_DIRS;
+      }
 
     } else if (strcasecmp(s, "READ") == 0) {
-      classes |= CL_READ;
+      if (exclude) {
+        incl &= ~CL_READ;
+        excl |= CL_READ;
+
+      } else { 
+        incl |= CL_READ;
+      }
 
     } else if (strcasecmp(s, "WRITE") == 0) {
-      classes |= CL_WRITE;
+      if (exclude) {
+        incl &= ~CL_WRITE;
+        excl |= CL_WRITE;
+
+      } else {
+        incl |= CL_WRITE;
+      }
 
     } else if (strcasecmp(s, "MISC") == 0) {
-      classes |= CL_MISC;
+      if (exclude) {
+        incl &= ~CL_MISC;
+        excl |= CL_MISC;
+
+      } else {
+        incl |= CL_MISC;
+      }
 
     } else if (strcasecmp(s, "SEC") == 0 ||
                strcasecmp(s, "SECURE") == 0) {
-      classes |= CL_SEC;
+      if (exclude) {
+        incl &= ~CL_SEC;
+        excl |= CL_SEC;
+
+      } else {
+        incl |= CL_SEC;
+      }
 
     } else if (strcasecmp(s, "EXIT") == 0) {
-      classes |= CL_EXIT;
+      if (exclude) {
+        incl &= ~CL_EXIT;
+        excl |= CL_EXIT;
+
+      } else {
+        incl |= CL_EXIT;
+      }
+
+    } else if (strcasecmp(s, "SSH") == 0) {
+      if (exclude) {
+        incl &= ~CL_SSH;
+        excl |= CL_SSH;
+
+      } else {
+        incl |= CL_SSH;
+      }
+
+    } else if (strcasecmp(s, "SFTP") == 0) {
+      if (exclude) {
+        incl &= ~CL_SFTP;
+        excl |= CL_SFTP;
+
+      } else {
+        incl |= CL_SFTP;
+      }
 
     } else {
       pr_log_pri(PR_LOG_NOTICE, "ExtendedLog class '%s' is not defined", s);
+      errno = EINVAL;
       return -1;
     }
 
@@ -525,61 +652,68 @@ static int parse_classes(char *s) {
 
   } while (s);
 
-  return classes;
+  *incl_classes = incl;
+  *excl_classes = excl;
+  return 0;
 }
 
 /* Syntax: ExtendedLog file [<cmd-classes> [<nickname>]] */
 MODRET set_extendedlog(cmd_rec *cmd) {
   config_rec *c = NULL;
   int argc;
+  char *path;
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
 
   argc = cmd->argc;
 
-  if (argc < 2)
+  if (argc < 2) {
     CONF_ERROR(cmd, "Syntax: ExtendedLog file [<cmd-classes> [<nickname>]]");
+  }
 
-  c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+  c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
 
-  if (strncasecmp(cmd->argv[1], "syslog:", 7) == 0) {
-    char *tmp = strchr(cmd->argv[1], ':');
+  path = cmd->argv[1];
+  if (strncasecmp(path, "syslog:", 7) == 0) {
+    char *ptr;
 
-    if (pr_log_str2sysloglevel(++tmp) < 0) {
-      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown syslog level: '",
-        tmp, "'", NULL));
+    ptr = strchr(path, ':');
 
-    } else {
-      c->argv[0] = pstrdup(log_pool, cmd->argv[1]);
+    if (pr_log_str2sysloglevel(++ptr) < 0) {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown syslog level: '",
+        ptr, "'", NULL));
     }
 
-  } else if (cmd->argv[1][0] != '/') {
+    c->argv[0] = pstrdup(log_pool, path);
+
+  } else if (path[0] != '/') {
     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "relative paths not allowed: '",
-        cmd->argv[1], "'", NULL));
+      path, "'", NULL));
 
   } else {
-    c->argv[0] = pstrdup(log_pool, cmd->argv[1]);
+    c->argv[0] = pstrdup(log_pool, path);
   }
 
   if (argc > 2) {
-    int res;
+    int incl_classes = 0, excl_classes = 0, res;
 
     /* Parse the given class names, to make sure that they are all valid. */
-    res = parse_classes(cmd->argv[2]);
+    res = parse_classes(cmd->argv[2], &incl_classes, &excl_classes);
     if (res < 0) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid log class in '",
         cmd->argv[2], "'", NULL));    
     }
 
     c->argv[1] = palloc(c->pool, sizeof(int));
-    *((int *) c->argv[1]) = res;
+    *((int *) c->argv[1]) = incl_classes;
+    c->argv[2] = palloc(c->pool, sizeof(int));
+    *((int *) c->argv[2]) = excl_classes;
   }
 
   if (argc > 3) {
-    c->argv[2] = pstrdup(log_pool, cmd->argv[3]);
+    c->argv[3] = pstrdup(log_pool, cmd->argv[3]);
   }
 
-  c->argc = argc-1;
   return PR_HANDLED(cmd);
 }
 
@@ -621,26 +755,40 @@ MODRET set_systemlog(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-static struct tm *_get_gmtoff(int *tz) {
-  time_t tt = time(NULL);
-  struct tm gmt;
-  struct tm *t;
-  int days, hours, minutes;
-
-  gmt = *gmtime(&tt);
-  t = pr_localtime(NULL, &tt);
-
-  days = t->tm_yday - gmt.tm_yday;
-  hours = ((days < -1 ? 24 : 1 < days ? -24 : days * 24)
-          + t->tm_hour - gmt.tm_hour);
-  minutes = hours * 60 + t->tm_min - gmt.tm_min;
-  *tz = minutes;
-  return t;
+static struct tm *get_gmtoff(int *tz) {
+  time_t now;
+  struct tm *gmt, *tm = NULL;
+
+  /* Note that the ordering of the calls to gmtime(3) and pr_localtime()
+   * here are IMPORTANT; gmtime(3) MUST be called first.  Otherwise,
+   * the TZ environment variable may not be honored as one would expect;
+   * see:
+   *  https://forums.proftpd.org/smf/index.php/topic,11971.0.html
+   */
+  time(&now);
+  gmt = gmtime(&now);
+  if (gmt != NULL) {
+    int days, hours, minutes;
+
+    tm = pr_localtime(NULL, &now);
+    if (tm != NULL) {
+      days = tm->tm_yday - gmt->tm_yday;
+      hours = ((days < -1 ? 24 : 1 < days ? -24 : days * 24)
+              + tm->tm_hour - gmt->tm_hour);
+      minutes = hours * 60 + tm->tm_min - gmt->tm_min;
+      *tz = minutes;
+    }
+  }
+
+  return tm;
 }
 
-static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
+static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f,
+    size_t *mlen) {
   unsigned char *m;
-  char arg[PR_TUNABLE_PATH_MAX+1] = {'\0'}, *argp = NULL, *pass;
+  const char *pass;
+  char arg[PR_TUNABLE_PATH_MAX+1] = {'\0'}, *argp = NULL;
+  int len = 0;
 
   /* This function can cause potential problems.  Custom logformats
    * might overrun the arg buffer.  Fixing this problem involves a
@@ -652,8 +800,10 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
     case LOGFMT_META_ARG:
       m++;
       argp = arg;
-      while (*m != LOGFMT_META_ARG_END)
+      while (*m != LOGFMT_META_ARG_END) {
         *argp++ = (char) *m++;
+        len++;
+      }
 
       *argp = 0;
       argp = arg;
@@ -664,33 +814,35 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
 
       pass = pr_table_get(session.notes, "mod_auth.anon-passwd", NULL);
-      if (!pass)
+      if (pass == NULL) {
         pass = "UNKNOWN";
+      }
 
-      sstrncpy(argp, pass, sizeof(arg));
-
+      len = sstrncpy(argp, pass, sizeof(arg));
       m++;
       break;
 
     case LOGFMT_META_BYTES_SENT:
       argp = arg;
       if (session.xfer.p) {
-        snprintf(argp, sizeof(arg), "%" PR_LU,
+        len = snprintf(argp, sizeof(arg), "%" PR_LU,
           (pr_off_t) session.xfer.total_bytes);
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_DELE_ID) == 0) {
-        snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) log_dele_filesz);
+        len = snprintf(argp, sizeof(arg), "%" PR_LU,
+          (pr_off_t) log_dele_filesz);
 
-      } else
-        sstrncpy(argp, "-", sizeof(arg));
+      } else {
+        len = sstrncpy(argp, "-", sizeof(arg));
+      }
 
       m++;
       break;
 
     case LOGFMT_META_CLASS:
       argp = arg;
-      sstrncpy(argp, session.conn_class ? session.conn_class->cls_name : "-",
-        sizeof(arg));
+      len = sstrncpy(argp,
+        session.conn_class ? session.conn_class->cls_name : "-", sizeof(arg));
       m++;
       break;
 
@@ -715,21 +867,21 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
         if (tmp != NULL) {
           if (tmp != path) {
-            sstrncpy(argp, tmp + 1, sizeof(arg));
+            len = sstrncpy(argp, tmp + 1, sizeof(arg));
 
           } else if (*(tmp+1) != '\0') {
-            sstrncpy(argp, tmp + 1, sizeof(arg));
+            len = sstrncpy(argp, tmp + 1, sizeof(arg));
 
           } else {
-            sstrncpy(argp, path, sizeof(arg));
+            len = sstrncpy(argp, path, sizeof(arg));
           }
 
         } else {
-          sstrncpy(argp, path, sizeof(arg));
+          len = sstrncpy(argp, path, sizeof(arg));
         }
 
       } else {
-        sstrncpy(argp, pr_fs_getvwd(), sizeof(arg));
+        len = sstrncpy(argp, pr_fs_getvwd(), sizeof(arg));
       }
 
       m++;
@@ -747,8 +899,11 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
           pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
           pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
-        sstrncpy(argp, dir_abs_path(p, pr_fs_decode_path(p, cmd->arg), TRUE),
-          sizeof(arg));
+        char *decoded_path, *abs_path;
+
+        decoded_path = pr_fs_decode_path(p, cmd->arg);
+        abs_path = dir_abs_path(p, decoded_path, TRUE);
+        len = sstrncpy(argp, abs_path, sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_XCWD_ID) == 0) {
@@ -763,34 +918,38 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
         if (session.chroot_path) { 
           /* Chrooted session. */
-          sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
+          len = sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
             pdircat(p, session.chroot_path, pr_fs_getvwd(), NULL) :
             session.chroot_path, sizeof(arg));
 
         } else {
-
           /* Non-chrooted session. */
-          sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
+          len = sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
         }
 
       } else {
-        sstrncpy(argp, "", sizeof(arg));
+        len = sstrncpy(argp, "", sizeof(arg));
       }
 
       m++;
       break;
 
     case LOGFMT_META_EOS_REASON: {
-      const char *reason_str;
-      char *details = NULL;
+      const char *details = NULL, *reason_str;
 
       argp = arg;
 
       reason_str = pr_session_get_disconnect_reason(&details);
-      sstrncpy(argp, reason_str, sizeof(arg));
+      len = sstrncpy(argp, reason_str, sizeof(arg));
       if (details != NULL) {
+        size_t details_len;
+
+        details_len = strlen(details);
+
         sstrcat(argp, ": ", sizeof(arg));
         sstrcat(argp, details, sizeof(arg));
+
+        len += details_len + 2;
       }
 
       m++;
@@ -801,31 +960,47 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
 
       if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
-        sstrncpy(argp, dir_abs_path(p, pr_fs_decode_path(p, cmd->arg), TRUE),
-          sizeof(arg));
+        char *decoded_path, *abs_path;
+
+        decoded_path = pr_fs_decode_path(p, cmd->arg);
+        abs_path = dir_abs_path(p, decoded_path, TRUE);
+
+        len = sstrncpy(argp, abs_path, sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0) {
-        char *path;
+        const char *path;
 
         path = pr_table_get(cmd->notes, "mod_xfer.retr-path", NULL);
-        sstrncpy(arg, dir_abs_path(p, path, TRUE), sizeof(arg));
+        if (path != NULL) {
+          len = sstrncpy(arg, dir_abs_path(p, path, TRUE), sizeof(arg));
+
+        } else {
+          len = sstrncpy(argp, "", sizeof(arg));
+        }
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0) {
-        char *path;
+        const char *path;
       
         path = pr_table_get(cmd->notes, "mod_xfer.store-path", NULL);
-        sstrncpy(arg, dir_abs_path(p, path, TRUE), sizeof(arg));
+        if (path != NULL) {
+          len = sstrncpy(arg, dir_abs_path(p, path, TRUE), sizeof(arg));
+
+        } else {
+          len = sstrncpy(argp, "", sizeof(arg));
+        }
 
       } else if (session.xfer.p &&
                  session.xfer.path) {
-        sstrncpy(argp, dir_abs_path(p, session.xfer.path, TRUE), sizeof(arg));
+        len = sstrncpy(argp, dir_abs_path(p, session.xfer.path, TRUE),
+          sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_CDUP_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_PWD_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_XPWD_ID) == 0) {
-        sstrncpy(argp, dir_abs_path(p, pr_fs_getcwd(), TRUE), sizeof(arg));
+        len = sstrncpy(argp, dir_abs_path(p, pr_fs_getcwd(), TRUE),
+          sizeof(arg));
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
                  pr_cmd_cmp(cmd, PR_CMD_XCWD_ID) == 0) {
@@ -839,13 +1014,13 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
          */
         if (session.chroot_path) {
           /* Chrooted session. */
-          sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
+          len = sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
             pdircat(p, session.chroot_path, pr_fs_getvwd(), NULL) :
             session.chroot_path, sizeof(arg));
 
         } else {
           /* Non-chrooted session. */
-          sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
+          len = sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));
         }
 
       } else if (pr_cmd_cmp(cmd, PR_CMD_SITE_ID) == 0 &&
@@ -860,7 +1035,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
             pr_fs_decode_path(cmd->tmp_pool, cmd->argv[i]), NULL);
         }
 
-        sstrncpy(argp, dir_abs_path(p, tmp, TRUE), sizeof(arg));
+        len = sstrncpy(argp, dir_abs_path(p, tmp, TRUE), sizeof(arg));
 
       } else {
         /* Some commands (i.e. DELE, MKD, RMD, XMKD, and XRMD) have associated
@@ -877,17 +1052,41 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
             pr_cmd_cmp(cmd, PR_CMD_RMD_ID) == 0 ||
             pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
             pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
-          sstrncpy(arg, dir_abs_path(p, pr_fs_decode_path(p, cmd->arg), TRUE),
-            sizeof(arg));
+          char *abs_path, *decoded_path;
+
+          decoded_path = pr_fs_decode_path(p, cmd->arg);
+          abs_path = dir_abs_path(p, decoded_path, TRUE);
+          if (abs_path == NULL) {
+            /* This time, try without the interpolation. */
+            abs_path = dir_abs_path(p, decoded_path, FALSE);
+          }
+
+          if (abs_path == NULL) {
+            abs_path = decoded_path;
+          }
+
+          len = sstrncpy(arg, abs_path, sizeof(arg));
 
         } else if (pr_cmd_cmp(cmd, PR_CMD_MFMT_ID) == 0) {
+          char *abs_path, *decoded_path;
+
           /* MFMT has, as its filename, the second argument. */
-          sstrncpy(arg, dir_abs_path(p, pr_fs_decode_path(p, cmd->argv[2]),
-            TRUE), sizeof(arg));
+          decoded_path = pr_fs_decode_path(p, cmd->argv[2]);
+          abs_path = dir_abs_path(p, decoded_path, TRUE);
+          if (abs_path == NULL) {
+            /* This time, try without the interpolation. */
+            abs_path = dir_abs_path(p, decoded_path, FALSE);
+          }
+
+          if (abs_path == NULL) {
+            abs_path = decoded_path;
+          }
+
+          len = sstrncpy(arg, abs_path, sizeof(arg));
  
         } else {
           /* All other situations get a "-".  */
-          sstrncpy(argp, "-", sizeof(arg));
+          len = sstrncpy(argp, "-", sizeof(arg));
         }
       }
 
@@ -981,22 +1180,22 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
         ptr = strrchr(path, '/');
         if (ptr != NULL) {
           if (ptr != path) {
-            sstrncpy(argp, ptr + 1, sizeof(arg));
+            len = sstrncpy(argp, ptr + 1, sizeof(arg));
 
           } else if (*(ptr+1) != '\0') {
-            sstrncpy(argp, ptr + 1, sizeof(arg));
+            len = sstrncpy(argp, ptr + 1, sizeof(arg));
 
           } else {
-            sstrncpy(argp, path, sizeof(arg));
+            len = sstrncpy(argp, path, sizeof(arg));
           }
 
         } else {
-          sstrncpy(argp, path, sizeof(arg));
+          len = sstrncpy(argp, path, sizeof(arg));
         }
 
       } else {
         /* All other situations get a "-".  */
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1011,11 +1210,11 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
         path = dir_best_path(cmd->tmp_pool,
           pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
-        sstrncpy(arg, path, sizeof(arg));
+        len = sstrncpy(arg, path, sizeof(arg));
 
       } else if (session.xfer.p &&
                  session.xfer.path) {
-        sstrncpy(argp, session.xfer.path, sizeof(arg));
+        len = sstrncpy(argp, session.xfer.path, sizeof(arg));
 
       } else {
         /* Some commands (i.e. DELE, MKD, XMKD, RMD, XRMD) have associated
@@ -1031,10 +1230,10 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
           path = dir_best_path(cmd->tmp_pool,
             pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
-          sstrncpy(arg, path, sizeof(arg));
+          len = sstrncpy(arg, path, sizeof(arg));
 
         } else {
-          sstrncpy(argp, "-", sizeof(arg));
+          len = sstrncpy(argp, "-", sizeof(arg));
         }
       }
 
@@ -1047,11 +1246,15 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
       if (*m == LOGFMT_META_START &&
           *(m+1) == LOGFMT_META_ARG) {
-        char *key = get_next_meta(p, cmd, &m);
-        if (key) {
-          char *env = pr_env_get(cmd->tmp_pool, key);
-          if (env) {
-            sstrncpy(argp, env, sizeof(arg));
+        char *key;
+
+        key = get_next_meta(p, cmd, &m, NULL);
+        if (key != NULL) {
+          char *env;
+
+          env = pr_env_get(cmd->tmp_pool, key);
+          if (env != NULL) {
+            len = sstrncpy(argp, env, sizeof(arg));
           }
         }
       }
@@ -1064,9 +1267,11 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
       if (*m == LOGFMT_META_START &&
           *(m+1) == LOGFMT_META_ARG) {
-        char *key = get_next_meta(p, cmd, &m);
-        if (key) {
-          char *note = NULL;
+        char *key;
+
+        key = get_next_meta(p, cmd, &m, NULL);
+        if (key != NULL) {
+          const char *note = NULL;
 
           /* Check in the cmd->notes table first. */
           note = pr_table_get(cmd->notes, key, NULL);
@@ -1075,8 +1280,8 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
             note = pr_table_get(session.notes, key, NULL);
           }
  
-          if (note) {
-            sstrncpy(argp, note, sizeof(arg));
+          if (note != NULL) {
+            len = sstrncpy(argp, note, sizeof(arg));
           }
         }
       }
@@ -1085,19 +1290,30 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
     case LOGFMT_META_REMOTE_HOST:
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_sess_remote_name(), sizeof(arg));
+      len = sstrncpy(argp, pr_netaddr_get_sess_remote_name(), sizeof(arg));
       m++;
       break;
 
     case LOGFMT_META_REMOTE_IP:
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr()),
-        sizeof(arg));
+      len = sstrncpy(argp,
+        pr_netaddr_get_ipstr(pr_netaddr_get_sess_remote_addr()), sizeof(arg));
+      m++;
+      break;
+
+    case LOGFMT_META_REMOTE_PORT: {
+      const pr_netaddr_t *addr;
+
+      argp = arg;
+
+      addr = pr_netaddr_get_sess_remote_addr();
+      len = snprintf(argp, sizeof(arg), "%d", ntohs(pr_netaddr_get_port(addr)));
       m++;
       break;
+    }
 
     case LOGFMT_META_RENAME_FROM: {
-      char *rnfr_path = "-";
+      const char *rnfr_path = "-";
 
       argp = arg;
       if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
@@ -1110,21 +1326,22 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
         }
       }
 
-      sstrncpy(argp, rnfr_path, sizeof(arg));
+      len = sstrncpy(argp, rnfr_path, sizeof(arg));
       m++;
       break;
     }
 
     case LOGFMT_META_IDENT_USER: {
-      char *rfc1413_ident;
+      const char *rfc1413_ident;
 
       argp = arg;
       rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident",
         NULL);
-      if (rfc1413_ident == NULL)
+      if (rfc1413_ident == NULL) {
         rfc1413_ident = "UNKNOWN";
+      }
 
-      sstrncpy(argp, rfc1413_ident, sizeof(arg));
+      len = sstrncpy(argp, rfc1413_ident, sizeof(arg));
       m++;
       break;
     }
@@ -1133,7 +1350,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
 
       if (pr_cmd_cmp(cmd, PR_CMD_SITE_ID) != 0) {
-        sstrncpy(argp, cmd->argv[0], sizeof(arg));
+        len = sstrncpy(argp, cmd->argv[0], sizeof(arg));
 
       } else {
         char *ptr;
@@ -1146,7 +1363,8 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
           *ptr = toupper((int) *ptr);
         }
 
-        snprintf(argp, sizeof(arg), "%s %s", cmd->argv[0], cmd->argv[1]);
+        len = snprintf(argp, sizeof(arg), "%s %s", (char *) cmd->argv[0],
+          (char *) cmd->argv[1]);
       }
 
       m++;
@@ -1154,27 +1372,27 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
     case LOGFMT_META_LOCAL_PORT:
       argp = arg;
-      snprintf(argp, sizeof(arg), "%d", cmd->server->ServerPort);
+      len = snprintf(argp, sizeof(arg), "%d", cmd->server->ServerPort);
       m++;
       break;
 
     case LOGFMT_META_LOCAL_IP:
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_ipstr(pr_netaddr_get_sess_local_addr()),
-        sizeof(arg));
+      len = sstrncpy(argp,
+        pr_netaddr_get_ipstr(pr_netaddr_get_sess_local_addr()), sizeof(arg));
       m++;
       break;
 
     case LOGFMT_META_LOCAL_FQDN:
       argp = arg;
-      sstrncpy(argp, pr_netaddr_get_dnsstr(pr_netaddr_get_sess_local_addr()),
-        sizeof(arg));
+      len = sstrncpy(argp,
+        pr_netaddr_get_dnsstr(pr_netaddr_get_sess_local_addr()), sizeof(arg));
       m++;
       break;
 
     case LOGFMT_META_PID:
       argp = arg;
-      snprintf(argp, sizeof(arg), "%u",(unsigned int)getpid());
+      len = snprintf(argp, sizeof(arg), "%u", (unsigned int) session.pid);
       m++;
       break;
 
@@ -1184,7 +1402,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
       gettimeofday(&now, NULL);
 
-      snprintf(argp, sizeof(arg), "%06lu", (unsigned long) now.tv_usec);
+      len = snprintf(argp, sizeof(arg), "%06lu", (unsigned long) now.tv_usec);
       m++;
       break;
     }
@@ -1200,7 +1418,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       /* Convert microsecs to millisecs. */
       millis = now.tv_usec / 1000;
 
-      snprintf(argp, sizeof(arg), "%03lu", millis);
+      len = snprintf(argp, sizeof(arg), "%03lu", millis);
       m++;
       break;
     }
@@ -1210,7 +1428,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
         char *time_fmt = "[%d/%b/%Y:%H:%M:%S ";
         struct tm t;
         int internal_fmt = 1;
-        int timz;
+        int timz = 0;
         char sign;
 
         argp = arg;
@@ -1218,20 +1436,24 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
         if (*m == LOGFMT_META_START &&
             *(m+1) == LOGFMT_META_ARG) {
-          time_fmt = get_next_meta(p, cmd, &m);
+          time_fmt = get_next_meta(p, cmd, &m, NULL);
           internal_fmt = 0;
         }
 
-        t = *_get_gmtoff(&timz);
+        t = *get_gmtoff(&timz);
         sign = (timz < 0 ? '-' : '+');
-        if (timz < 0)
+        if (timz < 0) {
           timz = -timz;
+        }
 
         if (time_fmt) {
-          strftime(argp, 80, time_fmt, &t);
+          len += strftime(argp, 80, time_fmt, &t);
           if (internal_fmt) {
-            if (strlen(argp) < sizeof(arg)) {
-              snprintf(argp + strlen(argp), sizeof(arg) - strlen(argp),
+            size_t arglen;
+
+            arglen = strlen(argp);
+            if (arglen < sizeof(arg)) {
+              len += snprintf(argp + arglen, sizeof(arg) - arglen,
                 "%c%.2d%.2d]", sign, timz/60, timz%60);
 
             } else {
@@ -1247,19 +1469,22 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       struct tm *tm;
       struct timeval now;
       unsigned long millis;
-      size_t len;
+      size_t fmt_len;
 
       argp = arg;
 
       gettimeofday(&now, NULL);
       tm = pr_localtime(NULL, (const time_t *) &(now.tv_sec));
+      if (tm != NULL) {
+        fmt_len = strftime(argp, sizeof(arg), "%Y-%m-%d %H:%M:%S", tm);
+        len += fmt_len;
 
-      len = strftime(argp, sizeof(arg), "%Y-%m-%d %H:%M:%S", tm);
+        /* Convert microsecs to millisecs. */
+        millis = now.tv_usec / 1000;
 
-      /* Convert microsecs to millisecs. */
-      millis = now.tv_usec / 1000;
+        len += snprintf(argp + fmt_len, sizeof(arg), ",%03lu", millis);
+      }
 
-      snprintf(argp + len, sizeof(arg), ",%03lu", millis);
       m++;
       break;
     }
@@ -1272,29 +1497,49 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
          */
         if (session.xfer.start_time.tv_sec != 0 ||
             session.xfer.start_time.tv_usec != 0) {
-          struct timeval end_time;
+          uint64_t start_ms = 0, end_ms = 0;
+          float transfer_secs = 0.0;
 
-          gettimeofday(&end_time, NULL);
-          end_time.tv_sec -= session.xfer.start_time.tv_sec;
+          pr_timeval2millis(&(session.xfer.start_time), &start_ms);
+          pr_gettimeofday_millis(&end_ms);
 
-          if (end_time.tv_usec >= session.xfer.start_time.tv_usec) {
-            end_time.tv_usec -= session.xfer.start_time.tv_usec;
+          transfer_secs = (end_ms - start_ms) / 1000.0;
+          len = snprintf(argp, sizeof(arg), "%0.3f", transfer_secs);
 
-          } else {
-            end_time.tv_usec = 1000000L - (session.xfer.start_time.tv_usec -
-              end_time.tv_usec);
-            end_time.tv_sec--;
-          }
+        } else {
+          len = sstrncpy(argp, "-", sizeof(arg));
+        }
+
+      } else {
+        len = sstrncpy(argp, "-", sizeof(arg));
+      }
+
+      m++;
+      break;
 
-          snprintf(argp, sizeof(arg), "%ld.%03ld", (long) end_time.tv_sec,
-            (long) (end_time.tv_usec / 1000));
+    case LOGFMT_META_XFER_MS:
+      argp = arg;
+      if (session.xfer.p) {
+        /* Make sure that session.xfer.start_time actually has values (which
+         * is not always the case).
+         */
+        if (session.xfer.start_time.tv_sec != 0 ||
+            session.xfer.start_time.tv_usec != 0) {
+          uint64_t start_ms = 0, end_ms = 0;
+          off_t transfer_ms;
+
+          pr_timeval2millis(&(session.xfer.start_time), &start_ms);
+          pr_gettimeofday_millis(&end_ms);
+
+          transfer_ms = end_ms - start_ms;
+          len = snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) transfer_ms);
 
         } else {
-          sstrncpy(argp, "-", sizeof(arg));
+          len = sstrncpy(argp, "-", sizeof(arg));
         }
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1305,10 +1550,11 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
       if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
           session.hide_password) {
-        sstrncpy(argp, "PASS (hidden)", sizeof(arg));
+        len = sstrncpy(argp, "PASS (hidden)", sizeof(arg));
 
       } else {
-        sstrncpy(argp, pr_cmd_get_displayable_str(cmd, NULL), sizeof(arg));
+        len = sstrncpy(argp, pr_cmd_get_displayable_str(cmd, NULL),
+          sizeof(arg));
       }
 
       m++;
@@ -1318,10 +1564,10 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
       if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
           session.hide_password) {
-        sstrncpy(argp, "(hidden)", sizeof(arg));
+        len = sstrncpy(argp, "(hidden)", sizeof(arg));
 
       } else {
-        sstrncpy(argp, pr_fs_decode_path(p, cmd->arg), sizeof(arg));
+        len = sstrncpy(argp, pr_fs_decode_path(p, cmd->arg), sizeof(arg));
       }
 
       m++;
@@ -1329,7 +1575,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
     case LOGFMT_META_LOCAL_NAME:
       argp = arg;
-      sstrncpy(argp, cmd->server->ServerName, sizeof(arg));
+      len = sstrncpy(argp, cmd->server->ServerName, sizeof(arg));
       m++;
       break;
 
@@ -1337,26 +1583,26 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
 
       if (session.user != NULL) {
-        sstrncpy(argp, session.user, sizeof(arg));
+        len = sstrncpy(argp, session.user, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
       break;
 
     case LOGFMT_META_ORIGINAL_USER: {
-      char *login_user;
+      const char *login_user;
 
       argp = arg;
 
       login_user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-      if (login_user) {
-        sstrncpy(argp, login_user, sizeof(arg));
+      if (login_user != NULL) {
+        len = sstrncpy(argp, login_user, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1367,17 +1613,17 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       argp = arg;
 
       if (session.group != NULL) {
-        sstrncpy(argp, session.group, sizeof(arg));
+        len = sstrncpy(argp, session.group, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
       break;
 
     case LOGFMT_META_RESPONSE_CODE: {
-      char *resp_code = NULL;
+      const char *resp_code = NULL;
       int res;
 
       argp = arg;
@@ -1385,14 +1631,37 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       res = pr_response_get_last(cmd->tmp_pool, &resp_code, NULL);
       if (res == 0 &&
           resp_code != NULL) {
-        sstrncpy(argp, resp_code, sizeof(arg));
+        len = sstrncpy(argp, resp_code, sizeof(arg));
 
       /* Hack to add return code for proper logging of QUIT command. */
       } else if (pr_cmd_cmp(cmd, PR_CMD_QUIT_ID) == 0) {
-        sstrncpy(argp, R_221, sizeof(arg));
+        len = sstrncpy(argp, R_221, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_RESPONSE_MS: {
+      const uint64_t *start_ms = NULL;
+
+      argp = arg;
+
+      start_ms = pr_table_get(cmd->notes, "start_ms", NULL);
+      if (start_ms != NULL) {
+        uint64_t end_ms = 0;
+        off_t response_ms;
+
+        pr_gettimeofday_millis(&end_ms);
+
+        response_ms = end_ms - *start_ms;
+        len = snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) response_ms);
+
+      } else {
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1400,7 +1669,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
     }
 
     case LOGFMT_META_RESPONSE_STR: {
-      char *resp_msg = NULL;
+      const char *resp_msg = NULL;
       int res;
 
       argp = arg;
@@ -1408,10 +1677,10 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
       res = pr_response_get_last(cmd->tmp_pool, NULL, &resp_msg);
       if (res == 0 &&
           resp_msg != NULL) {
-        sstrncpy(argp, resp_msg, sizeof(arg));
+        len = sstrncpy(argp, resp_msg, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1420,19 +1689,21 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
     case LOGFMT_META_PROTOCOL:
       argp = arg;
-      sstrncpy(argp, pr_session_get_protocol(0), sizeof(arg));
+      len = sstrncpy(argp, pr_session_get_protocol(0), sizeof(arg));
       m++;
       break;
 
     case LOGFMT_META_UID:
       argp = arg;
-      snprintf(argp, sizeof(arg), "%lu", (unsigned long) session.login_uid);
+      len = snprintf(argp, sizeof(arg), "%s",
+        pr_uid2str(NULL, session.login_uid));
       m++;
       break;
 
     case LOGFMT_META_GID:
       argp = arg;
-      snprintf(argp, sizeof(arg), "%lu", (unsigned long) session.login_gid);
+      len = snprintf(argp, sizeof(arg), "%s",
+        pr_gid2str(NULL, session.login_gid));
       m++;
       break;
 
@@ -1457,11 +1728,11 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
             strncmp(proto, "ftps", 5) == 0) {
 
           if (XFER_ABORTED) {
-            sstrncpy(argp, "-", sizeof(arg));
+            len = sstrncpy(argp, "-", sizeof(arg));
 
           } else {
             int res;
-            char *resp_code = NULL, *resp_msg = NULL;
+            const char *resp_code = NULL, *resp_msg = NULL;
 
             /* Get the last response code/message.  We use heuristics here to
              * determine when to use "failed" versus "success".
@@ -1476,18 +1747,18 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
                 /* Parse out/prettify the resp_msg here */
                 ptr = strchr(resp_msg, '.');
                 if (ptr != NULL) {
-                  sstrncpy(argp, ptr + 2, sizeof(arg));
+                  len = sstrncpy(argp, ptr + 2, sizeof(arg));
 
                 } else {
-                  sstrncpy(argp, resp_msg, sizeof(arg));
+                  len = sstrncpy(argp, resp_msg, sizeof(arg));
                 }
 
               } else {
-                sstrncpy(argp, "-", sizeof(arg));
+                len = sstrncpy(argp, "-", sizeof(arg));
               }
 
             } else {
-              sstrncpy(argp, "-", sizeof(arg));
+              len = sstrncpy(argp, "-", sizeof(arg));
             }
           }
 
@@ -1495,11 +1766,11 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
           /* Currently, for failed SFTP/SCP transfers, we can't properly
            * populate the failure reason.  Maybe in the future.
            */
-          sstrncpy(argp, "-", sizeof(arg));
+          len = sstrncpy(argp, "-", sizeof(arg));
         }
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1529,7 +1800,7 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
           if (!(XFER_ABORTED)) {
             int res;
-            char *resp_code = NULL, *resp_msg = NULL;
+            const char *resp_code = NULL, *resp_msg = NULL;
 
             /* Get the last response code/message.  We use heuristics here to
              * determine when to use "failed" versus "success".
@@ -1540,13 +1811,13 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
               if (*resp_code == '2') {
 
                 if (pr_cmd_cmp(cmd, PR_CMD_ABOR_ID) != 0) {
-                  sstrncpy(argp, "success", sizeof(arg));
+                  len = sstrncpy(argp, "success", sizeof(arg));
 
                 } else {
                   /* We're handling the ABOR command, so obviously the value
                    * should be 'cancelled'.
                    */
-                  sstrncpy(argp, "cancelled", sizeof(arg));
+                  len = sstrncpy(argp, "cancelled", sizeof(arg));
                 }
 
               } else if (*resp_code == '1') {
@@ -1556,37 +1827,78 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
                  * complete with a 2xx/4xx response code) when we are called
                  * here, which in turn means a timeout kicked in.
                  */
-                sstrncpy(argp, "timeout", sizeof(arg));
+                len = sstrncpy(argp, "timeout", sizeof(arg));
 
               } else {
-                sstrncpy(argp, "failed", sizeof(arg));
+                len = sstrncpy(argp, "failed", sizeof(arg));
               }
 
             } else {
-              sstrncpy(argp, "success", sizeof(arg));
+              len = sstrncpy(argp, "success", sizeof(arg));
             }
 
           } else {
-            sstrncpy(argp, "cancelled", sizeof(arg));
+            len = sstrncpy(argp, "cancelled", sizeof(arg));
           }
 
         } else {
           /* mod_sftp stashes a note for us in the command notes if the
            * transfer failed.
            */
-          char *status;
+          const char *status;
 
           status = pr_table_get(cmd->notes, "mod_sftp.file-status", NULL);
           if (status == NULL) {
-            sstrncpy(argp, "success", sizeof(arg));
+            len = sstrncpy(argp, "success", sizeof(arg));
+
+          } else {
+            len = sstrncpy(argp, status, sizeof(arg));
+          }
+        }
+
+      } else {
+        len = sstrncpy(argp, "-", sizeof(arg));
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_XFER_TYPE: {
+      argp = arg;
+
+      /* If the current command is one that incurs a data transfer, then we
+       * need to do more work.  If not, it's an easy substitution.
+       */
+      if (session.curr_cmd_id == PR_CMD_APPE_ID ||
+          session.curr_cmd_id == PR_CMD_LIST_ID ||
+          session.curr_cmd_id == PR_CMD_MLSD_ID ||
+          session.curr_cmd_id == PR_CMD_NLST_ID ||
+          session.curr_cmd_id == PR_CMD_RETR_ID ||
+          session.curr_cmd_id == PR_CMD_STOR_ID ||
+          session.curr_cmd_id == PR_CMD_STOU_ID) {
+        const char *proto;
+
+        proto = pr_session_get_protocol(0);
+
+        if (strncmp(proto, "sftp", 5) == 0 ||
+            strncmp(proto, "scp", 4) == 0) {
+
+          /* Always binary. */
+          len = sstrncpy(argp, "binary", sizeof(arg));
+
+        } else {
+          if ((session.sf_flags & SF_ASCII) ||
+              (session.sf_flags & SF_ASCII_OVERRIDE)) {
+            len = sstrncpy(argp, "ASCII", sizeof(arg));
 
           } else {
-            sstrncpy(argp, "failed", sizeof(arg));
+            len = sstrncpy(argp, "binary", sizeof(arg));
           }
         }
 
       } else {
-        sstrncpy(argp, "-", sizeof(arg));
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1595,21 +1907,65 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
     case LOGFMT_META_VERSION:
       argp = arg;
-      sstrncpy(argp, PROFTPD_VERSION_TEXT, sizeof(arg));
+      len = sstrncpy(argp, PROFTPD_VERSION_TEXT, sizeof(arg));
       m++;
       break;
 
     case LOGFMT_META_FILE_MODIFIED: {
-      char *modified;
+      const char *modified;
 
       argp = arg;
 
       modified = pr_table_get(cmd->notes, "mod_xfer.file-modified", NULL);
-      if (modified) {
-        sstrncpy(argp, modified, sizeof(arg));
+      if (modified != NULL) {
+        len = sstrncpy(argp, modified, sizeof(arg));
 
       } else {
-        sstrncpy(argp, "false", sizeof(arg));
+        len = sstrncpy(argp, "false", sizeof(arg));
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_FILE_OFFSET: {
+      const off_t *offset;
+
+      argp = arg;
+
+      offset = pr_table_get(cmd->notes, "mod_xfer.file-offset", NULL);
+      if (offset != NULL) {
+        char offset_str[1024];
+
+        memset(offset_str, '\0', sizeof(offset_str));
+        snprintf(offset_str, sizeof(offset_str)-1, "%" PR_LU,
+          (pr_off_t) *offset);
+        len = sstrncpy(argp, offset_str, sizeof(arg));
+
+      } else {
+        len = sstrncpy(argp, "-", sizeof(arg));
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_FILE_SIZE: {
+      const off_t *file_size;
+
+      argp = arg;
+
+      file_size = pr_table_get(cmd->notes, "mod_xfer.file-size", NULL);
+      if (file_size != NULL) {
+        char size_str[1024];
+
+        memset(size_str, '\0', sizeof(size_str));
+        snprintf(size_str, sizeof(size_str)-1, "%" PR_LU,
+          (pr_off_t) *file_size);
+        len = sstrncpy(argp, size_str, sizeof(arg));
+
+      } else {
+        len = sstrncpy(argp, "-", sizeof(arg));
       }
 
       m++;
@@ -1618,24 +1974,37 @@ static char *get_next_meta(pool *p, cmd_rec *cmd, unsigned char **f) {
 
     case LOGFMT_META_RAW_BYTES_IN:
       argp = arg;
-      snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) session.total_raw_in);
+      len = snprintf(argp, sizeof(arg), "%" PR_LU,
+        (pr_off_t) session.total_raw_in);
       m++;
       break;
 
     case LOGFMT_META_RAW_BYTES_OUT:
       argp = arg;
-      snprintf(argp, sizeof(arg), "%" PR_LU, (pr_off_t) session.total_raw_out);
+      len = snprintf(argp, sizeof(arg), "%" PR_LU,
+        (pr_off_t) session.total_raw_out);
       m++;
       break;
 
     case LOGFMT_META_VHOST_IP:
       argp = arg;
-      sstrncpy(argp, cmd->server->ServerAddress, sizeof(arg));
+      len = sstrncpy(argp, cmd->server->ServerAddress, sizeof(arg));
       m++;
       break;
   }
  
   *f = m;
+  if (mlen != NULL) {
+    /* Guard the caller against errors here (e.g. from sstrncpy() returning
+     * -1 due to bad inputs.
+     */
+    if (len < 0) {
+      len = 0;
+    }
+
+    *mlen = len;
+  }
+
   if (argp) {
     return pstrdup(p, argp);
   }
@@ -1662,17 +2031,17 @@ static void do_log(cmd_rec *cmd, logfile_t *lf) {
     pr_signals_handle();
 
     if (*f == LOGFMT_META_START) {
-      s = get_next_meta(cmd->tmp_pool, cmd, &f);
-      if (s) {
-        size_t tmp;
+      size_t len = 0;
 
-        tmp = strlen(s);
-        if (tmp > size)
-          tmp = size;
+      s = get_next_meta(cmd->tmp_pool, cmd, &f, &len);
+      if (s != NULL) {
+        if (len > size) {
+          len = size;
+        }
 
-        memcpy(bp, s, tmp);
-        size -= tmp;
-        bp += tmp;
+        memcpy(bp, s, len);
+        size -= len;
+        bp += len;
       }
 
     } else {
@@ -1706,25 +2075,44 @@ MODRET log_any(cmd_rec *cmd) {
 
   /* If not in anon mode, only handle logs for main servers */
   for (lf = logs; lf; lf = lf->next) {
-    if (lf->lf_fd != -1 &&
+    int log_cmd = FALSE;
 
-        /* If the logging class of this command is one of the classes
-         * configured for this ExtendedLog...
-         */ 
-        ((cmd->cmd_class & lf->lf_classes) ||
+    pr_signals_handle();
 
-         /* ...or if the logging class of this command is unknown (defaults to
-          * zero), and this ExtendedLog is configured to log ALL commands, then
-          * log it.
-          */
-         (cmd->cmd_class == 0 && lf->lf_classes == CL_ALL))) {
+    /* Skip any unopened files (obviously); make sure that special fd
+     * for syslog is NOT skipped, though.
+     */
+    if (lf->lf_fd < 0 &&
+        lf->lf_fd != EXTENDED_LOG_SYSLOG) {
+      continue;
+    }
 
-      if (!session.anon_config &&
-          lf->lf_conf &&
-          lf->lf_conf->config_type == CONF_ANON) {
-        continue;
-      }
+    /* If this is not an <Anonymous> section, and this IS an <Anonymous>
+     * ExtendedLog, skip it.
+     */
+    if (session.anon_config == NULL &&
+        lf->lf_conf != NULL &&
+        lf->lf_conf->config_type == CONF_ANON) {
+      continue;
+    }
 
+    if (cmd->cmd_class & lf->lf_incl_classes) {
+      log_cmd = TRUE;
+    }
+
+    if (cmd->cmd_class & lf->lf_excl_classes) {
+      log_cmd = FALSE;
+    }
+
+    /* If the logging class of this command is unknown (defaults to zero),
+     * AND this ExtendedLog is configured to log ALL commands, log it.
+     */
+    if (cmd->cmd_class == 0 &&
+        lf->lf_incl_classes == CL_ALL) {
+      log_cmd = TRUE;
+    }
+
+    if (log_cmd) {
       do_log(cmd, lf);
     }
   }
@@ -1812,6 +2200,37 @@ static void log_restart_ev(const void *event_data, void *user_data) {
   return;
 }
 
+static void log_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+  logfile_t *lf = NULL;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&log_module, "core.exit", log_exit_ev);
+  pr_event_unregister(&log_module, "core.session-reinit", log_sess_reinit_ev);
+  pr_event_unregister(&log_module, "core.timeout-stalled", log_xfer_stalled_ev);
+
+  /* XXX If ServerLog configured, close/reopen syslog? */
+
+  /* XXX Close all ExtendedLog files, to prevent duplicate fds. */
+  for (lf = logs; lf; lf = lf->next) {
+    if (lf->lf_fd > -1) {
+      /* No need to close the special EXTENDED_LOG_SYSLOG (i.e. fake) fd. */
+      if (lf->lf_fd != EXTENDED_LOG_SYSLOG) {
+        (void) close(lf->lf_fd);
+      }
+
+      lf->lf_fd = -1;
+    }
+  }
+
+  res = log_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&log_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 static void log_xfer_stalled_ev(const void *event_data, void *user_data) {
   if (session.curr_cmd_rec != NULL) {
     /* Automatically dispatch the current command, at the LOG_CMD_ERR phase,
@@ -1839,14 +2258,14 @@ static int log_init(void) {
 
 static void find_extendedlogs(void) {
   config_rec *c;
-  char *logfname, *logfmt_s;
-  int logclasses = CL_ALL;
+  char *logfname, *logfmt_s = NULL;
+  int incl_classes = CL_ALL, excl_classes = CL_NONE;
   logformat_t *logfmt;
   logfile_t *extlog = NULL;
   unsigned long config_flags = (PR_CONFIG_FIND_FL_SKIP_DIR|PR_CONFIG_FIND_FL_SKIP_LIMIT|PR_CONFIG_FIND_FL_SKIP_DYNDIR);
 
-  /* We _do_ actually want the recursion here.  The reason is that we want
-   * to find _all_ ExtendedLog directives in the configuration, including
+  /* We DO actually want the recursion here.  The reason is that we want
+   * to find ALL_ ExtendedLog directives in the configuration, including
    * those in <Anonymous> sections.  We have the ability to use root privs
    * now, to make sure these files can be opened, but after the user has
    * authenticated (and we know for sure whether they're anonymous or not),
@@ -1867,10 +2286,18 @@ static void find_extendedlogs(void) {
     logfmt_s = NULL;
 
     if (c->argc > 1) {
-      logclasses = *((int *) c->argv[1]);
+      if (c->argv[1] != NULL) {
+        incl_classes = *((int *) c->argv[1]);
+      }
 
-      if (c->argc > 2) {
-        logfmt_s = c->argv[2];
+      if (c->argv[2] != NULL) {
+        excl_classes = *((int *) c->argv[2]);
+      }
+
+      if (c->argc > 3) {
+        if (c->argv[3] != NULL) {
+          logfmt_s = c->argv[3];
+        }
       }
     }
 
@@ -1879,7 +2306,7 @@ static void find_extendedlogs(void) {
      * directive might be trying to override a higher-level config; see
      * Bug#1908.
      */
-    if (logclasses == CL_NONE &&
+    if (incl_classes == CL_NONE &&
         (c->parent != NULL && c->parent->config_type != CONF_ANON)) {
       goto loop_extendedlogs;
     }
@@ -1921,7 +2348,8 @@ static void find_extendedlogs(void) {
     extlog->lf_filename = pstrdup(session.pool, logfname);
     extlog->lf_fd = -1;
     extlog->lf_syslog_level = -1;
-    extlog->lf_classes = logclasses;
+    extlog->lf_incl_classes = incl_classes;
+    extlog->lf_excl_classes = excl_classes;
     extlog->lf_format = logfmt;
     extlog->lf_conf = c->parent;
     if (log_set == NULL) {
@@ -1950,45 +2378,9 @@ MODRET log_pre_dele(cmd_rec *cmd) {
     /* Briefly cache the size of the file being deleted, so that it can be
      * logged properly using %b.
      */
-    pr_fs_clear_cache();
-    if (pr_fsio_stat(path, &st) == 0)
+    pr_fs_clear_cache2(path);
+    if (pr_fsio_stat(path, &st) == 0) {
       log_dele_filesz = st.st_size;
-  }
-
-  return PR_DECLINED(cmd);
-}
-
-MODRET log_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-    logfile_t *lf = NULL;
-
-    pr_event_unregister(&log_module, "core.exit", log_exit_ev);
-    pr_event_unregister(&log_module, "core.timeout-stalled",
-      log_xfer_stalled_ev);
-
-    /* XXX If ServerLog configured, close/reopen syslog? */
-
-    /* XXX Close all ExtendedLog files, to prevent duplicate fds. */
-    for (lf = logs; lf; lf = lf->next) {
-      if (lf->lf_fd > -1) {
-        /* No need to close the special EXTENDED_LOG_SYSLOG (i.e. fake) fd. */
-        if (lf->lf_fd != EXTENDED_LOG_SYSLOG) {
-          (void) close(lf->lf_fd);
-        }
-
-        lf->lf_fd = -1;
-      }
-    }
-
-    res = log_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&log_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
     }
   }
 
@@ -2009,7 +2401,7 @@ MODRET log_post_pass(cmd_rec *cmd) {
           lf->lf_conf->config_type == CONF_ANON) {
         pr_log_debug(DEBUG7, "mod_log: closing ExtendedLog '%s' (fd %d)",
           lf->lf_filename, lf->lf_fd);
-        close(lf->lf_fd);
+        (void) close(lf->lf_fd);
         lf->lf_fd = -1;
       }
     }
@@ -2025,7 +2417,7 @@ MODRET log_post_pass(cmd_rec *cmd) {
           lf->lf_conf != session.anon_config) {
         pr_log_debug(DEBUG7, "mod_log: closing ExtendedLog '%s' (fd %d)",
           lf->lf_filename, lf->lf_fd);
-        close(lf->lf_fd);
+        (void) close(lf->lf_fd);
         lf->lf_fd = -1;
       }
     }
@@ -2048,7 +2440,7 @@ MODRET log_post_pass(cmd_rec *cmd) {
               strcmp(lfi->lf_filename, lf->lf_filename) == 0) {
             pr_log_debug(DEBUG7, "mod_log: closing ExtendedLog '%s' (fd %d)",
               lf->lf_filename, lfi->lf_fd);
-            close(lfi->lf_fd);
+            (void) close(lfi->lf_fd);
             lfi->lf_fd = -1;
           }
         }
@@ -2056,10 +2448,10 @@ MODRET log_post_pass(cmd_rec *cmd) {
         /* Go ahead and close the log if it's CL_NONE */
         if (lf->lf_fd != -1 &&
             lf->lf_fd != EXTENDED_LOG_SYSLOG &&
-            lf->lf_classes == CL_NONE) {
+            lf->lf_incl_classes == CL_NONE) {
           pr_log_debug(DEBUG7, "mod_log: closing ExtendedLog '%s' (fd %d)",
             lf->lf_filename, lf->lf_fd);
-          close(lf->lf_fd);
+          (void) close(lf->lf_fd);
           lf->lf_fd = -1;
         }
       }
@@ -2074,6 +2466,9 @@ static int log_sess_init(void) {
   char *serverlog_name = NULL;
   logfile_t *lf = NULL;
 
+  pr_event_register(&log_module, "core.session-reinit", log_sess_reinit_ev,
+    NULL);
+
   /* Open the ServerLog, if present. */
   serverlog_name = get_param_ptr(main_server->conf, "ServerLog", FALSE);
   if (serverlog_name != NULL) {
@@ -2226,7 +2621,6 @@ static cmdtable log_cmdtab[] = {
   { PRE_CMD,		C_DELE,	G_NONE,	log_pre_dele,	FALSE, FALSE },
   { LOG_CMD,		C_ANY,	G_NONE,	log_any,	FALSE, FALSE },
   { LOG_CMD_ERR,	C_ANY,	G_NONE,	log_any,	FALSE, FALSE },
-  { POST_CMD,		C_HOST,	G_NONE,	log_post_host,	FALSE, FALSE },
   { POST_CMD,		C_PASS,	G_NONE,	log_post_pass,	FALSE, FALSE },
   { 0, NULL }
 };
diff --git a/modules/mod_ls.c b/modules/mod_ls.c
index 4959c7c..58765a7 100644
--- a/modules/mod_ls.c
+++ b/modules/mod_ls.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project
  *
  * 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,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Directory listing module for ProFTPD.
- * $Id: mod_ls.c,v 1.207 2014-01-20 19:36:27 castaglia Exp $
- */
+/* Directory listing module for ProFTPD. */
 
 #include "conf.h"
 
@@ -43,8 +41,8 @@
 static void addfile(cmd_rec *, const char *, const char *, time_t, off_t);
 static int outputfiles(cmd_rec *);
 
-static int listfile(cmd_rec *, pool *, const char *);
-static int listdir(cmd_rec *, pool *, const char *);
+static int listfile(cmd_rec *, pool *, const char *, const char *);
+static int listdir(cmd_rec *, pool *, const char *, const char *);
 
 static int sendline(int flags, char *fmt, ...)
 #ifdef __GNUC__
@@ -57,7 +55,9 @@ static int sendline(int flags, char *fmt, ...)
 #define LS_FL_NO_ERROR_IF_ABSENT	0x0001
 #define LS_FL_LIST_ONLY			0x0002
 #define LS_FL_NLST_ONLY			0x0004
-static unsigned long list_flags = 0;
+#define LS_FL_NO_ADJUSTED_SYMLINKS	0x0008
+#define LS_FL_SORTED_NLST		0x0010
+static unsigned long list_flags = 0UL;
 
 /* Maximum size of the "dsize" directory block we'll allocate for all of the
  * entries in a directory (Bug#4247).
@@ -68,7 +68,7 @@ static unsigned char list_strict_opts = FALSE;
 static char *list_options = NULL;
 static unsigned char list_show_symlinks = TRUE, list_times_gmt = TRUE;
 static unsigned char show_symlinks_hold;
-static char *fakeuser = NULL, *fakegroup = NULL;
+static const char *fakeuser = NULL, *fakegroup = NULL;
 static mode_t fakemode;
 static unsigned char have_fake_mode = FALSE;
 static int ls_errno = 0;
@@ -104,6 +104,7 @@ static int
     opt_r = 0,
     opt_S = 0,
     opt_t = 0,
+    opt_U = 0,
     opt_u = 0,
     opt_STAT = 0;
 
@@ -219,21 +220,21 @@ static void pop_cwd(char *_cwd, unsigned char *symhold) {
 }
 
 static int ls_perms_full(pool *p, cmd_rec *cmd, const char *path, int *hidden) {
-  int res, canon = 0;
+  int res, use_canon = FALSE;
   char *fullpath;
   mode_t *fake_mode = NULL;
 
   fullpath = dir_realpath(p, path);
-
-  if (!fullpath) {
+  if (fullpath == NULL) {
     fullpath = dir_canonical_path(p, path);
-    canon = 1;
+    use_canon = TRUE;
   }
 
-  if (!fullpath)
+  if (fullpath == NULL) {
     fullpath = pstrdup(p, path);
-
-  if (canon) {
+  }
+ 
+  if (use_canon) {
     res = dir_check_canon(p, cmd, cmd->group, fullpath, hidden);
 
   } else {
@@ -266,11 +267,13 @@ static int ls_perms(pool *p, cmd_rec *cmd, const char *path, int *hidden) {
   mode_t *fake_mode = NULL;
 
   /* No need to process dotdirs. */
-  if (is_dotdir(path))
+  if (is_dotdir(path)) {
     return 1;
+  }
 
-  if (*path == '~')
+  if (*path == '~') {
     return ls_perms_full(p, cmd, path, hidden);
+  }
 
   if (*path != '/') {
     pr_fs_clean_path(pdircat(p, pr_fs_getcwd(), path, NULL), fullpath,
@@ -312,10 +315,12 @@ static size_t listbufsz = 0;
 
 static int sendline(int flags, char *fmt, ...) {
   va_list msg;
-  char buf[PR_TUNABLE_BUFFER_SIZE+1] = {'\0'};
+  char buf[PR_TUNABLE_BUFFER_SIZE+1];
   int res = 0;
   size_t buflen, listbuflen;
 
+  memset(buf, '\0', sizeof(buf));
+
   if (listbuf == NULL) {
     listbufsz = pr_config_get_server_xfer_bufsz(PR_NETIO_IO_WR);
     listbuf = listbuf_ptr = pcalloc(session.pool, listbufsz);
@@ -360,9 +365,9 @@ static int sendline(int flags, char *fmt, ...) {
 
       memset(listbuf, '\0', listbufsz);
       listbuf_ptr = listbuf;
-      listbuflen = 0;
       pr_trace_msg("data", 8, "flushed %lu bytes of list buffer",
         (unsigned long) listbuflen);
+      listbuflen = 0;
     }
 
     return res;
@@ -403,9 +408,9 @@ static int sendline(int flags, char *fmt, ...) {
 
     memset(listbuf, '\0', listbufsz);
     listbuf_ptr = listbuf;
-    listbuflen = 0;
     pr_trace_msg("data", 8, "flushed %lu bytes of list buffer",
       (unsigned long) listbuflen);
+    listbuflen = 0;
   }
 
   sstrcat(listbuf_ptr, buf, listbufsz - listbuflen);
@@ -449,7 +454,8 @@ static char months[12][4] =
   { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
     "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
 
-static int listfile(cmd_rec *cmd, pool *p, const char *name) {
+static int listfile(cmd_rec *cmd, pool *p, const char *resp_code,
+    const char *name) {
   register unsigned int i;
   int rval = 0, len;
   time_t sort_time;
@@ -495,9 +501,11 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
   }
   list_nfiles.curr++;
 
-  if (!p)
+  if (p == NULL) {
     p = cmd->tmp_pool;
+  }
 
+  pr_fs_clear_cache2(name);
   if (pr_fsio_lstat(name, &st) == 0) {
     char *display_name = NULL;
 
@@ -524,7 +532,7 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
        */
       for (i = 0, j = 0; i < display_namelen && j < printable_namelen; i++) {
         if (!PR_ISPRINT(display_name[i])) {
-          register unsigned int k;
+          register int k;
           int replace_len = 0;
           char replace[32];
 
@@ -545,11 +553,11 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
     }
 #endif /* PR_USE_NLS */
 
-    if (S_ISLNK(st.st_mode) && (opt_L || !list_show_symlinks)) {
+    if (S_ISLNK(st.st_mode) &&
+        (opt_L || !list_show_symlinks)) {
       /* Attempt to fully dereference symlink */
       struct stat l_st;
 
-      pr_fs_clear_cache();
       if (pr_fsio_stat(name, &l_st) != -1) {
         memcpy(&st, &l_st, sizeof(struct stat));
 
@@ -564,12 +572,21 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
           return 0;
         }
 
-        len = pr_fsio_readlink(name, m, sizeof(m) - 1);
-        if (len < 0)
+        if (list_flags & LS_FL_NO_ADJUSTED_SYMLINKS) {
+          len = pr_fsio_readlink(name, m, sizeof(m) - 1);
+
+        } else {
+          len = dir_readlink(p, name, m, sizeof(m) - 1,
+            PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+        }
+
+        if (len < 0) {
           return 0;
+        }
 
-        if (len >= sizeof(m))
+        if ((size_t) len >= sizeof(m)) {
           return 0;
+        }
 
         m[len] = '\0';
 
@@ -598,12 +615,21 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
         return 0;
       }
 
-      len = pr_fsio_readlink(name, l, sizeof(l) - 1);
-      if (len < 0)
+      if (list_flags & LS_FL_NO_ADJUSTED_SYMLINKS) {
+        len = pr_fsio_readlink(name, l, sizeof(l) - 1);
+
+      } else {
+        len = dir_readlink(p, name, l, sizeof(l) - 1,
+          PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      }
+
+      if (len < 0) {
         return 0;
+      }
 
-      if (len >= sizeof(l))
+      if ((size_t) len >= sizeof(l)) {
         return 0;
+      }
 
       l[len] = '\0';
 
@@ -629,8 +655,9 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
       return 0;
     }
 
-    if (hidden)
+    if (hidden) {
       return 0;
+    }
 
     switch (ls_sort_by) {
       case LS_SORT_BY_MTIME:
@@ -651,10 +678,10 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
     }
 
     if (list_times_gmt) {
-      t = pr_gmtime(p, (time_t *) &sort_time);
+      t = pr_gmtime(p, (const time_t *) &sort_time);
 
     } else {
-      t = pr_localtime(p, (time_t *) &sort_time);
+      t = pr_localtime(p, (const time_t *) &sort_time);
     }
 
     if (opt_F) {
@@ -776,16 +803,18 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
           char *buf = nameline + strlen(nameline);
 
           suffix[0] = '\0';
-          if (opt_F && pr_fsio_stat(name, &st) == 0) {
-            if (S_ISLNK(st.st_mode)) {
-              suffix[0] = '@';
-
-            } else if (S_ISDIR(st.st_mode)) {
-              suffix[0] = '/';
-
-            } else if (st.st_mode & 0111) {
-              suffix[0] = '*';
-            }
+          if (opt_F) {
+            if (pr_fsio_stat(name, &st) == 0) {
+              if (S_ISLNK(st.st_mode)) {
+                suffix[0] = '@';
+
+              } else if (S_ISDIR(st.st_mode)) {
+                suffix[0] = '/';
+
+              } else if (st.st_mode & 0111) {
+                suffix[0] = '*';
+              }
+           }
           }
 
           if (!opt_L && list_show_symlinks) {
@@ -802,7 +831,7 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
         }
 
         if (opt_STAT) {
-          pr_response_add(R_211, "%s%s", nameline, suffix);
+          pr_response_add(resp_code, "%s%s", nameline, suffix);
 
         } else {
           addfile(cmd, nameline, suffix, sort_time, st.st_size);
@@ -813,8 +842,8 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
       if (S_ISREG(st.st_mode) ||
           S_ISDIR(st.st_mode) ||
           S_ISLNK(st.st_mode)) {
-           addfile(cmd, pr_fs_encode_path(cmd->tmp_pool, name), suffix,
-             sort_time, st.st_size);
+        addfile(cmd, pr_fs_encode_path(cmd->tmp_pool, name), suffix, sort_time,
+          st.st_size);
       }
     }
   }
@@ -822,8 +851,8 @@ static int listfile(cmd_rec *cmd, pool *p, const char *name) {
   return rval;
 }
 
-static int colwidth = 0;
-static int filenames = 0;
+static size_t colwidth = 0;
+static unsigned int filenames = 0;
 
 struct filename {
   struct filename *down;
@@ -849,19 +878,32 @@ static void addfile(cmd_rec *cmd, const char *name, const char *suffix,
   struct filename *p;
   size_t l;
 
-  if (!name || !suffix)
+  if (name == NULL ||
+      suffix == NULL) {
     return;
+  }
+
+  /* If we are not sorting (-U is in effect), then we have no need to buffer
+   * up the line, and can send it immediately.  This can provide quite a bit
+   * of memory/CPU savings, especially for LIST commands on wide/deep
+   * directories (Bug#4060).
+   */
+  if (opt_U == 1) {
+    (void) sendline(0, "%s%s\r\n", name, suffix);
+    return;
+  }
 
-  if (!fpool) {
+  if (fpool == NULL) {
     fpool = make_sub_pool(cmd->tmp_pool);
-    pr_pool_tag(fpool, "mod_ls: addfile() fpool");
+    pr_pool_tag(fpool, "mod_ls addfile pool");
   }
 
   if (opt_S || opt_t) {
     struct sort_filename *s;
 
-    if (!sort_arr)
+    if (sort_arr == NULL) {
       sort_arr = make_array(fpool, 50, sizeof(struct sort_filename));
+    }
 
     s = (struct sort_filename *) push_array(sort_arr);
     s->sort_time = sort_time;
@@ -873,18 +915,20 @@ static void addfile(cmd_rec *cmd, const char *name, const char *suffix,
   }
 
   l = strlen(name) + strlen(suffix);
-  if (l > colwidth)
+  if (l > colwidth) {
     colwidth = l;
+  }
 
   p = (struct filename *) pcalloc(fpool, sizeof(struct filename));
   p->line = pcalloc(fpool, l + 2);
   snprintf(p->line, l + 1, "%s%s", name, suffix);
 
-  if (tail)
+  if (tail) {
     tail->down = p;
 
-  else
+  } else {
     head = p;
+  }
 
   tail = p;
   filenames++;
@@ -977,21 +1021,37 @@ static int outputfiles(cmd_rec *cmd) {
   int n, res = 0;
   struct filename *p = NULL, *q = NULL;
 
-  if (opt_S || opt_t)
+  if (opt_S || opt_t) {
     sortfiles(cmd);
+  }
 
-  if (!head)		/* nothing to display */
-    return 0;
+  if (head == NULL) {
+    /* Nothing to display. */
+    if (sendline(LS_SENDLINE_FL_FLUSH, " ") < 0) {
+      res = -1;
+    }
+
+    destroy_pool(fpool);
+    fpool = NULL;
+    sort_arr = NULL;
+    head = tail = NULL;
+    colwidth = 0;
+    filenames = 0;
+
+    return res;
+  }
 
   tail->down = NULL;
   tail = NULL;
   colwidth = (colwidth | 7) + 1;
-  if (opt_l || !opt_C)
+  if (opt_l || !opt_C) {
     colwidth = 75;
+  }
 
   /* avoid division by 0 if colwidth > 75 */
-  if (colwidth > 75)
+  if (colwidth > 75) {
     colwidth = 75;
+  }
 
   if (opt_C) {
     p = head;
@@ -1002,8 +1062,9 @@ static int outputfiles(cmd_rec *cmd) {
       pr_signals_handle();
 
       p = p->down;
-      if (p)
+      if (p) {
         p->top = 0;
+      }
       n--;
     }
 
@@ -1030,8 +1091,9 @@ static int outputfiles(cmd_rec *cmd) {
       p = p->down;
     }
 
-    if (p && p->down)
+    if (p && p->down) {
       p->down = NULL;
+    }
   }
 
   p = head;
@@ -1084,8 +1146,9 @@ static int outputfiles(cmd_rec *cmd) {
 }
 
 static void discard_output(void) {
-  if (fpool)
+  if (fpool) {
     destroy_pool(fpool);
+  }
   fpool = NULL;
 
   head = tail = NULL;
@@ -1107,10 +1170,13 @@ static char **sreaddir(const char *dirname, const int sort) {
   struct stat st;
   int i, dir_fd;
   char **p;
-  size_t ssize, dsize;
+  long ssize;
+  size_t dsize;
 
-  if (pr_fsio_stat(dirname, &st) < 0)
+  pr_fs_clear_cache2(dirname);
+  if (pr_fsio_stat(dirname, &st) < 0) {
     return NULL;
+  }
 
   if (!S_ISDIR(st.st_mode)) {
     errno = ENOTDIR;
@@ -1118,8 +1184,9 @@ static char **sreaddir(const char *dirname, const int sort) {
   }
 
   d = pr_fsio_opendir(dirname);
-  if (d == NULL)
+  if (d == NULL) {
     return NULL;
+  }
 
   /* It doesn't matter if the following guesses are wrong, but it slows
    * the system a bit and wastes some memory if they are wrong, so
@@ -1180,7 +1247,7 @@ static char **sreaddir(const char *dirname, const int sort) {
   while ((de = pr_fsio_readdir(d)) != NULL) {
     pr_signals_handle();
 
-    if (i >= dsize - 1) {
+    if ((size_t) i >= dsize - 1) {
       char **newp;
 
       /* The test above goes off one item early in case this is the last item
@@ -1226,7 +1293,8 @@ static char **sreaddir(const char *dirname, const int sort) {
 }
 
 /* This listdir() requires a chdir() first. */
-static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
+static int listdir(cmd_rec *cmd, pool *workp, const char *resp_code,
+    const char *name) {
   char **dir;
   int dest_workp = 0;
   register unsigned int i = 0;
@@ -1256,10 +1324,11 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
   }
   list_ndirs.curr++;
 
-  if (XFER_ABORTED)
+  if (XFER_ABORTED) {
     return -1;
+  }
 
-  if (!workp) {
+  if (workp == NULL) {
     workp = make_sub_pool(cmd->tmp_pool);
     pr_pool_tag(workp, "mod_ls: listdir(): workp (from cmd->tmp_pool)");
     dest_workp++;
@@ -1270,8 +1339,7 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
     dest_workp++;
   }
 
-  PR_DEVEL_CLOCK(dir = sreaddir(".", TRUE));
-
+  PR_DEVEL_CLOCK(dir = sreaddir(".", opt_U ? FALSE : TRUE));
   if (dir) {
     char **s;
     char **r;
@@ -1285,11 +1353,11 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
           d = 0;
 
         } else {
-          d = listfile(cmd, workp, *s);
+          d = listfile(cmd, workp, resp_code, *s);
         }
 
       } else {
-        d = listfile(cmd, workp, *s);
+        d = listfile(cmd, workp, resp_code, *s);
       }
 
       if (opt_R && d == 0) {
@@ -1302,22 +1370,25 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
         **s = '.';
         *(*s + 1) = '\0';
 
-      } else if (d == 2)
+      } else if (d == 2) {
         break;
+      }
 
       s++;
     }
 
     if (outputfiles(cmd) < 0) {
-      if (dest_workp)
+      if (dest_workp) {
         destroy_pool(workp);
+      }
 
       /* Explicitly free the memory allocated for containing the list of
        * filenames.
        */
       i = 0;
-      while (dir[i] != NULL)
+      while (dir[i] != NULL) {
         free(dir[i++]);
+      }
       free(dir);
 
       return -1;
@@ -1369,29 +1440,41 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
         char *subdir;
         int res = 0;
 
-        if (strcmp(name, ".") == 0)
+        if (strcmp(name, ".") == 0) {
           subdir = *r;
-        else
+
+        } else {
           subdir = pdircat(workp, name, *r, NULL);
+        }
 
         if (opt_STAT) {
-          pr_response_add(R_211, "%s", "");
-          pr_response_add(R_211, "%s:",
+          pr_response_add(resp_code, "%s", "");
+          pr_response_add(resp_code, "%s:",
             pr_fs_encode_path(cmd->tmp_pool, subdir));
 
         } else if (sendline(0, "\r\n%s:\r\n",
-                     pr_fs_encode_path(cmd->tmp_pool, subdir)) < 0 ||
+                   pr_fs_encode_path(cmd->tmp_pool, subdir)) < 0 ||
             sendline(LS_SENDLINE_FL_FLUSH, " ") < 0) {
           pop_cwd(cwd_buf, &symhold);
 
-          if (dest_workp)
+          if (dest_workp) {
             destroy_pool(workp);
+          }
+
+          /* Explicitly free the memory allocated for containing the list of
+           * filenames.
+           */
+          i = 0;
+          while (dir[i] != NULL) {
+            free(dir[i++]);
+          }
+          free(dir);
 
           return -1;
         }
 
         list_ndepth.curr++;
-        res = listdir(cmd, workp, subdir);
+        res = listdir(cmd, workp, resp_code, subdir);
         list_ndepth.curr--;
         pop_cwd(cwd_buf, &symhold);
 
@@ -1399,15 +1482,17 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
           break;
 
         } else if (res < 0) {
-          if (dest_workp)
+          if (dest_workp) {
             destroy_pool(workp);
+          }
 
           /* Explicitly free the memory allocated for containing the list of
            * filenames.
            */
           i = 0;
-          while (dir[i] != NULL)
+          while (dir[i] != NULL) {
             free(dir[i++]);
+          }
           free(dir);
 
           return -1;
@@ -1415,18 +1500,24 @@ static int listdir(cmd_rec *cmd, pool *workp, const char *name) {
       }
       r++;
     }
+
+  } else {
+    pr_trace_msg("fsio", 9,
+      "sreaddir() error on '.': %s", strerror(errno));
   }
 
-  if (dest_workp)
+  if (dest_workp) {
     destroy_pool(workp);
+  }
 
   /* Explicitly free the memory allocated for containing the list of
    * filenames.
    */
   if (dir) {
     i = 0;
-    while (dir[i] != NULL)
+    while (dir[i] != NULL) {
       free(dir[i++]);
+    }
     free(dir);
   }
 
@@ -1439,11 +1530,13 @@ static void ls_terminate(void) {
 
     if (!XFER_ABORTED) {
       /* An error has occured, other than client ABOR */
-      if (ls_errno)
+      if (ls_errno) {
         pr_data_abort(ls_errno,FALSE);
-      else
+
+      } else {
         pr_data_abort((session.d && session.d->outstrm ?
-                   PR_NETIO_ERRNO(session.d->outstrm) : errno),FALSE);
+                      PR_NETIO_ERRNO(session.d->outstrm) : errno), FALSE);
+      }
     }
     ls_errno = 0;
 
@@ -1478,7 +1571,7 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
     while ((*opt)++ && PR_ISALNUM(**opt)) {
       switch (**opt) {
         case '1':
-          if (strcmp(session.curr_cmd, C_STAT) != 0) {
+          if (session.curr_cmd_id != PR_CMD_STAT_ID) {
             opt_1 = 1;
             opt_l = opt_C = 0;
           }
@@ -1497,7 +1590,7 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
           break;
 
         case 'C':
-          if (strcmp(session.curr_cmd, C_NLST) != 0) {
+          if (session.curr_cmd_id != PR_CMD_NLST_ID) {
             opt_l = 0;
             opt_C = 1;
           }
@@ -1513,13 +1606,13 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
           break;
 
         case 'F':
-          if (strcmp(session.curr_cmd, C_NLST) != 0) {
+          if (session.curr_cmd_id != PR_CMD_NLST_ID) {
             opt_F = 1;
           }
           break;
 
         case 'h':
-          if (strcmp(session.curr_cmd, C_NLST) != 0) {
+          if (session.curr_cmd_id != PR_CMD_NLST_ID) {
             opt_h = 1;
           }
           break;
@@ -1529,7 +1622,7 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
           break;
 
         case 'l':
-          if (strcmp(session.curr_cmd, C_NLST) != 0) {
+          if (session.curr_cmd_id != PR_CMD_NLST_ID) {
             opt_l = 1;
             opt_C = 0;
             opt_1 = 0;
@@ -1537,7 +1630,7 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
           break;
 
         case 'n':
-          if (strcmp(session.curr_cmd, C_NLST) != 0) {
+          if (session.curr_cmd_id != PR_CMD_NLST_ID) {
             opt_n = 1;
           }
           break;
@@ -1556,15 +1649,20 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
 
         case 't':
           opt_t = 1;
-          if (glob_flags)
+          if (glob_flags) {
             *glob_flags |= GLOB_NOSORT;
+          }
+          break;
+
+        case 'U':
+          opt_U = 1;
+          opt_c = opt_S = opt_t = 0;
           break;
 
         case 'u':
           opt_u = 1;
           ls_sort_by = LS_SORT_BY_ATIME;
           break;
-
       }
     }
 
@@ -1591,8 +1689,9 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
     }
   }
 
-  if (!handle_plus_opts)
+  if (!handle_plus_opts) {
     return;
+  }
 
   /* Check for non-standard options */
   while (*opt && **opt == '+') {
@@ -1669,6 +1768,10 @@ static void parse_list_opts(char **opt, int *glob_flags, int handle_plus_opts) {
             *glob_flags &= GLOB_NOSORT;
           break;
 
+        case 'U':
+          opt_U = 0;
+          break;
+
         case 'u':
           opt_u = 0;
 
@@ -1720,8 +1823,9 @@ static int have_options(cmd_rec *cmd, const char *arg) {
    * options.
    */
 
-  if (cmd->argc > 2)
+  if (cmd->argc > 2) {
     return TRUE;
+  }
 
   /* Now we need to determine if the given string (arg) should be handled
    * as options (as when the target path is implied, e.g. "LIST -al") or
@@ -1729,11 +1833,12 @@ static int have_options(cmd_rec *cmd, const char *arg) {
    * then it's a path.
    */
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(arg);
   res = pr_fsio_stat(arg, &st);
 
-  if (res == 0)
+  if (res == 0) {
     return FALSE;
+  }
 
   return TRUE;
 }
@@ -1741,7 +1846,8 @@ static int have_options(cmd_rec *cmd, const char *arg) {
 /* The main work for LIST and STAT (not NLST).  Returns -1 on error, 0 if
  * successful.
  */
-static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
+static int dolist(cmd_rec *cmd, const char *opt, const char *resp_code,
+    int clear_flags) {
   int skiparg = 0;
   int glob_flags = 0;
 
@@ -1749,7 +1855,7 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
 
   ls_curtime = time(NULL);
 
-  if (clearflags) {
+  if (clear_flags) {
     opt_1 = opt_A = opt_a = opt_B = opt_C = opt_d = opt_F = opt_h = opt_n =
       opt_r = opt_R = opt_S = opt_t = opt_STAT = opt_L = 0;
   }
@@ -1833,7 +1939,7 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
       const char *p;
 
       for (i = 0, p = arg + 1;
-          (i < sizeof(pbuffer) - 1) && p && *p && *p != '/';
+          ((size_t) i < sizeof(pbuffer) - 1) && p && *p && *p != '/';
           pbuffer[i++] = *p++);
 
       pbuffer[i] = '\0';
@@ -1857,6 +1963,7 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
         pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
           strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return -1;
       }
@@ -1870,7 +1977,7 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
     if (pr_str_is_fnmatch(target) == FALSE) {
       struct stat st;
 
-      pr_fs_clear_cache();
+      pr_fs_clear_cache2(target);
       if (pr_fsio_stat(target, &st) < 0) {
         int xerrno = errno;
 
@@ -1942,16 +2049,20 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
 
         pr_signals_handle();
 
+        pr_fs_clear_cache2(*path);
         if (pr_fsio_lstat(*path, &st) == 0) {
           mode_t target_mode, lmode;
           target_mode = st.st_mode;
 
           if (S_ISLNK(st.st_mode) &&
-              (lmode = file_mode((char *) *path)) != 0) {
-            if (opt_L || !list_show_symlinks)
+              (lmode = symlink_mode2(cmd->tmp_pool, (char *) *path)) != 0) {
+            if (opt_L || !list_show_symlinks) {
               st.st_mode = lmode;
+            }
 
-            target_mode = lmode;
+            if (lmode != 0) {
+              target_mode = lmode;
+            }
           }
 
           /* If the -d option is used or the file is not a directory, OR
@@ -1963,10 +2074,11 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
               !(S_ISDIR(target_mode)) ||
               (!opt_R && S_ISDIR(target_mode) && strcmp(*path, target) != 0)) {
 
-            if (listfile(cmd, cmd->tmp_pool, *path) < 0) {
+            if (listfile(cmd, cmd->tmp_pool, resp_code, *path) < 0) {
               ls_terminate();
-              if (use_globbing && globbed)
+              if (use_globbing && globbed) {
                 pr_fs_globfree(&g);
+              }
               return -1;
             }
 
@@ -2005,8 +2117,8 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
 
           if (!justone) {
             if (opt_STAT) {
-              pr_response_add(R_211, "%s", "");
-              pr_response_add(R_211, "%s:",
+              pr_response_add(resp_code, "%s", "");
+              pr_response_add(resp_code, "%s:",
                 pr_fs_encode_path(cmd->tmp_pool, *path));
 
             } else {
@@ -2023,7 +2135,7 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
             int res = 0;
 
             list_ndepth.curr++;
-            res = listdir(cmd, cmd->tmp_pool, *path);
+            res = listdir(cmd, cmd->tmp_pool, resp_code, *path);
             list_ndepth.curr--;
 
             pop_cwd(cwd_buf, &symhold);
@@ -2033,8 +2145,9 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
 
             } else if (res < 0) {
               ls_terminate();
-              if (use_globbing && globbed)
+              if (use_globbing && globbed) {
                 pr_fs_globfree(&g);
+              }
               return -1;
             }
           }
@@ -2042,8 +2155,9 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
 
         if (XFER_ABORTED) {
           discard_output();
-          if (use_globbing && globbed)
+          if (use_globbing && globbed) {
             pr_fs_globfree(&g);
+          }
           return -1;
         }
 
@@ -2073,8 +2187,9 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
       }
     }
 
-    if (!skiparg && use_globbing && globbed)
+    if (!skiparg && use_globbing && globbed) {
       pr_fs_globfree(&g);
+    }
 
     if (XFER_ABORTED) {
       discard_output();
@@ -2091,6 +2206,7 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
         pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0], 
           strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return -1;
       }
@@ -2101,14 +2217,14 @@ static int dolist(cmd_rec *cmd, const char *opt, int clearflags) {
     if (ls_perms_full(cmd->tmp_pool, cmd, ".", NULL)) {
 
       if (opt_d) {
-        if (listfile(cmd, NULL, ".") < 0) {
+        if (listfile(cmd, NULL, resp_code, ".") < 0) {
           ls_terminate();
           return -1;
         }
 
       } else {
         list_ndepth.curr++;
-        if (listdir(cmd, NULL, ".") < 0) {
+        if (listdir(cmd, NULL, resp_code, ".") < 0) {
           ls_terminate();
           return -1;
         }
@@ -2168,7 +2284,7 @@ static int nlstfile(cmd_rec *cmd, const char *file) {
      */
     for (i = 0, j = 0; i < display_namelen && j < printable_namelen; i++) {
       if (!PR_ISPRINT(display_name[i])) {
-        register unsigned int k;
+        register int k;
         int replace_len = 0;
         char replace[32];
 
@@ -2226,7 +2342,7 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
   char cwd_buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
   pool *workp;
   unsigned char symhold;
-  int curdir = FALSE, i, j, count = 0, hidden = 0;
+  int curdir = FALSE, i, j, count = 0, hidden = 0, use_sorting = FALSE;
   mode_t mode;
   config_rec *c = NULL;
   unsigned char ignore_hidden = FALSE;
@@ -2279,10 +2395,18 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
     }
   }
 
-  PR_DEVEL_CLOCK(list = sreaddir(".", FALSE));
+  if (list_flags & LS_FL_SORTED_NLST) {
+    use_sorting = TRUE;
+  }
+
+  PR_DEVEL_CLOCK(list = sreaddir(".", use_sorting));
   if (list == NULL) {
-    if (!curdir)
+    pr_trace_msg("fsio", 9,
+      "sreaddir() error on '.': %s", strerror(errno));
+
+    if (!curdir) {
       pop_cwd(cwd_buf, &symhold);
+    }
 
     destroy_pool(workp);
     return 0;
@@ -2295,8 +2419,10 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
   if (c != NULL) {
     unsigned char *ignore = get_param_ptr(c->subset, "IgnoreHidden", FALSE);
 
-    if (ignore && *ignore == TRUE)
+    if (ignore &&
+        *ignore == TRUE) {
       ignore_hidden = TRUE;
+    }
   }
 
   j = 0;
@@ -2315,10 +2441,18 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
       }
     }
 
-    i = pr_fsio_readlink(p, file, sizeof(file) - 1);
+    if (list_flags & LS_FL_NO_ADJUSTED_SYMLINKS) {
+      i = pr_fsio_readlink(p, file, sizeof(file) - 1);
+
+    } else {
+      i = dir_readlink(cmd->tmp_pool, p, file, sizeof(file) - 1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+    }
+
     if (i > 0) {
-      if (i >= sizeof(file))
+      if ((size_t) i >= sizeof(file)) {
         continue;
+      }
 
       file[i] = '\0';
       f = file;
@@ -2328,12 +2462,14 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
     }
 
     if (ls_perms(workp, cmd, dir_best_path(cmd->tmp_pool, f), &hidden)) {
-      if (hidden)
+      if (hidden) {
         continue;
+      }
 
-      mode = file_mode(f);
-      if (mode == 0)
+      mode = file_mode2(cmd->tmp_pool, f);
+      if (mode == 0) {
         continue;
+      }
 
       if (!curdir) {
         char *str = NULL;
@@ -2395,16 +2531,18 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
 
   sendline(LS_SENDLINE_FL_FLUSH, " ");
 
-  if (!curdir)
+  if (!curdir) {
     pop_cwd(cwd_buf, &symhold);
+  }
   destroy_pool(workp);
 
   /* Explicitly free the memory allocated for containing the list of
    * filenames.
    */
   i = 0;
-  while (list[i] != NULL)
+  while (list[i] != NULL) {
     free(list[i++]);
+  }
   free(list);
 
   return count;
@@ -2413,6 +2551,7 @@ static int nlstdir(cmd_rec *cmd, const char *dir) {
 /* The LIST command.  */
 MODRET genericlist(cmd_rec *cmd) {
   int res = 0;
+  char *decoded_path = NULL;
   unsigned char *tmp = NULL;
   mode_t *fake_mode = NULL;
   config_rec *c = NULL;
@@ -2427,22 +2566,24 @@ MODRET genericlist(cmd_rec *cmd) {
 
   c = find_config(CURRENT_CONF, CONF_PARAM, "ListOptions", FALSE);
   while (c != NULL) {
+    unsigned long flags;
+
     pr_signals_handle();
 
-    list_flags = *((unsigned long *) c->argv[5]);
+    flags = *((unsigned long *) c->argv[5]);
 
     /* Make sure that this ListOptions can be applied to the LIST command.
      * If not, keep looking for other applicable ListOptions.
      */
-    if (list_flags & LS_FL_NLST_ONLY) {
-      pr_log_debug(DEBUG10, "%s: skipping NLSTOnly ListOptions", cmd->argv[0]);
+    if (flags & LS_FL_NLST_ONLY) {
+      pr_log_debug(DEBUG10, "%s: skipping NLSTOnly ListOptions",
+        (char *) cmd->argv[0]);
       c = find_config_next(c, c->next, CONF_PARAM, "ListOptions", FALSE);
       continue;
     }
 
     list_options = c->argv[0];
     list_strict_opts = *((unsigned char *) c->argv[1]);
-
     list_ndepth.max = *((unsigned int *) c->argv[2]);
 
     /* We add one to the configured maxdepth in order to allow it to
@@ -2451,11 +2592,13 @@ MODRET genericlist(cmd_rec *cmd) {
      * layer deeper.  For the checks to work, the maxdepth of 2 needs to
      * handled internally as a maxdepth of 3.
      */
-    if (list_ndepth.max)
+    if (list_ndepth.max) {
       list_ndepth.max += 1;
+    }
 
     list_nfiles.max = *((unsigned int *) c->argv[3]);
     list_ndirs.max = *((unsigned int *) c->argv[4]);
+    list_flags = *((unsigned long *) c->argv[5]);
 
     break;
   }
@@ -2490,7 +2633,22 @@ MODRET genericlist(cmd_rec *cmd) {
     list_times_gmt = *tmp;
   }
 
-  res = dolist(cmd, pr_fs_decode_path(cmd->tmp_pool, cmd->arg), TRUE);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  res = dolist(cmd, decoded_path, R_211, TRUE);
 
   if (XFER_ABORTED) {
     pr_data_abort(0, 0);
@@ -2516,9 +2674,10 @@ MODRET ls_err_nlst(cmd_rec *cmd) {
 }
 
 MODRET ls_stat(cmd_rec *cmd) {
+  struct stat st;
   int res;
-  char *arg = cmd->arg;
-  unsigned char *tmp = NULL;
+  char *arg = cmd->arg, *decoded_path, *path, *resp_code = NULL;
+  unsigned char *ptr = NULL;
   mode_t *fake_mode = NULL;
   config_rec *c = NULL;
 
@@ -2526,7 +2685,13 @@ MODRET ls_stat(cmd_rec *cmd) {
     /* In this case, the client is requesting the current session status. */
 
     if (!dir_check(cmd->tmp_pool, cmd, cmd->group, session.cwd, NULL)) {
-      pr_response_add_err(R_500, "%s: %s", cmd->argv[0], strerror(EPERM));
+      int xerrno = EPERM;
+
+      pr_response_add_err(R_500, "%s: %s", (char *) cmd->argv[0],
+        strerror(xerrno));
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
       return PR_ERROR(cmd);
     }
 
@@ -2573,7 +2738,22 @@ MODRET ls_stat(cmd_rec *cmd) {
   list_nfiles.curr = list_ndirs.curr = list_ndepth.curr = 0;
   list_nfiles.logged = list_ndirs.logged = list_ndepth.logged = FALSE;
 
-  arg = pr_fs_decode_path(cmd->tmp_pool, arg);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  arg = decoded_path;
 
   /* Get to the actual argument. */
   if (*arg == '-') {
@@ -2586,9 +2766,9 @@ MODRET ls_stat(cmd_rec *cmd) {
     arg++;
   }
 
-  tmp = get_param_ptr(TOPLEVEL_CONF, "ShowSymlinks", FALSE);
-  if (tmp != NULL) {
-    list_show_symlinks = *tmp;
+  ptr = get_param_ptr(TOPLEVEL_CONF, "ShowSymlinks", FALSE);
+  if (ptr != NULL) {
+    list_show_symlinks = *ptr;
   }
 
   list_strict_opts = FALSE;
@@ -2596,21 +2776,25 @@ MODRET ls_stat(cmd_rec *cmd) {
 
   c = find_config(CURRENT_CONF, CONF_PARAM, "ListOptions", FALSE);
   while (c != NULL) {
+    unsigned long flags;
+
     pr_signals_handle();
 
-    list_flags = *((unsigned long *) c->argv[5]);
+    flags = *((unsigned long *) c->argv[5]);
 
     /* Make sure that this ListOptions can be applied to the STAT command.
      * If not, keep looking for other applicable ListOptions.
      */
-    if (list_flags & LS_FL_LIST_ONLY) {
-      pr_log_debug(DEBUG10, "%s: skipping LISTOnly ListOptions", cmd->argv[0]);
+    if (flags & LS_FL_LIST_ONLY) {
+      pr_log_debug(DEBUG10, "%s: skipping LISTOnly ListOptions",
+        (char *) cmd->argv[0]);
       c = find_config_next(c, c->next, CONF_PARAM, "ListOptions", FALSE);
       continue;
     }
 
-    if (list_flags & LS_FL_NLST_ONLY) {
-      pr_log_debug(DEBUG10, "%s: skipping NLSTOnly ListOptions", cmd->argv[0]);
+    if (flags & LS_FL_NLST_ONLY) {
+      pr_log_debug(DEBUG10, "%s: skipping NLSTOnly ListOptions",
+        (char *) cmd->argv[0]);
       c = find_config_next(c, c->next, CONF_PARAM, "ListOptions", FALSE);
       continue;
     }
@@ -2632,6 +2816,7 @@ MODRET ls_stat(cmd_rec *cmd) {
 
     list_nfiles.max = *((unsigned int *) c->argv[3]);
     list_ndirs.max = *((unsigned int *) c->argv[4]);
+    list_flags = *((unsigned long *) c->argv[5]);
 
     break;
   }
@@ -2661,19 +2846,45 @@ MODRET ls_stat(cmd_rec *cmd) {
     have_fake_mode = FALSE;
   }
 
-  tmp = get_param_ptr(TOPLEVEL_CONF, "TimesGMT", FALSE);
-  if (tmp != NULL) {
-    list_times_gmt = *tmp;
+  ptr = get_param_ptr(TOPLEVEL_CONF, "TimesGMT", FALSE);
+  if (ptr != NULL) {
+    list_times_gmt = *ptr;
   }
 
   opt_C = opt_d = opt_F = opt_R = 0;
   opt_a = opt_l = opt_STAT = 1;
 
-  pr_response_add(R_211, _("Status of %s:"), arg && *arg ?
-    pr_fs_encode_path(cmd->tmp_pool, arg) :
-    pr_fs_encode_path(cmd->tmp_pool, "."));
-  res = dolist(cmd, arg && *arg ? arg : ".", FALSE);
-  pr_response_add(R_211, _("End of status"));
+  path = (arg && *arg) ? arg : ".";
+
+  pr_fs_clear_cache2(path);
+  if (list_show_symlinks) {
+    res = pr_fsio_lstat(path, &st);
+
+  } else {
+    res = pr_fsio_stat(path, &st);
+  }
+
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_response_add_err(R_450, "%s: %s", path, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  if (S_ISDIR(st.st_mode)) {
+    resp_code = R_212;
+
+  } else {
+    resp_code = R_213;
+  }
+
+  pr_response_add(resp_code, _("Status of %s:"),
+    pr_fs_encode_path(cmd->tmp_pool, path));
+  res = dolist(cmd, path, resp_code, FALSE);
+  pr_response_add(resp_code, _("End of status"));
   return (res == -1 ? PR_ERROR(cmd) : PR_HANDLED(cmd));
 }
 
@@ -2686,13 +2897,11 @@ MODRET ls_list(cmd_rec *cmd) {
   return genericlist(cmd);
 }
 
-/* NLST is a very simplistic directory listing, unlike LIST (which
- * emulates ls), it only sends a list of all files/directories
- * matching the glob(s).
+/* NLST is a very simplistic directory listing, unlike LIST (which emulates
+ * ls(1)), it only sends a list of all files/directories matching the glob(s).
  */
-
 MODRET ls_nlst(cmd_rec *cmd) {
-  char *target, buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+  char *decoded_path, *target, buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
   size_t targetlen = 0;
   config_rec *c = NULL;
   int res = 0, hidden = 0;
@@ -2707,20 +2916,37 @@ MODRET ls_nlst(cmd_rec *cmd) {
     list_show_symlinks = *tmp;
   }
 
-  target = cmd->argc == 1 ? "." :
-    pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  target = cmd->argc == 1 ? "." : decoded_path;
 
   c = find_config(CURRENT_CONF, CONF_PARAM, "ListOptions", FALSE);
   while (c != NULL) {
+    unsigned long flags;
+
     pr_signals_handle();
 
-    list_flags = *((unsigned long *) c->argv[5]);
+    flags = *((unsigned long *) c->argv[5]);
     
     /* Make sure that this ListOptions can be applied to the NLST command.
      * If not, keep looking for other applicable ListOptions.
      */
-    if (list_flags & LS_FL_LIST_ONLY) {
-      pr_log_debug(DEBUG10, "%s: skipping LISTOnly ListOptions", cmd->argv[0]);
+    if (flags & LS_FL_LIST_ONLY) {
+      pr_log_debug(DEBUG10, "%s: skipping LISTOnly ListOptions",
+        (char *) cmd->argv[0]);
       c = find_config_next(c, c->next, CONF_PARAM, "ListOptions", FALSE);
       continue;
     }
@@ -2736,12 +2962,12 @@ MODRET ls_nlst(cmd_rec *cmd) {
      * layer deeper.  For the checks to work, the maxdepth of 2 needs to
      * handled internally as a maxdepth of 3.
      */
-    if (list_ndepth.max)
+    if (list_ndepth.max) {
       list_ndepth.max += 1;
+    }
 
     list_nfiles.max = *((unsigned int *) c->argv[3]);
     list_ndirs.max = *((unsigned int *) c->argv[4]);
-
     list_flags = *((unsigned long *) c->argv[5]);
 
     break;
@@ -2778,8 +3004,9 @@ MODRET ls_nlst(cmd_rec *cmd) {
 
       while (target && *target == '-') {
         /* Advance to the next whitespace */
-        while (*target != '\0' && !PR_ISSPACE(*target))
+        while (*target != '\0' && !PR_ISSPACE(*target)) {
           target++;
+        }
 
         ptr = target;
 
@@ -2793,7 +3020,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
           /* Options are found; skip past the leading whitespace. */
           target = ptr;
 
-        } else if (*(target + 1) == ' ') {
+        } else if (*target && *(target + 1) == ' ') {
           /* If the next character is a blank space, advance just one
            * character.
            */
@@ -2860,6 +3087,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
       if (res == GLOB_NOMATCH) {
         struct stat st;
 
+        pr_fs_clear_cache2(target);
         if (pr_fsio_stat(target, &st) == 0) {
           pr_log_debug(DEBUG10, "NLST: glob(3) returned GLOB_NOMATCH for '%s', "
             "handling as literal path", target);
@@ -2879,6 +3107,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
               pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0], 
                 strerror(xerrno));
 
+              pr_cmd_set_errno(cmd, xerrno);
               errno = xerrno;
               return PR_ERROR(cmd);
             }
@@ -2891,6 +3120,9 @@ MODRET ls_nlst(cmd_rec *cmd) {
           }
 
           pr_response_add_err(R_450, _("No files found"));
+
+          pr_cmd_set_errno(cmd, ENOENT);
+          errno = ENOENT;
           return PR_ERROR(cmd);
         }
 
@@ -2902,6 +3134,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
             pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
               strerror(xerrno));
 
+            pr_cmd_set_errno(cmd, xerrno);
             errno = xerrno;
             return PR_ERROR(cmd);
           }
@@ -2914,6 +3147,9 @@ MODRET ls_nlst(cmd_rec *cmd) {
         }
 
         pr_response_add_err(R_450, _("No files found"));
+
+        pr_cmd_set_errno(cmd, ENOENT);
+        errno = ENOENT;
         return PR_ERROR(cmd);
       }
     }
@@ -2924,6 +3160,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
       pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
         strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -2940,19 +3177,29 @@ MODRET ls_nlst(cmd_rec *cmd) {
       p = *path;
       path++;
 
-      if (*p == '.' && (!opt_A || is_dotdir(p)))
+      if (*p == '.' && (!opt_A || is_dotdir(p))) {
         continue;
+      }
 
+      pr_fs_clear_cache2(p);
       if (pr_fsio_stat(p, &st) == 0) {
-        /* If it's a directory, hand off to nlstdir */
+        /* If it's a directory... */
         if (S_ISDIR(st.st_mode)) {
-          res = nlstdir(cmd, p);
+          if (opt_R) {
+            /* ...and we are recursing, hand off to nlstdir()...*/
+            res = nlstdir(cmd, p);
+
+          } else {
+            /*...otherwise, just list the name. */
+            res = nlstfile(cmd, p);
+          }
 
         } else if (S_ISREG(st.st_mode) &&
             ls_perms(cmd->tmp_pool, cmd, p, &hidden)) {
           /* Don't display hidden files */
-          if (hidden)
+          if (hidden) {
             continue;
+          }
 
           res = nlstfile(cmd, p);
         }
@@ -3006,6 +3253,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
           pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
             strerror(xerrno));
 
+          pr_cmd_set_errno(cmd, xerrno);
           errno = xerrno;
           return PR_ERROR(cmd);
         }
@@ -3019,6 +3267,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
       pr_response_add_err(R_450, "%s: %s", *cmd->arg ? cmd->arg :
         pr_fs_encode_path(cmd->tmp_pool, session.vwd), strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -3026,21 +3275,22 @@ MODRET ls_nlst(cmd_rec *cmd) {
     /* Don't display hidden files */
     if (hidden) {
       c = find_ls_limit(target);
-
       if (c) {
-        unsigned char *ignore_hidden = get_param_ptr(c->subset,
-          "IgnoreHidden", FALSE);
+        unsigned char *ignore_hidden;
+        int xerrno;
 
+        ignore_hidden = get_param_ptr(c->subset, "IgnoreHidden", FALSE);
         if (ignore_hidden &&
             *ignore_hidden == TRUE) {
 
           if (list_flags & LS_FL_NO_ERROR_IF_ABSENT) {
             if (pr_data_open(NULL, "file list", PR_NETIO_IO_WR, 0) < 0) {
-              int xerrno = errno;
+              xerrno = errno;
 
               pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
                 strerror(xerrno));
 
+              pr_cmd_set_errno(cmd, xerrno);
               errno = xerrno;
               return PR_ERROR(cmd);
             }
@@ -3051,14 +3301,17 @@ MODRET ls_nlst(cmd_rec *cmd) {
             return PR_HANDLED(cmd);
           }
 
-          pr_response_add_err(R_450, "%s: %s",
-            pr_fs_encode_path(cmd->tmp_pool, target), strerror(ENOENT));
+          xerrno = ENOENT;
 
         } else {
-          pr_response_add_err(R_450, "%s: %s",
-            pr_fs_encode_path(cmd->tmp_pool, target), strerror(EACCES));
+          xerrno = EACCES;
         }
 
+        pr_response_add_err(R_450, "%s: %s",
+          pr_fs_encode_path(cmd->tmp_pool, target), strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
     }
@@ -3066,7 +3319,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
     /* Make sure the target is a file or directory, and that we have access
      * to it.
      */
-    pr_fs_clear_cache();
+    pr_fs_clear_cache2(target);
     if (pr_fsio_stat(target, &st) < 0) {
       int xerrno = errno;
 
@@ -3078,6 +3331,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
           pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
             strerror(xerrno));
 
+          pr_cmd_set_errno(cmd, xerrno);
           errno = xerrno;
           return PR_ERROR(cmd);
         }
@@ -3090,6 +3344,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
 
       pr_response_add_err(R_450, "%s: %s", cmd->arg, strerror(xerrno));
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -3101,6 +3356,7 @@ MODRET ls_nlst(cmd_rec *cmd) {
         pr_response_add_err(R_450, "%s: %s", (char *) cmd->argv[0],
           strerror(xerrno));
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
@@ -3110,6 +3366,10 @@ MODRET ls_nlst(cmd_rec *cmd) {
 
     } else if (S_ISDIR(st.st_mode)) {
       if (pr_data_open(NULL, "file list", PR_NETIO_IO_WR, 0) < 0) {
+        int xerrno = errno;
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
         return PR_ERROR(cmd);
       }
       session.sf_flags |= SF_ASCII_OVERRIDE;
@@ -3118,6 +3378,9 @@ MODRET ls_nlst(cmd_rec *cmd) {
 
     } else {
       pr_response_add_err(R_450, _("%s: Not a regular file"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
@@ -3167,7 +3430,7 @@ MODRET set_dirfakeusergroup(cmd_rec *cmd) {
 
   if (cmd->argc < 2 ||
       cmd->argc > 3) {
-    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "syntax: ", cmd->argv[0],
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "syntax: ", (char *) cmd->argv[0],
       " on|off [<id to display>]", NULL));
   }
 
@@ -3219,8 +3482,9 @@ MODRET set_listoptions(cmd_rec *cmd) {
   config_rec *c = NULL;
   unsigned long flags = 0;
 
-  if (cmd->argc-1 < 1)
+  if (cmd->argc-1 < 1) {
     CONF_ERROR(cmd, "wrong number of parameters");
+  }
 
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|
     CONF_DIR|CONF_DYNDIR);
@@ -3261,45 +3525,54 @@ MODRET set_listoptions(cmd_rec *cmd) {
       } else if (strcasecmp(cmd->argv[i], "maxdepth") == 0) {
         int maxdepth = atoi(cmd->argv[++i]);
 
-        if (maxdepth < 1)
+        if (maxdepth < 1) {
           CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
             ": maxdepth must be greater than 0: '", cmd->argv[i],
             "'", NULL));
+        }
 
         *((unsigned int *) c->argv[2]) = maxdepth;
 
       } else if (strcasecmp(cmd->argv[i], "maxfiles") == 0) {
         int maxfiles = atoi(cmd->argv[++i]);
 
-        if (maxfiles < 1)
+        if (maxfiles < 1) {
           CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-            ": maxfiles must be greater than 0: '", cmd->argv[i],
+            ": maxfiles must be greater than 0: '", (char *) cmd->argv[i],
             "'", NULL));
+        }
 
-          *((unsigned int *) c->argv[3]) = maxfiles;
+        *((unsigned int *) c->argv[3]) = maxfiles;
 
       } else if (strcasecmp(cmd->argv[i], "maxdirs") == 0) {
         int maxdirs = atoi(cmd->argv[++i]);
 
-        if (maxdirs < 1)
+        if (maxdirs < 1) {
           CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
-            ": maxdirs must be greater than 0: '", cmd->argv[i],
+            ": maxdirs must be greater than 0: '", (char *) cmd->argv[i],
             "'", NULL));
+        }
 
-          *((unsigned int *) c->argv[4]) = maxdirs;
+        *((unsigned int *) c->argv[4]) = maxdirs;
 
       } else if (strcasecmp(cmd->argv[i], "LISTOnly") == 0) {
-          flags |= LS_FL_LIST_ONLY;
+        flags |= LS_FL_LIST_ONLY;
 
       } else if (strcasecmp(cmd->argv[i], "NLSTOnly") == 0) {
-          flags |= LS_FL_NLST_ONLY;
+        flags |= LS_FL_NLST_ONLY;
 
       } else if (strcasecmp(cmd->argv[i], "NoErrorIfAbsent") == 0) {
-          flags |= LS_FL_NO_ERROR_IF_ABSENT;
+        flags |= LS_FL_NO_ERROR_IF_ABSENT;
+
+      } else if (strcasecmp(cmd->argv[i], "NoAdjustedSymlinks") == 0) {
+        flags |= LS_FL_NO_ADJUSTED_SYMLINKS;
+
+      } else if (strcasecmp(cmd->argv[i], "SortedNLST") == 0) {
+        flags |= LS_FL_SORTED_NLST;
 
       } else {
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown keyword: '",
-          cmd->argv[i], "'", NULL));
+          (char *) cmd->argv[i], "'", NULL));
       }
     }
   }
@@ -3333,8 +3606,10 @@ MODRET set_useglobbing(cmd_rec *cmd) {
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON);
 
-  if ((bool = get_boolean(cmd, 1)) == -1)
+  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 char));
diff --git a/modules/mod_memcache.c b/modules/mod_memcache.c
index d0888c4..80ab0db 100644
--- a/modules/mod_memcache.c
+++ b/modules/mod_memcache.c
@@ -1,7 +1,6 @@
 /*
  * ProFTPD: mod_memcache -- a module for managing memcache data
- *
- * Copyright (c) 2010-2013 The ProFTPD Project
+ * Copyright (c) 2010-2016 The ProFTPD Project
  *
  * 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
@@ -23,7 +22,6 @@
  * source distribution.
  *
  * $Libraries: -lmemcached -lmemcachedutil$
- * $Id: mod_memcache.c,v 1.21 2013-10-13 17:34:01 castaglia Exp $
  */
 
 #include "conf.h"
@@ -47,42 +45,6 @@ static array_header *memcache_server_lists = NULL;
 static void mcache_exit_ev(const void *, void *);
 static int mcache_sess_init(void);
 
-/* Command handlers
- */
-
-MODRET memcache_post_host(cmd_rec *cmd) {
-
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
-   */
-  if (session.prev_server != NULL) {
-    int res;
-    config_rec *c;
-
-    pr_event_unregister(&memcache_module, "core.exit", mcache_exit_ev);
-    (void) close(memcache_logfd);
-
-    c = find_config(session.prev_server->conf, CONF_PARAM, "MemcacheServers",
-      FALSE);
-    if (c != NULL) {
-      memcached_server_st *memcache_servers;
-
-      memcache_servers = c->argv[0];
-      memcache_set_servers(memcache_servers);
-    }
-
-    /* XXX Restore other memcache settings? */
-
-    res = mcache_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&memcache_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
-    }
-  }
-
-  return PR_DECLINED(cmd);
-}
-
 /* Configuration handlers
  */
 
@@ -348,6 +310,40 @@ static void mcache_restart_ev(const void *event_data, void *user_data) {
     sizeof(memcached_server_st **));
 }
 
+static void mcache_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+  config_rec *c;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&memcache_module, "core.exit", mcache_exit_ev);
+  pr_event_unregister(&memcache_module, "core.session-reinit",
+    mcache_sess_reinit_ev);
+
+  (void) close(memcache_logfd);
+  memcache_logfd = -1;
+
+  c = find_config(session.prev_server->conf, CONF_PARAM, "MemcacheServers",
+    FALSE);
+  if (c != NULL) {
+    memcached_server_st *memcache_servers;
+
+    memcache_servers = c->argv[0];
+    memcache_set_servers(memcache_servers);
+  }
+
+  /* XXX Restore other memcache settings? */
+  /* reset MemcacheOptions */
+  /* reset MemcacheReplicas */
+  /* reset MemcacheTimeout */
+
+  res = mcache_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&memcache_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
 /* Initialization functions
  */
 
@@ -366,7 +362,7 @@ static int mcache_init(void) {
 
   version = memcached_lib_version();
   if (strcmp(version, LIBMEMCACHED_VERSION_STRING) != 0) {
-    pr_log_pri(PR_LOG_WARNING, MOD_MEMCACHE_VERSION
+    pr_log_pri(PR_LOG_INFO, MOD_MEMCACHE_VERSION
       ": compiled using libmemcached-%s headers, but linked to "
       "libmemcached-%s library", LIBMEMCACHED_VERSION_STRING, version);
 
@@ -381,6 +377,9 @@ static int mcache_init(void) {
 static int mcache_sess_init(void) {
   config_rec *c;
 
+  pr_event_register(&memcache_module, "core.session-reinit",
+    mcache_sess_reinit_ev, NULL);
+
   c = find_config(main_server->conf, CONF_PARAM, "MemcacheEngine", FALSE);
   if (c) {
     int engine;
@@ -418,16 +417,19 @@ static int mcache_sess_init(void) {
           pr_log_pri(PR_LOG_NOTICE, MOD_MEMCACHE_VERSION
             ": notice: unable to open MemcacheLog '%s': %s", path,
             strerror(xerrno));
+          break;
 
         case PR_LOG_WRITABLE_DIR:
           pr_log_pri(PR_LOG_WARNING, MOD_MEMCACHE_VERSION
             ": notice: unable to use MemcacheLog '%s': parent directory is "
               "world-writable", path);
+          break;
 
         case PR_LOG_SYMLINK:
           pr_log_pri(PR_LOG_WARNING, MOD_MEMCACHE_VERSION
             ": notice: unable to use MemcacheLog '%s': cannot log to a symlink",
             path);
+          break;
       }
     }
   }
@@ -486,11 +488,6 @@ static int mcache_sess_init(void) {
 /* Module API tables
  */
 
-static cmdtable memcache_cmdtab[] = {
-  { POST_CMD,	C_HOST,	G_NONE,	memcache_post_host,	FALSE,	FALSE },
-  { 0, NULL }
-};
-
 static conftable memcache_conftab[] = {
   { "MemcacheConnectFailures",	set_memcacheconnectfailures,	NULL },
   { "MemcacheEngine",		set_memcacheengine,		NULL },
@@ -516,7 +513,7 @@ module memcache_module = {
   memcache_conftab,
 
   /* Module command handler table */
-  memcache_cmdtab,
+  NULL,
 
   /* Module authentication handler table */
   NULL,
diff --git a/modules/mod_redis.c b/modules/mod_redis.c
new file mode 100644
index 0000000..edaeb81
--- /dev/null
+++ b/modules/mod_redis.c
@@ -0,0 +1,1942 @@
+/*
+ * ProFTPD: mod_redis -- a module for managing Redis data
+ * Copyright (c) 2017 The ProFTPD Project
+ *
+ * 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.
+ *
+ * -----DO NOT EDIT BELOW THIS LINE-----
+ * $Libraries: -lhiredis$
+ */
+
+#include "conf.h"
+#include "privs.h"
+#include "logfmt.h"
+#include "json.h"
+
+#define MOD_REDIS_VERSION		"mod_redis/0.1"
+
+#if PROFTPD_VERSION_NUMBER < 0x0001030605
+# error "ProFTPD 1.3.6rc5 or later required"
+#endif
+
+#include <hiredis/hiredis.h>
+
+extern xaset_t *server_list;
+
+module redis_module;
+
+#define REDIS_DEFAULT_PORT		6379
+
+static int redis_engine = FALSE;
+static int redis_logfd = -1;
+static pool *redis_pool = NULL;
+
+static void redis_exit_ev(const void *, void *);
+static int redis_sess_init(void);
+
+static const char *trace_channel = "redis";
+
+/* Logging
+ */
+
+#define REDIS_LOG_EVENT_FL_CONNECT		1
+#define REDIS_LOG_EVENT_FL_DISCONNECT		2
+#define REDIS_LOG_EVENT_FL_COMMAND		3
+
+#define REDIS_EVENT_ALL				-1
+#define REDIS_EVENT_CONNECT_ID			-2
+#define REDIS_EVENT_DISCONNECT_ID		-3
+
+/* The LogFormat "meta" values are in the unsigned char range; for our
+ * specific "meta" values, then, choose something greater than 256.
+ */
+#define REDIS_META_CONNECT		427
+#define REDIS_META_DISCONNECT		428
+
+/* For tracking the size of deleted files. */
+static off_t redis_dele_filesz = 0;
+
+static pr_table_t *redis_field_idtab = NULL;
+
+/* Entries in the field table identify the field name, and the data type:
+ * Boolean, number, or string.
+ *
+ * Note: This idea of a table of names to JSON types is just what a core
+ * JSON API for ProFTPD would include.
+ */
+struct field_info {
+  unsigned int field_type;
+  const char *field_name;
+  size_t field_namelen;
+};
+
+/* Key comparison for the ID/name table. */
+static int field_idcmp(const void *k1, size_t ksz1, const void *k2,
+  size_t ksz2) {
+
+  /* Return zero to indicate a match, non-zero otherwise. */
+  return (*((unsigned int *) k1) == *((unsigned int *) k2) ? 0 : 1);
+}
+
+/* Key "hash" callback for ID/name table. */
+static unsigned int field_idhash(const void *k, size_t ksz) {
+  unsigned int c;
+  unsigned int res;
+
+  memcpy(&c, k, ksz);
+  res = (c << 8);
+
+  return res;
+}
+
+static int field_add(pool *p, unsigned int id, const char *name,
+    unsigned int field_type) {
+  unsigned int *k;
+  struct field_info *fi;
+  int res;
+
+  k = palloc(p, sizeof(unsigned int));
+  *k = id;
+
+  fi = palloc(p, sizeof(struct field_info));
+  fi->field_type = field_type;
+  fi->field_name = name;
+  fi->field_namelen = strlen(name) + 1;
+
+  res = pr_table_kadd(redis_field_idtab, (const void *) k, sizeof(unsigned int),
+    fi, sizeof(struct field_info *));
+  return res;
+}
+
+static int make_fieldtab(pool *p) {
+  int res, xerrno;
+
+  redis_field_idtab = pr_table_alloc(p, 0);
+
+  res = pr_table_ctl(redis_field_idtab, PR_TABLE_CTL_SET_KEY_CMP,
+    (void *) field_idcmp);
+  xerrno = errno;
+
+  /* Since this function is only called once, at module startup, the logging
+   * SHOULD use pr_log_pri().
+   */
+
+  if (res < 0) {
+    pr_log_pri(PR_LOG_INFO, MOD_REDIS_VERSION
+      ": error setting key comparison callback for field ID/names: %s",
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  res = pr_table_ctl(redis_field_idtab, PR_TABLE_CTL_SET_KEY_HASH,
+    (void *) field_idhash);
+  xerrno = errno;
+
+  if (res < 0) {
+    pr_log_pri(PR_LOG_INFO, MOD_REDIS_VERSION
+      ": error setting key hash callback for field ID/names: %s",
+      strerror(errno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  /* Now populate the table with the ID/name values.  The key is the
+   * LogFormat "meta" ID, and the value is the corresponding name string,
+   * for use e.g. as JSON object member names.
+   */
+
+  field_add(p, LOGFMT_META_BYTES_SENT, "bytes_sent", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_FILENAME, "file", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_ENV_VAR, "ENV:", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_REMOTE_HOST, "remote_dns", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_REMOTE_IP, "remote_ip", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_IDENT_USER, "identd_user", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_PID, "pid", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_TIME, "local_time", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_SECONDS, "transfer_secs", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_COMMAND, "raw_command", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_LOCAL_NAME, "server_name", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_LOCAL_PORT, "local_port", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_LOCAL_IP, "local_ip", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_LOCAL_FQDN, "server_dns", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_USER, "user", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_ORIGINAL_USER, "original_user", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_RESPONSE_CODE, "response_code", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_CLASS, "connection_class", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_ANON_PASS, "anon_password", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_METHOD, "command", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_XFER_PATH, "transfer_path", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_DIR_NAME, "dir_name", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_DIR_PATH, "dir_path", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_CMD_PARAMS, "command_params", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_RESPONSE_STR, "response_msg", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_PROTOCOL, "protocol", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_VERSION, "server_version", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_RENAME_FROM, "rename_from", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_FILE_MODIFIED, "file_modified", PR_JSON_TYPE_BOOL);
+  field_add(p, LOGFMT_META_UID, "uid", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_GID, "gid", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_RAW_BYTES_IN, "session_bytes_rcvd",
+    PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_RAW_BYTES_OUT, "session_bytes_sent",
+    PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_EOS_REASON, "session_end_reason",
+    PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_VHOST_IP, "server_ip", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_NOTE_VAR, "NOTE:", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_XFER_STATUS, "transfer_status", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_XFER_FAILURE, "transfer_failure",
+    PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_MICROSECS, "microsecs", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_MILLISECS, "millisecs", PR_JSON_TYPE_NUMBER);
+  field_add(p, LOGFMT_META_ISO8601, "timestamp", PR_JSON_TYPE_STRING);
+  field_add(p, LOGFMT_META_GROUP, "group", PR_JSON_TYPE_STRING);
+
+  field_add(p, REDIS_META_CONNECT, "connecting", PR_JSON_TYPE_BOOL);
+  field_add(p, REDIS_META_DISCONNECT, "disconnecting", PR_JSON_TYPE_BOOL);
+
+  return 0;
+}
+
+static void encode_json(pool *p, void *json, const char *field_name,
+    size_t field_namelen, unsigned int field_type, const void *field_value) {
+
+  switch (field_type) {
+    case PR_JSON_TYPE_STRING:
+      (void) pr_json_object_set_string(p, json, field_name,
+        (const char *) field_value);
+      break;
+
+    case PR_JSON_TYPE_NUMBER:
+      (void) pr_json_object_set_number(p, json, field_name,
+        *((double *) field_value));
+      break;
+
+    case PR_JSON_TYPE_BOOL:
+      (void) pr_json_object_set_bool(p, json, field_name,
+        *((int *) field_value));
+      break;
+
+    default:
+      (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
+        "unsupported JSON field type: %u", field_type);
+  }
+}
+
+static char *get_meta_arg(pool *p, unsigned char *meta, size_t *arg_len) {
+  char buf[PR_TUNABLE_PATH_MAX+1], *ptr;
+  size_t len;
+
+  ptr = buf;
+  len = 0;
+
+  while (*meta != LOGFMT_META_ARG_END) {
+    pr_signals_handle();
+    *ptr++ = (char) *meta++;
+    len++;
+  }
+
+  *ptr = '\0';
+  *arg_len = len;
+
+  return pstrdup(p, buf);
+}
+
+static const char *get_meta_dir_name(cmd_rec *cmd) {
+  const char *dir_name = NULL;
+  pool *p;
+
+  p = cmd->tmp_pool;
+
+  if (pr_cmd_cmp(cmd, PR_CMD_CDUP_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_LIST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_MLSD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_MKD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_NLST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_RMD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XCWD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
+    char *path, *ptr;
+
+    path = pr_fs_decode_path(p, cmd->arg);
+    ptr = strrchr(path, '/');
+
+    if (ptr != NULL) {
+      if (ptr != path) {
+        dir_name = ptr + 1;
+
+      } else if (*(ptr + 1) != '\0') {
+        dir_name = ptr + 1;
+
+      } else {
+        dir_name = path;
+      }
+
+    } else {
+      dir_name = path;
+    }
+
+  } else {
+    dir_name = pr_fs_getvwd();
+  }
+
+  return dir_name;
+}
+
+static const char *get_meta_dir_path(cmd_rec *cmd) {
+  const char *dir_path = NULL;
+  pool *p;
+
+  p = cmd->tmp_pool;
+
+  if (pr_cmd_cmp(cmd, PR_CMD_CDUP_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_LIST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_MLSD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_MKD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_NLST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_RMD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
+    dir_path = dir_abs_path(p, pr_fs_decode_path(p, cmd->arg), TRUE);
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_XCWD_ID) == 0) {
+
+    /* Note: by this point in the dispatch cycle, the current working
+     * directory has already been changed.  For the CWD/XCWD commands, this
+     * means that dir_abs_path() may return an improper path, with the target
+     * directory being reported twice.  To deal with this, do not use
+     * dir_abs_path(), and use pr_fs_getvwd()/pr_fs_getcwd() instead.
+     */
+
+    if (session.chroot_path != NULL) {
+      /* Chrooted session. */
+      if (strncmp(pr_fs_getvwd(), "/", 2) == 0) {
+        dir_path = session.chroot_path;
+
+      } else {
+        dir_path = pdircat(p, session.chroot_path, pr_fs_getvwd(), NULL);
+      }
+
+    } else {
+      /* Non-chrooted session. */
+      dir_path = pr_fs_getcwd();
+    }
+  }
+
+  return dir_path;
+}
+
+static const char *get_meta_filename(cmd_rec *cmd) {
+  const char *filename = NULL;
+  pool *p;
+
+  p = cmd->tmp_pool;
+
+  if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
+    filename = dir_abs_path(p, pr_fs_decode_path(p, cmd->arg), TRUE);
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0) {
+    const char *path;
+
+    path = pr_table_get(cmd->notes, "mod_xfer.retr-path", NULL);
+    if (path != NULL) {
+      filename = dir_abs_path(p, path, TRUE);
+    }
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0) {
+    const char *path;
+
+    path = pr_table_get(cmd->notes, "mod_xfer.store-path", NULL);
+    if (path != NULL) {
+      filename = dir_abs_path(p, path, TRUE);
+    }
+
+  } else if (session.xfer.p != NULL &&
+             session.xfer.path != NULL) {
+    filename = dir_abs_path(p, session.xfer.path, TRUE);
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_CDUP_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_PWD_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_XCUP_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_XPWD_ID) == 0) {
+    filename = dir_abs_path(p, pr_fs_getcwd(), TRUE);
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_CWD_ID) == 0 ||
+             pr_cmd_cmp(cmd, PR_CMD_XCWD_ID) == 0) {
+
+    /* Note: by this point in the dispatch cycle, the current working
+     * directory has already been changed.  For the CWD/XCWD commands, this
+     * means that dir_abs_path() may return an improper path, with the target
+     * directory being reported twice.  To deal with this, do not use
+     * dir_abs_path(), and use pr_fs_getvwd()/pr_fs_getcwd() instead.
+     */
+    if (session.chroot_path != NULL) {
+      /* Chrooted session. */
+      if (strncmp(pr_fs_getvwd(), "/", 2) == 0) {
+        filename = session.chroot_path;
+
+      } else {
+        filename = pdircat(p, session.chroot_path, pr_fs_getvwd(), NULL);
+      }
+
+    } else {
+      /* Non-chrooted session. */
+      filename = pr_fs_getcwd();
+    }
+
+  } else if (pr_cmd_cmp(cmd, PR_CMD_SITE_ID) == 0 &&
+             (strncasecmp(cmd->argv[1], "CHGRP", 6) == 0 ||
+              strncasecmp(cmd->argv[1], "CHMOD", 6) == 0 ||
+              strncasecmp(cmd->argv[1], "UTIME", 6) == 0)) {
+    register unsigned int i;
+    char *ptr = "";
+
+    for (i = 3; i <= cmd->argc-1; i++) {
+      ptr = pstrcat(p, ptr, *ptr ? " " : "",
+        pr_fs_decode_path(p, cmd->argv[i]), NULL);
+    }
+
+    filename = dir_abs_path(p, ptr, TRUE);
+
+  } else {
+    /* Some commands (i.e. DELE, MKD, RMD, XMKD, and XRMD) have associated
+     * filenames that are not stored in the session.xfer structure; these
+     * should be expanded properly as well.
+     */
+    if (pr_cmd_cmp(cmd, PR_CMD_DELE_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_LIST_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_MDTM_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_MKD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_MLSD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_MLST_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_NLST_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_RMD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
+      filename = dir_abs_path(p, pr_fs_decode_path(p, cmd->arg), TRUE);
+
+    } else if (pr_cmd_cmp(cmd, PR_CMD_MFMT_ID) == 0) {
+      /* MFMT has, as its filename, the second argument. */
+      filename = dir_abs_path(p, pr_fs_decode_path(p, cmd->argv[2]), TRUE);
+    }
+  }
+
+  return filename;
+}
+
+static const char *get_meta_transfer_failure(cmd_rec *cmd) {
+  const char *transfer_failure = NULL;
+
+  /* If the current command is one that incurs a data transfer, then we
+   * need to do more work.  If not, it's an easy substitution.
+   */
+  if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_LIST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_MLSD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_NLST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_STOU_ID) == 0) {
+    const char *proto;
+
+    proto = pr_session_get_protocol(0);
+
+    if (strncmp(proto, "ftp", 4) == 0 ||
+        strncmp(proto, "ftps", 5) == 0) {
+
+      if (!(XFER_ABORTED)) {
+        int res;
+        const char *resp_code = NULL, *resp_msg = NULL;
+
+        /* Get the last response code/message.  We use heuristics here to
+         * determine when to use "failed" versus "success".
+         */
+        res = pr_response_get_last(cmd->tmp_pool, &resp_code, &resp_msg);
+        if (res == 0 &&
+            resp_code != NULL) {
+          if (*resp_code != '2' &&
+              *resp_code != '1') {
+            char *ptr;
+
+            /* Parse out/prettify the resp_msg here */
+            ptr = strchr(resp_msg, '.');
+            if (ptr != NULL) {
+              transfer_failure = ptr + 2;
+
+            } else {
+              transfer_failure = resp_msg;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return transfer_failure;
+}
+
+static const char *get_meta_transfer_path(cmd_rec *cmd) {
+  const char *transfer_path = NULL;
+
+  if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
+    transfer_path = dir_best_path(cmd->tmp_pool,
+      pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+
+  } else if (session.xfer.p != NULL &&
+             session.xfer.path != NULL) {
+    transfer_path = session.xfer.path;
+
+  } else {
+    /* Some commands (i.e. DELE, MKD, XMKD, RMD, XRMD) have associated
+     * filenames that are not stored in the session.xfer structure; these
+     * should be expanded properly as well.
+     */
+    if (pr_cmd_cmp(cmd, PR_CMD_DELE_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_MKD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_RMD_ID) == 0 ||
+        pr_cmd_cmp(cmd, PR_CMD_XRMD_ID) == 0) {
+      transfer_path = dir_best_path(cmd->tmp_pool,
+        pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+    }
+  }
+
+  return transfer_path;
+}
+
+static int get_meta_transfer_secs(cmd_rec *cmd, double *transfer_secs) {
+  if (session.xfer.p == NULL) {
+    return -1;
+  }
+
+  /* Make sure that session.xfer.start_time actually has values (which is
+   * not always the case).
+   */
+  if (session.xfer.start_time.tv_sec != 0 ||
+      session.xfer.start_time.tv_usec != 0) {
+    struct timeval end_time;
+
+    gettimeofday(&end_time, NULL);
+    end_time.tv_sec -= session.xfer.start_time.tv_sec;
+
+    if (end_time.tv_usec >= session.xfer.start_time.tv_usec) {
+      end_time.tv_usec -= session.xfer.start_time.tv_usec;
+
+    } else {
+      end_time.tv_usec = 1000000L - (session.xfer.start_time.tv_usec -
+        end_time.tv_usec);
+      end_time.tv_sec--;
+    }
+
+    *transfer_secs = end_time.tv_sec;
+    *transfer_secs += (double) ((double) end_time.tv_usec / (double) 1000);
+
+    return 0;
+  }
+
+  return -1;
+}
+
+static const char *get_meta_transfer_status(cmd_rec *cmd) {
+  const char *transfer_status = NULL;
+
+  /* If the current command is one that incurs a data transfer, then we need
+   * to do more work.  If not, it's an easy substitution.
+   */
+  if (pr_cmd_cmp(cmd, PR_CMD_ABOR_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_LIST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_MLSD_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_NLST_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_RETR_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_STOR_ID) == 0 ||
+      pr_cmd_cmp(cmd, PR_CMD_STOU_ID) == 0) {
+    const char *proto;
+
+    proto = pr_session_get_protocol(0);
+
+    if (strncmp(proto, "ftp", 4) == 0 ||
+        strncmp(proto, "ftps", 5) == 0) {
+      if (!(XFER_ABORTED)) {
+        int res;
+        const char *resp_code = NULL, *resp_msg = NULL;
+
+        /* Get the last response code/message.  We use heuristics here to
+         * determine when to use "failed" versus "success".
+         */
+        res = pr_response_get_last(cmd->tmp_pool, &resp_code, &resp_msg);
+        if (res == 0 &&
+            resp_code != NULL) {
+          if (*resp_code == '2') {
+            if (pr_cmd_cmp(cmd, PR_CMD_ABOR_ID) != 0) {
+              transfer_status = "success";
+
+            } else {
+              /* We're handling the ABOR command, so obviously the value
+               * should be 'cancelled'.
+               */
+              transfer_status = "cancelled";
+            }
+
+          } else if (*resp_code == '1') {
+
+            /* If the first digit of the response code is 1, then the
+             * response code (for a data transfer command) is probably 150,
+             * which means that the transfer was still in progress (didn't
+             * complete with a 2xx/4xx response code) when we are called here,
+             * which in turn means a timeout kicked in.
+             */
+
+            transfer_status = "timeout";
+
+          } else {
+            transfer_status = "failed";
+          }
+
+        } else {
+          transfer_status = "success";
+        }
+
+      } else {
+        transfer_status = "cancelled";
+      }
+
+    } else {
+      /* mod_sftp stashes a note for us in the command notes if the transfer
+       * failed.
+       */
+      const char *sftp_status;
+
+      sftp_status = pr_table_get(cmd->notes, "mod_sftp.file-status", NULL);
+      if (sftp_status == NULL) {
+        transfer_status = "success";
+
+      } else {
+        transfer_status = "failed";
+      }
+    }
+  }
+
+  return transfer_status;
+}
+
+static int find_next_meta(pool *p, cmd_rec *cmd, int flags,
+    unsigned char **log_fmt, void *obj,
+    void (*encode_field)(pool *, void *, const char *, size_t, unsigned int,
+      const void *)) {
+  const struct field_info *fi;
+  unsigned char *m;
+  unsigned int meta;
+
+  m = (*log_fmt) + 1;
+
+  meta = *m;
+  fi = pr_table_kget(redis_field_idtab, (const void *) &meta,
+    sizeof(unsigned int), NULL);
+
+  switch (meta) {
+    case LOGFMT_META_BYTES_SENT: {
+      if (session.xfer.p != NULL) {
+        double bytes_sent;
+
+        bytes_sent = session.xfer.total_bytes;
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          &bytes_sent);
+
+      } else if (pr_cmd_cmp(cmd, PR_CMD_DELE_ID) == 0) {
+        double bytes_sent;
+
+        bytes_sent = redis_dele_filesz;
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          &bytes_sent);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_FILENAME: {
+      const char *filename;
+
+      filename = get_meta_filename(cmd);
+      if (filename != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          filename);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_ENV_VAR: {
+      m++;
+
+      if (*m == LOGFMT_META_START &&
+          *(m + 1) == LOGFMT_META_ARG) {
+        char *key, *env = NULL;
+        size_t key_len = 0;
+
+        key = get_meta_arg(p, (m + 2), &key_len);
+        m += key_len;
+
+        env = pr_env_get(p, key);
+        if (env != NULL) {
+          char *field_name;
+          size_t field_namelen;
+
+          field_name = pstrcat(p, fi->field_name, key, NULL);
+          field_namelen = strlen(field_name);
+
+          encode_field(p, obj, field_name, field_namelen, fi->field_type, env);
+        }
+      }
+
+      break;
+    }
+
+    case LOGFMT_META_REMOTE_HOST: {
+      const char *name;
+
+      name = pr_netaddr_get_sess_remote_name();
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        name);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_REMOTE_IP: {
+      const char *ipstr;
+
+      ipstr = pr_netaddr_get_ipstr(pr_netaddr_get_sess_local_addr());
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        ipstr);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_IDENT_USER: {
+      const char *ident_user;
+
+      ident_user = pr_table_get(session.notes, "mod_ident.rfc1413-ident", NULL);
+      if (ident_user != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          ident_user);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_PID: {
+      double sess_pid;
+
+      sess_pid = session.pid;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &sess_pid);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_TIME: {
+      char *time_fmt = "%Y-%m-%d %H:%M:%S %z", ts[128];
+      struct tm *tm;
+      time_t now;
+
+      m++;
+
+      now = time(NULL);
+      tm = pr_gmtime(NULL, &now);
+
+      if (*m == LOGFMT_META_START &&
+          *(m+1) == LOGFMT_META_ARG) {
+        size_t fmt_len = 0;
+
+        time_fmt = get_meta_arg(p, (m + 2), &fmt_len);
+      }
+
+      strftime(ts, sizeof(ts)-1, time_fmt, tm);
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        ts);
+
+      break;
+    }
+
+    case LOGFMT_META_SECONDS: {
+      double transfer_secs;
+
+      if (get_meta_transfer_secs(cmd, &transfer_secs) == 0) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          &transfer_secs);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_COMMAND: {
+      if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 &&
+          session.hide_password) {
+        const char *full_cmd = "PASS (hidden)";
+
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          full_cmd);
+
+      } else if (pr_cmd_cmp(cmd, PR_CMD_ADAT_ID) == 0) {
+        const char *full_cmd = "ADAT (hidden)";
+
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          full_cmd);
+
+      } else if (flags == REDIS_LOG_EVENT_FL_COMMAND) {
+        const char *full_cmd;
+
+        full_cmd = get_full_cmd(cmd);
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          full_cmd);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_LOCAL_NAME: {
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        cmd->server->ServerName);
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_LOCAL_PORT: {
+      double server_port;
+
+      server_port = cmd->server->ServerPort;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &server_port);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_LOCAL_IP: {
+      const char *ipstr;
+
+      ipstr = pr_netaddr_get_ipstr(pr_netaddr_get_sess_local_addr());
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        ipstr);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_LOCAL_FQDN: {
+      const char *dnsstr;
+
+      dnsstr = pr_netaddr_get_dnsstr(pr_netaddr_get_sess_local_addr());
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        dnsstr);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_USER: {
+      if (session.user != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          session.user);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_ORIGINAL_USER: {
+      const char *orig_user = NULL;
+
+      orig_user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
+      if (orig_user != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          orig_user);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_RESPONSE_CODE: {
+      const char *resp_code = NULL;
+      int res;
+
+      res = pr_response_get_last(cmd->tmp_pool, &resp_code, NULL);
+      if (res == 0 &&
+          resp_code != NULL) {
+        double code;
+
+        code = atoi(resp_code);
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          &code);
+
+      /* Hack to add return code for proper logging of QUIT command. */
+      } else if (pr_cmd_cmp(cmd, PR_CMD_QUIT_ID) == 0) {
+        double code = 221;
+
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          &code);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_CLASS: {
+      if (session.conn_class != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          session.conn_class);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_ANON_PASS: {
+      const char *anon_pass;
+
+      anon_pass = pr_table_get(session.notes, "mod_auth.anon-passwd", NULL);
+      if (anon_pass == NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          anon_pass);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_METHOD: {
+      if (flags == REDIS_LOG_EVENT_FL_COMMAND) {
+        if (pr_cmd_cmp(cmd, PR_CMD_SITE_ID) != 0) {
+          encode_field(p, obj, fi->field_name, fi->field_namelen,
+            fi->field_type, cmd->argv[0]);
+
+        } else {
+          char buf[32], *ptr;
+
+          /* Make sure that the SITE command used is all in uppercase,
+           * for logging purposes.
+           */
+          for (ptr = cmd->argv[1]; *ptr; ptr++) {
+            *ptr = toupper((int) *ptr);
+          }
+
+          memset(buf, '\0', sizeof(buf));
+          snprintf(buf, sizeof(buf)-1, "%s %s", (char *) cmd->argv[0],
+            (char *) cmd->argv[1]);
+
+          encode_field(p, obj, fi->field_name, fi->field_namelen,
+            fi->field_type, buf);
+        }
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_XFER_PATH: {
+      const char *transfer_path;
+
+      transfer_path = get_meta_transfer_path(cmd);
+      if (transfer_path != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          transfer_path);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_DIR_NAME: {
+      const char *dir_name;
+
+      dir_name = get_meta_dir_name(cmd);
+      if (dir_name != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          dir_name);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_DIR_PATH: {
+      const char *dir_path;
+
+      dir_path = get_meta_dir_path(cmd);
+      if (dir_path != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          dir_path);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_CMD_PARAMS: {
+      if (pr_cmd_cmp(cmd, PR_CMD_ADAT_ID) == 0 ||
+          pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0) {
+        const char *params = "(hidden)";
+
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          params);
+
+      } else if (REDIS_LOG_EVENT_FL_COMMAND &&
+                 cmd->argc > 1) {
+        const char *params;
+
+        params = pr_fs_decode_path(p, cmd->arg);
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          params);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_RESPONSE_STR: {
+      const char *resp_msg = NULL;
+      int res;
+
+      res = pr_response_get_last(p, NULL, &resp_msg);
+      if (res == 0 &&
+          resp_msg != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          resp_msg);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_PROTOCOL: {
+      const char *proto;
+
+      proto = pr_session_get_protocol(0);
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        proto);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_VERSION: {
+      const char *version;
+
+      version = PROFTPD_VERSION_TEXT;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        version);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_RENAME_FROM: {
+      if (pr_cmd_cmp(cmd, PR_CMD_RNTO_ID) == 0) {
+        const char *rnfr_path;
+
+        rnfr_path = pr_table_get(session.notes, "mod_core.rnfr-path", NULL);
+        if (rnfr_path != NULL) {
+          encode_field(p, obj, fi->field_name, fi->field_namelen,
+            fi->field_type, rnfr_path);
+        }
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_FILE_MODIFIED: {
+      int modified = FALSE;
+      const char *val;
+
+      val = pr_table_get(cmd->notes, "mod_xfer.file-modified", NULL);
+      if (val != NULL) {
+        if (strncmp(val, "true", 5) == 0) {
+          modified = TRUE;
+        }
+      }
+
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &modified);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_UID: {
+      double sess_uid;
+
+      sess_uid = session.login_uid;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &sess_uid);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_GID: {
+      double sess_gid;
+
+      sess_gid = session.login_gid;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &sess_gid);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_RAW_BYTES_IN: {
+      double bytes_rcvd;
+
+      bytes_rcvd = session.total_raw_in;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &bytes_rcvd);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_RAW_BYTES_OUT: {
+      double bytes_sent;
+
+      bytes_sent = session.total_raw_out;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &bytes_sent);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_EOS_REASON: {
+      const char *reason = NULL, *details = NULL;
+
+      reason = pr_session_get_disconnect_reason(&details);
+      if (reason != NULL) {
+        if (details != NULL) {
+          char buf[256];
+
+          memset(buf, '\0', sizeof(buf));
+          snprintf(buf, sizeof(buf)-1, "%s: %s", reason, details);
+          encode_field(p, obj, fi->field_name, fi->field_namelen,
+            fi->field_type, buf);
+
+        } else {
+          encode_field(p, obj, fi->field_name, fi->field_namelen,
+            fi->field_type, reason);
+        }
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_VHOST_IP:
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        cmd->server->ServerAddress);
+      m++;
+      break;
+
+    case LOGFMT_META_NOTE_VAR: {
+      m++;
+
+      if (*m == LOGFMT_META_START &&
+          *(m + 1) == LOGFMT_META_ARG) {
+        const char *note = NULL;
+        char *key;
+        size_t key_len = 0;
+
+        key = get_meta_arg(p, (m + 2), &key_len);
+        m += key_len;
+
+        /* Check in the cmd->notes table first. */
+        note = pr_table_get(cmd->notes, key, NULL);
+        if (note == NULL) {
+          /* If not there, check in the session.notes table. */
+          note = pr_table_get(session.notes, key, NULL);
+        }
+
+        if (note != NULL) {
+          char *field_name;
+          size_t field_namelen;
+
+          field_name = pstrcat(p, fi->field_name, note, NULL);
+          field_namelen = strlen(field_name);
+
+          encode_field(p, obj, field_name, field_namelen, fi->field_type, note);
+        }
+      }
+
+      break;
+    }
+
+    case LOGFMT_META_XFER_STATUS: {
+      const char *transfer_status;
+
+      transfer_status = get_meta_transfer_status(cmd);
+      if (transfer_status != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          transfer_status);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_XFER_FAILURE: {
+      const char *transfer_failure;
+
+      transfer_failure = get_meta_transfer_failure(cmd);
+      if (transfer_failure != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          transfer_failure);
+      }
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_MICROSECS: {
+      double sess_usecs;
+      struct timeval now;
+
+      gettimeofday(&now, NULL);
+      sess_usecs = now.tv_usec;
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &sess_usecs);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_MILLISECS: {
+      double sess_msecs;
+      struct timeval now;
+
+      gettimeofday(&now, NULL);
+
+      /* Convert microsecs to millisecs. */
+      sess_msecs = (now.tv_usec / 1000);
+
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        &sess_msecs);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_ISO8601: {
+      char ts[128];
+      struct tm *tm;
+      struct timeval now;
+      unsigned long millis;
+      size_t len;
+
+      gettimeofday(&now, NULL);
+      tm = pr_localtime(NULL, (const time_t *) &(now.tv_sec));
+
+      len = strftime(ts, sizeof(ts)-1, "%Y-%m-%d %H:%M:%S", tm);
+
+      /* Convert microsecs to millisecs. */
+      millis = now.tv_usec / 1000;
+
+      snprintf(ts + len, sizeof(ts) - len - 1, ",%03lu", millis);
+      encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+        ts);
+
+      m++;
+      break;
+    }
+
+    case LOGFMT_META_GROUP: {
+      if (session.group != NULL) {
+        encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+          session.group);
+      }
+      m++;
+      break;
+    }
+
+    default:
+      (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
+        "skipping unsupported LogFormat meta %u", meta);
+      break;
+  }
+
+  *log_fmt = m;
+  return 0;
+}
+
+static int encode_fields(pool *p, cmd_rec *cmd, int flags,
+    unsigned char *log_fmt, void *obj,
+    void (*encode_field)(pool *, void *, const char *, size_t, unsigned int,
+      const void *)) {
+
+  if (flags == REDIS_LOG_EVENT_FL_CONNECT &&
+      session.prev_server == NULL) {
+    unsigned int meta = REDIS_META_CONNECT;
+    const struct field_info *fi;
+    int connecting = TRUE;
+
+    fi = pr_table_kget(redis_field_idtab, (const void *) &meta,
+      sizeof(unsigned int), NULL);
+
+    encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+      &connecting);
+
+  } else if (flags == REDIS_LOG_EVENT_FL_DISCONNECT) {
+    unsigned int meta = REDIS_META_DISCONNECT;
+    const struct field_info *fi;
+    int disconnecting = TRUE;
+
+    fi = pr_table_kget(redis_field_idtab, (const void *) &meta,
+      sizeof(unsigned int), NULL);
+
+    encode_field(p, obj, fi->field_name, fi->field_namelen, fi->field_type,
+      &disconnecting);
+  }
+
+  while (*log_fmt) {
+    pr_signals_handle();
+
+    if (*log_fmt == LOGFMT_META_START) {
+      find_next_meta(cmd->tmp_pool, cmd, flags, &log_fmt, obj, encode_field);
+
+    } else {
+      log_fmt++;
+    }
+  }
+
+  return 0;
+}
+
+static int encode_log_fmt(pool *p, cmd_rec *cmd, int flags,
+    unsigned char *log_fmt, char **payload, size_t *payload_len) {
+  pr_json_object_t *json;
+  char *text = NULL;
+
+  json = pr_json_object_alloc(p);
+  (void) encode_fields(p, cmd, flags, log_fmt, json, encode_json);
+
+  text = pr_json_object_to_text(p, json, "");
+  pr_trace_msg(trace_channel, 3, "generated JSON payload: %s", text);
+
+  *payload_len = strlen(text);
+  *payload = text;
+
+  pr_json_object_free(json);
+  return 0;
+}
+
+static int is_loggable_event(array_header *logged_events, cmd_rec *cmd,
+    int flags) {
+  register unsigned int i;
+  int *event_ids, loggable = FALSE;
+
+  event_ids = logged_events->elts;
+
+  for (i = 0; i < logged_events->nelts; i++) {
+    switch (event_ids[i]) {
+      case REDIS_EVENT_ALL:
+        loggable = TRUE;
+         break;
+
+      case REDIS_EVENT_CONNECT_ID:
+        if (flags == REDIS_LOG_EVENT_FL_CONNECT) {
+          loggable = TRUE;
+        }
+        break;
+
+      case REDIS_EVENT_DISCONNECT_ID:
+        if (flags == REDIS_LOG_EVENT_FL_DISCONNECT) {
+          loggable = TRUE;
+        }
+        break;
+
+      default:
+        if (flags == REDIS_LOG_EVENT_FL_COMMAND &&
+            pr_cmd_cmp(cmd, event_ids[i]) == 0) {
+          loggable = TRUE;
+        }
+        break;
+    }
+  }
+
+  return loggable;
+}
+
+static void log_event(cmd_rec *cmd, int flags) {
+  pr_redis_t *redis;
+  config_rec *c;
+
+  redis = pr_redis_conn_get(session.pool);
+  if (redis == NULL) {
+    (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
+      "error connecting to Redis: %s", strerror(errno));
+    return;
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisLogOnCommand", FALSE);
+  while (c != NULL) {
+    int res;
+    array_header *logged_events;
+    const char *fmt_name = NULL;
+    char *payload = NULL;
+    size_t payload_len = 0;
+    unsigned char *log_fmt;
+
+    pr_signals_handle();
+
+    logged_events = c->argv[0];
+
+    if (is_loggable_event(logged_events, cmd, flags) == FALSE) {
+      c = find_config_next(c, c->next, CONF_PARAM, "RedisLogOnCommand", FALSE);
+      continue;
+    }
+
+    fmt_name = c->argv[1];
+    log_fmt = c->argv[2];
+    res = encode_log_fmt(cmd->tmp_pool, cmd, flags, log_fmt, &payload,
+      &payload_len);
+    if (res < 0) {
+      (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
+        "error generating JSON formatted log message: %s", strerror(errno));
+      payload = NULL;
+      payload_len = 0;
+
+    } else {
+      res = pr_redis_list_append(redis, &redis_module, fmt_name, payload,
+        payload_len);
+      if (res < 0) {
+        (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
+          "error appending log message to '%s': %s", log_fmt, strerror(errno));
+      }
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "RedisLogOnCommand", FALSE);
+  }
+}
+
+/* Configuration handlers
+ */
+
+/* usage: RedisEngine on|off */
+MODRET set_redisengine(cmd_rec *cmd) {
+  int engine = -1;
+  config_rec *c = NULL;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  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(int));
+  *((int *) c->argv[0]) = engine;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: RedisLog path|"none" */
+MODRET set_redislog(cmd_rec *cmd) {
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  if (strcasecmp(cmd->argv[1], "none") != 0 &&
+      pr_fs_valid_path(cmd->argv[1]) < 0) {
+    CONF_ERROR(cmd, "must be an absolute path");
+  }
+
+  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
+  return PR_HANDLED(cmd);
+}
+
+/* XXX Would be nice to a pr_str_csv2array() function */
+static array_header *csv2array(pool *p, char *csv) {
+  array_header *event_names;
+  char *ptr, *name;
+
+  event_names = make_array(p, 1, sizeof(char *));
+
+  ptr = csv;
+  name = pr_str_get_word(&ptr, 0);
+  while (name != NULL) {
+    pr_signals_handle();
+
+    *((char **) push_array(event_names)) = pstrdup(p, name);
+
+    /* Skip commas. */
+    while (*ptr == ',') {
+      ptr++;
+    }
+
+    name = pr_str_get_word(&ptr, 0);
+  }
+
+  return event_names;
+}
+
+static array_header *event_names2ids(pool *p, const char *directive,
+    array_header *event_names) {
+  register unsigned int i;
+  array_header *event_ids;
+
+  event_ids = make_array(p, event_names->nelts, sizeof(int));
+  for (i = 0; i < event_names->nelts; i++) {
+    const char *name;
+    int event_id, valid = TRUE;
+
+    name = ((const char **) event_names->elts)[i];
+
+    event_id = pr_cmd_get_id(name);
+    if (event_id < 0) {
+      if (strcmp(name, "ALL") == 0) {
+        event_id = REDIS_EVENT_ALL;
+
+      } else if (strcmp(name, "CONNECT") == 0) {
+        event_id = REDIS_EVENT_CONNECT_ID;
+
+      } else if (strcmp(name, "DISCONNECT") == 0) {
+        event_id = REDIS_EVENT_DISCONNECT_ID;
+
+      } else {
+        pr_log_debug(DEBUG0, "%s: skipping unsupported event '%s'", directive,
+          name);
+        valid = FALSE;
+      }
+    }
+
+    if (valid == TRUE) {
+      *((int *) push_array(event_ids)) = event_id;
+    }
+  }
+
+  return event_ids;
+}
+
+/* usage: RedisLogOnCommand commands log-fmt */
+MODRET set_redislogoncommand(cmd_rec *cmd) {
+  config_rec *c;
+  const char *fmt_name;
+  unsigned char *log_fmt = NULL;
+  array_header *event_names;
+
+  CHECK_ARGS(cmd, 2);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);
+
+  event_names = csv2array(cmd->tmp_pool, cmd->argv[1]);
+  fmt_name = cmd->argv[2];
+
+  /* Make sure that the given LogFormat name is known. */
+  c = find_config(cmd->server->conf, CONF_PARAM, "LogFormat", FALSE);
+  while (c != NULL) {
+    if (strcmp(fmt_name, c->argv[0]) == 0) {
+      log_fmt = c->argv[1];
+      break;
+    }
+
+    c = find_config_next(c, c->next, CONF_PARAM, "LogFormat", FALSE);
+  }
+
+  if (log_fmt == NULL) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "no LogFormat '", fmt_name,
+      "' configured", NULL));
+  }
+
+  c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL);
+  c->argv[0] = event_names2ids(c->pool, cmd->argv[0], event_names);
+  c->argv[1] = pstrdup(c->pool, fmt_name);
+  c->argv[2] = log_fmt;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: RedisServer host[:port] [password] */
+MODRET set_redisserver(cmd_rec *cmd) {
+  config_rec *c;
+  char *server, *password = NULL, *ptr;
+  size_t server_len;
+  int ctx, port = REDIS_DEFAULT_PORT;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  server = pstrdup(cmd->tmp_pool, cmd->argv[1]);
+  server_len = strlen(server);
+
+  ptr = strrchr(server, ':');
+  if (ptr != NULL) {
+    /* We also need to check for IPv6 addresses, e.g. "[::1]" or "[::1]:6379",
+     * before assuming that the text following our discovered ':' is indeed
+     * a port number.
+     */
+
+    if (*server == '[') {
+      if (*(ptr-1) == ']') {
+        /* We have an IPv6 address with an explicit port number. */
+        server = pstrndup(cmd->tmp_pool, server + 1, (ptr - 1) - (server + 1));
+        *ptr = '\0';
+        port = atoi(ptr + 1);
+
+      } else if (server[server_len-1] == ']') {
+        /* We have an IPv6 address without an explicit port number. */
+        server = pstrndup(cmd->tmp_pool, server + 1, server_len - 2);
+        port = REDIS_DEFAULT_PORT;
+      }
+
+    } else {
+      *ptr = '\0';
+      port = atoi(ptr + 1);
+    }
+  }
+
+  if (cmd->argc == 3) {
+    password = cmd->argv[2];
+  }
+
+  c = add_config_param(cmd->argv[0], 3, NULL, NULL, NULL);
+  c->argv[0] = pstrdup(c->pool, server);
+  c->argv[1] = palloc(c->pool, sizeof(int));
+  *((int *) c->argv[1]) = port;
+  c->argv[2] = pstrdup(c->pool, password);
+
+  ctx = (cmd->config && cmd->config->config_type != CONF_PARAM ?
+    cmd->config->config_type : cmd->server->config_type ?
+    cmd->server->config_type : CONF_ROOT);
+
+  if (ctx == CONF_ROOT) {
+    /* If we're the "server config" context, set the server now.  This
+     * would let mod_redis talk to those servers for e.g. ftpdctl actions.
+     */
+    (void) redis_set_server(c->argv[0], port, c->argv[2]);
+  }
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: RedisTimeouts conn-timeout io-timeout */
+MODRET set_redistimeouts(cmd_rec *cmd) {
+  config_rec *c;
+  unsigned long connect_millis, io_millis;
+  char *ptr = NULL;
+
+  CHECK_ARGS(cmd, 2);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  connect_millis = strtoul(cmd->argv[1], &ptr, 10);
+  if (ptr && *ptr) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+      "badly formatted connect timeout value: ", cmd->argv[1], NULL));
+  }
+
+  ptr = NULL;
+  io_millis = strtoul(cmd->argv[2], &ptr, 10);
+  if (ptr && *ptr) {
+    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
+      "badly formatted IO timeout value: ", cmd->argv[2], NULL));
+  }
+
+#if 0
+  /* XXX If we're the "server config" context, set the timeouts now.
+   * This would let mod_redis talk to those servers for e.g. ftpdctl
+   * actions.
+   */
+  redis_set_timeouts(conn_timeout, io_timeout);
+#endif
+
+  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
+  c->argv[0] = palloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = connect_millis;
+  c->argv[1] = palloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[1]) = io_millis;
+
+  return PR_HANDLED(cmd);
+}
+
+/* Command handlers
+ */
+
+MODRET redis_log_any(cmd_rec *cmd) {
+  if (redis_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  log_event(cmd, REDIS_LOG_EVENT_FL_COMMAND);
+  return PR_DECLINED(cmd);
+}
+
+MODRET redis_pre_dele(cmd_rec *cmd) {
+  const char *path;
+
+  if (redis_engine == FALSE) {
+    return PR_DECLINED(cmd);
+  }
+
+  path = dir_canonical_path(cmd->tmp_pool,
+    pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+  if (path != NULL) {
+    struct stat st;
+
+    /* Briefly cache the size of the file being deleted, so that it can be
+     * logged properly using %b.
+     */
+    pr_fs_clear_cache2(path);
+    if (pr_fsio_stat(path, &st) == 0) {
+      redis_dele_filesz = st.st_size;
+    }
+  }
+
+  redis_dele_filesz = 0;
+
+  return PR_DECLINED(cmd);
+}
+
+/* Event handlers
+ */
+
+static void redis_exit_ev(const void *event_data, void *user_data) {
+  cmd_rec *cmd;
+  pool *tmp_pool;
+
+  tmp_pool = make_sub_pool(session.pool);
+  cmd = pr_cmd_alloc(tmp_pool, 1, "DISCONNECT");
+  log_event(cmd, REDIS_LOG_EVENT_FL_DISCONNECT);
+  destroy_pool(tmp_pool);
+
+  redis_clear();
+}
+
+static void redis_restart_ev(const void *event_data, void *user_data) {
+  destroy_pool(redis_pool);
+  redis_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(redis_pool, MOD_REDIS_VERSION);
+}
+
+static void redis_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
+
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
+
+  pr_event_unregister(&redis_module, "core.exit", redis_exit_ev);
+  pr_event_unregister(&redis_module, "core.session-reinit",
+    redis_sess_reinit_ev);
+
+  (void) close(redis_logfd);
+  redis_logfd = -1;
+
+  /* XXX Restore other Redis settings? */
+  /* reset RedisTimeouts */
+
+  res = redis_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&redis_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
+
+static void redis_shutdown_ev(const void *event_data, void *user_data) {
+  destroy_pool(redis_pool);
+  redis_field_idtab = NULL;
+}
+
+/* Initialization functions
+ */
+
+static int redis_module_init(void) {
+  redis_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(redis_pool, MOD_REDIS_VERSION);
+
+  redis_init();
+  pr_event_register(&redis_module, "core.restart", redis_restart_ev, NULL);
+  pr_event_register(&redis_module, "core.shutdown", redis_shutdown_ev, NULL);
+
+  pr_log_debug(DEBUG2, MOD_REDIS_VERSION ": using hiredis-%d.%d.%d",
+    HIREDIS_MAJOR, HIREDIS_MINOR, HIREDIS_PATCH);
+
+  if (make_fieldtab(redis_pool) < 0) {
+    return -1;
+  }
+
+  return 0;
+}
+
+static int redis_sess_init(void) {
+  config_rec *c;
+  cmd_rec *cmd;
+  pool *tmp_pool;
+
+  pr_event_register(&redis_module, "core.session-reinit",
+    redis_sess_reinit_ev, NULL);
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisEngine", FALSE);
+  if (c != NULL) {
+    int engine;
+
+    engine = *((int *) c->argv[0]);
+    if (engine == FALSE) {
+      return 0;
+    }
+
+    redis_engine = engine;
+  }
+
+  pr_event_register(&redis_module, "core.exit", redis_exit_ev, NULL);
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisLog", FALSE);
+  if (c != NULL) {
+    const char *path;
+
+    path = c->argv[0];
+    if (strcasecmp(path, "none") != 0) {
+      int res, xerrno;
+
+      pr_signals_block();
+      PRIVS_ROOT
+      res = pr_log_openfile(path, &redis_logfd, PR_LOG_SYSTEM_MODE);
+      xerrno = errno;
+      PRIVS_RELINQUISH
+      pr_signals_unblock();
+
+      switch (res) {
+        case 0:
+          break;
+
+        case -1:
+          pr_log_pri(PR_LOG_NOTICE, MOD_REDIS_VERSION
+            ": notice: unable to open RedisLog '%s': %s", path,
+            strerror(xerrno));
+          break;
+
+        case PR_LOG_WRITABLE_DIR:
+          pr_log_pri(PR_LOG_WARNING, MOD_REDIS_VERSION
+            ": notice: unable to use RedisLog '%s': parent directory is "
+              "world-writable", path);
+          break;
+
+        case PR_LOG_SYMLINK:
+          pr_log_pri(PR_LOG_WARNING, MOD_REDIS_VERSION
+            ": notice: unable to use RedisLog '%s': cannot log to a symlink",
+            path);
+          break;
+      }
+    }
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisServer", FALSE);
+  if (c != NULL) {
+    const char *server, *password;
+    int port;
+
+    server = c->argv[0];
+    port = *((int *) c->argv[1]);
+    password = c->argv[2];
+
+    (void) redis_set_server(server, port, password);
+  }
+
+  c = find_config(main_server->conf, CONF_PARAM, "RedisTimeouts", FALSE);
+  if (c) {
+    unsigned long connect_millis, io_millis;
+
+    connect_millis = *((unsigned long *) c->argv[0]);
+    io_millis = *((unsigned long *) c->argv[1]);
+
+    if (redis_set_timeouts(connect_millis, io_millis) < 0) {
+      (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
+        "error setting Redis timeouts: %s", strerror(errno));
+    }
+  }
+
+  tmp_pool = make_sub_pool(session.pool);
+  cmd = pr_cmd_alloc(tmp_pool, 1, "CONNECT");
+  log_event(cmd, REDIS_LOG_EVENT_FL_CONNECT);
+  destroy_pool(tmp_pool);
+
+  return 0;
+}
+
+/* Module API tables
+ */
+
+static conftable redis_conftab[] = {
+  { "RedisEngine",		set_redisengine,	NULL },
+  { "RedisLog",			set_redislog,		NULL },
+  { "RedisLogOnCommand",	set_redislogoncommand,	NULL },
+  { "RedisServer",		set_redisserver,	NULL },
+  { "RedisTimeouts",		set_redistimeouts,	NULL },
+ 
+  { NULL }
+};
+
+static cmdtable redis_cmdtab[] = {
+  { LOG_CMD,		C_ANY,	G_NONE,	redis_log_any,	FALSE,	FALSE },
+  { LOG_CMD_ERR,	C_ANY,	G_NONE,	redis_log_any,	FALSE,	FALSE },
+  { PRE_CMD,		C_DELE,	G_NONE,	redis_pre_dele,	FALSE,	FALSE },
+
+  { 0, NULL }
+};
+
+module redis_module = {
+  NULL, NULL,
+
+  /* Module API version 2.0 */
+  0x20,
+
+  /* Module name */
+  "redis",
+
+  /* Module configuration handler table */
+  redis_conftab,
+
+  /* Module command handler table */
+  redis_cmdtab,
+
+  /* Module authentication handler table */
+  NULL,
+
+  /* Module initialization function */
+  redis_module_init,
+
+  /* Session initialization function */
+  redis_sess_init,
+
+  /* Module version */
+  MOD_REDIS_VERSION
+};
diff --git a/modules/mod_rlimit.c b/modules/mod_rlimit.c
index 420bc53..6788e4c 100644
--- a/modules/mod_rlimit.c
+++ b/modules/mod_rlimit.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2013-2014 The ProFTPD Project team
+ * Copyright (c) 2013-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,15 +22,27 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Resource limit module
- * $Id: mod_rlimit.c,v 1.8 2014-01-31 17:29:27 castaglia Exp $
- */
+/* Resource limit module */
 
 #include "conf.h"
 #include "privs.h"
 
 #define MOD_RLIMIT_VERSION		"mod_rlimit/1.0"
 
+/* On some platforms, including both sys/prctl.h and linux/prctl.h will cause
+ * build errors similar to this one:
+ *
+ *  https://github.com/dvarrazzo/py-setproctitle/issues/44
+ *
+ * So try to work around this behavior by including sys/prctrl.h if available,
+ * and if not, try linux/prctl.h.
+ */
+#if defined(HAVE_SYS_PRCTL_H)
+# include <sys/prctl.h>
+#elif defined(HAVE_LINUX_PRCTL_H)
+# include <linux/prctl.h>
+#endif
+
 module rlimit_module;
 
 #define DAEMON_SCOPE		3
@@ -142,7 +154,8 @@ MODRET set_rlimitcpu(cmd_rec *cmd) {
     CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
   }
 
-  if (pr_rlimit_get_cpu(&current, &max) < 0) {
+  if (pr_rlimit_get_cpu(&current, &max) < 0 &&
+      errno != ENOSYS) {
     pr_log_pri(PR_LOG_NOTICE, "unable to retrieve CPU resource limits: %s",
       strerror(errno));
   }
@@ -281,7 +294,8 @@ MODRET set_rlimitmemory(cmd_rec *cmd) {
   }
 
   /* Retrieve the current values */
-  if (pr_rlimit_get_memory(&current, &max) < 0) {
+  if (pr_rlimit_get_memory(&current, &max) < 0 &&
+      errno != ENOSYS) {
     pr_log_pri(PR_LOG_NOTICE, "unable to get memory resource limits: %s",
       strerror(errno));
   }
@@ -551,6 +565,26 @@ static int rlimit_set_core(int scope) {
     pr_log_debug(DEBUG2, "set core resource limits for daemon");
   }
 
+#if !defined(PR_DEVEL_COREDUMP) && \
+    defined(HAVE_PRCTL) && \
+    defined(PR_SET_DUMPABLE)
+  if (max == 0) {
+    /* Really, no core dumps please. On Linux, there are exceptions made
+     * even when setting RLIMIT_CORE = 0; see:
+     *
+     *  https://lkml.org/lkml/2011/8/24/136
+     *
+     * so when possible, use PR_SET_DUMPABLE to ensure that no coredumps
+     * happen.
+     */
+    if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0) {
+      pr_log_pri(PR_LOG_ERR, "error setting PR_SET_DUMPABLE to false: %s",
+        strerror(errno));
+    }
+  }
+#endif /* no --enable-devel=coredump and HAVE_PRCTL and PR_SET_DUMPABLE */
+
+  errno = xerrno;
   return res;
 }
 
diff --git a/modules/mod_site.c b/modules/mod_site.c
index 9ae5310..ac0c68b 100644
--- a/modules/mod_site.c
+++ b/modules/mod_site.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,17 +23,15 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* "SITE" commands module for ProFTPD
- * $Id: mod_site.c,v 1.58 2013-10-07 05:51:30 castaglia Exp $
- */
+/* "SITE" commands module for ProFTPD. */
 
 #include "conf.h"
 
 /* From mod_core.c */
-extern int core_chmod(cmd_rec *cmd, char *dir, mode_t mode);
-extern int core_chgrp(cmd_rec *cmd, char *dir, uid_t uid, gid_t gid);
+extern int core_chmod(cmd_rec *cmd, const char *path, mode_t mode);
+extern int core_chgrp(cmd_rec *cmd, const char *path, uid_t uid, gid_t gid);
 
-modret_t *site_dispatch(cmd_rec *);
+modret_t *site_dispatch(cmd_rec *cmd);
 
 static struct {
   char *cmd;
@@ -46,19 +44,20 @@ static struct {
   { NULL,	NULL,					FALSE }
 };
 
-static char *_get_full_cmd(cmd_rec *cmd) {
+static char *full_cmd(cmd_rec *cmd) {
+  register unsigned int i;
   char *res = "";
   size_t reslen = 0;
-  int i;
 
-  for (i = 0; i < cmd->argc; i++)
+  for (i = 0; i < cmd->argc; i++) {
     res = pstrcat(cmd->tmp_pool, res, cmd->argv[i], " ", NULL);
+  }
 
   reslen = strlen(res);
   while (reslen >= 1 &&
          res[reslen-1] == ' ') {
     res[reslen-1] = '\0';
-    reslen = strlen(res);
+    reslen--;
   }
 
   return res;
@@ -68,48 +67,92 @@ MODRET site_chgrp(cmd_rec *cmd) {
   int res;
   gid_t gid;
   char *path = NULL, *tmp = NULL, *arg = "";
+  struct stat st;
   register unsigned int i = 0;
 #ifdef PR_USE_REGEX
   pr_regex_t *pre;
 #endif
 
   if (cmd->argc < 3) {
-    pr_response_add_err(R_500, _("'SITE %s' not understood"),
-      _get_full_cmd(cmd));
+    pr_response_add_err(R_500, _("'SITE %s' not understood"), full_cmd(cmd));
     return NULL;
   }
 
   /* Construct the target file name by concatenating all the parameters after
    * the mode, separating them with spaces.
    */
-  for (i = 2; i <= cmd->argc-1; i++)
-    arg = pstrcat(cmd->tmp_pool, arg, *arg ? " " : "",
-      pr_fs_decode_path(cmd->tmp_pool, cmd->argv[i]), NULL);
+  for (i = 2; i <= cmd->argc-1; i++) {
+    char *decoded_path;
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[i],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[i], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("SITE %s: Illegal character sequence in command"),
+        (char *) cmd->argv[1]);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    arg = pstrcat(cmd->tmp_pool, arg, *arg ? " " : "", decoded_path, NULL);
+  }
 
 #ifdef PR_USE_REGEX
   pre = get_param_ptr(CURRENT_CONF, "PathAllowFilter", FALSE);
   if (pre != NULL &&
       pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) != 0) {
-    pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-      arg);
+    pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+      (char *) cmd->argv[0], arg);
     pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   pre = get_param_ptr(CURRENT_CONF, "PathDenyFilter", FALSE);
   if (pre != NULL &&
       pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) == 0) {
-    pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-      arg);
+    pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+      (char *) cmd->argv[0], arg);
     pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 #endif
 
+  pr_fs_clear_cache2(arg);
+  if (pr_fsio_lstat(arg, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(cmd->tmp_pool, arg, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        arg = pstrdup(cmd->tmp_pool, link_path);
+      }
+    }
+  }
+
   path = dir_realpath(cmd->tmp_pool, arg);
+  if (path == NULL) {
+    int xerrno = errno;
+
+    pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
 
-  if (!path) {
-    pr_response_add_err(R_550, "%s: %s", arg, strerror(errno));
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
@@ -123,9 +166,15 @@ MODRET site_chgrp(cmd_rec *cmd) {
     /* Try the parameter as a group name. */
     gid = pr_auth_name2gid(cmd->tmp_pool, cmd->argv[1]);
     if (gid == (gid_t) -1) {
+      int xerrno = EINVAL;
+
       pr_log_debug(DEBUG9,
-        "SITE CHGRP: Unable to resolve group name '%s' to GID", cmd->argv[1]);
-      pr_response_add_err(R_550, "%s: %s", arg, strerror(EINVAL));
+        "SITE CHGRP: Unable to resolve group name '%s' to GID",
+        (char *) cmd->argv[1]);
+      pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
       return PR_ERROR(cmd);
     }
   }
@@ -134,35 +183,36 @@ MODRET site_chgrp(cmd_rec *cmd) {
   if (res < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error chown'ing '%s' to GID %lu: %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
-      path, (unsigned long) gid, strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error chown'ing '%s' to GID %s: %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), path,
+      pr_gid2str(cmd->tmp_pool, gid), strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
-
-  } else {
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[0]);
   }
 
+  pr_response_add(R_200, _("SITE %s command successful"),
+    (char *) cmd->argv[0]);
   return PR_HANDLED(cmd);
 }
 
 MODRET site_chmod(cmd_rec *cmd) {
   int res;
   mode_t mode = 0;
-  char *dir, *endp, *tmp, *arg = "";
+  char *dir, *endp, *mode_str, *tmp, *arg = "";
+  struct stat st;
   register unsigned int i = 0;
 #ifdef PR_USE_REGEX
   pr_regex_t *pre;
 #endif
 
   if (cmd->argc < 3) {
-    pr_response_add_err(R_500, _("'SITE %s' not understood"),
-      _get_full_cmd(cmd));
+    pr_response_add_err(R_500, _("'SITE %s' not understood"), full_cmd(cmd));
     return NULL;
   }
 
@@ -170,36 +220,76 @@ MODRET site_chmod(cmd_rec *cmd) {
    * the mode, separating them with spaces.
    */
   for (i = 2; i <= cmd->argc-1; i++) {
-    arg = pstrcat(cmd->tmp_pool, arg, *arg ? " " : "",
-      pr_fs_decode_path(cmd->tmp_pool, cmd->argv[i]), NULL);
+    char *decoded_path;
+
+    decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->argv[i],
+      FSIO_DECODE_FL_TELL_ERRORS);
+    if (decoded_path == NULL) {
+      int xerrno = errno;
+
+      pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s",
+        (char *) cmd->argv[i], strerror(xerrno));
+      pr_response_add_err(R_550,
+        _("SITE %s: Illegal character sequence in command"),
+        (char *) cmd->argv[1]);
+
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
+      return PR_ERROR(cmd);
+    }
+
+    arg = pstrcat(cmd->tmp_pool, arg, *arg ? " " : "", decoded_path, NULL);
   }
 
 #ifdef PR_USE_REGEX
   pre = get_param_ptr(CURRENT_CONF, "PathAllowFilter", FALSE);
   if (pre != NULL &&
       pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) != 0) {
-    pr_log_debug(DEBUG2, "'%s %s %s' denied by PathAllowFilter", cmd->argv[0],
-      cmd->argv[1], arg);
+    pr_log_debug(DEBUG2, "'%s %s %s' denied by PathAllowFilter",
+      (char *) cmd->argv[0], (char *) cmd->argv[1], arg);
     pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
   pre = get_param_ptr(CURRENT_CONF, "PathDenyFilter", FALSE);
   if (pre != NULL &&
       pr_regexp_exec(pre, arg, 0, NULL, 0, 0, 0) == 0) {
-    pr_log_debug(DEBUG2, "'%s %s %s' denied by PathDenyFilter", cmd->argv[0],
-      cmd->argv[1], arg);
+    pr_log_debug(DEBUG2, "'%s %s %s' denied by PathDenyFilter",
+      (char *) cmd->argv[0], (char *) cmd->argv[1], arg);
     pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 #endif
 
+  pr_fs_clear_cache2(arg);
+  if (pr_fsio_lstat(arg, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char link_path[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(link_path, '\0', sizeof(link_path));
+      len = dir_readlink(cmd->tmp_pool, arg, link_path, sizeof(link_path)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        link_path[len] = '\0';
+        arg = pstrdup(cmd->tmp_pool, link_path);
+      }
+    }
+  }
+
   dir = dir_realpath(cmd->tmp_pool, arg);
   if (dir == NULL) {
     int xerrno = errno;
 
     pr_response_add_err(R_550, "%s: %s", arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -208,39 +298,40 @@ MODRET site_chmod(cmd_rec *cmd) {
    * This will fail if the chmod is a symbolic, but takes care of the
    * case where an octal number is sent without the leading '0'.
    */
-
-  if (cmd->argv[1][0] != '0') {
-    tmp = pstrcat(cmd->tmp_pool, "0", cmd->argv[1], NULL);
+  mode_str = cmd->argv[1];
+  if (mode_str[0] != '0') {
+    tmp = pstrcat(cmd->tmp_pool, "0", mode_str, NULL);
 
   } else {
-    tmp = cmd->argv[1];
+    tmp = mode_str;
   }
 
-  mode = strtol(tmp,&endp,0);
+  mode = strtol(tmp, &endp, 0);
   if (endp && *endp) {
     /* It's not an absolute number, try symbolic */
-    char *cp = cmd->argv[1];
-    int mask = 0, mode_op = 0, curmode = 0, curumask = umask(0);
+    char *cp = mode_str;
+    int mask = 0, mode_op = 0, curr_mode = 0, curr_umask = umask(0);
     int invalid = 0;
     char *who, *how, *what;
-    struct stat st;
 
-    umask(curumask);
+    umask(curr_umask);
     mode = 0;
 
-    if (pr_fsio_stat(dir, &st) != -1)
-      curmode = st.st_mode;
+    if (pr_fsio_stat(dir, &st) != -1) {
+      curr_mode = st.st_mode;
+    }
 
     while (TRUE) {
-      who = pstrdup(cmd->tmp_pool, cp);
-
       pr_signals_handle();
 
+      who = pstrdup(cmd->tmp_pool, cp);
+
       tmp = strpbrk(who, "+-=");
       if (tmp != NULL) {
         how = pstrdup(cmd->tmp_pool, tmp);
-        if (*how != '=')
-          mode = curmode;
+        if (*how != '=') {
+          mode = curr_mode;
+        }
 
         *tmp = '\0';
 
@@ -279,7 +370,7 @@ MODRET site_chmod(cmd_rec *cmd) {
             break;
 
           case '\0':
-            mask = curumask;
+            mask = curr_umask;
             break;
 
           default:
@@ -311,6 +402,7 @@ MODRET site_chmod(cmd_rec *cmd) {
           case 'w':
             mode_op |= (S_IWUSR|S_IWGRP|S_IWOTH);
             break;
+
           case 'x':
             mode_op |= (S_IXUSR|S_IXGRP|S_IXOTH);
             break;
@@ -327,21 +419,21 @@ MODRET site_chmod(cmd_rec *cmd) {
             break;
 
           case 'o':
-            mode_op |= curmode & S_IRWXO;
-            mode_op |= (curmode & S_IRWXO) << 3;
-            mode_op |= (curmode & S_IRWXO) << 6;
+            mode_op |= (curr_mode & S_IRWXO);
+            mode_op |= ((curr_mode & S_IRWXO) << 3);
+            mode_op |= ((curr_mode & S_IRWXO) << 6);
             break;
 
           case 'g':
-            mode_op |= (curmode & S_IRWXG) >> 3;
-            mode_op |= curmode & S_IRWXG;
-            mode_op |= (curmode & S_IRWXG) << 3;
+            mode_op |= ((curr_mode & S_IRWXG) >> 3);
+            mode_op |= (curr_mode & S_IRWXG);
+            mode_op |= ((curr_mode & S_IRWXG) << 3);
             break;
 
           case 'u':
-            mode_op |= (curmode & S_IRWXO) >> 6;
-            mode_op |= (curmode & S_IRWXO) >> 3;
-            mode_op |= curmode & S_IRWXU;
+            mode_op |= ((curr_mode & S_IRWXU) >> 6);
+            mode_op |= ((curr_mode & S_IRWXU) >> 3);
+            mode_op |= (curr_mode & S_IRWXU);
             break;
 
           case '\0':
@@ -363,26 +455,31 @@ MODRET site_chmod(cmd_rec *cmd) {
               cp = what;
               continue;
 
-            } else
+            } else {
               cp = NULL;
-
+            }
             break;
 
           default:
             invalid++;
         }
 
-        if (invalid)
+        if (invalid) {
           break;
+        }
 
-        if (cp)
+        if (cp) {
           cp++;
+        }
       }
       break;
     }
 
     if (invalid) {
-      pr_response_add_err(R_550, _("'%s': invalid mode"), cmd->argv[1]);
+      pr_response_add_err(R_550, _("'%s': invalid mode"), (char *) cmd->argv[1]);
+
+      pr_cmd_set_errno(cmd, EINVAL);
+      errno = EINVAL;
       return PR_ERROR(cmd);
     }
   }
@@ -391,20 +488,21 @@ MODRET site_chmod(cmd_rec *cmd) {
   if (res < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error chmod'ing '%s' to %04o: %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
-      dir, (unsigned int) mode, strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error chmod'ing '%s' to %04o: %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), dir, (unsigned int) mode,
+      strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
-
-  } else {
-    pr_response_add(R_200, _("SITE %s command successful"), cmd->argv[0]);
   }
 
+  pr_response_add(R_200, _("SITE %s command successful"),
+    (char *) cmd->argv[0]);
   return PR_HANDLED(cmd);
 }
 
@@ -423,27 +521,34 @@ MODRET site_help(cmd_rec *cmd) {
         strcasecmp(cmd->argv[1], "SITE") == 0)))) {
 
     for (i = 0; _help[i].cmd; i++) {
-      if (_help[i].implemented)
+      if (_help[i].implemented) {
         pr_response_add(i != 0 ? R_DUP : R_214, "%s", _help[i].cmd);
-      else
+
+      } else {
         pr_response_add(i != 0 ? R_DUP : R_214, "%s",
           pstrcat(cmd->pool, _help[i].cmd, "*", NULL));
+      }
     }
 
   } else {
-    char *cp = NULL;
+    char *arg, *cp = NULL;
 
-    for (cp = cmd->argv[1]; *cp; cp++)
+    arg = cmd->argv[1];
+    for (cp = arg; *cp; cp++) {
       *cp = toupper(*cp);
+    }
 
-    for (i = 0; _help[i].cmd; i++)
-      if (strcasecmp(cmd->argv[1], _help[i].cmd) == 0) {
+    for (i = 0; _help[i].cmd; i++) {
+      if (strcasecmp(arg, _help[i].cmd) == 0) {
         pr_response_add(R_214, _("Syntax: SITE %s %s"),
-          cmd->argv[1], _help[i].syntax);
+          (char *) cmd->argv[1], _help[i].syntax);
         return PR_HANDLED(cmd);
       }
+    }
 
     pr_response_add_err(R_502, _("Unknown command 'SITE %s'"), cmd->arg);
+    pr_cmd_set_errno(cmd, ENOSYS);
+    errno = ENOSYS;
     return PR_ERROR(cmd);
   }
 
@@ -466,21 +571,32 @@ modret_t *site_dispatch(cmd_rec *cmd) {
 
   if (!cmd->argc) {
     pr_response_add_err(R_500, _("'SITE' requires parameters"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  for (i = 0; site_commands[i].command; i++)
+  for (i = 0; site_commands[i].command; i++) {
     if (strcmp(cmd->argv[0], site_commands[i].command) == 0) {
       if (site_commands[i].requires_auth && cmd_auth_chk &&
           !cmd_auth_chk(cmd)) {
         pr_response_send(R_530, _("Please login with USER and PASS"));
+
+        pr_cmd_set_errno(cmd, EPERM);
+        errno = EPERM;
         return PR_ERROR(cmd);
+      }
 
-      } else
-        return site_commands[i].handler(cmd);
+      return site_commands[i].handler(cmd);
     }
+  }
+
+  pr_response_add_err(R_500, _("'SITE %s' not understood"),
+    (char *) cmd->argv[0]);
 
-  pr_response_add_err(R_500, _("'SITE %s' not understood"), cmd->argv[0]);
+  pr_cmd_set_errno(cmd, EINVAL);
+  errno = EINVAL;
   return PR_ERROR(cmd);
 }
 
diff --git a/modules/mod_xfer.c b/modules/mod_xfer.c
index de5c52a..e569888 100644
--- a/modules/mod_xfer.c
+++ b/modules/mod_xfer.c
@@ -52,6 +52,7 @@ static pr_fh_t *stor_fh = NULL;
 static pr_fh_t *displayfilexfer_fh = NULL;
 
 static unsigned char have_rfc2228_data = FALSE;
+static unsigned char have_type = FALSE;
 static unsigned char have_zmode = FALSE;
 static unsigned char use_sendfile = TRUE;
 static off_t use_sendfile_len = 0;
@@ -61,6 +62,7 @@ static int xfer_check_limit(cmd_rec *);
 
 /* TransferOptions */
 #define PR_XFER_OPT_HANDLE_ALLO		0x0001
+#define PR_XFER_OPT_IGNORE_ASCII	0x0002
 static unsigned long xfer_opts = PR_XFER_OPT_HANDLE_ALLO;
 
 /* Transfer priority */
@@ -74,7 +76,8 @@ static unsigned long xfer_prio_flags = 0;
 
 static void xfer_exit_ev(const void *, void *);
 static void xfer_sigusr2_ev(const void *, void *);
-static void xfer_xfer_stalled_ev(const void *, void *);
+static void xfer_timeout_session_ev(const void *, void *);
+static void xfer_timeout_stalled_ev(const void *, void *);
 static int xfer_sess_init(void);
 
 static int xfer_prio_adjust(void);
@@ -173,7 +176,7 @@ static off_t find_max_nbytes(char *directive) {
   }
 
   /* Print out some nice debugging information. */
-  if (max_nbytes > 0UL &&
+  if (max_nbytes > 0 &&
       (have_user_limit || have_group_limit ||
        have_class_limit || have_all_limit)) {
     pr_log_debug(DEBUG5, "%s (%" PR_LU " bytes) in effect for %s",
@@ -429,8 +432,7 @@ static int xfer_check_limit(cmd_rec *cmd) {
   return 0;
 }
 
-static int xfer_displayfile(void) {
-  int res = -1;
+static void xfer_displayfile(void) {
 
   if (displayfilexfer_fh) {
     if (pr_display_fh(displayfilexfer_fh, session.vwd, R_226, 0) < 0) {
@@ -444,26 +446,22 @@ static int xfer_displayfile(void) {
         "file '%s': %s", displayfilexfer_fh->fh_path, strerror(errno));
     }
 
-    res = 0;
-
   } else {
-    char *displayfilexfer = get_param_ptr(main_server->conf,
-      "DisplayFileTransfer", FALSE);
+    char *displayfilexfer;
+
+    displayfilexfer = get_param_ptr(main_server->conf, "DisplayFileTransfer",
+      FALSE);
     if (displayfilexfer) {
       if (pr_display_file(displayfilexfer, session.vwd, R_226, 0) < 0) {
         pr_log_debug(DEBUG6, "unable to display DisplayFileTransfer "
           "file '%s': %s", displayfilexfer, strerror(errno));
       }
-
-      res = 0;
     }
   }
-
-  return res;
 }
 
 static int xfer_prio_adjust(void) {
-  int res;
+  int res, xerrno = 0;
 
   if (xfer_prio_config == 0) {
     return 0;
@@ -473,8 +471,11 @@ static int xfer_prio_adjust(void) {
   res = getpriority(PRIO_PROCESS, 0);
   if (res < 0 &&
       errno != 0) {
+    xerrno = errno;
+
     pr_trace_msg(trace_channel, 7, "unable to get current process priority: %s",
-      strerror(errno));
+      strerror(xerrno));
+    errno = xerrno;
     return -1;
   }
 
@@ -483,27 +484,33 @@ static int xfer_prio_adjust(void) {
    */
   if (res == xfer_prio_config) {
     pr_trace_msg(trace_channel, 10,
-      "current process priority matches configured priority");
+      "current process priority (%d) matches configured priority", res);
     return 0;
   }
 
   xfer_prio_curr = res;
 
-  pr_trace_msg(trace_channel, 10, "adjusting process priority to be %d",
-    xfer_prio_config);
+  pr_trace_msg(trace_channel, 10,
+    "adjusting process priority to be %d (currently %d)", xfer_prio_config,
+    xfer_prio_curr);
+
   if (xfer_prio_config > 0) {
     res = setpriority(PRIO_PROCESS, 0, xfer_prio_config);
+    xerrno = errno;
 
   } else {
     /* Increasing the process priority requires root privs. */
     PRIVS_ROOT
     res = setpriority(PRIO_PROCESS, 0, xfer_prio_config);
+    xerrno = errno;
     PRIVS_RELINQUISH
   }
 
   if (res < 0) {
     pr_trace_msg(trace_channel, 1, "error adjusting process priority: %s",
-      strerror(errno));
+      strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
@@ -511,7 +518,7 @@ static int xfer_prio_adjust(void) {
 }
 
 static int xfer_prio_restore(void) {
-  int res;
+  int res, xerrno;
 
   if (xfer_prio_config == 0 ||
       xfer_prio_curr == 0) {
@@ -523,11 +530,12 @@ static int xfer_prio_restore(void) {
 
   PRIVS_ROOT
   res = setpriority(PRIO_PROCESS, 0, xfer_prio_curr);
+  xerrno = errno;
   PRIVS_RELINQUISH
 
   if (res < 0) {
     pr_trace_msg(trace_channel, 1, "error restoring process priority: %s",
-      strerror(errno));
+      strerror(xerrno));
   }
 
   xfer_prio_curr = 0;
@@ -569,15 +577,15 @@ static int xfer_parse_cmdlist(const char *name, config_rec *c,
   return 0;
 }
 
-static int transmit_normal(char *buf, long bufsz) {
+static int transmit_normal(pool *p, char *buf, size_t bufsz) {
   long sz = pr_fsio_read(retr_fh, buf, bufsz);
 
   if (sz < 0) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "RETR, user '%s' (UID %lu, GID %lu): "
+    (void) pr_trace_msg("fileperms", 1, "RETR, user '%s' (UID %s, GID %s): "
       "error reading from '%s': %s", session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
+      pr_uid2str(p, session.uid), pr_gid2str(p, session.gid),
       retr_fh->fh_path, strerror(xerrno));
 
     errno = xerrno;
@@ -727,8 +735,8 @@ static int transmit_sendfile(off_t data_len, off_t *data_offset,
  * transmit_sendfile(), if sendfile support is enabled.  The transmit_normal()
  * function only needs/uses buf and bufsz.
  */
-static long transmit_data(off_t data_len, off_t *data_offset, char *buf,
-    long bufsz) {
+static long transmit_data(pool *p, off_t data_len, off_t *data_offset,
+    char *buf, size_t bufsz) {
   long res;
   int xerrno = 0;
 
@@ -752,7 +760,7 @@ static long transmit_data(off_t data_len, off_t *data_offset, char *buf,
     /* sendfile() should not be used for some reason, fallback to using
      * normal data transmission methods.
      */
-    res = transmit_normal(buf, bufsz);
+    res = transmit_normal(p, buf, bufsz);
     xerrno = errno;
 
   } else {
@@ -764,7 +772,7 @@ static long transmit_data(off_t data_len, off_t *data_offset, char *buf,
     pr_log_debug(DEBUG10, "use of sendfile(2) failed due to %s (%d), "
       "falling back to normal data transmission", strerror(errno),
       errno);
-    res = transmit_normal(buf, bufsz);
+    res = transmit_normal(p, buf, bufsz);
     xerrno = errno;
 
 # else
@@ -778,7 +786,7 @@ static long transmit_data(off_t data_len, off_t *data_offset, char *buf,
   }
 
 #else
-  res = transmit_normal(buf, bufsz);
+  res = transmit_normal(p, buf, bufsz);
   xerrno = errno;
 #endif /* HAVE_SENDFILE */
 
@@ -787,8 +795,10 @@ static long transmit_data(off_t data_len, off_t *data_offset, char *buf,
      * client aborts the transfer, thus we need to check for this.
      */
     if (pr_inet_set_proto_cork(PR_NETIO_FD(session.d->outstrm), 0) < 0) {
-      pr_log_pri(PR_LOG_NOTICE, "error uncorking socket fd %d: %s",
-        PR_NETIO_FD(session.d->outstrm), strerror(errno));
+      if (errno != EINVAL) {
+        pr_log_pri(PR_LOG_NOTICE, "error uncorking socket fd %d: %s",
+          PR_NETIO_FD(session.d->outstrm), strerror(errno));
+      }
     }
   }
 
@@ -796,9 +806,9 @@ static long transmit_data(off_t data_len, off_t *data_offset, char *buf,
   return res;
 }
 
-static void stor_chown(void) {
+static void stor_chown(pool *p) {
   struct stat st;
-  char *xfer_path = NULL;
+  const char *xfer_path = NULL;
 
   if (session.xfer.xfer_type == STOR_HIDDEN) {
     xfer_path = session.xfer.path_hidden;
@@ -825,17 +835,21 @@ static void stor_chown(void) {
 
     } else {
       if (session.fsgid != (gid_t) -1) {
-        pr_log_debug(DEBUG2, "root lchown(%s) to uid %lu, gid %lu successful",
-          xfer_path, (unsigned long) session.fsuid,
-          (unsigned long) session.fsgid);
+        pr_log_debug(DEBUG2, "root lchown(%s) to UID %s, GID %s successful",
+          xfer_path, pr_uid2str(p, session.fsuid),
+          pr_gid2str(p, session.fsgid));
 
       } else {
-        pr_log_debug(DEBUG2, "root lchown(%s) to uid %lu successful", xfer_path,
-          (unsigned long) session.fsuid);
+        pr_log_debug(DEBUG2, "root lchown(%s) to UID %s successful", xfer_path,
+          pr_uid2str(p, session.fsuid));
       }
 
-      pr_fs_clear_cache();
-      pr_fsio_stat(xfer_path, &st);
+      pr_fs_clear_cache2(xfer_path);
+      if (pr_fsio_stat(xfer_path, &st) < 0) {
+        pr_log_debug(DEBUG0,
+          "'%s' stat(2) error during root chmod: %s", xfer_path,
+          strerror(errno));
+      }
 
       /* The chmod happens after the chown because chown will remove
        * the S{U,G}ID bits on some files (namely, directories); the subsequent
@@ -893,12 +907,16 @@ static void stor_chown(void) {
         use_root_privs ? "root " : "", xfer_path, strerror(xerrno));
 
     } else {
-      pr_log_debug(DEBUG2, "%slchown(%s) to gid %lu successful",
+      pr_log_debug(DEBUG2, "%slchown(%s) to GID %s successful",
         use_root_privs ? "root " : "", xfer_path,
-        (unsigned long) session.fsgid);
+        pr_gid2str(p, session.fsgid));
 
-      pr_fs_clear_cache();
-      pr_fsio_stat(xfer_path, &st);
+      pr_fs_clear_cache2(xfer_path);
+      if (pr_fsio_stat(xfer_path, &st) < 0) {
+        pr_log_debug(DEBUG0,
+          "'%s' stat(2) error during %schmod: %s", xfer_path,
+          use_root_privs ? "root " : "", strerror(errno));
+      }
 
       if (use_root_privs) {
         PRIVS_ROOT
@@ -952,8 +970,9 @@ static void stor_abort(void) {
     stor_fh = NULL;
   }
 
+  delete_stores = get_param_ptr(CURRENT_CONF, "DeleteAbortedStores", FALSE);
+
   if (session.xfer.xfer_type == STOR_HIDDEN) {
-    delete_stores = get_param_ptr(CURRENT_CONF, "DeleteAbortedStores", FALSE);
     if (delete_stores == NULL ||
         *delete_stores == TRUE) {
       /* If a hidden store was aborted, remove only hidden file, not real
@@ -962,16 +981,24 @@ static void stor_abort(void) {
       if (session.xfer.path_hidden) {
         pr_log_debug(DEBUG5, "removing aborted HiddenStores file '%s'",
           session.xfer.path_hidden);
-        pr_fsio_unlink(session.xfer.path_hidden);
+        if (pr_fsio_unlink(session.xfer.path_hidden) < 0) {
+          if (errno != ENOENT) {
+            pr_log_debug(DEBUG0, "error deleting HiddenStores file '%s': %s",
+              session.xfer.path_hidden, strerror(errno));
+          }
+        } 
       }
     }
+  }
 
-  } else if (session.xfer.path) {
-    delete_stores = get_param_ptr(CURRENT_CONF, "DeleteAbortedStores", FALSE);
+  if (session.xfer.path) {
     if (delete_stores != NULL &&
         *delete_stores == TRUE) {
       pr_log_debug(DEBUG5, "removing aborted file '%s'", session.xfer.path);
-      pr_fsio_unlink(session.xfer.path);
+      if (pr_fsio_unlink(session.xfer.path) < 0) {
+        pr_log_debug(DEBUG0, "error deleting aborted file '%s': %s",
+          session.xfer.path, strerror(errno));
+      }
     }
   }
 
@@ -994,7 +1021,12 @@ static int stor_complete(void) {
       if (session.xfer.path_hidden) {
         pr_log_debug(DEBUG5, "failed to close HiddenStores file '%s', removing",
           session.xfer.path_hidden);
-        pr_fsio_unlink(session.xfer.path_hidden);
+        if (pr_fsio_unlink(session.xfer.path_hidden) < 0) {
+          if (errno != ENOENT) {
+            pr_log_debug(DEBUG0, "error deleting HiddenStores file '%s': %s",
+              session.xfer.path_hidden, strerror(errno));
+          }
+        } 
       }
     }
 
@@ -1006,9 +1038,10 @@ static int stor_complete(void) {
   return res;
 }
 
-static int get_hidden_store_path(cmd_rec *cmd, char *path, char *prefix,
-    char *suffix) {
-  char *c = NULL, *hidden_path, *parent_dir = NULL;
+static int get_hidden_store_path(cmd_rec *cmd, const char *path,
+    const char *prefix, const char *suffix) {
+  const char *c = NULL;
+  char *hidden_path, *parent_dir = NULL;
   int dotcount = 0, found_slash = FALSE, basenamestart = 0, maxlen;
 
   /* We have to also figure out the temporary hidden file name for receiving
@@ -1054,6 +1087,7 @@ static int get_hidden_store_path(cmd_rec *cmd, char *path, char *prefix,
 
     /* This probably shouldn't happen */
     pr_response_add_err(R_451, _("%s: Bad file name"), path);
+    errno = EINVAL;
     return -1;
   }
 
@@ -1070,6 +1104,7 @@ static int get_hidden_store_path(cmd_rec *cmd, char *path, char *prefix,
 
     /* This probably shouldn't happen */
     pr_response_add_err(R_451, _("%s: File name too long"), path);
+    errno = EPERM;
     return -1;
   }
 
@@ -1102,7 +1137,8 @@ static int get_hidden_store_path(cmd_rec *cmd, char *path, char *prefix,
       hidden_path, path);
   }
 
-  if (file_mode(hidden_path)) {
+  pr_fs_clear_cache2(hidden_path);
+  if (file_mode2(cmd->tmp_pool, hidden_path)) {
     session.xfer.xfer_type = STOR_DEFAULT;
 
     pr_log_debug(DEBUG3, "HiddenStore path '%s' already exists",
@@ -1110,6 +1146,7 @@ static int get_hidden_store_path(cmd_rec *cmd, char *path, char *prefix,
 
     pr_response_add_err(R_550, _("%s: Temporary hidden file %s already exists"),
       cmd->arg, hidden_path);
+    errno = EEXIST;
     return -1;
   }
 
@@ -1126,9 +1163,6 @@ static int get_hidden_store_path(cmd_rec *cmd, char *path, char *prefix,
   if (found_slash == FALSE) {
     parent_dir = "./";
 
-  } else if (basenamestart == 0) {
-    parent_dir = "/";
-
   } else {
     parent_dir = pstrndup(cmd->tmp_pool, path, basenamestart);
   }
@@ -1178,30 +1212,64 @@ MODRET xfer_post_mode(cmd_rec *cmd) {
  * the duration of this function.
  */
 MODRET xfer_pre_stor(cmd_rec *cmd) {
-  char *path;
+  char *decoded_path, *path;
   mode_t fmode;
   unsigned char *allow_overwrite = NULL, *allow_restart = NULL;
   config_rec *c;
   int res;
+  struct stat st;
 
   if (cmd->argc < 2) {
     pr_response_add_err(R_500, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  path = dir_best_path(cmd->tmp_pool,
-    pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  pr_fs_clear_cache2(decoded_path);
+  if (pr_fsio_lstat(decoded_path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char buf[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(buf, '\0', sizeof(buf));
+      len = dir_readlink(cmd->tmp_pool, decoded_path, buf, sizeof(buf)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        buf[len] = '\0';
+        decoded_path = pstrdup(cmd->tmp_pool, buf);
+      }
+    }
+  }
+
+  path = dir_best_path(cmd->tmp_pool, decoded_path);
 
-  if (!path ||
+  if (path == NULL ||
       !dir_check(cmd->tmp_pool, cmd, cmd->group, path, NULL)) {
     int xerrno = errno;
 
-    pr_log_debug(DEBUG8, "%s %s denied by <Limit> configuration", cmd->argv[0],
-      cmd->arg);
+    pr_log_debug(DEBUG8, "%s %s denied by <Limit> configuration",
+      (char *) cmd->argv[0], cmd->arg);
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -1212,27 +1280,33 @@ MODRET xfer_pre_stor(cmd_rec *cmd) {
       break;
 
     case PR_FILTER_ERR_FAILS_ALLOW_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathAllowFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
 
     case PR_FILTER_ERR_FAILS_DENY_FILTER:
-      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter", cmd->argv[0],
-        path);
+      pr_log_debug(DEBUG2, "'%s %s' denied by PathDenyFilter",
+        (char *) cmd->argv[0], path);
       pr_response_add_err(R_550, _("%s: Forbidden filename"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EPERM);
       errno = EPERM;
       return PR_ERROR(cmd);
   }
 
   if (xfer_check_limit(cmd) < 0) {
     pr_response_add_err(R_451, _("%s: Too many transfers"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
 
-  fmode = file_mode(path);
+  fmode = file_mode2(cmd->tmp_pool, path);
 
   allow_overwrite = get_param_ptr(CURRENT_CONF, "AllowOverwrite", FALSE);
 
@@ -1240,6 +1314,8 @@ MODRET xfer_pre_stor(cmd_rec *cmd) {
       (!allow_overwrite || *allow_overwrite == FALSE)) {
     pr_log_debug(DEBUG6, "AllowOverwrite denied permission for %s", cmd->arg);
     pr_response_add_err(R_550, _("%s: Overwrite permission denied"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EACCES);
     errno = EACCES;
     return PR_ERROR(cmd);
   }
@@ -1263,6 +1339,7 @@ MODRET xfer_pre_stor(cmd_rec *cmd) {
       pr_response_add_err(R_550, _("%s: Not a regular file"), cmd->arg);
 
       /* Deliberately use EISDIR for anything non-file (e.g. directories). */
+      pr_cmd_set_errno(cmd, EISDIR);
       errno = EISDIR;
       return PR_ERROR(cmd);
     }
@@ -1281,6 +1358,8 @@ MODRET xfer_pre_stor(cmd_rec *cmd) {
       cmd->arg);
     session.restart_pos = 0L;
     session.xfer.xfer_type = STOR_DEFAULT;
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -1312,31 +1391,60 @@ MODRET xfer_pre_stor(cmd_rec *cmd) {
   c = find_config(CURRENT_CONF, CONF_PARAM, "HiddenStores", FALSE);
   if (c &&
       *((int *) c->argv[0]) == TRUE) {
-    char *prefix, *suffix;
-
-    prefix = c->argv[1];
-    suffix = c->argv[2];
+    const char *prefix, *suffix;
 
     /* If we're using HiddenStores, then REST won't work. */
     if (session.restart_pos) {
       pr_log_debug(DEBUG9, "HiddenStore in effect, refusing restarted upload");
       pr_response_add_err(R_501,
         _("REST not compatible with server configuration"));
-      errno = EINVAL;
-      return PR_ERROR(cmd);
-    }
 
-    /* APPE is not compatible with HiddenStores either (Bug#3598). */
-    if (session.xfer.xfer_type == STOR_APPEND) {
-      pr_log_debug(DEBUG9, "HiddenStore in effect, refusing APPE upload");
-      pr_response_add_err(R_550,
-        _("APPE not compatible with server configuration"));
-      errno = EINVAL;
+      pr_cmd_set_errno(cmd, EPERM);
+      errno = EPERM;
       return PR_ERROR(cmd);
     }
 
-    if (get_hidden_store_path(cmd, path, prefix, suffix) < 0) {
-      return PR_ERROR(cmd);
+    /* For Bug#3598, we rejected any APPE command when HiddenStores are in
+     * effect (for good reasons).
+     *
+     * However, for Bug#4144, we're relaxing that policy.  Instead of rejecting
+     * the APPE command, we accept that command, but we disable the HiddenStores
+     * functionality.
+     */
+    if (session.xfer.xfer_type != STOR_APPEND) {
+      prefix = c->argv[1];
+      suffix = c->argv[2];
+
+      /* Substitute the %P variable for the PID, if present. */
+      if (strstr(prefix, "%P") != NULL) {
+        char pid_buf[32];
+
+        memset(pid_buf, '\0', sizeof(pid_buf));
+        snprintf(pid_buf, sizeof(pid_buf)-1, "%lu",
+          (unsigned long) session.pid);
+        prefix = sreplace(cmd->pool, prefix, "%P", pid_buf, NULL);
+      }
+
+      if (strstr(suffix, "%P") != NULL) {
+        char pid_buf[32];
+
+        memset(pid_buf, '\0', sizeof(pid_buf));
+        snprintf(pid_buf, sizeof(pid_buf)-1, "%lu",
+          (unsigned long) session.pid);
+        suffix = sreplace(cmd->pool, suffix, "%P", pid_buf, NULL);
+      }
+
+      if (get_hidden_store_path(cmd, path, prefix, suffix) < 0) {
+        int xerrno = errno;
+
+        pr_cmd_set_errno(cmd, xerrno);
+        errno = xerrno;
+        return PR_ERROR(cmd);
+      }
+
+    } else {
+      pr_log_debug(DEBUG9,
+        "HiddenStores in effect for APPE, ignoring HiddenStores");
     }
   }
 
@@ -1365,11 +1473,17 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
   if (cmd->argc > 2) {
     pr_response_add_err(R_500, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   if (xfer_check_limit(cmd) < 0) {
     pr_response_add_err(R_451, _("%s: Too many transfers"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -1379,6 +1493,9 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
    */
   if (session.restart_pos) {
     pr_response_add_err(R_550, _("STOU incompatible with REST"));
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -1404,23 +1521,22 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
 
     /* If we can't guarantee a unique filename, refuse the command. */
     pr_response_add_err(R_450, _("%s: unable to generate unique filename"),
-      cmd->argv[0]);
+      (char *) cmd->argv[0]);
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
+  }
 
-  } else {
-    cmd->arg = filename;
+  cmd->arg = filename;
 
-    /* Close the unique file.  This introduces a small race condition
-     * between the time this function returns, and the STOU CMD handler
-     * opens the unique file, but this may have to do, as closing that
-     * race would involve some major restructuring.
-     */
-    (void) close(stou_fd);
-  }
+  /* Close the unique file.  This introduces a small race condition
+   * between the time this function returns, and the STOU CMD handler
+   * opens the unique file, but this may have to do, as closing that
+   * race would involve some major restructuring.
+   */
+  (void) close(stou_fd);
 
-  /* It's OK to reuse the char * pointer for filename. */
   filename = dir_best_path(cmd->tmp_pool, cmd->arg);
 
   if (filename == NULL ||
@@ -1433,11 +1549,13 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
     (void) pr_fsio_unlink(cmd->arg);
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  mode = file_mode(filename);
+  mode = file_mode2(cmd->tmp_pool, filename);
 
   /* Note: this case should never happen: how one can be appending to
    * a supposedly unique filename?  Should probably be removed...
@@ -1448,6 +1566,9 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
       (!allow_overwrite || *allow_overwrite == FALSE)) {
     pr_log_debug(DEBUG6, "AllowOverwrite denied permission for %s", cmd->arg);
     pr_response_add_err(R_550, _("%s: Overwrite permission denied"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EACCES);
+    errno = EACCES;
     return PR_ERROR(cmd);
   }
 
@@ -1458,6 +1579,7 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
     pr_response_add_err(R_550, _("%s: Not a regular file"), cmd->arg);
 
     /* Deliberately use EISDIR for anything non-file (e.g. directories). */
+    pr_cmd_set_errno(cmd, EISDIR);
     errno = EISDIR;
     return PR_ERROR(cmd);
   }
@@ -1477,20 +1599,41 @@ MODRET xfer_pre_stou(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+MODRET xfer_post_stor(cmd_rec *cmd) {
+  const char *path;
+
+  path = pr_table_get(cmd->notes, "mod_xfer.store-path", NULL);
+  if (path != NULL) {
+    struct stat st;
+
+    if (pr_fsio_stat(path, &st) == 0) {
+      off_t *file_size;
+
+      file_size = palloc(cmd->pool, sizeof(off_t));
+      *file_size = st.st_size;
+      (void) pr_table_add(cmd->notes, "mod_xfer.file-size", file_size,
+        sizeof(off_t));
+    }
+  }
+
+  return PR_DECLINED(cmd);
+}
+
 /* xfer_post_stou() is a POST_CMD handler that changes the mode of the
  * STOU file from 0600, which is what mkstemp() makes it, to 0666 (modulo
  * Umask), the default for files uploaded via STOR.  This is to prevent users
  * from being surprised.
  */
 MODRET xfer_post_stou(cmd_rec *cmd) {
-  mode_t mask, perms, *umask;
+  mode_t mask, perms, *umask_setting;
+  struct stat st;
 
   /* mkstemp(3) creates a file with 0600 perms; we need to adjust this
    * for the Umask (Bug#4223).
    */
-  umask = get_param_ptr(CURRENT_CONF, "Umask", FALSE);
-  if (umask != NULL) {
-    mask = *umask;
+  umask_setting = get_param_ptr(CURRENT_CONF, "Umask", FALSE);
+  if (umask_setting != NULL) {
+    mask = *umask_setting;
 
   } else {
     mask = (mode_t) 0022;
@@ -1504,6 +1647,15 @@ MODRET xfer_post_stou(cmd_rec *cmd) {
       cmd->arg, perms, strerror(errno));
   }
 
+  if (pr_fsio_stat(cmd->arg, &st) == 0) {
+    off_t *file_size;
+
+    file_size = palloc(cmd->pool, sizeof(off_t));
+    *file_size = st.st_size;
+    (void) pr_table_add(cmd->notes, "mod_xfer.file-size", file_size,
+      sizeof(off_t));
+  }
+
   return PR_DECLINED(cmd);
 }
 
@@ -1515,6 +1667,8 @@ MODRET xfer_pre_appe(cmd_rec *cmd) {
 
   if (xfer_check_limit(cmd) < 0) {
     pr_response_add_err(R_451, _("%s: Too many transfers"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -1524,13 +1678,13 @@ MODRET xfer_pre_appe(cmd_rec *cmd) {
 }
 
 MODRET xfer_stor(cmd_rec *cmd) {
-  char *path;
+  const char *path;
   char *lbuf;
-  int bufsz, len, ferrno = 0, res;
+  int bufsz, len, xerrno = 0, res;
   off_t nbytes_stored, nbytes_max_store = 0;
   unsigned char have_limit = FALSE;
   struct stat st;
-  off_t curr_pos = 0;
+  off_t curr_offset, curr_pos = 0;
 
   memset(&st, 0, sizeof(st));
 
@@ -1549,7 +1703,7 @@ MODRET xfer_stor(cmd_rec *cmd) {
   pr_fs_setcwd(pr_fs_getcwd());
 
   if (session.xfer.xfer_type == STOR_HIDDEN) {
-    void *nfs;
+    const void *nfs;
     int oflags;
 
     oflags = O_WRONLY;
@@ -1573,18 +1727,40 @@ MODRET xfer_stor(cmd_rec *cmd) {
 
     stor_fh = pr_fsio_open(session.xfer.path_hidden, oflags);
     if (stor_fh == NULL) {
-      ferrno = errno;
+      xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error opening '%s': %s", cmd->argv[0], session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
-        session.xfer.path_hidden, strerror(ferrno));
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error opening '%s': %s", (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), session.xfer.path_hidden,
+        strerror(xerrno));
     }
 
   } else if (session.xfer.xfer_type == STOR_APPEND) {
-    stor_fh = pr_fsio_open(session.xfer.path, O_CREAT|O_WRONLY);
+    const char *appe_path;
+
+    /* Need to handle the case where the path may be a symlink, and we are
+     * chrooted (Bug#4219).
+     */
+    appe_path = session.xfer.path;
+
+    pr_fs_clear_cache2(appe_path);
+    if (pr_fsio_lstat(appe_path, &st) == 0) {
+      if (S_ISLNK(st.st_mode)) {
+        char buf[PR_TUNABLE_PATH_MAX];
+
+        memset(buf, '\0', sizeof(buf));
+        len = dir_readlink(cmd->tmp_pool, appe_path, buf, sizeof(buf)-1,
+          PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+        if (len > 0) {
+          buf[len] = '\0';
+          appe_path = pstrdup(cmd->pool, buf);
+        }
+      }
+    }
 
-    if (stor_fh) {
+    stor_fh = pr_fsio_open(appe_path, O_CREAT|O_WRONLY);
+    if (stor_fh != NULL) {
       if (pr_fsio_lseek(stor_fh, 0, SEEK_END) == (off_t) -1) {
         pr_log_debug(DEBUG4, "unable to seek to end of '%s' for appending: %s",
           cmd->arg, strerror(errno));
@@ -1593,12 +1769,12 @@ MODRET xfer_stor(cmd_rec *cmd) {
       }
 
     } else {
-      ferrno = errno;
+      xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error opening '%s': %s", cmd->argv[0], session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
-        session.xfer.path, strerror(ferrno));
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error opening '%s': %s", (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), appe_path, strerror(xerrno));
     }
 
   } else {
@@ -1606,25 +1782,26 @@ MODRET xfer_stor(cmd_rec *cmd) {
     stor_fh = pr_fsio_open(path,
         O_WRONLY|(session.restart_pos ? 0 : O_TRUNC|O_CREAT));
     if (stor_fh == NULL) {
-      ferrno = errno;
+      xerrno = errno;
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error opening '%s': %s", cmd->argv[0], session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid, path,
-        strerror(ferrno));
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error opening '%s': %s", (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), path, strerror(xerrno));
     }
   }
 
   if (stor_fh != NULL &&
       session.restart_pos) {
-    int xerrno = 0;
+    xerrno = 0;
 
+    pr_fs_clear_cache2(path);
     if (pr_fsio_lseek(stor_fh, session.restart_pos, SEEK_SET) == -1) {
       pr_log_debug(DEBUG4, "unable to seek to position %" PR_LU " of '%s': %s",
         (pr_off_t) session.restart_pos, cmd->arg, strerror(errno));
       xerrno = errno;
 
-    } else if (pr_fsio_stat(path, &st) == -1) {
+    } else if (pr_fsio_stat(path, &st) < 0) {
       pr_log_debug(DEBUG4, "unable to stat '%s': %s", cmd->arg,
         strerror(errno));
       xerrno = errno;
@@ -1644,6 +1821,9 @@ MODRET xfer_stor(cmd_rec *cmd) {
       pr_response_add_err(R_554, _("%s: invalid REST argument"), cmd->arg);
       (void) pr_fsio_close(stor_fh);
       stor_fh = NULL;
+
+      pr_cmd_set_errno(cmd, EINVAL);
+      errno = EINVAL;
       return PR_ERROR(cmd);
     }
 
@@ -1653,16 +1833,44 @@ MODRET xfer_stor(cmd_rec *cmd) {
 
   if (stor_fh == NULL) {
     pr_log_debug(DEBUG4, "unable to open '%s' for writing: %s", cmd->arg,
-      strerror(ferrno));
-    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(ferrno));
+      strerror(xerrno));
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
+  /* Advise the platform that we will be only writing this file.  Note that a
+   * preceding REST command does not mean we need to use a different offset
+   * value here; we can/should still tell the platform that the entire file
+   * should be treated this way.
+   */
+  pr_fs_fadvise(PR_FH_FD(stor_fh), 0, 0, PR_FS_FADVISE_DONTNEED);
+
+  /* Stash the offset at which we're writing to this file. */
+  curr_offset = pr_fsio_lseek(stor_fh, (off_t) 0, SEEK_CUR);
+  if (curr_offset != (off_t) -1) {
+    off_t *file_offset;
+
+    file_offset = palloc(cmd->pool, sizeof(off_t));
+    *file_offset = (off_t) curr_offset;
+    (void) pr_table_add(cmd->notes, "mod_xfer.file-offset", file_offset,
+      sizeof(off_t));
+  }
+
   /* Get the latest stats on the file.  If the file already existed, we
    * want to know its current size.
    */
   (void) pr_fsio_fstat(stor_fh, &st);
 
+  /* Block any timers for this section, where we want to prepare the
+   * data connection, then need to reprovision the session.xfer struct,
+   * and do NOT want timers (which may want/need that session.xfer data)
+   * to fire until after the reprovisioning (Bug#4168).
+   */
+  pr_alarms_block();
+
   /* Perform the actual transfer now */
   pr_data_init(cmd->arg, PR_NETIO_IO_RD);
 
@@ -1675,12 +1883,19 @@ MODRET xfer_stor(cmd_rec *cmd) {
     "mod_xfer.store-hidden-path", NULL);
   session.xfer.file_size = curr_pos;
 
+  pr_alarms_unblock();
+
   /* First, make sure the uploaded file has the requested ownership. */
-  stor_chown();
+  stor_chown(cmd->tmp_pool);
 
   if (pr_data_open(cmd->arg, NULL, PR_NETIO_IO_RD, 0) < 0) {
+    xerrno = errno;
+
     stor_abort();
     pr_data_abort(0, TRUE);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
@@ -1699,31 +1914,6 @@ MODRET xfer_stor(cmd_rec *cmd) {
     have_limit = TRUE;
   }
 
-  /* Check the MaxStoreFileSize, and abort now if zero. */
-  if (have_limit &&
-      nbytes_max_store == 0) {
-
-    pr_log_pri(PR_LOG_NOTICE, "MaxStoreFileSize (%" PR_LU " %s) reached: "
-      "aborting transfer of '%s'", (pr_off_t) nbytes_max_store,
-      nbytes_max_store != 1 ? "bytes" : "byte", path);
-
-    /* Abort the transfer. */
-    stor_abort();
-
-    /* Set errno to EFBIG (or the most appropriate alternative). */
-#if defined(EFBIG)
-    pr_data_abort(EFBIG, FALSE);
-    errno = EFBIG;
-#elif defined(EDQUOT)
-    pr_data_abort(EDQUOT, FALSE);
-    errno = EDQUOT;
-#else
-    pr_data_abort(EPERM, FALSE);
-    errno = EPERM;
-#endif
-    return PR_ERROR(cmd);
-  }
-
   bufsz = pr_config_get_server_xfer_bufsz(PR_NETIO_IO_RD);
   lbuf = (char *) palloc(cmd->tmp_pool, bufsz);
   pr_trace_msg("data", 8, "allocated upload buffer of %lu bytes",
@@ -1732,8 +1922,9 @@ MODRET xfer_stor(cmd_rec *cmd) {
   while ((len = pr_data_xfer(lbuf, bufsz)) > 0) {
     pr_signals_handle();
 
-    if (XFER_ABORTED)
+    if (XFER_ABORTED) {
       break;
+    }
 
     nbytes_stored += len;
 
@@ -1743,24 +1934,24 @@ MODRET xfer_stor(cmd_rec *cmd) {
      */
     if (have_limit &&
         (nbytes_stored + st.st_size > nbytes_max_store)) {
-
       pr_log_pri(PR_LOG_NOTICE, "MaxStoreFileSize (%" PR_LU " bytes) reached: "
         "aborting transfer of '%s'", (pr_off_t) nbytes_max_store, path);
 
       /* Abort the transfer. */
       stor_abort();
 
-    /* Set errno to EFBIG (or the most appropriate alternative). */
+      /* Set errno to EFBIG (or the most appropriate alternative). */
 #if defined(EFBIG)
-      pr_data_abort(EFBIG, FALSE);
-      errno = EFBIG;
+      xerrno = EFBIG;
 #elif defined(EDQUOT)
-      pr_data_abort(EDQUOT, FALSE);
-      errno = EDQUOT;
+      xerrno = EDQUOT;
 #else
-      pr_data_abort(EPERM, FALSE);
-      errno = EPERM;
+      xerrno = EPERM;
 #endif
+
+      pr_data_abort(xerrno, FALSE);
+      pr_cmd_set_errno(cmd, xerrno);
+      errno = xerrno;
       return PR_ERROR(cmd);
     }
 
@@ -1771,19 +1962,22 @@ MODRET xfer_stor(cmd_rec *cmd) {
      */
     res = pr_fsio_write(stor_fh, lbuf, len);
     if (res != len) {
-      int xerrno = EIO;
+      xerrno = EIO;
 
-      if (res < 0)
+      if (res < 0) {
         xerrno = errno;
+      }
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error writing to '%s': %s", cmd->argv[0], session.user,
-        (unsigned long) session.uid, (unsigned long) session.gid,
-        stor_fh->fh_path, strerror(xerrno));
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error writing to '%s': %s", (char *) cmd->argv[0], session.user,
+        pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), stor_fh->fh_path,
+        strerror(xerrno));
 
       stor_abort();
       pr_data_abort(xerrno, FALSE);
 
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -1795,12 +1989,15 @@ MODRET xfer_stor(cmd_rec *cmd) {
   if (XFER_ABORTED) {
     stor_abort();
     pr_data_abort(0, 0);
+
+    pr_cmd_set_errno(cmd, EIO);
+    errno = EIO; 
     return PR_ERROR(cmd);
 
   } else if (len < 0) {
 
     /* default abort errno, in case session.d et al has already gone away */
-    int xerrno = ECONNABORTED;
+    xerrno = ECONNABORTED;
 
     stor_abort();
 
@@ -1810,6 +2007,7 @@ MODRET xfer_stor(cmd_rec *cmd) {
     }
 
     pr_data_abort(xerrno, FALSE);
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
 
@@ -1819,7 +2017,7 @@ MODRET xfer_stor(cmd_rec *cmd) {
     pr_throttle_pause(nbytes_stored, TRUE);
 
     if (stor_complete() < 0) {
-      int xerrno = errno;
+      xerrno = errno;
 
       _log_transfer('i', 'i');
 
@@ -1831,26 +2029,32 @@ MODRET xfer_stor(cmd_rec *cmd) {
 #if defined(EDQUOT)
       if (xerrno == EDQUOT) {
         pr_response_add_err(R_552, "%s: %s", cmd->arg, strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
 #elif defined(EFBIG)
       if (xerrno == EFBIG) {
         pr_response_add_err(R_552, "%s: %s", cmd->arg, strerror(xerrno));
+
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
 #endif
 
       pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
 
     if (session.xfer.path &&
         session.xfer.path_hidden) {
-      if (pr_fsio_rename(session.xfer.path_hidden, session.xfer.path) != 0) {
-        int xerrno = errno;
+      if (pr_fsio_rename(session.xfer.path_hidden, session.xfer.path) < 0) {
+        xerrno = errno;
 
         /* This should only fail on a race condition with a chmod/chown
          * or if STOR_APPEND is on and the permissions are squirrely.
@@ -1863,19 +2067,24 @@ MODRET xfer_stor(cmd_rec *cmd) {
         pr_response_add_err(R_550, _("%s: Rename of hidden file %s failed: %s"),
           session.xfer.path, session.xfer.path_hidden, strerror(xerrno));
 
-        pr_fsio_unlink(session.xfer.path_hidden);
+        if (pr_fsio_unlink(session.xfer.path_hidden) < 0) {
+          if (errno != ENOENT) {
+            pr_log_debug(DEBUG0, "failed to delete HiddenStores file '%s': %s",
+              session.xfer.path_hidden, strerror(errno));
+          }
+        } 
 
+        pr_cmd_set_errno(cmd, xerrno);
         errno = xerrno;
         return PR_ERROR(cmd);
       }
-    }
 
-    if (xfer_displayfile() < 0) {
-      pr_data_close(FALSE);
+      /* One way or another, we've dealt with the HiddenStores file. */
+      session.xfer.path_hidden = NULL;
+    }
 
-    } else {
-      pr_data_close(TRUE);
-    } 
+    xfer_displayfile();
+    pr_data_close(FALSE);
   }
 
   return PR_HANDLED(cmd);
@@ -1883,33 +2092,43 @@ MODRET xfer_stor(cmd_rec *cmd) {
 
 MODRET xfer_rest(cmd_rec *cmd) {
   off_t pos;
-  char *endp = NULL;
+  char *endp = NULL, *ptr;
 
   if (cmd->argc != 2) {
     pr_response_add_err(R_500, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
   /* Don't allow negative numbers.  strtoul()/strtoull() will silently
    * handle them.
    */
-  if (*cmd->argv[1] == '-') {
+  ptr = cmd->argv[1];
+  if (*ptr == '-') {
     pr_response_add_err(R_501,
       _("REST requires a value greater than or equal to 0"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
 #ifdef HAVE_STRTOULL
-  pos = strtoull(cmd->argv[1], &endp, 10);
+  pos = strtoull(ptr, &endp, 10);
 #else
-  pos = strtoul(cmd->argv[1], &endp, 10);
+  pos = strtoul(ptr, &endp, 10);
 #endif /* HAVE_STRTOULL */
 
   if (endp &&
       *endp) {
     pr_response_add_err(R_501,
       _("REST requires a value greater than or equal to 0"));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -1923,10 +2142,15 @@ MODRET xfer_rest(cmd_rec *cmd) {
    * clients.
    */
   if ((session.sf_flags & SF_ASCII) &&
-      pos != 0) {
-    pr_log_debug(DEBUG5, "%s not allowed in ASCII mode", cmd->argv[0]);
+      pos != 0 &&
+      !(xfer_opts & PR_XFER_OPT_IGNORE_ASCII)) {
+    pr_log_debug(DEBUG5, "%s not allowed in ASCII mode", (char *) cmd->argv[0]);
     pr_response_add_err(R_501,
-      _("%s: Resuming transfers not allowed in ASCII mode"), cmd->argv[0]);
+      _("%s: Resuming transfers not allowed in ASCII mode"),
+      (char *) cmd->argv[0]);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   } 
 
@@ -1942,29 +2166,70 @@ MODRET xfer_rest(cmd_rec *cmd) {
  * for this, as tmp_pool only lasts for the duration of this function).
  */
 MODRET xfer_pre_retr(cmd_rec *cmd) {
-  char *dir = NULL;
+  char *decoded_path, *dir = NULL;
   mode_t fmode;
   unsigned char *allow_restart = NULL;
   config_rec *c;
+  struct stat st;
 
   xfer_logged_sendfile_decline_msg = FALSE;
 
   if (cmd->argc < 2) {
     pr_response_add_err(R_500, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
     errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  dir = dir_realpath(cmd->tmp_pool,
-    pr_fs_decode_path(cmd->tmp_pool, cmd->arg));
+  decoded_path = pr_fs_decode_path2(cmd->tmp_pool, cmd->arg,
+    FSIO_DECODE_FL_TELL_ERRORS);
+  if (decoded_path == NULL) {
+    int xerrno = errno;
 
-  if (!dir ||
+    pr_log_debug(DEBUG8, "'%s' failed to decode properly: %s", cmd->arg,
+      strerror(xerrno));
+    pr_response_add_err(R_550, _("%s: Illegal character sequence in filename"),
+      cmd->arg);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
+    return PR_ERROR(cmd);
+  }
+
+  pr_fs_clear_cache2(decoded_path);
+  if (pr_fsio_lstat(decoded_path, &st) == 0) {
+    if (S_ISLNK(st.st_mode)) {
+      char buf[PR_TUNABLE_PATH_MAX];
+      int len;
+
+      memset(buf, '\0', sizeof(buf));
+      len = dir_readlink(cmd->tmp_pool, decoded_path, buf, sizeof(buf)-1,
+        PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+      if (len > 0) {
+        buf[len] = '\0';
+        dir = pstrdup(cmd->tmp_pool, buf);
+
+      } else {
+        dir = dir_realpath(cmd->tmp_pool, decoded_path);
+      }
+
+    } else {
+      dir = dir_realpath(cmd->tmp_pool, decoded_path);
+    }
+
+  } else {
+    dir = dir_realpath(cmd->tmp_pool, decoded_path);
+  }
+
+  if (dir == NULL ||
       !dir_check(cmd->tmp_pool, cmd, cmd->group, dir, NULL)) {
     int xerrno = errno;
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -1983,16 +2248,19 @@ MODRET xfer_pre_retr(cmd_rec *cmd) {
 
   if (xfer_check_limit(cmd) < 0) {
     pr_response_add_err(R_451, _("%s: Too many transfers"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
 
-  fmode = file_mode(dir);
+  fmode = file_mode2(cmd->tmp_pool, dir);
   if (fmode == 0) {
     int xerrno = errno;
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
 
+    pr_cmd_set_errno(cmd, xerrno);
     errno = xerrno;
     return PR_ERROR(cmd);
   }
@@ -2005,6 +2273,7 @@ MODRET xfer_pre_retr(cmd_rec *cmd) {
     pr_response_add_err(R_550, _("%s: Not a regular file"), cmd->arg);
 
     /* Deliberately use EISDIR for anything non-file (e.g. directories). */
+    pr_cmd_set_errno(cmd, EISDIR);
     errno = EISDIR;
     return PR_ERROR(cmd);
   }
@@ -2019,6 +2288,8 @@ MODRET xfer_pre_retr(cmd_rec *cmd) {
     pr_response_add_err(R_451, _("%s: Restart not permitted, try again"),
       cmd->arg);
     session.restart_pos = 0L;
+
+    pr_cmd_set_errno(cmd, EPERM);
     errno = EPERM;
     return PR_ERROR(cmd);
   }
@@ -2036,13 +2307,34 @@ MODRET xfer_pre_retr(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+MODRET xfer_post_retr(cmd_rec *cmd) {
+  const char *path;
+
+  path = pr_table_get(cmd->notes, "mod_xfer.retr-path", NULL);
+  if (path != NULL) {
+    struct stat st;
+
+    if (pr_fsio_stat(path, &st) == 0) {
+      off_t *file_size;
+
+      file_size = palloc(cmd->pool, sizeof(off_t));
+      *file_size = st.st_size;
+      (void) pr_table_add(cmd->notes, "mod_xfer.file-size", file_size,
+        sizeof(off_t));
+    }
+  }
+
+  return PR_DECLINED(cmd);
+}
+
 MODRET xfer_retr(cmd_rec *cmd) {
-  char *dir = NULL, *lbuf;
+  const char *dir = NULL;
+  char *lbuf;
   struct stat st;
   off_t nbytes_max_retrieve = 0;
   unsigned char have_limit = FALSE;
   long bufsz, len = 0;
-  off_t curr_pos = 0, nbytes_sent = 0, cnt_steps = 0, cnt_next = 0;
+  off_t curr_offset, curr_pos = 0, nbytes_sent = 0, cnt_steps = 0, cnt_next = 0;
 
   /* Prepare for any potential throttling. */
   pr_throttle_init(cmd);
@@ -2053,36 +2345,52 @@ MODRET xfer_retr(cmd_rec *cmd) {
   if (retr_fh == NULL) {
     int xerrno = errno;
 
-    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-      "error opening '%s': %s", cmd->argv[0], session.user,
-      (unsigned long) session.uid, (unsigned long) session.gid,
-      dir, strerror(xerrno));
+    (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+      "error opening '%s': %s", (char *) cmd->argv[0], session.user,
+      pr_uid2str(cmd->tmp_pool, session.uid),
+      pr_gid2str(cmd->tmp_pool, session.gid), dir, strerror(xerrno));
 
     pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  if (pr_fsio_stat(dir, &st) < 0) {
+  if (pr_fsio_fstat(retr_fh, &st) < 0) {
     /* Error stat'ing the file. */
     int xerrno = errno;
-    pr_fsio_close(retr_fh);
-    errno = xerrno;
 
+    pr_fsio_close(retr_fh);
     retr_fh = NULL;
-    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(errno));
+    pr_response_add_err(R_550, "%s: %s", cmd->arg, strerror(xerrno));
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
-  if (session.restart_pos) {
+  /* Advise the platform that we will be only reading this file
+   * sequentially.  Note that a preceding REST command does not mean we
+   * need to use a different offset value here; we can/should still
+   * tell the platform that the entire file should be treated this way.
+   */
+  pr_fs_fadvise(PR_FH_FD(retr_fh), 0, 0, PR_FS_FADVISE_SEQUENTIAL);
 
+  if (session.restart_pos) {
     /* Make sure that the requested offset is valid (within the size of the
      * file being resumed).
      */
     if (session.restart_pos > st.st_size) {
+      pr_trace_msg(trace_channel, 4,
+        "REST offset %" PR_LU " exceeds file size (%" PR_LU " bytes)",
+        (pr_off_t) session.restart_pos, (pr_off_t) st.st_size);
       pr_response_add_err(R_554, _("%s: invalid REST argument"), cmd->arg);
       pr_fsio_close(retr_fh);
       retr_fh = NULL;
 
+      pr_cmd_set_errno(cmd, EINVAL);
+      errno = EINVAL;
       return PR_ERROR(cmd);
     }
 
@@ -2093,15 +2401,19 @@ MODRET xfer_retr(cmd_rec *cmd) {
       errno = xerrno;
       retr_fh = NULL;
 
-      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %lu, GID %lu): "
-        "error seeking to byte %" PR_LU " of '%s': %s", cmd->argv[0],
-        session.user, (unsigned long) session.uid, (unsigned long) session.gid,
-        (pr_off_t) session.restart_pos, dir, strerror(xerrno));
+      (void) pr_trace_msg("fileperms", 1, "%s, user '%s' (UID %s, GID %s): "
+        "error seeking to byte %" PR_LU " of '%s': %s", (char *) cmd->argv[0],
+        session.user, pr_uid2str(cmd->tmp_pool, session.uid),
+        pr_gid2str(cmd->tmp_pool, session.gid), (pr_off_t) session.restart_pos,
+        dir, strerror(xerrno));
 
       pr_log_debug(DEBUG0, "error seeking to offset %" PR_LU
         " for file %s: %s", (pr_off_t) session.restart_pos, dir,
         strerror(xerrno));
       pr_response_add_err(R_554, _("%s: invalid REST argument"), cmd->arg);
+
+      pr_cmd_set_errno(cmd, EINVAL);
+      errno = EINVAL;
       return PR_ERROR(cmd);
     }
 
@@ -2109,19 +2421,44 @@ MODRET xfer_retr(cmd_rec *cmd) {
     session.restart_pos = 0L;
   }
 
+  /* Stash the offset at which we're writing from this file. */
+  curr_offset = pr_fsio_lseek(retr_fh, (off_t) 0, SEEK_CUR);
+  if (curr_offset != (off_t) -1) {
+    off_t *file_offset;
+
+    file_offset = palloc(cmd->pool, sizeof(off_t));
+    *file_offset = (off_t) curr_offset;
+    (void) pr_table_add(cmd->notes, "mod_xfer.file-offset", file_offset,
+      sizeof(off_t));
+  }
+
+  /* Block any timers for this section, where we want to prepare the
+   * data connection, then need to reprovision the session.xfer struct,
+   * and do NOT want timers (which may want/need that session.xfer data)
+   * to fire until after the reprovisioning (Bug#4168).
+   */
+  pr_alarms_block();
+
   /* Send the data */
   pr_data_init(cmd->arg, PR_NETIO_IO_WR);
 
   session.xfer.path = dir;
   session.xfer.file_size = st.st_size;
 
+  pr_alarms_unblock();
+
   cnt_steps = session.xfer.file_size / 100;
   if (cnt_steps == 0)
     cnt_steps = 1;
 
   if (pr_data_open(cmd->arg, NULL, PR_NETIO_IO_WR, st.st_size - curr_pos) < 0) {
+    int xerrno = errno;
+
     retr_abort();
     pr_data_abort(0, TRUE);
+
+    pr_cmd_set_errno(cmd, xerrno);
+    errno = xerrno;
     return PR_ERROR(cmd);
   }
 
@@ -2150,6 +2487,9 @@ MODRET xfer_retr(cmd_rec *cmd) {
 
     /* Set errno to EPERM ("Operation not permitted") */
     pr_data_abort(EPERM, FALSE);
+
+    pr_cmd_set_errno(cmd, EPERM);
+    errno = EPERM;
     return PR_ERROR(cmd);
   }
 
@@ -2168,22 +2508,58 @@ MODRET xfer_retr(cmd_rec *cmd) {
   while (nbytes_sent != session.xfer.file_size) {
     pr_signals_handle();
 
-    if (XFER_ABORTED)
+    if (XFER_ABORTED) {
       break;
+    }
 
-    len = transmit_data(nbytes_sent, &curr_pos, lbuf, bufsz);
-    if (len == 0)
+    len = transmit_data(cmd->pool, nbytes_sent, &curr_pos, lbuf, bufsz);
+    if (len == 0) {
       break;
+    }
 
     if (len < 0) {
       /* Make sure that the errno value, needed for the pr_data_abort() call,
        * is preserved; errno itself might be overwritten in retr_abort().
        */
-      int xerrno = errno;
+      int already_aborted = FALSE, xerrno = errno;
 
       retr_abort();
-      pr_data_abort(xerrno, FALSE);
 
+      /* Do we need to abort the data transfer here?  It's possible that
+       * the transfer has already been aborted, e.g. via the TCP OOB marker
+       * and/or the ABOR command.  And if that is the case, then calling
+       * pr_data_abort() here will only lead to a spurious response code
+       * (see Bug#4252).
+       *
+       * However, there are OTHER error conditions which would lead to this
+       * code path.  So we need to resort to some heuristics to differentiate
+       * between these cases.  The errno value checks match those in the
+       * pr_data_xfer() function, after the control channel has been polled
+       * for commands such as ABOR.
+       */
+
+      if (session.d == NULL &&
+#if defined(ECONNABORTED)
+          xerrno == ECONNABORTED &&
+#elif defined(ENOTCONN)
+          xerrno == ENOTCONN &&
+#else
+          xerrno == EIO &&
+#endif
+          session.xfer.xfer_type == STOR_DEFAULT) {
+
+        /* If the ABOR command has been sent, then pr_data_reset() and
+         * pr_data_cleanup() will have been called; the latter resets the
+         * xfer_type value to DEFAULT.
+         */
+        already_aborted = TRUE;
+      }
+
+      if (already_aborted == FALSE) {
+        pr_data_abort(xerrno, FALSE);
+      }
+
+      pr_cmd_set_errno(cmd, xerrno);
       errno = xerrno;
       return PR_ERROR(cmd);
     }
@@ -2210,9 +2586,13 @@ MODRET xfer_retr(cmd_rec *cmd) {
   if (XFER_ABORTED) {
     retr_abort();
     pr_data_abort(0, FALSE);
+
+    pr_cmd_set_errno(cmd, EIO);
+    errno = EIO;
     return PR_ERROR(cmd);
 
   } else {
+
     /* If no throttling is configured, this simply updates the scoreboard.
      * In this case, we want to use session.xfer.total_bytes, rather than
      * nbytes_sent, as the latter incorporates a REST position and the
@@ -2222,13 +2602,8 @@ MODRET xfer_retr(cmd_rec *cmd) {
     pr_throttle_pause(session.xfer.total_bytes, TRUE);
 
     retr_complete();
-
-    if (xfer_displayfile() < 0) {
-      pr_data_close(FALSE);
-
-    } else {
-      pr_data_close(TRUE);
-    }
+    xfer_displayfile();
+    pr_data_close(FALSE);
   }
 
   return PR_HANDLED(cmd);
@@ -2238,6 +2613,9 @@ MODRET xfer_abor(cmd_rec *cmd) {
   if (cmd->argc != 1) {
     pr_response_add_err(R_500, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -2270,6 +2648,9 @@ MODRET xfer_type(cmd_rec *cmd) {
       cmd->argc > 3) {
     pr_response_add_err(R_500, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -2294,24 +2675,45 @@ MODRET xfer_type(cmd_rec *cmd) {
 
   } else {
     pr_response_add_err(R_504, _("%s not implemented for '%s' parameter"),
-      cmd->argv[0], cmd->argv[1]);
+      (char *) cmd->argv[0], (char *) cmd->argv[1]);
+
+    pr_cmd_set_errno(cmd, ENOSYS);
+    errno = ENOSYS;
     return PR_ERROR(cmd);
   }
 
-  pr_response_add(R_200, _("Type set to %s"), cmd->argv[1]);
+  /* Note that the client may NOT be authenticated at this point in time.
+   * If that is the case, set a flag so that the POST_CMD PASS handler does
+   * not overwrite the TYPE command's setting.
+   *
+   * Alternatively, we COULD bar/reject any TYPE commands before authentication.
+   * However, I think that doing so would interfere with many existing clients
+   * which assume that they can send TYPE before authenticating.
+   */
+  if (session.auth_mech == NULL) {
+    have_type = TRUE;
+  }
+
+  pr_response_add(R_200, _("Type set to %s"), (char *) cmd->argv[1]);
   return PR_HANDLED(cmd);
 }
 
 MODRET xfer_stru(cmd_rec *cmd) {
+  char *stru;
+
   if (cmd->argc != 2) {
     pr_response_add_err(R_501, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  cmd->argv[1][0] = toupper(cmd->argv[1][0]);
+  stru = cmd->argv[1];
+  stru[0] = toupper(stru[0]);
 
-  switch ((int) cmd->argv[1][0]) {
+  switch ((int) stru[0]) {
     case 'F':
       /* Should 202 be returned instead??? */
       pr_response_add(R_200, _("Structure set to F"));
@@ -2335,27 +2737,37 @@ MODRET xfer_stru(cmd_rec *cmd) {
       /* RFC-1123 recommends against implementing P. */
       pr_response_add_err(R_504, _("'%s' unsupported structure type"),
         pr_cmd_get_displayable_str(cmd, NULL));
+
+      pr_cmd_set_errno(cmd, ENOSYS);
+      errno = ENOSYS;
       return PR_ERROR(cmd);
-      break;
 
     default:
       pr_response_add_err(R_501, _("'%s' unrecognized structure type"),
         pr_cmd_get_displayable_str(cmd, NULL));
+
+      pr_cmd_set_errno(cmd, EINVAL);
+      errno = EINVAL;
       return PR_ERROR(cmd);
-      break;
   }
 }
 
 MODRET xfer_mode(cmd_rec *cmd) {
+  char *mode;
+
   if (cmd->argc != 2) {
     pr_response_add_err(R_501, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
-  cmd->argv[1][0] = toupper(cmd->argv[1][0]);
+  mode = cmd->argv[1];
+  mode[0] = toupper(mode[0]);
 
-  switch ((int) cmd->argv[1][0]) {
+  switch ((int) mode[0]) {
     case 'S':
       /* Should 202 be returned instead??? */
       pr_response_add(R_200, _("Mode set to S"));
@@ -2367,11 +2779,17 @@ MODRET xfer_mode(cmd_rec *cmd) {
     case 'C':
       pr_response_add_err(R_504, _("'%s' unsupported transfer mode"),
         pr_cmd_get_displayable_str(cmd, NULL));
+
+      pr_cmd_set_errno(cmd, ENOSYS);
+      errno = ENOSYS;
       return PR_ERROR(cmd);
   }
 
   pr_response_add_err(R_501, _("'%s' unrecognized transfer mode"),
     pr_cmd_get_displayable_str(cmd, NULL));
+
+  pr_cmd_set_errno(cmd, EINVAL);
+  errno = EINVAL;
   return PR_ERROR(cmd);
 }
 
@@ -2387,6 +2805,9 @@ MODRET xfer_allo(cmd_rec *cmd) {
       cmd->argc != 4) {
     pr_response_add_err(R_504, _("'%s' not understood"),
       pr_cmd_get_displayable_str(cmd, NULL));
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -2398,6 +2819,9 @@ MODRET xfer_allo(cmd_rec *cmd) {
 
   if (tmp && *tmp) {
     pr_response_add_err(R_504, _("%s: Invalid ALLO argument"), cmd->arg);
+
+    pr_cmd_set_errno(cmd, EINVAL);
+    errno = EINVAL;
     return PR_ERROR(cmd);
   }
 
@@ -2428,16 +2852,19 @@ MODRET xfer_allo(cmd_rec *cmd) {
 
       if (requested_kb > avail_kb) {
         pr_log_debug(DEBUG5, "%s requested %" PR_LU " KB, only %" PR_LU
-          " KB available on '%s'", cmd->argv[0], (pr_off_t) requested_kb,
-          (pr_off_t) avail_kb, path);
+          " KB available on '%s'", (char *) cmd->argv[0],
+          (pr_off_t) requested_kb, (pr_off_t) avail_kb, path);
         pr_response_add_err(R_552, "%s: %s", cmd->arg, strerror(ENOSPC));
+
+        pr_cmd_set_errno(cmd, ENOSPC);
+        errno = ENOSPC;
         return PR_ERROR(cmd);
       }
 
       pr_log_debug(DEBUG9, "%s requested %" PR_LU " KB, %" PR_LU
-        " KB available on '%s'", cmd->argv[0], (pr_off_t) requested_kb,
+        " KB available on '%s'", (char *) cmd->argv[0], (pr_off_t) requested_kb,
         (pr_off_t) avail_kb, path);
-      pr_response_add(R_200, _("%s command successful"), cmd->argv[0]);
+      pr_response_add(R_200, _("%s command successful"), (char *) cmd->argv[0]);
     }
 
   } else {
@@ -2453,6 +2880,27 @@ MODRET xfer_smnt(cmd_rec *cmd) {
 }
 
 MODRET xfer_err_cleanup(cmd_rec *cmd) {
+
+  /* If a hidden store was aborted, remove it. */
+  if (session.xfer.xfer_type == STOR_HIDDEN) {
+    unsigned char *delete_stores = NULL;
+
+    delete_stores = get_param_ptr(CURRENT_CONF, "DeleteAbortedStores", FALSE);
+    if (delete_stores == NULL ||
+        *delete_stores == TRUE) {
+      if (session.xfer.path_hidden) {
+        pr_log_debug(DEBUG5, "removing aborted HiddenStores file '%s'",
+          session.xfer.path_hidden);
+        if (pr_fsio_unlink(session.xfer.path_hidden) < 0) {
+          if (errno != ENOENT) {
+            pr_log_debug(DEBUG0, "error deleting HiddenStores file '%s': %s",
+              session.xfer.path_hidden, strerror(errno));
+          }
+        }
+      }
+    }
+  }
+
   pr_data_clear_xfer_pool();
 
   memset(&session.xfer, '\0', sizeof(session.xfer));
@@ -2497,17 +2945,23 @@ MODRET xfer_log_retr(cmd_rec *cmd) {
 }
 
 static int noxfer_timeout_cb(CALLBACK_FRAME) {
+  int timeout;
   const char *proto;
 
+  timeout = pr_data_get_timeout(PR_DATA_TIMEOUT_NO_TRANSFER);
+
   if (session.sf_flags & SF_XFER) {
+    pr_trace_msg("timer", 4,
+      "TimeoutNoTransfer (%d %s) reached, but data transfer in progress, "
+      "ignoring", timeout, timeout != 1 ? "seconds" : "second");
+
     /* Transfer in progress, ignore this timeout */
     return 1;
   }
 
   pr_event_generate("core.timeout-no-transfer", NULL);
   pr_response_send_async(R_421,
-    _("No transfer timeout (%d seconds): closing control connection"),
-    pr_data_get_timeout(PR_DATA_TIMEOUT_NO_TRANSFER));
+    _("No transfer timeout (%d seconds): closing control connection"), timeout);
 
   pr_timer_remove(PR_TIMER_IDLE, ANY_MODULE);
   pr_timer_remove(PR_TIMER_LOGIN, ANY_MODULE);
@@ -2534,37 +2988,26 @@ static int noxfer_timeout_cb(CALLBACK_FRAME) {
   return 0;
 }
 
-MODRET xfer_post_host(cmd_rec *cmd) {
+MODRET xfer_post_pass(cmd_rec *cmd) {
+  config_rec *c;
 
-  /* If the HOST command changed the main_server pointer, reinitialize
-   * ourselves.
+  /* Default transfer mode is ASCII, per RFC 959, Section 3.1.1.1.  Unless
+   * the client has already sent a TYPE command.
    */
-  if (session.prev_server != NULL) {
-    int res;
-
-    pr_event_unregister(&xfer_module, "core.exit", xfer_exit_ev);
-    pr_event_unregister(&xfer_module, "core.timeout-stalled",
-      xfer_xfer_stalled_ev);
-    pr_event_unregister(&xfer_module, "core.signal.USR2", xfer_sigusr2_ev);
-
-    if (displayfilexfer_fh != NULL) {
-      (void) pr_fsio_close(displayfilexfer_fh);
-      displayfilexfer_fh = NULL;
-    }
-
-    res = xfer_sess_init();
-    if (res < 0) {
-      pr_session_disconnect(&xfer_module,
-        PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  if (have_type == FALSE) {
+    session.sf_flags |= SF_ASCII;
+    c = find_config(main_server->conf, CONF_PARAM, "DefaultTransferMode",
+      FALSE);
+    if (c != NULL) {
+      char *default_transfer_mode;
+
+      default_transfer_mode = c->argv[0];
+      if (strcasecmp(default_transfer_mode, "binary") == 0) {
+        session.sf_flags &= (SF_ALL^SF_ASCII);
+      }
     }
   }
 
-  return PR_DECLINED(cmd);
-}
-
-MODRET xfer_post_pass(cmd_rec *cmd) {
-  config_rec *c;
-
   c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TimeoutNoTransfer", FALSE);
   if (c != NULL) {
     int timeout = *((int *) c->argv[0]);
@@ -2587,6 +3030,23 @@ MODRET xfer_post_pass(cmd_rec *cmd) {
      */
   }
 
+  c = find_config(main_server->conf, CONF_PARAM, "TransferOptions", FALSE);
+  while (c != NULL) {
+    unsigned long opts = 0;
+
+    pr_signals_handle();
+
+    opts = *((unsigned long *) c->argv[0]);
+    xfer_opts |= opts;
+
+    c = find_config_next(c, c->next, CONF_PARAM, "TransferOptions", FALSE);
+  }
+
+  if (xfer_opts & PR_XFER_OPT_IGNORE_ASCII) {
+    pr_log_debug(DEBUG8, "Ignoring ASCII translation for this session");
+    pr_data_ignore_ascii(TRUE);
+  }
+
   /* Check for TransferPriority. */
   c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TransferPriority", FALSE);
   if (c) {
@@ -2647,6 +3107,23 @@ MODRET set_allowrestart(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
+/* usage: DefaultTransferMode ascii|binary */
+MODRET set_defaulttransfermode(cmd_rec *cmd) {
+  char *default_mode;
+
+  CHECK_ARGS(cmd, 1);
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  default_mode = cmd->argv[1];
+  if (strcasecmp(default_mode, "ascii") != 0 &&
+      strcasecmp(default_mode, "binary") != 0) {
+    CONF_ERROR(cmd, "parameter must be 'ascii' or 'binary'");
+  }
+
+  add_config_param_str(cmd->argv[0], 1, default_mode);
+  return PR_HANDLED(cmd);
+}
+
 MODRET set_deleteabortedstores(cmd_rec *cmd) {
   int bool = -1;
   config_rec *c = NULL;
@@ -2679,6 +3156,7 @@ MODRET set_displayfiletransfer(cmd_rec *cmd) {
 MODRET set_hiddenstores(cmd_rec *cmd) {
   int enabled = -1, add_periods = TRUE;
   config_rec *c = NULL;
+  char *prefix = NULL;
 
   CHECK_ARGS(cmd, 1);
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR);
@@ -2690,8 +3168,9 @@ MODRET set_hiddenstores(cmd_rec *cmd) {
    * get_boolean(): if the value begins AND ends with a period, then treat
    * it as a custom prefix.
    */
-  if ((cmd->argv[1])[0] == '.' &&
-      (cmd->argv[1])[strlen(cmd->argv[1])-1] == '.') {
+  prefix = cmd->argv[1];
+  if (prefix[0] == '.' &&
+      prefix[strlen(prefix)-1] == '.') {
     add_periods = FALSE;
     enabled = -1;
 
@@ -2823,6 +3302,10 @@ MODRET set_maxfilesize(cmd_rec *cmd) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to parse: ",
         cmd->argv[1], " ", cmd->argv[2], ": ", strerror(errno), NULL));
     }
+
+    if (nbytes == 0) {
+      CONF_ERROR(cmd, "size must be greater than zero");
+    }
   }
 
   if (cmd->argc-1 == 1 ||
@@ -2835,16 +3318,19 @@ MODRET set_maxfilesize(cmd_rec *cmd) {
 
   } else {
     array_header *acl = NULL;
-    int argc = cmd->argc - 4;
-    char **argv = cmd->argv + 3;
+    unsigned int argc;
+    void **argv;
 
-    acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
+    argc = cmd->argc - 4;
+    argv = cmd->argv + 3;
+
+    acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
 
     c = add_config_param(cmd->argv[0], 0);
     c->argc = argc + 3;
-    c->argv = pcalloc(c->pool, ((argc + 4) * sizeof(char *)));
+    c->argv = pcalloc(c->pool, ((argc + 4) * sizeof(void *)));
 
-    argv = (char **) c->argv;
+    argv = c->argv;
 
     /* Copy in the configured bytes */
     *argv = pcalloc(c->pool, sizeof(unsigned long));
@@ -2999,12 +3485,41 @@ MODRET set_timeoutstalled(cmd_rec *cmd) {
   return PR_HANDLED(cmd);
 }
 
-/* usage: TransferPriority cmds "low"|"medium"|"high"|number
- */
+/* usage: TransferOptions opt1 opt2 ... */
+MODRET set_transferoptions(cmd_rec *cmd) {
+  config_rec *c = NULL;
+  register unsigned int i = 0;
+  unsigned long opts = 0UL;
+
+  if (cmd->argc-1 == 0) {
+    CONF_ERROR(cmd, "wrong number of parameters");
+  }
+
+  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
+
+  c = add_config_param(cmd->argv[0], 1, NULL);
+
+  for (i = 1; i < cmd->argc; i++) {
+    if (strcasecmp(cmd->argv[i], "IgnoreASCII") == 0) {
+      opts |= PR_XFER_OPT_IGNORE_ASCII;
+
+    } else {
+      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown TransferOption '",
+        cmd->argv[i], "'", NULL));
+    }
+  }
+
+  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
+  *((unsigned long *) c->argv[0]) = opts;
+
+  return PR_HANDLED(cmd);
+}
+
+/* usage: TransferPriority cmds "low"|"medium"|"high"|number */
 MODRET set_transferpriority(cmd_rec *cmd) {
   config_rec *c;
   int prio;
-  char *str;
+  char *param, *str;
   unsigned long flags = 0;
 
   CHECK_ARGS(cmd, 2);
@@ -3034,7 +3549,9 @@ MODRET set_transferpriority(cmd_rec *cmd) {
   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
 
   /* Parse the command list. */
-  while ((str = get_cmd_from_list(&cmd->argv[1])) != NULL) {
+
+  param = cmd->argv[1];
+  while ((str = get_cmd_from_list(&param)) != NULL) {
 
     if (strcmp(str, C_APPE) == 0) {
       flags |= PR_XFER_PRIO_FL_APPE;
@@ -3164,11 +3681,13 @@ MODRET set_transferrate(cmd_rec *cmd) {
 
   } else {
     array_header *acl = NULL;
-    int argc = cmd->argc - 4;
-    char **argv = cmd->argv + 3;
+    unsigned int argc;
+    void **argv;
 
-    acl = pr_expr_create(cmd->tmp_pool, &argc, argv);
+    argc = cmd->argc - 4;
+    argv = cmd->argv + 3;
 
+    acl = pr_expr_create(cmd->tmp_pool, &argc, (char **) argv);
     c = add_config_param(cmd->argv[0], 0);
 
     /* Parse the command list.
@@ -3178,11 +3697,12 @@ MODRET set_transferrate(cmd_rec *cmd) {
      */
     c->argc = argc + 5;
 
-    c->argv = pcalloc(c->pool, ((c->argc + 1) * sizeof(char *)));
-    argv = (char **) c->argv;
+    c->argv = pcalloc(c->pool, ((c->argc + 1) * sizeof(void *)));
+    argv = c->argv;
 
-    if (xfer_parse_cmdlist(cmd->argv[0], c, cmd->argv[1]) < 0)
+    if (xfer_parse_cmdlist(cmd->argv[0], c, cmd->argv[1]) < 0) {
       CONF_ERROR(cmd, "error with command list");
+    }
 
     /* Note: the command list is at index 0, hence this increment. */
     argv++;
@@ -3226,27 +3746,29 @@ MODRET set_usesendfile(cmd_rec *cmd) {
      */
     bool = get_boolean(cmd, 1);
     if (bool == -1) {
+      char *arg;
       size_t arglen;
 
       /* See if the given parameter is a percentage. */
-      arglen = strlen(cmd->argv[1]);
+      arg = cmd->argv[1];
+      arglen = strlen(arg);
       if (arglen > 1 &&
-          cmd->argv[1][arglen-1] == '%') {
+          arg[arglen-1] == '%') {
           char *ptr = NULL;
   
-          cmd->argv[1][arglen-1] = '\0';
+          arg[arglen-1] = '\0';
 
 #ifdef HAVE_STRTOF
-          sendfile_pct = strtof(cmd->argv[1], &ptr);
+          sendfile_pct = strtof(arg, &ptr);
 #elif HAVE_STRTOD
-          sendfile_pct = strtod(cmd->argv[1], &ptr);
+          sendfile_pct = strtod(arg, &ptr);
 #else
-          sendfile_pct = atof(cmd->argv[1]);
+          sendfile_pct = atof(arg);
 #endif /* !HAVE_STRTOF and !HAVE_STRTOD */
 
           if (ptr && *ptr) {
             CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "bad percentage value '",
-              cmd->argv[1], "%'", NULL));
+              arg, "%'", NULL));
           }
 
           sendfile_pct /= 100.0;
@@ -3287,79 +3809,115 @@ MODRET set_usesendfile(cmd_rec *cmd) {
 /* Event handlers
  */
 
-static void xfer_sigusr2_ev(const void *event_data, void *user_data) {
+static void xfer_exit_ev(const void *event_data, void *user_data) {
 
-  /* Only do this if we're currently involved in a data transfer.
-   * This is a hack put in to support mod_shaper's antics.
-   */
-  if (strcmp(session.curr_cmd, C_APPE) == 0 ||
-      strcmp(session.curr_cmd, C_RETR) == 0 ||
-      strcmp(session.curr_cmd, C_STOR) == 0 ||
-      strcmp(session.curr_cmd, C_STOU) == 0) {
-    pool *p = make_sub_pool(session.pool);
-    cmd_rec *cmd = pr_cmd_alloc(p, 1, session.curr_cmd);
-
-    /* Rescan the config tree for TransferRates, picking up any possible
-     * changes.
-     */
-    pr_log_debug(DEBUG2, "rechecking TransferRates");
-    pr_throttle_init(cmd);
+  if (stor_fh != NULL) {
+     /* An upload is occurring... */
+    pr_trace_msg(trace_channel, 6, "session exiting, aborting upload");
+    stor_abort();
+  
+  } else if (retr_fh != NULL) {
+    /* A download is occurring... */
+    pr_trace_msg(trace_channel, 6, "session exiting, aborting download");
+    retr_abort();
+  }
+
+  if (session.sf_flags & SF_XFER) {
+    cmd_rec *cmd;
+    pr_data_abort(0, FALSE);
 
-    destroy_pool(p);
+    cmd = session.curr_cmd_rec;
+    if (cmd == NULL) {
+      cmd = pr_cmd_alloc(session.pool, 2, session.curr_cmd, session.xfer.path);
+    }
+
+    (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
+    (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
   }
 
   return;
 }
 
-/* Events handlers
- */
+static void xfer_sess_reinit_ev(const void *event_data, void *user_data) {
+  int res;
 
-static void xfer_exit_ev(const void *event_data, void *user_data) {
+  /* A HOST command changed the main_server pointer, reinitialize ourselves. */
 
-  if (session.sf_flags & SF_XFER) {
-    cmd_rec *cmd;
+  pr_event_unregister(&xfer_module, "core.exit", xfer_exit_ev);
+  pr_event_unregister(&xfer_module, "core.session-reinit", xfer_sess_reinit_ev);
+  pr_event_unregister(&xfer_module, "core.signal.USR2", xfer_sigusr2_ev);
+  pr_event_unregister(&xfer_module, "core.timeout-stalled",
+    xfer_timeout_stalled_ev);
 
-    if (session.xfer.direction == PR_NETIO_IO_RD) {
-       /* An upload is occurring... */
-      pr_trace_msg(trace_channel, 6, "session exiting, aborting upload");
-      stor_abort();
+  if (displayfilexfer_fh != NULL) {
+    (void) pr_fsio_close(displayfilexfer_fh);
+    displayfilexfer_fh = NULL;
+  }
 
-    } else {
-      /* A download is occurring... */
-      pr_trace_msg(trace_channel, 6, "session exiting, aborting download");
-      retr_abort();
-    }
+  res = xfer_sess_init();
+  if (res < 0) {
+    pr_session_disconnect(&xfer_module,
+      PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
+  }
+}
 
-    pr_data_abort(0, FALSE);
+static void xfer_sigusr2_ev(const void *event_data, void *user_data) {
 
-    cmd = pr_cmd_alloc(session.pool, 2, session.curr_cmd, session.xfer.path);
-    (void) pr_cmd_dispatch_phase(cmd, POST_CMD_ERR, 0);
-    (void) pr_cmd_dispatch_phase(cmd, LOG_CMD_ERR, 0);
+  if (pr_module_exists("mod_shaper.c")) {
+    /* Only do this if we're currently involved in a data transfer.
+     * This is a hack put in to support mod_shaper's antics.
+     */
+    if (session.curr_cmd_id == PR_CMD_APPE_ID ||
+        session.curr_cmd_id == PR_CMD_RETR_ID ||
+        session.curr_cmd_id == PR_CMD_STOR_ID ||
+        session.curr_cmd_id == PR_CMD_STOU_ID) {
+      pool *tmp_pool;
+      cmd_rec *cmd;
+
+      tmp_pool = make_sub_pool(session.pool);
+      pr_pool_tag(tmp_pool, "Data Transfer SIGUSR2 pool");
+
+      cmd = pr_cmd_alloc(tmp_pool, 1, session.curr_cmd);
+
+      /* Rescan the config tree for TransferRates, picking up any possible
+       * changes.
+       */
+      pr_log_debug(DEBUG2, "rechecking TransferRates");
+      pr_throttle_init(cmd);
+
+      destroy_pool(tmp_pool);
+    }
   }
 
   return;
 }
 
-static void xfer_xfer_stalled_ev(const void *event_data, void *user_data) {
-  if (!(session.sf_flags & SF_XFER)) {
-    if (session.xfer.direction == PR_NETIO_IO_RD) {
-      pr_trace_msg(trace_channel, 6, "transfer stalled, aborting upload");
-      stor_abort();
+static void xfer_timedout(const char *reason) {
+  if (stor_fh != NULL) {
+    pr_trace_msg(trace_channel, 6, "%s, aborting upload", reason);
+    stor_abort();
 
-    } else {
-      pr_trace_msg(trace_channel, 6, "transfer stalled, aborting download");
-      retr_abort();
-    }
+  } else if (retr_fh != NULL) {
+    pr_trace_msg(trace_channel, 6, "%s, aborting download", reason);
+    retr_abort();
   }
+}
 
-  /* The "else" case, for a stalled transfer, will be handled by the
-   * 'core.exit' event handler above.  In that case, a data transfer
-   * _will_ have actually been in progress, whereas in the !SF_XFER
-   * case, the client requested a transfer, but never actually opened
-   * the data connection.
+static void xfer_timeout_session_ev(const void *event_data, void *user_data) {
+  xfer_timedout("session timeout");
+}
+
+static void xfer_timeout_stalled_ev(const void *event_data, void *user_data) {
+  /* In this event handler, the "else" case, for a stalled transfer, will
+   * be handled by the 'core.exit' event handler above.  For in that
+   * scenario, a data transfer WILL have actually been in progress,
+   * whereas in the !SF_XFER case, the client requested a transfer, but
+   * never actually opened the data connection.
    */
 
-  return;
+  if (!(session.sf_flags & SF_XFER)) {
+    xfer_timedout("transfer stalled");
+  }
 }
 
 /* Initialization routines
@@ -3386,11 +3944,16 @@ static int xfer_sess_init(void) {
 
   /* Exit handlers for HiddenStores cleanup */
   pr_event_register(&xfer_module, "core.exit", xfer_exit_ev, NULL);
-  pr_event_register(&xfer_module, "core.timeout-stalled",
-    xfer_xfer_stalled_ev, NULL);
-
+  pr_event_register(&xfer_module, "core.session-reinit", xfer_sess_reinit_ev,
+    NULL);
   pr_event_register(&xfer_module, "core.signal.USR2", xfer_sigusr2_ev,
     NULL);
+  pr_event_register(&xfer_module, "core.timeout-session",
+    xfer_timeout_session_ev, NULL);
+  pr_event_register(&xfer_module, "core.timeout-stalled",
+    xfer_timeout_stalled_ev, NULL);
+
+  have_type = FALSE;
 
   /* Look for a DisplayFileTransfer file which has an absolute path.  If we
    * find one, open a filehandle, such that that file can be displayed
@@ -3449,6 +4012,7 @@ static conftable xfer_conftab[] = {
   { "AllowOverwrite",		set_allowoverwrite,		NULL },
   { "AllowRetrieveRestart",	set_allowrestart,		NULL },
   { "AllowStoreRestart",	set_allowrestart,		NULL },
+  { "DefaultTransferMode",	set_defaulttransfermode,	NULL },
   { "DeleteAbortedStores",	set_deleteabortedstores,	NULL },
   { "DisplayFileTransfer",	set_displayfiletransfer,	NULL },
   { "HiddenStores",		set_hiddenstores,		NULL },
@@ -3459,6 +4023,7 @@ static conftable xfer_conftab[] = {
   { "StoreUniquePrefix",	set_storeuniqueprefix,		NULL },
   { "TimeoutNoTransfer",	set_timeoutnoxfer,		NULL },
   { "TimeoutStalled",		set_timeoutstalled,		NULL },
+  { "TransferOptions",		set_transferoptions,		NULL },
   { "TransferPriority",		set_transferpriority,		NULL },
   { "TransferRate",		set_transferrate,		NULL },
   { "UseSendfile",		set_usesendfile,		NULL },
@@ -3475,10 +4040,12 @@ static cmdtable xfer_cmdtab[] = {
   { CMD,     C_SMNT,	G_NONE,	 xfer_smnt,	TRUE,	FALSE, CL_MISC },
   { PRE_CMD, C_RETR,	G_READ,	 xfer_pre_retr,	TRUE,	FALSE },
   { CMD,     C_RETR,	G_READ,	 xfer_retr,	TRUE,	FALSE, CL_READ },
+  { POST_CMD,C_RETR,	G_NONE,  xfer_post_retr,FALSE,	FALSE },
   { LOG_CMD, C_RETR,	G_NONE,	 xfer_log_retr,	FALSE,  FALSE },
   { LOG_CMD_ERR, C_RETR,G_NONE,  xfer_err_cleanup,  FALSE,  FALSE },
   { PRE_CMD, C_STOR,	G_WRITE, xfer_pre_stor,	TRUE,	FALSE },
   { CMD,     C_STOR,	G_WRITE, xfer_stor,	TRUE,	FALSE, CL_WRITE },
+  { POST_CMD,C_STOR,	G_NONE,  xfer_post_stor,FALSE,	FALSE },
   { LOG_CMD, C_STOR,    G_NONE,	 xfer_log_stor,	FALSE,  FALSE },
   { LOG_CMD_ERR, C_STOR,G_NONE,  xfer_err_cleanup,  FALSE,  FALSE },
   { PRE_CMD, C_STOU,	G_WRITE, xfer_pre_stou,	TRUE,	FALSE },
@@ -3488,6 +4055,7 @@ static cmdtable xfer_cmdtab[] = {
   { LOG_CMD_ERR, C_STOU,G_NONE,  xfer_err_cleanup,  FALSE,  FALSE },
   { PRE_CMD, C_APPE,	G_WRITE, xfer_pre_appe,	TRUE,	FALSE },
   { CMD,     C_APPE,	G_WRITE, xfer_stor,	TRUE,	FALSE, CL_WRITE },
+  { POST_CMD,C_APPE,	G_NONE,  xfer_post_stor,FALSE,	FALSE },
   { LOG_CMD, C_APPE,	G_NONE,  xfer_log_stor,	FALSE,  FALSE },
   { LOG_CMD_ERR, C_APPE,G_NONE,  xfer_err_cleanup,  FALSE,  FALSE },
   { CMD,     C_ABOR,	G_NONE,	 xfer_abor,	TRUE,	TRUE,  CL_MISC  },
@@ -3495,7 +4063,6 @@ static cmdtable xfer_cmdtab[] = {
   { CMD,     C_REST,	G_NONE,	 xfer_rest,	TRUE,	FALSE, CL_MISC  },
   { POST_CMD,C_PROT,	G_NONE,  xfer_post_prot,	FALSE,	FALSE },
   { POST_CMD,C_PASS,	G_NONE,	 xfer_post_pass,	FALSE, FALSE },
-  { POST_CMD,C_HOST,	G_NONE,	 xfer_post_host,	FALSE, FALSE },
   { 0, NULL }
 };
 
diff --git a/src/Makefile.in b/src/Makefile.in
index 3a42ccb..a27f5c2 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -22,8 +22,11 @@ Makefile: Makefile.in ../config.status
 src: $(OBJS) $(FTPDCTL_OBJS)
 
 clean:
-	rm -f *.o
+	$(RM) -f *.o
 
 depend:
 	$(MAKEDEPEND) $(CPPFLAGS) *.c 2>/dev/null
 	$(MAKEDEPEND) $(CPPFLAGS) -fMakefile.in *.c 2>/dev/null
+
+distclean: clean
+	-$(RM) *.gcda *.gcno
diff --git a/src/ascii.c b/src/ascii.c
new file mode 100644
index 0000000..a022dbc
--- /dev/null
+++ b/src/ascii.c
@@ -0,0 +1,180 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2015-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* FTP ASCII conversions. */
+
+#include "conf.h"
+
+int pr_ascii_ftp_from_crlf(pool *p, char *in, size_t inlen, char **out,
+    size_t *outlen) {
+  char *src, *dst;
+  size_t rem;
+  int adj;
+
+  (void) p;
+
+  if (in == NULL ||
+      out == NULL ||
+      outlen == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (inlen == 0) {
+    *outlen = inlen;
+    return 0;
+  }
+
+  src = in;
+  rem = inlen;
+  dst = *out;
+  adj = 0;
+
+  while (rem--) {
+    if (*src != '\r') {
+      *dst++ = *src++;
+      (*outlen)++;
+
+    } else {
+      if (rem == 0) {
+        /* copy, but save it for later */
+        adj++;
+        *dst++ = *src++;
+
+      } else {
+        if (*(src+1) == '\n') {
+          /* Skip the CR. */
+          src++;
+
+        } else {
+          *dst++ = *src++;
+          (*outlen)++;
+        }
+      }
+    }
+  }
+
+  return adj;
+}
+
+static int have_dangling_cr = FALSE;
+
+/* This function rewrites the contents of the given buffer, making sure that
+ * each LF has a preceding CR, as required by RFC959.
+ */
+int pr_ascii_ftp_to_crlf(pool *p, char *in, size_t inlen, char **out,
+    size_t *outlen) {
+  register unsigned int i = 0, j = 0;
+  char *dst = NULL, *src;
+  size_t src_len, lf_pos;
+
+  if (p == NULL ||
+      in == NULL ||
+      out == NULL ||
+      outlen == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (inlen == 0) {
+    *out = in;
+    return 0;
+  }
+
+  src = in;
+  src_len = lf_pos = inlen;
+
+  /* First, determine the position of the first bare LF. */
+  if (have_dangling_cr == FALSE &&
+      src[0] == '\n') {
+    lf_pos = 0;
+    goto found_lf;
+  }
+
+  for (i = 1; i < src_len; i++) {
+    if (src[i] == '\n' &&
+        src[i-1] != '\r') {
+      lf_pos = i;
+      break;
+    }
+  }
+
+found_lf:
+  /* If the last character in the buffer is CR, then we have a dangling CR.
+   * The first character in the next buffer could be an LF, and without
+   * this flag, that LF would be treated as a bare LF, thus resulting in
+   * an added extraneous CR in the stream.
+   */
+  have_dangling_cr = (src[src_len-1] == '\r') ? TRUE : FALSE;
+
+  if (lf_pos == src_len) {
+    /* No translation needed. */
+    *out = in;
+    *outlen = inlen;
+    return 0;
+  }
+
+  /* Assume the worst: a block containing only LF characters, needing twice
+   * the size for holding the corresponding CRs.
+   */
+  dst = malloc(src_len * 2);
+  if (dst == NULL) {
+    pr_log_pri(PR_LOG_ALERT, "Out of memory!");
+    exit(1);
+  }
+
+  if (lf_pos > 0) {
+    memcpy(dst, src, lf_pos);
+    i = j = lf_pos;
+
+  } else {
+    dst[0] = '\r';
+    dst[1] = '\n';
+    i = 2;
+    j = 1;
+  }
+
+  while (j < src_len) {
+    if (src[j] == '\n' &&
+        src[j-1] != '\r') {
+      dst[i++] = '\r';
+    }
+
+    dst[i++] = src[j++];
+  }
+  pr_signals_handle();
+
+  *outlen = i;
+  *out = pcalloc(p, *outlen);
+  memcpy(*out, dst, i);
+
+  free(dst);
+  dst = NULL;
+
+  return i - j;
+}
+
+void pr_ascii_ftp_reset(void) {
+  have_dangling_cr = FALSE;
+}
diff --git a/src/auth.c b/src/auth.c
index b6727fb..149ecf0 100644
--- a/src/auth.c
+++ b/src/auth.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2016 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,13 +24,15 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Authentication front-end for ProFTPD */
+/* Authentication front-end for ProFTPD. */
 
 #include "conf.h"
 #include "privs.h"
 
 static pool *auth_pool = NULL;
-static pr_table_t *auth_tab = NULL, *uid_tab = NULL, *gid_tab = NULL;
+static size_t auth_max_passwd_len = PR_TUNABLE_PASSWORD_MAX;
+static pr_table_t *auth_tab = NULL, *uid_tab = NULL, *user_tab = NULL,
+  *gid_tab = NULL, *group_tab = NULL;
 static xaset_t *auth_module_list = NULL;
 
 struct auth_module_elt {
@@ -43,7 +45,7 @@ static const char *trace_channel = "auth";
 /* Caching of ID-to-name lookups, for both UIDs and GIDs, is enabled by
  * default.
  */
-static unsigned int auth_caching = PR_AUTH_CACHE_FL_UID2NAME|PR_AUTH_CACHE_FL_GID2NAME|PR_AUTH_CACHE_FL_AUTH_MODULE;
+static unsigned int auth_caching = PR_AUTH_CACHE_FL_DEFAULT;
 
 /* Key comparison callback for the uidcache and gidcache. */
 static int uid_keycmp_cb(const void *key1, size_t keysz1,
@@ -82,9 +84,8 @@ static unsigned int gid_hash_cb(const void *key, size_t keysz) {
 }
 
 static void uidcache_create(void) {
-  if ((auth_caching & PR_AUTH_CACHE_FL_UID2NAME) &&
-      !uid_tab &&
-      auth_pool) {
+  if (uid_tab == NULL &&
+      auth_pool != NULL) {
     int ok = TRUE;
 
     uid_tab = pr_table_alloc(auth_pool, 0);
@@ -115,19 +116,16 @@ static void uidcache_create(void) {
 }
 
 static void uidcache_add(uid_t uid, const char *name) {
-  if (!(auth_caching & PR_AUTH_CACHE_FL_UID2NAME)) {
-    return;
-  }
-
   uidcache_create();
 
-  if (uid_tab) {
+  if (uid_tab != NULL) {
     int count;
 
     (void) pr_table_rewind(uid_tab);
     count = pr_table_kexists(uid_tab, (const void *) &uid, sizeof(uid_t));
     if (count <= 0) {
       uid_t *cache_uid;
+      size_t namelen;
 
       /* Allocate memory for a UID out of the ID cache pool, so that this
        * UID can be used as a key.
@@ -135,26 +133,51 @@ static void uidcache_add(uid_t uid, const char *name) {
       cache_uid = palloc(auth_pool, sizeof(uid_t));
       *cache_uid = uid;
 
+      namelen = strlen(name);
+
       if (pr_table_kadd(uid_tab, (const void *) cache_uid, sizeof(uid_t),
-          pstrdup(auth_pool, name), strlen(name) + 1) < 0 &&
+          pstrndup(auth_pool, name, namelen), namelen + 1) < 0 &&
           errno != EEXIST) {
         pr_trace_msg(trace_channel, 3,
-          "error adding name '%s' for UID %lu to the uidcache: %s", name,
-          (unsigned long) uid, strerror(errno));
+          "error adding name '%s' for UID %s to the uidcache: %s", name,
+          pr_uid2str(NULL, uid), strerror(errno));
 
       } else {
         pr_trace_msg(trace_channel, 5,
-          "stashed name '%s' for UID %lu in the uidcache", name,
-          (unsigned long) uid);
+          "stashed name '%s' for UID %s in the uidcache", name,
+          pr_uid2str(NULL, uid));
       }
     }
   }
 }
 
+static int uidcache_get(uid_t uid, char *name, size_t namesz) {
+  if (uid_tab != NULL) {
+    const void *v = NULL;
+
+    v = pr_table_kget(uid_tab, (const void *) &uid, sizeof(uid_t), NULL);
+    if (v != NULL) {
+      memset(name, '\0', namesz);
+      sstrncpy(name, v, namesz);
+
+      pr_trace_msg(trace_channel, 8,
+        "using name '%s' from uidcache for UID %s", name,
+        pr_uid2str(NULL, uid));
+      return 0;
+    }
+
+   pr_trace_msg(trace_channel, 9,
+      "no value found in uidcache for UID %s: %s", pr_uid2str(NULL, uid),
+      strerror(errno));
+  }
+
+  errno = ENOENT;
+  return -1;
+}
+
 static void gidcache_create(void) {
-  if ((auth_caching & PR_AUTH_CACHE_FL_GID2NAME) &&
-      !gid_tab &&
-      auth_pool) {
+  if (gid_tab == NULL&&
+      auth_pool != NULL) {
     int ok = TRUE;
 
     gid_tab = pr_table_alloc(auth_pool, 0);
@@ -185,19 +208,16 @@ static void gidcache_create(void) {
 }
 
 static void gidcache_add(gid_t gid, const char *name) {
-  if (!(auth_caching & PR_AUTH_CACHE_FL_GID2NAME)) {
-    return;
-  }
-
   gidcache_create();
 
-  if (gid_tab) {
+  if (gid_tab != NULL) {
     int count;
 
     (void) pr_table_rewind(gid_tab);
     count = pr_table_kexists(gid_tab, (const void *) &gid, sizeof(gid_t));
     if (count <= 0) {
       gid_t *cache_gid;
+      size_t namelen;
 
       /* Allocate memory for a GID out of the ID cache pool, so that this
        * GID can be used as a key.
@@ -205,22 +225,174 @@ static void gidcache_add(gid_t gid, const char *name) {
       cache_gid = palloc(auth_pool, sizeof(gid_t));
       *cache_gid = gid;
 
+      namelen = strlen(name);
+
       if (pr_table_kadd(gid_tab, (const void *) cache_gid, sizeof(gid_t),
-          pstrdup(auth_pool, name), strlen(name) + 1) < 0 &&
+          pstrndup(auth_pool, name, namelen), namelen + 1) < 0 &&
+          errno != EEXIST) {
+        pr_trace_msg(trace_channel, 3,
+          "error adding name '%s' for GID %s to the gidcache: %s", name,
+          pr_gid2str(NULL, gid), strerror(errno));
+
+      } else {
+        pr_trace_msg(trace_channel, 5,
+          "stashed name '%s' for GID %s in the gidcache", name,
+          pr_gid2str(NULL, gid));
+      }
+    }
+  }
+}
+
+static int gidcache_get(gid_t gid, char *name, size_t namesz) {
+  if (gid_tab != NULL) {
+    const void *v = NULL;
+
+    v = pr_table_kget(gid_tab, (const void *) &gid, sizeof(gid_t), NULL);
+    if (v != NULL) {
+      memset(name, '\0', namesz);
+      sstrncpy(name, v, namesz);
+
+      pr_trace_msg(trace_channel, 8,
+        "using name '%s' from gidcache for GID %s", name,
+        pr_gid2str(NULL, gid));
+      return 0;
+    }
+
+   pr_trace_msg(trace_channel, 9,
+      "no value found in gidcache for GID %s: %s", pr_gid2str(NULL, gid),
+      strerror(errno));
+  }
+
+  errno = ENOENT;
+  return -1;
+}
+
+static void usercache_create(void) {
+  if (user_tab == NULL &&
+      auth_pool != NULL) {
+    user_tab = pr_table_alloc(auth_pool, 0);
+  }
+}
+
+static void usercache_add(const char *name, uid_t uid) {
+  usercache_create();
+
+  if (user_tab != NULL) {
+    int count;
+
+    (void) pr_table_rewind(user_tab);
+    count = pr_table_exists(user_tab, name);
+    if (count <= 0) {
+      const char *cache_name;
+      uid_t *cache_key;
+
+      /* Allocate memory for a key out of the ID cache pool, so that this
+       * name can be used as a key.
+       */
+      cache_name = pstrdup(auth_pool, name); 
+      cache_key = palloc(auth_pool, sizeof(uid_t));
+      *cache_key = uid;
+
+      if (pr_table_add(user_tab, cache_name, cache_key, sizeof(uid_t)) < 0 &&
+          errno != EEXIST) {
+        pr_trace_msg(trace_channel, 3,
+          "error adding UID %s for user '%s' to the usercache: %s",
+          pr_uid2str(NULL, uid), name, strerror(errno));
+
+      } else {
+        pr_trace_msg(trace_channel, 5,
+          "stashed UID %s for user '%s' in the usercache",
+          pr_uid2str(NULL, uid), name);
+      }
+    }
+  }
+}
+
+static int usercache_get(const char *name, uid_t *uid) {
+  if (user_tab != NULL) {
+    const void *v = NULL;
+
+    v = pr_table_get(user_tab, name, NULL);
+    if (v != NULL) {
+      *uid = *((uid_t *) v);
+
+      pr_trace_msg(trace_channel, 8,
+        "using UID %s for user '%s' from usercache", pr_uid2str(NULL, *uid),
+        name);
+      return 0;
+    }
+
+   pr_trace_msg(trace_channel, 9,
+      "no value found in usercache for user '%s': %s", name, strerror(errno));
+  }
+
+  errno = ENOENT;
+  return -1;
+}
+
+static void groupcache_create(void) {
+  if (group_tab == NULL &&
+      auth_pool != NULL) {
+    group_tab = pr_table_alloc(auth_pool, 0);
+  }
+}
+
+static void groupcache_add(const char *name, gid_t gid) {
+  groupcache_create();
+
+  if (group_tab != NULL) {
+    int count;
+
+    (void) pr_table_rewind(group_tab);
+    count = pr_table_exists(group_tab, name);
+    if (count <= 0) {
+      const char *cache_name;
+      gid_t *cache_key;
+
+      /* Allocate memory for a key out of the ID cache pool, so that this
+       * name can be used as a key.
+       */
+      cache_name = pstrdup(auth_pool, name); 
+      cache_key = palloc(auth_pool, sizeof(gid_t));
+      *cache_key = gid;
+
+      if (pr_table_add(group_tab, cache_name, cache_key, sizeof(gid_t)) < 0 &&
           errno != EEXIST) {
         pr_trace_msg(trace_channel, 3,
-          "error adding name '%s' for GID %lu to the gidcache: %s", name,
-          (unsigned long) gid, strerror(errno));
+          "error adding GID %s for group '%s' to the groupcache: %s",
+          pr_gid2str(NULL, gid), name, strerror(errno));
 
       } else {
         pr_trace_msg(trace_channel, 5,
-          "stashed name '%s' for GID %lu in the gidcache", name,
-          (unsigned long) gid);
+          "stashed GID %s for group '%s' in the groupcache",
+          pr_gid2str(NULL, gid), name);
       }
     }
   }
 }
 
+static int groupcache_get(const char *name, gid_t *gid) {
+  if (group_tab != NULL) {
+    const void *v = NULL;
+
+    v = pr_table_get(group_tab, name, NULL);
+    if (v != NULL) {
+      *gid = *((gid_t *) v);
+
+      pr_trace_msg(trace_channel, 8,
+        "using GID %s for group '%s' from groupcache", pr_gid2str(NULL, *gid),
+        name);
+      return 0;
+    }
+
+   pr_trace_msg(trace_channel, 9,
+      "no value found in groupcache for group '%s': %s", name, strerror(errno));
+  }
+
+  errno = ENOENT;
+  return -1;
+}
+
 /* The difference between this function, and pr_cmd_alloc(), is that this
  * allocates the cmd_rec directly from the given pool, whereas pr_cmd_alloc()
  * will allocate a subpool from the given pool, and allocate its cmd_rec
@@ -228,17 +400,17 @@ static void gidcache_add(gid_t gid, const char *name) {
  * subsequently destroyed easily; this function's cmd_rec's will be destroyed
  * when the given pool is destroyed.
  */
-static cmd_rec *make_cmd(pool *cp, int argc, ...) {
+static cmd_rec *make_cmd(pool *cp, unsigned int argc, ...) {
   va_list args;
   cmd_rec *c;
   pool *sub_pool;
 
   c = pcalloc(cp, sizeof(cmd_rec));
-
   c->argc = argc;
   c->stash_index = -1;
+  c->stash_hash = 0;
 
-  if (argc) {
+  if (argc > 0) {
     register unsigned int i;
 
     c->argv = pcalloc(cp, sizeof(void *) * (argc + 1));
@@ -265,8 +437,8 @@ static modret_t *dispatch_auth(cmd_rec *cmd, char *match, module **m) {
   authtable *start_tab = NULL, *iter_tab = NULL;
   modret_t *mr = NULL;
 
-  start_tab = pr_stash_get_symbol(PR_SYM_AUTH, match, NULL,
-    &cmd->stash_index);
+  start_tab = pr_stash_get_symbol2(PR_SYM_AUTH, match, NULL,
+    &cmd->stash_index, &cmd->stash_hash);
   if (start_tab == NULL) {
     int xerrno = errno;
 
@@ -318,8 +490,8 @@ static modret_t *dispatch_auth(cmd_rec *cmd, char *match, module **m) {
     }
 
   next:
-    iter_tab = pr_stash_get_symbol(PR_SYM_AUTH, match, iter_tab,
-      &cmd->stash_index);
+    iter_tab = pr_stash_get_symbol2(PR_SYM_AUTH, match, iter_tab,
+      &cmd->stash_index, &cmd->stash_hash);
 
     if (iter_tab == start_tab) {
       /* We have looped back to the start.  Break out now and do not loop
@@ -360,7 +532,12 @@ void pr_auth_endpwent(pool *p) {
   }
 
   if (auth_tab) {
-    pr_trace_msg(trace_channel, 5, "emptying authcache");
+    int item_count;
+
+    item_count = pr_table_count(auth_tab);
+    pr_trace_msg(trace_channel, 5, "emptying authcache (%d %s)", item_count,
+      item_count != 1 ? "items" : "item");
+
     (void) pr_table_empty(auth_tab);
     (void) pr_table_free(auth_tab);
     auth_tab = NULL;
@@ -402,6 +579,11 @@ struct passwd *pr_auth_getpwent(pool *p) {
   modret_t *mr = NULL;
   struct passwd *res = NULL;
 
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   cmd = make_cmd(p, 0);
   mr = dispatch_auth(cmd, "getpwent", NULL);
 
@@ -416,17 +598,20 @@ struct passwd *pr_auth_getpwent(pool *p) {
   }
 
   /* Sanity check */
-  if (res == NULL)
+  if (res == NULL) {
     return NULL;
+  }
 
   /* Make sure the UID and GID are not -1 */
   if (res->pw_uid == (uid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: UID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
   if (res->pw_gid == (gid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: GID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
@@ -438,6 +623,11 @@ struct group *pr_auth_getgrent(pool *p) {
   modret_t *mr = NULL;
   struct group *res = NULL;
 
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   cmd = make_cmd(p, 0);
   mr = dispatch_auth(cmd, "getgrent", NULL);
 
@@ -452,12 +642,14 @@ struct group *pr_auth_getgrent(pool *p) {
   }
 
   /* Sanity check */
-  if (res == NULL)
+  if (res == NULL) {
     return NULL;
+  }
 
   /* Make sure the GID is not -1 */
   if (res->gr_gid == (gid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: GID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
@@ -470,6 +662,12 @@ struct passwd *pr_auth_getpwnam(pool *p, const char *name) {
   struct passwd *res = NULL;
   module *m = NULL;
 
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   cmd = make_cmd(p, 1, name);
   mr = dispatch_auth(cmd, "getpwnam", &m);
 
@@ -492,11 +690,13 @@ struct passwd *pr_auth_getpwnam(pool *p, const char *name) {
   /* Make sure the UID and GID are not -1 */
   if (res->pw_uid == (uid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: UID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
   if (res->pw_gid == (gid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: GID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
@@ -543,13 +743,19 @@ struct passwd *pr_auth_getpwnam(pool *p, const char *name) {
     }
   }
 
-  uidcache_add(res->pw_uid, res->pw_name);
+  if (auth_caching & PR_AUTH_CACHE_FL_UID2NAME) {
+    uidcache_add(res->pw_uid, res->pw_name);
+  }
+
+  if (auth_caching & PR_AUTH_CACHE_FL_NAME2UID) {
+    usercache_add(res->pw_name, res->pw_uid);
+  }
 
   /* Get the (possibly rewritten) home directory. */
-  res->pw_dir = pr_auth_get_home(p, res->pw_dir);
+  res->pw_dir = (char *) pr_auth_get_home(p, res->pw_dir);
 
-  pr_log_debug(DEBUG10, "retrieved UID %lu for user '%s'",
-    (unsigned long) res->pw_uid, name);
+  pr_log_debug(DEBUG10, "retrieved UID %s for user '%s'",
+    pr_uid2str(NULL, res->pw_uid), name);
   return res;
 }
 
@@ -558,6 +764,11 @@ struct passwd *pr_auth_getpwuid(pool *p, uid_t uid) {
   modret_t *mr = NULL;
   struct passwd *res = NULL;
 
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   cmd = make_cmd(p, 1, (void *) &uid);
   mr = dispatch_auth(cmd, "getpwuid", NULL);
 
@@ -580,16 +791,18 @@ struct passwd *pr_auth_getpwuid(pool *p, uid_t uid) {
   /* Make sure the UID and GID are not -1 */
   if (res->pw_uid == (uid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: UID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
   if (res->pw_gid == (gid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: GID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
-  pr_log_debug(DEBUG10, "retrieved user '%s' for UID %lu",
-    res->pw_name, (unsigned long) uid);
+  pr_log_debug(DEBUG10, "retrieved user '%s' for UID %s",
+    res->pw_name, pr_uid2str(NULL, uid));
   return res;
 }
 
@@ -598,6 +811,12 @@ struct group *pr_auth_getgrnam(pool *p, const char *name) {
   modret_t *mr = NULL;
   struct group *res = NULL;
 
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   cmd = make_cmd(p, 1, name);
   mr = dispatch_auth(cmd, "getgrnam", NULL);
 
@@ -620,13 +839,20 @@ struct group *pr_auth_getgrnam(pool *p, const char *name) {
   /* Make sure the GID is not -1 */
   if (res->gr_gid == (gid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: GID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
-  gidcache_add(res->gr_gid, name);
+  if (auth_caching & PR_AUTH_CACHE_FL_GID2NAME) {
+    gidcache_add(res->gr_gid, name);
+  }
+
+  if (auth_caching & PR_AUTH_CACHE_FL_NAME2GID) {
+    groupcache_add(name, res->gr_gid);
+  }
 
-  pr_log_debug(DEBUG10, "retrieved GID %lu for group '%s'",
-    (unsigned long) res->gr_gid, name);
+  pr_log_debug(DEBUG10, "retrieved GID %s for group '%s'",
+    pr_gid2str(NULL, res->gr_gid), name);
   return res;
 }
 
@@ -635,6 +861,11 @@ struct group *pr_auth_getgrgid(pool *p, gid_t gid) {
   modret_t *mr = NULL;
   struct group *res = NULL;
 
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   cmd = make_cmd(p, 1, (void *) &gid);
   mr = dispatch_auth(cmd, "getgrgid", NULL);
 
@@ -657,6 +888,7 @@ struct group *pr_auth_getgrgid(pool *p, gid_t gid) {
   /* Make sure the GID is not -1 */
   if (res->gr_gid == (gid_t) -1) {
     pr_log_pri(PR_LOG_WARNING, "error: GID of -1 not allowed");
+    errno = ENOENT;
     return NULL;
   }
 
@@ -665,12 +897,90 @@ struct group *pr_auth_getgrgid(pool *p, gid_t gid) {
   return res;
 }
 
+static const char *get_authcode_str(int auth_code) {
+  const char *name = "(unknown)";
+
+  switch (auth_code) {
+    case PR_AUTH_OK_NO_PASS:
+      name = "OK_NO_PASS";
+      break;
+
+    case PR_AUTH_RFC2228_OK:
+      name = "RFC2228_OK";
+      break;
+
+    case PR_AUTH_OK:
+      name = "OK";
+      break;
+
+    case PR_AUTH_ERROR:
+      name = "ERROR";
+      break;
+
+    case PR_AUTH_NOPWD:
+      name = "NOPWD";
+      break;
+
+    case PR_AUTH_BADPWD:
+      name = "BADPWD";
+      break;
+
+    case PR_AUTH_AGEPWD:
+      name = "AGEPWD";
+      break;
+
+    case PR_AUTH_DISABLEDPWD:
+      name = "DISABLEDPWD";
+      break;
+
+    case PR_AUTH_CRED_INSUFFICIENT:
+      name = "CRED_INSUFFICIENT";
+      break;
+
+    case PR_AUTH_CRED_UNAVAIL:
+      name = "CRED_UNAVAIL";
+      break;
+
+    case PR_AUTH_CRED_ERROR:
+      name = "CRED_ERROR";
+      break;
+
+    case PR_AUTH_INFO_UNAVAIL:
+      name = "INFO_UNAVAIL";
+      break;
+
+    case PR_AUTH_MAX_ATTEMPTS_EXCEEDED:
+      name = "MAX_ATTEMPTS_EXCEEDED";
+      break;
+
+    case PR_AUTH_INIT_ERROR:
+      name = "INIT_ERROR";
+      break;
+
+    case PR_AUTH_NEW_TOKEN_REQUIRED:
+      name = "NEW_TOKEN_REQUIRED";
+      break;
+
+    default:
+      break;
+  }
+
+  return name;
+}
+
 int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
   module *m = NULL;
   int res = PR_AUTH_NOPWD;
 
+  if (p == NULL ||
+      name == NULL ||
+      pw == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   cmd = make_cmd(p, 2, name, pw);
 
   /* First, check for any of the modules in the "authenticating only" list
@@ -682,6 +992,7 @@ int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
 
     for (elt = (struct auth_module_elt *) auth_module_list->xas_list; elt;
         elt = elt->next) {
+      pr_signals_handle();
 
       pr_trace_msg(trace_channel, 7, "checking with auth-only module '%s'",
         elt->name);
@@ -701,6 +1012,9 @@ int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
             cmd->tmp_pool = NULL;
           }
 
+          pr_trace_msg(trace_channel, 9,
+            "module '%s' returned HANDLED (%s) for authenticating user '%s'",
+            elt->name, get_authcode_str(res), name);
           return res;
         }
 
@@ -712,6 +1026,9 @@ int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
             cmd->tmp_pool = NULL;
           }
 
+          pr_trace_msg(trace_channel, 9,
+            "module '%s' returned ERROR (%s) for authenticating user '%s'",
+            elt->name, get_authcode_str(res), name);
           return res;
         }
 
@@ -720,11 +1037,12 @@ int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
     }
   }
 
-  if (auth_tab) {
+  if (auth_tab != NULL) {
+    const void *v;
 
     /* Fetch the specific module to be used for authenticating this user. */
-    void *v = pr_table_get(auth_tab, name, NULL);
-    if (v) {
+    v = pr_table_get(auth_tab, name, NULL);
+    if (v != NULL) {
       m = *((module **) v);
 
       pr_trace_msg(trace_channel, 4,
@@ -737,9 +1055,15 @@ int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
 
   if (MODRET_ISHANDLED(mr)) {
     res = MODRET_HASDATA(mr) ? PR_AUTH_RFC2228_OK : PR_AUTH_OK;
+    pr_trace_msg(trace_channel, 9,
+      "obtained HANDLED (%s) for authenticating user '%s'",
+      get_authcode_str(res), name);
 
   } else if (MODRET_ISERROR(mr)) {
     res = MODRET_ERROR(mr);
+    pr_trace_msg(trace_channel, 9,
+      "obtained ERROR (%s) for authenticating user '%s'", get_authcode_str(res),
+      name);
   }
 
   if (cmd->tmp_pool) {
@@ -756,13 +1080,20 @@ int pr_auth_authorize(pool *p, const char *name) {
   module *m = NULL;
   int res = PR_AUTH_OK;
 
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   cmd = make_cmd(p, 1, name);
 
-  if (auth_tab) {
+  if (auth_tab != NULL) {
+    const void *v;
 
     /* Fetch the specific module to be used for authenticating this user. */
-    void *v = pr_table_get(auth_tab, name, NULL);
-    if (v) {
+    v = pr_table_get(auth_tab, name, NULL);
+    if (v != NULL) {
       m = *((module **) v);
 
       pr_trace_msg(trace_channel, 4,
@@ -782,6 +1113,9 @@ int pr_auth_authorize(pool *p, const char *name) {
 
   if (MODRET_ISERROR(mr)) {
     res = MODRET_ERROR(mr);
+    pr_trace_msg(trace_channel, 9,
+      "obtained ERROR (%s) for authorizing user '%s'", get_authcode_str(res),
+      name);
   }
 
   if (cmd->tmp_pool) {
@@ -792,13 +1126,35 @@ int pr_auth_authorize(pool *p, const char *name) {
   return res;
 }
 
-int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
+int pr_auth_check(pool *p, const char *ciphertext_passwd, const char *name,
+    const char *cleartext_passwd) {
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
   module *m = NULL;
   int res = PR_AUTH_BADPWD;
+  size_t cleartext_passwd_len = 0;
+
+  /* Note: it's possible for ciphertext_passwd to be NULL (mod_ldap might do
+   * this, for example), so we cannot enforce that it be non-NULL.
+   */
+
+  if (p == NULL ||
+      name == NULL ||
+      cleartext_passwd == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  cleartext_passwd_len = strlen(cleartext_passwd);
+  if (cleartext_passwd_len > auth_max_passwd_len) {
+    pr_log_auth(PR_LOG_INFO,
+      "client-provided password size exceeds MaxPasswordSize (%lu), "
+      "rejecting", (unsigned long) auth_max_passwd_len);
+    errno = EPERM;
+    return -1;
+  }
 
-  cmd = make_cmd(p, 3, cpw, name, pw);
+  cmd = make_cmd(p, 3, ciphertext_passwd, name, cleartext_passwd);
 
   /* First, check for any of the modules in the "authenticating only" list
    * of modules.  This is usually only mod_auth_pam, but other modules
@@ -809,6 +1165,7 @@ int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
 
     for (elt = (struct auth_module_elt *) auth_module_list->xas_list; elt;
         elt = elt->next) {
+      pr_signals_handle();
 
       m = pr_module_get(elt->name);
       if (m) {
@@ -825,6 +1182,9 @@ int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
             cmd->tmp_pool = NULL;
           }
 
+          pr_trace_msg(trace_channel, 9,
+            "module '%s' returned HANDLED (%s) for checking user '%s'",
+            elt->name, get_authcode_str(res), name);
           return res;
         }
 
@@ -836,6 +1196,9 @@ int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
             cmd->tmp_pool = NULL;
           }
 
+          pr_trace_msg(trace_channel, 9,
+            "module '%s' returned ERROR (%s) for checking user '%s'",
+            elt->name, get_authcode_str(res), name);
           return res;
         }
 
@@ -844,11 +1207,12 @@ int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
     }
   }
 
-  if (auth_tab) {
+  if (auth_tab != NULL) {
+    const void *v;
 
     /* Fetch the specific module to be used for authenticating this user. */
-    void *v = pr_table_get(auth_tab, name, NULL);
-    if (v) {
+    v = pr_table_get(auth_tab, name, NULL);
+    if (v != NULL) {
       m = *((module **) v);
 
       pr_trace_msg(trace_channel, 4,
@@ -861,6 +1225,9 @@ int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
 
   if (MODRET_ISHANDLED(mr)) {
     res = MODRET_HASDATA(mr) ? PR_AUTH_RFC2228_OK : PR_AUTH_OK;
+    pr_trace_msg(trace_channel, 9,
+      "obtained HANDLED (%s) for checking user '%s'", get_authcode_str(res),
+      name);
   }
 
   if (cmd->tmp_pool) {
@@ -876,6 +1243,12 @@ int pr_auth_requires_pass(pool *p, const char *name) {
   modret_t *mr;
   int res = TRUE;
 
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   cmd = make_cmd(p, 1, name);
   mr = dispatch_auth(cmd, "requires_pass", NULL);
 
@@ -895,35 +1268,24 @@ int pr_auth_requires_pass(pool *p, const char *name) {
 }
 
 const char *pr_auth_uid2name(pool *p, uid_t uid) {
-  static char namebuf[64];
+  static char namebuf[PR_TUNABLE_LOGIN_MAX+1];
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
   char *res = NULL;
+  unsigned int cache_lookup_flags = (PR_AUTH_CACHE_FL_UID2NAME|PR_AUTH_CACHE_FL_BAD_UID2NAME);
   int have_name = FALSE;
 
-  memset(namebuf, '\0', sizeof(namebuf));
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
 
   uidcache_create();
 
-  if ((auth_caching & PR_AUTH_CACHE_FL_UID2NAME) &&
-      uid_tab) {
-    void *v = NULL;
-
-    v = pr_table_kget(uid_tab, (const void *) &uid, sizeof(uid_t), NULL);
-    if (v) {
-      sstrncpy(namebuf, v, sizeof(namebuf));
-
-      pr_trace_msg(trace_channel, 8,
-        "using name '%s' from uidcache for UID %lu", namebuf,
-        (unsigned long) uid);
- 
+  if (auth_caching & cache_lookup_flags) {
+    if (uidcache_get(uid, namebuf, sizeof(namebuf)) == 0) {
       res = namebuf;
       return res;
-
-    } else {
-      pr_trace_msg(trace_channel, 9,
-        "no value found in uidcache for UID %lu: %s", (unsigned long) uid,
-        strerror(errno));
     }
   }
 
@@ -936,7 +1298,10 @@ const char *pr_auth_uid2name(pool *p, uid_t uid) {
     sstrncpy(namebuf, res, sizeof(namebuf));
     res = namebuf;
 
-    uidcache_add(uid, res);
+    if (auth_caching & PR_AUTH_CACHE_FL_UID2NAME) {
+      uidcache_add(uid, res);
+    }
+
     have_name = TRUE;
   }
 
@@ -946,43 +1311,37 @@ const char *pr_auth_uid2name(pool *p, uid_t uid) {
   }
 
   if (!have_name) {
+    /* TODO: This conversion is data type sensitive, per Bug#4164. */
     snprintf(namebuf, sizeof(namebuf)-1, "%lu", (unsigned long) uid);
+    res = namebuf;
+
+    if (auth_caching & PR_AUTH_CACHE_FL_BAD_UID2NAME) {
+      uidcache_add(uid, res);
+    }
   }
 
-  res = namebuf;
   return res;
 }
 
 const char *pr_auth_gid2name(pool *p, gid_t gid) {
+  static char namebuf[PR_TUNABLE_LOGIN_MAX+1];
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
-  static char namebuf[64];
   char *res = NULL;
+  unsigned int cache_lookup_flags = (PR_AUTH_CACHE_FL_GID2NAME|PR_AUTH_CACHE_FL_BAD_GID2NAME);
   int have_name = FALSE;
 
-  memset(namebuf, '\0', sizeof(namebuf));
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
 
   gidcache_create();
 
-  if ((auth_caching & PR_AUTH_CACHE_FL_GID2NAME) &&
-       gid_tab) {
-    void *v = NULL;
- 
-    v = pr_table_kget(gid_tab, (const void *) &gid, sizeof(gid_t), NULL);
-    if (v) {
-      sstrncpy(namebuf, v, sizeof(namebuf));
-
-      pr_trace_msg(trace_channel, 8,
-        "using name '%s' from gidcache for GID %lu", namebuf,
-        (unsigned long) gid);
-
+  if (auth_caching & cache_lookup_flags) {
+    if (gidcache_get(gid, namebuf, sizeof(namebuf)) == 0) {
       res = namebuf;
       return res;
-
-    } else {
-      pr_trace_msg(trace_channel, 9,
-        "no value found in gidcache for GID %lu: %s", (unsigned long) gid,
-        strerror(errno));
     }
   }
 
@@ -995,7 +1354,10 @@ const char *pr_auth_gid2name(pool *p, gid_t gid) {
     sstrncpy(namebuf, res, sizeof(namebuf));
     res = namebuf;
 
-    gidcache_add(gid, res);
+    if (auth_caching & PR_AUTH_CACHE_FL_GID2NAME) {
+      gidcache_add(gid, res);
+    }
+
     have_name = TRUE;
   }
 
@@ -1005,10 +1367,15 @@ const char *pr_auth_gid2name(pool *p, gid_t gid) {
   }
 
   if (!have_name) {
+    /* TODO: This conversion is data type sensitive, per Bug#4164. */
     snprintf(namebuf, sizeof(namebuf)-1, "%lu", (unsigned long) gid);
+    res = namebuf;
+
+    if (auth_caching & PR_AUTH_CACHE_FL_BAD_GID2NAME) {
+      gidcache_add(gid, res);
+    }
   }
 
-  res = namebuf;
   return res;
 }
 
@@ -1016,15 +1383,46 @@ uid_t pr_auth_name2uid(pool *p, const char *name) {
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
   uid_t res = (uid_t) -1;
+  unsigned int cache_lookup_flags = (PR_AUTH_CACHE_FL_NAME2UID|PR_AUTH_CACHE_FL_BAD_NAME2UID);
+  int have_id = FALSE;
+
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return (uid_t) -1;
+  }
+
+  usercache_create();
+
+  if (auth_caching & cache_lookup_flags) {
+    uid_t cache_uid;
+
+    if (usercache_get(name, &cache_uid) == 0) {
+      res = cache_uid;
+
+      if (res == (uid_t) -1) {
+        errno = ENOENT;
+      }
+
+      return res;
+    }
+  }
 
   cmd = make_cmd(p, 1, name);
   mr = dispatch_auth(cmd, "name2uid", NULL);
 
-  if (MODRET_ISHANDLED(mr)) {
+  if (MODRET_ISHANDLED(mr) &&
+      MODRET_HASDATA(mr)) {
     res = *((uid_t *) mr->data);
 
+    if (auth_caching & PR_AUTH_CACHE_FL_NAME2UID) {
+      usercache_add(name, res);
+    }
+
+    have_id = TRUE;
+
   } else {
-    errno = EINVAL;
+    errno = ENOENT;
   }
 
   if (cmd->tmp_pool) {
@@ -1032,6 +1430,11 @@ uid_t pr_auth_name2uid(pool *p, const char *name) {
     cmd->tmp_pool = NULL;
   }
 
+  if (!have_id &&
+      (auth_caching & PR_AUTH_CACHE_FL_BAD_NAME2UID)) {
+    usercache_add(name, res);
+  }
+
   return res;
 }
 
@@ -1039,15 +1442,46 @@ gid_t pr_auth_name2gid(pool *p, const char *name) {
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
   gid_t res = (gid_t) -1;
+  unsigned int cache_lookup_flags = (PR_AUTH_CACHE_FL_NAME2GID|PR_AUTH_CACHE_FL_BAD_NAME2GID);
+  int have_id = FALSE;
+
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return (gid_t) -1;
+  }
+
+  groupcache_create();
+
+  if (auth_caching & cache_lookup_flags) {
+    gid_t cache_gid;
+
+    if (groupcache_get(name, &cache_gid) == 0) {
+      res = cache_gid;
+
+      if (res == (gid_t) -1) {
+        errno = ENOENT;
+      }
+
+      return res;
+    }
+  }
 
   cmd = make_cmd(p, 1, name);
   mr = dispatch_auth(cmd, "name2gid", NULL);
 
-  if (MODRET_ISHANDLED(mr)) {
+  if (MODRET_ISHANDLED(mr) &&
+      MODRET_HASDATA(mr)) {
     res = *((gid_t *) mr->data);
 
+    if (auth_caching & PR_AUTH_CACHE_FL_NAME2GID) {
+      groupcache_add(name, res);
+    }
+
+    have_id = TRUE;
+
   } else {
-    errno = EINVAL;
+    errno = ENOENT;
   }
 
   if (cmd->tmp_pool) {
@@ -1055,22 +1489,34 @@ gid_t pr_auth_name2gid(pool *p, const char *name) {
     cmd->tmp_pool = NULL;
   }
 
+  if (!have_id &&
+      (auth_caching & PR_AUTH_CACHE_FL_BAD_NAME2GID)) {
+    groupcache_add(name, res);
+  }
+
   return res;
 }
 
 int pr_auth_getgroups(pool *p, const char *name, array_header **group_ids,
     array_header **group_names) {
-
   cmd_rec *cmd = NULL;
   modret_t *mr = NULL;
   int res = -1;
 
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   /* Allocate memory for the array_headers of GIDs and group names. */
-  if (group_ids)
+  if (group_ids) {
     *group_ids = make_array(permanent_pool, 2, sizeof(gid_t));
+  }
 
-  if (group_names)
+  if (group_names) {
     *group_names = make_array(permanent_pool, 2, sizeof(char *));
+  }
 
   cmd = make_cmd(p, 3, name, group_ids ? *group_ids : NULL,
     group_names ? *group_names : NULL);
@@ -1094,12 +1540,9 @@ int pr_auth_getgroups(pool *p, const char *name, array_header **group_ids,
       gid_t *gids = (*group_ids)->elts;
 
       for (i = 0; i < (*group_ids)->nelts; i++) {
-        char buf[64];
-        snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) gids[i]);
-        buf[sizeof(buf)-1] = '\0';
-
         pr_signals_handle();
-        strgids = pstrcat(p, strgids, i != 0 ? ", " : "", buf, NULL);
+        strgids = pstrcat(p, strgids, i != 0 ? ", " : "",
+          pr_gid2str(NULL, gids[i]), NULL);
       }
 
       pr_log_debug(DEBUG10, "retrieved group %s: %s",
@@ -1132,7 +1575,7 @@ int pr_auth_getgroups(pool *p, const char *name, array_header **group_ids,
 }
 
 /* This is one messy function.  Yuck.  Yay legacy code. */
-config_rec *pr_auth_get_anon_config(pool *p, char **login_user,
+config_rec *pr_auth_get_anon_config(pool *p, const char **login_user,
     char **real_user, char **anon_name) {
   config_rec *c = NULL, *alias_config = NULL, *anon_config = NULL;
   char *config_user_name = NULL, *config_anon_name = NULL;
@@ -1188,10 +1631,10 @@ config_rec *pr_auth_get_anon_config(pool *p, char **login_user,
     c = alias_config;
   }
 
-  while (c && c->parent &&
-    (auth_alias_only = get_param_ptr(c->parent->subset, "AuthAliasOnly", FALSE))) {
+  while (c != NULL &&
+         c->parent != NULL &&
+         (auth_alias_only = get_param_ptr(c->parent->subset, "AuthAliasOnly", FALSE))) {
 
-    /* while() loops should always handle signals. */
     pr_signals_handle();
 
     /* If AuthAliasOnly is on, ignore this one and continue. */
@@ -1214,7 +1657,7 @@ config_rec *pr_auth_get_anon_config(pool *p, char **login_user,
     c = find_config_next2(c, c->next, CONF_PARAM, "UserAlias", TRUE,
       config_flags);
 
-    if (c &&
+    if (c != NULL &&
         (strncmp(c->argv[0], "*", 2) == 0 ||
          strcmp(c->argv[0], *login_user) == 0)) {
       is_alias = TRUE;
@@ -1348,23 +1791,32 @@ int pr_auth_banned_by_ftpusers(xaset_t *ctx, const char *user) {
   int res = FALSE;
   unsigned char *use_ftp_users;
 
-  use_ftp_users = get_param_ptr(ctx, "UseFtpUsers", FALSE);
+  if (user == NULL) {
+    return res;
+  }
 
+  use_ftp_users = get_param_ptr(ctx, "UseFtpUsers", FALSE);
   if (use_ftp_users == NULL ||
       *use_ftp_users == TRUE) {
     FILE *fh = NULL;
-    char buf[256];
+    char buf[512];
+    int xerrno;
 
     PRIVS_ROOT
     fh = fopen(PR_FTPUSERS_PATH, "r");
+    xerrno = errno;
     PRIVS_RELINQUISH
 
-    if (fh == NULL)
+    if (fh == NULL) {
+      pr_trace_msg(trace_channel, 14,
+        "error opening '%s' for checking user '%s': %s", PR_FTPUSERS_PATH,
+        user, strerror(xerrno));
       return res;
+    }
 
     memset(buf, '\0', sizeof(buf));
 
-    while (fgets(buf, sizeof(buf)-1, fh)) {
+    while (fgets(buf, sizeof(buf)-1, fh) != NULL) {
       char *ptr;
 
       pr_signals_handle();
@@ -1400,8 +1852,9 @@ int pr_auth_is_valid_shell(xaset_t *ctx, const char *shell) {
   int res = TRUE;
   unsigned char *require_valid_shell;
 
-  if (shell == NULL)
+  if (shell == NULL) {
     return res;
+  }
 
   require_valid_shell = get_param_ptr(ctx, "RequireValidShell", FALSE);
 
@@ -1411,13 +1864,14 @@ int pr_auth_is_valid_shell(xaset_t *ctx, const char *shell) {
     char buf[256];
 
     fh = fopen(PR_VALID_SHELL_PATH, "r");
-    if (fh == NULL)
+    if (fh == NULL) {
       return res;
+    }
 
     res = FALSE;
     memset(buf, '\0', sizeof(buf));
 
-    while (fgets(buf, sizeof(buf)-1, fh)) {
+    while (fgets(buf, sizeof(buf)-1, fh) != NULL) {
       pr_signals_handle();
 
       buf[sizeof(buf)-1] = '\0';
@@ -1440,23 +1894,37 @@ int pr_auth_is_valid_shell(xaset_t *ctx, const char *shell) {
 int pr_auth_chroot(const char *path) {
   int res, xerrno = 0;
   time_t now;
+  char *tz = NULL;
+  const char *default_tz;
 
-#if defined(HAVE_SETENV) && defined(__GLIBC__) && defined(__GLIBC_MINOR__) && \
-  __GLIBC__ == 2 && __GLIBC_MINOR__ >= 3
-  char *tz;
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+#if defined(__GLIBC__) && \
+    defined(__GLIBC_MINOR__) && \
+    __GLIBC__ == 2 && __GLIBC_MINOR__ >= 3
+  default_tz = tzname[0];
+#else
+  /* Per the tzset(3) man page, this should be the assumed default. */
+  default_tz = ":/etc/localtime";
+#endif
 
   tz = pr_env_get(session.pool, "TZ"); 
   if (tz == NULL) {
     if (pr_env_set(session.pool, "TZ", pstrdup(permanent_pool,
-        tzname[0])) < 0) { 
+        default_tz)) < 0) {
       pr_log_debug(DEBUG0, "error setting TZ environment variable to " 
-        "'%s': %s", tzname[0], strerror(errno));
+        "'%s': %s", default_tz, strerror(errno));
 
     } else {
-      pr_log_debug(DEBUG10, "set TZ environment variable to '%s'", tzname[0]);
+      pr_log_debug(DEBUG10, "set TZ environment variable to '%s'", default_tz);
     }
+
+  } else {
+    pr_log_debug(DEBUG10, "TZ environment variable already set to '%s'", tz);
   }
-#endif
 
   pr_log_debug(DEBUG1, "Preparing to chroot to directory '%s'", path);
 
@@ -1476,7 +1944,7 @@ int pr_auth_chroot(const char *path) {
 
   if (res < 0) {
     pr_log_pri(PR_LOG_ERR, "chroot to '%s' failed for user '%s': %s", path,
-      session.user, strerror(xerrno));
+      session.user ? session.user : "(unknown)", strerror(xerrno));
 
     errno = xerrno;
     return -1;
@@ -1495,6 +1963,21 @@ int set_groups(pool *p, gid_t primary_gid, array_header *suppl_gids) {
   gid_t *gids = NULL, *proc_gids = NULL;
   size_t ngids = 0, nproc_gids = 0;
   char *strgids = "";
+  int have_root_privs = TRUE;
+
+  /* First, check to see whether we even CAN set the process GIDs, which
+   * requires root privileges.
+   */
+  if (getuid() != PR_ROOT_UID) {
+    have_root_privs = FALSE;
+  }
+
+  if (have_root_privs == FALSE) {
+    pr_trace_msg(trace_channel, 3,
+      "unable to set groups due to lack of root privs");
+    errno = ENOSYS;
+    return -1;
+  }
 
   /* sanity check */
   if (p == NULL ||
@@ -1549,8 +2032,9 @@ int set_groups(pool *p, gid_t primary_gid, array_header *suppl_gids) {
       }
     }
 
-    if (!skip_gid)
+    if (!skip_gid) {
       proc_gids[nproc_gids++] = gids[i];
+    }
   }
 
   for (i = 0; i < nproc_gids; i++) {
@@ -1567,36 +2051,77 @@ int set_groups(pool *p, gid_t primary_gid, array_header *suppl_gids) {
   /* Set the supplemental groups. */
   res = setgroups(nproc_gids, proc_gids);
   if (res < 0) {
+    int xerrno = errno;
+
     destroy_pool(tmp_pool);
+
+    errno = xerrno;
     return res;
   }
 #endif /* !HAVE_SETGROUPS */
 
 #ifndef PR_DEVEL_COREDUMP
-  /* Set the primary GID of the process.
-   */
+  /* Set the primary GID of the process. */
   res = setgid(primary_gid);
   if (res < 0) {
-    if (tmp_pool)
+    int xerrno = errno;
+
+    if (tmp_pool != NULL) {
       destroy_pool(tmp_pool);
+    }
+
+    errno = xerrno;
     return res;
   }
 #endif /* PR_DEVEL_COREDUMP */
 
-  if (tmp_pool)
+  if (tmp_pool != NULL) {
     destroy_pool(tmp_pool);
+  }
 
   return res;
 }
 
-int pr_auth_cache_set(int bool, unsigned int flags) {
-  if (bool != 0 &&
-      bool != 1) {
+void pr_auth_cache_clear(void) {
+  if (auth_tab != NULL) {
+    pr_table_empty(auth_tab);
+    pr_table_free(auth_tab);
+    auth_tab = NULL;
+  }
+
+  if (uid_tab != NULL) {
+    pr_table_empty(uid_tab);
+    pr_table_free(uid_tab);
+    uid_tab = NULL;
+  }
+
+  if (user_tab != NULL) {
+    pr_table_empty(user_tab);
+    pr_table_free(user_tab);
+    user_tab = NULL;
+  }
+
+  if (gid_tab != NULL) {
+    pr_table_empty(gid_tab);
+    pr_table_free(gid_tab);
+    gid_tab = NULL;
+  }
+
+  if (group_tab != NULL) {
+    pr_table_empty(group_tab);
+    pr_table_free(group_tab);
+    group_tab = NULL;
+  }  
+}
+
+int pr_auth_cache_set(int enable, unsigned int flags) {
+  if (enable != FALSE &&
+      enable != TRUE) {
     errno = EINVAL;
     return -1;
   }
 
-  if (bool == 0) {
+  if (enable == FALSE) {
     if (flags & PR_AUTH_CACHE_FL_UID2NAME) {
       auth_caching &= ~PR_AUTH_CACHE_FL_UID2NAME;
       pr_trace_msg(trace_channel, 7, "UID-to-name caching (uidcache) disabled");
@@ -1612,9 +2137,45 @@ int pr_auth_cache_set(int bool, unsigned int flags) {
       pr_trace_msg(trace_channel, 7,
         "auth module caching (authcache) disabled");
     }
+
+    if (flags & PR_AUTH_CACHE_FL_NAME2UID) {
+      auth_caching &= ~PR_AUTH_CACHE_FL_NAME2UID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-UID caching (usercache) disabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_NAME2GID) {
+      auth_caching &= ~PR_AUTH_CACHE_FL_NAME2GID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-GID caching (groupcache) disabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_UID2NAME) {
+      auth_caching &= ~PR_AUTH_CACHE_FL_BAD_UID2NAME;
+      pr_trace_msg(trace_channel, 7,
+        "UID-to-name negative caching (uidcache) disabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_GID2NAME) {
+      auth_caching &= ~PR_AUTH_CACHE_FL_BAD_GID2NAME;
+      pr_trace_msg(trace_channel, 7,
+        "GID-to-name negative caching (gidcache) disabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_NAME2UID) {
+      auth_caching &= ~PR_AUTH_CACHE_FL_BAD_NAME2UID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-UID negative caching (usercache) disabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_NAME2GID) {
+      auth_caching &= ~PR_AUTH_CACHE_FL_BAD_NAME2GID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-GID negative caching (groupcache) disabled");
+    }
   }
 
-  if (bool == 1) {
+  if (enable == TRUE) {
     if (flags & PR_AUTH_CACHE_FL_UID2NAME) {
       auth_caching |= PR_AUTH_CACHE_FL_UID2NAME;
       pr_trace_msg(trace_channel, 7, "UID-to-name caching (uidcache) enabled");
@@ -1626,9 +2187,44 @@ int pr_auth_cache_set(int bool, unsigned int flags) {
     }
 
     if (flags & PR_AUTH_CACHE_FL_AUTH_MODULE) {
-      auth_caching &= ~PR_AUTH_CACHE_FL_AUTH_MODULE;
+      auth_caching |= PR_AUTH_CACHE_FL_AUTH_MODULE;
       pr_trace_msg(trace_channel, 7, "auth module caching (authcache) enabled");
     }
+
+    if (flags & PR_AUTH_CACHE_FL_NAME2UID) {
+      auth_caching |= PR_AUTH_CACHE_FL_NAME2UID;
+      pr_trace_msg(trace_channel, 7, "name-to-UID caching (usercache) enabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_NAME2GID) {
+      auth_caching |= PR_AUTH_CACHE_FL_NAME2GID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-GID caching (groupcache) enabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_UID2NAME) {
+      auth_caching |= PR_AUTH_CACHE_FL_BAD_UID2NAME;
+      pr_trace_msg(trace_channel, 7,
+        "UID-to-name negative caching (uidcache) enabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_GID2NAME) {
+      auth_caching |= PR_AUTH_CACHE_FL_BAD_GID2NAME;
+      pr_trace_msg(trace_channel, 7,
+        "GID-to-name negative caching (gidcache) enabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_NAME2UID) {
+      auth_caching |= PR_AUTH_CACHE_FL_BAD_NAME2UID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-UID negative caching (usercache) enabled");
+    }
+
+    if (flags & PR_AUTH_CACHE_FL_BAD_NAME2GID) {
+      auth_caching |= PR_AUTH_CACHE_FL_BAD_NAME2GID;
+      pr_trace_msg(trace_channel, 7,
+        "name-to-GID negative caching (groupcache) enabled");
+    }
   }
 
   return 0;
@@ -1637,12 +2233,12 @@ int pr_auth_cache_set(int bool, unsigned int flags) {
 int pr_auth_add_auth_only_module(const char *name) {
   struct auth_module_elt *elt = NULL;
 
-  if (!name) {
+  if (name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  if (!auth_pool) {
+  if (auth_pool == NULL) {
     auth_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(auth_pool, "Auth API");
   }
@@ -1684,7 +2280,7 @@ int pr_auth_add_auth_only_module(const char *name) {
 
 int pr_auth_clear_auth_only_modules(void) {
   if (auth_module_list == NULL) {
-    errno = EINVAL;
+    errno = EPERM;
     return -1;
   }
 
@@ -1696,7 +2292,7 @@ int pr_auth_clear_auth_only_modules(void) {
 int pr_auth_remove_auth_only_module(const char *name) {
   struct auth_module_elt *elt = NULL;
 
-  if (!name) {
+  if (name == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -1705,7 +2301,7 @@ int pr_auth_remove_auth_only_module(const char *name) {
     /* We won't be using the auth-only module cache, so there's no need to
      * accept this.
      */
-    pr_trace_msg(trace_channel, 9, "not removing '%s' to the auth-only list: "
+    pr_trace_msg(trace_channel, 9, "not removing '%s' from the auth-only list: "
       "caching of auth-only modules disabled", name);
     return 0;
   }
@@ -1713,7 +2309,7 @@ int pr_auth_remove_auth_only_module(const char *name) {
   if (auth_module_list == NULL) {
     pr_trace_msg(trace_channel, 9, "not removing '%s' from list: "
       "empty auth-only module list", name);
-    errno = ENOENT;
+    errno = EPERM;
     return -1;
   }
 
@@ -1736,18 +2332,26 @@ int pr_auth_remove_auth_only_module(const char *name) {
   return -1;
 }
 
-char *pr_auth_get_home(pool *p, char *pw_dir) {
+const char *pr_auth_get_home(pool *p, const char *pw_dir) {
   config_rec *c;
-  char *home_dir;
+  const char *home_dir;
+
+  if (p == NULL ||
+      pw_dir == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
 
   home_dir = pw_dir;
 
   c = find_config(main_server->conf, CONF_PARAM, "RewriteHome", FALSE);
-  if (c == NULL)
+  if (c == NULL) {
     return home_dir;
+  }
 
-  if (*((int *) c->argv[0]) == FALSE)
+  if (*((int *) c->argv[0]) == FALSE) {
     return home_dir;
+  }
 
   /* Rather than using a cmd_rec dispatched to mod_rewrite's PRE_CMD handler,
    * we use an approach with looser coupling to mod_rewrite: stash the
@@ -1788,9 +2392,25 @@ char *pr_auth_get_home(pool *p, char *pw_dir) {
   return home_dir;
 }
 
+size_t pr_auth_set_max_password_len(pool *p, size_t len) {
+  size_t prev_len;
+
+  prev_len = auth_max_passwd_len;
+
+  if (len == 0) {
+    /* Restore default. */
+    auth_max_passwd_len = PR_TUNABLE_PASSWORD_MAX;
+
+  } else {
+    auth_max_passwd_len = len;
+  }
+
+  return prev_len;
+}
+
 /* Internal use only.  To be called in the session process. */
 int init_auth(void) {
-  if (!auth_pool) {
+  if (auth_pool == NULL) {
     auth_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(auth_pool, "Auth API");
   }
diff --git a/src/bindings.c b/src/bindings.c
index ad84a88..7a7cbcd 100644
--- a/src/bindings.c
+++ b/src/bindings.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,17 +22,10 @@
  * OpenSSL in the source distribution.
  */
 
-/* Routines to work with ProFTPD bindings */
+/* Routines to work with ProFTPD bindings. */
 
 #include "conf.h"
 
-/* Some convenience macros */
-#define PR_CLOSE_NAMEBIND(n, a, p) \
-  if ((res = pr_namebind_close((n), (a), (p))) < 0) \
-    pr_log_pri(PR_LOG_NOTICE, \
-      "%s:%d: notice, unable to close namebind '%s': %s", \
-      __FILE__, __LINE__, (n), strerror(errno))
-
 /* From src/dirtree.c */
 extern xaset_t *server_list;
 extern server_rec *main_server;
@@ -52,7 +45,7 @@ static void server_cleanup_cb(void *conn) {
 /* The hashing function for the hash table of bindings.  This algorithm
  * is stolen from Apache's http_vhost.c
  */
-static unsigned int ipbind_hash_addr(pr_netaddr_t *addr) {
+static unsigned int ipbind_hash_addr(const pr_netaddr_t *addr) {
   size_t offset;
   unsigned int key;
 
@@ -74,14 +67,14 @@ struct listener_rec {
   struct listener_rec *next, *prev;
 
   pool *pool;
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   unsigned int port;
   conn_t *conn;
   int claimed;
 };
 
-conn_t *pr_ipbind_get_listening_conn(server_rec *server, pr_netaddr_t *addr,
-    unsigned int port) {
+conn_t *pr_ipbind_get_listening_conn(server_rec *server,
+    const pr_netaddr_t *addr, unsigned int port) {
   conn_t *l;
   pool *p;
   struct listener_rec *lr;
@@ -180,12 +173,8 @@ conn_t *pr_ipbind_accept_conn(fd_set *readfds, int *listenfd) {
   conn_t **listeners = listener_list->elts;
   register unsigned int i = 0;
 
-  if (!readfds) {
-    errno = EINVAL;
-    return NULL;
-  }
-
-  if (!listenfd) {
+  if (readfds == NULL ||
+      listenfd == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -199,15 +188,26 @@ conn_t *pr_ipbind_accept_conn(fd_set *readfds, int *listenfd) {
       int fd = pr_inet_accept_nowait(listener->pool, listener);
 
       if (fd == -1) {
+        int xerrno = errno;
+
         /* Handle errors gracefully.  If we're here, then
          * ipbind->ib_server->listen contains either error information, or
          * we just got caught in a blocking condition.
          */
         if (listener->mode == CM_ERROR) {
-          pr_log_pri(PR_LOG_ERR, "error: unable to accept an incoming "
-            "connection: %s", strerror(listener->xerrno));
+
+          /* Ignore ECONNABORTED, as they tend to be health checks/probes by
+           * e.g. load balancers and other naive TCP clients.
+           */
+          if (listener->xerrno != ECONNABORTED) {
+            pr_log_pri(PR_LOG_ERR, "error: unable to accept an incoming "
+              "connection: %s", strerror(listener->xerrno));
+          }
+
           listener->xerrno = 0;
           listener->mode = CM_LISTEN;
+
+          errno = xerrno;
           return NULL;
         }
       }
@@ -225,13 +225,15 @@ int pr_ipbind_add_binds(server_rec *serv) {
   int res = 0;
   config_rec *c = NULL;
   conn_t *listen_conn = NULL;
-  pr_netaddr_t *addr = NULL;
+  const pr_netaddr_t *addr = NULL;
 
-  if (!serv)
+  if (serv == NULL) {
+    errno = EINVAL;
     return -1;
+  }
 
   c = find_config(serv->conf, CONF_PARAM, "_bind_", FALSE);
-  while (c) {
+  while (c != NULL) {
     listen_conn = NULL;
 
     pr_signals_handle();
@@ -269,9 +271,8 @@ int pr_ipbind_add_binds(server_rec *serv) {
   return 0;
 }
 
-int pr_ipbind_close(pr_netaddr_t *addr, unsigned int port,
+int pr_ipbind_close(const pr_netaddr_t *addr, unsigned int port,
     unsigned char close_namebinds) {
-  int res = 0;
   register unsigned int i = 0;
 
   if (addr) {
@@ -338,17 +339,17 @@ int pr_ipbind_close(pr_netaddr_t *addr, unsigned int port,
       for (j = 0; j < ipbind->ib_namebinds->nelts; j++) {
         pr_namebind_t *nb = namebinds[j];
 
-        PR_CLOSE_NAMEBIND(nb->nb_name, nb->nb_server->addr,
-          nb->nb_server->ServerPort);
+        if (pr_namebind_close(nb->nb_name, nb->nb_server->addr) < 0) {
+          pr_trace_msg(trace_channel, 7,
+            "notice: error closing namebind '%s' for address %s: %s",
+            nb->nb_name, pr_netaddr_get_ipstr(nb->nb_server->addr),
+            strerror(errno));
+        }
       }
     }
 
   } else {
-
-    /* A NULL addr has a special meaning: close _all_ ipbinds in the
-     * list.
-     */
-
+    /* A NULL addr has a special meaning: close _all_ ipbinds in the list. */
     for (i = 0; i < PR_BINDINGS_TABLE_SIZE; i++) {
       pr_ipbind_t *ipbind = NULL;
       for (ipbind = ipbind_table[i]; ipbind; ipbind = ipbind->ib_next) {
@@ -372,8 +373,12 @@ int pr_ipbind_close(pr_netaddr_t *addr, unsigned int port,
           for (j = 0; j < ipbind->ib_namebinds->nelts; j++) {
             pr_namebind_t *nb = namebinds[j];
 
-            PR_CLOSE_NAMEBIND(nb->nb_name, nb->nb_server->addr,
-              nb->nb_server->ServerPort);
+            if (pr_namebind_close(nb->nb_name, nb->nb_server->addr) < 0) {
+              pr_trace_msg(trace_channel, 7,
+                "notice: error closing namebind '%s' for address %s: %s",
+                nb->nb_name, pr_netaddr_get_ipstr(nb->nb_server->addr),
+               strerror(errno));
+            }
           }
         }
       }
@@ -407,7 +412,7 @@ int pr_ipbind_close_listeners(void) {
   return 0;
 }
 
-int pr_ipbind_create(server_rec *server, pr_netaddr_t *addr,
+int pr_ipbind_create(server_rec *server, const pr_netaddr_t *addr,
     unsigned int port) {
   pr_ipbind_t *ipbind = NULL;
   register unsigned int i = 0;
@@ -461,23 +466,74 @@ int pr_ipbind_create(server_rec *server, pr_netaddr_t *addr,
   return 0;
 }
 
-pr_ipbind_t *pr_ipbind_find(pr_netaddr_t *addr, unsigned int port,
+/* Returns the LAST matching ipbind for the given port, if any.  If provided,
+ * the match_count pointer will contain the number of ipbinds found that
+ * matched that port.
+ */
+static pr_ipbind_t *ipbind_find_port(unsigned int port,
+    unsigned char skip_inactive, unsigned int *match_count) {
+  register unsigned int i;
+  pr_ipbind_t *matched_ipbind = NULL;
+
+  for (i = 0; i < PR_BINDINGS_TABLE_SIZE; i++) {
+    pr_ipbind_t *ipbind;
+
+    for (ipbind = ipbind_table[i]; ipbind; ipbind = ipbind->ib_next) {
+      pr_signals_handle();
+
+      if (skip_inactive &&
+          !ipbind->ib_isactive) {
+        continue;
+      }
+
+      if (ipbind->ib_port == port) {
+        if (match_count != NULL) {
+          (*match_count)++;
+        }
+
+        matched_ipbind = ipbind;
+      }
+    }
+  }
+
+  if (matched_ipbind == NULL) {
+    errno = ENOENT;
+    return NULL;
+  }
+
+  return matched_ipbind;
+}
+
+pr_ipbind_t *pr_ipbind_find(const pr_netaddr_t *addr, unsigned int port,
     unsigned char skip_inactive) {
+  register unsigned int i;
   pr_ipbind_t *ipbind = NULL;
-  register unsigned int i = ipbind_hash_addr(addr);
+
+  if (addr == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  i = ipbind_hash_addr(addr);
 
   for (ipbind = ipbind_table[i]; ipbind; ipbind = ipbind->ib_next) {
+    pr_signals_handle();
 
     if (skip_inactive &&
         !ipbind->ib_isactive) {
       continue;
     }
 
-    if (pr_netaddr_cmp(ipbind->ib_addr, addr) == 0 &&
-        (!ipbind->ib_port || ipbind->ib_port == port))
-      return ipbind;
+    if (pr_netaddr_cmp(ipbind->ib_addr, addr) == 0) {
+      if (ipbind->ib_port == 0 ||
+          port == 0 ||
+          ipbind->ib_port == port) {
+        return ipbind;
+      }
+    }
   }
 
+  errno = ENOENT;
   return NULL;
 }
 
@@ -487,14 +543,17 @@ pr_ipbind_t *pr_ipbind_get(pr_ipbind_t *prev) {
   if (prev) {
 
     /* If there's another ipbind in this chain, simply return that. */
-    if (prev->ib_next)
+    if (prev->ib_next) {
       return prev->ib_next;
+    }
 
     /* If the increment is at the maximum size, return NULL (no more chains
      * to be examined).
      */
-    if (i == PR_BINDINGS_TABLE_SIZE)
+    if (i == PR_BINDINGS_TABLE_SIZE) {
+      errno = ENOENT;
       return NULL;
+    }
 
     /* Increment the index. At this point, we know that the given pointer is
      * the last in the chain, and that there are more chains in the table
@@ -502,30 +561,35 @@ pr_ipbind_t *pr_ipbind_get(pr_ipbind_t *prev) {
      */
     i++;
 
-  } else
+  } else {
     /* Reset the index if prev is NULL. */
     i = 0;
+  }
 
   /* Search for the next non-empty chain in the table. */
   for (; i < PR_BINDINGS_TABLE_SIZE; i++) {
-    if (ipbind_table[i])
+    if (ipbind_table[i]) {
       return ipbind_table[i];
+    }
   }
 
+  errno = ENOENT;
   return NULL;
 }
 
-server_rec *pr_ipbind_get_server(pr_netaddr_t *addr, unsigned int port) {
+server_rec *pr_ipbind_get_server(const pr_netaddr_t *addr, unsigned int port) {
   pr_ipbind_t *ipbind = NULL;
   pr_netaddr_t wildcard_addr;
   int addr_family;
+  unsigned int match_count = 0;
 
   /* If we've got a binding configured for this exact address, return it
    * straightaway.
    */
   ipbind = pr_ipbind_find(addr, port, TRUE);
-  if (ipbind != NULL)
+  if (ipbind != NULL) {
     return ipbind->ib_server;
+  }
 
   /* Look for a vhost bound to the wildcard address (i.e. INADDR_ANY).
    *
@@ -553,11 +617,12 @@ server_rec *pr_ipbind_get_server(pr_netaddr_t *addr, unsigned int port) {
     if (addr_family == AF_INET6 &&
         pr_netaddr_use_ipv6()) {
 
-      /* The pr_ipbind_find() probably returned NULL because there aren't
-       * any <VirtualHost> sections configured explicitly for the wildcard
-       * IPv6 address of "::", just the IPv4 wildcard "0.0.0.0" address.
+      /* The pr_ipbind_find() probably returned NULL because there aren't any
+       * <VirtualHost> sections configured explicitly for the wildcard IPv6
+       * address of "::", just the IPv4 wildcard "0.0.0.0" address.
        *
-       * So try the pr_ipbind_find() again, this time using the IPv4 wildcard.
+       * So try the pr_ipbind_find() again, this time using the IPv4
+       * wildcard.
        */
       pr_netaddr_clear(&wildcard_addr);
       pr_netaddr_set_family(&wildcard_addr, AF_INET);
@@ -574,6 +639,18 @@ server_rec *pr_ipbind_get_server(pr_netaddr_t *addr, unsigned int port) {
 #endif /* PR_USE_IPV6 */
   }
 
+  /* Check for any bindings that match the port.  IF there is only one ONE
+   * vhost which matches the requested port, use that (Bug#4251).
+   */
+  ipbind = ipbind_find_port(port, TRUE, &match_count);
+  if (ipbind != NULL) {
+    pr_trace_msg(trace_channel, 18, "found %u possible %s for port %u",
+      match_count, match_count != 1 ? "ipbinds" : "ipbind", port);
+    if (match_count == 1) {
+      return ipbind->ib_server;
+    }
+  }
+
   /* Use the default server, if set. */
   if (ipbind_default_server &&
       ipbind_default_server->ib_isactive) {
@@ -599,12 +676,14 @@ int pr_ipbind_listen(fd_set *readfds) {
   register unsigned int i = 0;
 
   /* sanity check */
-  if (!readfds)
+  if (readfds == NULL) {
+    errno = EINVAL;
     return -1;
+  }
 
   FD_ZERO(readfds);
 
-  if (!binding_pool) {
+  if (binding_pool == NULL) {
     binding_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(binding_pool, "Bindings Pool");
   }
@@ -641,7 +720,13 @@ int pr_ipbind_listen(fd_set *readfds) {
         }
 
         if (ipbind->ib_listener->mode == CM_ACCEPT) {
-          pr_inet_resetlisten(ipbind->ib_listener->pool, ipbind->ib_listener);
+          if (pr_inet_resetlisten(ipbind->ib_listener->pool,
+              ipbind->ib_listener) < 0) {
+            pr_trace_msg(trace_channel, 3,
+              "error resetting %s#%u for listening: %s",
+              pr_netaddr_get_ipstr(ipbind->ib_addr), ipbind->ib_port,
+              strerror(errno));
+          }
         }
 
         if (ipbind->ib_listener->mode == CM_LISTEN) {
@@ -659,8 +744,8 @@ int pr_ipbind_listen(fd_set *readfds) {
   return maxfd;
 }
 
-int pr_ipbind_open(pr_netaddr_t *addr, unsigned int port, conn_t *listen_conn,
-    unsigned char isdefault, unsigned char islocalhost,
+int pr_ipbind_open(const pr_netaddr_t *addr, unsigned int port,
+    conn_t *listen_conn, unsigned char isdefault, unsigned char islocalhost,
     unsigned char open_namebinds) {
   int res = 0;
   pr_ipbind_t *ipbind = NULL;
@@ -714,8 +799,7 @@ int pr_ipbind_open(pr_netaddr_t *addr, unsigned int port, conn_t *listen_conn,
     for (i = 0; i < ipbind->ib_namebinds->nelts; i++) {
       pr_namebind_t *nb = namebinds[i];
 
-      res = pr_namebind_open(nb->nb_name, nb->nb_server->addr,
-        nb->nb_server->ServerPort);
+      res = pr_namebind_open(nb->nb_name, nb->nb_server->addr);
       if (res < 0) {
         pr_trace_msg(trace_channel, 2,
           "notice: unable to open namebind '%s': %s", nb->nb_name,
@@ -730,16 +814,17 @@ int pr_ipbind_open(pr_netaddr_t *addr, unsigned int port, conn_t *listen_conn,
   return 0;
 }
 
-int pr_namebind_close(const char *name, pr_netaddr_t *addr,
-    unsigned int port) {
+int pr_namebind_close(const char *name, const pr_netaddr_t *addr) {
   pr_namebind_t *namebind = NULL;
+  unsigned int port;
 
-  if (!name ||
-      !addr) {
+  if (name == NULL||
+      addr == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  port = ntohs(pr_netaddr_get_port(addr));
   namebind = pr_namebind_find(name, addr, port, FALSE);
   if (namebind == NULL) {
     errno = ENOENT;
@@ -751,9 +836,10 @@ int pr_namebind_close(const char *name, pr_netaddr_t *addr,
 }
 
 int pr_namebind_create(server_rec *server, const char *name,
-    pr_netaddr_t *addr, unsigned int port) {
+    const pr_netaddr_t *addr, unsigned int server_port) {
   pr_ipbind_t *ipbind = NULL;
   pr_namebind_t *namebind = NULL, **namebinds = NULL;
+  unsigned int port;
 
   if (server == NULL ||
       name == NULL) {
@@ -762,7 +848,14 @@ int pr_namebind_create(server_rec *server, const char *name,
   }
 
   /* First, find the ipbind to hold this namebind. */
+  port = ntohs(pr_netaddr_get_port(addr));
+  if (port == 0) {
+    port = server_port;
+  }
   ipbind = pr_ipbind_find(addr, port, FALSE);
+  pr_trace_msg(trace_channel, 19,
+    "found ipbind %p for namebind (name '%s', addr %s, port %u)", ipbind, name,
+    pr_netaddr_get_ipstr(addr), port);
 
   if (ipbind == NULL) {
     pr_netaddr_t wildcard_addr;
@@ -789,6 +882,10 @@ int pr_namebind_create(server_rec *server, const char *name,
       ipbind = pr_ipbind_find(&wildcard_addr, port, FALSE);
     }
 #endif /* PR_USE_IPV6 */
+
+    pr_trace_msg(trace_channel, 19,
+      "found wildcard ipbind %p for namebind (name '%s', addr %s, port %u)",
+      ipbind, name, pr_netaddr_get_ipstr(addr), port);
   }
 
   if (ipbind == NULL) {
@@ -878,7 +975,7 @@ int pr_namebind_create(server_rec *server, const char *name,
   return 0;
 }
 
-pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
+pr_namebind_t *pr_namebind_find(const char *name, const pr_netaddr_t *addr,
     unsigned int port, unsigned char skip_inactive) {
   pr_ipbind_t *ipbind = NULL;
   pr_namebind_t *namebind = NULL;
@@ -889,9 +986,8 @@ pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
     return NULL;
   }
 
-  /* First, find an active ipbind for the given addr/port */
+  /* First, find an active ipbind for the given address. */
   ipbind = pr_ipbind_find(addr, port, skip_inactive);
-
   if (ipbind == NULL) {
     pr_netaddr_t wildcard_addr;
     int addr_family;
@@ -917,6 +1013,10 @@ pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
       ipbind = pr_ipbind_find(&wildcard_addr, port, FALSE);
     }
 #endif /* PR_USE_IPV6 */
+  } else {
+    pr_trace_msg(trace_channel, 17,
+      "found ipbind %p (server %p) for %s#%u", ipbind, ipbind->ib_server,
+      pr_netaddr_get_ipstr(addr), port);
   }
 
   if (ipbind == NULL) {
@@ -924,20 +1024,32 @@ pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
     return NULL;
   }
 
-  if (!ipbind->ib_namebinds) {
+  if (ipbind->ib_namebinds == NULL) {
+    pr_trace_msg(trace_channel, 17,
+      "ipbind %p (server %p) for %s#%u has no namebinds", ipbind,
+      ipbind->ib_server, pr_netaddr_get_ipstr(addr), port);
     return NULL;
 
   } else {
     register unsigned int i = 0;
     pr_namebind_t **namebinds = (pr_namebind_t **) ipbind->ib_namebinds->elts;
 
+    pr_trace_msg(trace_channel, 17,
+      "ipbind %p (server %p) for %s#%u has namebinds (%d)", ipbind,
+      ipbind->ib_server, pr_netaddr_get_ipstr(addr), port,
+      ipbind->ib_namebinds->nelts);
+
     for (i = 0; i < ipbind->ib_namebinds->nelts; i++) {
       namebind = namebinds[i];
+      if (namebind == NULL) {
+        continue;
+      }
 
       /* Skip inactive namebinds */
       if (skip_inactive == TRUE &&
-          namebind != NULL &&
           namebind->nb_isactive == FALSE) {
+        pr_trace_msg(trace_channel, 17,
+          "namebind #%u: %s is inactive, skipping", i, namebind->nb_name);
         continue;
       }
 
@@ -947,11 +1059,12 @@ pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
        * that scheme, however, is specific to DNS; should any other naming
        * scheme be desired, that sort of matching will be unnecessary.
        */
-      if (namebind != NULL &&
-          namebind->nb_name != NULL) {
+      if (namebind->nb_name != NULL) {
+        pr_trace_msg(trace_channel, 17,
+          "namebind #%u: %s", i, namebind->nb_name);
 
         if (namebind->nb_iswildcard == FALSE) {
-          if (strcasecmp(namebind->nb_name, name) == 0)
+          if (strcasecmp(namebind->nb_name, name) == 0) {
             return namebind;
           }
 
@@ -969,26 +1082,47 @@ pr_namebind_t *pr_namebind_find(const char *name, pr_netaddr_t *addr,
             "failed to match name '%s' against pattern '%s'", name,
             namebind->nb_name);
         }
+      }
     }
   }
 
   return NULL;
 }
 
-server_rec *pr_namebind_get_server(const char *name, pr_netaddr_t *addr,
+server_rec *pr_namebind_get_server(const char *name, const pr_netaddr_t *addr,
     unsigned int port) {
   pr_namebind_t *namebind = NULL;
 
   /* Basically, just a wrapper around pr_namebind_find() */
+
   namebind = pr_namebind_find(name, addr, port, TRUE);
-  if (namebind == NULL)
+  if (namebind == NULL) {
     return NULL;
+  }
 
   return namebind->nb_server;
 }
 
-int pr_namebind_open(const char *name, pr_netaddr_t *addr, unsigned int port) {
+unsigned int pr_namebind_count(server_rec *srv) {
+  unsigned int count = 0;
+  pr_ipbind_t *ipbind = NULL;
+
+  if (srv == NULL) {
+    return 0;
+  }
+
+  ipbind = pr_ipbind_find(srv->addr, srv->ServerPort, FALSE); 
+  if (ipbind != NULL &&
+      ipbind->ib_namebinds != NULL) {
+    count = ipbind->ib_namebinds->nelts; 
+  }
+
+  return count;
+}
+
+int pr_namebind_open(const char *name, const pr_netaddr_t *addr) {
   pr_namebind_t *namebind = NULL;
+  unsigned int port;
 
   if (name == NULL ||
       addr == NULL) {
@@ -996,6 +1130,7 @@ int pr_namebind_open(const char *name, pr_netaddr_t *addr, unsigned int port) {
     return -1;
   }
 
+  port = ntohs(pr_netaddr_get_port(addr));
   namebind = pr_namebind_find(name, addr, port, FALSE);
   if (namebind == NULL) {
     errno = ENOENT;
@@ -1172,18 +1307,20 @@ static int init_standalone_bindings(void) {
       if (res == 0) {
         is_namebind = TRUE;
 
-        res = pr_namebind_open(c->argv[0], serv->addr, serv->ServerPort);
+        res = pr_namebind_open(c->argv[0], serv->addr);
         if (res < 0) {
-          pr_trace_msg(trace_channel, 2, 
+          pr_trace_msg(trace_channel, 2,
             "notice: unable to open namebind '%s': %s", (char *) c->argv[0],
             strerror(errno));
         }
 
       } else {
-        pr_trace_msg(trace_channel, 2,
-          "unable to create namebind for '%s' to %s#%u: %s",
-          (char *) c->argv[0], pr_netaddr_get_ipstr(serv->addr),
-          serv->ServerPort, strerror(errno));
+        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(serv->addr),
+            serv->ServerPort, strerror(errno));
+        }
       }
 
       c = find_config_next(c, c->next, CONF_PARAM, "ServerAlias", FALSE);
diff --git a/src/child.c b/src/child.c
index 9a1fff8..ab0b91f 100644
--- a/src/child.c
+++ b/src/child.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2013 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Children management code
- * $Id: child.c,v 1.9 2013-10-08 07:01:43 castaglia Exp $
- */
+/* Children management code */
 
 #include "conf.h"
 
diff --git a/src/class.c b/src/class.c
index cb5cac0..9db4721 100644
--- a/src/class.c
+++ b/src/class.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2008 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Class routines
- * $Id: class.c,v 1.10 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Class routines */
 
 #include "conf.h"
 
@@ -36,18 +34,23 @@ static const char *trace_channel = "class";
 static pr_class_t *class_list = NULL;
 static pr_class_t *curr_cls = NULL;
 
-pr_class_t *pr_class_get(pr_class_t *prev) {
-  if (prev)
+const pr_class_t *pr_class_get(const pr_class_t *prev) {
+  if (prev != NULL) {
     return prev->cls_next;
+  }
+
+  if (class_list == NULL) {
+    errno = ENOENT;
+  }
 
   return class_list;
 }
 
-pr_class_t *pr_class_match_addr(pr_netaddr_t *addr) {
+const pr_class_t *pr_class_match_addr(const pr_netaddr_t *addr) {
   pr_class_t *cls;
   pool *tmp_pool;
 
-  if (!addr) {
+  if (addr == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -55,11 +58,14 @@ pr_class_t *pr_class_match_addr(pr_netaddr_t *addr) {
   tmp_pool = make_sub_pool(permanent_pool);
 
   for (cls = class_list; cls; cls = cls->cls_next) {
-    array_header *acl_list = cls->cls_acls;
-    pr_netacl_t **acls = acl_list->elts;
-    register int i;
+    array_header *acl_list;
+    const pr_netacl_t **acls;
+    register unsigned int i;
     int next_class = FALSE;
 
+    acl_list = cls->cls_acls;
+    acls = acl_list->elts;
+
     /* For each ACL rule in this class, compare the rule against the given
      * address.  The address matches the given class depending on the
      * Satisfy setting: if "any", the class matches if any rule matches;
@@ -68,11 +74,15 @@ pr_class_t *pr_class_match_addr(pr_netaddr_t *addr) {
     for (i = 0; i < acl_list->nelts; i++) {
       int res;
 
-      if (next_class)
+      pr_signals_handle();
+
+      if (next_class) {
         break;
+      }
 
-      if (acls[i] == NULL)
+      if (acls[i] == NULL) {
         continue;
+      }
 
       switch (cls->cls_satisfy) {
         case PR_CLASS_SATISFY_ANY:
@@ -97,9 +107,9 @@ pr_class_t *pr_class_match_addr(pr_netaddr_t *addr) {
             pr_netacl_get_str(tmp_pool, acls[i]));
 
           res = pr_netacl_match(acls[i], addr);
-
-          if (res <= 0)
+          if (res <= 0) {
             next_class = TRUE;
+          }
           break;
       }
     }
@@ -121,38 +131,42 @@ pr_class_t *pr_class_match_addr(pr_netaddr_t *addr) {
   return NULL;
 }
 
-pr_class_t *pr_class_find(const char *name) {
+const pr_class_t *pr_class_find(const char *name) {
   pr_class_t *cls;
 
-  if (!name) {
+  if (name == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
-  for (cls = class_list; cls; cls = cls->cls_next)
-    if (strcmp(cls->cls_name, name) == 0)
+  for (cls = class_list; cls; cls = cls->cls_next) {
+    pr_signals_handle();
+    if (strcmp(cls->cls_name, name) == 0) {
       return cls;
+    }
+  }
 
   errno = ENOENT;
   return NULL;
 }
 
-int pr_class_add_acl(pr_netacl_t *acl) {
+int pr_class_add_acl(const pr_netacl_t *acl) {
 
-  if (!acl) {
+  if (acl == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  if (!curr_cls) {
+  if (curr_cls == NULL) {
     errno = EPERM;
     return -1;
   }
 
   /* Add this ACL rule to the current Class. */
-  if (!curr_cls->cls_acls)
+  if (curr_cls->cls_acls == NULL) {
     curr_cls->cls_acls = make_array(curr_cls->cls_pool, 1,
       sizeof(pr_netacl_t *));
+  }
 
   *((pr_netacl_t **) push_array(curr_cls->cls_acls)) =
     pr_netacl_dup(curr_cls->cls_pool, acl);
@@ -161,8 +175,7 @@ int pr_class_add_acl(pr_netacl_t *acl) {
 }
 
 int pr_class_set_satisfy(int satisfy) {
-
-  if (!curr_cls) {
+  if (curr_cls == NULL) {
     errno = EPERM;
     return -1;
   }
@@ -179,11 +192,29 @@ int pr_class_set_satisfy(int satisfy) {
   return 0;
 }
 
+int pr_class_add_note(const char *key, void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (curr_cls == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  res = pr_table_add(curr_cls->cls_notes, key, value, valuesz);
+  return res;
+}
+
 int pr_class_open(pool *p, const char *name) {
   pr_class_t *cls;
   pool *cls_pool;
 
-  if (!p || !name) {
+  if (p == NULL ||
+      name == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -198,6 +229,7 @@ int pr_class_open(pool *p, const char *name) {
   cls->cls_pool = cls_pool;
   cls->cls_name = pstrdup(cls->cls_pool, name);
   cls->cls_satisfy = PR_CLASS_SATISFY_ANY;
+  cls->cls_notes = pr_table_nalloc(cls_pool, 0, 1);
  
   /* Change the configuration context type. */
   main_server->config_type = CONF_CLASS;
@@ -209,13 +241,14 @@ int pr_class_open(pool *p, const char *name) {
 int pr_class_close(void) {
 
   /* If there is no current Class, there is nothing to do. */
-  if (!curr_cls)
+  if (curr_cls == NULL) {
     return 0;
+  }
 
   /* If there are no client rules in this class, simply remove it.  No need
    * to waste space.
    */
-  if (!curr_cls->cls_acls) {
+  if (curr_cls->cls_acls == NULL) {
     destroy_pool(curr_cls->cls_pool);
     curr_cls = NULL;
 
@@ -231,14 +264,19 @@ int pr_class_close(void) {
 
   /* Now add the current Class to the end of the list. */
   if (class_list) {
-    pr_class_t *ci = class_list;
-    while (ci && ci->cls_next)
+    pr_class_t *ci;
+
+    ci = class_list;
+    while (ci != NULL &&
+           ci->cls_next != NULL) {
       ci = ci->cls_next;
+    }
 
     ci->cls_next = curr_cls;
 
-  } else
+  } else {
     class_list = curr_cls;
+  }
 
   curr_cls = NULL;
 
@@ -250,5 +288,4 @@ int pr_class_close(void) {
 
 void init_class(void) {
   class_list = NULL;
-  return;
 }
diff --git a/src/cmd.c b/src/cmd.c
index f940088..229f867 100644
--- a/src/cmd.c
+++ b/src/cmd.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2012 The ProFTPD Project team
+ * Copyright (c) 2009-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@
  * 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.
- *
- * $Id: cmd.c,v 1.12 2012-12-28 17:40:36 castaglia Exp $
  */
 
 #include "conf.h"
@@ -108,6 +106,7 @@ static struct cmd_entry cmd_ids[] = {
   { C_MFF,	3 },	/* PR_CMD_MFF_ID (56) */
   { C_MFMT,	4 },	/* PR_CMD_MFMT_ID (57) */
   { C_HOST,	4 },	/* PR_CMD_HOST_ID (58) */
+  { C_CLNT,	4 },	/* PR_CMD_CLNT_ID (59) */
 
   { NULL,	0 }
 };
@@ -144,9 +143,12 @@ static struct cmd_entry smtp_ids[] = {
   { NULL,	0 }
 };
 
-cmd_rec *pr_cmd_alloc(pool *p, int argc, ...) { 
+static const char *trace_channel = "command";
+
+cmd_rec *pr_cmd_alloc(pool *p, unsigned int argc, ...) {
   pool *newpool = NULL;
   cmd_rec *cmd = NULL;
+  int *xerrno = NULL;
   va_list args;
 
   if (p == NULL) {
@@ -160,21 +162,22 @@ cmd_rec *pr_cmd_alloc(pool *p, int argc, ...) {
   cmd = pcalloc(newpool, sizeof(cmd_rec));
   cmd->argc = argc;
   cmd->stash_index = -1;
+  cmd->stash_hash = 0;
   cmd->pool = newpool;
   cmd->tmp_pool = make_sub_pool(cmd->pool);
   pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
 
-  if (argc) {
+  if (argc > 0) {
     register unsigned int i = 0;
 
     cmd->argv = pcalloc(newpool, sizeof(void *) * (argc + 1));
     va_start(args, argc);
 
-    for (i = 0; i < argc; i++)
-      cmd->argv[i] = (void *) va_arg(args, char *);
+    for (i = 0; i < argc; i++) {
+      cmd->argv[i] = va_arg(args, void *);
+    }
 
     va_end(args);
-
     cmd->argv[argc] = NULL;
   }
 
@@ -183,6 +186,11 @@ cmd_rec *pr_cmd_alloc(pool *p, int argc, ...) {
    */
   cmd->notes = pr_table_nalloc(cmd->pool, 0, 8);
 
+  /* Initialize the "errno" note to be zero, so that it is always present. */
+  xerrno = palloc(cmd->pool, sizeof(int));
+  *xerrno = 0;
+  (void) pr_table_add(cmd->notes, "errno", xerrno, sizeof(int));
+
   return cmd;
 }
 
@@ -197,6 +205,7 @@ int pr_cmd_clear_cache(cmd_rec *cmd) {
    */
 
   (void) pr_table_remove(cmd->notes, "displayable-str", NULL);
+  (void) pr_cmd_set_errno(cmd, 0);
 
   return 0;
 }
@@ -230,6 +239,44 @@ int pr_cmd_cmp(cmd_rec *cmd, int cmd_id) {
   return cmd->cmd_id < cmd_id ? -1 : 1;
 }
 
+int pr_cmd_get_errno(cmd_rec *cmd) {
+  void *v;
+  int *xerrno;
+
+  if (cmd == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  v = (void *) pr_table_get(cmd->notes, "errno", NULL);
+  if (v == NULL) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  xerrno = v;
+  return *xerrno;
+}
+
+int pr_cmd_set_errno(cmd_rec *cmd, int xerrno) {
+  void *v;
+
+  if (cmd == NULL ||
+      cmd->notes == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  v = (void *) pr_table_get(cmd->notes, "errno", NULL);
+  if (v == NULL) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  *((int *) v) = xerrno;
+  return 0;
+}
+
 int pr_cmd_set_name(cmd_rec *cmd, const char *cmd_name) {
   if (cmd == NULL ||
       cmd_name == NULL) {
@@ -279,10 +326,10 @@ int pr_cmd_strcmp(cmd_rec *cmd, const char *cmd_name) {
   return strncmp(cmd->argv[0], cmd_name, cmd_namelen + 1);
 }
 
-char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
-  char *res;
-  int argc;
-  char **argv;
+const char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
+  const char *res;
+  unsigned int argc;
+  void **argv;
   pool *p;
 
   if (cmd == NULL) {
@@ -291,7 +338,7 @@ char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
   }
 
   res = pr_table_get(cmd->notes, "displayable-str", NULL);
-  if (res) {
+  if (res != NULL) {
     if (str_len != NULL) {
       *str_len = strlen(res);
     }
@@ -322,9 +369,13 @@ char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
     }
   }
 
-  /* XXX Check for errors here */
-  pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
-    pstrdup(cmd->pool, res), 0);
+  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
+      pstrdup(cmd->pool, res), 0) < 0) {
+    if (errno != EEXIST) {
+      pr_trace_msg(trace_channel, 4,
+        "error setting 'displayable-str' command note: %s", strerror(errno));
+    }
+  }
 
   if (str_len != NULL) {
     *str_len = strlen(res);
@@ -405,6 +456,14 @@ int pr_cmd_is_http(cmd_rec *cmd) {
     return -1;
   }
 
+  if (cmd->cmd_id == 0) {
+    cmd->cmd_id = pr_cmd_get_id(cmd_name);
+  }
+
+  if (cmd->cmd_id >= 0) {
+    return FALSE;
+  }
+
   cmd_namelen = strlen(cmd_name);
   return is_known_cmd(http_ids, cmd_name, cmd_namelen);
 }
@@ -424,6 +483,44 @@ int pr_cmd_is_smtp(cmd_rec *cmd) {
     return -1;
   }
 
+  if (cmd->cmd_id == 0) {
+    cmd->cmd_id = pr_cmd_get_id(cmd_name);
+  }
+
+  if (cmd->cmd_id >= 0) {
+    return FALSE;
+  }
+
   cmd_namelen = strlen(cmd_name);
   return is_known_cmd(smtp_ids, cmd_name, cmd_namelen);
 }
+
+int pr_cmd_is_ssh2(cmd_rec *cmd) {
+  const char *cmd_name;
+
+  if (cmd == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  cmd_name = cmd->argv[0];
+  if (cmd_name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (cmd->cmd_id == 0) {
+    cmd->cmd_id = pr_cmd_get_id(cmd_name);
+  }
+
+  if (cmd->cmd_id >= 0) {
+    return FALSE;
+  }
+
+  if (strncmp(cmd_name, "SSH-2.0-", 8) == 0 ||
+      strncmp(cmd_name, "SSH-1.99-", 9) == 0) {
+    return TRUE;
+  }
+
+  return FALSE;
+}
diff --git a/src/configdb.c b/src/configdb.c
new file mode 100644
index 0000000..8ce8167
--- /dev/null
+++ b/src/configdb.c
@@ -0,0 +1,965 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2014-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, Public Flood Software/MacGyver aka Habeeb J. Dihu
+ * 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.
+ */
+
+/* Configuration database implementation. */
+
+#include "conf.h"
+#include "privs.h"
+
+#ifdef HAVE_ARPA_INET_H
+# include <arpa/inet.h>
+#endif
+
+/* From src/pool.c */
+extern pool *global_config_pool;
+
+/* Used by find_config_* */
+static xaset_t *find_config_top = NULL;
+
+static void config_dumpf(const char *, ...);
+
+static config_rec *last_param_ptr = NULL;
+
+static pool *config_tab_pool = NULL;
+static pr_table_t *config_tab = NULL;
+static unsigned int config_id = 0;
+
+static const char *trace_channel = "config";
+
+/* Adds a config_rec to the specified set */
+config_rec *pr_config_add_set(xaset_t **set, const char *name, int flags) {
+  pool *conf_pool = NULL, *set_pool = NULL;
+  config_rec *c, *parent = NULL;
+
+  if (set == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+ 
+  if (!*set) {
+
+    /* Allocate a subpool from permanent_pool for the set. */
+    set_pool = make_sub_pool(permanent_pool);
+    pr_pool_tag(set_pool, "config set pool");
+
+    *set = xaset_create(set_pool, NULL);
+    (*set)->pool = set_pool;
+
+    /* Now, make a subpool for the config_rec to be allocated.  The default
+     * pool size (PR_TUNABLE_NEW_POOL_SIZE, 512 by default) is a bit large
+     * for config_rec pools; use a smaller size.
+     */
+    conf_pool = pr_pool_create_sz(set_pool, 128);
+
+  } else {
+
+    /* Find the parent set for the config_rec to be allocated. */
+    if ((*set)->xas_list) {
+      parent = ((config_rec *) ((*set)->xas_list))->parent;
+    }
+
+    /* Now, make a subpool for the config_rec to be allocated.  The default
+     * pool size (PR_TUNABLE_NEW_POOL_SIZE, 512 by default) is a bit large
+     * for config_rec pools; use a smaller size.  Allocate the subpool
+     * from the parent's pool.
+     */
+    conf_pool = pr_pool_create_sz((*set)->pool, 128);
+  }
+
+  pr_pool_tag(conf_pool, "config_rec pool");
+
+  c = (config_rec *) pcalloc(conf_pool, sizeof(config_rec));
+  c->pool = conf_pool;
+  c->set = *set;
+  c->parent = parent;
+
+  if (name) {
+    c->name = pstrdup(conf_pool, name);
+    c->config_id = pr_config_set_id(c->name);
+  }
+
+  if (flags & PR_CONFIG_FL_INSERT_HEAD) {
+    xaset_insert(*set, (xasetmember_t *) c);
+    
+  } else {
+    xaset_insert_end(*set, (xasetmember_t *) c);
+  }
+
+  return c;
+}
+
+config_rec *add_config_set(xaset_t **set, const char *name) {
+  return pr_config_add_set(set, name, 0);
+}
+
+/* Adds a config_rec to the given server.  If no server is specified, the
+ * config_rec is added to the current "level".
+ */
+config_rec *pr_config_add(server_rec *s, const char *name, int flags) {
+  config_rec *parent = NULL, *c = NULL;
+  pool *p = NULL;
+  xaset_t **set = NULL;
+
+  if (s == NULL) {
+    s = pr_parser_server_ctxt_get();
+  }
+
+  if (s == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  c = pr_parser_config_ctxt_get();
+
+  if (c) {
+    parent = c;
+    p = c->pool;
+    set = &c->subset;
+
+  } else {
+    parent = NULL;
+
+    if (s->conf == NULL ||
+        s->conf->xas_list == NULL) {
+
+      p = make_sub_pool(s->pool);
+      pr_pool_tag(p, "pr_config_add() subpool");
+
+    } else {
+      p = ((config_rec *) s->conf->xas_list)->pool;
+    }
+
+    set = &s->conf;
+  }
+
+  if (!*set) {
+    *set = xaset_create(p, NULL);
+  }
+
+  c = pr_config_add_set(set, name, flags);
+  c->parent = parent;
+
+  return c;
+}
+
+config_rec *add_config(server_rec *s, const char *name) {
+  return pr_config_add(s, name, 0);
+}
+
+static void config_dumpf(const char *fmt, ...) {
+  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
+  va_list msg;
+
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf), fmt, msg);
+  va_end(msg);
+
+  buf[sizeof(buf)-1] = '\0';
+
+  pr_log_debug(DEBUG5, "%s", buf);
+}
+
+void pr_config_dump(void (*dumpf)(const char *, ...), xaset_t *s,
+    char *indent) {
+  config_rec *c = NULL;
+
+  if (dumpf == NULL) {
+    dumpf = config_dumpf;
+  }
+
+  if (s == NULL) {
+    return;
+  }
+
+  if (indent == NULL) {
+    indent = "";
+  }
+
+  for (c = (config_rec *) s->xas_list; c; c = c->next) {
+    pr_signals_handle();
+
+    /* Don't display directives whose name starts with an underscore. */
+    if (c->name != NULL &&
+        *(c->name) != '_') {
+      dumpf("%s%s", indent, c->name);
+    }
+
+    if (c->subset) {
+      pr_config_dump(dumpf, c->subset, pstrcat(c->pool, indent, " ", NULL));
+    }
+  }
+}
+
+static const char *config_type_str(int config_type) {
+  const char *type = "(unknown)";
+
+  switch (config_type) {
+    case CONF_ROOT:
+      type = "CONF_ROOT";
+      break;
+
+    case CONF_DIR:
+      type = "CONF_DIR";
+      break;
+
+    case CONF_ANON:
+      type = "CONF_ANON";
+      break;
+
+    case CONF_LIMIT:
+      type = "CONF_LIMIT";
+      break;
+
+    case CONF_VIRTUAL:
+      type = "CONF_VIRTUAL";
+      break;
+
+    case CONF_DYNDIR:
+      type = "CONF_DYNDIR";
+      break;
+
+    case CONF_GLOBAL:
+      type = "CONF_GLOBAL";
+      break;
+
+    case CONF_CLASS:
+      type = "CONF_CLASS";
+      break;
+
+    case CONF_NAMED:
+      type = "CONF_NAMED";
+      break;
+
+    case CONF_USERDATA:
+      type = "CONF_USERDATA";
+      break;
+
+    case CONF_PARAM:
+      type = "CONF_PARAM";
+      break;
+  };
+
+  return type;
+}
+
+/* Compare two different config_recs to see if they are the same.  Note
+ * that "same" here has to be very specific.
+ *
+ * Returns 0 if the two config_recs are the same, and 1 if they differ, and
+ * -1 if there was an error.
+ */
+static int config_cmp(const config_rec *a, const char *a_name,
+    const config_rec *b, const char *b_name) {
+
+  if (a == NULL ||
+      b == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (a->config_type != b->config_type) {
+    pr_trace_msg(trace_channel, 18,
+      "configs '%s' and '%s' have mismatched config_type (%s != %s)",
+      a_name, b_name, config_type_str(a->config_type),
+      config_type_str(b->config_type));
+    return 1;
+  }
+
+  if (a->flags != b->flags) {
+    pr_trace_msg(trace_channel, 18,
+      "configs '%s' and '%s' have mismatched flags (%ld != %ld)",
+      a_name, b_name, a->flags, b->flags);
+    return 1;
+  }
+
+  if (a->argc != b->argc) {
+    pr_trace_msg(trace_channel, 18,
+      "configs '%s' and '%s' have mismatched argc (%d != %d)",
+      a_name, b_name, a->argc, b->argc);
+    return 1;
+  }
+
+  if (a->argc > 0) {
+    register unsigned int i;
+
+    for (i = 0; i < a->argc; i++) {
+      if (a->argv[i] != b->argv[i]) {
+        pr_trace_msg(trace_channel, 18,
+          "configs '%s' and '%s' have mismatched argv[%u] (%p != %p)",
+          a_name, b_name, i, a->argv[i], b->argv[i]);
+        return 1;
+      }
+    }
+  }
+
+  if (a->config_id != b->config_id) {
+    pr_trace_msg(trace_channel, 18,
+      "configs '%s' and '%s' have mismatched config_id (%d != %d)",
+      a_name, b_name, a->config_id, b->config_id);
+    return 1;
+  }
+
+  /* Save the string comparison for last, to try to save some CPU. */
+  if (strcmp(a->name, b->name) != 0) {
+    pr_trace_msg(trace_channel, 18,
+      "configs '%s' and '%s' have mismatched name ('%s' != '%s')",
+      a_name, b_name, a->name, b->name);
+    return 1;
+  }
+
+  return 0;
+}
+
+static config_rec *copy_config_from(const config_rec *src, config_rec *dst) {
+  config_rec *c;
+  unsigned int cargc;
+  void **cargv, **sargv;
+
+  if (src == NULL ||
+      dst == NULL) {
+    return NULL;
+  }
+
+  /* If the destination parent config_rec doesn't already have a subset
+   * container, allocate one.
+   */
+  if (dst->subset == NULL) {
+    dst->subset = xaset_create(dst->pool, NULL);
+  }
+
+  c = pr_config_add_set(&dst->subset, src->name, 0);
+  c->config_type = src->config_type;
+  c->flags = src->flags;
+  c->config_id = src->config_id;
+
+  c->argc = src->argc;
+  c->argv = pcalloc(c->pool, (src->argc + 1) * sizeof(void *));
+
+  cargc = c->argc;
+  cargv = c->argv;
+  sargv = src->argv;
+
+  while (cargc--) {
+    pr_signals_handle();
+    *cargv++ = *sargv++;
+  }
+
+  *cargv = NULL; 
+  return c;
+}
+
+void pr_config_merge_down(xaset_t *s, int dynamic) {
+  config_rec *c, *dst;
+
+  if (s == NULL ||
+      s->xas_list == NULL) {
+    return;
+  }
+
+  for (c = (config_rec *) s->xas_list; c; c = c->next) {
+    pr_signals_handle();
+
+    if ((c->flags & CF_MERGEDOWN) ||
+        (c->flags & CF_MERGEDOWN_MULTI)) {
+
+      for (dst = (config_rec *) s->xas_list; dst; dst = dst->next) {
+        if (dst->config_type == CONF_ANON ||
+           dst->config_type == CONF_DIR) {
+
+          /* If an option of the same name/type is found in the
+           * next level down, it overrides, so we don't merge.
+           */
+          if ((c->flags & CF_MERGEDOWN) &&
+              find_config(dst->subset, c->config_type, c->name, FALSE)) {
+            continue;
+          }
+
+          if (dynamic) {
+            /* If we are doing a dynamic merge (i.e. .ftpaccess files) then
+             * we do not need to re-merge the static configs that are already
+             * there.  Otherwise we are creating copies needlessly of any
+             * config_rec marked with the CF_MERGEDOWN_MULTI flag, which
+             * adds to the memory usage/processing time.
+             *
+             * If neither the src or the dst config have the CF_DYNAMIC
+             * flag, it's a static config, and we can skip this merge and move
+             * on.  Otherwise, we can merge it.
+             */
+            if (!(c->flags & CF_DYNAMIC) && !(dst->flags & CF_DYNAMIC)) {
+              continue;
+            }
+          }
+
+          /* We want to scan the config_recs contained in dst's subset to see
+           * if we can find another config_rec that duplicates the one we want
+           * to merge into dst.
+           */
+          if (dst->subset != NULL) {
+              config_rec *r = NULL;
+            int merge = TRUE;
+
+            for (r = (config_rec *) dst->subset->xas_list; r; r = r->next) {
+              pr_signals_handle();
+
+              if (config_cmp(r, r->name, c, c->name) == 0) {
+                merge = FALSE;
+
+                pr_trace_msg(trace_channel, 15,
+                  "found duplicate '%s' record in '%s', skipping merge",
+                  r->name, dst->name);
+                break;
+              }
+            }
+
+            if (merge) {
+              (void) copy_config_from(c, dst);
+            }
+ 
+          } else {
+            /* No existing subset in dst; we can merge this one in. */
+            (void) copy_config_from(c, dst);
+          }
+        }
+      }
+    }
+  }
+
+  /* Top level merged, recursively merge lower levels */
+  for (c = (config_rec *) s->xas_list; c; c = c->next) {
+    if (c->subset &&
+        (c->config_type == CONF_ANON ||
+         c->config_type == CONF_DIR)) {
+      pr_config_merge_down(c->subset, dynamic);
+    }
+  }
+}
+
+config_rec *find_config_next2(config_rec *prev, config_rec *c, int type,
+    const char *name, int recurse, unsigned long flags) {
+  config_rec *top = c;
+  unsigned int cid = 0;
+  size_t namelen = 0;
+
+  /* We do two searches (if recursing) so that we find the "deepest"
+   * level first.
+   */
+
+  if (c == NULL &&
+      prev == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (prev == NULL) {
+    prev = top;
+  }
+
+  if (name != NULL) {
+    cid = pr_config_get_id(name);
+    namelen = strlen(name);
+  }
+
+  if (recurse) {
+    do {
+      config_rec *res = NULL;
+
+      pr_signals_handle();
+
+      for (c = top; c; c = c->next) {
+        if (c->subset &&
+            c->subset->xas_list) {
+          config_rec *subc = NULL;
+
+          for (subc = (config_rec *) c->subset->xas_list;
+               subc;
+               subc = subc->next) {
+            pr_signals_handle();
+
+            if (subc->config_type == CONF_ANON &&
+                (flags & PR_CONFIG_FIND_FL_SKIP_ANON)) {
+              /* Skip <Anonymous> config_rec */
+              continue;
+            }
+
+            if (subc->config_type == CONF_DIR &&
+                (flags & PR_CONFIG_FIND_FL_SKIP_DIR)) {
+              /* Skip <Directory> config_rec */
+              continue;
+            }
+
+            if (subc->config_type == CONF_LIMIT &&
+                (flags & PR_CONFIG_FIND_FL_SKIP_LIMIT)) {
+              /* Skip <Limit> config_rec */
+              continue;
+            }
+
+            if (subc->config_type == CONF_DYNDIR &&
+                (flags & PR_CONFIG_FIND_FL_SKIP_DYNDIR)) {
+              /* Skip .ftpaccess config_rec */
+              continue;
+            }
+
+            res = find_config_next2(NULL, subc, type, name, recurse + 1, flags);
+            if (res) {
+              return res;
+            }
+          }
+        }
+      }
+
+      /* If deep recursion yielded no match try the current subset.
+       *
+       * NOTE: the string comparison here is specifically case sensitive.
+       * The config_rec names are supplied by the modules and intentionally
+       * case sensitive (they shouldn't be verbatim from the config file)
+       * Do NOT change this to strcasecmp(), no matter how tempted you are
+       * to do so, it will break stuff. ;)
+       */
+      for (c = top; c; c = c->next) {
+        pr_signals_handle();
+
+        if (type == -1 ||
+            type == c->config_type) {
+
+          if (name == NULL) {
+            return c;
+          }
+
+          if (cid != 0 &&
+              cid == c->config_id) {
+            return c;
+          }
+
+          if (strncmp(name, c->name, namelen + 1) == 0) {
+            return c;
+          }
+        }
+      }
+
+      /* Restart the search at the previous level if required */
+      if (prev->parent &&
+          recurse == 1 &&
+          prev->parent->next &&
+          prev->parent->set != find_config_top) {
+        prev = top = prev->parent->next;
+        c = top;
+        continue;
+      }
+
+      break;
+    } while (TRUE);
+
+  } else {
+    for (c = top; c; c = c->next) {
+      pr_signals_handle();
+
+      if (type == -1 ||
+          type == c->config_type) {
+
+        if (name == NULL) {
+          return c;
+        }
+
+        if (cid != 0 &&
+            cid == c->config_id) {
+          return c;
+        }
+
+        if (strncmp(name, c->name, namelen + 1) == 0) {
+          return c;
+        }
+      }
+    }
+  }
+
+  errno = ENOENT;
+  return NULL;
+}
+
+config_rec *find_config_next(config_rec *prev, config_rec *c, int type,
+    const char *name, int recurse) {
+  return find_config_next2(prev, c, type, name, recurse, 0UL);
+}
+
+void find_config_set_top(config_rec *c) {
+  if (c &&
+      c->parent) {
+    find_config_top = c->parent->set;
+
+  } else {
+    find_config_top = NULL;
+  }
+}
+
+config_rec *find_config2(xaset_t *set, int type, const char *name,
+  int recurse, unsigned long flags) {
+
+  if (set == NULL ||
+      set->xas_list == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  find_config_set_top((config_rec *) set->xas_list);
+
+  return find_config_next2(NULL, (config_rec *) set->xas_list, type, name,
+    recurse, flags);
+}
+
+config_rec *find_config(xaset_t *set, int type, const char *name, int recurse) {
+  return find_config2(set, type, name, recurse, 0UL);
+}
+
+void *get_param_ptr(xaset_t *set, const char *name, int recurse) {
+  config_rec *c;
+
+  if (set == NULL) {
+    last_param_ptr = NULL;
+    errno = ENOENT;
+    return NULL;
+  }
+
+  c = find_config(set, CONF_PARAM, name, recurse);
+  if (c &&
+      c->argc) {
+    last_param_ptr = c;
+    return c->argv[0];
+  }
+
+  last_param_ptr = NULL;
+  errno = ENOENT;
+  return NULL;
+}
+
+void *get_param_ptr_next(const char *name, int recurse) {
+  config_rec *c;
+
+  if (!last_param_ptr ||
+      !last_param_ptr->next) {
+    last_param_ptr = NULL;
+    errno = ENOENT; 
+    return NULL;
+  }
+
+  c = find_config_next(last_param_ptr, last_param_ptr->next, CONF_PARAM,
+    name, recurse);
+  if (c &&
+      c->argv) {
+    last_param_ptr = c;
+    return c->argv[0];
+  }
+
+  last_param_ptr = NULL;
+  errno = ENOENT;
+  return NULL;
+}
+
+int pr_config_remove(xaset_t *set, const char *name, int flags, int recurse) {
+  server_rec *s;
+  config_rec *c;
+  int found = 0;
+  xaset_t *found_set;
+
+  s = pr_parser_server_ctxt_get();
+  if (s == NULL) {
+    s = main_server;
+  }
+
+  while ((c = find_config(set, -1, name, recurse)) != NULL) {
+    pr_signals_handle();
+
+    found++;
+
+    found_set = c->set;
+    xaset_remove(found_set, (xasetmember_t *) c);
+
+    /* If the set is empty, and has no more contained members in the xas_list,
+     * destroy the set.
+     */
+    if (!found_set->xas_list) {
+
+      /* First, set any pointers to the container of the set to NULL. */
+      if (c->parent &&
+          c->parent->subset == found_set) {
+        c->parent->subset = NULL;
+
+      } else if (s && s->conf == found_set) {
+        s->conf = NULL;
+      }
+
+      if (!(flags & PR_CONFIG_FL_PRESERVE_ENTRY)) {
+        /* Next, destroy the set's pool, which destroys the set as well. */
+        destroy_pool(found_set->pool);
+      }
+
+    } else {
+      if (!(flags & PR_CONFIG_FL_PRESERVE_ENTRY)) {
+        /* If the set was not empty, destroy only the requested config_rec. */
+        destroy_pool(c->pool);
+      }
+    }
+  }
+
+  return found;
+}
+
+int remove_config(xaset_t *set, const char *name, int recurse) {
+  return pr_config_remove(set, name, 0, recurse);
+}
+
+config_rec *add_config_param_set(xaset_t **set, const char *name,
+    unsigned int num, ...) {
+  config_rec *c;
+  void **argv;
+  va_list ap;
+
+  c = pr_config_add_set(set, name, 0);
+  if (c == NULL) {
+    return NULL;
+  }
+
+  c->config_type = CONF_PARAM;
+  c->argc = num;
+  c->argv = pcalloc(c->pool, (num+1) * sizeof(void *));
+
+  argv = c->argv;
+  va_start(ap,num);
+
+  while (num-- > 0) {
+    *argv++ = va_arg(ap, void *);
+  }
+
+  va_end(ap);
+
+  return c;
+}
+
+config_rec *add_config_param_str(const char *name, unsigned int num, ...) {
+  config_rec *c;
+  char *arg = NULL;
+  void **argv = NULL;
+  va_list ap;
+
+  c = pr_config_add(NULL, name, 0);
+  if (c != NULL) {
+    c->config_type = CONF_PARAM;
+    c->argc = num;
+    c->argv = pcalloc(c->pool, (num+1) * sizeof(char *));
+
+    argv = c->argv;
+    va_start(ap, num);
+
+    while (num-- > 0) {
+      arg = va_arg(ap, char *);
+      if (arg) {
+        *argv++ = pstrdup(c->pool, arg);
+
+      } else {
+        *argv++ = NULL;
+      }
+    }
+
+    va_end(ap);
+  }
+
+  return c;
+}
+
+config_rec *pr_conf_add_server_config_param_str(server_rec *s, const char *name,
+    unsigned int num, ...) {
+  config_rec *c;
+  char *arg = NULL;
+  void **argv = NULL;
+  va_list ap;
+
+  c = pr_config_add(s, name, 0);
+  if (c == NULL) {
+    return NULL;
+  }
+
+  c->config_type = CONF_PARAM;
+  c->argc = num;
+  c->argv = pcalloc(c->pool, (num+1) * sizeof(char *));
+
+  argv = c->argv;
+  va_start(ap, num);
+
+  while (num-- > 0) {
+    arg = va_arg(ap, char *);
+    if (arg) {
+      *argv++ = pstrdup(c->pool, arg);
+
+    } else {
+      *argv++ = NULL;
+    }
+  }
+
+  va_end(ap);
+  return c;
+}
+
+config_rec *add_config_param(const char *name, unsigned int num, ...) {
+  config_rec *c;
+  void **argv;
+  va_list ap;
+
+  if (name == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  c = pr_config_add(NULL, name, 0);
+  if (c) {
+    c->config_type = CONF_PARAM;
+    c->argc = num;
+    c->argv = pcalloc(c->pool, (num+1) * sizeof(void*));
+
+    argv = c->argv;
+    va_start(ap, num);
+
+    while (num-- > 0) {
+      *argv++ = va_arg(ap, void *);
+    }
+
+    va_end(ap);
+  }
+
+  return c;
+}
+
+unsigned int pr_config_get_id(const char *name) {
+  const void *ptr = NULL;
+  unsigned int id = 0;
+
+  if (name == NULL) {
+    errno = EINVAL;
+    return 0;
+  }
+
+  if (config_tab == NULL) {
+    errno = EPERM;
+    return 0;
+  }
+
+  ptr = pr_table_get(config_tab, name, NULL);
+  if (ptr == NULL) {
+    errno = ENOENT;
+    return 0;
+  }
+
+  id = *((unsigned int *) ptr);
+  return id;
+}
+
+unsigned int pr_config_set_id(const char *name) {
+  unsigned int *ptr = NULL;
+  unsigned int id;
+
+  if (!name) {
+    errno = EINVAL;
+    return 0;
+  }
+
+  if (!config_tab) {
+    errno = EPERM;
+    return 0;
+  }
+
+  ptr = pr_table_pcalloc(config_tab, sizeof(unsigned int));
+  *ptr = ++config_id;
+
+  if (pr_table_add(config_tab, name, ptr, sizeof(unsigned int *)) < 0) {
+    if (errno == EEXIST) {
+      id = pr_config_get_id(name);
+
+    } else {
+      if (errno == ENOSPC) {
+        pr_log_debug(DEBUG9,
+         "error adding '%s' to config ID table: table is full", name);
+
+      } else {
+        pr_log_debug(DEBUG9, "error adding '%s' to config ID table: %s",
+          name, strerror(errno));
+      }
+
+      return 0;
+    }
+
+  } else {
+    id = *ptr;
+  }
+
+  return id;
+}
+
+void init_config(void) {
+  unsigned int maxents;
+
+  /* Make sure global_config_pool is destroyed */
+  if (global_config_pool) {
+    destroy_pool(global_config_pool);
+    global_config_pool = NULL;
+  }
+
+  if (config_tab) {
+    /* Clear the existing config ID table.  This needs to happen when proftpd
+     * is restarting.
+     */
+    if (pr_table_empty(config_tab) < 0) {
+      pr_log_debug(DEBUG0, "error emptying config ID table: %s",
+        strerror(errno));
+    }
+
+    if (pr_table_free(config_tab) < 0) {
+      pr_log_debug(DEBUG0, "error destroying config ID table: %s",
+        strerror(errno));
+    }
+
+    config_tab = pr_table_alloc(config_tab_pool, 0);
+
+    /* Reset the ID counter as well.  Otherwise, an exceedingly long-lived
+     * proftpd, restarted many times, has the possibility of overflowing
+     * the counter data type.
+     */
+    config_id = 0;
+
+  } else {
+
+    config_tab_pool = make_sub_pool(permanent_pool);
+    pr_pool_tag(config_tab_pool, "Config Table Pool");
+    config_tab = pr_table_alloc(config_tab_pool, 0);
+  }
+
+  /* Increase the max "size" of the table; some configurations can lead
+   * to a large number of configuration directives.
+   */
+  maxents = 32768;
+
+  if (pr_table_ctl(config_tab, PR_TABLE_CTL_SET_MAX_ENTS, &maxents) < 0) {
+    pr_log_debug(DEBUG2, "error setting config ID table max size to %u: %s",
+      maxents, strerror(errno));
+  }
+
+  return;
+}
diff --git a/src/ctrls.c b/src/ctrls.c
index 8b659b9..6539dd7 100644
--- a/src/ctrls.c
+++ b/src/ctrls.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -43,6 +43,12 @@
 
 #include "mod_ctrls.h"
 
+/* Maximum number of request arguments. */
+#define CTRLS_MAX_NREQARGS	32
+
+/* Maximum number of response arguments. */
+#define CTRLS_MAX_NRESPARGS	1024
+
 /* Maximum length of a single request argument. */
 #define CTRLS_MAX_REQARGLEN	256
 
@@ -237,10 +243,12 @@ static char *ctrls_sep(char **str) {
 int pr_ctrls_register(const module *mod, const char *action,
     const char *desc, int (*cb)(pr_ctrls_t *, int, char **)) {
   ctrls_action_t *act = NULL, *acti = NULL;
-  int act_id = -1;
+  unsigned int act_id = 0;
 
   /* sanity checks */
-  if (!action || !desc || !cb) {
+  if (action == NULL ||
+      desc == NULL ||
+      cb == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -348,7 +356,7 @@ int pr_ctrls_add_arg(pr_ctrls_t *ctrl, char *ctrls_arg, size_t ctrls_arglen) {
 
   /* Scan for non-printable characters. */
   for (i = 0; i < ctrls_arglen; i++) {
-    if (!isprint((int) ctrls_arg[i])) {
+    if (!PR_ISPRINT((int) ctrls_arg[i])) {
       errno = EPERM;
       return -1;
     }
@@ -357,18 +365,18 @@ int pr_ctrls_add_arg(pr_ctrls_t *ctrl, char *ctrls_arg, size_t ctrls_arglen) {
   /* Make sure the pr_ctrls_t has a temporary pool, from which the args will
    * be allocated.
    */
-  if (!ctrl->ctrls_tmp_pool) {
+  if (ctrl->ctrls_tmp_pool == NULL) {
     ctrl->ctrls_tmp_pool = make_sub_pool(ctrls_pool);
     pr_pool_tag(ctrl->ctrls_tmp_pool, "ctrls tmp pool");
   }
 
-  if (!ctrl->ctrls_cb_args) {
+  if (ctrl->ctrls_cb_args == NULL) {
     ctrl->ctrls_cb_args = make_array(ctrl->ctrls_tmp_pool, 0, sizeof(char *));
   }
 
   /* Add the given argument */
-  *((char **) push_array(ctrl->ctrls_cb_args)) = pstrdup(ctrl->ctrls_tmp_pool,
-    ctrls_arg);
+  *((char **) push_array(ctrl->ctrls_cb_args)) = pstrndup(ctrl->ctrls_tmp_pool,
+    ctrls_arg, ctrls_arglen);
 
   return 0;
 }
@@ -376,7 +384,8 @@ int pr_ctrls_add_arg(pr_ctrls_t *ctrl, char *ctrls_arg, size_t ctrls_arglen) {
 int pr_ctrls_copy_args(pr_ctrls_t *src_ctrl, pr_ctrls_t *dst_ctrl) {
 
   /* Sanity checks */
-  if (!src_ctrl || !dst_ctrl) {
+  if (src_ctrl == NULL ||
+      dst_ctrl == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -384,13 +393,14 @@ int pr_ctrls_copy_args(pr_ctrls_t *src_ctrl, pr_ctrls_t *dst_ctrl) {
   /* If source ctrl has no ctrls_cb_args member, there's nothing to be
    * done.
    */
-  if (!src_ctrl->ctrls_cb_args)
+  if (src_ctrl->ctrls_cb_args == NULL) {
     return 0;
+  }
 
   /* Make sure the pr_ctrls_t has a temporary pool, from which the args will
    * be allocated.
    */
-  if (!dst_ctrl->ctrls_tmp_pool) {
+  if (dst_ctrl->ctrls_tmp_pool == NULL) {
     dst_ctrl->ctrls_tmp_pool = make_sub_pool(ctrls_pool);
     pr_pool_tag(dst_ctrl->ctrls_tmp_pool, "ctrls tmp pool");
   }
@@ -518,10 +528,10 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
   char reqaction[128] = {'\0'}, *reqarg = NULL;
   size_t reqargsz = 0;
   unsigned int nreqargs = 0, reqarglen = 0;
-  int status = 0;
-  register int i = 0;
+  int bread, status = 0;
+  register unsigned int i = 0;
 
-  if (!cl) {
+  if (cl == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -540,7 +550,8 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
    * the same function, pr_ctrls_send_msg(), is used to send requests
    * as well as responses, and the status is a necessary part of a response.
    */
-  if (read(cl->cl_fd, &status, sizeof(int)) < 0) {
+  bread = read(cl->cl_fd, &status, sizeof(int));
+  if (bread < 0) {
     int xerrno = errno;
 
     pr_signals_unblock();
@@ -548,9 +559,20 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
     errno = xerrno;
     return -1;
   }
+
+  /* Watch for short reads. */
+  if (bread != sizeof(int)) {
+    (void) pr_trace_msg(trace_channel, 3,
+      "short read (%d of %u bytes) of status, unable to receive request",
+      bread, (unsigned int) sizeof(int));
+    pr_signals_unblock();
+    errno = EPERM;
+    return -1;
+  }
  
   /* Read in the args, length first, then string. */
-  if (read(cl->cl_fd, &nreqargs, sizeof(unsigned int)) < 0) {
+  bread = read(cl->cl_fd, &nreqargs, sizeof(unsigned int));
+  if (bread < 0) {
     int xerrno = errno;
 
     pr_signals_unblock();
@@ -559,13 +581,33 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
     return -1;
   }
 
+  /* Watch for short reads. */
+  if (bread != sizeof(unsigned int)) {
+    (void) pr_trace_msg(trace_channel, 3,
+      "short read (%d of %u bytes) of nreqargs, unable to receive request",
+      bread, (unsigned int) sizeof(unsigned int));
+    pr_signals_unblock();
+    errno = EPERM;
+    return -1;
+  }
+
+  if (nreqargs > CTRLS_MAX_NREQARGS) {
+    (void) pr_trace_msg(trace_channel, 3,
+      "nreqargs (%u) exceeds max (%u), rejecting", nreqargs,
+      CTRLS_MAX_NREQARGS);
+    pr_signals_unblock();
+    errno = ENOMEM;
+    return -1;
+  }
+
   /* Next, read in the requested number of arguments.  The client sends
    * the arguments in pairs: first the length of the argument, then the
    * argument itself.  The first argument is the action, so get the first
    * matching pr_ctrls_t (if present), and add the remaining arguments to it.
    */
   
-  if (read(cl->cl_fd, &reqarglen, sizeof(unsigned int)) < 0) {
+  bread = read(cl->cl_fd, &reqarglen, sizeof(unsigned int));
+  if (bread < 0) {
     int xerrno = errno;
 
     pr_signals_unblock();
@@ -574,6 +616,16 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
     return -1;
   }
 
+  /* Watch for short reads. */
+  if (bread != sizeof(unsigned int)) {
+    (void) pr_trace_msg(trace_channel, 3,
+      "short read (%d of %u bytes) of reqarglen, unable to receive request",
+      bread, (unsigned int) sizeof(unsigned int));
+    pr_signals_unblock();
+    errno = EPERM;
+    return -1;
+  }
+
   if (reqarglen >= sizeof(reqaction)) {
     pr_signals_unblock();
     errno = ENOMEM;
@@ -582,7 +634,8 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
 
   memset(reqaction, '\0', sizeof(reqaction));
 
-  if (read(cl->cl_fd, reqaction, reqarglen) < 0) {
+  bread = read(cl->cl_fd, reqaction, reqarglen);
+  if (bread < 0) {
     int xerrno = errno;
 
     pr_signals_unblock();
@@ -591,6 +644,16 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
     return -1;
   }
 
+  /* Watch for short reads. */
+  if ((size_t) bread != reqarglen) {
+    (void) pr_trace_msg(trace_channel, 3,
+      "short read (%d of %u bytes) of reqaction, unable to receive request",
+      bread, reqarglen);
+    pr_signals_unblock();
+    errno = EPERM;
+    return -1;
+  }
+
   reqaction[sizeof(reqaction)-1] = '\0';
   nreqargs--;
 
@@ -600,6 +663,8 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
   ctrl = ctrls_lookup_action(NULL, reqaction, TRUE);
   if (ctrl == NULL) {
     pr_signals_unblock();
+
+    /* XXX This is where we could also add "did you mean" functionality. */
     errno = EINVAL;
     return -1;
   }
@@ -607,7 +672,8 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
   for (i = 0; i < nreqargs; i++) {
     memset(reqarg, '\0', reqargsz);
 
-    if (read(cl->cl_fd, &reqarglen, sizeof(unsigned int)) < 0) {
+    bread = read(cl->cl_fd, &reqarglen, sizeof(unsigned int));
+    if (bread < 0) {
       int xerrno = errno;
 
       pr_signals_unblock();
@@ -616,12 +682,23 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
       return -1;
     }
 
+    /* Watch for short reads. */
+    if (bread != sizeof(unsigned int)) {
+      (void) pr_trace_msg(trace_channel, 3,
+        "short read (%d of %u bytes) of reqarglen (#%u), skipping",
+        bread, i+1, (unsigned int) sizeof(unsigned int));
+      continue;
+    }
+
     if (reqarglen == 0) {
       /* Skip any zero-length arguments. */
       continue;
     }
 
     if (reqarglen > CTRLS_MAX_REQARGLEN) {
+      (void) pr_trace_msg(trace_channel, 3,
+        "reqarglen (#%u) of %u bytes exceeds max (%u bytes), rejecting",
+        i+1, reqarglen, CTRLS_MAX_REQARGLEN);
       pr_signals_unblock();
       errno = ENOMEM;
       return -1;
@@ -642,7 +719,8 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
       reqarg = pcalloc(ctrl->ctrls_tmp_pool, reqargsz);
     }
 
-    if (read(cl->cl_fd, reqarg, reqarglen) < 0) {
+    bread = read(cl->cl_fd, reqarg, reqarglen);
+    if (bread < 0) {
       int xerrno = errno;
 
       pr_signals_unblock();
@@ -651,6 +729,14 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
       return -1;
     }
 
+    /* Watch for short reads. */
+    if ((size_t) bread != reqarglen) {
+      (void) pr_trace_msg(trace_channel, 3,
+        "short read (%d of %u bytes) of reqarg (#%u), skipping",
+        bread, i+1, reqarglen);
+      continue;
+    }
+
     if (pr_ctrls_add_arg(ctrl, reqarg, reqarglen)) {
       int xerrno = errno;
 
@@ -673,8 +759,13 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
    */
   next_ctrl = ctrls_lookup_next_action(NULL, TRUE);
 
-  while (next_ctrl) {
-    if (pr_ctrls_copy_args(ctrl, next_ctrl)) {
+  while (next_ctrl != NULL) {
+    if (pr_ctrls_copy_args(ctrl, next_ctrl) < 0) {
+      int xerrno = errno;
+
+      pr_signals_unblock();
+
+      errno = xerrno;
       return -1;
     }
 
@@ -694,13 +785,15 @@ int pr_ctrls_recv_request(pr_ctrls_cl_t *cl) {
 
 int pr_ctrls_recv_response(pool *resp_pool, int ctrls_sockfd,
     int *status, char ***respargv) {
-  register int i = 0;
+  register unsigned int i = 0;
   array_header *resparr = NULL;
   unsigned int respargc = 0, resparglen = 0;
   char response[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
 
   /* Sanity checks */
-  if (!resp_pool || ctrls_sockfd < 0 || !status) {
+  if (resp_pool == NULL ||
+      ctrls_sockfd < 0 ||
+      status == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -724,6 +817,15 @@ int pr_ctrls_recv_response(pool *resp_pool, int ctrls_sockfd,
     return -1;
   }
 
+  if (respargc > CTRLS_MAX_NRESPARGS) {
+    (void) pr_trace_msg(trace_channel, 3,
+      "respargc (%u) exceeds max (%u), rejecting", respargc,
+      CTRLS_MAX_NRESPARGS);
+    pr_signals_unblock();
+    errno = ENOMEM;
+    return -1;
+  }
+
   /* Read each response, and add it to the array */ 
   for (i = 0; i < respargc; i++) {
     int bread = 0, blen = 0;
@@ -744,7 +846,7 @@ int pr_ctrls_recv_response(pool *resp_pool, int ctrls_sockfd,
     memset(response, '\0', sizeof(response));
 
     bread = read(ctrls_sockfd, response, resparglen);
-    while (bread != resparglen) {
+    while ((size_t) bread != resparglen) {
       if (bread < 0) {
         pr_signals_unblock(); 
         return -1;
@@ -770,7 +872,7 @@ int pr_ctrls_recv_response(pool *resp_pool, int ctrls_sockfd,
 
 int pr_ctrls_send_msg(int sockfd, int msgstatus, unsigned int msgargc,
     char **msgargv) {
-  register int i = 0;
+  register unsigned int i = 0;
   unsigned int msgarglen = 0;
 
   /* Sanity checks */
@@ -828,8 +930,7 @@ int pr_ctrls_send_msg(int sockfd, int msgstatus, unsigned int msgargc,
 
     while (TRUE) {
       res = write(sockfd, msgargv[i], msgarglen);
-
-      if (res != msgarglen) {
+      if ((size_t) res != msgarglen) {
         if (errno == EAGAIN) {
           continue;
         }
@@ -1012,7 +1113,8 @@ int pr_ctrls_connect(const char *socket_file) {
   if (fcntl(sockfd, F_SETFD, FD_CLOEXEC) < 0) {
     int xerrno = errno;
 
-    close(sockfd);
+    (void) close(sockfd);
+    pr_signals_unblock();
 
     errno = xerrno;
     return -1;
@@ -1034,13 +1136,14 @@ int pr_ctrls_connect(const char *socket_file) {
   len = sizeof(cl_sock);
 
   /* Make sure the file doesn't already exist */
-  unlink(cl_sock.sun_path);
+  (void) unlink(cl_sock.sun_path);
 
   /* Make it a socket */
   if (bind(sockfd, (struct sockaddr *) &cl_sock, len) < 0) {
     int xerrno = errno;
 
-    unlink(cl_sock.sun_path);
+    (void) unlink(cl_sock.sun_path);
+    (void) close(sockfd);
     pr_signals_unblock();
 
     errno = xerrno;
@@ -1051,7 +1154,8 @@ int pr_ctrls_connect(const char *socket_file) {
   if (chmod(cl_sock.sun_path, PR_CTRLS_CL_MODE) < 0) {
     int xerrno = errno;
 
-    unlink(cl_sock.sun_path);
+    (void) unlink(cl_sock.sun_path);
+    (void) close(sockfd);
     pr_signals_unblock();
 
     errno = xerrno;
@@ -1068,7 +1172,8 @@ int pr_ctrls_connect(const char *socket_file) {
   if (connect(sockfd, (struct sockaddr *) &ctrl_sock, len) < 0) {
     int xerrno = errno;
 
-    unlink(cl_sock.sun_path);
+    (void) unlink(cl_sock.sun_path);
+    (void) close(sockfd);
     pr_signals_unblock();
 
     errno = xerrno;
@@ -1080,7 +1185,8 @@ int pr_ctrls_connect(const char *socket_file) {
   if (ctrls_connect_local_creds(sockfd) < 0) {
     int xerrno = errno;
 
-    unlink(cl_sock.sun_path);
+    (void) unlink(cl_sock.sun_path);
+    (void) close(sockfd);
     pr_signals_unblock();
 
     errno = xerrno;
@@ -1625,7 +1731,7 @@ int pr_reset_ctrls(void) {
  */
 unsigned char pr_ctrls_check_group_acl(gid_t cl_gid,
     const ctrls_grp_acl_t *grp_acl) {
-  register int i = 0;
+  register unsigned int i = 0;
   unsigned char res = FALSE;
 
   /* Note: the special condition of ngids of 1 and gids of NULL signals
@@ -1638,11 +1744,13 @@ unsigned char pr_ctrls_check_group_acl(gid_t cl_gid,
       }
     }
 
-  } else if (grp_acl->ngids == 1)
+  } else if (grp_acl->ngids == 1) {
     res = TRUE;
+  }
 
-  if (!grp_acl->allow)
+  if (!grp_acl->allow) {
     res = !res;
+  }
 
   return res;
 }
@@ -1653,7 +1761,7 @@ unsigned char pr_ctrls_check_group_acl(gid_t cl_gid,
  */
 unsigned char pr_ctrls_check_user_acl(uid_t cl_uid,
     const ctrls_usr_acl_t *usr_acl) {
-  register int i = 0;
+  register unsigned int i = 0;
   unsigned char res = FALSE;
 
   /* Note: the special condition of nuids of 1 and uids of NULL signals
@@ -1666,11 +1774,13 @@ unsigned char pr_ctrls_check_user_acl(uid_t cl_uid,
       }
     }
 
-  } else if (usr_acl->nuids == 1)
+  } else if (usr_acl->nuids == 1) {
     res = TRUE;
+  }
 
-  if (!usr_acl->allow)
+  if (!usr_acl->allow) {
     res = !res;
+  }
 
   return res;
 }
diff --git a/src/data.c b/src/data.c
index f2ea94f..05b3a34 100644
--- a/src/data.c
+++ b/src/data.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2014 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Data connection management functions
- * $Id: data.c,v 1.152 2013-11-09 23:20:23 castaglia Exp $
- */
+/* Data connection management functions */
 
 #include "conf.h"
 
@@ -39,6 +37,13 @@
 #endif /* HAVE_SYS_UIO_H */
 
 static const char *trace_channel = "data";
+static const char *timing_channel = "timing";
+
+#define PR_DATA_OPT_IGNORE_ASCII	0x0001
+static unsigned long data_opts = 0UL;
+static uint64_t data_start_ms = 0L;
+static int data_first_byte_read = FALSE;
+static int data_first_byte_written = FALSE;
 
 /* local macro */
 
@@ -65,8 +70,7 @@ static int stalled_timeout_cb(CALLBACK_FRAME) {
   pr_session_disconnect(NULL, PR_SESS_DISCONNECT_TIMEOUT,
     "TimeoutStalled during data transfer");
 
-  /* Prevent compiler warning.
-   */
+  /* Prevent compiler warning. */
   return 0;
 }
 
@@ -87,162 +91,30 @@ static RETSIGTYPE data_urgent(int signo) {
   signal(SIGURG, data_urgent);
 }
 
-static int xfrm_ascii_read(char *buf, int *bufsize, int *adjlen) {
-  char *dest = buf,*src = buf;
-  int thislen = *bufsize;
-
-  *adjlen = 0;
-  while (thislen--) {
-    if (*src != '\r')
-      *dest++ = *src++;
-    else {
-      if (thislen == 0) {
-	/* copy, but save it for later */
-	*dest++ = *src++;
-	(*adjlen)++;
-	(*bufsize)--;
-      } else {
-	if (*(src+1) == '\n') { /* skip */
-	  (*bufsize)--;
-	  src++;
-	} else
-	  *dest++ = *src++;
-      }
-    }
-  }
-
-  return *bufsize;
-}
-
-/* this function rewrites the contents of the given buffer, making sure that
- * each LF has a preceding CR, as required by RFC959:
- *
- *  buf = pointer to a buffer
- *  buflen = length of data in buffer
- *  bufsize = total size of buffer
- *  expand = will contain the number of expansion bytes (CRs) added,
- *           and should be the difference between buflen's original
- *           value and its value when this function returns
- */
-
-static int have_dangling_cr = FALSE;
-static unsigned int xfrm_ascii_write(char **buf, unsigned int *buflen,
-    unsigned int bufsize) {
-  char *tmpbuf = *buf;
-  unsigned int tmplen = *buflen;
-  unsigned int lfcount = 0;
-  unsigned int added = 0;
-
-  int res = 0;
-  register unsigned int i = 0;
-
-  /* First, determine how many bare LFs are present. */
-  if (!have_dangling_cr && tmpbuf[0] == '\n')
-    lfcount++;
-
-  for (i = 1; i < tmplen; i++)
-    if (tmpbuf[i] == '\n' && tmpbuf[i-1] != '\r')
-      lfcount++;
-
-  /* If the last character in the buffer is CR, then we have a dangling CR.
-   * The first character in the next buffer could be an LF, and without
-   * this flag, that LF would be treated as a bare LF, thus resulting in
-   * an added extraneous CR in the stream.
-   */
-  have_dangling_cr = (tmpbuf[tmplen-1] == '\r') ? TRUE : FALSE;
-
-  if (lfcount == 0)
-    /* No translation needed. */
-    return 0;
-
-  /* Assume that for each LF (including a leading LF), space for another
-   * char (a '\r') is needed.  Determine whether there is enough space in
-   * the buffer for the adjusted data.  If not, allocate a new buffer that is
-   * large enough.  The new buffer is allocated from session.xfer.p, which is
-   * fine; this pool has a lifetime only for this current data transfer, and
-   * will be cleared after the transfer is done, either having succeeded or
-   * failed.
-   *
-   * Note: the res variable is needed in order to force signedness of the
-   * resulting difference.  Without it, this condition would never evaluate
-   * to true, as C's promotion rules would ensure that the resulting value
-   * would be of the same type as the operands: an unsigned int (which will
-   * never be less than zero).
-   */
-  if ((res = (bufsize - tmplen - lfcount)) <= 0) {
-    char *copybuf = malloc(tmplen);
-    if (copybuf == NULL) {
-      pr_log_pri(PR_LOG_ALERT, "Out of memory!");
-      exit(1);
-    }
-
-    memcpy(copybuf, tmpbuf, tmplen);
-
-    /* Allocate a new session.xfer.buf of the needed size. */
-    session.xfer.bufsize = tmplen + lfcount + 1;
-    session.xfer.buf = pcalloc(session.xfer.p, session.xfer.bufsize);
-
-    memcpy(session.xfer.buf, copybuf, tmplen);
-
-    free(copybuf);
-    copybuf = NULL;
-
-    tmpbuf = session.xfer.buf;
-    bufsize = session.xfer.bufsize;
-  }
-
-  if (tmpbuf[0] == '\n') {
-
-    /* Shift everything in the buffer to the right one character, making
-     * space for a '\r'
-     */
-    memmove(&(tmpbuf[1]), &(tmpbuf[0]), tmplen);
-    tmpbuf[0] = '\r';
-
-    /* Increment the number of added characters, and decrement the number
-     * of bare LFs.
-     */
-    added++;
-    lfcount--;
-  }
-
-  for (i = 1; i < bufsize && (lfcount > 0); i++) {
-    if (tmpbuf[i] == '\n' && tmpbuf[i-1] != '\r') {
-      memmove(&(tmpbuf[i+1]), &(tmpbuf[i]), bufsize - i - 1);
-      tmpbuf[i] = '\r';
-      added++;
-      lfcount--;
-    }
-  }
-
-  *buf = tmpbuf;
-  *buflen = tmplen + added;
-
-  return added;
-}
-
 static void data_new_xfer(char *filename, int direction) {
   pr_data_clear_xfer_pool();
 
   session.xfer.p = make_sub_pool(session.pool);
-  pr_pool_tag(session.xfer.p, "data transfer pool");
+  pr_pool_tag(session.xfer.p, "Data Transfer pool");
 
   session.xfer.filename = pstrdup(session.xfer.p, filename);
   session.xfer.direction = direction;
   session.xfer.bufsize = pr_config_get_server_xfer_bufsz(direction);
   session.xfer.buf = pcalloc(session.xfer.p, session.xfer.bufsize + 1);
-  pr_trace_msg("data", 8, "allocated data transfer buffer of %lu bytes",
+  pr_trace_msg(trace_channel, 8, "allocated data transfer buffer of %lu bytes",
     (unsigned long) session.xfer.bufsize);
   session.xfer.buf++;	/* leave room for ascii translation */
   session.xfer.buflen = 0;
 }
 
-static int data_pasv_open(char *reason, off_t size) {
+static int data_passive_open(const char *reason, off_t size) {
   conn_t *c;
-  int rev;
+  int rev, xerrno = 0;
 
-  if (!reason && session.xfer.filename)
+  if (reason == NULL &&
+      session.xfer.filename) {
     reason = session.xfer.filename;
+  }
 
   /* Set the "stalled" timer, if any, to prevent the connection
    * open from taking too long
@@ -276,7 +148,7 @@ static int data_pasv_open(char *reason, off_t size) {
 
   if (c && c->mode != CM_ERROR) {
     pr_inet_close(session.pool, session.d);
-    pr_inet_set_nonblock(session.pool, c);
+    (void) pr_inet_set_nonblock(session.pool, c);
     session.d = c;
 
     pr_log_debug(DEBUG4, "passive data connection opened - local  : %s:%d",
@@ -322,26 +194,38 @@ static int data_pasv_open(char *reason, off_t size) {
   }
 
   /* Check for error conditions. */
-  if (c && c->mode == CM_ERROR) {
+  if (c != NULL &&
+      c->mode == CM_ERROR) {
     pr_log_pri(PR_LOG_ERR, "error: unable to accept an incoming data "
       "connection: %s", strerror(c->xerrno));
   }
 
+  xerrno = session.d->xerrno;
   pr_response_add_err(R_425, _("Unable to build data connection: %s"),
-    strerror(session.d->xerrno));
+    strerror(xerrno));
   destroy_pool(session.d->pool);
   session.d = NULL;
+
+  errno = xerrno;
   return -1;
 }
 
-static int data_active_open(char *reason, off_t size) {
+static int data_active_open(const char *reason, off_t size) {
   conn_t *c;
   int bind_port, rev;
-  pr_netaddr_t *bind_addr;
+  const pr_netaddr_t *bind_addr = NULL;
   unsigned char *root_revoke = NULL;
 
-  if (!reason && session.xfer.filename)
+  if (session.c->remote_addr == NULL) {
+    /* An opened but unconnected connection? */
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reason == NULL &&
+      session.xfer.filename) {
     reason = session.xfer.filename;
+  }
 
   if (pr_netaddr_get_family(session.c->local_addr) == pr_netaddr_get_family(session.c->remote_addr)) {
     bind_addr = session.c->local_addr;
@@ -441,17 +325,20 @@ static int data_active_open(char *reason, off_t size) {
     session.d->local_addr, session.d->listen_fd);
 
   if (pr_inet_connect(session.d->pool, session.d, &session.data_addr,
-      session.data_port) == -1) {
+      session.data_port) < 0) {
+    int xerrno = session.d->xerrno;
+
     pr_log_debug(DEBUG6,
       "Error connecting to %s#%u for active data transfer: %s",
       pr_netaddr_get_ipstr(&session.data_addr), session.data_port,
-      strerror(session.d->xerrno));
+      strerror(xerrno));
     pr_response_add_err(R_425, _("Unable to build data connection: %s"),
-      strerror(session.d->xerrno));
-    errno = session.d->xerrno;
+      strerror(xerrno));
 
     destroy_pool(session.d->pool);
     session.d = NULL;
+
+    errno = xerrno;
     return -1;
   }
 
@@ -501,7 +388,7 @@ static int data_active_open(char *reason, off_t size) {
     }
 
     pr_inet_close(session.pool, session.d);
-    pr_inet_set_nonblock(session.pool, session.d);
+    (void) pr_inet_set_nonblock(session.pool, session.d);
     session.d = c;
     return 0;
   }
@@ -574,32 +461,84 @@ void pr_data_reset(void) {
   }
 
   /* Clear any leftover state from previous transfers. */
-  have_dangling_cr = FALSE;
+  pr_ascii_ftp_reset();
 
   session.d = NULL;
   session.sf_flags &= (SF_ALL^(SF_ABORT|SF_POST_ABORT|SF_XFER|SF_PASSIVE|SF_ASCII_OVERRIDE|SF_EPSV_ALL));
 }
 
+int pr_data_ignore_ascii(int ignore_ascii) {
+  int res;
+
+  if (ignore_ascii != TRUE && 
+      ignore_ascii != FALSE) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (data_opts & PR_DATA_OPT_IGNORE_ASCII) {
+    if (!ignore_ascii) {
+      data_opts &= ~PR_DATA_OPT_IGNORE_ASCII;
+    }
+
+    res = TRUE;
+
+  } else {
+    if (ignore_ascii) {
+      data_opts |= PR_DATA_OPT_IGNORE_ASCII;
+    }
+
+    res = FALSE;
+  }
+
+  return res;
+}
+
 void pr_data_init(char *filename, int direction) {
   if (session.xfer.p == NULL) {
     data_new_xfer(filename, direction);
 
   } else {
     if (!(session.sf_flags & SF_PASSIVE)) {
-      pr_log_debug(DEBUG0, "data_init oddity: session.xfer exists in "
-        "non-PASV mode.");
+      pr_log_debug(DEBUG5,
+        "data_init oddity: session.xfer exists in non-PASV mode");
     }
 
     session.xfer.direction = direction;
   }
 
   /* Clear any leftover state from previous transfers. */
-  have_dangling_cr = FALSE;
+  pr_ascii_ftp_reset();
 }
 
 int pr_data_open(char *filename, char *reason, int direction, off_t size) {
   int res = 0;
 
+  if (session.c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if ((session.sf_flags & SF_PASSIVE) ||
+      (session.sf_flags & SF_EPSV_ALL)) {
+    /* For passive transfers, we expect there to already be an existing
+     * data connection...
+     */
+    if (session.d == NULL) {
+      errno = EINVAL;
+      return -1;
+    }
+
+  } else {
+    /* ...but for active transfers, we expect there to NOT be an existing
+     * data connection.
+     */
+    if (session.d != NULL) {
+      errno = session.d->xerrno = EINVAL;
+      return -1;
+    }
+  }
+
   /* Make sure that any abort flags have been cleared. */
   session.sf_flags &= ~(SF_ABORT|SF_POST_ABORT);
 
@@ -610,28 +549,17 @@ int pr_data_open(char *filename, char *reason, int direction, off_t size) {
     session.xfer.direction = direction;
   }
 
-  if (!reason)
+  if (!reason) {
     reason = filename;
+  }
 
   /* Passive data transfers... */
-  if (session.sf_flags & SF_PASSIVE ||
-      session.sf_flags & SF_EPSV_ALL) {
-    if (session.d == NULL) {
-      pr_log_pri(PR_LOG_ERR, "Internal error: PASV mode set, but no data "
-        "connection listening");
-      pr_session_disconnect(NULL, PR_SESS_DISCONNECT_BY_APPLICATION, NULL);
-    }
-
-    res = data_pasv_open(reason, size);
+  if ((session.sf_flags & SF_PASSIVE) ||
+      (session.sf_flags & SF_EPSV_ALL)) {
+    res = data_passive_open(reason, size);
 
   /* Active data transfers... */
   } else {
-    if (session.d != NULL) {
-      pr_log_pri(PR_LOG_ERR, "Internal error: non-PASV mode, yet data "
-        "connection already exists?!?");
-      pr_session_disconnect(NULL, PR_SESS_DISCONNECT_BY_APPLICATION, NULL);
-    }
-
     res = data_active_open(reason, size);
   }
 
@@ -659,16 +587,18 @@ int pr_data_open(char *filename, char *reason, int direction, off_t size) {
     memset(&session.xfer.start_time, '\0', sizeof(session.xfer.start_time));
     gettimeofday(&session.xfer.start_time, NULL);
 
-    if (session.xfer.direction == PR_NETIO_IO_RD)
+    if (session.xfer.direction == PR_NETIO_IO_RD) {
       nstrm = session.d->instrm;
 
-    else
+    } else {
       nstrm = session.d->outstrm;
+    }
 
     session.sf_flags |= SF_XFER;
 
-    if (timeout_noxfer)
+    if (timeout_noxfer) {
       pr_timer_reset(PR_TIMER_NOXFER, ANY_MODULE);
+    }
 
     /* Allow aborts -- set the current NetIO stream to allow interrupted
      * syscalls, so our SIGURG handler can interrupt it
@@ -689,19 +619,26 @@ int pr_data_open(char *filename, char *reason, int direction, off_t size) {
     act.sa_flags |= SA_INTERRUPT;
 #endif
 
-    if (sigaction(SIGURG, &act, NULL) < 0)
+    if (sigaction(SIGURG, &act, NULL) < 0) {
       pr_log_pri(PR_LOG_WARNING,
         "warning: unable to set SIGURG signal handler: %s", strerror(errno));
+    }
 
 #ifdef HAVE_SIGINTERRUPT
     /* This is the BSD way of ensuring interruption.
      * Linux uses it too (??)
      */
-    if (siginterrupt(SIGURG, 1) < 0)
+    if (siginterrupt(SIGURG, 1) < 0) {
       pr_log_pri(PR_LOG_WARNING,
         "warning: unable to make SIGURG interrupt system calls: %s",
         strerror(errno));
+    }
 #endif
+
+    /* Reset all of the timing-related variables for data transfers. */
+    pr_gettimeofday_millis(&data_start_ms);
+    data_first_byte_read = FALSE;
+    data_first_byte_written = FALSE;
   }
 
   return res;
@@ -741,7 +678,7 @@ void pr_data_close(int quiet) {
  * send the OOB byte (which results in a broken pipe on our
  * end).  Thus, it's a race between the OOB data and the tcp close
  * finishing.  Either way, it's ok (client will see either "Broken pipe"
- * error or "Aborted").  cmd_abor in mod_xfer cleans up the session
+ * error or "Aborted").  xfer_abor() in mod_xfer cleans up the session
  * flags in any case.  session flags will end up have SF_POST_ABORT
  * set if the OOB byte won the race.
  */
@@ -769,6 +706,11 @@ void pr_data_abort(int err, int quiet) {
   int true_abort = XFER_ABORTED;
   nstrm = NULL;
 
+  pr_trace_msg(trace_channel, 9,
+    "aborting data transfer (errno = %s (%d), quiet = %s, true abort = %s)",
+    strerror(err), err, quiet ? "true" : "false",
+    true_abort ? "true" : "false");
+
   if (session.d) {
     if (true_abort == FALSE) {
       pr_inet_lingering_close(session.pool, session.d, timeout_linger);
@@ -943,7 +885,7 @@ void pr_data_abort(int err, int quiet) {
     }
 
     pr_log_pri(PR_LOG_NOTICE, "notice: user %s: aborting transfer: %s",
-      session.user, msg);
+      session.user ? session.user : "(unknown)", msg);
 
     /* If we are aborting, then a 426 response has already been sent,
      * and we don't want to add another to the error queue.
@@ -961,21 +903,13 @@ void pr_data_abort(int err, int quiet) {
 /* From response.c.  XXX Need to provide these symbols another way. */
 extern pr_response_t *resp_list, *resp_err_list;
 
-/* pr_data_xfer() actually transfers the data on the data connection.  ASCII
- * translation is performed if necessary.  `direction' is set when the data
- * connection was opened.
- *
- * We determine if the client buffer is read from or written to.  Returns 0 if
- * reading and data connection closes, or -1 if error (with errno set).
- */
-int pr_data_xfer(char *cl_buf, size_t cl_size) {
-  int len = 0;
-  int total = 0;
-  int res = 0;
+static void poll_ctrl(void) {
+  int res;
+
+  if (session.c == NULL) {
+    return;
+  }
 
-  /* Poll the control channel for any commands we should handle, like
-   * QUIT or ABOR.
-   */
   pr_trace_msg(trace_channel, 4, "polling for commands on control channel");
   pr_netio_set_poll_interval(session.c->instrm, 0);
   res = pr_netio_poll(session.c->instrm);
@@ -1008,14 +942,15 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
       /* Otherwise, EOF */
       pr_session_disconnect(NULL, PR_SESS_DISCONNECT_CLIENT_EOF, NULL);
 #else
-      return -1;
+      return;
 #endif /* PR_DEVEL_NO_DAEMON */
 
     } else if (cmd != NULL) {
       char *ch;
 
-      for (ch = cmd->argv[0]; *ch; ch++)
+      for (ch = cmd->argv[0]; *ch; ch++) {
         *ch = toupper(*ch);
+      }
 
       cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
 
@@ -1046,7 +981,7 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
 
         pr_trace_msg(trace_channel, 5,
           "client sent '%s' command during data transfer, denying",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
 
         resp_list = resp_err_list = NULL;
         resp_pool = pr_response_get_pool();
@@ -1054,7 +989,7 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
         pr_response_set_pool(cmd->pool);
 
         pr_response_add_err(R_450, _("%s: data transfer in progress"),
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
 
         pr_response_flush(&resp_err_list);
 
@@ -1072,7 +1007,7 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
 
         pr_trace_msg(trace_channel, 5,
           "client sent '%s' command during data transfer, ignoring",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
 
         resp_list = resp_err_list = NULL;
         resp_pool = pr_response_get_pool();
@@ -1080,7 +1015,7 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
         pr_response_set_pool(cmd->pool);
 
         pr_response_add(R_200, _("%s: data transfer in progress"),
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
 
         pr_response_flush(&resp_list);
 
@@ -1094,7 +1029,7 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
 
         pr_trace_msg(trace_channel, 5,
           "client sent '%s' command during data transfer, dispatching",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
 
         title_len = pr_proctitle_get(NULL, 0);
         if (title_len > 0) {
@@ -1125,9 +1060,34 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
       pr_response_send(R_500, _("Invalid command: try being more creative"));
     }
   }
+}
+
+/* pr_data_xfer() actually transfers the data on the data connection.  ASCII
+ * translation is performed if necessary.  `direction' is set when the data
+ * connection was opened.
+ *
+ * We determine if the client buffer is read from or written to.  Returns 0 if
+ * reading and data connection closes, or -1 if error (with errno set).
+ */
+int pr_data_xfer(char *cl_buf, size_t cl_size) {
+  int len = 0;
+  int total = 0;
+  int res = 0;
+  pool *tmp_pool = NULL;
+
+  if (cl_buf == NULL ||
+      cl_size == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Poll the control channel for any commands we should handle, like
+   * QUIT or ABOR.
+   */
+  poll_ctrl();
 
   /* If we don't have a data connection here (e.g. might have been closed
-   * by an ABOR, then return zero (no data transferred).
+   * by an ABOR), then return zero (no data transferred).
    */
   if (session.d == NULL) {
     int xerrno;
@@ -1152,11 +1112,16 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
   }
 
   if (session.xfer.direction == PR_NETIO_IO_RD) {
-    char *buf = session.xfer.buf;
-    pr_buffer_t *pbuf;
-    pool *tmp_pool;
+    char *buf;
+
+    buf = session.xfer.buf;
 
-    if (session.sf_flags & (SF_ASCII|SF_ASCII_OVERRIDE)) {
+    /* We use ASCII translation if:
+     *
+     * - SF_ASCII session flag is set, AND IGNORE_ASCII data opt NOT set
+     */
+    if (((session.sf_flags & SF_ASCII) &&
+        !(data_opts & PR_DATA_OPT_IGNORE_ASCII))) {
       int adjlen, buflen;
 
       do {
@@ -1183,28 +1148,26 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
             continue;
           }
 
+          destroy_pool(tmp_pool);
+          errno = xerrno;
           return -1;
         }
 
-        /* Before we process the data read from the client, generate an event
-         * for any listeners which may want to examine this data.  Use a
-         * temporary pool, so that we don't exhaust the transfer pool for
-         * long/large transfers (Bug#4277).
-         */
+        if (len > 0 &&
+            data_first_byte_read == FALSE) {
+          if (pr_trace_get_level(timing_channel)) {
+            unsigned long elapsed_ms;
+            uint64_t read_ms;
 
-        tmp_pool = make_sub_pool(session.xfer.p);
-        pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
-        pbuf->buf = buf;
-        pbuf->buflen = len;
-        pbuf->current = pbuf->buf;
-        pbuf->remaining = 0;
+            pr_gettimeofday_millis(&read_ms);
+            elapsed_ms = (unsigned long) (read_ms - data_start_ms);
 
-        pr_event_generate("core.data-read", pbuf);
+            pr_trace_msg(timing_channel, 7,
+              "Time for first data byte read: %lu ms", elapsed_ms);
+          }
 
-        /* The event listeners may have changed the data to write out. */
-        buf = pbuf->buf;
-        len = pbuf->buflen - pbuf->remaining;
-        destroy_pool(tmp_pool);
+          data_first_byte_read = TRUE;
+        }
 
         if (len > 0) {
           buflen += len;
@@ -1218,25 +1181,42 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
         if (len >= 0 &&
             buflen > 0) {
 
-          /* Perform translation:
+          /* Perform ASCII translation:
            *
-           * buflen is returned as the modified buffer length after
-           *        translation
-           * adjlen is returned as the number of characters unprocessed in
-           *        the buffer (to be dealt with later)
+           * buflen: is returned as the modified buffer length after
+           *         translation
+           * res:    is returned as the number of characters unprocessed in
+           *         the buffer (to be dealt with later)
            *
-           * We skip the call to xfrm_ascii_read() in one case:
+           * We skip the call to pr_ascii_ftp_from_crlf() in one case:
            * when we have one character in the buffer and have reached
-           * end of data, this is so that xfrm_ascii_read() won't sit
+           * end of data, this is so that pr_ascii_ftp_from_crlf() won't sit
            * forever waiting for the next character after a final '\r'.
            */
           if (len > 0 ||
               buflen > 1) {
-            xfrm_ascii_read(buf, &buflen, &adjlen);
+            size_t outlen = 0;
+
+            /* Use a temporary pool for the CRLF conversion, lest the
+             * session.xfer.p pool grow quite large while downloading a large
+             * file for ASCII conversion (Bug#4277).
+             */
+            tmp_pool = make_sub_pool(session.xfer.p);
+            pr_pool_tag(tmp_pool, "ASCII download");
+
+            res = pr_ascii_ftp_from_crlf(tmp_pool, buf, buflen, &buf, &outlen);
+            if (res < 0) {
+              pr_trace_msg(trace_channel, 3, "error reading ASCII data: %s",
+                strerror(errno));
+
+            } else {
+              adjlen += res;
+              buflen = (int) outlen;
+            }
           }
 
           /* Now copy everything we can into cl_buf */
-          if (buflen > cl_size) {
+          if ((size_t) buflen > cl_size) {
             /* Because we have to cut our buffer short, make sure this
              * is made up for later by increasing adjlen.
              */
@@ -1264,8 +1244,8 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
 	
         /* Restart if data was returned by pr_netio_read() (len > 0) but no
          * data was copied to the client buffer (buflen = 0).  This indicates
-         * that xfrm_ascii_read() needs more data in order to translate, so we
-         * need to call pr_netio_read() again.
+         * that pr_ascii_ftp_from_crlf() needs more data in order to
+         * translate, so we need to call pr_netio_read() again.
          */
       } while (len > 0 && buflen == 0);
 
@@ -1293,25 +1273,20 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
       }
 
       if (len > 0) {
-        /* Before we process the data read from the client, generate an event
-         * for any listeners which may want to examine this data.  Use a
-         * temporary pool, so that we don't exhaust the transfer pool for
-         * long/large transfers (Bug#4277).
-         */
+        if (data_first_byte_read == FALSE) {
+          if (pr_trace_get_level(timing_channel)) {
+            unsigned long elapsed_ms;
+            uint64_t read_ms;
 
-        tmp_pool = make_sub_pool(session.xfer.p);
-        pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
-        pbuf->buf = buf;
-        pbuf->buflen = len;
-        pbuf->current = pbuf->buf;
-        pbuf->remaining = 0;
+            pr_gettimeofday_millis(&read_ms);
+            elapsed_ms = (unsigned long) (read_ms - data_start_ms);
 
-        pr_event_generate("core.data-read", pbuf);
+            pr_trace_msg(timing_channel, 7,
+              "Time for first data byte read: %lu ms", elapsed_ms);
+          }
 
-        /* The event listeners may have changed the data to write out. */
-        buf = pbuf->buf;
-        len = pbuf->buflen - pbuf->remaining;
-        destroy_pool(tmp_pool);
+          data_first_byte_read = TRUE;
+        }
 
         /* Non-ASCII mode doesn't need to use session.xfer.buf */
         if (timeout_stalled) {
@@ -1340,13 +1315,39 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
       /* Fill up our internal buffer. */
       memcpy(session.xfer.buf, cl_buf, buflen);
 
-      if (session.sf_flags & (SF_ASCII|SF_ASCII_OVERRIDE)) {
+      /* We use ASCII translation if:
+       *
+       * - SF_ASCII_OVERRIDE session flag is set (e.g. for LIST/NLST)
+       * - SF_ASCII session flag is set, AND IGNORE_ASCII data opt NOT set
+       */
+      if ((session.sf_flags & SF_ASCII_OVERRIDE) ||
+          ((session.sf_flags & SF_ASCII) &&
+           !(data_opts & PR_DATA_OPT_IGNORE_ASCII))) {
+        char *out = NULL;
+        size_t outlen = 0;
+
+        /* Use a temporary pool for the CRLF conversion, lest the
+         * session.xfer.p pool grow quite large while downloading a large
+         * file for ASCII conversion (Bug#4277).
+         */
+        tmp_pool = make_sub_pool(session.xfer.p);
+        pr_pool_tag(tmp_pool, "ASCII upload");
+
         /* Scan the internal buffer, looking for LFs with no preceding CRs.
          * Add CRs (and expand the internal buffer) as necessary. xferbuflen
          * will be adjusted so that it contains the length of data in
          * the internal buffer, including any added CRs.
          */
-        xfrm_ascii_write(&session.xfer.buf, &xferbuflen, session.xfer.bufsize);
+        res = pr_ascii_ftp_to_crlf(tmp_pool, session.xfer.buf, xferbuflen,
+          &out, &outlen);
+        if (res < 0) {
+          pr_trace_msg(trace_channel, 1, "error writing ASCII data: %s",
+            strerror(errno));
+
+        } else {
+          session.xfer.buf = out;
+          session.xfer.buflen = xferbuflen = outlen;
+        }
       }
 
       bwrote = pr_netio_write(session.d->outstrm, session.xfer.buf, xferbuflen);
@@ -1366,10 +1367,27 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
           continue;
         }
 
+        destroy_pool(tmp_pool);
+        errno = xerrno;
         return -1;
       }
 
       if (bwrote > 0) {
+        if (data_first_byte_written == FALSE) {
+          if (pr_trace_get_level(timing_channel)) {
+            unsigned long elapsed_ms;
+            uint64_t write_ms;
+
+            pr_gettimeofday_millis(&write_ms);
+            elapsed_ms = (unsigned long) (write_ms - data_start_ms);
+
+            pr_trace_msg(timing_channel, 7,
+              "Time for first data byte written: %lu ms", elapsed_ms);
+          }
+
+          data_first_byte_written = TRUE;
+        }
+
         if (timeout_stalled) {
           pr_timer_reset(PR_TIMER_STALLED, ANY_MODULE);
         }
@@ -1384,8 +1402,9 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
   }
 
   if (total &&
-      timeout_idle)
+      timeout_idle) {
     pr_timer_reset(PR_TIMER_IDLE, ANY_MODULE);
+  }
 
   session.xfer.total_bytes += total;
   session.total_bytes += total;
@@ -1396,6 +1415,7 @@ int pr_data_xfer(char *cl_buf, size_t cl_size) {
     session.total_bytes_out += total;
   }
 
+  destroy_pool(tmp_pool);
   return (len < 0 ? -1 : len);
 }
 
@@ -1412,17 +1432,32 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
   int rc;
 #endif /* HAVE_AIX_SENDFILE */
 
-  if (session.xfer.direction == PR_NETIO_IO_RD)
+  if (offset == NULL ||
+      count == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (session.xfer.direction == PR_NETIO_IO_RD) {
+    errno = EPERM;
+    return -1;
+  }
+
+  if (session.d == NULL) {
+    errno = EPERM;
     return -1;
+  }
 
   flags = fcntl(PR_NETIO_FD(session.d->outstrm), F_GETFL);
-  if (flags == -1)
+  if (flags < 0) {
     return -1;
+  }
 
   /* Set fd to blocking-mode for sendfile() */
   if (flags & O_NONBLOCK) {
-    if (fcntl(PR_NETIO_FD(session.d->outstrm), F_SETFL, flags^O_NONBLOCK) == -1)
+    if (fcntl(PR_NETIO_FD(session.d->outstrm), F_SETFL, flags^O_NONBLOCK) < 0) {
       return -1;
+    }
   }
 
   for (;;) {
@@ -1445,24 +1480,26 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
      */
 
 #if defined(HAVE_LINUX_SENDFILE)
-    if (count > INT_MAX)
+    if (count > INT_MAX) {
       count = INT_MAX;
-
+    }
 #elif defined(HAVE_SOLARIS_SENDFILE)
 # if SIZEOF_SIZE_T == SIZEOF_INT
-    if (count > INT_MAX)
+    if (count > INT_MAX) {
       count = INT_MAX;
+    }
 # elif SIZEOF_SIZE_T == SIZEOF_LONG
-    if (count > LONG_MAX)
+    if (count > LONG_MAX) {
       count = LONG_MAX;
+    }
 # elif SIZEOF_SIZE_T == SIZEOF_LONG_LONG
-    if (count > LLONG_MAX)
+    if (count > LLONG_MAX) {
       count = LLONG_MAX;
+    }
 # endif
 #endif /* !HAVE_SOLARIS_SENDFILE */
 
     len = sendfile(PR_NETIO_FD(session.d->outstrm), retr_fd, offset, count);
-
     if (len != -1 &&
         len < count) {
       /* Under Linux semantics, this occurs when a signal has interrupted
@@ -1526,14 +1563,17 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
      */
 
 #if SIZEOF_SIZE_T == SIZEOF_INT
-    if (count > UINT_MAX)
+    if (count > UINT_MAX) {
       count = UINT_MAX;
+    }
 #elif SIZEOF_SIZE_T == SIZEOF_LONG
-    if (count > ULONG_MAX)
+    if (count > ULONG_MAX) {
       count = ULONG_MAX;
+    }
 #elif SIZEOF_SIZE_T == SIZEOF_LONG_LONG
-    if (count > ULLONG_MAX)
+    if (count > ULLONG_MAX) {
       count = ULLONG_MAX;
+    }
 #endif
 
     if (sendfile(retr_fd, PR_NETIO_FD(session.d->outstrm), *offset, count,
@@ -1549,10 +1589,10 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
      */
 
     res = sendfile(retr_fd, PR_NETIO_FD(session.d->outstrm), *offset, &orig_len,
-        NULL, 0);
+      NULL, 0);
     len = orig_len;
 
-    if (res == -1) {
+    if (res < 0) {
 #elif defined(HAVE_AIX_SENDFILE)
 
     memset(&parms, 0, sizeof(parms));
@@ -1561,10 +1601,10 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
     parms.file_offset = (uint64_t) *offset;
     parms.file_bytes = (int64_t) count;
 
-    rc  = send_file(&(PR_NETIO_FD(session.d->outstrm)), &(parms), (uint_t)0);
+    rc = send_file(&(PR_NETIO_FD(session.d->outstrm)), &(parms), (uint_t)0);
     len = (int) parms.bytes_sent;
 
-    if (rc == -1 || rc == 1) {
+    if (rc < -1 || rc == 1) {
 
 #endif /* HAVE_AIX_SENDFILE */
 
@@ -1623,7 +1663,7 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
       }
 
       error = errno;
-      fcntl(PR_NETIO_FD(session.d->outstrm), F_SETFL, flags);
+      (void) fcntl(PR_NETIO_FD(session.d->outstrm), F_SETFL, flags);
       errno = error;
 
       return -1;
@@ -1632,8 +1672,9 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
     break;
   }
 
-  if (flags & O_NONBLOCK)
-    fcntl(PR_NETIO_FD(session.d->outstrm), F_SETFL, flags);
+  if (flags & O_NONBLOCK) {
+    (void) fcntl(PR_NETIO_FD(session.d->outstrm), F_SETFL, flags);
+  }
 
   if (timeout_stalled) {
     pr_timer_reset(PR_TIMER_STALLED, ANY_MODULE);
@@ -1651,4 +1692,9 @@ pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
 
   return total;
 }
+#else
+pr_sendfile_t pr_data_sendfile(int retr_fd, off_t *offset, off_t count) {
+  errno = ENOSYS;
+  return -1;
+}
 #endif /* HAVE_SENDFILE */
diff --git a/src/dirtree.c b/src/dirtree.c
index bda3f11..4f6b449 100644
--- a/src/dirtree.c
+++ b/src/dirtree.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,12 +24,9 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Read configuration file(s), and manage server/configuration structures.
- * $Id: dirtree.c,v 1.292 2013-10-13 18:06:57 castaglia Exp $
- */
+/* Read configuration file(s), and manage server/configuration structures. */
 
 #include "conf.h"
-#include "privs.h"
 
 #ifdef HAVE_ARPA_INET_H
 # include <arpa/inet.h>
@@ -40,7 +37,7 @@ server_rec *main_server = NULL;
 int tcpBackLog = PR_TUNABLE_DEFAULT_BACKLOG;
 int SocketBindTight = FALSE;
 char ServerType = SERVER_STANDALONE;
-int ServerMaxInstances = 0;
+unsigned long ServerMaxInstances = 0UL;
 int ServerUseReverseDNS = TRUE;
 
 /* Default TCP send/receive buffer sizes. */
@@ -48,16 +45,6 @@ static int tcp_rcvbufsz = 0;
 static int tcp_sndbufsz = 0;
 static int xfer_bufsz = 0;
 
-/* From src/pool.c */
-extern pool *global_config_pool;
-
-/* Used by find_config_* */
-static xaset_t *find_config_top = NULL;
-
-static void config_dumpf(const char *, ...);
-static void merge_down(xaset_t *, int);
-
-static config_rec *_last_param_ptr = NULL;
 static unsigned char _kludge_disable_umask = 0;
 
 /* We have two different lists for Defines.  The 'perm' pool/list are
@@ -70,10 +57,6 @@ static array_header *defines_list = NULL;
 static pool *defines_perm_pool = NULL;
 static array_header *defines_perm_list = NULL;
 
-static pool *config_tab_pool = NULL;
-static pr_table_t *config_tab = NULL;
-static unsigned int config_id = 0;
-
 static int allow_dyn_config(const char *path) {
   config_rec *c = NULL;
   unsigned int ctxt_precedence = 0;
@@ -96,10 +79,14 @@ static int allow_dyn_config(const char *path) {
     c = find_config_next(c, c->next, CONF_PARAM, "AllowOverride", FALSE);
   }
 
-  /* Print out some nice debugging information. */
-  if (found_config) {
-    pr_log_debug(DEBUG8, "AllowOverride for path '%s' %s .ftpaccess files",
-      path, allow ? "allows" : "denies");
+  /* Print out some nice debugging information, but only if we have a real
+   * path.
+   */
+  if (found_config &&
+      *path) {
+    pr_trace_msg("config", 8,
+      "AllowOverride for path '%s' %s .ftpaccess files", path,
+      allow ? "allows" : "denies");
   }
 
   return allow;
@@ -157,125 +144,6 @@ xaset_t *get_dir_ctxt(pool *p, char *dir_path) {
     main_server->conf;
 }
 
-/* Substitute any appearance of the %u variable in the given string with
- * the value.
- */
-char *path_subst_uservar(pool *path_pool, char **path) {
-  char *new_path = NULL, *substr = NULL, *substr_path = NULL;
-
-  /* Sanity check. */
-  if (path_pool == NULL ||
-      path == NULL ||
-      !*path) {
-    errno = EINVAL;
-    return NULL;
-  }
-
-  /* If no %u string present, do nothing. */
-  if (strstr(*path, "%u") == NULL)
-    return *path;
-
-  /* First, deal with occurrences of "%u[index]" strings.  Note that
-   * with this syntax, the '[' and ']' characters become invalid in paths,
-   * but only if that '[' appears after a "%u" string -- certainly not
-   * a common phenomenon (I hope).  This means that in the future, an escape
-   * mechanism may be needed in this function.  Caveat emptor.
-   */
-
-  substr_path = *path;
-  substr = substr_path ? strstr(substr_path, "%u[") : NULL;
-  while (substr) {
-    int i = 0;
-    char *substr_end = NULL, *substr_dup = NULL, *endp = NULL;
-    char ref_char[2] = {'\0', '\0'};
-
-    pr_signals_handle();
-
-    /* Now, find the closing ']'. If not found, it is a syntax error;
-     * continue on without processing this occurrence.
-     */
-    substr_end = strchr(substr, ']');
-    if (substr_end == NULL) {
-      /* Just end here. */
-      break;
-    }
-
-    /* Make a copy of the entire substring. */
-    substr_dup = pstrdup(path_pool, substr);
-
-    /* The substr_end variable (used as an index) should work here, too
-     * (trying to obtain the entire substring).
-     */
-    substr_dup[substr_end - substr + 1] = '\0';
-
-    /* Advance the substring pointer by three characters, so that it is
-     * pointing at the character after the '['.
-     */
-    substr += 3;
-
-    /* If the closing ']' is the next character after the opening '[', it
-     * is a syntax error.
-     */
-    if (substr_end == substr) {
-
-      /* Do not forget to advance the substring search path pointer. */
-      substr_path = substr;
-
-      continue;
-    }
-
-    /* Temporarily set the ']' to '\0', to make it easy for the string
-     * scanning below.
-     */
-    *substr_end = '\0';
-
-    /* Scan the index string into a number, watching for bad strings. */
-    i = strtol(substr, &endp, 10);
-
-    if (endp && *endp) {
-      *substr_end = ']';
-      substr_path = substr;
-      continue;
-    }
-
-    /* Make sure that index is within bounds. */
-    if (i < 0 || i > strlen(session.user) - 1) {
-
-      /* Put the closing ']' back. */
-      *substr_end = ']';
-
-      /* Syntax error. Advance the substring search path pointer, and move
-       * on.
-       */
-      substr_path = substr;
-
-      continue;
-    }
-
-    ref_char[0] = session.user[i];
-
-    /* Put the closing ']' back. */
-    *substr_end = ']';
-
-    /* Now, to substitute the whole "%u[index]" substring with the
-     * referenced character/string.
-     */
-    substr_path = sreplace(path_pool, substr_path, substr_dup, ref_char, NULL);
-    substr = substr_path ? strstr(substr_path, "%u[") : NULL;
-  }
-
-  /* Check for any bare "%u", and handle those if present. */
-  if (substr_path &&
-      strstr(substr_path, "%u") != NULL) {
-    new_path = sreplace(path_pool, substr_path, "%u", session.user, NULL);
-
-  } else {
-    new_path = substr_path;
-  }
-
-  return new_path;
-}
-
 /* Check for configured HideFiles directives, and check the given path (full
  * _path_, not just filename) against those regexes if configured.
  *
@@ -554,116 +422,6 @@ void kludge_enable_umask(void) {
   _kludge_disable_umask = FALSE;
 }
 
-/* Adds a config_rec to the specified set */
-config_rec *pr_config_add_set(xaset_t **set, const char *name, int flags) {
-  pool *conf_pool = NULL, *set_pool = NULL;
-  config_rec *c, *parent = NULL;
-
-  if (!*set) {
-
-    /* Allocate a subpool from permanent_pool for the set. */
-    set_pool = make_sub_pool(permanent_pool);
-    pr_pool_tag(set_pool, "config set pool");
-
-    *set = xaset_create(set_pool, NULL);
-    (*set)->pool = set_pool;
-
-    /* Now, make a subpool for the config_rec to be allocated.  The default
-     * pool size (PR_TUNABLE_NEW_POOL_SIZE, 512 by default) is a bit large
-     * for config_rec pools; use a smaller size.
-     */
-    conf_pool = pr_pool_create_sz(set_pool, 128);
-
-  } else {
-
-    /* Find the parent set for the config_rec to be allocated. */
-    if ((*set)->xas_list)
-      parent = ((config_rec *) ((*set)->xas_list))->parent;
-
-    /* Now, make a subpool for the config_rec to be allocated.  The default
-     * pool size (PR_TUNABLE_NEW_POOL_SIZE, 512 by default) is a bit large
-     * for config_rec pools; use a smaller size.  Allocate the subpool
-     * from the parent's pool.
-     */
-    conf_pool = pr_pool_create_sz((*set)->pool, 128);
-  }
-
-  pr_pool_tag(conf_pool, "config_rec pool");
-
-  c = (config_rec *) pcalloc(conf_pool, sizeof(config_rec));
-
-  c->pool = conf_pool;
-  c->set = *set;
-  c->parent = parent;
-
-  if (name) {
-    c->name = pstrdup(conf_pool, name);
-    c->config_id = pr_config_set_id(c->name);
-  }
-
-  if (flags & PR_CONFIG_FL_INSERT_HEAD) {
-    xaset_insert(*set, (xasetmember_t *) c);
-    
-  } else {
-    xaset_insert_end(*set, (xasetmember_t *) c);
-  }
-
-  return c;
-}
-
-config_rec *add_config_set(xaset_t **set, const char *name) {
-  return pr_config_add_set(set, name, 0);
-}
-
-/* Adds a config_rec to the given server.  If no server is specified, the
- * config_rec is added to the current "level".
- */
-config_rec *pr_config_add(server_rec *s, const char *name, int flags) {
-  config_rec *parent = NULL, *c = NULL;
-  pool *p = NULL;
-  xaset_t **set = NULL;
-
-  if (s == NULL) {
-    s = pr_parser_server_ctxt_get();
-  }
-
-  c = pr_parser_config_ctxt_get();
-
-  if (c) {
-    parent = c;
-    p = c->pool;
-    set = &c->subset;
-
-  } else {
-    parent = NULL;
-
-    if (s->conf == NULL ||
-        s->conf->xas_list == NULL) {
-
-      p = make_sub_pool(s->pool);
-      pr_pool_tag(p, "pr_config_add() subpool");
-
-    } else {
-      p = ((config_rec *) s->conf->xas_list)->pool;
-    }
-
-    set = &s->conf;
-  }
-
-  if (!*set) {
-    *set = xaset_create(p, NULL);
-  }
-
-  c = pr_config_add_set(set, name, flags);
-  c->parent = parent;
-
-  return c;
-}
-
-config_rec *add_config(server_rec *s, const char *name) {
-  return pr_config_add(s, name, 0);
-}
-
 /* Per-directory configuration */
 
 static size_t _strmatch(register char *s1, register char *s2) {
@@ -807,16 +565,18 @@ config_rec *dir_match_path(pool *p, char *path) {
     tmplen = strlen(tmp);
   }
 
-  if (*(tmp + tmplen - 1) == '/' && tmplen > 1)
+  if (*(tmp + tmplen - 1) == '/' && tmplen > 1) {
     *(tmp + tmplen - 1) = '\0';
+  }
 
   if (session.anon_config) {
     res = recur_match_path(p, session.anon_config->subset, tmp);
 
     if (!res) {
       if (session.chroot_path &&
-          !strncmp(session.chroot_path, tmp, strlen(session.chroot_path)))
+          !strncmp(session.chroot_path, tmp, strlen(session.chroot_path))) {
         return NULL;
+      }
     }
   }
 
@@ -878,13 +638,17 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
         }
 
         if (file_uid == hide_uid) {
-          if (!inverted)
+          if (!inverted) {
+            pr_trace_msg("hiding", 8,
+              "hiding file '%s' because of HideUser %s", path, hide_user);
             res = FALSE;
-
+          }
           break;
 
         } else {
           if (inverted) {
+            pr_trace_msg("hiding", 8,
+              "hiding file '%s' because of HideUser !%s", path, hide_user);
             res = FALSE;
             break;
           }
@@ -922,7 +686,7 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
                 "HideGroup '%s' is not a known/valid group, ignoring",
                 hide_group);
 
-              c = find_config_next(c, c->next, CONF_PARAM, "HideUser", FALSE);
+              c = find_config_next(c, c->next, CONF_PARAM, "HideGroup", FALSE);
               continue;
             }
 
@@ -931,13 +695,19 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
 
           if (hide_gid != (gid_t) -1) {
             if (file_gid == hide_gid) {
-              if (!inverted)
+              if (!inverted) {
+                pr_trace_msg("hiding", 8,
+                  "hiding file '%s' because of HideGroup %s", path, hide_group);
                 res = FALSE;
+              }
 
               break;
 
             } else {
               if (inverted) {
+                pr_trace_msg("hiding", 8,
+                  "hiding file '%s' because of HideGroup !%s", path,
+                  hide_group);
                 res = FALSE;
                 break;
               }
@@ -949,8 +719,11 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
 
             /* First check to see if the file GID matches the session GID. */
             if (file_gid == session.gid) {
-              if (!inverted)
+              if (!inverted) {
+                pr_trace_msg("hiding", 8,
+                  "hiding file '%s' because of HideGroup %s", path, hide_group);
                 res = FALSE;
+              }
 
               break;
             }
@@ -958,14 +731,20 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
             /* Next, scan the list of supplemental groups for this user. */
             for (i = 0; i < session.gids->nelts; i++) {
               if (file_gid == group_ids[i]) {
-                if (!inverted)
+                if (!inverted) {
+                  pr_trace_msg("hiding", 8,
+                    "hiding file '%s' because of HideGroup %s", path, 
+                    hide_group);
                   res = FALSE;
+                }
 
                 break;
               }
             }
 
             if (inverted) {
+              pr_trace_msg("hiding", 8,
+                "hiding file '%s' because of HideGroup !%s", path, hide_group);
               res = FALSE;
               break;
             }
@@ -992,6 +771,14 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
              */
             res = pr_fsio_access(path, X_OK, session.uid, session.gid,
               session.gids) == 0 ? TRUE : FALSE;
+            if (res == FALSE) {
+              int xerrno = errno;
+
+              pr_trace_msg("hiding", 8,
+                "hiding directory '%s' because of HideNoAccess (errno = %s)",
+                path, strerror(xerrno));
+              errno = xerrno;
+            }
 
           } else {
             /* Check to see if the mode of this file allows the current
@@ -999,6 +786,14 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
              */
             res = pr_fsio_access(path, R_OK, session.uid, session.gid,
               session.gids) == 0 ? TRUE : FALSE;
+            if (res == FALSE) {
+              int xerrno = errno;
+
+              pr_trace_msg("hiding", 8,
+                "hiding file '%s' because of HideNoAccess (errno = %s)", path,
+                strerror(xerrno));
+              errno = xerrno;
+            }
           }
         }
       }
@@ -1015,6 +810,9 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
 
       } else if (deny_all &&
                  *deny_all == TRUE) {
+        pr_trace_msg("hiding", 8,
+          "hiding file '%s' because of DenyAll limit for command (errno = %s)",
+          path, strerror(EACCES));
         res = FALSE;
         errno = EACCES;
       }
@@ -1028,8 +826,16 @@ static int dir_check_op(pool *p, xaset_t *set, int op, const char *path,
 
 static int check_user_access(xaset_t *set, const char *name) {
   int res = 0;
-  config_rec *c = find_config(set, CONF_PARAM, name, FALSE);
+  config_rec *c;
+
+  /* If no user has been authenticated yet for this session, short-circuit the
+   * check.
+   */
+  if (session.user == NULL) {
+    return 0;
+  }
 
+  c = find_config(set, CONF_PARAM, name, FALSE);
   while (c) {
     pr_signals_handle();
 
@@ -1047,15 +853,15 @@ static int check_user_access(xaset_t *set, const char *name) {
 
     if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_OR) {
       res = pr_expr_eval_user_or((char **) &c->argv[1]);
-
-      if (res == TRUE)
+      if (res == TRUE) {
         break;
+      }
 
     } else if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_AND) {
       res = pr_expr_eval_user_and((char **) &c->argv[1]);
-
-      if (res == TRUE)
+      if (res == TRUE) {
         break;
+      }
     }
 
     c = find_config_next(c, c->next, CONF_PARAM, name, FALSE);
@@ -1066,8 +872,16 @@ static int check_user_access(xaset_t *set, const char *name) {
 
 static int check_group_access(xaset_t *set, const char *name) {
   int res = 0;
-  config_rec *c = find_config(set, CONF_PARAM, name, FALSE);
+  config_rec *c;
+
+  /* If no groups has been authenticated yet for this session, short-circuit the
+   * check.
+   */
+  if (session.group == NULL) {
+    return 0;
+  }
 
+  c = find_config(set, CONF_PARAM, name, FALSE);
   while (c) {
 #ifdef PR_USE_REGEX
     if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_REGEX) {
@@ -1081,12 +895,13 @@ static int check_group_access(xaset_t *set, const char *name) {
       } else if (session.groups) {
         register int i = 0;
 
-        for (i = session.groups->nelts-1; i >= 0; i--)
+        for (i = session.groups->nelts-1; i >= 0; i--) {
           if (pr_regexp_exec(pre, *(((char **) session.groups->elts) + i), 0,
               NULL, 0, 0, 0) == 0) {
             res = TRUE;
             break;
           }
+        }
       }
 
     } else
@@ -1094,15 +909,15 @@ static int check_group_access(xaset_t *set, const char *name) {
 
     if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_OR) {
       res = pr_expr_eval_group_or((char **) &c->argv[1]);
-
-      if (res == TRUE)
+      if (res == TRUE) {
         break;
+      }
 
     } else if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_AND) {
       res = pr_expr_eval_group_and((char **) &c->argv[1]);
-
-      if (res == TRUE)
+      if (res == TRUE) {
         break;
+      }
     }
 
     c = find_config_next(c, c->next, CONF_PARAM, name, FALSE);
@@ -1140,15 +955,15 @@ static int check_class_access(xaset_t *set, const char *name) {
 
     if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_OR) {
       res = pr_expr_eval_class_or((char **) &c->argv[1]);
-
-      if (res == TRUE)
+      if (res == TRUE) {
         break;
+      }
 
     } else if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_AND) {
       res = pr_expr_eval_class_and((char **) &c->argv[1]);
-
-      if (res == TRUE)
+      if (res == TRUE) {
         break;
+      }
     }
 
     c = find_config_next(c, c->next, CONF_PARAM, name, FALSE);
@@ -1162,8 +977,9 @@ static int check_filter_access(xaset_t *set, const char *name, cmd_rec *cmd) {
   int res = 0;
   config_rec *c;
 
-  if (!cmd)
-    return res;
+  if (cmd == NULL) {
+    return 0;
+  }
 
   c = find_config(set, CONF_PARAM, name, FALSE);
   while (c) {
@@ -1173,12 +989,13 @@ static int check_filter_access(xaset_t *set, const char *name, cmd_rec *cmd) {
     pr_signals_handle();
 
     pr_trace_msg("filter", 8,
-      "comparing %s argument '%s' against %s pattern '%s'", cmd->argv[0],
-      cmd->arg, name, pr_regexp_get_pattern(pre));
+      "comparing %s argument '%s' against %s pattern '%s'",
+      (char *) cmd->argv[0], cmd->arg, name, pr_regexp_get_pattern(pre));
     matched = pr_regexp_exec(pre, cmd->arg, 0, NULL, 0, 0, 0);
     pr_trace_msg("filter", 8,
       "comparing %s argument '%s' against %s pattern '%s' returned %d",
-      cmd->argv[0], cmd->arg, name, pr_regexp_get_pattern(pre), matched);
+      (char *) cmd->argv[0], cmd->arg, name, pr_regexp_get_pattern(pre),
+      matched);
 
     if (matched == 0) {
       res = TRUE;
@@ -1189,8 +1006,8 @@ static int check_filter_access(xaset_t *set, const char *name, cmd_rec *cmd) {
   }
 
   pr_trace_msg("filter", 8,
-    "comparing %s argument '%s' against %s patterns returned %d", cmd->argv[0],
-    cmd->arg, name, res);
+    "comparing %s argument '%s' against %s patterns returned %d",
+    (char *) cmd->argv[0], cmd->arg, name, res);
   return res;
 #else
   return 0;
@@ -1490,6 +1307,8 @@ int login_check_limits(xaset_t *set, int recurse, int and, int *found) {
           switch (check_limit(c, NULL)) {
             case 1:
               res = TRUE;
+              (*found)++;
+              break;
 
 	    case -1:
             case -2:
@@ -1516,8 +1335,9 @@ int login_check_limits(xaset_t *set, int recurse, int and, int *found) {
          int rres;
 
          rres = login_check_limits(c->subset, recurse, and, &rfound);
-         if (rfound)
+         if (rfound) {
            res = (res || rres);
+         }
 
          (*found) += rfound;
          if (res)
@@ -1853,7 +1673,7 @@ void build_dyn_config(pool *p, const char *_path, struct stat *stp,
 
       if (res == 0) {
         d->config_type = CONF_DIR;
-        merge_down(*set, TRUE);
+        pr_config_merge_down(*set, TRUE);
 
         pr_trace_msg("ftpaccess", 3, "fixing up directory configs");
         fixup_dirs(main_server, CF_SILENT);
@@ -1873,7 +1693,7 @@ void build_dyn_config(pool *p, const char *_path, struct stat *stp,
         d &&
         set) {
       pr_trace_msg("ftpaccess", 6, "adding config for '%s'", ftpaccess_name);
-      merge_down(*set, FALSE);
+      pr_config_merge_down(*set, FALSE);
     }
 
     if (!recurse)
@@ -1932,7 +1752,7 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
   int res = 1, isfile;
   int op_hidden = FALSE, regex_hidden = FALSE;
 
-  if (!path) {
+  if (path == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -1942,17 +1762,21 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
 
   fullpath = (char *) path;
 
-  if (session.chroot_path)
+  if (session.chroot_path) {
     fullpath = pdircat(p, session.chroot_path, fullpath, NULL);
+  }
 
-  pr_log_debug(DEBUG5, "in dir_check_full(): path = '%s', fullpath = '%s'.",
-            path, fullpath);
+  if (*path) {
+    /* Only log this debug line if we are dealing with a real path. */
+    pr_log_debug(DEBUG5, "in dir_check_full(): path = '%s', fullpath = '%s'",
+      path, fullpath);
+  }
 
   /* Check and build all appropriate dynamic configuration entries */
-  pr_fs_clear_cache();
   isfile = pr_fsio_stat(path, &st);
-  if (isfile == -1)
+  if (isfile < 0) {
     memset(&st, '\0', sizeof(st));
+  }
 
   build_dyn_config(p, path, &st, TRUE);
 
@@ -1968,8 +1792,9 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
       session.dir_config->name, fullpath);
   }
 
-  if (!c && session.anon_config)
+  if (!c && session.anon_config) {
     c = session.anon_config;
+  }
 
   /* Make sure this cmd_rec has a cmd_id. */
   if (cmd->cmd_id == 0) {
@@ -1981,7 +1806,14 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
     if (S_ISDIR(st.st_mode) ||
         pr_cmd_cmp(cmd, PR_CMD_MKD_ID) == 0 ||
         pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0) {
-      mode_t *dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
+      mode_t *dir_umask = NULL;
+
+      dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
+      if (dir_umask) {
+        pr_trace_msg("directory", 2, "found DirUmask %04o for directory '%s'",
+          *dir_umask, path);
+      }
+
       _umask = dir_umask ? *dir_umask : (mode_t) -1;
     }
 
@@ -2001,8 +1833,9 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
     struct passwd *pw;
 
     pw = pr_auth_getpwnam(p, owner);
-    if (pw != NULL)
+    if (pw != NULL) {
       session.fsuid = pw->pw_uid;
+    }
   }
 
   owner = get_param_ptr(CURRENT_CONF, "GroupOwner", FALSE);
@@ -2041,8 +1874,9 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
     /* If specifically allowed, res will be > 1 and we don't want to
      * check the command group limit.
      */
-    if (res == 1 && group)
+    if (res == 1 && group) {
       res = dir_check_limits(cmd, c, group, op_hidden || regex_hidden);
+    }
 
     /* If still == 1, no explicit allow so check lowest priority "ALL" group.
      * Note that certain commands are deliberately excluded from the
@@ -2060,15 +1894,17 @@ int dir_check_full(pool *pp, cmd_rec *cmd, const char *group, const char *path,
   }
 
   if (res &&
-      _umask != (mode_t) -1)
+      _umask != (mode_t) -1) {
     pr_log_debug(DEBUG5,
       "in dir_check_full(): setting umask to %04o (was %04o)",
         (unsigned int) _umask, (unsigned int) umask(_umask));
+  }
 
   destroy_pool(p);
 
-  if (hidden)
+  if (hidden) {
     *hidden = op_hidden || regex_hidden;
+  }
 
   return res;
 }
@@ -2088,7 +1924,7 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
   int res = 1, isfile;
   int op_hidden = FALSE, regex_hidden = FALSE;
 
-  if (!path) {
+  if (path == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -2098,8 +1934,9 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
 
   fullpath = (char *) path;
 
-  if (session.chroot_path)
+  if (session.chroot_path) {
     fullpath = pdircat(p, session.chroot_path, fullpath, NULL);
+  }
 
   c = (session.dir_config ? session.dir_config :
         (session.anon_config ? session.anon_config : NULL));
@@ -2110,10 +1947,10 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
   }
 
   /* Check and build all appropriate dynamic configuration entries */
-  pr_fs_clear_cache();
   isfile = pr_fsio_stat(path, &st);
-  if (isfile == -1)
+  if (isfile < 0) {
     memset(&st, 0, sizeof(st));
+  }
 
   build_dyn_config(p, path, &st, FALSE);
 
@@ -2129,8 +1966,9 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
       session.dir_config->name, fullpath);
   }
 
-  if (!c && session.anon_config)
+  if (!c && session.anon_config) {
     c = session.anon_config;
+  }
 
   /* Make sure this cmd_rec has a cmd_id. */
   if (cmd->cmd_id == 0) {
@@ -2142,7 +1980,14 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
     if (S_ISDIR(st.st_mode) ||
         pr_cmd_cmp(cmd, PR_CMD_MKD_ID) == 0 ||
         pr_cmd_cmp(cmd, PR_CMD_XMKD_ID) == 0) {
-      mode_t *dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
+      mode_t *dir_umask = NULL;
+
+      dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
+      if (dir_umask) {
+        pr_trace_msg("directory", 2, "found DirUmask %04o for directory '%s'",
+          *dir_umask, path);
+      }
+
       _umask = dir_umask ? *dir_umask : (mode_t) -1;
     }
 
@@ -2162,8 +2007,9 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
     struct passwd *pw;
 
     pw = pr_auth_getpwnam(p, owner);
-    if (pw != NULL)
+    if (pw != NULL) {
       session.fsuid = pw->pw_uid;
+    }
   }
 
   owner = get_param_ptr(CURRENT_CONF, "GroupOwner", FALSE);
@@ -2200,8 +2046,9 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
     /* If specifically allowed, res will be > 1 and we don't want to
      * check the command group limit.
      */
-    if (res == 1 && group)
+    if (res == 1 && group) {
       res = dir_check_limits(cmd, c, group, op_hidden || regex_hidden);
+    }
 
     /* If still == 1, no explicit allow so check lowest priority "ALL" group.
      * Note that certain commands are deliberately excluded from the
@@ -2219,14 +2066,16 @@ int dir_check(pool *pp, cmd_rec *cmd, const char *group, const char *path,
   }
 
   if (res &&
-      _umask != (mode_t) -1)
+      _umask != (mode_t) -1) {
     pr_log_debug(DEBUG5, "in dir_check(): setting umask to %04o (was %04o)",
         (unsigned int) _umask, (unsigned int) umask(_umask));
+  }
 
   destroy_pool(p);
 
-  if (hidden)
+  if (hidden) {
     *hidden = op_hidden || regex_hidden;
+  }
 
   return res;
 }
@@ -2410,46 +2259,6 @@ static void reorder_dirs(xaset_t *set, int flags) {
   }
 }
 
-static void config_dumpf(const char *fmt, ...) {
-  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
-  va_list msg;
-
-  va_start(msg, fmt);
-  vsnprintf(buf, sizeof(buf), fmt, msg);
-  va_end(msg);
-
-  buf[sizeof(buf)-1] = '\0';
-
-  pr_log_debug(DEBUG5, "%s", buf);
-}
-
-void pr_config_dump(void (*dumpf)(const char *, ...), xaset_t *s,
-    char *indent) {
-  config_rec *c = NULL;
-
-  if (s == NULL) {
-    return;
-  }
-
-  if (indent == NULL) {
-    indent = "";
-  }
-
-  for (c = (config_rec *) s->xas_list; c; c = c->next) {
-    pr_signals_handle();
-
-    /* Don't display directives whose name starts with an underscore. */
-    if (c->name != NULL &&
-        *(c->name) != '_') {
-      dumpf("%s%s", indent, c->name);
-    }
-
-    if (c->subset) {
-      pr_config_dump(dumpf, c->subset, pstrcat(c->pool, indent, " ", NULL));
-    }
-  }
-}
-
 #ifdef PR_USE_DEVEL
 void pr_dirs_dump(void (*dumpf)(const char *, ...), xaset_t *s, char *indent) {
   config_rec *c;
@@ -2480,298 +2289,60 @@ void pr_dirs_dump(void (*dumpf)(const char *, ...), xaset_t *s, char *indent) {
 }
 #endif /* PR_USE_DEVEL */
 
-static const char *config_type_str(int config_type) {
-  const char *type = "(unknown)";
+/* Iterate through <Directory> blocks inside of anonymous and
+ * resolve each one.
+ */
+void resolve_anonymous_dirs(xaset_t *clist) {
+  config_rec *c;
+  char *realdir;
 
-  switch (config_type) {
-    case CONF_ROOT:
-      type = "CONF_ROOT";
-      break;
+  if (!clist) {
+    return;
+  }
 
-    case CONF_DIR:
-      type = "CONF_DIR";
-      break;
+  for (c = (config_rec *) clist->xas_list; c; c = c->next) {
+    if (c->config_type == CONF_DIR) {
+      if (c->argv[1]) {
+        realdir = dir_best_path(c->pool, c->argv[1]);
+        if (realdir) {
+          c->argv[1] = realdir;
 
-    case CONF_ANON:
-      type = "CONF_ANON";
-      break;
+        } else {
+          realdir = dir_canonical_path(c->pool, c->argv[1]);
+          if (realdir) {
+            c->argv[1] = realdir;
+          }
+        }
+      }
 
-    case CONF_LIMIT:
-      type = "CONF_LIMIT";
-      break;
+      if (c->subset) {
+        resolve_anonymous_dirs(c->subset);
+      }
+    }
+  }
+}
 
-    case CONF_VIRTUAL:
-      type = "CONF_VIRTUAL";
-      break;
+/* Iterate through directory configuration items and resolve ~ references. */
+void resolve_deferred_dirs(server_rec *s) {
+  config_rec *c;
 
-    case CONF_DYNDIR:
-      type = "CONF_DYNDIR";
-      break;
+  if (s == NULL ||
+      s->conf == NULL) {
+    return;
+  }
 
-    case CONF_GLOBAL:
-      type = "CONF_GLOBAL";
-      break;
-
-    case CONF_CLASS:
-      type = "CONF_CLASS";
-      break;
-
-    case CONF_NAMED:
-      type = "CONF_NAMED";
-      break;
-
-    case CONF_USERDATA:
-      type = "CONF_USERDATA";
-      break;
-
-    case CONF_PARAM:
-      type = "CONF_PARAM";
-      break;
-  };
-
-  return type;
-}
-
-/* Compare two different config_recs to see if they are the same.  Note
- * that "same" here has to be very specific.
- *
- * Returns 0 if the two config_recs are the same, and 1 if they differ, and
- * -1 if there was an error.
- */
-static int config_cmp(const config_rec *a, const char *a_name,
-    const config_rec *b, const char *b_name) {
-  const char *trace_channel = "config";
-
-  if (a == NULL ||
-      b == NULL) {
-    errno = EINVAL;
-    return -1;
-  }
-
-  if (a->config_type != b->config_type) {
-    pr_trace_msg(trace_channel, 18,
-      "configs '%s' and '%s' have mismatched config_type (%s != %s)",
-      a_name, b_name, config_type_str(a->config_type),
-      config_type_str(b->config_type));
-    return 1;
-  }
-
-  if (a->flags != b->flags) {
-    pr_trace_msg(trace_channel, 18,
-      "configs '%s' and '%s' have mismatched flags (%ld != %ld)",
-      a_name, b_name, a->flags, b->flags);
-    return 1;
-  }
-
-  if (a->argc != b->argc) {
-    pr_trace_msg(trace_channel, 18,
-      "configs '%s' and '%s' have mismatched argc (%d != %d)",
-      a_name, b_name, a->argc, b->argc);
-    return 1;
-  }
-
-  if (a->argc > 0) {
-    register unsigned int i;
-
-    for (i = 0; i < a->argc; i++) {
-      if (a->argv[i] != b->argv[i]) {
-        pr_trace_msg(trace_channel, 18,
-          "configs '%s' and '%s' have mismatched argv[%u] (%p != %p)",
-          a_name, b_name, i, a->argv[i], b->argv[i]);
-        return 1;
-      }
-    }
-  }
-
-  if (a->config_id != b->config_id) {
-    pr_trace_msg(trace_channel, 18,
-      "configs '%s' and '%s' have mismatched config_id (%d != %d)",
-      a_name, b_name, a->config_id, b->config_id);
-    return 1;
-  }
-
-  /* Save the string comparison for last, to try to save some CPU. */
-  if (strcmp(a->name, b->name) != 0) {
-    pr_trace_msg(trace_channel, 18,
-      "configs '%s' and '%s' have mismatched name ('%s' != '%s')",
-      a_name, b_name, a->name, b->name);
-    return 1;
-  }
-
-  return 0;
-}
-
-static config_rec *copy_config_from(const config_rec *src, config_rec *dst) {
-  config_rec *c;
-  int cargc;
-  void **cargv, **sargv;
-
-  if (src == NULL ||
-      dst == NULL) {
-    return NULL;
-  }
-
-  /* If the destination parent config_rec doesn't already have a subset
-   * container, allocate one.
-   */
-  if (dst->subset == NULL) {
-    dst->subset = xaset_create(dst->pool, NULL);
-  }
-
-  c = pr_config_add_set(&dst->subset, src->name, 0);
-  c->config_type = src->config_type;
-  c->flags = src->flags;
-  c->config_id = src->config_id;
-
-  c->argc = src->argc;
-  c->argv = pcalloc(c->pool, (src->argc + 1) * sizeof(void *));
-
-  cargc = c->argc;
-  cargv = c->argv;
-  sargv = src->argv;
-
-  while (cargc--) {
-    pr_signals_handle();
-    *cargv++ = *sargv++;
-  }
-
-  *cargv = NULL; 
-  return c;
-}
-
-static void merge_down(xaset_t *s, int dynamic) {
-  config_rec *c, *dst;
-
-  if (s == NULL ||
-      s->xas_list == NULL)
-    return;
-
-  for (c = (config_rec *) s->xas_list; c; c = c->next) {
-    if ((c->flags & CF_MERGEDOWN) ||
-        (c->flags & CF_MERGEDOWN_MULTI)) {
-
-      for (dst = (config_rec *) s->xas_list; dst; dst = dst->next) {
-        if (dst->config_type == CONF_ANON ||
-           dst->config_type == CONF_DIR) {
-
-          /* If an option of the same name/type is found in the
-           * next level down, it overrides, so we don't merge.
-           */
-          if ((c->flags & CF_MERGEDOWN) &&
-              find_config(dst->subset, c->config_type, c->name, FALSE))
-            continue;
-
-          if (dynamic) {
-            /* If we are doing a dynamic merge (i.e. .ftpaccess files) then
-             * we do not need to re-merge the static configs that are already
-             * there.  Otherwise we are creating copies needlessly of any
-             * config_rec marked with the CF_MERGEDOWN_MULTI flag, which
-             * adds to the memory usage/processing time.
-             *
-             * If neither the src or the dst config have the CF_DYNAMIC
-             * flag, it's a static config, and we can skip this merge and move
-             * on.  Otherwise, we can merge it.
-             */
-            if (!(c->flags & CF_DYNAMIC) && !(dst->flags & CF_DYNAMIC)) {
-              continue;
-            }
-          }
-
-          /* We want to scan the config_recs contained in dst's subset to see
-           * if we can find another config_rec that duplicates the one we want
-           * to merge into dst.
-           */
-          if (dst->subset != NULL) {
-              config_rec *r = NULL;
-            int merge = TRUE;
-
-            for (r = (config_rec *) dst->subset->xas_list; r; r = r->next) {
-              pr_signals_handle();
-
-              if (config_cmp(r, r->name, c, c->name) == 0) {
-                merge = FALSE;
-
-                pr_trace_msg("config", 15,
-                  "found duplicate '%s' record in '%s', skipping merge",
-                  r->name, dst->name);
-                break;
-              }
-            }
-
-            if (merge) {
-              (void) copy_config_from(c, dst);
-            }
- 
-          } else {
-            /* No existing subset in dst; we can merge this one in. */
-            (void) copy_config_from(c, dst);
-          }
-        }
-      }
-    }
-  }
-
-  /* Top level merged, recursively merge lower levels */
-  for (c = (config_rec *) s->xas_list; c; c = c->next) {
-    if (c->subset &&
-        (c->config_type == CONF_ANON ||
-         c->config_type == CONF_DIR)) {
-      merge_down(c->subset, dynamic);
-    }
-  }
-}
-
-/* Iterate through <Directory> blocks inside of anonymous and
- * resolve each one.
- */
-void resolve_anonymous_dirs(xaset_t *clist) {
-  config_rec *c;
-  char *realdir;
-
-  if (!clist)
-    return;
-
-  for (c = (config_rec *) clist->xas_list; c; c = c->next) {
-    if (c->config_type == CONF_DIR) {
-      if (c->argv[1]) {
-        realdir = dir_best_path(c->pool, c->argv[1]);
-        if (realdir) {
-          c->argv[1] = realdir;
-
-        } else {
-          realdir = dir_canonical_path(c->pool, c->argv[1]);
-          if (realdir)
-            c->argv[1] = realdir;
-        }
-      }
-
-      if (c->subset)
-        resolve_anonymous_dirs(c->subset);
-    }
-  }
-}
-
-/* Iterate through directory configuration items and resolve ~ references. */
-void resolve_deferred_dirs(server_rec *s) {
-  config_rec *c;
-
-  if (s == NULL ||
-      s->conf == NULL) {
-    return;
-  }
-
-  for (c = (config_rec *) s->conf->xas_list; c; c = c->next) {
-    if (c->config_type == CONF_DIR &&
-        (c->flags & CF_DEFER)) {
-      char *interp_dir = NULL, *real_dir = NULL, *orig_name = NULL;
-      const char *trace_channel = "directory";
+  for (c = (config_rec *) s->conf->xas_list; c; c = c->next) {
+    if (c->config_type == CONF_DIR &&
+        (c->flags & CF_DEFER)) {
+      char *interp_dir = NULL, *real_dir = NULL, *orig_name = NULL;
+      const char *trace_channel = "directory";
 
       if (pr_trace_get_level(trace_channel) >= 11) {
         orig_name = pstrdup(c->pool, c->name);
       }
 
       /* Check for any expandable variables. */
-      c->name = path_subst_uservar(c->pool, &c->name);
+      c->name = (char *) path_subst_uservar(c->pool, (const char **) &c->name);
 
       /* Handle any '~' interpolation. */
       interp_dir = dir_interpolate(c->pool, c->name);
@@ -2808,8 +2379,9 @@ static void copy_recur(xaset_t **set, pool *p, config_rec *c,
   int argc;
   void **argv, **sargv;
 
-  if (!*set)
+  if (!*set) {
     *set = xaset_create(p, NULL);
+  }
 
   newconf = pr_config_add_set(set, c->name, 0);
   newconf->config_type = c->config_type;
@@ -2823,28 +2395,37 @@ static void copy_recur(xaset_t **set, pool *p, config_rec *c,
     sargv = c->argv;
     argc = newconf->argc;
 
-    while (argc--)
+    while (argc--) {
       *argv++ = *sargv++;
+    }
 
-    if (argv)
+    if (argv) {
       *argv++ = NULL;
+    }
   }
 
-  if (c->subset)
-    for (c = (config_rec *) c->subset->xas_list; c; c = c->next)
+  if (c->subset) {
+    for (c = (config_rec *) c->subset->xas_list; c; c = c->next) {
+      pr_signals_handle();
       copy_recur(&newconf->subset, p, c, newconf);
+    }
+  }
 }
 
 static void copy_global_to_all(xaset_t *set) {
   server_rec *s;
   config_rec *c;
 
-  if (!set || !set->xas_list)
+  if (!set || !set->xas_list) {
     return;
+  }
 
-  for (c = (config_rec *) set->xas_list; c; c = c->next)
-    for (s = (server_rec *) server_list->xas_list; s; s = s->next)
+  for (c = (config_rec *) set->xas_list; c; c = c->next) {
+    for (s = (server_rec *) server_list->xas_list; s; s = s->next) {
+      pr_signals_handle();
       copy_recur(&s->conf, s->pool, c, NULL);
+    }
+  }
 }
 
 static void fixup_globals(xaset_t *list) {
@@ -2857,8 +2438,9 @@ static void fixup_globals(xaset_t *list) {
      * context.
      */
     if (!s->conf ||
-        !s->conf->xas_list)
+        !s->conf->xas_list) {
       continue;
+    }
 
     for (c = (config_rec *) s->conf->xas_list; c; c = cnext) {
       cnext = c->next;
@@ -2869,8 +2451,9 @@ static void fixup_globals(xaset_t *list) {
          * (including this one), then pull the block "out of play".
          */
         if (c->subset &&
-            c->subset->xas_list)
+            c->subset->xas_list) {
           copy_global_to_all(c->subset);
+        }
 
         xaset_remove(s->conf, (xasetmember_t *) c);
 
@@ -2900,490 +2483,17 @@ void fixup_dirs(server_rec *s, int flags) {
   reorder_dirs(s->conf, flags);
 
   /* Merge mergeable configuration items down. */
-  merge_down(s->conf, FALSE);
+  pr_config_merge_down(s->conf, FALSE);
 
   if (!(flags & CF_SILENT)) {
     pr_log_debug(DEBUG5, "%s", "");
     pr_log_debug(DEBUG5, "Config for %s:", s->ServerName);
-    pr_config_dump(config_dumpf, s->conf, NULL);
+    pr_config_dump(NULL, s->conf, NULL);
   }
 
   return;
 }
 
-config_rec *find_config_next2(config_rec *prev, config_rec *c, int type,
-    const char *name, int recurse, unsigned long flags) {
-  config_rec *top = c;
-  unsigned int cid = 0;
-  size_t namelen = 0;
-
-  /* We do two searches (if recursing) so that we find the "deepest"
-   * level first.
-   */
-
-  if (c == NULL &&
-      prev == NULL) {
-    return NULL;
-  }
-
-  if (prev == NULL) {
-    prev = top;
-  }
-
-  if (name != NULL) {
-    cid = pr_config_get_id(name);
-    namelen = strlen(name);
-  }
-
-  if (recurse) {
-    do {
-      config_rec *res = NULL;
-
-      pr_signals_handle();
-
-      for (c = top; c; c = c->next) {
-        if (c->subset &&
-            c->subset->xas_list) {
-          config_rec *subc = NULL;
-
-          for (subc = (config_rec *) c->subset->xas_list;
-               subc;
-               subc = subc->next) {
-            pr_signals_handle();
-
-            if (subc->config_type == CONF_ANON &&
-                (flags & PR_CONFIG_FIND_FL_SKIP_ANON)) {
-              /* Skip <Anonymous> config_rec */
-              continue;
-            }
-
-            if (subc->config_type == CONF_DIR &&
-                (flags & PR_CONFIG_FIND_FL_SKIP_DIR)) {
-              /* Skip <Directory> config_rec */
-              continue;
-            }
-
-            if (subc->config_type == CONF_LIMIT &&
-                (flags & PR_CONFIG_FIND_FL_SKIP_LIMIT)) {
-              /* Skip <Limit> config_rec */
-              continue;
-            }
-
-            if (subc->config_type == CONF_DYNDIR &&
-                (flags & PR_CONFIG_FIND_FL_SKIP_DYNDIR)) {
-              /* Skip .ftpaccess config_rec */
-              continue;
-            }
-
-            res = find_config_next2(NULL, subc, type, name, recurse + 1, flags);
-            if (res)
-              return res;
-          }
-        }
-      }
-
-      /* If deep recursion yielded no match try the current subset.
-       *
-       * NOTE: the string comparison here is specifically case sensitive.
-       * The config_rec names are supplied by the modules and intentionally
-       * case sensitive (they shouldn't be verbatim from the config file)
-       * Do NOT change this to strcasecmp(), no matter how tempted you are
-       * to do so, it will break stuff. ;)
-       */
-      for (c = top; c; c = c->next) {
-        if (type == -1 ||
-            type == c->config_type) {
-
-          if (name == NULL) {
-            return c;
-          }
-
-          if (cid != 0 &&
-              cid == c->config_id) {
-            return c;
-          }
-
-          if (strncmp(name, c->name, namelen + 1) == 0) {
-            return c;
-          }
-        }
-      }
-
-      /* Restart the search at the previous level if required */
-      if (prev->parent &&
-          recurse == 1 &&
-          prev->parent->next &&
-          prev->parent->set != find_config_top) {
-        prev = top = prev->parent->next;
-        c = top;
-        continue;
-      }
-
-      break;
-    } while (TRUE);
-
-  } else {
-    for (c = top; c; c = c->next) {
-      if (type == -1 ||
-          type == c->config_type) {
-
-        if (name == NULL) {
-          return c;
-        }
-
-        if (cid != 0 &&
-            cid == c->config_id) {
-          return c;
-        }
-
-        if (strncmp(name, c->name, namelen + 1) == 0)
-          return c;
-      }
-    }
-  }
-
-  return NULL;
-}
-
-config_rec *find_config_next(config_rec *prev, config_rec *c, int type,
-    const char *name, int recurse) {
-  return find_config_next2(prev, c, type, name, recurse, 0UL);
-}
-
-void find_config_set_top(config_rec *c) {
-  if (c &&
-      c->parent) {
-    find_config_top = c->parent->set;
-
-  } else {
-    find_config_top = NULL;
-  }
-}
-
-config_rec *find_config2(xaset_t *set, int type, const char *name,
-  int recurse, unsigned long flags) {
-
-  if (set == NULL ||
-      set->xas_list == NULL) {
-    return NULL;
-  }
-
-  find_config_set_top((config_rec *) set->xas_list);
-
-  return find_config_next2(NULL, (config_rec *) set->xas_list, type, name,
-    recurse, flags);
-}
-
-config_rec *find_config(xaset_t *set, int type, const char *name, int recurse) {
-  return find_config2(set, type, name, recurse, 0UL);
-}
-
-void *get_param_ptr(xaset_t *set, const char *name, int recurse) {
-  config_rec *c;
-
-  if (!set) {
-    _last_param_ptr = NULL;
-    return NULL;
-  }
-
-  c = find_config(set, CONF_PARAM, name, recurse);
-
-  if (c &&
-      c->argc) {
-    _last_param_ptr = c;
-    return c->argv[0];
-  }
-
-  _last_param_ptr = NULL;
-  return NULL;
-}
-
-void *get_param_ptr_next(const char *name,int recurse) {
-  config_rec *c;
-
-  if (!_last_param_ptr ||
-      !_last_param_ptr->next) {
-    _last_param_ptr = NULL;
-    return NULL;
-  }
-
-  c = find_config_next(_last_param_ptr, _last_param_ptr->next, CONF_PARAM,
-    name, recurse);
-
-  if (c &&
-      c->argv) {
-    _last_param_ptr = c;
-    return c->argv[0];
-  }
-
-  _last_param_ptr = NULL;
-  return NULL;
-}
-
-int remove_config(xaset_t *set, const char *name, int recurse) {
-  server_rec *s = pr_parser_server_ctxt_get();
-  config_rec *c;
-  int found = 0;
-  xaset_t *fset;
-
-  if (!s)
-    s = main_server;
-
-  while ((c = find_config(set, -1, name, recurse)) != NULL) {
-    found++;
-
-    fset = c->set;
-    xaset_remove(fset, (xasetmember_t *) c);
-
-    /* If the set is empty, and has no more contained members in the xas_list,
-     * destroy the set.
-     */
-    if (!fset->xas_list) {
-
-      /* First, set any pointers to the container of the set to NULL. */
-      if (c->parent && c->parent->subset == fset)
-        c->parent->subset = NULL;
-
-      else if (s->conf == fset)
-        s->conf = NULL;
-
-      /* Next, destroy the set's pool, which destroys the set as well. */
-      destroy_pool(fset->pool);
-
-    } else {
-
-      /* If the set was not empty, destroy only the requested config_rec. */
-      destroy_pool(c->pool);
-    }
-  }
-
-  return found;
-}
-
-config_rec *add_config_param_set(xaset_t **set, const char *name,
-    int num, ...) {
-  config_rec *c = pr_config_add_set(set, name, 0);
-  void **argv;
-  va_list ap;
-
-  if (c) {
-    c->config_type = CONF_PARAM;
-    c->argc = num;
-    c->argv = pcalloc(c->pool, (num+1) * sizeof(void *));
-
-    argv = c->argv;
-    va_start(ap,num);
-
-    while (num-- > 0)
-      *argv++ = va_arg(ap, void *);
-
-    va_end(ap);
-  }
-
-  return c;
-}
-
-config_rec *add_config_param_str(const char *name, int num, ...) {
-  config_rec *c = pr_config_add(NULL, name, 0);
-  char *arg = NULL;
-  void **argv = NULL;
-  va_list ap;
-
-  if (c) {
-    c->config_type = CONF_PARAM;
-    c->argc = num;
-    c->argv = pcalloc(c->pool, (num+1) * sizeof(char *));
-
-    argv = c->argv;
-    va_start(ap, num);
-
-    while (num-- > 0) {
-      arg = va_arg(ap, char *);
-      if (arg)
-        *argv++ = pstrdup(c->pool, arg);
-      else
-        *argv++ = NULL;
-    }
-
-    va_end(ap);
-  }
-
-  return c;
-}
-
-config_rec *pr_conf_add_server_config_param_str(server_rec *s, const char *name,
-    int num, ...) {
-  config_rec *c = pr_config_add(s, name, 0);
-  char *arg = NULL;
-  void **argv = NULL;
-  va_list ap;
-
-  if (c) {
-    c->config_type = CONF_PARAM;
-    c->argc = num;
-    c->argv = pcalloc(c->pool, (num+1) * sizeof(char *));
-
-    argv = c->argv;
-    va_start(ap, num);
-
-    while (num-- > 0) {
-      arg = va_arg(ap, char *);
-      if (arg)
-        *argv++ = pstrdup(c->pool, arg);
-      else
-        *argv++ = NULL;
-    }
-
-    va_end(ap);
-  }
-
-  return c;
-}
-
-config_rec *add_config_param(const char *name, int num, ...) {
-  config_rec *c;
-  void **argv;
-  va_list ap;
-
-  if (name == NULL) {
-    errno = EINVAL;
-    return NULL;
-  }
-
-  c = pr_config_add(NULL, name, 0);
-  if (c) {
-    c->config_type = CONF_PARAM;
-    c->argc = num;
-    c->argv = pcalloc(c->pool, (num+1) * sizeof(void*));
-
-    argv = c->argv;
-    va_start(ap, num);
-
-    while (num-- > 0)
-      *argv++ = va_arg(ap, void *);
-
-    va_end(ap);
-  }
-
-  return c;
-}
-
-static int config_filename_cmp(const void *a, const void *b) {
-  return strcmp(*((char **) a), *((char **) b));
-}
-
-int parse_config_path(pool *p, const char *path) {
-  struct stat st;
-  int have_glob;
-  
-  if (!path) {
-    errno = EINVAL;
-    return -1;
-  }
-
-  have_glob = pr_str_is_fnmatch(path); 
-
-  if (!have_glob && pr_fsio_lstat(path, &st) < 0)
-    return -1;
-
-  if (have_glob ||
-      (!S_ISLNK(st.st_mode) && S_ISDIR(st.st_mode))) {
-    void *dirh;
-    struct dirent *dent;
-    array_header *file_list;
-    char *dup_path = pstrdup(p, path);
-    char *tmp = strrchr(dup_path, '/');
-
-    if (have_glob && tmp) {
-      *tmp++ = '\0';
-
-      if (pr_str_is_fnmatch(dup_path)) {
-        pr_log_pri(PR_LOG_WARNING, "error: wildcard patterns not allowed in "
-          "configuration directory name '%s'", dup_path);
-        errno = EINVAL;
-        return -1;
-      }
-
-      /* Check the directory component. */
-      pr_fsio_lstat(dup_path, &st);
-
-      if (S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) {
-        pr_log_pri(PR_LOG_WARNING,
-          "error: cannot read configuration path '%s': Not a directory",
-          dup_path);
-        errno = EINVAL;
-        return -1;
-      }
-
-      if (!pr_str_is_fnmatch(tmp)) {
-        pr_log_pri(PR_LOG_WARNING,
-          "error: wildcard pattern required for file '%s'", tmp);
-        errno = EINVAL;
-        return -1;
-      }
-    }
-
-    pr_log_pri(PR_LOG_INFO, "processing configuration directory '%s'",
-      dup_path);
-
-    dirh = pr_fsio_opendir(dup_path);
-    if (dirh == NULL) {
-      pr_log_pri(PR_LOG_WARNING,
-        "error: unable to open configuration directory '%s': %s", dup_path,
-        strerror(errno));
-      errno = EINVAL;
-      return -1;
-    }
-
-    file_list = make_array(p, 0, sizeof(char *));
-
-    while ((dent = pr_fsio_readdir(dirh)) != NULL) {
-      if (strncmp(dent->d_name, ".", 2) != 0 &&
-          strncmp(dent->d_name, "..", 3) != 0 &&
-          (!have_glob ||
-           pr_fnmatch(tmp, dent->d_name, PR_FNM_PERIOD) == 0))
-        *((char **) push_array(file_list)) = pdircat(p, dup_path,
-          dent->d_name, NULL);
-    }
-
-    pr_fsio_closedir(dirh);
-
-    if (file_list->nelts) {
-      register unsigned int i;
-
-      qsort((void *) file_list->elts, file_list->nelts, sizeof(char *),
-        config_filename_cmp);
-
-      for (i = 0; i < file_list->nelts; i++) {
-        int res, xerrno;
-        char *file;
-
-        file = ((char **) file_list->elts)[i];
-
-        /* Make sure we always parse the files with root privs.  The
-         * previously parsed file might have had root privs relinquished
-         * (e.g. by its directive handlers), but when we first start up,
-         * we have root privs.  See Bug#3855.
-         */
-        PRIVS_ROOT
-        res = pr_parser_parse_file(p, file, NULL, 0);
-        xerrno = errno;
-        PRIVS_RELINQUISH
-
-        if (res < 0) {
-          pr_log_pri(PR_LOG_WARNING,
-            "error: unable to open parse file '%s': %s", file,
-            strerror(xerrno));
-        }
-      }
-    }
-
-    return 0;
-  }
-
-  return pr_parser_parse_file(p, path, NULL, 0);
-}
-
 /* Go through each server configuration and complain if important information
  * is missing (post reading configuration files).  Otherwise, fill in defaults
  * where applicable.
@@ -3430,8 +2540,9 @@ int fixup_servers(xaset_t *list) {
           }
 #endif /* PR_USE_IPV6 */
 
-          if (ipstr)
-            pr_conf_add_server_config_param_str(s, "_bind", 1, ipstr);
+          if (ipstr) {
+            pr_conf_add_server_config_param_str(s, "_bind_", 1, ipstr);
+          }
         }
       }
  
@@ -3478,9 +2589,17 @@ int fixup_servers(xaset_t *list) {
 
     c = find_config(s->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
     if (c != NULL) {
+      const char *masq_addr;
+
+      if (c->argv[0] != NULL) {
+        masq_addr = pr_netaddr_get_ipstr(c->argv[0]);
+
+      } else {
+        masq_addr = c->argv[1];
+      }
+
       pr_log_pri(PR_LOG_INFO, "%s:%d masquerading as %s",
-        pr_netaddr_get_ipstr(s->addr), s->ServerPort,
-        pr_netaddr_get_ipstr((pr_netaddr_t *) c->argv[0]));
+        pr_netaddr_get_ipstr(s->addr), s->ServerPort, masq_addr);
     }
 
     /* Honor the DefaultServer directive only if SocketBindTight is not
@@ -3492,7 +2611,7 @@ int fixup_servers(xaset_t *list) {
         *default_server == TRUE) {
 
       if (SocketBindTight == FALSE) {
-        pr_netaddr_set_sockaddr_any(s->addr);
+        pr_netaddr_set_sockaddr_any((pr_netaddr_t *) s->addr);
 
       } else {
         pr_log_pri(PR_LOG_NOTICE,
@@ -3561,6 +2680,8 @@ static void set_tcp_bufsz(server_rec *s) {
 
     pr_log_debug(DEBUG3, "socket error: %s", strerror(errno));
     pr_log_debug(DEBUG4, "using default TCP receive/send buffer sizes");
+
+    return;
   }
 
 #ifndef PR_TUNABLE_RCVBUFSZ
@@ -3617,44 +2738,9 @@ static void set_tcp_bufsz(server_rec *s) {
   (void) close(sockfd);
 }
 
-void init_config(void) {
-  pool *conf_pool = make_sub_pool(permanent_pool);
-  pr_pool_tag(conf_pool, "Config Pool");
-
-  /* Make sure global_config_pool is destroyed */
-  if (global_config_pool) {
-    destroy_pool(global_config_pool);
-    global_config_pool = NULL;
-  }
-
-  if (config_tab) {
-    /* Clear the existing config ID table.  This needs to happen when proftpd
-     * is restarting.
-     */
-    if (pr_table_empty(config_tab) < 0) {
-      pr_log_debug(DEBUG0, "error emptying config ID table: %s",
-        strerror(errno));
-    }
-
-    if (pr_table_free(config_tab) < 0) {
-      pr_log_debug(DEBUG0, "error destroying config ID table: %s",
-        strerror(errno));
-    }
-
-    config_tab = pr_table_alloc(config_tab_pool, 0);
-
-    /* Reset the ID counter as well.  Otherwise, an exceedingly long-lived
-     * proftpd, restarted many times, has the possibility of overflowing
-     * the counter data type.
-     */
-    config_id = 0;
-
-  } else {
-
-    config_tab_pool = make_sub_pool(permanent_pool);
-    pr_pool_tag(config_tab_pool, "Config ID Table Pool");
-    config_tab = pr_table_alloc(config_tab_pool, 0);
-  }
+void init_dirtree(void) {
+  pool *dirtree_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(dirtree_pool, "Dirtree Pool");
 
   if (server_list) {
     server_rec *s, *s_next;
@@ -3681,18 +2767,18 @@ void init_config(void) {
    * why we create yet another subpool, reusing the conf_pool pointer.
    * The pool creation below is not redundant.
    */
-  server_list = xaset_create(conf_pool, NULL);
+  server_list = xaset_create(dirtree_pool, NULL);
 
-  conf_pool = make_sub_pool(permanent_pool);
-  pr_pool_tag(conf_pool, "main_server pool");
+  dirtree_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(dirtree_pool, "main_server pool");
 
-  main_server = (server_rec *) pcalloc(conf_pool, sizeof(server_rec));
+  main_server = (server_rec *) pcalloc(dirtree_pool, sizeof(server_rec));
   xaset_insert(server_list, (xasetmember_t *) main_server);
 
-  main_server->pool = conf_pool;
+  main_server->pool = dirtree_pool;
   main_server->set = server_list;
   main_server->sid = 1;
-  main_server->notes = pr_table_nalloc(conf_pool, 0, 8);
+  main_server->notes = pr_table_nalloc(dirtree_pool, 0, 8);
 
   /* TCP KeepAlive is enabled by default, with the system defaults. */
   main_server->tcp_keepalive = palloc(main_server->pool,
@@ -3710,8 +2796,7 @@ void init_config(void) {
   return;
 }
 
-/* These functions are used by modules to help parse configuration.
- */
+/* These functions are used by modules to help parse configuration. */
 
 unsigned char check_context(cmd_rec *cmd, int allowed) {
   int ctxt = (cmd->config && cmd->config->config_type != CONF_PARAM ?
@@ -3729,10 +2814,11 @@ char *get_context_name(cmd_rec *cmd) {
   static char cbuf[20];
 
   if (!cmd->config || cmd->config->config_type == CONF_PARAM) {
-    if (cmd->server->config_type == CONF_VIRTUAL)
+    if (cmd->server->config_type == CONF_VIRTUAL) {
       return "<VirtualHost>";
-    else
-      return "server config";
+    }
+
+    return "server config";
   }
 
   switch (cmd->config->config_type) {
@@ -3773,68 +2859,10 @@ int get_boolean(cmd_rec *cmd, int av) {
   return pr_str_is_boolean(cp);
 }
 
-char *get_full_cmd(cmd_rec *cmd) {
+const char *get_full_cmd(cmd_rec *cmd) {
   return pr_cmd_get_displayable_str(cmd, NULL);
 }
 
-unsigned int pr_config_get_id(const char *name) {
-  void *ptr = NULL;
-  unsigned int id = 0;
-
-  if (!name) {
-    errno = EINVAL;
-    return 0;
-  }
-
-  if (!config_tab) {
-    errno = EPERM;
-    return 0;
-  }
-
-  ptr = pr_table_get(config_tab, name, NULL);
-  if (ptr == NULL) {
-    errno = ENOENT;
-    return 0;
-  }
-
-  id = *((unsigned int *) ptr);
-  return id;
-}
-
-unsigned int pr_config_set_id(const char *name) {
-  unsigned int *ptr = NULL;
-  unsigned int id;
-
-  if (!name) {
-    errno = EINVAL;
-    return 0;
-  }
-
-  if (!config_tab) {
-    errno = EPERM;
-    return 0;
-  }
-
-  ptr = pr_table_pcalloc(config_tab, sizeof(unsigned int));
-  *ptr = ++config_id;
-
-  if (pr_table_add(config_tab, name, ptr, sizeof(unsigned int *)) < 0) {
-    if (errno == EEXIST) {
-      id = pr_config_get_id(name);
-
-    } else {
-      pr_log_debug(DEBUG0, "error adding '%s' to config ID table: %s",
-        name, strerror(errno));
-      return 0;
-    }
-
-  } else {
-    id = *ptr;
-  }
-
-  return id;
-}
-
 int pr_config_get_xfer_bufsz(void) {
   return xfer_bufsz;
 }
@@ -3864,4 +2892,3 @@ int pr_config_get_server_xfer_bufsz(int direction) {
 
   return pr_config_get_xfer_bufsz2(direction);
 }
-
diff --git a/src/display.c b/src/display.c
index 46ee945..914a86d 100644
--- a/src/display.c
+++ b/src/display.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2014 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Display of files
- * $Id: display.c,v 1.32 2013-10-07 05:51:30 castaglia Exp $
- */
+/* Display of files */
 
 #include "conf.h"
 
@@ -34,14 +32,17 @@ static const char *prev_msg = NULL;
 
 /* Note: The size provided by pr_fs_getsize2() is in KB, not bytes. */
 static void format_size_str(char *buf, size_t buflen, off_t size) {
-  char *units[] = {"K", "M", "G", "T", "P"};
-  unsigned int nunits = 5;
+  char *units[] = {"K", "M", "G", "T", "P", "E", "Z", "Y"};
+  unsigned int nunits = 8;
   register unsigned int i = 0;
   int res;
 
-  /* Determine the appropriate units label to use. */
+  /* Determine the appropriate units label to use. Do not exceed the max
+   * possible unit support (yottabytes), by ensuring that i maxes out at
+   * index 7 (of 8 possible units).
+   */
   while (size > 1024 &&
-         i < nunits) {
+         i < (nunits - 1)) {
     pr_signals_handle();
 
     size /= 1024;
@@ -136,33 +137,33 @@ static int display_flush_lines(pool *p, const char *resp_code, int flags) {
   return 0;
 }
 
-static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
+static int display_fh(pr_fh_t *fh, const char *fs, const char *resp_code,
     int flags) {
   struct stat st;
   char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
   int len, res;
-  unsigned int *current_clients = NULL;
-  unsigned int *max_clients = NULL;
+  const unsigned int *current_clients = NULL;
+  const unsigned int *max_clients = NULL;
   off_t fs_size = 0;
   pool *p;
-  void *v;
+  const void *v;
   xaset_t *s;
   config_rec *c = NULL;
+  const char *mg_time, *outs = NULL, *rfc1413_ident = NULL, *user;
   const char *serverfqdn = main_server->ServerFQDN;
-  char *outs, mg_size[12] = {'\0'}, mg_size_units[12] = {'\0'},
+  char mg_size[12] = {'\0'}, mg_size_units[12] = {'\0'},
     mg_max[12] = "unlimited";
   char total_files_in[12] = {'\0'}, total_files_out[12] = {'\0'},
     total_files_xfer[12] = {'\0'};
   char mg_class_limit[12] = {'\0'}, mg_cur[12] = {'\0'},
     mg_xfer_bytes[12] = {'\0'}, mg_cur_class[12] = {'\0'};
-  char mg_xfer_units[12] = {'\0'}, *user;
-  const char *mg_time;
-  char *rfc1413_ident = NULL;
+  char mg_xfer_units[12] = {'\0'};
 
   /* Stat the opened file to determine the optimal buffer size for IO. */
   memset(&st, 0, sizeof(st));
-  pr_fsio_fstat(fh, &st);
-  fh->fh_iosz = st.st_blksize;
+  if (pr_fsio_fstat(fh, &st) == 0) {
+    fh->fh_iosz = st.st_blksize;
+  }
 
   /* Note: The size provided by pr_fs_getsize() is in KB, not bytes. */
   res = pr_fs_fgetsize(fh->fh_fd, &fs_size);
@@ -186,20 +187,21 @@ static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
   max_clients = get_param_ptr(s, "MaxClients", FALSE);
 
   v = pr_table_get(session.notes, "client-count", NULL);
-  if (v) {
+  if (v != NULL) {
     current_clients = v;
   }
 
-  snprintf(mg_cur, sizeof(mg_cur), "%u", current_clients ? *current_clients: 1);
+  snprintf(mg_cur, sizeof(mg_cur), "%u",
+    current_clients ? *current_clients : 1);
 
   if (session.conn_class != NULL &&
       session.conn_class->cls_name != NULL) {
-    unsigned int *class_clients = NULL;
+    const unsigned int *class_clients = NULL;
     config_rec *maxc = NULL;
     unsigned int maxclients = 0;
 
     v = pr_table_get(session.notes, "class-client-count", NULL);
-    if (v) {
+    if (v != NULL) {
       class_clients = v;
     }
 
@@ -214,7 +216,7 @@ static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
     maxc = find_config(main_server->conf, CONF_PARAM, "MaxClientsPerClass",
       FALSE);
 
-    while (maxc) {
+    while (maxc != NULL) {
       pr_signals_handle();
 
       if (strcmp(maxc->argv[0], session.conn_class->cls_name) != 0) {
@@ -229,9 +231,9 @@ static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
 
     if (maxclients == 0) {
       maxc = find_config(main_server->conf, CONF_PARAM, "MaxClients", FALSE);
-
-      if (maxc)
+      if (maxc != NULL) {
         maxclients = *((unsigned int *) maxc->argv[0]);
+      }
     }
 
     snprintf(mg_class_limit, sizeof(mg_class_limit), "%u", maxclients);
@@ -263,13 +265,21 @@ static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
   snprintf(mg_max, sizeof(mg_max), "%u", max_clients ? *max_clients : 0);
 
   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
-  if (user == NULL)
+  if (user == NULL) {
     user = "";
+  }
 
   c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
-  if (c) {
-    pr_netaddr_t *masq_addr = (pr_netaddr_t *) c->argv[0];
-    serverfqdn = pr_netaddr_get_dnsstr(masq_addr);
+  if (c != NULL) {
+    pr_netaddr_t *masq_addr = NULL;
+
+    if (c->argv[0] != NULL) {
+      masq_addr = c->argv[0];
+    }
+
+    if (masq_addr != NULL) {
+      serverfqdn = pr_netaddr_get_dnsstr(masq_addr);
+    }
   }
 
   /* "Stringify" the file number for this session. */
@@ -333,15 +343,17 @@ static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
       if (strncmp(key, "%{time:", 7) == 0) {
         char time_str[128], *fmt;
         time_t now;
-        struct tm *time_info;
+        struct tm *tm;
 
         fmt = pstrndup(p, key + 7, strlen(key) - 8);
 
-        now = time(NULL);
-        time_info = pr_localtime(NULL, &now);
-
+        time(&now);
         memset(time_str, 0, sizeof(time_str));
-        strftime(time_str, sizeof(time_str), fmt, time_info);
+
+        tm = pr_localtime(NULL, &now);
+        if (tm != NULL) {
+          strftime(time_str, sizeof(time_str), fmt, tm);
+        }
 
         val = pstrdup(p, time_str);
 
@@ -406,38 +418,40 @@ static int display_fh(pr_fh_t *fh, const char *fs, const char *code,
        * response chains to be flushed, which won't work (i.e. DisplayConnect
        * and DisplayQuit).
        */
-      display_add_line(p, code, outs);
+      display_add_line(p, resp_code, outs);
 
     } else {
-      pr_response_add(code, "%s", outs);
+      pr_response_add(resp_code, "%s", outs);
     }
   }
 
   if (flags & PR_DISPLAY_FL_SEND_NOW) {
-    display_flush_lines(p, code, flags);
+    display_flush_lines(p, resp_code, flags);
   }
 
   destroy_pool(p);
   return 0;
 }
 
-int pr_display_fh(pr_fh_t *fh, const char *fs, const char *code, int flags) {
-  if (!fh || !code) {
+int pr_display_fh(pr_fh_t *fh, const char *fs, const char *resp_code,
+    int flags) {
+  if (fh == NULL ||
+      resp_code == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  return display_fh(fh, fs, code, flags);
+  return display_fh(fh, fs, resp_code, flags);
 }
 
-int pr_display_file(const char *path, const char *fs, const char *code,
+int pr_display_file(const char *path, const char *fs, const char *resp_code,
     int flags) {
   pr_fh_t *fh = NULL;
   int res, xerrno;
   struct stat st;
 
   if (path == NULL ||
-      code == NULL) {
+      resp_code == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -449,7 +463,11 @@ int pr_display_file(const char *path, const char *fs, const char *code,
 
   res = pr_fsio_fstat(fh, &st);
   if (res < 0) {
+    xerrno = errno;
+
     pr_fsio_close(fh);
+
+    errno = xerrno;
     return -1;
   }
 
@@ -459,7 +477,7 @@ int pr_display_file(const char *path, const char *fs, const char *code,
     return -1;
   }
 
-  res = display_fh(fh, fs, code, flags);
+  res = display_fh(fh, fs, resp_code, flags);
   xerrno = errno;
 
   pr_fsio_close(fh);
diff --git a/src/encode.c b/src/encode.c
index b2ceeae..ade5f68 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -22,7 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* UTF8/charset encoding/decoding */
+/* UTF8/charset encoding/decoding. */
 
 #include "conf.h"
 
@@ -40,6 +40,7 @@
 static iconv_t decode_conv = (iconv_t) -1;
 static iconv_t encode_conv = (iconv_t) -1;
 
+static unsigned long encoding_policy = 0UL;
 static const char *local_charset = NULL;
 static const char *encoding = "UTF-8";
 static int supports_telnet_iac = TRUE;
@@ -245,8 +246,9 @@ char *pr_decode_str(pool *p, const char *in, size_t inlen, size_t *outlen) {
 
   outbuflen = sizeof(outbuf);
 
-  if (str_convert(decode_conv, inbuf, &inbuflen, outbuf, &outbuflen) < 0)
+  if (str_convert(decode_conv, inbuf, &inbuflen, outbuf, &outbuflen) < 0) {
     return NULL;
+  }
 
   *outlen = sizeof(outbuf) - outbuflen;
 
@@ -299,8 +301,9 @@ char *pr_encode_str(pool *p, const char *in, size_t inlen, size_t *outlen) {
 
   outbuflen = sizeof(outbuf);
 
-  if (str_convert(encode_conv, inbuf, &inbuflen, outbuf, &outbuflen) < 0)
+  if (str_convert(encode_conv, inbuf, &inbuflen, outbuf, &outbuflen) < 0) {
     return NULL;
+  }
 
   *outlen = sizeof(outbuf) - outbuflen;
 
@@ -376,6 +379,15 @@ int pr_encode_enable_encoding(const char *codeset) {
 #endif /* !HAVE_ICONV_H */
 }
 
+unsigned long pr_encode_get_policy(void) {
+  return encoding_policy;
+}
+
+int pr_encode_set_policy(unsigned long policy) {
+  encoding_policy = policy;
+  return 0;
+}
+
 const char *pr_encode_get_local_charset(void) {
   const char *charset = NULL;
 
@@ -469,11 +481,15 @@ int pr_encode_set_charset_encoding(const char *charset, const char *codeset) {
 
   res = encode_init();
   if (res < 0) {
+    int xerrno = errno;
+
     pr_trace_msg(trace_channel, 1,
       "failed to initialize encoding for local charset %s, encoding %s, "
       "disabling encoding", charset, codeset);
     local_charset = NULL;
     encoding = NULL;
+
+    errno = xerrno;
   }
 
   return res;
diff --git a/src/env.c b/src/env.c
index 8f9ba61..8f05a24 100644
--- a/src/env.c
+++ b/src/env.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2013 The ProFTPD Project team
+ * Copyright (c) 2007-2014 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,14 +22,13 @@
  * OpenSSL in the source distribution.
  */
 
-/* Environment management
- * $Id: env.c,v 1.11 2013-10-07 01:29:05 castaglia Exp $
- */
+/* Environment management */
 
 #include "conf.h"
 
 char *pr_env_get(pool *p, const char *key) {
-  if (!p || !key) {
+  if (p == NULL ||
+      key == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -51,7 +50,9 @@ int pr_env_set(pool *p, const char *key, const char *value) {
   const char *str;
 #endif /* !HAVE_SETENV and !HAVE_PUTENV */
 
-  if (!p || !key || !value) {
+  if (p == NULL ||
+      key == NULL ||
+      value == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -68,23 +69,8 @@ int pr_env_set(pool *p, const char *key, const char *value) {
    */
 
 #if defined(HAVE_SETENV)
-# ifdef PR_USE_DEVEL
-  k = strdup(key);
-  if (k == NULL) {
-    pr_log_pri(PR_LOG_ALERT, "Out of memory!");
-    exit(1);
-  }
-
-  v = strdup(value);
-  if (v == NULL) {
-    pr_log_pri(PR_LOG_ALERT, "Out of memory!");
-    exit(1);
-  }
-
-# else
   k = key;
   v = value;
-# endif /* PR_USE_DEVEL */
   return setenv(k, v, 1);
 
 #elif defined(HAVE_PUTENV)
@@ -110,7 +96,8 @@ int pr_env_unset(pool *p, const char *key) {
   char *res;
 #endif /* !HAVE_UNSETENV */
 
-  if (!p || !key) {
+  if (p == NULL ||
+      key == NULL) {
     errno = EINVAL;
     return -1;
   }
diff --git a/src/event.c b/src/event.c
index 4a1c85d..b0dddb5 100644
--- a/src/event.c
+++ b/src/event.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2013 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Event management code
- * $Id: event.c,v 1.28 2013-02-24 16:46:42 castaglia Exp $
- */
+/* Event management code */
 
 #include "conf.h"
 
@@ -160,13 +158,8 @@ int pr_event_register(module *m, const char *event,
 
         } else {
           /* Core event listeners go at the end. */
-          if (evhl != NULL) {
-            evhl->next = evh;
-            evh->prev = evhl;
-
-          } else {
-            evl->handlers = evh;
-          }
+          evhl->next = evh;
+          evh->prev = evhl;
         }
 
       } else {
diff --git a/src/expr.c b/src/expr.c
index 310ff29..44d4695 100644
--- a/src/expr.c
+++ b/src/expr.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,15 +22,13 @@
  * OpenSSL in the source distribution.
  */
 
-/* Expression API implementation
- * $Id: expr.c,v 1.6 2011-12-11 02:14:43 castaglia Exp $
- */
+/* Expression API implementation */
 
 #include "conf.h"
 
-array_header *pr_expr_create(pool *p, int *argc, char **argv) {
+array_header *pr_expr_create(pool *p, unsigned int *argc, char **argv) {
   array_header *acl = NULL;
-  int cnt;
+  unsigned int cnt;
   char *s, *ent;
 
   if (p == NULL ||
@@ -56,8 +54,9 @@ array_header *pr_expr_create(pool *p, int *argc, char **argv) {
         while ((ent = pr_str_get_token(&s, sep)) != NULL) {
           pr_signals_handle();
 
-          if (*ent)
+          if (*ent) {
             *((char **) push_array(acl)) = ent;
+          }
         }
 
       } else {
diff --git a/src/feat.c b/src/feat.c
index d8002d8..a1476a0 100644
--- a/src/feat.c
+++ b/src/feat.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2008 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Feature management code
- * $Id: feat.c,v 1.9 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Feature management code */
 
 #include "conf.h"
 
@@ -32,13 +30,13 @@ static pool *feat_pool = NULL;
 static pr_table_t *feat_tab = NULL;
 
 int pr_feat_add(const char *feat) {
-  if (!feat) {
+  if (feat == NULL) {
     errno = EINVAL;
     return -1;
   }
 
   /* If no feature-tracking list has been allocated, create one. */
-  if (!feat_pool) {
+  if (feat_pool == NULL) {
     feat_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(feat_pool, "Feat API");
     feat_tab = pr_table_alloc(feat_pool, 0);
@@ -54,29 +52,29 @@ int pr_feat_add(const char *feat) {
 }
 
 int pr_feat_remove(const char *feat) {
-  void *res;
+  const void *res;
 
-  if (!feat_tab) {
+  if (feat_tab == NULL) {
     errno = EPERM;
     return -1;
   }
 
-  if (!feat) {
+  if (feat == NULL) {
     errno = EINVAL;
     return -1;
   }
 
   res = pr_table_remove(feat_tab, feat, NULL);
-
-  if (res)
+  if (res != NULL) {
     return 0;
+  }
 
   errno = ENOENT;
   return -1;
 }
 
 const char *pr_feat_get(void) {
-  if (!feat_tab) {
+  if (feat_tab == NULL) {
     errno = EPERM;
     return NULL;
   }
@@ -86,7 +84,7 @@ const char *pr_feat_get(void) {
 }
 
 const char *pr_feat_get_next(void) {
-  if (!feat_tab) {
+  if (feat_tab == NULL) {
     errno = EPERM;
     return NULL;
   }
diff --git a/src/filter.c b/src/filter.c
index f27b3e2..1763ad7 100644
--- a/src/filter.c
+++ b/src/filter.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2011 The ProFTPD Project team
+ * Copyright (c) 2009-2014 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@
  * 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.
- *
- * $Id: filter.c,v 1.8 2011-12-20 22:56:48 castaglia Exp $
  */
 
 #include "conf.h"
@@ -33,10 +31,16 @@ int pr_filter_allow_path(xaset_t *set, const char *path) {
   pr_regex_t *pre;
   int res;
 
+  if (set == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   /* Check any relevant PathAllowFilter first. */
 
   pre = get_param_ptr(set, "PathAllowFilter", FALSE);
-  if (pre) {
+  if (pre != NULL) {
     res = pr_regexp_exec(pre, path, 0, NULL, 0, 0, 0);
     if (res != 0) {
       return PR_FILTER_ERR_FAILS_ALLOW_FILTER;
@@ -48,8 +52,8 @@ int pr_filter_allow_path(xaset_t *set, const char *path) {
 
   /* Next check any applicable PathDenyFilter. */
 
-  pre = get_param_ptr(CURRENT_CONF, "PathDenyFilter", FALSE);
-  if (pre) {
+  pre = get_param_ptr(set, "PathDenyFilter", FALSE);
+  if (pre != NULL) {
     res = pr_regexp_exec(pre, path, 0, NULL, 0, 0, 0);
     if (res == 0) {
       return PR_FILTER_ERR_FAILS_DENY_FILTER;
@@ -66,14 +70,18 @@ int pr_filter_allow_path(xaset_t *set, const char *path) {
 }
 
 int pr_filter_parse_flags(pool *p, const char *flags_str) {
+  size_t flags_len;
+
   if (p == NULL ||
       flags_str == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  flags_len = strlen(flags_str);
+
   if (flags_str[0] != '[' ||
-      flags_str[strlen(flags_str)-1] != ']') {
+      flags_str[flags_len-1] != ']') {
     errno = EINVAL;
     return -1;
   }
diff --git a/src/fsio.c b/src/fsio.c
index c484a52..95bcaa7 100644
--- a/src/fsio.c
+++ b/src/fsio.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project
+ * Copyright (c) 2001-2017 The ProFTPD Project
  *
  * 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,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD virtual/modular file-system support
- * $Id: fsio.c,v 1.159 2014-02-11 15:17:54 castaglia Exp $
- */
+/* ProFTPD virtual/modular file-system support */
 
 #include "conf.h"
 #include "privs.h"
@@ -55,6 +53,15 @@
 # include <acl/libacl.h>
 #endif
 
+/* We will reset timers in the progress callback every Nth iteration of the
+ * callback when copying a file.
+ */
+static size_t copy_iter_count = 0;
+
+#ifndef COPY_PROGRESS_NTH_ITER
+# define COPY_PROGRESS_NTH_ITER       50000
+#endif
+
 /* For determining whether a file is on an NFS filesystem.  Note that
  * this value is Linux specific.  See Bug#3874 for details.
  */
@@ -74,14 +81,9 @@ struct fsopendir {
   DIR *dir;
 };
 
-static const char *trace_channel = "fsio";
 static pr_fs_t *root_fs = NULL, *fs_cwd = NULL;
 static array_header *fs_map = NULL;
 
-#ifdef PR_FS_MATCH
-static pr_fs_match_t *fs_match_list = NULL;
-#endif /* PR_FS_MATCH */
-
 static fsopendir_t *fsopendir_list;
 
 static void *fs_cache_dir = NULL;
@@ -94,20 +96,25 @@ static unsigned char chk_fs_map = FALSE;
 
 /* Virtual working directory */
 static char vwd[PR_TUNABLE_PATH_MAX + 1] = "/";
+
 static char cwd[PR_TUNABLE_PATH_MAX + 1] = "/";
+static size_t cwd_len = 1;
 
-static int guard_chroot = FALSE;
+static int fsio_guard_chroot = FALSE;
+static unsigned long fsio_opts = 0UL;
 
 /* Runtime enabling/disabling of mkdtemp(3) use. */
 #ifdef HAVE_MKDTEMP
-static int use_mkdtemp = TRUE;
+static int fsio_use_mkdtemp = TRUE;
 #else
-static int use_mkdtemp = FALSE;
+static int fsio_use_mkdtemp = FALSE;
 #endif /* HAVE_MKDTEMP */
 
 /* Runtime enabling/disabling of encoding of paths. */
 static int use_encoding = TRUE;
 
+static const char *trace_channel = "fsio";
+
 /* Guard against attacks like "Roaring Beast" when we are chrooted.  See:
  *
  *  https://auscert.org.au/15286
@@ -155,6 +162,38 @@ static int chroot_allow_path(const char *path) {
   return res;
 }
 
+/* Builtin/default "progress" callback for long-running file copies. */
+static void copy_progress_cb(int nwritten) {
+  int res;
+
+  copy_iter_count++;
+  if ((copy_iter_count % COPY_PROGRESS_NTH_ITER) != 0) {
+    return;
+  }
+
+  /* Reset some of the Timeouts which might interfere, i.e. TimeoutIdle and
+   * TimeoutNoDataTransfer.
+   */
+
+  res = pr_timer_reset(PR_TIMER_IDLE, ANY_MODULE);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 14, "error resetting TimeoutIdle timer: %s",
+      strerror(errno));
+  }
+
+  res = pr_timer_reset(PR_TIMER_NOXFER, ANY_MODULE);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 14,
+      "error resetting TimeoutNoTransfer timer: %s", strerror(errno));
+  }
+
+  res = pr_timer_reset(PR_TIMER_STALLED, ANY_MODULE);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 14,
+      "error resetting TimeoutStalled timer: %s", strerror(errno));
+  }
+}
+
 /* The following static functions are simply wrappers for system functions
  */
 
@@ -173,7 +212,7 @@ static int sys_lstat(pr_fs_t *fs, const char *path, struct stat *sbuf) {
 static int sys_rename(pr_fs_t *fs, const char *rnfm, const char *rnto) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(rnfm);
     if (res < 0) {
       return -1;
@@ -192,7 +231,7 @@ static int sys_rename(pr_fs_t *fs, const char *rnfm, const char *rnto) {
 static int sys_unlink(pr_fs_t *fs, const char *path) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(path);
     if (res < 0) {
       return -1;
@@ -213,9 +252,11 @@ static int sys_open(pr_fh_t *fh, const char *path, int flags) {
   flags |= O_BINARY;
 #endif
 
-  if (guard_chroot) {
-    /* If we are creating (or truncating) a file, then we need to check. */
-    if (flags & (O_APPEND|O_CREAT|O_TRUNC)) {
+  if (fsio_guard_chroot) {
+    /* If we are creating (or truncating) a file, then we need to check.
+     * Note: should O_RDWR be added to this list?
+     */
+    if (flags & (O_APPEND|O_CREAT|O_TRUNC|O_WRONLY)) {
       res = chroot_allow_path(path);
       if (res < 0) {
         return -1;
@@ -227,20 +268,6 @@ static int sys_open(pr_fh_t *fh, const char *path, int flags) {
   return res;
 }
 
-static int sys_creat(pr_fh_t *fh, const char *path, mode_t mode) {
-  int res;
-
-  if (guard_chroot) {
-    res = chroot_allow_path(path);
-    if (res < 0) {
-      return -1;
-    }
-  }
-
-  res = creat(path, mode);
-  return res;
-}
-
 static int sys_close(pr_fh_t *fh, int fd) {
   return close(fd);
 }
@@ -257,31 +284,33 @@ static off_t sys_lseek(pr_fh_t *fh, int fd, off_t offset, int whence) {
   return lseek(fd, offset, whence);
 }
 
-static int sys_link(pr_fs_t *fs, const char *path1, const char *path2) {
+static int sys_link(pr_fs_t *fs, const char *target_path,
+    const char *link_path) {
   int res;
 
-  if (guard_chroot) {
-    res = chroot_allow_path(path2);
+  if (fsio_guard_chroot) {
+    res = chroot_allow_path(link_path);
     if (res < 0) {
       return -1;
     }
   }
 
-  res = link(path1, path2);
+  res = link(target_path, link_path);
   return res;
 }
 
-static int sys_symlink(pr_fs_t *fs, const char *path1, const char *path2) {
+static int sys_symlink(pr_fs_t *fs, const char *target_path,
+    const char *link_path) {
   int res;
 
-  if (guard_chroot) {
-    res = chroot_allow_path(path2);
+  if (fsio_guard_chroot) {
+    res = chroot_allow_path(link_path);
     if (res < 0) {
       return -1;
     }
   }
 
-  res = symlink(path1, path2);
+  res = symlink(target_path, link_path);
   return res;
 }
 
@@ -297,7 +326,7 @@ static int sys_ftruncate(pr_fh_t *fh, int fd, off_t len) {
 static int sys_truncate(pr_fs_t *fs, const char *path, off_t len) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(path);
     if (res < 0) {
       return -1;
@@ -311,7 +340,7 @@ static int sys_truncate(pr_fs_t *fs, const char *path, off_t len) {
 static int sys_chmod(pr_fs_t *fs, const char *path, mode_t mode) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(path);
     if (res < 0) {
       return -1;
@@ -329,7 +358,7 @@ static int sys_fchmod(pr_fh_t *fh, int fd, mode_t mode) {
 static int sys_chown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(path);
     if (res < 0) {
       return -1;
@@ -347,7 +376,7 @@ static int sys_fchown(pr_fh_t *fh, int fd, uid_t uid, gid_t gid) {
 static int sys_lchown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(path);
     if (res < 0) {
       return -1;
@@ -364,74 +393,13 @@ static int sys_lchown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) {
  */
 static int sys_access(pr_fs_t *fs, const char *path, int mode, uid_t uid,
     gid_t gid, array_header *suppl_gids) {
-  mode_t mask;
   struct stat st;
 
-  pr_fs_clear_cache();
-  if (pr_fsio_stat(path, &st) < 0)
+  if (pr_fsio_stat(path, &st) < 0) {
     return -1;
-
-  /* Root always succeeds. */
-  if (uid == PR_ROOT_UID)
-    return 0;
-
-  /* Initialize mask to reflect the permission bits that are applicable for
-   * the given user. mask contains the user-bits if the user ID equals the
-   * ID of the file owner. mask contains the group bits if the group ID
-   * belongs to the group of the file. mask will always contain the other
-   * bits of the permission bits.
-   */
-  mask = S_IROTH|S_IWOTH|S_IXOTH;
-
-  if (st.st_uid == uid)
-    mask |= S_IRUSR|S_IWUSR|S_IXUSR;
-
-  /* Check the current group, as well as all supplementary groups.
-   * Fortunately, we have this information cached, so accessing it is
-   * almost free.
-   */
-  if (st.st_gid == gid) {
-    mask |= S_IRGRP|S_IWGRP|S_IXGRP;
-
-  } else {
-    if (suppl_gids) {
-      register unsigned int i = 0;
-
-      for (i = 0; i < suppl_gids->nelts; i++) {
-        if (st.st_gid == ((gid_t *) suppl_gids->elts)[i]) {
-          mask |= S_IRGRP|S_IWGRP|S_IXGRP;
-          break;
-        }
-      }
-    }
-  }
-
-  mask &= st.st_mode;
-
-  /* Perform requested access checks. */
-  if (mode & R_OK) {
-    if (!(mask & (S_IRUSR|S_IRGRP|S_IROTH))) {
-      errno = EACCES;
-      return -1;
-    }
-  }
-
-  if (mode & W_OK) {
-    if (!(mask & (S_IWUSR|S_IWGRP|S_IWOTH))) {
-      errno = EACCES;
-      return -1;
-    }
-  }
-
-  if (mode & X_OK) {
-    if (!(mask & (S_IXUSR|S_IXGRP|S_IXOTH))) {
-      errno = EACCES;
-      return -1;
-    }
   }
 
-  /* F_OK already checked by checking the return value of stat. */
-  return 0;
+  return pr_fs_have_access(&st, mode, uid, gid, suppl_gids);
 }
 
 static int sys_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
@@ -442,7 +410,7 @@ static int sys_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
 static int sys_utimes(pr_fs_t *fs, const char *path, struct timeval *tvs) {
   int res;
 
-  if (guard_chroot) {
+  if (fsio_guard_chroot) {
     res = chroot_allow_path(path);
     if (res < 0) {
       return -1;
@@ -473,1877 +441,2376 @@ static int sys_futimes(pr_fh_t *fh, int fd, struct timeval *tvs) {
 #endif
 }
 
-static int sys_chroot(pr_fs_t *fs, const char *path) {
-  if (chroot(path) < 0)
-    return -1;
+static int sys_fsync(pr_fh_t *fh, int fd) {
+  int res;
 
-  session.chroot_path = (char *) path;
-  return 0;
+#ifdef HAVE_FSYNC
+  res = fsync(fd);
+#else
+  errno = ENOSYS;
+  res = -1;
+#endif /* HAVE_FSYNC */
+
+  return res;
 }
 
-static int sys_chdir(pr_fs_t *fs, const char *path) {
-  if (chdir(path) < 0)
-    return -1;
+static ssize_t sys_getxattr(pool *p, pr_fs_t *fs, const char *path,
+    const char *name, void *val, size_t valsz) {
+  ssize_t res;
 
-  pr_fs_setcwd(path);
-  return 0;
-}
+  (void) p;
 
-static void *sys_opendir(pr_fs_t *fs, const char *path) {
-  return opendir(path);
-}
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  res = extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, val, valsz);
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(XATTR_NOFOLLOW)
+  res = getxattr(path, name, val, valsz, 0, 0);
+#  else
+  res = getxattr(path, name, val, valsz);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fs;
+  (void) path;
+  (void) name;
+  (void) val;
+  (void) valsz;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-static int sys_closedir(pr_fs_t *fs, void *dir) {
-  return closedir((DIR *) dir);
+  return res;
 }
 
-static struct dirent *sys_readdir(pr_fs_t *fs, void *dir) {
-  return readdir((DIR *) dir);
+static ssize_t sys_lgetxattr(pool *p, pr_fs_t *fs, const char *path,
+    const char *name, void *val, size_t valsz) {
+  ssize_t res;
+
+  (void) p;
+
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+#  if defined(HAVE_EXTATTR_GET_LINK)
+  res = extattr_get_link(path, EXTATTR_NAMESPACE_USER, name, val, valsz);
+#  else
+  res = extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, val, valsz);
+#  endif /* HAVE_EXTATTR_GET_LINK */
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(HAVE_LGETXATTR)
+  res = lgetxattr(path, name, val, valsz);
+#  elif defined(XATTR_NOFOLLOW)
+  res = getxattr(path, name, val, valsz, 0, XATTR_NOFOLLOW);
+#  else
+  res = getxattr(path, name, val, valsz);
+#  endif /* HAVE_LGETXATTR */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fs;
+  (void) path;
+  (void) name;
+  (void) val;
+  (void) valsz;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
+
+  return res;
 }
 
-static int sys_mkdir(pr_fs_t *fs, const char *path, mode_t mode) {
-  int res;
+static ssize_t sys_fgetxattr(pool *p, pr_fh_t *fh, int fd, const char *name,
+    void *val, size_t valsz) {
+  ssize_t res;
 
-  if (guard_chroot) {
-    res = chroot_allow_path(path);
-    if (res < 0) {
-      return -1;
-    }
-  }
+  (void) p;
+
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  res = extattr_get_fd(fd, EXTATTR_NAMESPACE_USER, name, val, valsz);
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(XATTR_NOFOLLOW)
+  res = fgetxattr(fd, name, val, valsz, 0, 0);
+#  else
+  res = fgetxattr(fd, name, val, valsz);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fh;
+  (void) fd;
+  (void) name;
+  (void) val;
+  (void) valsz;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  res = mkdir(path, mode);
   return res;
 }
 
-static int sys_rmdir(pr_fs_t *fs, const char *path) {
-  int res;
+#ifdef PR_USE_XATTR
+static array_header *parse_xattr_namelist(pool *p, char *namelist, size_t sz) {
+  array_header *names;
+  char *ptr;
 
-  if (guard_chroot) {
-    res = chroot_allow_path(path);
-    if (res < 0) {
-      return -1;
-    }
+  names = make_array(p, 0, sizeof(char *));
+  ptr = namelist;
+
+# if defined(HAVE_SYS_EXTATTR_H)
+  /* BSD style name lists use a one-byte length prefix (limiting xattr names
+   * to a maximum length of 255 bytes), followed by the name, without any
+   * terminating NUL.
+   */
+  while (sz > 0) {
+    unsigned char len;
+
+    pr_signals_handle();
+
+    len = (unsigned char) *ptr;
+    ptr++;
+    sz--;
+
+    *((char **) push_array(names)) = pstrndup(p, ptr, len);
+
+    ptr += len;
+    sz -= len;
   }
 
-  res = rmdir(path);
+# elif defined(HAVE_SYS_XATTR_H)
+  /* Linux/MacOSX style name lists use NUL-terminated xattr names. */
+  while (sz > 0) {
+    char *ptr2;
+    size_t len;
+
+    pr_signals_handle();
+
+    for (ptr2 = ptr; *ptr2; ptr2++);
+    len = ptr2 - ptr;
+    *((char **) push_array(names)) = pstrndup(p, ptr, len);
+
+    ptr = ptr2 + 1;
+    sz -= (len + 1);
+  }
+# endif /* HAVE_SYS_XATTR_H */
+
+  return names;
+}
+
+static ssize_t unix_listxattr(const char *path, char *namelist, size_t len) {
+  ssize_t res;
+
+#if defined(HAVE_SYS_EXTATTR_H)
+  res = extattr_list_file(path, EXTATTR_NAMESPACE_USER, namelist, len);
+#elif defined(HAVE_SYS_XATTR_H)
+# if defined(XATTR_NOFOLLOW)
+  res = listxattr(path, namelist, len, 0);
+# else
+  res = listxattr(path, namelist, len);
+# endif /* XATTR_NOFOLLOW */
+#endif /* HAVE_SYS_XATTR_H */
+
   return res;
 }
 
-static int fs_cmp(const void *a, const void *b) {
-  pr_fs_t *fsa, *fsb;
+static ssize_t unix_llistxattr(const char *path, char *namelist, size_t len) {
+  ssize_t res;
 
-  fsa = *((pr_fs_t **) a);
-  fsb = *((pr_fs_t **) b);
+# if defined(HAVE_SYS_EXTATTR_H)
+#  if defined(HAVE_EXTATTR_LIST_LINK)
+  res = extattr_list_link(path, EXTATTR_NAMESPACE_USER, namelist, len);
+#  else
+  res = extattr_list_file(path, EXTATTR_NAMESPACE_USER, namelist, len);
+#  endif /* HAVE_EXTATTR_LIST_LINK */
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(HAVE_LLISTXATTR)
+  res = llistxattr(path, namelist, len);
+#  elif defined(XATTR_NOFOLLOW)
+  res = listxattr(path, namelist, len, XATTR_NOFOLLOW);
+#  else
+  res = listxattr(path, namelist, len);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
 
-  return strcmp(fsa->fs_path, fsb->fs_path);
+  return res;
 }
 
-/* Statcache stuff */
-typedef struct {
-  char sc_path[PR_TUNABLE_PATH_MAX+1];
-  size_t sc_pathlen;
-  struct stat sc_stat;
-  int sc_errno;
-  int sc_retval;
+static ssize_t unix_flistxattr(int fd, char *namelist, size_t len) {
+  ssize_t res;
 
-} fs_statcache_t;
+# if defined(HAVE_SYS_EXTATTR_H)
+  res = extattr_list_fd(fd, EXTATTR_NAMESPACE_USER, namelist, len);
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(XATTR_NOFOLLOW)
+  res = flistxattr(fd, namelist, len, 0);
+#  else
+  res = flistxattr(fd, namelist, len);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
 
-static fs_statcache_t statcache;
+  return res;
+}
+#endif /* PR_USE_XATTR */
 
-#define fs_cache_lstat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_LSTAT)
-#define fs_cache_stat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_STAT)
+static int sys_listxattr(pool *p, pr_fs_t *fs, const char *path,
+    array_header **names) {
+  ssize_t res;
+  char *namelist = NULL;
+  size_t len = 0;
 
-static int cache_stat(pr_fs_t *fs, const char *path, struct stat *sbuf,
-    unsigned int op) {
-  int res = -1;
-  char pathbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-  int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL;
-  size_t pathlen;
+#ifdef PR_USE_XATTR
+  /* We need to handle the different formats of namelists that listxattr et al
+   * can provide.  On *BSDs, the namelist buffer uses length prefixes and no
+   * terminating NULs; on Linux/Mac, the namelist buffer uses ONLY
+   * NUL-terminated names.
+   *
+   * Thus we ALWAYS provide all the available attribute names, by first
+   * querying for the full namelist buffer size, allocating that out of
+   * given pool, querying for the names (using the buffer), and then parsing
+   * them into an array.
+   */
 
-  /* Sanity checks */
-  if (fs == NULL) {
-    errno = EINVAL;
+  res = unix_listxattr(path, NULL, 0);
+  if (res < 0) {
     return -1;
   }
 
-  if (path == NULL) {
-    errno = ENOENT;
+  len = res;
+  namelist = palloc(p, len);
+
+  res = unix_listxattr(path, namelist, len);
+  if (res < 0) {
     return -1;
   }
 
-  /* Use only absolute path names.  Construct them, if given a relative
-   * path, based on cwd.  This obviates the need for something like
-   * realpath(3), which only introduces more stat system calls.
-   */
-  if (*path != '/') {
-    sstrcat(pathbuf, cwd, sizeof(pathbuf)-1);
+  *names = parse_xattr_namelist(p, namelist, len);
+  if (pr_trace_get_level(trace_channel) >= 15) {
+    register unsigned int i;
+    unsigned int count;
+    const char **attr_names;
 
-    /* If the cwd is "/", we don't need to duplicate the path separator. 
-     * On some systems (e.g. Cygwin), this duplication can cause problems,
-     * as the path may then have different semantics.
-     */
-    if (strncmp(cwd, "/", 2) != 0) {
-      sstrcat(pathbuf, "/", sizeof(pathbuf)-1);
+    count = (*names)->nelts;
+    attr_names = (*names)->elts;
+
+    pr_trace_msg(trace_channel, 15, "listxattr: found %d xattr names for '%s'",
+      count, path);
+    for (i = 0; i < count; i++) {
+      pr_trace_msg(trace_channel, 15, " [%u]: '%s'", i, attr_names[i]);
     }
+  }
 
-    sstrcat(pathbuf, path, sizeof(pathbuf)-1);
+  res = (*names)->nelts;
 
-  } else
-    sstrncpy(pathbuf, path, sizeof(pathbuf)-1);
+#else
+  (void) fs;
+  (void) path;
+  (void) names;
+  (void) namelist;
+  (void) len;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  /* Determine which filesystem function to use, stat() or lstat() */
-  if (op == FSIO_FILE_STAT) {
-    mystat = fs->stat ? fs->stat : sys_stat;
+  return (int) res;
+}
 
-  } else {
-    mystat = fs->lstat ? fs->lstat : sys_lstat;
+static int sys_llistxattr(pool *p, pr_fs_t *fs, const char *path,
+    array_header **names) {
+  ssize_t res;
+  char *namelist = NULL;
+  size_t len = 0;
+
+#ifdef PR_USE_XATTR
+  /* See sys_listxattr for a description of why we use this approach. */
+  res = unix_llistxattr(path, NULL, 0);
+  if (res < 0) {
+    return -1;
   }
 
-  pathlen = strlen(pathbuf);
+  len = res;
+  namelist = palloc(p, len);
 
-  /* Can the last cached stat be used? */
-  if (pathlen == statcache.sc_pathlen &&
-      strncmp(pathbuf, statcache.sc_path, pathlen + 1) == 0) {
+  res = unix_llistxattr(path, namelist, len);
+  if (res < 0) {
+    return -1;
+  }
 
-    /* Update the given struct stat pointer with the cached info */
-    memcpy(sbuf, &statcache.sc_stat, sizeof(struct stat));
+  *names = parse_xattr_namelist(p, namelist, len);
+  if (pr_trace_get_level(trace_channel) >= 15) {
+    register unsigned int i;
+    unsigned int count;
+    const char **attr_names;
 
-    /* Use the cached errno as well */
-    errno = statcache.sc_errno;
+    count = (*names)->nelts;
+    attr_names = (*names)->elts;
 
-    return statcache.sc_retval;
+    pr_trace_msg(trace_channel, 15, "llistxattr: found %d xattr names for '%s'",
+      count, path);
+    for (i = 0; i < count; i++) {
+      pr_trace_msg(trace_channel, 15, " [%u]: '%s'", i, attr_names[i]);
+    }
   }
 
-  res = mystat(fs, pathbuf, sbuf);
+  res = (*names)->nelts;
 
-  /* Update the cache */
-  memset(statcache.sc_path, '\0', sizeof(statcache.sc_path));
-  sstrncpy(statcache.sc_path, pathbuf, sizeof(statcache.sc_path));
-  memcpy(&statcache.sc_stat, sbuf, sizeof(struct stat));
-  statcache.sc_pathlen = pathlen;
-  statcache.sc_errno = errno;
-  statcache.sc_retval = res;
+#else
+  (void) fs;
+  (void) path;
+  (void) names;
+  (void) namelist;
+  (void) len;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  return res;
+  return (int) res;
 }
 
-/* Lookup routines */
+static int sys_flistxattr(pool *p, pr_fh_t *fh, int fd, array_header **names) {
+  ssize_t res;
+  char *namelist = NULL;
+  size_t len = 0;
 
-/* Necessary prototype for static function */
-static pr_fs_t *lookup_file_canon_fs(const char *, char **, int);
+#ifdef PR_USE_XATTR
+  /* See sys_listxattr for a description of why we use this approach. */
+  res = unix_flistxattr(fd, NULL, 0);
+  if (res < 0) {
+    return -1;
+  }
 
-/* lookup_dir_fs() is called when we want to perform some sort of directory
- * operation on a directory or file.  A "closest" match algorithm is used.  If
- * the lookup fails or is not "close enough" (i.e. the final target does not
- * exactly match an existing filesystem handle) scan the list of fs_matches for
- * matchable targets and call any callback functions, then rescan the pr_fs_t
- * list.  The rescan is performed in case any modules registered pr_fs_ts
- * during the hit.
- */
-static pr_fs_t *lookup_dir_fs(const char *path, int op) {
-  char buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-  char tmp_path[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-  pr_fs_t *fs = NULL;
-  int exact = FALSE;
-  size_t tmp_pathlen = 0;
+  len = res;
+  namelist = palloc(p, len);
 
-#ifdef PR_FS_MATCH
-  pr_fs_match_t *fsm = NULL;
-#endif /* PR_FS_MATCH */
+  res = unix_flistxattr(fd, namelist, len);
+  if (res < 0) {
+    return -1;
+  }
 
-  sstrncpy(buf, path, sizeof(buf));
+  *names = parse_xattr_namelist(p, namelist, len);
+  if (pr_trace_get_level(trace_channel) >= 15) {
+    register unsigned int i;
+    unsigned int count;
+    const char **attr_names;
 
-  /* Check if the given path is an absolute path.  Since there may be
-   * alternate fs roots, this is not a simple check.  If the path is
-   * not absolute, prepend the current location.
-   */
-  if (pr_fs_valid_path(path) < 0) {
-    if (pr_fs_dircat(tmp_path, sizeof(tmp_path), cwd, buf) < 0) {
-      return NULL;
+    count = (*names)->nelts;
+    attr_names = (*names)->elts;
+
+    pr_trace_msg(trace_channel, 15, "flistxattr: found %d xattr names for '%s'",
+      count, fh->fh_path);
+    for (i = 0; i < count; i++) {
+      pr_trace_msg(trace_channel, 15, " [%u]: '%s'", i, attr_names[i]);
     }
+  }
 
-  } else {
-    sstrncpy(tmp_path, buf, sizeof(tmp_path));
-  }
+  res = (*names)->nelts;
 
-  /* Make sure that if this is a directory operation, the path being
-   * search ends in a trailing slash -- this is how files and directories
-   * are differentiated in the fs_map.
-   */
-  tmp_pathlen = strlen(tmp_path);
-  if ((FSIO_DIR_COMMON & op) &&
-      tmp_pathlen > 0 &&
-      tmp_pathlen < sizeof(tmp_path) &&
-      tmp_path[tmp_pathlen - 1] != '/') {
-    sstrcat(tmp_path, "/", sizeof(tmp_path));
-  }
+#else
+  (void) fh;
+  (void) fd;
+  (void) names;
+  (void) namelist;
+  (void) len;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  fs = pr_get_fs(tmp_path, &exact);
+  return (int) res;
+}
 
-#ifdef PR_FS_MATCH
-/* NOTE: what if there is a perfect matching pr_fs_t for the given path,
- *  but an fs_match with pattern of "." is registered?  At present, that
- *  fs_match will never trigger...hmmm...OK.  fs_matches are only scanned
- *  if and only if there is *not* an exactly matching pr_fs_t.
- *
- *  NOTE: this is experimental code, not yet ready for module consumption.
- *  It was present in the older FS code, hence it's presence now.
- */
+static int sys_removexattr(pool *p, pr_fs_t *fs, const char *path,
+    const char *name) {
+  int res;
 
-  /* Is the returned pr_fs_t "close enough"? */
-  if (!fs || !exact) {
+  (void) p;
 
-    /* Look for an fs_match */
-    fsm = pr_get_fs_match(tmp_path, op);
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  res = extattr_delete_file(path, EXTATTR_NAMESPACE_USER, name);
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(XATTR_NOFOLLOW)
+  res = removexattr(path, name, 0);
+#  else
+  res = removexattr(path, name);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fs;
+  (void) path;
+  (void) name;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-    while (fsm) {
+  return res;
+}
 
-      /* Invoke the fs_match's callback function, if set
-       *
-       * NOTE: what pr_fs_t is being passed to the trigger??
-       */
-      if (fsm->trigger) {
-        if (fsm->trigger(fs, tmp_path, op) <= 0)
-          pr_log_pri(PR_LOG_DEBUG, "error: fs_match '%s' trigger failed",
-            fsm->name);
-      }
+static int sys_lremovexattr(pool *p, pr_fs_t *fs, const char *path,
+    const char *name) {
+  int res;
 
-      /* Get the next matching fs_match */
-      fsm = pr_get_next_fs_match(fsm, tmp_path, op);
-    }
-  }
+  (void) p;
 
-  /* Now, check for a new pr_fs_t, if any were registered by fs_match
-   * callbacks.  This time, it doesn't matter if it's an exact match --
-   * any pr_fs_t will do.
-   */
-  if (chk_fs_map)
-    fs = pr_get_fs(tmp_path, &exact);
-#endif /* PR_FS_MATCH */
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+#  if defined(HAVE_EXTATTR_DELETE_LINK)
+  res = extattr_delete_link(path, EXTATTR_NAMESPACE_USER, name);
+#  else
+  res = extattr_delete_file(path, EXTATTR_NAMESPACE_USER, name);
+#  endif /* HAVE_EXTATTR_DELETE_LINK */
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(HAVE_LREMOVEXATTR)
+  res = lremovexattr(path, name);
+#  elif defined(XATTR_NOFOLLOW)
+  res = removexattr(path, name, XATTR_NOFOLLOW);
+#  else
+  res = removexattr(path, name);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fs;
+  (void) path;
+  (void) name;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  return (fs ? fs : root_fs);
+  return res;
 }
 
-/* lookup_file_fs() performs the same function as lookup_dir_fs, however
- * because we are performing a file lookup, the target is the subdirectory
- * _containing_ the actual target.  A basic optimization is used here,
- * if the path contains no '/' characters, fs_cwd is returned.
- */
-static pr_fs_t *lookup_file_fs(const char *path, char **deref, int op) {
-
-  if (!strchr(path, '/')) {
-#ifdef PR_FS_MATCH
-    pr_fs_match_t *fsm = NULL;
+static int sys_fremovexattr(pool *p, pr_fh_t *fh, int fd, const char *name) {
+  int res;
 
-    fsm = pr_get_fs_match(path, op);
+  (void) p;
 
-    if (!fsm || fsm->trigger(fs_cwd, path, op) <= 0) {
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  res = extattr_delete_fd(fd, EXTATTR_NAMESPACE_USER, name);
+# elif defined(HAVE_SYS_XATTR_H)
+#  if defined(XATTR_NOFOLLOW)
+  res = fremovexattr(fd, name, 0);
+#  else
+  res = fremovexattr(fd, name);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
 #else
-    if (1) {
-#endif /* PR_FS_MATCH */
-      pr_fs_t *fs = fs_cwd;
-      struct stat sbuf;
-      int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL;
-
-      /* Determine which function to use, stat() or lstat(). */
-      if (op == FSIO_FILE_STAT) {
-        while (fs && fs->fs_next && !fs->stat) {
-          fs = fs->fs_next;
-        }
+  (void) fh;
+  (void) fd;
+  (void) name;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-        mystat = fs->stat;
+  return res;
+}
 
-      } else {
-        while (fs && fs->fs_next && !fs->lstat) {
-          fs = fs->fs_next;
-        }
+#if defined(PR_USE_XATTR) && defined(HAVE_SYS_XATTR_H)
+/* Map the given flags onto the sys/xattr.h flags */
+static int get_setxattr_flags(int fsio_flags) {
+  int xattr_flags = 0;
 
-        mystat = fs->lstat;
-      }
+  /* If both CREATE and REPLACE are set, use a value of zero; per the
+   * man pages, this value gives the desired "create or replace" semantics.
+   * Right?
+   */
 
-      if (mystat(fs, path, &sbuf) == -1 ||
-          !S_ISLNK(sbuf.st_mode)) {
-        return fs;
-      }
+  if (fsio_flags & PR_FSIO_XATTR_FL_CREATE) {
+#if defined(XATTR_CREATE)
+    xattr_flags = XATTR_CREATE;
+#endif /* XATTR_CREATE */
 
-    } else {
+    if (fsio_flags & PR_FSIO_XATTR_FL_REPLACE) {
+      xattr_flags = 0;
+    }
 
-      /* The given path is a symbolic link, in which case we need to find
-       * the actual path referenced, and return an pr_fs_t for _that_ path
-       */
-      char linkbuf[PR_TUNABLE_PATH_MAX + 1];
-      int i;
+  } else if (fsio_flags & PR_FSIO_XATTR_FL_REPLACE) {
+#if defined(XATTR_REPLACE)
+    xattr_flags = XATTR_REPLACE;
+#endif /* XATTR_REPLACE */
+  }
 
-      /* Three characters are reserved at the end of linkbuf for some path
-       * characters (and a trailing NUL).
-       */
-      i = pr_fsio_readlink(path, &linkbuf[2], sizeof(linkbuf)-3);
-      if (i != -1) {
-        linkbuf[i] = '\0';
-        if (strchr(linkbuf, '/') == NULL) {
-          if (i + 3 > PR_TUNABLE_PATH_MAX) {
-            i = PR_TUNABLE_PATH_MAX - 3;
-          }
+  return xattr_flags;
+}
+#endif /* PR_USE_XATTR and <sys/xattr.h> */
 
-          memmove(&linkbuf[2], linkbuf, i + 1);
+static int sys_setxattr(pool *p, pr_fs_t *fs, const char *path,
+    const char *name, void *val, size_t valsz, int flags) {
+  int res, xattr_flags = 0;
 
-          linkbuf[i+2] = '\0';
-          linkbuf[0] = '.';
-          linkbuf[1] = '/';
-          return lookup_file_canon_fs(linkbuf, deref, op);
-        }
-      }
+  (void) p;
 
-      /* What happens if fs_cwd->readlink is NULL, or readlink() returns -1?
-       * I guess, for now, we punt, and return fs_cwd.
-       */
-      return fs_cwd;
-    }
-  }
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  (void) xattr_flags;
+  res = extattr_set_file(path, EXTATTR_NAMESPACE_USER, name, val, valsz);
 
-  return lookup_dir_fs(path, op);
-}
+# elif defined(HAVE_SYS_XATTR_H)
+  xattr_flags = get_setxattr_flags(flags);
 
-static pr_fs_t *lookup_file_canon_fs(const char *path, char **deref, int op) {
-  static char workpath[PR_TUNABLE_PATH_MAX + 1];
+#  if defined(XATTR_NOFOLLOW)
+  res = setxattr(path, name, val, valsz, 0, xattr_flags);
+#  else
+  res = setxattr(path, name, val, valsz, xattr_flags);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fs;
+  (void) path;
+  (void) name;
+  (void) val;
+  (void) valsz;
+  (void) flags;
+  (void) xattr_flags;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  memset(workpath,'\0',sizeof(workpath));
+  return res;
+}
 
-  if (pr_fs_resolve_partial(path, workpath, sizeof(workpath)-1,
-      FSIO_FILE_OPEN) == -1) {
-    if (*path == '/' || *path == '~') {
-      if (pr_fs_interpolate(path, workpath, sizeof(workpath)-1) != -1) {
-        sstrncpy(workpath, path, sizeof(workpath));
-      }
+static int sys_lsetxattr(pool *p, pr_fs_t *fs, const char *path,
+    const char *name, void *val, size_t valsz, int flags) {
+  int res, xattr_flags = 0;
 
-    } else {
-      if (pr_fs_dircat(workpath, sizeof(workpath), cwd, path) < 0) {
-        return NULL;
-      }
-    }
-  }
+  (void) p;
 
-  if (deref) {
-    *deref = workpath;
-  }
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  (void) xattr_flags;
+#  if defined(HAVE_EXTATTR_SET_LINK)
+  res = extattr_set_link(path, EXTATTR_NAMESPACE_USER, name, val, valsz);
+#  else
+  res = extattr_set_file(path, EXTATTR_NAMESPACE_USER, name, val, valsz);
+#  endif /* HAVE_EXTATTR_SET_LINK */
+# elif defined(HAVE_SYS_XATTR_H)
+  xattr_flags = get_setxattr_flags(flags);
+
+#  if defined(HAVE_LSETXATTR)
+  res = lsetxattr(path, name, val, valsz, xattr_flags);
+#  elif defined(XATTR_NOFOLLOW)
+  xattr_flags |= XATTR_NOFOLLOW;
+  res = setxattr(path, name, val, valsz, 0, xattr_flags);
+#  else
+  res = setxattr(path, name, val, valsz, xattr_flags);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fs;
+  (void) path;
+  (void) name;
+  (void) val;
+  (void) valsz;
+  (void) flags;
+  (void) xattr_flags;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
 
-  return lookup_file_fs(workpath, deref, op);
+  return res;
 }
 
-/* FS functions proper */
+static int sys_fsetxattr(pool *p, pr_fh_t *fh, int fd, const char *name,
+    void *val, size_t valsz, int flags) {
+  int res, xattr_flags = 0;
 
-void pr_fs_clear_cache(void) {
-  memset(&statcache, '\0', sizeof(statcache));
-}
+  (void) p;
 
-int pr_fs_copy_file(const char *src, const char *dst) {
-  pr_fh_t *src_fh, *dst_fh;
-  struct stat src_st, dst_st;
-  char *buf;
-  size_t bufsz;
-  int dst_existed = FALSE, res;
+#ifdef PR_USE_XATTR
+# if defined(HAVE_SYS_EXTATTR_H)
+  (void) xattr_flags;
+  res = extattr_set_fd(fd, EXTATTR_NAMESPACE_USER, name, val, valsz);
 
-  if (src == NULL ||
-      dst == NULL) {
-    errno = EINVAL;
+# elif defined(HAVE_SYS_XATTR_H)
+  xattr_flags = get_setxattr_flags(flags);
+
+#  if defined(XATTR_NOFOLLOW)
+  res = fsetxattr(fd, name, val, valsz, 0, xattr_flags);
+#  else
+  res = fsetxattr(fd, name, val, valsz, xattr_flags);
+#  endif /* XATTR_NOFOLLOW */
+# endif /* HAVE_SYS_XATTR_H */
+#else
+  (void) fh;
+  (void) fd;
+  (void) name;
+  (void) val;
+  (void) valsz;
+  (void) flags;
+  (void) xattr_flags;
+  errno = ENOSYS;
+  res = -1;
+#endif /* PR_USE_XATTR */
+
+  return res;
+}
+
+static int sys_chroot(pr_fs_t *fs, const char *path) {
+  if (chroot(path) < 0) {
     return -1;
   }
 
-  /* Use a nonblocking open() for the path; it could be a FIFO, and we don't
-   * want to block forever if the other end of the FIFO is not running.
-   */
-  src_fh = pr_fsio_open(src, O_RDONLY|O_NONBLOCK);
-  if (src_fh == NULL) {
-    int xerrno = errno;
-
-    pr_log_pri(PR_LOG_WARNING, "error opening source file '%s' "
-      "for copying: %s", src, strerror(xerrno));
+  session.chroot_path = (char *) path;
+  return 0;
+}
 
-    errno = xerrno;
+static int sys_chdir(pr_fs_t *fs, const char *path) {
+  if (chdir(path) < 0) {
     return -1;
   }
 
-  pr_fsio_set_block(src_fh);
+  pr_fs_setcwd(path);
+  return 0;
+}
 
-  /* Do not allow copying of directories. open(2) may not fail when
-   * opening the source path, since it is only doing a read-only open,
-   * which does work on directories.
-   */
+static void *sys_opendir(pr_fs_t *fs, const char *path) {
+  return opendir(path);
+}
 
-  /* This should never fail. */
-  (void) pr_fsio_fstat(src_fh, &src_st);
-  if (S_ISDIR(src_st.st_mode)) {
-    int xerrno = EISDIR;
+static int sys_closedir(pr_fs_t *fs, void *dir) {
+  return closedir((DIR *) dir);
+}
 
-    pr_fsio_close(src_fh);
+static struct dirent *sys_readdir(pr_fs_t *fs, void *dir) {
+  return readdir((DIR *) dir);
+}
 
-    pr_log_pri(PR_LOG_WARNING, "warning: cannot copy source '%s': %s", src,
-      strerror(xerrno));
+static int sys_mkdir(pr_fs_t *fs, const char *path, mode_t mode) {
+  int res;
 
-    errno = xerrno;
-    return -1;
+  if (fsio_guard_chroot) {
+    res = chroot_allow_path(path);
+    if (res < 0) {
+      return -1;
+    }
   }
 
-  /* We use stat() here, not lstat(), since open() would follow a symlink
-   * to its target, and what we really want to know here is whether the
-   * ultimate destination file exists or not.
-   */
-  if (pr_fsio_stat(dst, &dst_st) == 0) {
-    dst_existed = TRUE;
-    pr_fs_clear_cache();
+  res = mkdir(path, mode);
+  return res;
+}
+
+static int sys_rmdir(pr_fs_t *fs, const char *path) {
+  int res;
+
+  if (fsio_guard_chroot) {
+    res = chroot_allow_path(path);
+    if (res < 0) {
+      return -1;
+    }
   }
 
-  /* Use a nonblocking open() for the path; it could be a FIFO, and we don't
-   * want to block forever if the other end of the FIFO is not running.
-   */
-  dst_fh = pr_fsio_open(dst, O_WRONLY|O_CREAT|O_NONBLOCK);
-  if (dst_fh == NULL) {
-    int xerrno = errno;
+  res = rmdir(path);
+  return res;
+}
 
-    pr_fsio_close(src_fh);
+static int fs_cmp(const void *a, const void *b) {
+  pr_fs_t *fsa, *fsb;
 
-    pr_log_pri(PR_LOG_WARNING, "error opening destination file '%s' "
-      "for copying: %s", dst, strerror(xerrno));
+  if (a == NULL) {
+    if (b == NULL) {
+      return 0;
+    }
 
-    errno = xerrno;
-    return -1;
-  }
+    return 1;
 
-  pr_fsio_set_block(dst_fh);
+  } else {
+    if (b == NULL) {
+      return -1;
+    }
+  }
+   
+  fsa = *((pr_fs_t **) a);
+  fsb = *((pr_fs_t **) b);
 
-  /* Stat the source file to find its optimal copy block size. */
-  if (pr_fsio_fstat(src_fh, &src_st) < 0) {
-    int xerrno = errno;
+  return strcmp(fsa->fs_path, fsb->fs_path);
+}
 
-    pr_log_pri(PR_LOG_WARNING, "error checking source file '%s' "
-      "for copying: %s", src, strerror(xerrno));
+/* Statcache stuff */
+struct fs_statcache {
+  pool *sc_pool;
+  struct stat sc_stat;
+  int sc_errno;
+  int sc_retval;
+  time_t sc_cached_ts;
+};
 
-    pr_fsio_close(src_fh);
-    pr_fsio_close(dst_fh);
+struct fs_statcache_evict_data {
+  time_t now;
+  time_t max_age;
+  pr_table_t *cache_tab;
+};
 
-    if (!dst_existed) {
-      /* Don't unlink the destination file if it already existed. */
-      pr_fsio_unlink(dst);
-    }
+static const char *statcache_channel = "fs.statcache";
+static pool *statcache_pool = NULL;
+static unsigned int statcache_size = 0;
+static unsigned int statcache_max_age = 0;
+static unsigned int statcache_flags = 0;
 
-    errno = xerrno;
-    return -1;
-  }
+/* We need to maintain two different caches: one for stat(2) data, and one
+ * for lstat(2) data.  For some files (e.g. symlinks), the struct stat data
+ * for the same path will be different for the two system calls.
+ */
+static pr_table_t *stat_statcache_tab = NULL;
+static pr_table_t *lstat_statcache_tab = NULL;
 
-  if (pr_fsio_fstat(dst_fh, &dst_st) == 0) {
+#define fs_cache_lstat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_LSTAT)
+#define fs_cache_stat(f, p, s) cache_stat((f), (p), (s), FSIO_FILE_STAT)
 
-    /* Check to see if the source and destination paths are identical.
-     * We wait until now, rather than simply comparing the path strings
-     * earlier, in order to do stats on the paths and compare things like
-     * file size, mtime, inode, etc.
-     */
+static const struct fs_statcache *fs_statcache_get(pr_table_t *cache_tab,
+    const char *path, size_t path_len, time_t now) {
+  const struct fs_statcache *sc = NULL;
 
-    if (strcmp(src, dst) == 0 &&
-        src_st.st_dev == dst_st.st_dev &&
-        src_st.st_ino == dst_st.st_ino &&
-        src_st.st_size == dst_st.st_size &&
-        src_st.st_mtime == dst_st.st_mtime) {
+  if (pr_table_count(cache_tab) == 0) {
+    errno = EPERM;
+    return NULL;
+  }
 
-      pr_fsio_close(src_fh);
-      pr_fsio_close(dst_fh);
+  sc = pr_table_get(cache_tab, path, NULL);
+  if (sc != NULL) {
+    time_t age;
 
-      /* No need to copy the same file. */
-      return 0;
+    /* If this item hasn't expired yet, return it, otherwise, remove it. */
+    age = now - sc->sc_cached_ts;
+    if (age <= statcache_max_age) {
+      pr_trace_msg(statcache_channel, 19,
+        "using cached entry for '%s' (age %lu %s)", path,
+        (unsigned long) age, age != 1 ? "secs" : "sec");
+      return sc;
     }
-  }
 
-  bufsz = src_st.st_blksize;
-  buf = malloc(bufsz);
-  if (buf == NULL) {
-    pr_log_pri(PR_LOG_ALERT, "Out of memory!");
-    exit(1);
+    pr_trace_msg(statcache_channel, 14,
+      "entry for '%s' expired (age %lu %s > max age %lu), removing", path,
+      (unsigned long) age, age != 1 ? "secs" : "sec",
+      (unsigned long) statcache_max_age);
+    (void) pr_table_remove(cache_tab, path, NULL);
+    destroy_pool(sc->sc_pool);
   }
 
-#ifdef S_ISFIFO
-  if (!S_ISFIFO(dst_st.st_mode)) {
-    /* Make sure the destination file starts with a zero size. */
-    pr_fsio_truncate(dst, 0);
-  }
-#endif
-
-  while ((res = pr_fsio_read(src_fh, buf, bufsz)) > 0) {
-    size_t datalen;
-    off_t offset;
-
-    pr_signals_handle();
+  errno = ENOENT;
+  return NULL;
+}
 
-    /* Be sure to handle short writes. */
-    datalen = res;
-    offset = 0;
+static int fs_statcache_evict_expired(const void *key_data, size_t key_datasz,
+    const void *value_data, size_t value_datasz, void *user_data) {
+  const struct fs_statcache *sc;
+  struct fs_statcache_evict_data *evict_data;
+  time_t age;
+  pr_table_t *cache_tab = NULL;
 
-    while (datalen > 0) {
-      res = pr_fsio_write(dst_fh, buf + offset, datalen);
-      if (res < 0) {
-        int xerrno = errno;
+  sc = value_data;
+  evict_data = user_data;
 
-        if (xerrno == EINTR ||
-            xerrno == EAGAIN) {
-          pr_signals_handle();
-          continue;
-        }
+  cache_tab = evict_data->cache_tab;
+  age = evict_data->now - sc->sc_cached_ts;
+  if (age > evict_data->max_age) {
+    pr_trace_msg(statcache_channel, 14,
+      "entry for '%s' expired (age %lu %s > max age %lu), evicting",
+      (char *) key_data, (unsigned long) age, age != 1 ? "secs" : "sec",
+      (unsigned long) evict_data->max_age);
+    (void) pr_table_kremove(cache_tab, key_data, key_datasz, NULL);
+    destroy_pool(sc->sc_pool);
+  }
 
-        pr_fsio_close(src_fh);
-        pr_fsio_close(dst_fh);
+  return 0;
+}
 
-        if (!dst_existed) {
-          /* Don't unlink the destination file if it already existed. */
-          pr_fsio_unlink(dst);
-        }
+static int fs_statcache_evict(pr_table_t *cache_tab, time_t now) {
+  int res, table_count;
+  struct fs_statcache_evict_data evict_data;
 
-        pr_log_pri(PR_LOG_WARNING, "error copying to '%s': %s", dst,
-          strerror(xerrno));
-        free(buf);
+  /* We try to make room in two passes.  First, evict any item that has
+   * exceeded the maximum age.  After that, if we are still not low enough,
+   * lower the maximum age, and try again.  If not enough room by then, then
+   * we'll try again on the next stat.
+   */
+ 
+  evict_data.now = now; 
+  evict_data.max_age = statcache_max_age;
+  evict_data.cache_tab = cache_tab;
 
-        errno = xerrno;
-        return -1;
-      }
+  res = pr_table_do(cache_tab, fs_statcache_evict_expired, &evict_data,
+    PR_TABLE_DO_FL_ALL);
+  if (res < 0) {
+    pr_trace_msg(statcache_channel, 4,
+      "error evicting expired items: %s", strerror(errno));
+  }
 
-      if (res == datalen) {
-        break;
-      }
+  table_count = pr_table_count(cache_tab);
+  if (table_count < 0 ||
+      (unsigned int) table_count < statcache_size) {
+    return 0;
+  }
 
-      offset += res;
-      datalen -= res;
+  /* Try for a shorter max age. */
+  if (statcache_max_age > 10) {
+    evict_data.max_age = (statcache_max_age - 10);
+    res = pr_table_do(cache_tab, fs_statcache_evict_expired, &evict_data,
+      PR_TABLE_DO_FL_ALL);
+    if (res < 0) {
+      pr_trace_msg(statcache_channel, 4,
+        "error evicting expired items: %s", strerror(errno));
     }
   }
 
-  free(buf);
+  table_count = pr_table_count(cache_tab);
+  if (table_count < 0 ||
+      (unsigned int) table_count < statcache_size) {
+    return 0;
+  }
 
-#if defined(HAVE_POSIX_ACL) && defined(PR_USE_FACL)
-  {
-    /* Copy any ACLs from the source file to the destination file as well. */
-# if defined(HAVE_BSD_POSIX_ACL)
-    acl_t facl, facl_dup = NULL;
-    int have_facl = FALSE, have_dup = FALSE;
+  pr_trace_msg(statcache_channel, 14,
+    "still not enough room in cache (size %d >= max %d)",
+    pr_table_count(cache_tab), statcache_size);
+  errno = EPERM;
+  return -1;
+}
 
-    facl = acl_get_fd(PR_FH_FD(src_fh));
-    if (facl)
-      have_facl = TRUE;
+/* Returns 1 if we successfully added a cache entry, 0 if not, and -1 if
+ * there was an error.
+ */
+static int fs_statcache_add(pr_table_t *cache_tab, const char *path,
+    size_t path_len, struct stat *st, int xerrno, int retval, time_t now) {
+  int res, table_count;
+  pool *sc_pool;
+  struct fs_statcache *sc;
+
+  if (statcache_size == 0 ||
+      statcache_max_age == 0) {
+    /* Caching disabled; nothing to do here. */
+    return 0;
+  }
 
-    if (have_facl)
-        facl_dup = acl_dup(facl);
+  table_count = pr_table_count(cache_tab);
+  if (table_count > 0 &&
+      (unsigned int) table_count >= statcache_size) {
+    /* We've reached capacity, and need to evict some items to make room. */
+    if (fs_statcache_evict(cache_tab, now) < 0) {
+      pr_trace_msg(statcache_channel, 8,
+        "unable to evict enough items from the cache: %s", strerror(errno));
+    }
+  }
 
-    if (facl_dup)
-      have_dup = TRUE;
+  sc_pool = make_sub_pool(statcache_pool);
+  pr_pool_tag(sc_pool, "FS statcache entry pool");
+  sc = pcalloc(sc_pool, sizeof(struct fs_statcache));
+  sc->sc_pool = sc_pool;
+  memcpy(&(sc->sc_stat), st, sizeof(struct stat));
+  sc->sc_errno = xerrno;
+  sc->sc_retval = retval;
+  sc->sc_cached_ts = now;
 
-    if (have_dup &&
-        acl_set_fd(PR_FH_FD(dst_fh), facl_dup) < 0)
-      pr_log_debug(DEBUG3, "error applying ACL to destination file: %s",
-        strerror(errno));
+  res = pr_table_add(cache_tab, pstrndup(sc_pool, path, path_len), sc,
+    sizeof(struct fs_statcache *));
+  if (res < 0) {
+    int tmp_errno = errno;
 
-    if (have_dup)
-      acl_free(facl_dup);
+    if (tmp_errno == EEXIST) {
+      res = 0;
+    }
 
-# elif defined(HAVE_LINUX_POSIX_ACL)
+    destroy_pool(sc->sc_pool);
+    errno = tmp_errno;
+  }
 
-#  if defined(HAVE_PERM_COPY_FD)
-    /* Linux provides the handy perm_copy_fd(3) function in its libacl
-     * library just for this purpose.
+  return (res == 0 ? 1 : res);
+}
+
+static int cache_stat(pr_fs_t *fs, const char *path, struct stat *st,
+    unsigned int op) {
+  int res = -1, retval, xerrno = 0;
+  char cleaned_path[PR_TUNABLE_PATH_MAX+1], pathbuf[PR_TUNABLE_PATH_MAX+1];
+  int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL;
+  size_t path_len;
+  pr_table_t *cache_tab = NULL; 
+  const struct fs_statcache *sc = NULL;
+  time_t now;
+
+  now = time(NULL);
+  memset(cleaned_path, '\0', sizeof(cleaned_path));
+  memset(pathbuf, '\0', sizeof(pathbuf));
+
+  if (fs->non_std_path == FALSE) {
+    /* Use only absolute path names.  Construct them, if given a relative
+     * path, based on cwd.  This obviates the need for something like
+     * realpath(3), which only introduces more stat(2) system calls.
      */
-    if (perm_copy_fd(src, PR_FH_FD(src_fh), dst, PR_FH_FD(dst_fh), NULL) < 0) {
-      pr_log_debug(DEBUG3, "error copying ACL to destination file: %s",
-        strerror(errno));
-    }
+    if (*path != '/') {
+      size_t pathbuf_len;
 
-#  else
-    acl_t src_acl = acl_get_fd(PR_FH_FD(src_fh));
-    if (src_acl == NULL) {
-      pr_log_debug(DEBUG3, "error obtaining ACL for fd %d: %s",
-        PR_FH_FD(src_fh), strerror(errno));
+      sstrcat(pathbuf, cwd, sizeof(pathbuf)-1);
+      pathbuf_len = cwd_len;
 
-    } else {
-      if (acl_set_fd(PR_FH_FD(dst_fh), src_acl) < 0) {
-        pr_log_debug(DEBUG3, "error setting ACL on fd %d: %s",
-          PR_FH_FD(dst_fh), strerror(errno));
+      /* If the cwd is "/", we don't need to duplicate the path separator.
+       * On some systems (e.g. Cygwin), this duplication can cause problems,
+       * as the path may then have different semantics.
+       */
+      if (strncmp(cwd, "/", 2) != 0) {
+        sstrcat(pathbuf + pathbuf_len, "/", sizeof(pathbuf) - pathbuf_len - 1);
+        pathbuf_len++;
+      }
 
-      } else {
-        acl_free(src_acl);
+      /* If the given directory is ".", then we don't need to append it. */
+      if (strncmp(path, ".", 2) != 0) {
+        sstrcat(pathbuf + pathbuf_len, path, sizeof(pathbuf)- pathbuf_len - 1);
       }
+
+    } else {
+      sstrncpy(pathbuf, path, sizeof(pathbuf)-1);
     }
 
-#  endif /* !HAVE_PERM_COPY_FD */
+    pr_fs_clean_path2(pathbuf, cleaned_path, sizeof(cleaned_path)-1, 0);
 
-# elif defined(HAVE_SOLARIS_POSIX_ACL)
-    int nents;
+  } else {
+    sstrncpy(cleaned_path, path, sizeof(cleaned_path)-1);
+  }
 
-    nents = facl(PR_FH_FD(src_fh), GETACLCNT, 0, NULL);
-    if (nents < 0) {
-      pr_log_debug(DEBUG3, "error getting source file ACL count: %s",
-        strerror(errno));
+  /* Determine which filesystem function to use, stat() or lstat() */
+  if (op == FSIO_FILE_STAT) {
+    mystat = fs->stat ? fs->stat : sys_stat;
+    cache_tab = stat_statcache_tab;
 
-    } else {
-      aclent_t *acls;
+  } else {
+    mystat = fs->lstat ? fs->lstat : sys_lstat;
+    cache_tab = lstat_statcache_tab;
+  }
 
-      acls = malloc(sizeof(aclent_t) * nents);
-      if (!acls) { 
-        pr_log_pri(PR_LOG_ALERT, "Out of memory!");
-        exit(1);
-      }
+  path_len = strlen(cleaned_path);
 
-      if (facl(PR_FH_FD(src_fh), GETACL, nents, acls) < 0) {
-        pr_log_debug(DEBUG3, "error getting source file ACLs: %s",
-          strerror(errno));
+  sc = fs_statcache_get(cache_tab, cleaned_path, path_len, now);
+  if (sc != NULL) {
 
-      } else {
-        if (facl(PR_FH_FD(dst_fh), SETACL, nents, acls) < 0) {
-          pr_log_debug(DEBUG3, "error setting dest file ACLs: %s",
-            strerror(errno));
-        }
-      }
+    /* Update the given struct stat pointer with the cached info */
+    memcpy(st, &(sc->sc_stat), sizeof(struct stat));
 
-      free(acls);
-    }
-# endif /* HAVE_SOLARIS_POSIX_ACL && PR_USE_FACL */
+    pr_trace_msg(trace_channel, 18,
+      "using cached stat for %s for path '%s' (retval %d, errno %s)",
+      op == FSIO_FILE_STAT ? "stat()" : "lstat()", path, sc->sc_retval,
+      strerror(sc->sc_errno));
+
+    /* Use the cached errno as well */
+    errno = sc->sc_errno;
+
+    return sc->sc_retval;
   }
-#endif /* HAVE_POSIX_ACL */
 
-  pr_fsio_close(src_fh);
+  pr_trace_msg(trace_channel, 8, "using %s %s for path '%s'",
+    fs->fs_name, op == FSIO_FILE_STAT ? "stat()" : "lstat()", path);
+  retval = mystat(fs, cleaned_path, st);
+  xerrno = errno;
 
-  res = pr_fsio_close(dst_fh);
+  if (retval == 0) {
+    xerrno = 0;
+  }
+
+  /* Update the cache */
+  res = fs_statcache_add(cache_tab, cleaned_path, path_len, st, xerrno, retval,     now);
   if (res < 0) {
-    int xerrno = errno;
+    pr_trace_msg(trace_channel, 8,
+      "error adding cached stat for '%s': %s", cleaned_path, strerror(errno));
 
-    pr_log_pri(PR_LOG_WARNING, "error closing '%s': %s", dst,
-      strerror(xerrno));
+  } else if (res > 0) {
+    pr_trace_msg(trace_channel, 18,
+      "added cached stat for path '%s' (retval %d, errno %s)", path,
+      retval, strerror(xerrno));
+  }
 
+  if (retval < 0) {
     errno = xerrno;
   }
 
-  return res;
+  return retval;
 }
 
-pr_fs_t *pr_register_fs(pool *p, const char *name, const char *path) {
-  pr_fs_t *fs = NULL;
+/* Lookup routines */
 
-  /* Sanity check */
-  if (!p || !name || !path) {
-    errno = EINVAL;
-    return NULL;
-  }
+/* Necessary prototype for static function */
+static pr_fs_t *lookup_file_canon_fs(const char *, char **, int);
 
-  /* Instantiate an pr_fs_t */
-  fs = pr_create_fs(p, name);
-  if (fs != NULL) {
+/* lookup_dir_fs() is called when we want to perform some sort of directory
+ * operation on a directory or file.  A "closest" match algorithm is used.  If
+ * the lookup fails or is not "close enough" (i.e. the final target does not
+ * exactly match an existing filesystem handle) scan the list of fs_matches for
+ * matchable targets and call any callback functions, then rescan the pr_fs_t
+ * list.  The rescan is performed in case any modules registered pr_fs_ts
+ * during the hit.
+ */
+static pr_fs_t *lookup_dir_fs(const char *path, int op) {
+  char buf[PR_TUNABLE_PATH_MAX + 1], tmp_path[PR_TUNABLE_PATH_MAX + 1];
+  pr_fs_t *fs = NULL;
+  int exact = FALSE;
+  size_t tmp_pathlen = 0;
 
-    /* Call pr_insert_fs() from here */
-    if (!pr_insert_fs(fs, path)) {
-      pr_trace_msg(trace_channel, 4, "error inserting FS '%s' at path '%s'",
-        name, path);
+  memset(buf, '\0', sizeof(buf));
+  memset(tmp_path, '\0', sizeof(tmp_path));
+  sstrncpy(buf, path, sizeof(buf));
 
-      destroy_pool(fs->fs_pool);
+  /* Check if the given path is an absolute path.  Since there may be
+   * alternate fs roots, this is not a simple check.  If the path is
+   * not absolute, prepend the current location.
+   */
+  if (pr_fs_valid_path(path) < 0) {
+    if (pr_fs_dircat(tmp_path, sizeof(tmp_path), cwd, buf) < 0) {
       return NULL;
     }
 
-  } else
-    pr_trace_msg(trace_channel, 6, "error creating FS '%s'", name);
+  } else {
+    sstrncpy(tmp_path, buf, sizeof(tmp_path));
+  }
+
+  /* Make sure that if this is a directory operation, the path being
+   * search ends in a trailing slash -- this is how files and directories
+   * are differentiated in the fs_map.
+   */
+  tmp_pathlen = strlen(tmp_path);
+  if ((FSIO_DIR_COMMON & op) &&
+      tmp_pathlen > 0 &&
+      tmp_pathlen < sizeof(tmp_path) &&
+      tmp_path[tmp_pathlen - 1] != '/') {
+    sstrcat(tmp_path, "/", sizeof(tmp_path));
+  }
+
+  fs = pr_get_fs(tmp_path, &exact);
+  if (fs == NULL) {
+    fs = root_fs;
+  }
 
   return fs;
 }
 
-pr_fs_t *pr_create_fs(pool *p, const char *name) {
-  pr_fs_t *fs = NULL;
-  pool *fs_pool = NULL;
+/* lookup_file_fs() performs the same function as lookup_dir_fs, however
+ * because we are performing a file lookup, the target is the subdirectory
+ * _containing_ the actual target.  A basic optimization is used here,
+ * if the path contains no '/' characters, fs_cwd is returned.
+ */
+static pr_fs_t *lookup_file_fs(const char *path, char **deref, int op) {
+  pr_fs_t *fs = fs_cwd;
+  struct stat st;
+  int (*mystat)(pr_fs_t *, const char *, struct stat *) = NULL, res;
+  char linkbuf[PR_TUNABLE_PATH_MAX + 1];
 
-  /* Sanity check */
-  if (!p || !name) {
-    errno = EINVAL;
-    return NULL;
+  if (strchr(path, '/') != NULL) {
+    return lookup_dir_fs(path, op);
   }
 
-  /* Allocate a subpool, then allocate an pr_fs_t object from that subpool */
-  fs_pool = make_sub_pool(p);
-  pr_pool_tag(fs_pool, "FS Pool");
-
-  fs = pcalloc(fs_pool, sizeof(pr_fs_t));
-  if (!fs)
-    return NULL;
-
-  fs->fs_pool = fs_pool;
-  fs->fs_next = fs->fs_prev = NULL;
-  fs->fs_name = pstrdup(fs->fs_pool, name);
-  fs->fs_next = root_fs;
-  fs->allow_xdev_link = TRUE;
-  fs->allow_xdev_rename = TRUE;
+  /* Determine which function to use, stat() or lstat(). */
+  if (op == FSIO_FILE_STAT) {
+    while (fs && fs->fs_next && !fs->stat) {
+      fs = fs->fs_next;
+    }
 
-  /* This is NULL until set by pr_insert_fs() */
-  fs->fs_path = NULL;
+    mystat = fs->stat;
 
-  return fs;
-}
+  } else {
+    while (fs && fs->fs_next && !fs->lstat) {
+      fs = fs->fs_next;
+    }
 
-int pr_insert_fs(pr_fs_t *fs, const char *path) {
-  char cleaned_path[PR_TUNABLE_PATH_MAX] = {'\0'};
+    mystat = fs->lstat;
+  }
 
-  if (!fs_map) {
-    pool *map_pool = make_sub_pool(permanent_pool);
-    pr_pool_tag(map_pool, "FSIO Map Pool");
+  res = mystat(fs, path, &st);
+  if (res < 0) {
+    return fs;
+  }
 
-    fs_map = make_array(map_pool, 0, sizeof(pr_fs_t *));
+  if (!S_ISLNK(st.st_mode)) {
+    return fs;
   }
 
-  /* Clean the path, but only if it starts with a '/'.  Non-local-filesystem
-   * paths may not want/need to be cleaned.
+  /* The given path is a symbolic link, in which case we need to find
+   * the actual path referenced, and return an pr_fs_t for _that_ path
    */
-  if (*path == '/') {
-    pr_fs_clean_path(path, cleaned_path, sizeof(cleaned_path));
 
-    /* Cleaning the path may have removed a trailing slash, which the
-     * caller may actually have wanted.  Make sure one is present in
-     * the cleaned version, if it was present in the original version and
-     * is not present in the cleaned version.
-     */
-    if (path[strlen(path)-1] == '/') {
-      size_t len = strlen(cleaned_path);
-
-      if (len > 1 &&
-          len < (PR_TUNABLE_PATH_MAX-3) &&
-          cleaned_path[len-1] != '/') {
-        cleaned_path[len] = '/';
-        cleaned_path[len+1] = '\0';
-      }
-    }
-
-  } else
-    sstrncpy(cleaned_path, path, sizeof(cleaned_path));
-
-  if (!fs->fs_path)
-    fs->fs_path = pstrdup(fs->fs_pool, cleaned_path);
+  /* Three characters are reserved at the end of linkbuf for some path
+   * characters (and a trailing NUL).
+   */
+  if (fs->readlink != NULL) {
+    res = (fs->readlink)(fs, path, &linkbuf[2], sizeof(linkbuf)-3);
 
-  /* Check for duplicates. */
-  if (fs_map->nelts > 0) {
-    pr_fs_t *fsi = NULL, **fs_objs = (pr_fs_t **) fs_map->elts;
-    register int i;
+  } else {
+    errno = ENOSYS;
+    res = -1;
+  }
 
-    for (i = 0; i < fs_map->nelts; i++) {
-      fsi = fs_objs[i];
+  if (res != -1) {
+    linkbuf[res] = '\0';
 
-      if (strcmp(fsi->fs_path, cleaned_path) == 0) {
-        /* An entry for this path already exists.  Make sure the FS being
-         * mounted is not the same as the one already present.
-         */
-        if (strcmp(fsi->fs_name, fs->fs_name) == 0) {
-          pr_log_pri(PR_LOG_NOTICE,
-            "error: duplicate fs paths not allowed: '%s'", cleaned_path);
-          errno = EEXIST;
-          return FALSE;
-        }
+    if (strchr(linkbuf, '/') == NULL) {
+      if (res + 3 > PR_TUNABLE_PATH_MAX) {
+        res = PR_TUNABLE_PATH_MAX - 3;
+      }
 
-        /* "Push" the given FS on top of the existing one. */
-        fs->fs_next = fsi;
-        fsi->fs_prev = fs;
-        fs_objs[i] = fs;
+      memmove(&linkbuf[2], linkbuf, res + 1);
 
-        chk_fs_map = TRUE;
-        return TRUE;
-      }
+      linkbuf[res+2] = '\0';
+      linkbuf[0] = '.';
+      linkbuf[1] = '/';
+      return lookup_file_canon_fs(linkbuf, deref, op);
     }
   }
 
-  /* Push the new FS into the container, then resort the contents. */
-  *((pr_fs_t **) push_array(fs_map)) = fs;
-
-  /* Sort the FSs in the map according to their paths (if there are
-   * more than one element in the array_header.
+  /* What happens if fs_cwd->readlink is NULL, or readlink() returns -1?
+   * I guess, for now, we punt, and return fs_cwd.
    */
-  if (fs_map->nelts > 1)
-    qsort(fs_map->elts, fs_map->nelts, sizeof(pr_fs_t *), fs_cmp);
+  return fs_cwd;
+}
 
-  /* Set the flag so that the fs wrapper functions know that a new FS
-   * has been registered.
-   */
-  chk_fs_map = TRUE;
+static pr_fs_t *lookup_file_canon_fs(const char *path, char **deref, int op) {
+  static char workpath[PR_TUNABLE_PATH_MAX + 1];
 
-  return TRUE;
-}
+  memset(workpath,'\0',sizeof(workpath));
 
-pr_fs_t *pr_unmount_fs(const char *path, const char *name) {
-  pr_fs_t *fsi = NULL, **fs_objs = NULL;
-  register unsigned int i = 0;
+  if (pr_fs_resolve_partial(path, workpath, sizeof(workpath)-1,
+      FSIO_FILE_OPEN) == -1) {
+    if (*path == '/' || *path == '~') {
+      if (pr_fs_interpolate(path, workpath, sizeof(workpath)-1) != -1) {
+        sstrncpy(workpath, path, sizeof(workpath));
+      }
 
-  /* Sanity check */
-  if (!path) {
-    errno = EINVAL;
-    return NULL;
+    } else {
+      if (pr_fs_dircat(workpath, sizeof(workpath), cwd, path) < 0) {
+        return NULL;
+      }
+    }
   }
 
-  /* This should never be called before pr_register_fs(), but, just in case...*/
-  if (!fs_map) {
-    errno = EACCES;
-    return NULL;
+  if (deref) {
+    *deref = workpath;
   }
 
-  fs_objs = (pr_fs_t **) fs_map->elts;
-
-  for (i = 0; i < fs_map->nelts; i++) {
-    fsi = fs_objs[i];
-
-    if (strcmp(fsi->fs_path, path) == 0 &&
-        (name ? strcmp(fsi->fs_name, name) == 0 : TRUE)) {
+  return lookup_file_fs(workpath, deref, op);
+}
 
-      /* Exact match -- remove this FS.  If there is an FS underneath, pop 
-       * the top FS off the stack.  Otherwise, allocate a new map.  Then
-       * iterate through the old map, pushing all other FSs into the new map.
-       * Destroy the old map.  Move the new map into place.
-       */
+/* FS Statcache API */
 
-      if (fsi->fs_next == NULL) {
-        register unsigned int j = 0;
-        pr_fs_t *tmp_fs, **old_objs = NULL;
-        pool *map_pool;
-        array_header *new_map;
+static void statcache_dumpf(const char *fmt, ...) {
+  char buf[PR_TUNABLE_BUFFER_SIZE];
+  va_list msg;
 
-        /* If removing this FS would leave an empty map, don't bother
-         * allocating a new one.
-         */
-        if (fs_map->nelts == 1) {
-          destroy_pool(fs_map->pool);
-          fs_map = NULL;
-          fs_cwd = root_fs;
+  memset(buf, '\0', sizeof(buf));
 
-          chk_fs_map = TRUE;
-          return NULL;
-        }
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf), fmt, msg);
+  va_end(msg);
 
-        map_pool = make_sub_pool(permanent_pool);
-        new_map = make_array(map_pool, 0, sizeof(pr_fs_t *));
+  buf[sizeof(buf)-1] = '\0';
+  (void) pr_trace_msg(statcache_channel, 9, "%s", buf);
+}
 
-        pr_pool_tag(map_pool, "FSIO Map Pool");
-        old_objs = (pr_fs_t **) fs_map->elts;
+void pr_fs_statcache_dump(void) {
+  pr_table_dump(statcache_dumpf, stat_statcache_tab);
+  pr_table_dump(statcache_dumpf, lstat_statcache_tab);
+}
 
-        for (j = 0; j < fs_map->nelts; j++) {
-          tmp_fs = old_objs[j];
+void pr_fs_statcache_free(void) {
+  if (stat_statcache_tab != NULL) {
+    int size;
 
-          if (strcmp(tmp_fs->fs_path, path) != 0)
-            *((pr_fs_t **) push_array(new_map)) = old_objs[j];
-        }
+    size = pr_table_count(stat_statcache_tab);
+    pr_trace_msg(statcache_channel, 11,
+      "resetting stat(2) statcache (clearing %d %s)", size,
+      size != 1 ? "entries" : "entry");
+    pr_table_empty(stat_statcache_tab);
+    pr_table_free(stat_statcache_tab);
+    stat_statcache_tab = NULL;
+  }
 
-        destroy_pool(fs_map->pool);
-        fs_map = new_map;
+  if (lstat_statcache_tab != NULL) {
+    int size;
 
-        /* Don't forget to set the flag so that wrapper functions scan the
-         * new map.
-         */
-        chk_fs_map = TRUE;
+    size = pr_table_count(lstat_statcache_tab);
+    pr_trace_msg(statcache_channel, 11,
+      "resetting lstat(2) statcache (clearing %d %s)", size,
+      size != 1 ? "entries" : "entry");
+    pr_table_empty(lstat_statcache_tab);
+    pr_table_free(lstat_statcache_tab);
+    lstat_statcache_tab = NULL;
+  }
 
-        return fsi;
-      }
+  /* Note: we do not need to explicitly destroy each entry in the statcache
+   * tables, since ALL entries are allocated out of this statcache_pool.
+   * And we destroy this pool here.  Much easier cleanup that way.
+   */
+  if (statcache_pool != NULL) {
+    destroy_pool(statcache_pool);
+    statcache_pool = NULL;
+  }
+}
 
-      /* "Pop" this FS off the stack. */
-      if (fsi->fs_next)
-        fsi->fs_next->fs_prev = NULL;
-      fs_objs[i] = fsi->fs_next;
-      fsi->fs_next = fsi->fs_prev = NULL; 
+void pr_fs_statcache_reset(void) {
+  pr_fs_statcache_free();
 
-      chk_fs_map = TRUE;
-      return fsi;
-    }
+  if (statcache_pool == NULL) {
+    statcache_pool = make_sub_pool(permanent_pool);
+    pr_pool_tag(statcache_pool, "FS Statcache Pool");
   }
 
-  return NULL;
+  stat_statcache_tab = pr_table_alloc(statcache_pool, 0);
+  lstat_statcache_tab = pr_table_alloc(statcache_pool, 0);
 }
 
-pr_fs_t *pr_remove_fs(const char *path) {
-  return pr_unmount_fs(path, NULL);
+int pr_fs_statcache_set_policy(unsigned int size, unsigned int max_age, 
+    unsigned int flags) {
+
+  statcache_size = size;
+  statcache_max_age = max_age;
+  statcache_flags = flags;
+
+  return 0;
 }
 
-int pr_unregister_fs(const char *path) {
-  pr_fs_t *fs = NULL;
+int pr_fs_clear_cache2(const char *path) {
+  int res;
 
-  if (!path) {
-    errno = EINVAL;
-    return -1;
-  }
+  (void) pr_event_generate("fs.statcache.clear", path);
 
-  /* Call pr_remove_fs() to get the fs for this path removed from the
-   * fs_map.
-   */
-  fs = pr_remove_fs(path);
-  if (fs) {
-    destroy_pool(fs->fs_pool);
+  if (pr_table_count(stat_statcache_tab) == 0 &&
+      pr_table_count(lstat_statcache_tab) == 0) {
     return 0;
   }
 
-  errno = ENOENT;
-  return -1;
-}
-
-/* This function returns the best pr_fs_t to handle the given path.  It will
- * return NULL if there are no registered pr_fs_ts to handle the given path,
- * in which case the default root_fs should be used.  This is so that
- * functions can look to see if an pr_fs_t, other than the default, for a
- * given path has been registered, if necessary.  If the return value is
- * non-NULL, that will be a registered pr_fs_t to handle the given path.  In
- * this case, if the exact argument is not NULL, it will either be TRUE,
- * signifying that the returned pr_fs_t is an exact match for the given
- * path, or FALSE, meaning the returned pr_fs_t is a "best match" -- most
- * likely the pr_fs_t that handles the directory in which the given path
- * occurs.
- */
-pr_fs_t *pr_get_fs(const char *path, int *exact) {
-  pr_fs_t *fs = NULL, **fs_objs = NULL, *best_match_fs = NULL;
-  register unsigned int i = 0;
-
-  /* Sanity check */
-  if (!path) {
-    errno = EINVAL;
-    return NULL;
-  }
+  if (path != NULL) {
+    char cleaned_path[PR_TUNABLE_PATH_MAX+1], pathbuf[PR_TUNABLE_PATH_MAX+1];
+    int lstat_count, stat_count;
 
-  /* Basic optimization -- if there're no elements in the fs_map,
-   * return the root_fs.
-   */
-  if (!fs_map ||
-      fs_map->nelts == 0) {
-    return root_fs;
-  }
+    if (*path != '/') {
+      size_t pathbuf_len;
 
-  fs_objs = (pr_fs_t **) fs_map->elts;
-  best_match_fs = root_fs;
+      memset(cleaned_path, '\0', sizeof(cleaned_path));
+      memset(pathbuf, '\0', sizeof(pathbuf));
 
-  /* In order to handle deferred-resolution paths (eg "~" paths), the given
-   * path will need to be passed through dir_realpath(), if necessary.
-   *
-   * The chk_fs_map flag, if TRUE, should be cleared on return of this
-   * function -- all that flag says is, if TRUE, that this function _might_
-   * return something different than it did on a previous call.
-   */
+      sstrcat(pathbuf, cwd, sizeof(pathbuf)-1);
+      pathbuf_len = cwd_len;
 
-  for (i = 0; i < fs_map->nelts; i++) {
-    int res = 0;
+      if (strncmp(cwd, "/", 2) != 0) {
+        sstrcat(pathbuf + pathbuf_len, "/", sizeof(pathbuf) - pathbuf_len - 1);
+        pathbuf_len++;
+      }
 
-    fs = fs_objs[i];
+      if (strncmp(path, ".", 2) != 0) {
+        sstrcat(pathbuf + pathbuf_len, path, sizeof(pathbuf)- pathbuf_len - 1);
+      }
 
-    /* If the current pr_fs_t's path ends in a slash (meaning it is a
-     * directory, and it matches the first part of the given path,
-     * assume it to be the best pr_fs_t found so far.
-     */
-    if ((fs->fs_path)[strlen(fs->fs_path) - 1] == '/' &&
-        !strncmp(path, fs->fs_path, strlen(fs->fs_path)))
-      best_match_fs = fs;
+    } else {
+      sstrncpy(pathbuf, path, sizeof(pathbuf)-1);
+    }
 
-    res = strcmp(fs->fs_path, path);
+    pr_fs_clean_path2(pathbuf, cleaned_path, sizeof(cleaned_path)-1, 0);
 
-    if (res == 0) {
+    res = 0;
 
-      /* Exact match */
-      if (exact)
-        *exact = TRUE;
+    stat_count = pr_table_exists(stat_statcache_tab, cleaned_path);
+    if (stat_count > 0) {
+      const struct fs_statcache *sc;
 
-      chk_fs_map = FALSE;
-      return fs;
+      sc = pr_table_remove(stat_statcache_tab, cleaned_path, NULL);
+      if (sc != NULL) {
+        destroy_pool(sc->sc_pool);
+      }
 
-    } else if (res > 0) {
+      pr_trace_msg(statcache_channel, 17, "cleared stat(2) entry for '%s'",
+        path);
+      res += stat_count;
+    }
 
-      if (exact)
-        *exact = FALSE;
+    lstat_count = pr_table_exists(lstat_statcache_tab, cleaned_path);
+    if (lstat_count > 0) {
+      const struct fs_statcache *sc;
 
-      chk_fs_map = FALSE;
+      sc = pr_table_remove(lstat_statcache_tab, cleaned_path, NULL);
+      if (sc != NULL) {
+        destroy_pool(sc->sc_pool);
+      }
 
-      /* Gone too far - return the best-match pr_fs_t */
-      return best_match_fs;
+      pr_trace_msg(statcache_channel, 17, "cleared lstat(2) entry for '%s'",
+        path);
+      res += lstat_count;
     }
-  }
 
-  chk_fs_map = FALSE;
+  } else {
+    /* Caller is requesting that we empty the entire cache. */
+    pr_fs_statcache_reset();
+    res = 0;
+  }
 
-  /* Return best-match by default */
-  return best_match_fs;
+  return res;
 }
 
-#if defined(PR_USE_REGEX) && defined(PR_FS_MATCH)
-void pr_associate_fs(pr_fs_match_t *fsm, pr_fs_t *fs) {
-  *((pr_fs_t **) push_array(fsm->fsm_fs_objs)) = fs;
+void pr_fs_clear_cache(void) {
+  (void) pr_fs_clear_cache2(NULL);
 }
 
-pr_fs_match_t *pr_create_fs_match(pool *p, const char *name,
-    const char *pattern, int opmask) {
-  pr_fs_match_t *fsm = NULL;
-  pool *match_pool = NULL;
-  regex_t *regexp = NULL;
-  int res = 0;
-  char regerr[80] = {'\0'};
+/* FS functions proper */
+
+int pr_fs_copy_file2(const char *src, const char *dst, int flags,
+    void (*progress_cb)(int)) {
+  pr_fh_t *src_fh, *dst_fh;
+  struct stat src_st, dst_st;
+  char *buf;
+  size_t bufsz;
+  int dst_existed = FALSE, res;
+#ifdef PR_USE_XATTR
+  array_header *xattrs = NULL;
+#endif /* PR_USE_XATTR */
 
-  if (!p || !name || !pattern) {
+  if (src == NULL ||
+      dst == NULL) {
     errno = EINVAL;
-    return NULL;
+    return -1;
   }
 
-  match_pool = make_sub_pool(p);
-  fsm = (pr_fs_match_t *) pcalloc(match_pool, sizeof(pr_fs_match_t));
+  copy_iter_count = 0;
 
-  if (!fsm)
-    return NULL;
+  /* Use a nonblocking open() for the path; it could be a FIFO, and we don't
+   * want to block forever if the other end of the FIFO is not running.
+   */
+  src_fh = pr_fsio_open(src, O_RDONLY|O_NONBLOCK);
+  if (src_fh == NULL) {
+    int xerrno = errno;
 
-  fsm->fsm_next = NULL;
-  fsm->fsm_prev = NULL;
+    pr_log_pri(PR_LOG_WARNING, "error opening source file '%s' "
+      "for copying: %s", src, strerror(xerrno));
 
-  fsm->fsm_pool = match_pool;
-  fsm->fsm_name = pstrdup(fsm->fsm_pool, name);
-  fsm->fsm_opmask = opmask;
-  fsm->fsm_pattern = pstrdup(fsm->fsm_pool, pattern);
+    errno = xerrno;
+    return -1;
+  }
 
-  regexp = pr_regexp_alloc();
+  /* Do not allow copying of directories. open(2) may not fail when
+   * opening the source path, since it is only doing a read-only open,
+   * which does work on directories.
+   */
 
-  res = pr_regexp_compile(regexp, pattern, REG_EXTENDED|REG_NOSUB);
-  if (res != 0) {
-    pr_regexp_error(res, regexp, regerr, sizeof(regerr));
-    pr_regexp_free(regexp);
+  /* This should never fail. */
+  if (pr_fsio_fstat(src_fh, &src_st) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error fstat'ing '%s': %s", src, strerror(errno));
+  }
 
-    pr_log_pri(PR_LOG_WARNING, "unable to compile regex '%s': %s", pattern,
-      regerr);
+  if (S_ISDIR(src_st.st_mode)) {
+    int xerrno = EISDIR;
 
-    /* Destroy the just allocated pr_fs_match_t */
-    destroy_pool(fsm->fsm_pool);
+    pr_fsio_close(src_fh);
 
-    return NULL;
+    pr_log_pri(PR_LOG_WARNING, "warning: cannot copy source '%s': %s", src,
+      strerror(xerrno));
 
-  } else
-    fsm->fsm_regex = regexp;
+    errno = xerrno;
+    return -1;
+  }
 
-  /* All pr_fs_match_ts start out as null patterns, i.e. no defined callback.
-   */
-  fsm->trigger = NULL;
+  if (pr_fsio_set_block(src_fh) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error putting '%s' into blocking mode: %s", src, strerror(errno));
+  }
 
-  /* Allocate an array_header, used to record the pointers of any pr_fs_ts
-   * this pr_fs_match_t may register.  This array_header should be accessed
-   * via associate_fs().
+  /* We use stat() here, not lstat(), since open() would follow a symlink
+   * to its target, and what we really want to know here is whether the
+   * ultimate destination file exists or not.
    */
-  fsm->fsm_fs_objs = make_array(fsm->fsm_pool, 0, sizeof(pr_fs_t *));
+  pr_fs_clear_cache2(dst);
+  if (pr_fsio_stat(dst, &dst_st) == 0) {
+    if (S_ISDIR(dst_st.st_mode)) {
+      int xerrno = EISDIR;
 
-  return fsm;
-}
+      (void) pr_fsio_close(src_fh);
 
-int pr_insert_fs_match(pr_fs_match_t *fsm) {
-  pr_fs_match_t *fsmi = NULL;
+      pr_log_pri(PR_LOG_WARNING,
+        "warning: cannot copy to destination '%s': %s", dst, strerror(xerrno));
 
-  if (fs_match_list) {
+      errno = xerrno;
+      return -1;
+    }
 
-    /* Find the end of the fs_match list */
-    fsmi = fs_match_list;
+    dst_existed = TRUE;
+    pr_fs_clear_cache2(dst);
+  }
 
-    /* Prevent pr_fs_match_ts with duplicate names */
-    if (strcmp(fsmi->fsm_name, fsm->fsm_name) == 0) {
-      pr_log_pri(PR_LOG_DEBUG,
-        "error: duplicate fs_match names not allowed: '%s'", fsm->fsm_name);
-      return FALSE;
-    }
+  /* Use a nonblocking open() for the path; it could be a FIFO, and we don't
+   * want to block forever if the other end of the FIFO is not running.
+   */
+  dst_fh = pr_fsio_open(dst, O_WRONLY|O_CREAT|O_NONBLOCK);
+  if (dst_fh == NULL) {
+    int xerrno = errno;
 
-    while (fsmi->fsm_next) {
-      fsmi = fsmi->fsm_next;
+    (void) pr_fsio_close(src_fh);
 
-      if (strcmp(fsmi->fsm_name, fsm->fsm_name) == 0) {
-        pr_log_pri(PR_LOG_DEBUG,
-          "error: duplicate fs_match names not allowed: '%s'", fsm->fsm_name);
-        return FALSE;
-      }
-    }
+    pr_log_pri(PR_LOG_WARNING, "error opening destination file '%s' "
+      "for copying: %s", dst, strerror(xerrno));
 
-    fsm->fsm_next = NULL;
-    fsm->fsm_prev = fsmi;
-    fsmi->fsm_next = fsm;
+    errno = xerrno;
+    return -1;
+  }
 
-  } else
+  if (pr_fsio_set_block(dst_fh) < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "error putting '%s' into blocking mode: %s", dst, strerror(errno));
+  }
 
-    /* This fs_match _becomes_ the start of the fs_match list */
-    fs_match_list = fsm;
+  /* Stat the source file to find its optimal copy block size. */
+  if (pr_fsio_fstat(src_fh, &src_st) < 0) {
+    int xerrno = errno;
 
-  return TRUE;
-}
+    pr_log_pri(PR_LOG_WARNING, "error checking source file '%s' "
+      "for copying: %s", src, strerror(xerrno));
 
-pr_fs_match_t *pr_register_fs_match(pool *p, const char *name,
-    const char *pattern, int opmask) {
-  pr_fs_match_t *fsm = NULL;
+    (void) pr_fsio_close(src_fh);
+    (void) pr_fsio_close(dst_fh);
 
-  /* Sanity check */
-  if (!p || !name || !pattern) {
-    errno = EINVAL;
-    return NULL;
+    /* Don't unlink the destination file if it already existed. */
+    if (!dst_existed) {
+      if (!(flags & PR_FSIO_COPY_FILE_FL_NO_DELETE_ON_FAILURE)) {
+        if (pr_fsio_unlink(dst) < 0) {
+          pr_trace_msg(trace_channel, 12,
+            "error deleting failed copy of '%s': %s", dst, strerror(errno));
+        }
+      }
+    }
+
+    errno = xerrno;
+    return -1;
   }
 
-  /* Instantiate an fs_match */
-  if ((fsm = pr_create_fs_match(p, name, pattern, opmask)) != NULL) {
+  if (pr_fsio_fstat(dst_fh, &dst_st) == 0) {
+
+    /* Check to see if the source and destination paths are identical.
+     * We wait until now, rather than simply comparing the path strings
+     * earlier, in order to do stats on the paths and compare things like
+     * file size, mtime, inode, etc.
+     */
+
+    if (strcmp(src, dst) == 0 &&
+        src_st.st_dev == dst_st.st_dev &&
+        src_st.st_ino == dst_st.st_ino &&
+        src_st.st_size == dst_st.st_size &&
+        src_st.st_mtime == dst_st.st_mtime) {
 
-    /* Insert the fs_match into the list */
-    if (!pr_insert_fs_match(fsm)) {
-      pr_regexp_free(fsm->fsm_regex);
-      destroy_pool(fsm->fsm_pool);
+      (void) pr_fsio_close(src_fh);
+      (void) pr_fsio_close(dst_fh);
 
-      return NULL;
+      /* No need to copy the same file. */
+      return 0;
     }
   }
 
-  return fsm;
-}
+  bufsz = src_st.st_blksize;
+  buf = malloc(bufsz);
+  if (buf == NULL) {
+    pr_log_pri(PR_LOG_ALERT, "Out of memory!");
+    exit(1);
+  }
 
-int pr_unregister_fs_match(const char *name) {
-  pr_fs_match_t *fsm = NULL;
-  pr_fs_t **assoc_fs_objs = NULL, *assoc_fs = NULL;
-  int removed = FALSE;
+#ifdef S_ISFIFO
+  if (!S_ISFIFO(dst_st.st_mode)) {
+    /* Make sure the destination file starts with a zero size. */
+    pr_fsio_truncate(dst, 0);
+  }
+#endif
 
-  /* fs_matches are required to have duplicate names, so using the name as
-   * the identifier will work.
-   */
+  while ((res = pr_fsio_read(src_fh, buf, bufsz)) > 0) {
+    size_t datalen;
+    off_t offset;
 
-  /* Sanity check*/
-  if (!name) {
-    errno = EINVAL;
-    return FALSE;
-  }
+    pr_signals_handle();
 
-  if (fs_match_list) {
-    for (fsm = fs_match_list; fsm; fsm = fsm->fsm_next) {
+    /* Be sure to handle short writes. */
+    datalen = res;
+    offset = 0;
 
-      /* Search by name */
-      if ((name && fsm->fsm_name && strcmp(fsm->fsm_name, name) == 0)) {
+    while (datalen > 0) {
+      res = pr_fsio_write(dst_fh, buf + offset, datalen);
+      if (res < 0) {
+        int xerrno = errno;
 
-        /* Remove this fs_match from the list */
-        if (fsm->fsm_prev)
-          fsm->fsm_prev->fsm_next = fsm->fsm_next;
+        if (xerrno == EINTR ||
+            xerrno == EAGAIN) {
+          pr_signals_handle();
+          continue;
+        }
 
-        if (fsm->fsm_next)
-          fsm->fsm_next->fsm_prev = fsm->fsm_prev;
+        (void) pr_fsio_close(src_fh);
+        (void) pr_fsio_close(dst_fh);
 
-        /* Check for any pr_fs_ts this pattern may have registered, and
-         * remove them as well.
-         */
-        assoc_fs_objs = (pr_fs_t **) fsm->fsm_fs_objs->elts;
+        /* Don't unlink the destination file if it already existed. */
+        if (!dst_existed) {
+          if (!(flags & PR_FSIO_COPY_FILE_FL_NO_DELETE_ON_FAILURE)) {
+            if (pr_fsio_unlink(dst) < 0) {
+              pr_trace_msg(trace_channel, 12,
+                "error deleting failed copy of '%s': %s", dst, strerror(errno));
+            }
+          }
+        }
 
-        for (assoc_fs = *assoc_fs_objs; assoc_fs; assoc_fs++)
-          pr_unregister_fs(assoc_fs->fs_path);
+        pr_log_pri(PR_LOG_WARNING, "error copying to '%s': %s", dst,
+          strerror(xerrno));
+        free(buf);
 
-        pr_regexp_free(fsm->fsm_regex);
-        destroy_pool(fsm->fsm_pool);
+        errno = xerrno;
+        return -1;
+      }
 
-        /* If this fs_match's prev and next pointers are NULL, it is the
-         * last fs_match in the list.  If this is the case, make sure
-         * that fs_match_list is set to NULL, signalling that there are
-         * no more registered fs_matches.
-         */
-        if (fsm->fsm_prev == NULL && fsm->fsm_next == NULL) {
-          fs_match_list = NULL;
-          fsm = NULL;
-        }
+      if (progress_cb != NULL) {
+        (progress_cb)(res);
 
-        removed = TRUE;
+      } else {
+        copy_progress_cb(res);
+      }
+
+      if ((size_t) res == datalen) {
+        break;
       }
+
+      offset += res;
+      datalen -= res;
     }
   }
 
-  return (removed ? TRUE : FALSE);
-}
+  free(buf);
 
-pr_fs_match_t *pr_get_next_fs_match(pr_fs_match_t *fsm, const char *path,
-    int op) {
-  pr_fs_match_t *fsmi = NULL;
+#if defined(HAVE_POSIX_ACL) && defined(PR_USE_FACL)
+  {
+    /* Copy any ACLs from the source file to the destination file as well. */
+# if defined(HAVE_BSD_POSIX_ACL)
+    acl_t facl, facl_dup = NULL;
+    int have_facl = FALSE, have_dup = FALSE;
 
-  /* Sanity check */
-  if (!fsm) {
-    errno = EINVAL;
-    return NULL;
-  }
+    facl = acl_get_fd(PR_FH_FD(src_fh));
+    if (facl) {
+      have_facl = TRUE;
+    }
 
-  for (fsmi = fsm->fsm_next; fsmi; fsmi = fsmi->fsm_next) {
-    if ((fsmi->fsm_opmask & op) &&
-        pr_regexp_exec(fsmi->fsm_regex, path, 0, NULL, 0) == 0)
-      return fsmi;
-  }
+    if (have_facl) {
+      facl_dup = acl_dup(facl);
+    }
 
-  return NULL;
-}
+    if (facl_dup) {
+      have_dup = TRUE;
+    }
 
-pr_fs_match_t *pr_get_fs_match(const char *path, int op) {
-  pr_fs_match_t *fsm = NULL;
+    if (have_dup &&
+        acl_set_fd(PR_FH_FD(dst_fh), facl_dup) < 0) {
+      pr_log_debug(DEBUG3, "error applying ACL to destination file: %s",
+        strerror(errno));
+    }
 
-  if (!fs_match_list)
-    return NULL;
+    if (have_dup) {
+      acl_free(facl_dup);
+    }
+# elif defined(HAVE_LINUX_POSIX_ACL)
 
-  /* Check the first element in the fs_match_list... */
-  fsm = fs_match_list;
+#  if defined(HAVE_PERM_COPY_FD)
+    /* Linux provides the handy perm_copy_fd(3) function in its libacl
+     * library just for this purpose.
+     */
+    if (perm_copy_fd(src, PR_FH_FD(src_fh), dst, PR_FH_FD(dst_fh), NULL) < 0) {
+      pr_log_debug(DEBUG3, "error copying ACL to destination file: %s",
+        strerror(errno));
+    }
 
-  if ((fsm->fsm_opmask & op) &&
-      pr_regexp_exec(fsm->fsm_regex, path, 0, NULL, 0) == 0)
-    return fsm;
+#  else
+    acl_t src_acl = acl_get_fd(PR_FH_FD(src_fh));
+    if (src_acl == NULL) {
+      pr_log_debug(DEBUG3, "error obtaining ACL for fd %d: %s",
+        PR_FH_FD(src_fh), strerror(errno));
 
-  /* ...otherwise, hand the search off to pr_get_next_fs_match() */
-  return pr_get_next_fs_match(fsm, path, op);
-}
-#endif /* PR_USE_REGEX and PR_FS_MATCH */
+    } else {
+      if (acl_set_fd(PR_FH_FD(dst_fh), src_acl) < 0) {
+        pr_log_debug(DEBUG3, "error setting ACL on fd %d: %s",
+          PR_FH_FD(dst_fh), strerror(errno));
 
-int pr_fs_setcwd(const char *dir) {
-  if (pr_fs_resolve_path(dir, cwd, sizeof(cwd)-1, FSIO_DIR_CHDIR) < 0) {
-    return -1;
-  }
+      } else {
+        acl_free(src_acl);
+      }
+    }
 
-  if (sstrncpy(cwd, dir, sizeof(cwd)) < 0) {
-    return -1;
-  }
+#  endif /* !HAVE_PERM_COPY_FD */
 
-  fs_cwd = lookup_dir_fs(cwd, FSIO_DIR_CHDIR);
-  cwd[sizeof(cwd) - 1] = '\0';
+# elif defined(HAVE_SOLARIS_POSIX_ACL)
+    int nents;
 
-  return 0;
-}
+    nents = facl(PR_FH_FD(src_fh), GETACLCNT, 0, NULL);
+    if (nents < 0) {
+      pr_log_debug(DEBUG3, "error getting source file ACL count: %s",
+        strerror(errno));
 
-const char *pr_fs_getcwd(void) {
-  return cwd;
-}
+    } else {
+      aclent_t *acls;
 
-const char *pr_fs_getvwd(void) {
-  return vwd;
-}
+      acls = malloc(sizeof(aclent_t) * nents);
+      if (!acls) { 
+        pr_log_pri(PR_LOG_ALERT, "Out of memory!");
+        exit(1);
+      }
 
-int pr_fs_dircat(char *buf, int buflen, const char *dir1, const char *dir2) {
-  /* Make temporary copies so that memory areas can overlap */
-  char *_dir1 = NULL, *_dir2 = NULL, *ptr = NULL;
-  size_t dir1len = 0, dir2len = 0;
+      if (facl(PR_FH_FD(src_fh), GETACL, nents, acls) < 0) {
+        pr_log_debug(DEBUG3, "error getting source file ACLs: %s",
+          strerror(errno));
 
-  /* The shortest possible path is "/", which requires 2 bytes. */
+      } else {
+        if (facl(PR_FH_FD(dst_fh), SETACL, nents, acls) < 0) {
+          pr_log_debug(DEBUG3, "error setting dest file ACLs: %s",
+            strerror(errno));
+        }
+      }
 
-  if (buf == NULL ||
-      buflen < 2 ||
-      dir1 == NULL ||
-      dir2 == NULL) {
-    errno = EINVAL;
-    return -1;
+      free(acls);
+    }
+# endif /* HAVE_SOLARIS_POSIX_ACL && PR_USE_FACL */
   }
+#endif /* HAVE_POSIX_ACL */
 
-  /* This is a test to see if we've got reasonable directories to concatenate.
+#ifdef PR_USE_XATTR
+  /* Copy any xattrs that the source file may have. We'll use the
+   * destination file handle's pool for our xattr allocations.
    */
-  dir1len = strlen(dir1);
-  dir2len = strlen(dir2);
+  if (pr_fsio_flistxattr(dst_fh->fh_pool, src_fh, &xattrs) > 0) {
+    register unsigned int i;
+    const char **names;
 
-  /* If both strings are empty, then the "concatenation" becomes trivial. */
-  if (dir1len == 0 &&
-      dir2len == 0) {
-    buf[0] = '/';
-    buf[1] = '\0';
-    return 0;
-  }
+    names = xattrs->elts;
+    for (i = 0; xattrs->nelts; i++) {
+      ssize_t valsz;
 
-  /* If dir2 is non-empty, but dir1 IS empty... */
-  if (dir1len == 0) {
-    sstrncpy(buf, dir2, buflen);
-    buflen -= dir2len;
-    sstrcat(buf, "/", buflen);
-    return 0;
+      /* First, find out how much memory we need for this attribute's
+       * value.
+       */
+      valsz = pr_fsio_fgetxattr(dst_fh->fh_pool, src_fh, names[i], NULL, 0);
+      if (valsz > 0) {
+        void *val;
+        ssize_t sz;
+
+        val = palloc(dst_fh->fh_pool, valsz);
+        sz = pr_fsio_fgetxattr(dst_fh->fh_pool, src_fh, names[i], val, valsz);
+        if (sz > 0) {
+          sz = pr_fsio_fsetxattr(dst_fh->fh_pool, dst_fh, names[i], val,
+            valsz, 0);
+          if (sz < 0 &&
+              errno != ENOSYS) {
+            pr_trace_msg(trace_channel, 7,
+              "error copying xattr '%s' (%lu bytes) from '%s' to '%s': %s",
+              names[i], (unsigned long) valsz, src, dst, strerror(errno));
+          }
+        }
+      }
+    }
   }
+#endif /* PR_USE_XATTR */
 
-  /* Likewise, if dir1 is non-empty, but dir2 IS empty... */
-  if (dir2len == 0) {
-    sstrncpy(buf, dir1, buflen);
-    buflen -= dir1len;
-    sstrcat(buf, "/", buflen);
-    return 0;
-  }
+  (void) pr_fsio_close(src_fh);
 
-  if ((dir1len + dir2len + 1) >= PR_TUNABLE_PATH_MAX) {
-    errno = ENAMETOOLONG;
-    buf[0] = '\0';  
-    return -1;
-  }
+  if (progress_cb != NULL) {
+    (progress_cb)(0);
 
-  _dir1 = strdup(dir1);
-  if (_dir1 == NULL) {
-    return -1;
+  } else {
+    copy_progress_cb(0);
   }
 
-  _dir2 = strdup(dir2);
-  if (_dir2 == NULL) {
+  res = pr_fsio_close(dst_fh);
+  if (res < 0) {
     int xerrno = errno;
 
-    free(_dir1);
+    /* Don't unlink the destination file if it already existed. */
+    if (!dst_existed) {
+      if (!(flags & PR_FSIO_COPY_FILE_FL_NO_DELETE_ON_FAILURE)) {
+        if (pr_fsio_unlink(dst) < 0) {
+          pr_trace_msg(trace_channel, 12,
+            "error deleting failed copy of '%s': %s", dst, strerror(errno));
+        }
+      }
+    }
 
-    errno = xerrno;
-    return -1;
-  }
+    pr_log_pri(PR_LOG_WARNING, "error closing '%s': %s", dst,
+      strerror(xerrno));
 
-  if (*_dir2 == '/') {
-    sstrncpy(buf, _dir2, buflen);
-    free(_dir1);
-    free(_dir2);
-    return 0;
+    errno = xerrno;
   }
 
-  ptr = buf;
-  sstrncpy(ptr, _dir1, buflen);
-  ptr += dir1len;
-  buflen -= dir1len;
+  return res;
+}
 
-  if (buflen > 0 &&
-      dir1len >= 1 &&
-      *(_dir1 + (dir1len-1)) != '/') {
-    sstrcat(ptr, "/", buflen);
-    ptr += 1;
-    buflen -= 1;
-  }
+int pr_fs_copy_file(const char *src, const char *dst) {
+  return pr_fs_copy_file2(src, dst, 0, NULL);
+}
 
-  sstrcat(ptr, _dir2, buflen);
+pr_fs_t *pr_register_fs(pool *p, const char *name, const char *path) {
+  pr_fs_t *fs = NULL;
+  int xerrno = 0;
 
-  if (*buf == '\0') {
-   *buf++ = '/';
-   *buf = '\0';
+  /* Sanity check */
+  if (p == NULL ||
+      name == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
   }
 
-  free(_dir1);
-  free(_dir2);
+  /* Instantiate an pr_fs_t */
+  fs = pr_create_fs(p, name);
+  xerrno = errno;
 
-  return 0;
-}
+  if (fs != NULL) {
+    if (pr_insert_fs(fs, path) == FALSE) {
+      xerrno = errno;
 
-/* This function performs any tilde expansion needed and then returns the
- * resolved path, if any.
- *
- * Returns: -1 (errno = ENOENT): user does not exist
- *           0 : no interpolation done (path exists)
- *           1 : interpolation done
- */
-int pr_fs_interpolate(const char *path, char *buf, size_t buflen) {
-  char *ptr = NULL;
-  size_t currlen, pathlen;
-  char user[PR_TUNABLE_LOGIN_MAX+1];
+      pr_trace_msg(trace_channel, 4, "error inserting FS '%s' at path '%s'",
+        name, path);
 
-  if (path == NULL) {
-    errno = EINVAL;
-    return -1;
-  }
+      destroy_pool(fs->fs_pool);
+      fs->fs_pool = NULL;
 
-  if (path[0] != '~') {
-    sstrncpy(buf, path, buflen);
-    return 1;
+      errno = xerrno;
+      return NULL;
+    }
+
+  } else {
+    pr_trace_msg(trace_channel, 6, "error creating FS '%s': %s", name,
+      strerror(errno));
   }
 
-  memset(user, '\0', sizeof(user));
+  errno = xerrno;
+  return fs;
+}
 
-  /* The first character of the given path is '~'.
-   *
-   * We next need to see what the rest of the path looks like.  Could be:
-   *
-   *  "~"
-   *  "~user"
-   *  "~/"
-   *  "~/path"
-   *  "~user/path"
-   */
+pr_fs_t *pr_create_fs(pool *p, const char *name) {
+  pr_fs_t *fs = NULL;
+  pool *fs_pool = NULL;
 
-  pathlen = strlen(path);
-  if (pathlen == 1) {
-    /* If the path is just "~", AND we're chrooted, then the interpolation
-     * is easy.
-     */
-    if (session.chroot_path != NULL) {
-      sstrncpy(buf, session.chroot_path, buflen);
-      return 1;
-    }
+  /* Sanity check */
+  if (p == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return NULL;
   }
 
-  ptr = strchr(path, '/');
-  if (ptr == NULL) {
-    struct stat st;
+  /* Allocate a subpool, then allocate an pr_fs_t object from that subpool */
+  fs_pool = make_sub_pool(p);
+  pr_pool_tag(fs_pool, "FS Pool");
 
-    /* No path separator present, which means path must be "~user".
-     *
-     * This means that a path of "~foo" could be a file with that exact
-     * name, or it could be that user's home directory.  Let's find out
-     * which it is.
-     */
+  fs = pcalloc(fs_pool, sizeof(pr_fs_t));
+  fs->fs_pool = fs_pool;
+  fs->fs_next = fs->fs_prev = NULL;
+  fs->fs_name = pstrdup(fs->fs_pool, name);
+  fs->fs_next = root_fs;
+  fs->allow_xdev_link = TRUE;
+  fs->allow_xdev_rename = TRUE;
 
-    if (pr_fsio_stat(path, &st) == -1) {
-       /* Must be a user, if anything...otherwise it's probably a typo.
-        *
-        * The user name, then, is everything just past the '~' character.
-        */
-      sstrncpy(user, path+1,
-        pathlen-1 > sizeof(user)-1 ? sizeof(user)-1 : pathlen-1);
+  /* This is NULL until set by pr_insert_fs() */
+  fs->fs_path = NULL;
 
-    } else {
-      /* This IS the file in question, perform no interpolation. */
-      return 0;
-    }
+  return fs;
+}
 
-  } else {
-    currlen = ptr - path;
-    if (currlen > 1) {
-      /* Copy over the username. */
-      sstrncpy(user, path+1,
-        currlen > sizeof(user)-1 ? sizeof(user)-1 : currlen);
-    }
+int pr_insert_fs(pr_fs_t *fs, const char *path) {
+  char cleaned_path[PR_TUNABLE_PATH_MAX] = {'\0'};
 
-    /* Advance past the '/'. */
-    ptr++;
+  if (fs == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  if (user[0] == '\0') {
-    /* No user name provided.  If we are chrooted, we leave it that way.
-     * Otherwise, we're not chrooted, and we can assume the current user.
-     */
-    if (session.chroot_path == NULL) {
-      sstrncpy(user, session.user, sizeof(user)-1);
-    }
+  if (fs_map == NULL) {
+    pool *map_pool = make_sub_pool(permanent_pool);
+    pr_pool_tag(map_pool, "FSIO Map Pool");
+
+    fs_map = make_array(map_pool, 0, sizeof(pr_fs_t *));
   }
 
-  if (user[0] != '\0') {
-    struct passwd *pw = NULL;
-    pool *p = NULL;
+  /* Clean the path, but only if it starts with a '/'.  Non-local-filesystem
+   * paths may not want/need to be cleaned.
+   */
+  if (*path == '/') {
+    pr_fs_clean_path(path, cleaned_path, sizeof(cleaned_path));
 
-    /* We need to look up the info for the given username, and add it
-     * into the buffer.
-     *
-     * The permanent pool is used here, rather than session.pool, as path
-     * interpolation can occur during startup parsing, when session.pool does
-     * not exist.  It does not really matter, since the allocated sub pool
-     * is destroyed shortly.
+    /* Cleaning the path may have removed a trailing slash, which the
+     * caller may actually have wanted.  Make sure one is present in
+     * the cleaned version, if it was present in the original version and
+     * is not present in the cleaned version.
      */
-    p = make_sub_pool(permanent_pool);
-    pr_pool_tag(p, "pr_fs_interpolate() pool");
+    if (path[strlen(path)-1] == '/') {
+      size_t len = strlen(cleaned_path);
 
-    pw = pr_auth_getpwnam(p, user);
-    if (pw == NULL) {
-      destroy_pool(p);
-      errno = ENOENT;
-      return -1;
+      if (len > 1 &&
+          len < (PR_TUNABLE_PATH_MAX-3) &&
+          cleaned_path[len-1] != '/') {
+        cleaned_path[len] = '/';
+        cleaned_path[len+1] = '\0';
+      }
     }
 
-    sstrncpy(buf, pw->pw_dir, buflen);
-
-    /* Done with pw, which means we can destroy the temporary pool now. */
-    destroy_pool(p);
-
   } else {
-    /* We're chrooted. */
-    sstrncpy(buf, "/", buflen);
-  }
- 
-  currlen = strlen(buf);
-
-  if (ptr != NULL &&
-      currlen < buflen &&
-      buf[currlen-1] != '/') {
-    buf[currlen++] = '/';
+    sstrncpy(cleaned_path, path, sizeof(cleaned_path));
   }
 
-  if (ptr != NULL) {
-    sstrncpy(&buf[currlen], ptr, buflen - currlen);
+  if (fs->fs_path == NULL) {
+    fs->fs_path = pstrdup(fs->fs_pool, cleaned_path);
   }
- 
-  return 1;
-}
 
-int pr_fs_resolve_partial(const char *path, char *buf, size_t buflen, int op) {
-  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
-       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
-       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
-       *where = NULL, *ptr = NULL, *last = NULL;
+  /* Check for duplicates. */
+  if (fs_map->nelts > 0) {
+    pr_fs_t *fsi = NULL, **fs_objs = (pr_fs_t **) fs_map->elts;
+    register unsigned int i;
 
-  pr_fs_t *fs = NULL;
-  int len = 0, fini = 1, link_cnt = 0;
-  ino_t prev_inode = 0;
-  dev_t prev_device = 0;
-  struct stat sbuf;
+    for (i = 0; i < fs_map->nelts; i++) {
+      fsi = fs_objs[i];
 
-  if (!path) {
-    errno = EINVAL;
-    return -1;
-  }
-
-  if (*path != '/') {
-    if (*path == '~') {
-      switch (pr_fs_interpolate(path, curpath, sizeof(curpath)-1)) {
-      case -1:
-        return -1;
-
-      case 0:
-        sstrncpy(curpath, path, sizeof(curpath));
-        sstrncpy(workpath, cwd, sizeof(workpath));
-        break;
-      }
-
-    } else {
-      sstrncpy(curpath, path, sizeof(curpath));
-      sstrncpy(workpath, cwd, sizeof(workpath));
-    }
-
-  } else
-    sstrncpy(curpath, path, sizeof(curpath));
-
-  while (fini--) {
-    where = curpath;
-
-    while (*where != '\0') {
-      pr_signals_handle();
-
-      /* Handle "." */
-      if (strncmp(where, ".", 2) == 0) {
-        where++;
-        continue;
-      }
-
-      /* Handle ".." */
-      if (strncmp(where, "..", 3) == 0) {
-        where += 2;
-        ptr = last = workpath;
-
-        while (*ptr) {
-          if (*ptr == '/')
-            last = ptr;
-          ptr++;
+      if (strcmp(fsi->fs_path, cleaned_path) == 0) {
+        /* An entry for this path already exists.  Make sure the FS being
+         * mounted is not the same as the one already present.
+         */
+        if (strcmp(fsi->fs_name, fs->fs_name) == 0) {
+          pr_log_pri(PR_LOG_NOTICE,
+            "error: duplicate fs paths not allowed: '%s'", cleaned_path);
+          errno = EEXIST;
+          return FALSE;
         }
 
-        *last = '\0';
-        continue;
-      }
-
-      /* Handle "./" */
-      if (strncmp(where, "./", 2) == 0) {
-        where += 2;
-        continue;
-      }
-
-      /* Handle "../" */
-      if (strncmp(where, "../", 3) == 0) {
-        where += 3;
-        ptr = last = workpath;
-
-        while (*ptr) {
-          if (*ptr == '/')
-            last = ptr;
-          ptr++;
-        }
+        /* "Push" the given FS on top of the existing one. */
+        fs->fs_next = fsi;
+        fsi->fs_prev = fs;
+        fs_objs[i] = fs;
 
-        *last = '\0';
-        continue;
+        chk_fs_map = TRUE;
+        return TRUE;
       }
+    }
+  }
 
-      ptr = strchr(where, '/');
-      if (ptr == NULL) {
-        size_t wherelen = strlen(where);
+  /* Push the new FS into the container, then resort the contents. */
+  *((pr_fs_t **) push_array(fs_map)) = fs;
 
-        ptr = where;
-        if (wherelen >= 1)
-          ptr += (wherelen - 1);
+  /* Sort the FSs in the map according to their paths, but only if there
+   * are more than one element in the array_header.
+   */
+  if (fs_map->nelts > 1) {
+    qsort(fs_map->elts, fs_map->nelts, sizeof(pr_fs_t *), fs_cmp);
+  }
 
-      } else {
-        *ptr = '\0';
-      }
+  /* Set the flag so that the fs wrapper functions know that a new FS
+   * has been registered.
+   */
+  chk_fs_map = TRUE;
 
-      sstrncpy(namebuf, workpath, sizeof(namebuf));
+  return TRUE;
+}
 
-      if (*namebuf) {
-        for (last = namebuf; *last; last++);
-        if (*--last != '/')
-          sstrcat(namebuf, "/", sizeof(namebuf)-1);
+pr_fs_t *pr_unmount_fs(const char *path, const char *name) {
+  pr_fs_t *fsi = NULL, **fs_objs = NULL;
+  register unsigned int i = 0;
 
-      } else {
-        sstrcat(namebuf, "/", sizeof(namebuf)-1);
-      }
+  /* Sanity check */
+  if (path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
 
-      sstrcat(namebuf, where, sizeof(namebuf)-1);
+  /* This should never be called before pr_register_fs(), but, just in case...*/
+  if (fs_map == NULL) {
+    errno = EACCES;
+    return NULL;
+  }
 
-      where = ++ptr;
+  fs_objs = (pr_fs_t **) fs_map->elts;
 
-      fs = lookup_dir_fs(namebuf, op);
+  for (i = 0; i < fs_map->nelts; i++) {
+    fsi = fs_objs[i];
 
-      if (fs_cache_lstat(fs, namebuf, &sbuf) == -1)
-        return -1;
+    if (strcmp(fsi->fs_path, path) == 0 &&
+        (name ? strcmp(fsi->fs_name, name) == 0 : TRUE)) {
 
-      if (S_ISLNK(sbuf.st_mode)) {
-        char linkpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+      /* Exact match -- remove this FS.  If there is an FS underneath, pop 
+       * the top FS off the stack.  Otherwise, allocate a new map.  Then
+       * iterate through the old map, pushing all other FSs into the new map.
+       * Destroy the old map.  Move the new map into place.
+       */
 
-        /* Detect an obvious recursive symlink */
-        if (sbuf.st_ino && (ino_t) sbuf.st_ino == prev_inode &&
-            sbuf.st_dev && (dev_t) sbuf.st_dev == prev_device) {
-          errno = ELOOP;
-          return -1;
-        }
+      if (fsi->fs_next == NULL) {
+        register unsigned int j = 0;
+        pr_fs_t *tmp_fs, **old_objs = NULL;
+        pool *map_pool;
+        array_header *new_map;
 
-        prev_inode = (ino_t) sbuf.st_ino;
-        prev_device = (dev_t) sbuf.st_dev;
+        /* If removing this FS would leave an empty map, don't bother
+         * allocating a new one.
+         */
+        if (fs_map->nelts == 1) {
+          destroy_pool(fs_map->pool);
+          fs_map = NULL;
+          fs_cwd = root_fs;
 
-        if (++link_cnt > PR_FSIO_MAX_LINK_COUNT) {
-          errno = ELOOP;
-          return -1;
-        }
-	
-        len = pr_fsio_readlink(namebuf, linkpath, sizeof(linkpath)-1);
-        if (len <= 0) {
-          errno = ENOENT;
-          return -1;
+          chk_fs_map = TRUE;
+          return NULL;
         }
 
-        *(linkpath + len) = '\0';
-        if (*linkpath == '/')
-          *workpath = '\0';
-
-        /* Trim any trailing slash. */
-        if (linkpath[len-1] == '/')
-          linkpath[len-1] = '\0';
+        map_pool = make_sub_pool(permanent_pool);
+        new_map = make_array(map_pool, 0, sizeof(pr_fs_t *));
 
-        if (*linkpath == '~') {
-          char tmpbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+        pr_pool_tag(map_pool, "FSIO Map Pool");
+        old_objs = (pr_fs_t **) fs_map->elts;
 
-          *workpath = '\0';
-          sstrncpy(tmpbuf, linkpath, sizeof(tmpbuf));
+        for (j = 0; j < fs_map->nelts; j++) {
+          tmp_fs = old_objs[j];
 
-          if (pr_fs_interpolate(tmpbuf, linkpath, sizeof(linkpath)-1) == -1)
-	    return -1;
+          if (strcmp(tmp_fs->fs_path, path) != 0) {
+            *((pr_fs_t **) push_array(new_map)) = old_objs[j];
+          }
         }
 
-        if (*where) {
-          sstrcat(linkpath, "/", sizeof(linkpath)-1);
-          sstrcat(linkpath, where, sizeof(linkpath)-1);
-        }
+        destroy_pool(fs_map->pool);
+        fs_map = new_map;
 
-        sstrncpy(curpath, linkpath, sizeof(curpath));
-        fini++;
-        break; /* continue main loop */
-      }
+        /* Don't forget to set the flag so that wrapper functions scan the
+         * new map.
+         */
+        chk_fs_map = TRUE;
 
-      if (S_ISDIR(sbuf.st_mode)) {
-        sstrncpy(workpath, namebuf, sizeof(workpath));
-        continue;
+        return fsi;
       }
 
-      if (*where) {
-        errno = ENOENT;
-        return -1;               /* path/notadir/morepath */
-
-      } else {
-        sstrncpy(workpath, namebuf, sizeof(workpath));
+      /* "Pop" this FS off the stack. */
+      if (fsi->fs_next) {
+        fsi->fs_next->fs_prev = NULL;
       }
+      fs_objs[i] = fsi->fs_next;
+      fsi->fs_next = fsi->fs_prev = NULL; 
+
+      chk_fs_map = TRUE;
+      return fsi;
     }
   }
 
-  if (!workpath[0])
-    sstrncpy(workpath, "/", sizeof(workpath));
-
-  sstrncpy(buf, workpath, buflen);
-
-  return 0;
+  errno = ENOENT;
+  return NULL;
 }
 
-int pr_fs_resolve_path(const char *path, char *buf, size_t buflen, int op) {
-  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
-       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
-       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
-       *where = NULL, *ptr = NULL, *last = NULL;
+pr_fs_t *pr_remove_fs(const char *path) {
+  return pr_unmount_fs(path, NULL);
+}
 
+int pr_unregister_fs(const char *path) {
   pr_fs_t *fs = NULL;
 
-  int len = 0, fini = 1, link_cnt = 0;
-  ino_t prev_inode = 0;
-  dev_t prev_device = 0;
-  struct stat sbuf;
-
-  if (!path) {
+  if (path == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  if (pr_fs_interpolate(path, curpath, sizeof(curpath)-1) != -1)
-    sstrncpy(curpath, path, sizeof(curpath));
-
-  if (curpath[0] != '/')
-    sstrncpy(workpath, cwd, sizeof(workpath));
-  else
-    workpath[0] = '\0';
-
-  while (fini--) {
-    where = curpath;
-
-    while (*where != '\0') {
-      pr_signals_handle();
+  /* Call pr_remove_fs() to get the fs for this path removed from the map. */
+  fs = pr_remove_fs(path);
+  if (fs != NULL) {
+    destroy_pool(fs->fs_pool);
+    fs->fs_pool = NULL;
+    return 0;
+  }
 
-      if (strncmp(where, ".", 2) == 0) {
-        where++;
-        continue;
-      }
+  errno = ENOENT;
+  return -1;
+}
 
-      /* handle "./" */
-      if (strncmp(where, "./", 2) == 0) {
-        where += 2;
-        continue;
-      }
-
-      /* handle "../" */
-      if (strncmp(where, "../", 3) == 0) {
-        where += 3;
-        ptr = last = workpath;
-        while (*ptr) {
-          if (*ptr == '/')
-            last = ptr;
-          ptr++;
-        }
-
-        *last = '\0';
-        continue;
-      }
+/* This function returns the best pr_fs_t to handle the given path.  It will
+ * return NULL if there are no registered pr_fs_ts to handle the given path,
+ * in which case the default root_fs should be used.  This is so that
+ * functions can look to see if an pr_fs_t, other than the default, for a
+ * given path has been registered, if necessary.  If the return value is
+ * non-NULL, that will be a registered pr_fs_t to handle the given path.  In
+ * this case, if the exact argument is not NULL, it will either be TRUE,
+ * signifying that the returned pr_fs_t is an exact match for the given
+ * path, or FALSE, meaning the returned pr_fs_t is a "best match" -- most
+ * likely the pr_fs_t that handles the directory in which the given path
+ * occurs.
+ */
+pr_fs_t *pr_get_fs(const char *path, int *exact) {
+  pr_fs_t *fs = NULL, **fs_objs = NULL, *best_match_fs = NULL;
+  register unsigned int i = 0;
 
-      ptr = strchr(where, '/');
+  /* Sanity check */
+  if (path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
 
-      if (!ptr) {
-        size_t wherelen = strlen(where);
+  /* Basic optimization -- if there're no elements in the fs_map,
+   * return the root_fs.
+   */
+  if (fs_map == NULL ||
+      fs_map->nelts == 0) {
+    return root_fs;
+  }
 
-        ptr = where;
-        if (wherelen >= 1)
-          ptr += (wherelen - 1);
+  fs_objs = (pr_fs_t **) fs_map->elts;
+  best_match_fs = root_fs;
 
-      } else
-        *ptr = '\0';
+  /* In order to handle deferred-resolution paths (eg "~" paths), the given
+   * path will need to be passed through dir_realpath(), if necessary.
+   *
+   * The chk_fs_map flag, if TRUE, should be cleared on return of this
+   * function -- all that flag says is, if TRUE, that this function _might_
+   * return something different than it did on a previous call.
+   */
 
-      sstrncpy(namebuf, workpath, sizeof(namebuf));
+  for (i = 0; i < fs_map->nelts; i++) {
+    int res = 0;
 
-      if (*namebuf) {
-        for (last = namebuf; *last; last++);
+    fs = fs_objs[i];
 
-        if (*--last != '/')
-          sstrcat(namebuf, "/", sizeof(namebuf)-1);
+    /* If the current pr_fs_t's path ends in a slash (meaning it is a
+     * directory, and it matches the first part of the given path,
+     * assume it to be the best pr_fs_t found so far.
+     */
+    if ((fs->fs_path)[strlen(fs->fs_path) - 1] == '/' &&
+        !strncmp(path, fs->fs_path, strlen(fs->fs_path))) {
+      best_match_fs = fs;
+    }
 
-      } else
-        sstrcat(namebuf, "/", sizeof(namebuf)-1);
+    res = strcmp(fs->fs_path, path);
+    if (res == 0) {
+      /* Exact match */
+      if (exact) {
+        *exact = TRUE;
+      }
 
-      sstrcat(namebuf, where, sizeof(namebuf)-1);
+      chk_fs_map = FALSE;
+      return fs;
 
-      where = ++ptr;
+    } else if (res > 0) {
+      if (exact != NULL) {
+        *exact = FALSE;
+      }
 
-      fs = lookup_dir_fs(namebuf, op);
+      chk_fs_map = FALSE;
 
-      if (fs_cache_lstat(fs, namebuf, &sbuf) == -1) {
-        errno = ENOENT;
-        return -1;
-      }
+      /* Gone too far - return the best-match pr_fs_t */
+      return best_match_fs;
+    }
+  }
 
-      if (S_ISLNK(sbuf.st_mode)) {
-        char linkpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+  chk_fs_map = FALSE;
 
-        /* Detect an obvious recursive symlink */
-        if (sbuf.st_ino && (ino_t) sbuf.st_ino == prev_inode &&
-            sbuf.st_dev && (dev_t) sbuf.st_dev == prev_device) {
-          errno = ELOOP;
-          return -1;
-        }
+  if (exact != NULL) {
+    *exact = FALSE;
+  }
 
-        prev_inode = (ino_t) sbuf.st_ino;
-        prev_device = (dev_t) sbuf.st_dev;
+  /* Return best-match by default */
+  return best_match_fs;
+}
 
-        if (++link_cnt > PR_FSIO_MAX_LINK_COUNT) {
-          errno = ELOOP;
-          return -1;
-        }
+int pr_fs_setcwd(const char *dir) {
+  if (pr_fs_resolve_path(dir, cwd, sizeof(cwd)-1, FSIO_DIR_CHDIR) < 0) {
+    return -1;
+  }
 
-        len = pr_fsio_readlink(namebuf, linkpath, sizeof(linkpath)-1);
-        if (len <= 0) {
-          errno = ENOENT;
-          return -1;
-        }
+  if (sstrncpy(cwd, dir, sizeof(cwd)) < 0) {
+    return -1;
+  }
 
-        *(linkpath+len) = '\0';
+  fs_cwd = lookup_dir_fs(cwd, FSIO_DIR_CHDIR);
+  cwd[sizeof(cwd) - 1] = '\0';
+  cwd_len = strlen(cwd);
 
-        if (*linkpath == '/')
-          *workpath = '\0';
+  return 0;
+}
 
-        /* Trim any trailing slash. */
-        if (linkpath[len-1] == '/')
-          linkpath[len-1] = '\0';
+const char *pr_fs_getcwd(void) {
+  return cwd;
+}
 
-        if (*linkpath == '~') {
-          char tmpbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-          *workpath = '\0';
+const char *pr_fs_getvwd(void) {
+  return vwd;
+}
 
-          sstrncpy(tmpbuf, linkpath, sizeof(tmpbuf));
+int pr_fs_dircat(char *buf, int buflen, const char *dir1, const char *dir2) {
+  /* Make temporary copies so that memory areas can overlap */
+  char *_dir1 = NULL, *_dir2 = NULL, *ptr = NULL;
+  size_t dir1len = 0, dir2len = 0;
 
-          if (pr_fs_interpolate(tmpbuf, linkpath, sizeof(linkpath)-1) == -1)
-	    return -1;
-        }
+  /* The shortest possible path is "/", which requires 2 bytes. */
 
-        if (*where) {
-          sstrcat(linkpath, "/", sizeof(linkpath)-1);
-          sstrcat(linkpath, where, sizeof(linkpath)-1);
-        }
+  if (buf == NULL ||
+      buflen < 2 ||
+      dir1 == NULL ||
+      dir2 == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-        sstrncpy(curpath, linkpath, sizeof(curpath));
-        fini++;
-        break; /* continue main loop */
-      }
+  /* This is a test to see if we've got reasonable directories to concatenate.
+   */
+  dir1len = strlen(dir1);
+  dir2len = strlen(dir2);
 
-      if (S_ISDIR(sbuf.st_mode)) {
-        sstrncpy(workpath, namebuf, sizeof(workpath));
-        continue;
-      }
+  /* If both strings are empty, then the "concatenation" becomes trivial. */
+  if (dir1len == 0 &&
+      dir2len == 0) {
+    buf[0] = '/';
+    buf[1] = '\0';
+    return 0;
+  }
 
-      if (*where) {
-        errno = ENOENT;
-        return -1;               /* path/notadir/morepath */
+  /* If dir2 is non-empty, but dir1 IS empty... */
+  if (dir1len == 0) {
+    sstrncpy(buf, dir2, buflen);
+    buflen -= dir2len;
+    sstrcat(buf, "/", buflen);
+    return 0;
+  }
 
-      } else
-        sstrncpy(workpath, namebuf, sizeof(workpath));
-    }
+  /* Likewise, if dir1 is non-empty, but dir2 IS empty... */
+  if (dir2len == 0) {
+    sstrncpy(buf, dir1, buflen);
+    buflen -= dir1len;
+    sstrcat(buf, "/", buflen);
+    return 0;
   }
 
-  if (!workpath[0])
-    sstrncpy(workpath, "/", sizeof(workpath));
+  if ((dir1len + dir2len + 1) >= PR_TUNABLE_PATH_MAX) {
+    errno = ENAMETOOLONG;
+    buf[0] = '\0';  
+    return -1;
+  }
 
-  sstrncpy(buf, workpath, buflen);
+  _dir1 = strdup(dir1);
+  if (_dir1 == NULL) {
+    return -1;
+  }
 
-  return 0;
-}
+  _dir2 = strdup(dir2);
+  if (_dir2 == NULL) {
+    int xerrno = errno;
 
-int pr_fs_clean_path2(const char *path, char *buf, size_t buflen, int flags) {
-  char workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
-  char namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
-  int fini = 1, have_abs_path = FALSE;
+    free(_dir1);
 
-  if (path == NULL ||
-      buf == NULL) {
-    errno = EINVAL;
+    errno = xerrno;
     return -1;
   }
 
-  if (buflen == 0) {
+  if (*_dir2 == '/') {
+    sstrncpy(buf, _dir2, buflen);
+    free(_dir1);
+    free(_dir2);
     return 0;
   }
 
-  sstrncpy(curpath, path, sizeof(curpath));
+  ptr = buf;
+  sstrncpy(ptr, _dir1, buflen);
+  ptr += dir1len;
+  buflen -= dir1len;
 
-  if (*curpath == '/') {
-    have_abs_path = TRUE;
+  if (buflen > 0 &&
+      dir1len >= 1 &&
+      *(_dir1 + (dir1len-1)) != '/') {
+    sstrcat(ptr, "/", buflen);
+    ptr += 1;
+    buflen -= 1;
   }
 
-  /* main loop */
-  while (fini--) {
-    char *where = NULL, *ptr = NULL, *last = NULL;
+  sstrcat(ptr, _dir2, buflen);
 
-    where = curpath;
-    while (*where != '\0') {
-      pr_signals_handle();
+  if (*buf == '\0') {
+   *buf++ = '/';
+   *buf = '\0';
+  }
 
-      if (strncmp(where, ".", 2) == 0) {
-        where++;
-        continue;
+  free(_dir1);
+  free(_dir2);
+
+  return 0;
+}
+
+/* This function performs any tilde expansion needed and then returns the
+ * resolved path, if any.
+ *
+ * Returns: -1 (errno = ENOENT): user does not exist
+ *           0 : no interpolation done (path exists)
+ *           1 : interpolation done
+ */
+int pr_fs_interpolate(const char *path, char *buf, size_t buflen) {
+  char *ptr = NULL;
+  size_t currlen, pathlen;
+  char user[PR_TUNABLE_LOGIN_MAX+1];
+
+  if (path == NULL ||
+      buf == NULL ||
+      buflen == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (path[0] != '~') {
+    sstrncpy(buf, path, buflen);
+    return 1;
+  }
+
+  memset(user, '\0', sizeof(user));
+
+  /* The first character of the given path is '~'.
+   *
+   * We next need to see what the rest of the path looks like.  Could be:
+   *
+   *  "~"
+   *  "~user"
+   *  "~/"
+   *  "~/path"
+   *  "~user/path"
+   */
+
+  pathlen = strlen(path);
+  if (pathlen == 1) {
+    /* If the path is just "~", AND we're chrooted, then the interpolation
+     * is easy.
+     */
+    if (session.chroot_path != NULL) {
+      sstrncpy(buf, session.chroot_path, buflen);
+      return 1;
+    }
+  }
+
+  ptr = strchr(path, '/');
+  if (ptr == NULL) {
+    struct stat st;
+
+    /* No path separator present, which means path must be "~user".
+     *
+     * This means that a path of "~foo" could be a file with that exact
+     * name, or it could be that user's home directory.  Let's find out
+     * which it is.
+     */
+
+    if (pr_fsio_stat(path, &st) < 0) {
+       /* Must be a user, if anything...otherwise it's probably a typo.
+        *
+        * The user name, then, is everything just past the '~' character.
+        */
+      sstrncpy(user, path+1,
+        pathlen-1 > sizeof(user)-1 ? sizeof(user)-1 : pathlen-1);
+
+    } else {
+      /* This IS the file in question, perform no interpolation. */
+      return 0;
+    }
+
+  } else {
+    currlen = ptr - path;
+    if (currlen > 1) {
+      /* Copy over the username. */
+      sstrncpy(user, path+1,
+        currlen > sizeof(user)-1 ? sizeof(user)-1 : currlen);
+    }
+
+    /* Advance past the '/'. */
+    ptr++;
+  }
+
+  if (user[0] == '\0') {
+    /* No user name provided.  If we are chrooted, we leave it that way.
+     * Otherwise, we're not chrooted, and we can assume the current user.
+     */
+    if (session.chroot_path == NULL) {
+      sstrncpy(user, session.user, sizeof(user)-1);
+    }
+  }
+
+  if (user[0] != '\0') {
+    struct passwd *pw = NULL;
+    pool *p = NULL;
+
+    /* We need to look up the info for the given username, and add it
+     * into the buffer.
+     *
+     * The permanent pool is used here, rather than session.pool, as path
+     * interpolation can occur during startup parsing, when session.pool does
+     * not exist.  It does not really matter, since the allocated sub pool
+     * is destroyed shortly.
+     */
+    p = make_sub_pool(permanent_pool);
+    pr_pool_tag(p, "pr_fs_interpolate() pool");
+
+    pw = pr_auth_getpwnam(p, user);
+    if (pw == NULL) {
+      destroy_pool(p);
+      errno = ENOENT;
+      return -1;
+    }
+
+    sstrncpy(buf, pw->pw_dir, buflen);
+
+    /* Done with pw, which means we can destroy the temporary pool now. */
+    destroy_pool(p);
+
+  } else {
+    /* We're chrooted. */
+    sstrncpy(buf, "/", buflen);
+  }
+ 
+  currlen = strlen(buf);
+
+  if (ptr != NULL &&
+      currlen < buflen &&
+      buf[currlen-1] != '/') {
+    buf[currlen++] = '/';
+  }
+
+  if (ptr != NULL) {
+    sstrncpy(&buf[currlen], ptr, buflen - currlen);
+  }
+ 
+  return 1;
+}
+
+int pr_fs_resolve_partial(const char *path, char *buf, size_t buflen, int op) {
+  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
+       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
+       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
+       *where = NULL, *ptr = NULL, *last = NULL;
+  pr_fs_t *fs = NULL;
+  int len = 0, fini = 1, link_cnt = 0;
+  ino_t prev_inode = 0;
+  dev_t prev_device = 0;
+  struct stat sbuf;
+
+  if (path == NULL ||
+      buf == NULL ||
+      buflen == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (*path != '/') {
+    if (*path == '~') {
+      switch (pr_fs_interpolate(path, curpath, sizeof(curpath)-1)) {
+        case -1:
+          return -1;
+
+        case 0:
+          sstrncpy(curpath, path, sizeof(curpath));
+          sstrncpy(workpath, cwd, sizeof(workpath));
+          break;
       }
 
-      /* handle "./" */
-      if (strncmp(where, "./", 2) == 0) {
-        where += 2;
+    } else {
+      sstrncpy(curpath, path, sizeof(curpath));
+      sstrncpy(workpath, cwd, sizeof(workpath));
+    }
+
+  } else {
+    sstrncpy(curpath, path, sizeof(curpath));
+  }
+
+  while (fini--) {
+    where = curpath;
+
+    while (*where != '\0') {
+      pr_signals_handle();
+
+      /* Handle "." */
+      if (strncmp(where, ".", 2) == 0) {
+        where++;
         continue;
       }
 
-      /* handle ".." */
+      /* Handle ".." */
       if (strncmp(where, "..", 3) == 0) {
         where += 2;
         ptr = last = workpath;
 
         while (*ptr) {
-          pr_signals_handle();
-
           if (*ptr == '/') {
             last = ptr;
           }
-
           ptr++;
         }
 
@@ -2351,14 +2818,18 @@ int pr_fs_clean_path2(const char *path, char *buf, size_t buflen, int flags) {
         continue;
       }
 
-      /* handle "../" */
+      /* Handle "./" */
+      if (strncmp(where, "./", 2) == 0) {
+        where += 2;
+        continue;
+      }
+
+      /* Handle "../" */
       if (strncmp(where, "../", 3) == 0) {
         where += 3;
         ptr = last = workpath;
 
         while (*ptr) {
-          pr_signals_handle();
-
           if (*ptr == '/') {
             last = ptr;
           }
@@ -2391,193 +2862,131 @@ int pr_fs_clean_path2(const char *path, char *buf, size_t buflen, int flags) {
         }
 
       } else {
-        if (have_abs_path ||
-            (flags & PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH)) {
-          sstrcat(namebuf, "/", sizeof(namebuf)-1);
-          have_abs_path = FALSE;
-        }
+        sstrcat(namebuf, "/", sizeof(namebuf)-1);
       }
 
       sstrcat(namebuf, where, sizeof(namebuf)-1);
-      namebuf[sizeof(namebuf)-1] = '\0';
 
       where = ++ptr;
 
-      sstrncpy(workpath, namebuf, sizeof(workpath));
-    }
-  }
+      fs = lookup_dir_fs(namebuf, op);
 
-  if (!workpath[0]) {
-    sstrncpy(workpath, "/", sizeof(workpath));
-  }
+      if (fs_cache_lstat(fs, namebuf, &sbuf) == -1) {
+        return -1;
+      }
 
-  sstrncpy(buf, workpath, buflen);
-  return 0;
-}
+      if (S_ISLNK(sbuf.st_mode)) {
+        char linkpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
 
-void pr_fs_clean_path(const char *path, char *buf, size_t buflen) {
-  pr_fs_clean_path2(path, buf, buflen, PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH);
-}
+        /* Detect an obvious recursive symlink */
+        if (sbuf.st_ino && (ino_t) sbuf.st_ino == prev_inode &&
+            sbuf.st_dev && (dev_t) sbuf.st_dev == prev_device) {
+          errno = ELOOP;
+          return -1;
+        }
 
-int pr_fs_use_encoding(int bool) {
-  int curr_setting = use_encoding;
-  use_encoding = bool;
+        prev_inode = (ino_t) sbuf.st_ino;
+        prev_device = (dev_t) sbuf.st_dev;
 
-  return curr_setting;
-}
+        if (++link_cnt > PR_FSIO_MAX_LINK_COUNT) {
+          errno = ELOOP;
+          return -1;
+        }
+	
+        len = pr_fsio_readlink(namebuf, linkpath, sizeof(linkpath)-1);
+        if (len <= 0) {
+          errno = ENOENT;
+          return -1;
+        }
 
-char *pr_fs_decode_path(pool *p, const char *path) {
-#ifdef PR_USE_NLS
-  size_t outlen;
-  char *res;
+        *(linkpath + len) = '\0';
+        if (*linkpath == '/') {
+          *workpath = '\0';
+        }
 
-  if (p == NULL ||
-      path == NULL) {
-    errno = EINVAL;
-    return NULL;
-  }
+        /* Trim any trailing slash. */
+        if (linkpath[len-1] == '/') {
+          linkpath[len-1] = '\0';
+        }
 
-  if (!use_encoding) {
-    return (char *) path;
-  }
+        if (*linkpath == '~') {
+          char tmpbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
 
-  res = pr_decode_str(p, path, strlen(path), &outlen);
-  if (res == NULL) {
-    pr_trace_msg("encode", 1, "error decoding path '%s': %s", path,
-      strerror(errno));
+          *workpath = '\0';
+          sstrncpy(tmpbuf, linkpath, sizeof(tmpbuf));
 
-    if (pr_trace_get_level("encode") >= 14) {
-      /* Write out the path we tried (and failed) to decode, in hex. */
-      register unsigned int i;
-      unsigned char *raw_path;
-      size_t pathlen, raw_pathlen;
+          if (pr_fs_interpolate(tmpbuf, linkpath, sizeof(linkpath)-1) < 0) {
+	    return -1;
+          }
+        }
 
-      pathlen = strlen(path);
-      raw_pathlen = (pathlen * 5) + 1;
-      raw_path = pcalloc(p, raw_pathlen + 1);
+        if (*where) {
+          sstrcat(linkpath, "/", sizeof(linkpath)-1);
+          sstrcat(linkpath, where, sizeof(linkpath)-1);
+        }
 
-      for (i = 0; i < pathlen; i++) {
-        snprintf((char *) (raw_path + (i * 5)), (raw_pathlen - 1) - (i * 5),
-          "0x%02x ", (unsigned char) path[i]);
+        sstrncpy(curpath, linkpath, sizeof(curpath));
+        fini++;
+        break; /* continue main loop */
       }
 
-      pr_trace_msg("encode", 14, "unable to decode path (raw bytes): %s",
-        raw_path);
-    } 
-
-    return (char *) path;
-  }
-
-  pr_trace_msg("encode", 5, "decoded '%s' into '%s'", path, res);
-  return res;
-#else
-  return (char *) path;
-#endif /* PR_USE_NLS */
-}
-
-char *pr_fs_encode_path(pool *p, const char *path) {
-#ifdef PR_USE_NLS
-  size_t outlen;
-  char *res;
-
-  if (p == NULL ||
-      path == NULL) {
-    errno = EINVAL;
-    return NULL;
-  }
-
-  if (!use_encoding) {
-    return (char *) path;
-  }
-
-  res = pr_encode_str(p, path, strlen(path), &outlen);
-  if (res == NULL) {
-    pr_trace_msg("encode", 1, "error encoding path '%s': %s", path,
-      strerror(errno));
-
-    if (pr_trace_get_level("encode") >= 14) {
-      /* Write out the path we tried (and failed) to encode, in hex. */
-      register unsigned int i; 
-      unsigned char *raw_path;
-      size_t pathlen, raw_pathlen;
-      
-      pathlen = strlen(path);
-      raw_pathlen = (pathlen * 5) + 1;
-      raw_path = pcalloc(p, raw_pathlen + 1);
-
-      for (i = 0; i < pathlen; i++) {
-        snprintf((char *) (raw_path + (i * 5)), (raw_pathlen - 1) - (i * 5),
-          "0x%02x ", (unsigned char) path[i]);
+      if (S_ISDIR(sbuf.st_mode)) {
+        sstrncpy(workpath, namebuf, sizeof(workpath));
+        continue;
       }
 
-      pr_trace_msg("encode", 14, "unable to encode path (raw bytes): %s",
-        raw_path);
-    } 
-
-    return (char *) path;
-  }
-
-  pr_trace_msg("encode", 5, "encoded '%s' into '%s'", path, res);
-  return res;
-#else
-  return (char *) path;
-#endif /* PR_USE_NLS */
-}
-
-/* This function checks the given path's prefix against the paths that
- * have been registered.  If no matching path prefix has been registered,
- * the path is considered invalid.
- */
-int pr_fs_valid_path(const char *path) {
-
-  if (fs_map && fs_map->nelts > 0) {
-    pr_fs_t *fsi = NULL, **fs_objs = (pr_fs_t **) fs_map->elts;
-    register int i;
-
-    for (i = 0; i < fs_map->nelts; i++) {
-      fsi = fs_objs[i];
-
-      if (strncmp(fsi->fs_path, path, strlen(fsi->fs_path)) == 0) {
-        return 0;
+      if (*where) {
+        errno = ENOENT;
+        return -1;               /* path/notadir/morepath */
       }
+
+      sstrncpy(workpath, namebuf, sizeof(workpath));
     }
   }
 
-  /* Also check the path against the default '/' path. */
-  if (*path == '/')
-    return 0;
+  if (!workpath[0]) {
+    sstrncpy(workpath, "/", sizeof(workpath));
+  }
 
-  errno = EINVAL;
-  return -1;
+  sstrncpy(buf, workpath, buflen);
+  return 0;
 }
 
-void pr_fs_virtual_path(const char *path, char *buf, size_t buflen) {
+int pr_fs_resolve_path(const char *path, char *buf, size_t buflen, int op) {
   char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
        workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
        namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
        *where = NULL, *ptr = NULL, *last = NULL;
+  pr_fs_t *fs = NULL;
+  int len = 0, fini = 1, link_cnt = 0;
+  ino_t prev_inode = 0;
+  dev_t prev_device = 0;
+  struct stat sbuf;
 
-  int fini = 1;
-
-  if (!path)
-    return;
+  if (path == NULL ||
+      buf == NULL ||
+      buflen == 0) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  if (pr_fs_interpolate(path, curpath, sizeof(curpath)-1) != -1)
+  if (pr_fs_interpolate(path, curpath, sizeof(curpath)-1) != -1) {
     sstrncpy(curpath, path, sizeof(curpath));
+  }
 
-  if (curpath[0] != '/')
-    sstrncpy(workpath, vwd, sizeof(workpath));
-  else
-    workpath[0] = '\0';
+  if (curpath[0] != '/') {
+    sstrncpy(workpath, cwd, sizeof(workpath));
 
-  /* curpath is path resolving */
-  /* linkpath is path a symlink pointed to */
-  /* workpath is the path we've resolved */
+  } else {
+    workpath[0] = '\0';
+  }
 
-  /* main loop */
   while (fini--) {
     where = curpath;
+
     while (*where != '\0') {
+      pr_signals_handle();
+
       if (strncmp(where, ".", 2) == 0) {
         where++;
         continue;
@@ -2589,1786 +2998,3192 @@ void pr_fs_virtual_path(const char *path, char *buf, size_t buflen) {
         continue;
       }
 
-      /* handle ".." */
-      if (strncmp(where, "..", 3) == 0) {
-        where += 2;
-        ptr = last = workpath;
-        while (*ptr) {
-          if (*ptr == '/')
-            last = ptr;
-          ptr++;
-        }
-
-        *last = '\0';
-        continue;
-      }
-
       /* handle "../" */
       if (strncmp(where, "../", 3) == 0) {
         where += 3;
         ptr = last = workpath;
         while (*ptr) {
-          if (*ptr == '/')
+          if (*ptr == '/') {
             last = ptr;
+          }
           ptr++;
         }
+
         *last = '\0';
         continue;
       }
-      ptr = strchr(where, '/');
 
-      if (!ptr) {
+      ptr = strchr(where, '/');
+      if (ptr == NULL) {
         size_t wherelen = strlen(where);
 
         ptr = where;
-        if (wherelen >= 1)
+        if (wherelen >= 1) {
           ptr += (wherelen - 1);
+        }
 
-      } else
+      } else {
         *ptr = '\0';
+      }
 
       sstrncpy(namebuf, workpath, sizeof(namebuf));
 
       if (*namebuf) {
         for (last = namebuf; *last; last++);
-        if (*--last != '/')
+        if (*--last != '/') {
           sstrcat(namebuf, "/", sizeof(namebuf)-1);
+        }
 
-      } else
+      } else {
         sstrcat(namebuf, "/", sizeof(namebuf)-1);
+      }
 
       sstrcat(namebuf, where, sizeof(namebuf)-1);
 
       where = ++ptr;
 
-      sstrncpy(workpath, namebuf, sizeof(workpath));
-    }
-  }
+      fs = lookup_dir_fs(namebuf, op);
 
-  if (!workpath[0])
-    sstrncpy(workpath, "/", sizeof(workpath));
+      if (fs_cache_lstat(fs, namebuf, &sbuf) == -1) {
+        errno = ENOENT;
+        return -1;
+      }
 
-  sstrncpy(buf, workpath, buflen);
-}
+      if (S_ISLNK(sbuf.st_mode)) {
+        char linkpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
 
-int pr_fsio_chdir_canon(const char *path, int hidesymlink) {
-  char resbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-  pr_fs_t *fs = NULL;
-  int res = 0;
+        /* Detect an obvious recursive symlink */
+        if (sbuf.st_ino && (ino_t) sbuf.st_ino == prev_inode &&
+            sbuf.st_dev && (dev_t) sbuf.st_dev == prev_device) {
+          errno = ELOOP;
+          return -1;
+        }
 
-  if (pr_fs_resolve_partial(path, resbuf, sizeof(resbuf)-1,
-      FSIO_DIR_CHDIR) == -1)
-    return -1;
+        prev_inode = (ino_t) sbuf.st_ino;
+        prev_device = (dev_t) sbuf.st_dev;
 
-  fs = lookup_dir_fs(resbuf, FSIO_DIR_CHDIR);
-  if (fs == NULL) {
-    return -1;
-  }
+        if (++link_cnt > PR_FSIO_MAX_LINK_COUNT) {
+          errno = ELOOP;
+          return -1;
+        }
 
-  /* Find the first non-NULL custom chdir handler.  If there are none,
-   * use the system chdir.
-   */
-  while (fs && fs->fs_next && !fs->chdir)
-    fs = fs->fs_next;
+        len = pr_fsio_readlink(namebuf, linkpath, sizeof(linkpath)-1);
+        if (len <= 0) {
+          errno = ENOENT;
+          return -1;
+        }
 
-  pr_trace_msg(trace_channel, 8, "using %s chdir() for path '%s'", fs->fs_name,
-    path);
-  res = (fs->chdir)(fs, resbuf);
+        *(linkpath+len) = '\0';
 
-  if (res != -1) {
-    /* chdir succeeded, so we set fs_cwd for future references. */
-     fs_cwd = fs ? fs : root_fs;
+        if (*linkpath == '/') {
+          *workpath = '\0';
+        }
 
-     if (hidesymlink) {
-       pr_fs_virtual_path(path, vwd, sizeof(vwd)-1);
+        /* Trim any trailing slash. */
+        if (linkpath[len-1] == '/') {
+          linkpath[len-1] = '\0';
+        }
 
-     } else {
-       sstrncpy(vwd, resbuf, sizeof(vwd));
-     }
-  }
+        if (*linkpath == '~') {
+          char tmpbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+          *workpath = '\0';
 
-  return res;
-}
+          sstrncpy(tmpbuf, linkpath, sizeof(tmpbuf));
 
-int pr_fsio_chdir(const char *path, int hidesymlink) {
-  char resbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
-  pr_fs_t *fs = NULL;
-  int res;
+          if (pr_fs_interpolate(tmpbuf, linkpath, sizeof(linkpath)-1) < 0) {
+	    return -1;
+          }
+        }
 
-  pr_fs_clean_path(path, resbuf, sizeof(resbuf)-1);
+        if (*where) {
+          sstrcat(linkpath, "/", sizeof(linkpath)-1);
+          sstrcat(linkpath, where, sizeof(linkpath)-1);
+        }
 
-  fs = lookup_dir_fs(path, FSIO_DIR_CHDIR);
-  if (fs == NULL) {
-    return -1;
-  }
+        sstrncpy(curpath, linkpath, sizeof(curpath));
+        fini++;
+        break; /* continue main loop */
+      }
 
-  /* Find the first non-NULL custom chdir handler.  If there are none,
-   * use the system chdir.
-   */
-  while (fs && fs->fs_next && !fs->chdir)
-    fs = fs->fs_next;
+      if (S_ISDIR(sbuf.st_mode)) {
+        sstrncpy(workpath, namebuf, sizeof(workpath));
+        continue;
+      }
 
-  pr_trace_msg(trace_channel, 8, "using %s chdir() for path '%s'", fs->fs_name,
-    path);
-  res = (fs->chdir)(fs, resbuf);
+      if (*where) {
+        errno = ENOENT;
+        return -1;               /* path/notadir/morepath */
+      }
 
-  if (res != -1) {
-    /* chdir succeeded, so we set fs_cwd for future references. */
-     fs_cwd = fs;
+      sstrncpy(workpath, namebuf, sizeof(workpath));
+    }
+  }
 
-     if (hidesymlink)
-       pr_fs_virtual_path(path, vwd, sizeof(vwd)-1);
-     else
-       sstrncpy(vwd, resbuf, sizeof(vwd));
+  if (!workpath[0]) {
+    sstrncpy(workpath, "/", sizeof(workpath));
   }
 
-  return res;
+  sstrncpy(buf, workpath, buflen);
+  return 0;
 }
 
-/* fs_opendir, fs_closedir and fs_readdir all use a nifty
- * optimization, caching the last-recently-used pr_fs_t, and
- * avoid future pr_fs_t lookups when iterating via readdir.
- */
-void *pr_fsio_opendir(const char *path) {
-  pr_fs_t *fs = NULL;
-  fsopendir_t *fsod = NULL, *fsodi = NULL;
-  pool *fsod_pool = NULL;
-  DIR *res = NULL;
+int pr_fs_clean_path2(const char *path, char *buf, size_t buflen, int flags) {
+  char workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
+  char namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
+  int fini = 1, have_abs_path = FALSE;
 
-  if (strchr(path, '/') == NULL) {
-    pr_fs_setcwd(pr_fs_getcwd());
-    fs = fs_cwd;
+  if (path == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  } else {
-    char buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+  if (buflen == 0) {
+    return 0;
+  }
 
-    if (pr_fs_resolve_partial(path, buf, sizeof(buf)-1, FSIO_DIR_OPENDIR) == -1)
-      return NULL;
+  sstrncpy(curpath, path, sizeof(curpath));
 
-    fs = lookup_dir_fs(buf, FSIO_DIR_OPENDIR);
+  if (*curpath == '/') {
+    have_abs_path = TRUE;
   }
 
-  /* Find the first non-NULL custom opendir handler.  If there are none,
-   * use the system opendir.
-   */
-  while (fs && fs->fs_next && !fs->opendir)
-    fs = fs->fs_next;
+  /* main loop */
+  while (fini--) {
+    char *where = NULL, *ptr = NULL, *last = NULL;
 
-  pr_trace_msg(trace_channel, 8, "using %s opendir() for path '%s'",
-    fs->fs_name, path);
-  res = (fs->opendir)(fs, path);
+    where = curpath;
+    while (*where != '\0') {
+      pr_signals_handle();
 
-  if (res == NULL) {
+      if (strncmp(where, ".", 2) == 0) {
+        where++;
+        continue;
+      }
+
+      /* handle "./" */
+      if (strncmp(where, "./", 2) == 0) {
+        where += 2;
+        continue;
+      }
+
+      /* handle ".." */
+      if (strncmp(where, "..", 3) == 0) {
+        where += 2;
+        ptr = last = workpath;
+
+        while (*ptr) {
+          pr_signals_handle();
+
+          if (*ptr == '/') {
+            last = ptr;
+          }
+
+          ptr++;
+        }
+
+        *last = '\0';
+        continue;
+      }
+
+      /* handle "../" */
+      if (strncmp(where, "../", 3) == 0) {
+        where += 3;
+        ptr = last = workpath;
+
+        while (*ptr) {
+          pr_signals_handle();
+
+          if (*ptr == '/') {
+            last = ptr;
+          }
+          ptr++;
+        }
+
+        *last = '\0';
+        continue;
+      }
+
+      ptr = strchr(where, '/');
+      if (ptr == NULL) {
+        size_t wherelen = strlen(where);
+
+        ptr = where;
+        if (wherelen >= 1) {
+          ptr += (wherelen - 1);
+        }
+
+      } else {
+        *ptr = '\0';
+      }
+
+      sstrncpy(namebuf, workpath, sizeof(namebuf));
+
+      if (*namebuf) {
+        for (last = namebuf; *last; last++);
+        if (*--last != '/') {
+          sstrcat(namebuf, "/", sizeof(namebuf)-1);
+        }
+
+      } else {
+        if (have_abs_path ||
+            (flags & PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH)) {
+          sstrcat(namebuf, "/", sizeof(namebuf)-1);
+          have_abs_path = FALSE;
+        }
+      }
+
+      sstrcat(namebuf, where, sizeof(namebuf)-1);
+      namebuf[sizeof(namebuf)-1] = '\0';
+
+      where = ++ptr;
+
+      sstrncpy(workpath, namebuf, sizeof(workpath));
+    }
+  }
+
+  if (!workpath[0]) {
+    sstrncpy(workpath, "/", sizeof(workpath));
+  }
+
+  sstrncpy(buf, workpath, buflen);
+  return 0;
+}
+
+void pr_fs_clean_path(const char *path, char *buf, size_t buflen) {
+  pr_fs_clean_path2(path, buf, buflen, PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH);
+}
+
+int pr_fs_use_encoding(int bool) {
+  int curr_setting = use_encoding;
+
+  if (bool != TRUE &&
+      bool != FALSE) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  use_encoding = bool;
+  return curr_setting;
+}
+
+char *pr_fs_decode_path2(pool *p, const char *path, int flags) {
+#ifdef PR_USE_NLS
+  size_t outlen;
+  char *res;
+
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
     return NULL;
   }
 
-  /* Cache it here */
-  fs_cache_dir = res;
-  fs_cache_fsdir = fs;
+  if (!use_encoding) {
+    return (char *) path;
+  }
 
-  fsod_pool = make_sub_pool(permanent_pool);
-  pr_pool_tag(fsod_pool, "fsod subpool");
+  res = pr_decode_str(p, path, strlen(path), &outlen);
+  if (res == NULL) {
+    int xerrno = errno;
 
-  fsod = pcalloc(fsod_pool, sizeof(fsopendir_t));
+    pr_trace_msg("encode", 1, "error decoding path '%s': %s", path,
+      strerror(xerrno));
 
-  if (fsod == NULL) {
-    if (fs->closedir) {
-      (fs->closedir)(fs, res);
-      errno = ENOMEM;
-      return NULL;
+    if (pr_trace_get_level("encode") >= 14) {
+      /* Write out the path we tried (and failed) to decode, in hex. */
+      register unsigned int i;
+      unsigned char *raw_path;
+      size_t pathlen, raw_pathlen;
 
-    } else {
-      sys_closedir(fs, res);
-      errno = ENOMEM;
-      return NULL;
+      pathlen = strlen(path);
+      raw_pathlen = (pathlen * 5) + 1;
+      raw_path = pcalloc(p, raw_pathlen + 1);
+
+      for (i = 0; i < pathlen; i++) {
+        snprintf((char *) (raw_path + (i * 5)), (raw_pathlen - 1) - (i * 5),
+          "0x%02x ", (unsigned char) path[i]);
+      }
+
+      pr_trace_msg("encode", 14, "unable to decode path (raw bytes): %s",
+        raw_path);
+    } 
+
+    if (flags & FSIO_DECODE_FL_TELL_ERRORS) {
+      unsigned long policy;
+
+      policy = pr_encode_get_policy();
+      if (policy & PR_ENCODE_POLICY_FL_REQUIRE_VALID_ENCODING) {
+        /* Note: At present, we DO return null here to callers, to indicate
+         * the illegal encoding (Bug#4125), if configured to do so via
+         * e.g. the RequireValidEncoding LangOption.
+         */
+        errno = xerrno;
+        return NULL;
+      }
     }
+
+    return (char *) path;
   }
 
-  fsod->pool = fsod_pool;
-  fsod->dir = res;
-  fsod->fsdir = fs;
-  fsod->next = NULL;
-  fsod->prev = NULL;
+  pr_trace_msg("encode", 5, "decoded '%s' into '%s'", path, res);
+  return res;
+#else
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  return (char *) path;
+#endif /* PR_USE_NLS */
+}
+
+char *pr_fs_decode_path(pool *p, const char *path) {
+  return pr_fs_decode_path2(p, path, 0);
+}
+
+char *pr_fs_encode_path(pool *p, const char *path) {
+#ifdef PR_USE_NLS
+  size_t outlen;
+  char *res;
+
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (!use_encoding) {
+    return (char *) path;
+  }
+
+  res = pr_encode_str(p, path, strlen(path), &outlen);
+  if (res == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg("encode", 1, "error encoding path '%s': %s", path,
+      strerror(xerrno));
+
+    if (pr_trace_get_level("encode") >= 14) {
+      /* Write out the path we tried (and failed) to encode, in hex. */
+      register unsigned int i; 
+      unsigned char *raw_path;
+      size_t pathlen, raw_pathlen;
+      
+      pathlen = strlen(path);
+      raw_pathlen = (pathlen * 5) + 1;
+      raw_path = pcalloc(p, raw_pathlen + 1);
+
+      for (i = 0; i < pathlen; i++) {
+        snprintf((char *) (raw_path + (i * 5)), (raw_pathlen - 1) - (i * 5),
+          "0x%02x ", (unsigned char) path[i]);
+      }
+
+      pr_trace_msg("encode", 14, "unable to encode path (raw bytes): %s",
+        raw_path);
+    } 
+
+    /* Note: At present, we do NOT return null here to callers; we assume
+     * that all local names, being encoded for the remote client, are OK.
+     * Revisit this assumption if necessary (Bug#4125).
+     */
+
+    return (char *) path;
+  }
+
+  pr_trace_msg("encode", 5, "encoded '%s' into '%s'", path, res);
+  return res;
+#else
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  return (char *) path;
+#endif /* PR_USE_NLS */
+}
+
+array_header *pr_fs_split_path(pool *p, const char *path) {
+  int res, have_abs_path = FALSE;
+  char *buf;
+  size_t buflen, bufsz, pathlen;
+  array_header *components;
+
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  pathlen = strlen(path);
+  if (pathlen == 0) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (*path == '/') {
+    have_abs_path = TRUE;
+  }
+
+  /* Clean the path first */
+  bufsz = PR_TUNABLE_PATH_MAX;
+  buf = pcalloc(p, bufsz + 1);
+
+  res = pr_fs_clean_path2(path, buf, bufsz,
+    PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 7, "error cleaning path '%s': %s", path,
+      strerror(xerrno));
+    errno = xerrno;
+    return NULL;
+  }
+
+  buflen = strlen(buf);
+
+  /* Special-case handling of just "/", since pr_str_text_to_array() will
+   * "eat" that delimiter.
+   */
+  if (buflen == 1 &&
+      buf[0] == '/') {
+    pr_trace_msg(trace_channel, 18, "split path '%s' into 1 component", path);
+
+    components = make_array(p, 1, sizeof(char *));
+    *((char **) push_array(components)) = pstrdup(p, "/");
+
+    return components;
+  }
+
+  components = pr_str_text_to_array(p, buf, '/');
+  if (components != NULL) {
+    pr_trace_msg(trace_channel, 17, "split path '%s' into %u %s", path,
+      components->nelts, components->nelts != 1 ? "components" : "component");
+
+    if (pr_trace_get_level(trace_channel) >= 18) {
+      register unsigned int i;
+
+      for (i = 0; i < components->nelts; i++) {
+        char *component;
+
+        component = ((char **) components->elts)[i];
+        if (component == NULL) {
+          component = "NULL";
+        }
+
+        pr_trace_msg(trace_channel, 18, "path '%s' component #%u: '%s'",
+          path, i + 1, component);
+      }
+    }
+  }
+
+  if (have_abs_path == TRUE) {
+    array_header *root_component;
+
+    /* Since pr_str_text_to_array() will treat the leading '/' as a delimiter,
+     * it will be stripped and not included as a path component.  But it
+     * DOES need to be there.
+     */
+    root_component = make_array(p, 1, sizeof(char *));
+    *((char **) push_array(root_component)) = pstrdup(p, "/");
+
+    array_cat(root_component, components);
+    components = root_component;
+  }
+
+  return components;
+}
+
+char *pr_fs_join_path(pool *p, array_header *components, size_t count) {
+  register unsigned int i;
+  char *path = NULL;
+
+  if (p == NULL ||
+      components == NULL ||
+      components->nelts == 0 ||
+      count == 0) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  /* Can't join more components than we have. */
+  if (count > components->nelts) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  path = ((char **) components->elts)[0];
+
+  for (i = 1; i < count; i++) {
+    char *elt;
+
+    elt = ((char **) components->elts)[i];
+    path = pdircat(p, path, elt, NULL);
+  }
+
+  return path;
+}
+
+/* This function checks the given path's prefix against the paths that
+ * have been registered.  If no matching path prefix has been registered,
+ * the path is considered invalid.
+ */
+int pr_fs_valid_path(const char *path) {
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (fs_map != NULL &&
+      fs_map->nelts > 0) {
+    pr_fs_t *fsi = NULL, **fs_objs = (pr_fs_t **) fs_map->elts;
+    register unsigned int i;
+
+    for (i = 0; i < fs_map->nelts; i++) {
+      fsi = fs_objs[i];
+
+      if (strncmp(fsi->fs_path, path, strlen(fsi->fs_path)) == 0) {
+        return 0;
+      }
+    }
+  }
+
+  /* Also check the path against the default '/' path. */
+  if (*path == '/') {
+    return 0;
+  }
+
+  errno = ENOENT;
+  return -1;
+}
+
+void pr_fs_virtual_path(const char *path, char *buf, size_t buflen) {
+  char curpath[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
+       workpath[PR_TUNABLE_PATH_MAX + 1] = {'\0'},
+       namebuf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'},
+       *where = NULL, *ptr = NULL, *last = NULL;
+  int fini = 1;
+
+  if (path == NULL) {
+    return;
+  }
+
+  if (pr_fs_interpolate(path, curpath, sizeof(curpath)-1) != -1) {
+    sstrncpy(curpath, path, sizeof(curpath));
+  }
+
+  if (curpath[0] != '/') {
+    sstrncpy(workpath, vwd, sizeof(workpath));
+
+  } else {
+    workpath[0] = '\0';
+  }
+
+  /* curpath is path resolving */
+  /* linkpath is path a symlink pointed to */
+  /* workpath is the path we've resolved */
+
+  /* main loop */
+  while (fini--) {
+    where = curpath;
+    while (*where != '\0') {
+      if (strncmp(where, ".", 2) == 0) {
+        where++;
+        continue;
+      }
+
+      /* handle "./" */
+      if (strncmp(where, "./", 2) == 0) {
+        where += 2;
+        continue;
+      }
+
+      /* handle ".." */
+      if (strncmp(where, "..", 3) == 0) {
+        where += 2;
+        ptr = last = workpath;
+        while (*ptr) {
+          if (*ptr == '/') {
+            last = ptr;
+          }
+          ptr++;
+        }
+
+        *last = '\0';
+        continue;
+      }
+
+      /* handle "../" */
+      if (strncmp(where, "../", 3) == 0) {
+        where += 3;
+        ptr = last = workpath;
+        while (*ptr) {
+          if (*ptr == '/') {
+            last = ptr;
+          }
+          ptr++;
+        }
+
+        *last = '\0';
+        continue;
+      }
+
+      ptr = strchr(where, '/');
+      if (ptr == NULL) {
+        size_t wherelen = strlen(where);
+
+        ptr = where;
+        if (wherelen >= 1) {
+          ptr += (wherelen - 1);
+        }
+
+      } else {
+        *ptr = '\0';
+      }
+
+      sstrncpy(namebuf, workpath, sizeof(namebuf));
+
+      if (*namebuf) {
+        for (last = namebuf; *last; last++);
+        if (*--last != '/') {
+          sstrcat(namebuf, "/", sizeof(namebuf)-1);
+        }
+
+      } else {
+        sstrcat(namebuf, "/", sizeof(namebuf)-1);
+      }
+
+      sstrcat(namebuf, where, sizeof(namebuf)-1);
+
+      where = ++ptr;
+
+      sstrncpy(workpath, namebuf, sizeof(workpath));
+    }
+  }
+
+  if (!workpath[0]) {
+    sstrncpy(workpath, "/", sizeof(workpath));
+  }
+
+  sstrncpy(buf, workpath, buflen);
+}
+
+int pr_fsio_chdir_canon(const char *path, int hidesymlink) {
+  char resbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+  pr_fs_t *fs = NULL;
+  int res = 0;
+
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (pr_fs_resolve_partial(path, resbuf, sizeof(resbuf)-1,
+      FSIO_DIR_CHDIR) < 0) {
+    return -1;
+  }
+
+  fs = lookup_dir_fs(resbuf, FSIO_DIR_CHDIR);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom chdir handler.  If there are none,
+   * use the system chdir.
+   */
+  while (fs && fs->fs_next && !fs->chdir) {
+    fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s chdir() for path '%s'", fs->fs_name,
+    path);
+  res = (fs->chdir)(fs, resbuf);
+
+  if (res == 0) {
+    /* chdir succeeded, so we set fs_cwd for future references. */
+     fs_cwd = fs;
+
+     if (hidesymlink) {
+       pr_fs_virtual_path(path, vwd, sizeof(vwd)-1);
+
+     } else {
+       sstrncpy(vwd, resbuf, sizeof(vwd));
+     }
+  }
+
+  return res;
+}
+
+int pr_fsio_chdir(const char *path, int hidesymlink) {
+  char resbuf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+  pr_fs_t *fs = NULL;
+  int res;
+
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_fs_clean_path(path, resbuf, sizeof(resbuf)-1);
+
+  fs = lookup_dir_fs(path, FSIO_DIR_CHDIR);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom chdir handler.  If there are none,
+   * use the system chdir.
+   */
+  while (fs && fs->fs_next && !fs->chdir) {
+    fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s chdir() for path '%s'", fs->fs_name,
+    path);
+  res = (fs->chdir)(fs, resbuf);
+  if (res == 0) {
+    /* chdir succeeded, so we set fs_cwd for future references. */
+    fs_cwd = fs;
+
+    if (hidesymlink) {
+      pr_fs_virtual_path(path, vwd, sizeof(vwd)-1);
+
+    } else {
+      sstrncpy(vwd, resbuf, sizeof(vwd));
+    }
+  }
+
+  return res;
+}
+
+/* fs_opendir, fs_closedir and fs_readdir all use a nifty
+ * optimization, caching the last-recently-used pr_fs_t, and
+ * avoid future pr_fs_t lookups when iterating via readdir.
+ */
+void *pr_fsio_opendir(const char *path) {
+  pr_fs_t *fs = NULL;
+  fsopendir_t *fsod = NULL, *fsodi = NULL;
+  pool *fsod_pool = NULL;
+  DIR *res = NULL;
+
+  if (path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (strchr(path, '/') == NULL) {
+    pr_fs_setcwd(pr_fs_getcwd());
+    fs = fs_cwd;
+
+  } else {
+    char buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
+
+    if (pr_fs_resolve_partial(path, buf, sizeof(buf)-1, FSIO_DIR_OPENDIR) < 0) {
+      return NULL;
+    }
+
+    fs = lookup_dir_fs(buf, FSIO_DIR_OPENDIR);
+  }
+
+  /* Find the first non-NULL custom opendir handler.  If there are none,
+   * use the system opendir.
+   */
+  while (fs && fs->fs_next && !fs->opendir) {
+    fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s opendir() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->opendir)(fs, path);
+  if (res == NULL) {
+    return NULL;
+  }
+
+  /* Cache it here */
+  fs_cache_dir = res;
+  fs_cache_fsdir = fs;
+
+  fsod_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(fsod_pool, "fsod subpool");
+
+  fsod = pcalloc(fsod_pool, sizeof(fsopendir_t));
+  if (fsod == NULL) {
+    if (fs->closedir) {
+      (fs->closedir)(fs, res);
+      errno = ENOMEM;
+      return NULL;
+    }
+
+    sys_closedir(fs, res);
+    errno = ENOMEM;
+    return NULL;
+  }
+
+  fsod->pool = fsod_pool;
+  fsod->dir = res;
+  fsod->fsdir = fs;
+  fsod->next = NULL;
+  fsod->prev = NULL;
+
+  if (fsopendir_list) {
+
+    /* find the end of the fsopendir list */
+    fsodi = fsopendir_list;
+    while (fsodi->next) {
+      pr_signals_handle();
+      fsodi = fsodi->next;
+    }
+
+    fsod->next = NULL;
+    fsod->prev = fsodi;
+    fsodi->next = fsod;
+
+  } else {
+    /* This fsopendir _becomes_ the start of the fsopendir list */
+    fsopendir_list = fsod;
+  }
+
+  return res;
+}
+
+static pr_fs_t *find_opendir(void *dir, int closing) {
+  pr_fs_t *fs = NULL;
+
+  if (fsopendir_list) {
+    fsopendir_t *fsod;
+
+    for (fsod = fsopendir_list; fsod; fsod = fsod->next) {
+      if (fsod->dir != NULL &&
+          fsod->dir == dir) {
+        fs = fsod->fsdir;
+        break;
+      }
+    }
+   
+    if (closing && fsod) {
+      if (fsod->prev) {
+        fsod->prev->next = fsod->next;
+      }
+ 
+      if (fsod->next) {
+        fsod->next->prev = fsod->prev;
+      }
+
+      if (fsod == fsopendir_list) {
+        fsopendir_list = fsod->next;
+      }
+
+      destroy_pool(fsod->pool);
+      fsod->pool = NULL;
+    }
+  }
+
+  if (dir == fs_cache_dir) {
+    fs = fs_cache_fsdir;
+
+    if (closing) {
+      fs_cache_dir = NULL;
+      fs_cache_fsdir = NULL;
+    }
+  }
+
+  if (fs == NULL) {
+    errno = ENOTDIR;
+  }
+
+  return fs;
+}
+
+int pr_fsio_closedir(void *dir) {
+  int res;
+  pr_fs_t *fs;
+
+  if (dir == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  fs = find_opendir(dir, TRUE);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom closedir handler.  If there are none,
+   * use the system closedir.
+   */
+  while (fs && fs->fs_next && !fs->closedir) {
+    fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s closedir()", fs->fs_name);
+  res = (fs->closedir)(fs, dir);
+
+  return res;
+}
+
+struct dirent *pr_fsio_readdir(void *dir) {
+  struct dirent *res;
+  pr_fs_t *fs;
+
+  if (dir == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  fs = find_opendir(dir, FALSE);
+  if (fs == NULL) {
+    return NULL;
+  }
+
+  /* Find the first non-NULL custom readdir handler.  If there are none,
+   * use the system readdir.
+   */
+  while (fs && fs->fs_next && !fs->readdir) {
+    fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s readdir()", fs->fs_name);
+  res = (fs->readdir)(fs, dir);
+
+  return res;
+}
+
+int pr_fsio_mkdir(const char *path, mode_t mode) {
+  int res;
+  pr_fs_t *fs;
+
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  fs = lookup_dir_fs(path, FSIO_DIR_MKDIR);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom mkdir handler.  If there are none,
+   * use the system mkdir.
+   */
+  while (fs && fs->fs_next && !fs->mkdir) {
+    fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s mkdir() for path '%s'", fs->fs_name,
+    path);
+  res = (fs->mkdir)(fs, path, mode);
+  if (res == 0) {
+    pr_fs_clear_cache2(path);
+  }
+
+  return res;
+}
+
+int pr_fsio_guard_chroot(int guard) {
+  int prev;
+
+  prev = fsio_guard_chroot;
+  fsio_guard_chroot = guard;
+
+  return prev;
+}
+
+unsigned long pr_fsio_set_options(unsigned long opts) {
+  unsigned long prev;
+
+  prev = fsio_opts;
+  fsio_opts = opts;
+
+  return prev;
+}
+
+int pr_fsio_set_use_mkdtemp(int value) {
+  int prev_value;
+
+  if (value != TRUE &&
+      value != FALSE) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  prev_value = fsio_use_mkdtemp;
+
+#ifdef HAVE_MKDTEMP
+  fsio_use_mkdtemp = value;
+#endif /* HAVE_MKDTEMP */
+
+  return prev_value;
+}
+
+/* Directory-specific "safe" chmod(2) which attempts to avoid/mitigate
+ * symlink attacks.
+ * 
+ * To do this, we first open a file descriptor on the given path, using
+ * O_NOFOLLOW to avoid symlinks.  If the fd is not to a directory, it's
+ * an error.  Then we use fchmod(2) to set the perms.  There is still a
+ * race condition here, between the time the directory is created and
+ * when we call open(2).  But hopefully the ensuing checks on the fd
+ * (i.e. that it IS a directory) can mitigate that race.
+ *
+ * The fun part is ensuring that the OS/filesystem will give us an fd
+ * on a directory path (using O_RDONLY to avoid getting an EISDIR error),
+ * whilst being able to do a write (effectively) on the fd by changing
+ * its permissions.
+ */
+static int schmod_dir(pool *p, const char *path, mode_t perms, int use_root) {
+  int flags, fd, ignore_eacces = FALSE, ignore_eperm = FALSE, res, xerrno = 0;
+  struct stat st;
+  mode_t dir_mode;
+
+  /* We're not using the pool at the moment. */
+  (void) p;
+
+  /* Open an fd on the path using O_RDONLY|O_NOFOLLOW, so that we a)
+   * avoid symlinks, and b) get an fd on the (hopefully) directory.
+   */
+  flags = O_RDONLY;
+#ifdef O_NOFOLLOW
+  flags |= O_NOFOLLOW;
+#endif
+  fd = open(path, flags);
+  xerrno = errno;
+
+  if (fd < 0) {
+    pr_trace_msg(trace_channel, 3,
+      "schmod: unable to open path '%s': %s", path, strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  res = fstat(fd, &st);
+  if (res < 0) {
+    xerrno = errno;
+
+    (void) close(fd);
+
+    pr_trace_msg(trace_channel, 3,
+      "schmod: unable to fstat path '%s': %s", path, strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  /* We expect only directories. */
+  if (!S_ISDIR(st.st_mode)) {
+    xerrno = ENOTDIR;
+
+    (void) close(fd);
+  
+    pr_trace_msg(trace_channel, 3,
+      "schmod: unable to use path '%s': %s", path, strerror(xerrno));
+
+    /* This is such an unexpected (and possibly malicious) situation that
+     * it warrants louder logging.
+     */
+    pr_log_pri(PR_LOG_WARNING,
+      "WARNING: detected non-directory '%s' during directory creation: "
+      "possible symlink attack", path);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  /* Note that some filesystems (e.g. CIFS) may not actually create a
+   * directory with the expected 0700 mode.  If that is the case, then a
+   * subsequence chmod(2) on that directory will likely fail.  Thus we also
+   * double-check the mode of the directory created via mkdtemp(3), and
+   * attempt to mitigate Bug#4063.
+   */
+  dir_mode = (st.st_mode & ~S_IFMT);
+  if (dir_mode != 0700) {
+    ignore_eacces = ignore_eperm = TRUE;
+
+    pr_trace_msg(trace_channel, 3,
+      "schmod: path '%s' has mode %04o, expected 0700", path, dir_mode);
+
+    /* This is such an unexpected situation that it warrants some logging. */
+    pr_log_pri(PR_LOG_DEBUG,
+      "NOTICE: directory '%s' has unexpected mode %04o (expected 0700)", path,
+      dir_mode);
+  }
+
+  if (use_root) {
+    PRIVS_ROOT
+  }
+
+  res = fchmod(fd, perms);
+  xerrno = errno;
+
+  /* Using fchmod(2) on a directory descriptor is not really kosher
+   * behavior, but appears to work on most filesystems.  Still, if we
+   * get an ENOENT back (as seen on some CIFS mounts, per Bug#4134), try
+   * using chmod(2) on the path.
+   */
+  if (res < 0 &&
+      xerrno == ENOENT) {
+    ignore_eacces = TRUE;
+    res = chmod(path, perms);
+    xerrno = errno;
+  }
+
+  if (use_root) {
+    PRIVS_RELINQUISH
+  }
+
+  /* At this point, succeed or fail, we're done with the fd. */
+  (void) close(fd);
+
+  if (res < 0) {
+    /* Note: Some filesystem implementations, particularly via FUSE,
+     * may not actually implement ownership/permissions (e.g. FAT-based
+     * filesystems).  In such cases, chmod(2) et al will return ENOSYS
+     * (see Bug#3986).
+     *
+     * Other filesystem implementations (e.g. CIFS, depending on the mount
+     * options) will a chmod(2) that returns ENOENT (see Bug#4134).
+     *
+     * Should this fail the entire operation?  I'm of two minds about this.
+     * On the one hand, such filesystem behavior can undermine wider site
+     * security policies; on the other, prohibiting a MKD/MKDIR operation
+     * on such filesystems, deliberately used by the site admin, is not
+     * useful/friendly behavior.
+     *
+     * Maybe these exceptions for ENOSYS/ENOENT here should be made
+     * configurable?
+     */
+
+    if (xerrno == ENOSYS ||
+        xerrno == ENOENT ||
+        (xerrno == EACCES && ignore_eacces == TRUE) ||
+        (xerrno == EPERM && ignore_eperm == TRUE)) {
+      pr_log_debug(DEBUG0, "schmod: unable to set perms %04o on "
+        "path '%s': %s (chmod(2) not supported by underlying filesystem?)",
+        perms, path, strerror(xerrno));
+      return 0;
+    }
+
+    pr_trace_msg(trace_channel, 3,
+      "schmod: unable to set perms %04o on path '%s': %s", perms, path,
+      strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+/* "safe mkdir" variant of mkdir(2), uses mkdtemp(3), lchown(2), and
+ * rename(2) to create a directory which cannot be hijacked by a symlink
+ * race (hopefully) before the UserOwner/GroupOwner ownership changes are
+ * applied.
+ */
+int pr_fsio_smkdir(pool *p, const char *path, mode_t mode, uid_t uid,
+    gid_t gid) {
+  int res, set_sgid = FALSE, use_mkdtemp, use_root_chown = FALSE, xerrno = 0;
+  char *tmpl_path;
+  char *dst_dir, *tmpl;
+  size_t dst_dirlen, tmpl_len;
+
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 9,
+    "smkdir: path '%s', mode %04o, UID %s, GID %s", path, (unsigned int) mode,
+    pr_uid2str(p, uid), pr_gid2str(p, gid));
+
+  if (fsio_guard_chroot) {
+    res = chroot_allow_path(path);
+    if (res < 0) {
+      return -1;
+    }
+  }
+
+  use_mkdtemp = fsio_use_mkdtemp;
+  if (use_mkdtemp == TRUE) {
+
+    /* Note that using mkdtemp(3) is a way of dealing with Bug#3841.  The
+     * problem in question, though, only applies if root privs are used
+     * to set the ownership.  Thus if root privs are NOT needed, then there
+     * is no need to use mkdtemp(3).
+     */
+
+    if (uid != (uid_t) -1) {
+      use_root_chown = TRUE;
+
+    } else if (gid != (gid_t) -1) {
+      register unsigned int i;
+
+      use_root_chown = TRUE;
+
+      /* Check if session.fsgid is in session.gids.  If not, use root privs.  */
+      for (i = 0; i < session.gids->nelts; i++) {
+        gid_t *group_ids = session.gids->elts;
+
+        if (group_ids[i] == gid) {
+          use_root_chown = FALSE;
+          break;
+        }
+      }
+    }
+
+    if (use_root_chown == FALSE) {
+      use_mkdtemp = FALSE;
+    }
+  }
+
+#ifdef HAVE_MKDTEMP
+  if (use_mkdtemp == TRUE) {
+    char *ptr;
+    struct stat st;
+
+    ptr = strrchr(path, '/');
+    if (ptr == NULL) {
+      errno = EINVAL;
+      return -1;
+    }
+
+    if (ptr != path) {
+      dst_dirlen = (ptr - path);
+      dst_dir = pstrndup(p, path, dst_dirlen);
+
+    } else {
+      dst_dirlen = 1;
+      dst_dir = "/";
+    }
+
+    res = lstat(dst_dir, &st);
+    if (res < 0) {
+      xerrno = errno;
+
+      pr_log_pri(PR_LOG_WARNING,
+        "smkdir: unable to lstat(2) parent directory '%s': %s", dst_dir,
+        strerror(xerrno));
+      pr_trace_msg(trace_channel, 1,
+        "smkdir: unable to lstat(2) parent directory '%s': %s", dst_dir,
+        strerror(xerrno));
+
+      errno = xerrno;
+      return -1;
+    }
+
+    if (!S_ISDIR(st.st_mode) &&
+        !S_ISLNK(st.st_mode)) {
+      errno = EPERM;
+      return -1;
+    }
+
+    if (st.st_mode & S_ISGID) {
+      set_sgid = TRUE;
+    }
+
+    /* Allocate enough space for the temporary name: the length of the
+     * destination directory, a slash, 9 X's, 3 for the prefix, and 1 for the
+     * trailing NUL.
+     */
+    tmpl_len = dst_dirlen + 15;
+    tmpl = pcalloc(p, tmpl_len);
+    snprintf(tmpl, tmpl_len-1, "%s/.dstXXXXXXXXX",
+      dst_dirlen > 1 ? dst_dir : "");
+
+    /* Use mkdtemp(3) to create the temporary directory (in the same destination
+     * directory as the target path).
+     */
+    tmpl_path = mkdtemp(tmpl);
+    if (tmpl_path == NULL) {
+      xerrno = errno;
+
+      pr_log_pri(PR_LOG_WARNING,
+        "smkdir: mkdtemp(3) failed to create directory using '%s': %s", tmpl,
+        strerror(xerrno));
+      pr_trace_msg(trace_channel, 1,
+        "smkdir: mkdtemp(3) failed to create directory using '%s': %s", tmpl,
+        strerror(xerrno));
+
+      errno = xerrno;
+      return -1;
+    }
+
+  } else {
+    res = pr_fsio_mkdir(path, mode);
+    if (res < 0) {
+      xerrno = errno;
+
+      pr_trace_msg(trace_channel, 1,
+        "mkdir(2) failed to create directory '%s' with perms %04o: %s", path,
+        mode, strerror(xerrno));
+
+      errno = xerrno;
+      return -1;
+    }
+
+    tmpl_path = pstrdup(p, path);
+  }
+#else
+
+  res = pr_fsio_mkdir(path, mode);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 1,
+      "mkdir(2) failed to create directory '%s' with perms %04o: %s", path,
+      mode, strerror(xerrno));
+        
+    errno = xerrno;
+    return -1;
+  }
+
+  tmpl_path = pstrdup(p, path);
+#endif /* HAVE_MKDTEMP */
+
+  if (use_mkdtemp == TRUE) {
+    mode_t mask, *dir_umask, perms;
+
+    /* mkdtemp(3) creates a directory with 0700 perms; we are given the
+     * target mode (modulo the configured Umask).
+     */
+    dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
+    if (dir_umask == NULL) {
+      /* If Umask was configured with a single parameter, then DirUmask
+       * would not be present; we still should check for Umask.
+       */
+      dir_umask = get_param_ptr(CURRENT_CONF, "Umask", FALSE);
+    }
+
+    if (dir_umask) {
+      mask = *dir_umask;
+
+    } else {
+      mask = (mode_t) 0022;
+    }
+
+    perms = (mode & ~mask);
+
+    if (set_sgid) {
+      perms |= S_ISGID;
+    }
+
+    /* If we're setting the SGID bit, we need to use root privs, in order
+     * to reliably set the SGID bit.  Sigh.
+     */
+    res = schmod_dir(p, tmpl_path, perms, set_sgid);
+    xerrno = errno;
+
+    if (set_sgid) {
+      if (res < 0 &&
+          xerrno == EPERM) {
+        /* Try again, this time without root privs.  NFS situations which
+         * squash root privs could cause the above chmod(2) to fail; it
+         * might succeed now that we've dropped root privs (Bug#3962).
+         */
+        res = schmod_dir(p, tmpl_path, perms, FALSE);
+        xerrno = errno;
+      }
+    }
+
+    if (res < 0) {
+      pr_log_pri(PR_LOG_WARNING, "chmod(%s) failed: %s", tmpl_path,
+        strerror(xerrno));
+
+      (void) rmdir(tmpl_path);
+
+      errno = xerrno;
+      return -1;
+    }
+  }
+
+  if (uid != (uid_t) -1) {
+    if (use_root_chown) {
+      PRIVS_ROOT
+    }
+
+    res = pr_fsio_lchown(tmpl_path, uid, gid);
+    xerrno = errno;
+
+    if (use_root_chown) {
+      PRIVS_RELINQUISH
+    }
+
+    if (res < 0) {
+      pr_log_pri(PR_LOG_WARNING, "lchown(%s) as root failed: %s", tmpl_path,
+        strerror(xerrno));
+
+    } else {
+      if (gid != (gid_t) -1) {
+        pr_log_debug(DEBUG2, "root lchown(%s) to UID %s, GID %s successful",
+          tmpl_path, pr_uid2str(p, uid), pr_gid2str(p, gid));
+
+      } else {
+        pr_log_debug(DEBUG2, "root lchown(%s) to UID %s successful",
+          tmpl_path, pr_uid2str(NULL, uid));
+      }
+    }
+
+  } else if (gid != (gid_t) -1) {
+    if (use_root_chown) {
+      PRIVS_ROOT
+    }
+
+    res = pr_fsio_lchown(tmpl_path, (uid_t) -1, gid);
+    xerrno = errno;
+
+    if (use_root_chown) {
+      PRIVS_RELINQUISH
+    }
+
+    if (res < 0) {
+      pr_log_pri(PR_LOG_WARNING, "%slchown(%s) failed: %s",
+        use_root_chown ? "root " : "", tmpl_path, strerror(xerrno));
+
+    } else {
+      pr_log_debug(DEBUG2, "%slchown(%s) to GID %s successful",
+        use_root_chown ? "root " : "", tmpl_path, pr_gid2str(p, gid));
+    }
+  }
+
+  if (use_mkdtemp == TRUE) {
+    /* Use rename(2) to move the temporary directory into place at the
+     * target path.
+     */
+    res = rename(tmpl_path, path);
+    if (res < 0) {
+      xerrno = errno;
+
+      pr_log_pri(PR_LOG_INFO, "renaming '%s' to '%s' failed: %s", tmpl_path,
+        path, strerror(xerrno));
+
+      (void) rmdir(tmpl_path);
+
+#ifdef ENOTEMPTY
+      if (xerrno == ENOTEMPTY) {
+        /* If the rename(2) failed with "Directory not empty" (ENOTEMPTY),
+         * then change the errno to "File exists" (EEXIST), so that the
+         * error reported to the client is more indicative of the actual
+         * cause.
+         */
+        xerrno = EEXIST;
+      }
+#endif /* ENOTEMPTY */
+ 
+      errno = xerrno;
+      return -1;
+    }
+  }
+
+  pr_fs_clear_cache2(path);
+  return 0;
+}
+
+int pr_fsio_rmdir(const char *path) {
+  int res;
+  pr_fs_t *fs;
 
-  if (fsopendir_list) {
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-    /* find the end of the fsopendir list */
-    fsodi = fsopendir_list;
-    while (fsodi->next) {
-      pr_signals_handle();
-      fsodi = fsodi->next;
-    }
+  fs = lookup_dir_fs(path, FSIO_DIR_RMDIR);
+  if (fs == NULL) {
+    return -1;
+  }
 
-    fsod->next = NULL;
-    fsod->prev = fsodi;
-    fsodi->next = fsod;
+  /* Find the first non-NULL custom rmdir handler.  If there are none,
+   * use the system rmdir.
+   */
+  while (fs && fs->fs_next && !fs->rmdir) {
+    fs = fs->fs_next;
+  }
 
-  } else {
-    /* This fsopendir _becomes_ the start of the fsopendir list */
-    fsopendir_list = fsod;
+  pr_trace_msg(trace_channel, 8, "using %s rmdir() for path '%s'", fs->fs_name,
+    path);
+  res = (fs->rmdir)(fs, path);
+  if (res == 0) {
+    pr_fs_clear_cache2(path);
   }
 
   return res;
 }
 
-static pr_fs_t *find_opendir(void *dir, int closing) {
-  pr_fs_t *fs = NULL;
+int pr_fsio_stat_canon(const char *path, struct stat *st) {
+  pr_fs_t *fs;
 
-  if (fsopendir_list) {
-    fsopendir_t *fsod;
+  if (path == NULL ||
+      st == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-    for (fsod = fsopendir_list; fsod; fsod = fsod->next) {
-      if (fsod->dir && fsod->dir == dir) {
-        fs = fsod->fsdir;
-        break;
-      }
-    }
-   
-    if (closing && fsod) {
-      if (fsod->prev)
-        fsod->prev->next = fsod->next;
- 
-      if (fsod->next)
-        fsod->next->prev = fsod->prev;
+  fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_STAT);
+  if (fs == NULL) {
+    return -1;
+  }
 
-      if (fsod == fsopendir_list)
-        fsopendir_list = fsod->next;
+  /* Find the first non-NULL custom stat handler.  If there are none,
+   * use the system stat.
+   */
+  while (fs && fs->fs_next && !fs->stat) {
+    fs = fs->fs_next;
+  }
 
-      destroy_pool(fsod->pool);
-    }
+  pr_trace_msg(trace_channel, 8, "using %s stat() for path '%s'",
+    fs ? fs->fs_name : "system", path);
+  return fs_cache_stat(fs ? fs : root_fs, path, st);
+}
+
+int pr_fsio_stat(const char *path, struct stat *st) {
+  pr_fs_t *fs = NULL;
+
+  if (path == NULL ||
+      st == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  if (dir == fs_cache_dir) {
-    fs = fs_cache_fsdir;
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_STAT);
+  if (fs == NULL) {
+    return -1;
+  }
 
-    if (closing) {
-      fs_cache_dir = NULL;
-      fs_cache_fsdir = NULL;
-    }
+  /* Find the first non-NULL custom stat handler.  If there are none,
+   * use the system stat.
+   */
+  while (fs && fs->fs_next && !fs->stat) {
+    fs = fs->fs_next;
   }
 
-  return fs;
+  pr_trace_msg(trace_channel, 8, "using %s stat() for path '%s'", fs->fs_name,
+    path);
+  return fs_cache_stat(fs ? fs : root_fs, path, st);
 }
 
-int pr_fsio_closedir(void *dir) {
+int pr_fsio_fstat(pr_fh_t *fh, struct stat *st) {
   int res;
-  pr_fs_t *fs = find_opendir(dir, TRUE);
+  pr_fs_t *fs;
 
-  if (!fs)
+  if (fh == NULL ||
+      st == NULL) {
+    errno = EINVAL;
     return -1;
+  }
 
-  /* Find the first non-NULL custom closedir handler.  If there are none,
-   * use the system closedir.
+  /* Find the first non-NULL custom fstat handler.  If there are none,
+   * use the system fstat.
    */
-  while (fs && fs->fs_next && !fs->closedir)
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->fstat) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s closedir()", fs->fs_name);
-  res = (fs->closedir)(fs, dir);
+  pr_trace_msg(trace_channel, 8, "using %s fstat() for path '%s'", fs->fs_name,
+    fh->fh_path);
+  res = (fs->fstat)(fh, fh->fh_fd, st);
 
   return res;
 }
 
-struct dirent *pr_fsio_readdir(void *dir) {
-  struct dirent *res;
-  pr_fs_t *fs = find_opendir(dir, FALSE);
+int pr_fsio_lstat_canon(const char *path, struct stat *st) {
+  pr_fs_t *fs;
 
-  if (!fs)
-    return NULL;
+  if (path == NULL ||
+      st == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  /* Find the first non-NULL custom readdir handler.  If there are none,
-   * use the system readdir.
+  fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_LSTAT);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom lstat handler.  If there are none,
+   * use the system lstat.
    */
-  while (fs && fs->fs_next && !fs->readdir)
+  while (fs && fs->fs_next && !fs->lstat) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s readdir()", fs->fs_name);
-  res = (fs->readdir)(fs, dir);
-
-  return res;
+  pr_trace_msg(trace_channel, 8, "using %s lstat() for path '%s'",
+    fs ? fs->fs_name : "system", path);
+  return fs_cache_lstat(fs ? fs : root_fs, path, st);
 }
 
-int pr_fsio_mkdir(const char *path, mode_t mode) {
-  int res;
+int pr_fsio_lstat(const char *path, struct stat *st) {
   pr_fs_t *fs;
 
-  fs = lookup_dir_fs(path, FSIO_DIR_MKDIR);
+  if (path == NULL ||
+      st == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_LSTAT);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom mkdir handler.  If there are none,
-   * use the system mkdir.
+  /* Find the first non-NULL custom lstat handler.  If there are none,
+   * use the system lstat.
    */
-  while (fs && fs->fs_next && !fs->mkdir)
+  while (fs && fs->fs_next && !fs->lstat) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s mkdir() for path '%s'", fs->fs_name,
+  pr_trace_msg(trace_channel, 8, "using %s lstat() for path '%s'", fs->fs_name,
     path);
-  res = (fs->mkdir)(fs, path, mode);
-
-  return res;
+  return fs_cache_lstat(fs ? fs : root_fs, path, st);
 }
 
-int pr_fsio_guard_chroot(int guard) {
-  int prev;
-
-  prev = guard_chroot;
-  guard_chroot = guard;
+int pr_fsio_readlink_canon(const char *path, char *buf, size_t buflen) {
+  int res;
+  pr_fs_t *fs;
 
-  return prev;
-}
+  if (path == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-int pr_fsio_set_use_mkdtemp(int value) {
-  int prev_value;
+  fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_READLINK);
+  if (fs == NULL) {
+    return -1;
+  }
 
-  prev_value = use_mkdtemp;
+  /* Find the first non-NULL custom readlink handler.  If there are none,
+   * use the system readlink.
+   */
+  while (fs && fs->fs_next && !fs->readlink) {
+    fs = fs->fs_next;
+  }
 
-#ifdef HAVE_MKDTEMP
-  use_mkdtemp = value;
-#endif /* HAVE_MKDTEMP */
+  pr_trace_msg(trace_channel, 8, "using %s readlink() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->readlink)(fs, path, buf, buflen);
 
-  return prev_value;
+  return res;
 }
 
-/* Directory-specific "safe" chmod(2) which attempts to avoid/mitigate
- * symlink attacks.
- * 
- * To do this, we first open a file descriptor on the given path, using
- * O_NOFOLLOW to avoid symlinks.  If the fd is not to a directory, it's
- * an error.  Then we use fchmod(2) to set the perms.  There is still a
- * race condition here, between the time the directory is created and
- * when we call open(2).  But hopefully the ensuing checks on the fd
- * (i.e. that it IS a directory) can mitigate that race.
- *
- * The fun part is ensuring that the OS/filesystem will give us an fd
- * on a directory path (using O_RDONLY to avoid getting an EISDIR error),
- * whilst being able to do a write (effectively) on the fd by changing
- * its permissions.
- */
-static int schmod_dir(pool *p, const char *path, mode_t perms, int use_root) {
-  int flags, fd, ignore_eacces = FALSE, res, xerrno = 0;
-  struct stat st;
-  mode_t dir_mode;
-
-  /* We're not using the pool at the moment. */
-  (void) p;
-
-  /* Open an fd on the path using O_RDONLY|O_NOFOLLOW, so that we a)
-   * avoid symlinks, and b) get an fd on the (hopefully) directory.
-   */
-  flags = O_RDONLY;
-#ifdef O_NOFOLLOW
-  flags |= O_NOFOLLOW;
-#endif
-  fd = open(path, flags);
-  xerrno = errno;
+int pr_fsio_readlink(const char *path, char *buf, size_t buflen) {
+  int res;
+  pr_fs_t *fs;
 
-  if (fd < 0) {
-    pr_trace_msg(trace_channel, 3,
-      "schmod: unable to open path '%s': %s", path, strerror(xerrno));
-    errno = xerrno;
+  if (path == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  res = fstat(fd, &st);
-  if (res < 0) {
-    xerrno = errno;
-
-    (void) close(fd);
-
-    pr_trace_msg(trace_channel, 3,
-      "schmod: unable to fstat path '%s': %s", path, strerror(xerrno));
-    errno = xerrno;
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_READLINK);
+  if (fs == NULL) {
     return -1;
   }
 
-  /* We expect only directories. */
-  if (!S_ISDIR(st.st_mode)) {
-    xerrno = ENOTDIR;
-
-    (void) close(fd);
-  
-    pr_trace_msg(trace_channel, 3,
-      "schmod: unable to use path '%s': %s", path, strerror(xerrno));
+  /* Find the first non-NULL custom readlink handler.  If there are none,
+   * use the system readlink.
+   */
+  while (fs && fs->fs_next && !fs->readlink) {
+    fs = fs->fs_next;
+  }
 
-    /* This is such an unexpected (and possibly malicious) situation that
-     * it warrants louder logging.
-     */
-    pr_log_pri(PR_LOG_WARNING,
-      "WARNING: detected non-directory '%s' during directory creation: "
-      "possible symlink attack", path);
+  pr_trace_msg(trace_channel, 8, "using %s readlink() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->readlink)(fs, path, buf, buflen);
 
-    errno = xerrno;
+  return res;
+}
+
+/* pr_fs_glob() is just a wrapper for glob(3), setting the various gl_
+ * callbacks to our fs functions.
+ */
+int pr_fs_glob(const char *pattern, int flags,
+    int (*errfunc)(const char *, int), glob_t *pglob) {
+
+  if (pattern == NULL ||
+      pglob == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  /* Note that some filesystems (e.g. CIFS) may not actually create a
-   * directory with the expected 0700 mode.  If that is the case, then a
-   * subsequence chmod(2) on that directory will likely fail.  Thus we also
-   * double-check the mode of the directory created via mkdtemp(3), and
-   * attempt to mitigate Bug#4063.
-   */
-  dir_mode = (st.st_mode & ~S_IFMT);
-  if (dir_mode != 0700) {
-    ignore_eacces = TRUE;
+  flags |= GLOB_ALTDIRFUNC;
 
-    pr_trace_msg(trace_channel, 3,
-      "schmod: path '%s' has mode %04o, expected 0700", path, dir_mode);
+  pglob->gl_closedir = (void (*)(void *)) pr_fsio_closedir;
+  pglob->gl_readdir = pr_fsio_readdir;
+  pglob->gl_opendir = pr_fsio_opendir;
+  pglob->gl_lstat = pr_fsio_lstat;
+  pglob->gl_stat = pr_fsio_stat;
 
-    /* This is such an unexpected situation that it warrants some logging. */
-    pr_log_pri(PR_LOG_DEBUG,
-      "NOTICE: directory '%s' has unexpected mode %04o (expected 0700)", path,
-      dir_mode);
-  }
+  return glob(pattern, flags, errfunc, pglob);
+}
 
-  if (use_root) {
-    PRIVS_ROOT
+void pr_fs_globfree(glob_t *pglob) {
+  if (pglob != NULL) {
+    globfree(pglob);
   }
+}
 
-  res = fchmod(fd, perms);
-  xerrno = errno;
+int pr_fsio_rename_canon(const char *rnfr, const char *rnto) {
+  int res;
+  pr_fs_t *from_fs, *to_fs, *fs;
 
-  if (use_root) {
-    PRIVS_RELINQUISH
+  if (rnfr == NULL ||
+      rnto == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  /* At this point, succeed or fail, we're done with the fd. */
-  (void) close(fd);
+  from_fs = lookup_file_canon_fs(rnfr, NULL, FSIO_FILE_RENAME);
+  if (from_fs == NULL) {
+    return -1;
+  }
 
-  if (res < 0) {
-    /* Note: Some filesystem implementations, particularly via FUSE,
-     * may not actually implement ownership/permissions (e.g. FAT-based
-     * filesystems).  In such cases, chmod(2) et al will return ENOSYS
-     * (see Bug#3986).
-     *
-     * Should this fail the entire operation?  I'm of two minds about this.
-     * On the one hand, such filesystem behavior can undermine wider site
-     * security policies; on the other, prohibiting a MKD/MKDIR operation
-     * on such filesystems, deliberately used by the site admin, is not
-     * useful/friendly behavior.
-     *
-     * Maybe this exception for ENOSYS here should be made configurable?
-     */
+  to_fs = lookup_file_canon_fs(rnto, NULL, FSIO_FILE_RENAME);
+  if (to_fs == NULL) {
+    return -1;
+  }
 
-    if (xerrno == ENOSYS) {
-      pr_log_debug(DEBUG0, "schmod: unable to set perms %04o on "
-        "path '%s': %s (chmod(2) not supported by underlying filesystem?)",
-        perms, path, strerror(xerrno));
-      return 0;
+  if (from_fs->allow_xdev_rename == FALSE ||
+      to_fs->allow_xdev_rename == FALSE) {
+    if (from_fs != to_fs) {
+      errno = EXDEV;
+      return -1;
     }
+  }
 
-    if (xerrno == EACCES &&
-        ignore_eacces == TRUE) {
-      pr_log_debug(DEBUG0, "schmod: unable to set perms %04o on "
-        "path '%s': %s (chmod(2) not supported by underlying filesystem?)",
-        perms, path, strerror(xerrno));
-      return 0;
-    }
+  fs = to_fs;
 
-    pr_trace_msg(trace_channel, 3,
-      "schmod: unable to set perms %04o on path '%s': %s", perms, path,
-      strerror(xerrno));
-    errno = xerrno;
-    return -1;
+  /* Find the first non-NULL custom rename handler.  If there are none,
+   * use the system rename.
+   */
+  while (fs && fs->fs_next && !fs->rename) {
+    fs = fs->fs_next;
   }
 
-  return 0;
+  pr_trace_msg(trace_channel, 8, "using %s rename() for paths '%s', '%s'",
+    fs->fs_name, rnfr, rnto);
+  res = (fs->rename)(fs, rnfr, rnto);
+
+  if (res == 0) {
+    pr_fs_clear_cache2(rnfr);
+    pr_fs_clear_cache2(rnto);
+  }
+
+  return res;
 }
 
-/* "safe mkdir" variant of mkdir(2), uses mkdtemp(3), lchown(2), and
- * rename(2) to create a directory which cannot be hijacked by a symlink
- * race (hopefully) before the UserOwner/GroupOwner ownership changes are
- * applied.
- */
-int pr_fsio_smkdir(pool *p, const char *path, mode_t mode, uid_t uid,
-    gid_t gid) {
-  int res, set_sgid = FALSE, xerrno = 0;
-  char *tmpl_path;
-  char *dst_dir, *tmpl;
-  size_t dst_dirlen, tmpl_len;
+int pr_fsio_rename(const char *rnfr, const char *rnto) {
+  int res;
+  pr_fs_t *from_fs, *to_fs, *fs;
 
-  if (path == NULL) {
+  if (rnfr == NULL ||
+      rnto == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  pr_trace_msg(trace_channel, 9,
-    "smkdir: path '%s', mode %04o, UID %lu, GID %lu", path, (unsigned int) mode,
-    (unsigned long) uid, (unsigned long) gid);
-
-  if (guard_chroot) {
-    res = chroot_allow_path(path);
-    if (res < 0) {
-      return -1;
-    }
+  from_fs = lookup_file_fs(rnfr, NULL, FSIO_FILE_RENAME);
+  if (from_fs == NULL) {
+    return -1;
   }
 
-#ifdef HAVE_MKDTEMP
-  if (use_mkdtemp == TRUE) {
-    char *ptr;
-    struct stat st;
+  to_fs = lookup_file_fs(rnto, NULL, FSIO_FILE_RENAME);
+  if (to_fs == NULL) {
+    return -1;
+  }
 
-    ptr = strrchr(path, '/');
-    if (ptr == NULL) {
-      errno = EINVAL;
+  if (from_fs->allow_xdev_rename == FALSE ||
+      to_fs->allow_xdev_rename == FALSE) {
+    if (from_fs != to_fs) {
+      errno = EXDEV;
       return -1;
     }
+  }
 
-    if (ptr != path) {
-      dst_dirlen = (ptr - path);
-      dst_dir = pstrndup(p, path, dst_dirlen);
+  fs = to_fs;
 
-    } else {
-      dst_dirlen = 1;
-      dst_dir = "/";
-    }
+  /* Find the first non-NULL custom rename handler.  If there are none,
+   * use the system rename.
+   */
+  while (fs && fs->fs_next && !fs->rename) {
+    fs = fs->fs_next;
+  }
 
-    res = lstat(dst_dir, &st);
-    if (res < 0) {
-      xerrno = errno;
+  pr_trace_msg(trace_channel, 8, "using %s rename() for paths '%s', '%s'",
+    fs->fs_name, rnfr, rnto);
+  res = (fs->rename)(fs, rnfr, rnto);
+  if (res == 0) {
+    pr_fs_clear_cache2(rnfr);
+    pr_fs_clear_cache2(rnto);
+  }
 
-      pr_log_pri(PR_LOG_WARNING,
-        "smkdir: unable to lstat(2) parent directory '%s': %s", dst_dir,
-        strerror(xerrno));
-      pr_trace_msg(trace_channel, 1,
-        "smkdir: unable to lstat(2) parent directory '%s': %s", dst_dir,
-        strerror(xerrno));
+  return res;
+}
 
-      errno = xerrno;
-      return -1;
-    }
+int pr_fsio_unlink_canon(const char *name) {
+  int res;
+  pr_fs_t *fs;
 
-    if (!S_ISDIR(st.st_mode) &&
-        !S_ISLNK(st.st_mode)) {
-      errno = EPERM;
-      return -1;
-    }
+  if (name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-    if (st.st_mode & S_ISGID) {
-      set_sgid = TRUE;
-    }
+  fs = lookup_file_canon_fs(name, NULL, FSIO_FILE_UNLINK);
+  if (fs == NULL) {
+    return -1;
+  }
 
-    /* Allocate enough space for the temporary name: the length of the
-     * destination directory, a slash, 9 X's, 3 for the prefix, and 1 for the
-     * trailing NUL.
-     */
-    tmpl_len = dst_dirlen + 15;
-    tmpl = pcalloc(p, tmpl_len);
-    snprintf(tmpl, tmpl_len-1, "%s/.dstXXXXXXXXX",
-      dst_dirlen > 1 ? dst_dir : "");
+  /* Find the first non-NULL custom unlink handler.  If there are none,
+   * use the system unlink.
+   */
+  while (fs && fs->fs_next && !fs->unlink) {
+    fs = fs->fs_next;
+  }
 
-    /* Use mkdtemp(3) to create the temporary directory (in the same destination
-     * directory as the target path).
-     */
-    tmpl_path = mkdtemp(tmpl);
-    if (tmpl_path == NULL) {
-      xerrno = errno;
+  pr_trace_msg(trace_channel, 8, "using %s unlink() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->unlink)(fs, name);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
+  }
 
-      pr_log_pri(PR_LOG_WARNING,
-        "smkdir: mkdtemp(3) failed to create directory using '%s': %s", tmpl,
-        strerror(xerrno));
-      pr_trace_msg(trace_channel, 1,
-        "smkdir: mkdtemp(3) failed to create directory using '%s': %s", tmpl,
-        strerror(xerrno));
+  return res;
+}
 
-      errno = xerrno;
-      return -1;
-    }
+int pr_fsio_unlink(const char *name) {
+  int res;
+  pr_fs_t *fs;
 
-  } else {
-    res = pr_fsio_mkdir(path, mode);
-    if (res < 0) {
-      xerrno = errno;
+  if (name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-      pr_trace_msg(trace_channel, 1,
-        "mkdir(2) fail to create directory '%s' with perms %04o: %s", path,
-        mode, strerror(xerrno));
+  fs = lookup_file_fs(name, NULL, FSIO_FILE_UNLINK);
+  if (fs == NULL) {
+    return -1;
+  }
 
-      errno = xerrno;
-      return -1;
-    }
+  /* Find the first non-NULL custom unlink handler.  If there are none,
+   * use the system unlink.
+   */
+  while (fs && fs->fs_next && !fs->unlink) {
+    fs = fs->fs_next;
+  }
 
-    tmpl_path = pstrdup(p, path);
+  pr_trace_msg(trace_channel, 8, "using %s unlink() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->unlink)(fs, name);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
   }
-#else
 
-  res = pr_fsio_mkdir(path, mode);
-  if (res < 0) {
-    xerrno = errno;
+  return res;
+}
 
-    pr_trace_msg(trace_channel, 1,
-      "mkdir(2) fail to create directory '%s' with perms %04o: %s", path,
-      mode, strerror(xerrno));
-        
-    errno = xerrno;
-    return -1;
+pr_fh_t *pr_fsio_open_canon(const char *name, int flags) {
+  char *deref = NULL;
+  pool *tmp_pool = NULL;
+  pr_fh_t *fh = NULL;
+  pr_fs_t *fs = NULL;
+
+  if (name == NULL) {
+    errno = EINVAL;
+    return NULL;
   }
 
-  tmpl_path = pstrdup(p, path);
-#endif /* HAVE_MKDTEMP */
+  fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_OPEN);
+  if (fs == NULL) {
+    return NULL;
+  }
 
-  if (use_mkdtemp == TRUE) {
-    mode_t mask, *dir_umask, perms;
+  /* Allocate a filehandle. */
+  tmp_pool = make_sub_pool(fs->fs_pool);
+  pr_pool_tag(tmp_pool, "pr_fsio_open_canon() subpool");
 
-    /* mkdtemp(3) creates a directory with 0700 perms; we are given the
-     * target mode (modulo the configured Umask).
-     */
-    dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
-    if (dir_umask == NULL) {
-      /* If Umask was configured with a single parameter, then DirUmask
-       * would not be present; we still should check for Umask.
-       */
-      dir_umask = get_param_ptr(CURRENT_CONF, "Umask", FALSE);
-    }
+  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
+  fh->fh_pool = tmp_pool;
+  fh->fh_path = pstrdup(fh->fh_pool, name);
+  fh->fh_fd = -1;
+  fh->fh_buf = NULL;
+  fh->fh_fs = fs;
 
-    if (dir_umask) {
-      mask = *dir_umask;
+  /* Find the first non-NULL custom open handler.  If there are none,
+   * use the system open.
+   */
+  while (fs && fs->fs_next && !fs->open) {
+    fs = fs->fs_next;
+  }
 
-    } else {
-      mask = (mode_t) 0022;
-    }
+  pr_trace_msg(trace_channel, 8, "using %s open() for path '%s'", fs->fs_name,
+    name);
+  fh->fh_fd = (fs->open)(fh, deref, flags);
+  if (fh->fh_fd < 0) {
+    int xerrno = errno;
 
-    perms = (mode & ~mask);
+    destroy_pool(fh->fh_pool);
+    fh->fh_pool = NULL;
 
-    if (set_sgid) {
-      perms |= S_ISGID;
-    }
+    errno = xerrno;
+    return NULL;
+  }
 
-    /* If we're setting the SGID bit, we need to use root privs, in order
-     * to reliably set the SGID bit.  Sigh.
-     */
-    res = schmod_dir(p, tmpl_path, perms, set_sgid);
-    xerrno = errno;
+  if ((flags & O_CREAT) ||
+      (flags & O_TRUNC)) {
+    pr_fs_clear_cache2(name);
+  }
 
-    if (set_sgid) {
-      if (res < 0 &&
-          xerrno == EPERM) {
-        /* Try again, this time without root privs.  NFS situations which
-         * squash root privs could cause the above chmod(2) to fail; it
-         * might succeed now that we've dropped root privs (Bug#3962).
-         */
-        res = schmod_dir(p, tmpl_path, perms, FALSE);
-        xerrno = errno;
-      }
+  if (fcntl(fh->fh_fd, F_SETFD, FD_CLOEXEC) < 0) {
+    if (errno != EBADF) {
+      pr_trace_msg(trace_channel, 1, "error setting CLOEXEC on file fd %d: %s",
+        fh->fh_fd, strerror(errno));
     }
+  }
 
-    if (res < 0) {
-      pr_log_pri(PR_LOG_WARNING, "chmod(%s) failed: %s", tmpl_path,
-        strerror(xerrno));
+  return fh;
+}
 
-      (void) rmdir(tmpl_path);
+pr_fh_t *pr_fsio_open(const char *name, int flags) {
+  pool *tmp_pool = NULL;
+  pr_fh_t *fh = NULL;
+  pr_fs_t *fs = NULL;
 
-      errno = xerrno;
-      return -1;
-    }
+  if (name == NULL) {
+    errno = EINVAL;
+    return NULL;
   }
 
-  if (uid != (uid_t) -1) {
-    PRIVS_ROOT
-    res = pr_fsio_lchown(tmpl_path, uid, gid);
-    xerrno = errno;
-    PRIVS_RELINQUISH
+  fs = lookup_file_fs(name, NULL, FSIO_FILE_OPEN);
+  if (fs == NULL) {
+    return NULL;
+  }
 
-    if (res < 0) {
-      pr_log_pri(PR_LOG_WARNING, "lchown(%s) as root failed: %s", tmpl_path,
-        strerror(xerrno));
+  /* Allocate a filehandle. */
+  tmp_pool = make_sub_pool(fs->fs_pool);
+  pr_pool_tag(tmp_pool, "pr_fsio_open() subpool");
 
-    } else {
-      if (gid != (gid_t) -1) {
-        pr_log_debug(DEBUG2, "root lchown(%s) to UID %lu, GID %lu successful",
-          tmpl_path, (unsigned long) uid, (unsigned long) gid);
+  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
+  fh->fh_pool = tmp_pool;
+  fh->fh_path = pstrdup(fh->fh_pool, name);
+  fh->fh_fd = -1;
+  fh->fh_buf = NULL;
+  fh->fh_fs = fs;
 
-      } else {
-        pr_log_debug(DEBUG2, "root lchown(%s) to UID %lu successful",
-          tmpl_path, (unsigned long) uid);
-      }
-    }
+  /* Find the first non-NULL custom open handler.  If there are none,
+   * use the system open.
+   */
+  while (fs && fs->fs_next && !fs->open) {
+    fs = fs->fs_next;
+  }
 
-  } else if (gid != (gid_t) -1) {
-    register unsigned int i;
-    int use_root_chown = TRUE;
+  pr_trace_msg(trace_channel, 8, "using %s open() for path '%s'", fs->fs_name,
+    name);
+  fh->fh_fd = (fs->open)(fh, name, flags);
+  if (fh->fh_fd < 0) {
+    int xerrno = errno;
 
-    /* Check if session.fsgid is in session.gids.  If not, use root privs.  */
-    for (i = 0; i < session.gids->nelts; i++) {
-      gid_t *group_ids = session.gids->elts;
+    destroy_pool(fh->fh_pool);
+    fh->fh_pool = NULL;
 
-      if (group_ids[i] == gid) {
-        use_root_chown = FALSE;
-        break;
-      }
-    }
+    errno = xerrno;
+    return NULL;
+  }
 
-    if (use_root_chown) {
-      PRIVS_ROOT
+  if ((flags & O_CREAT) ||
+      (flags & O_TRUNC)) {
+    pr_fs_clear_cache2(name);
+  }
+
+  if (fcntl(fh->fh_fd, F_SETFD, FD_CLOEXEC) < 0) {
+    if (errno != EBADF) {
+      pr_trace_msg(trace_channel, 1, "error setting CLOEXEC on file fd %d: %s",
+        fh->fh_fd, strerror(errno));
     }
+  }
 
-    res = pr_fsio_lchown(tmpl_path, (uid_t) -1, gid);
-    xerrno = errno;
+  return fh;
+}
 
-    if (use_root_chown) {
-      PRIVS_RELINQUISH
-    }
+int pr_fsio_close(pr_fh_t *fh) {
+  int res = 0, xerrno = 0;
+  pr_fs_t *fs;
 
-    if (res < 0) {
-      pr_log_pri(PR_LOG_WARNING, "%slchown(%s) failed: %s",
-        use_root_chown ? "root " : "", tmpl_path, strerror(xerrno));
+  if (fh == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-    } else {
-      pr_log_debug(DEBUG2, "%slchown(%s) to GID %lu successful",
-        use_root_chown ? "root " : "", tmpl_path, (unsigned long) gid);
-    }
+  /* Find the first non-NULL custom close handler.  If there are none,
+   * use the system close.
+   */
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->close) {
+    fs = fs->fs_next;
   }
 
-  if (use_mkdtemp == TRUE) {
-    /* Use rename(2) to move the temporary directory into place at the
-     * target path.
-     */
-    res = rename(tmpl_path, path);
-    if (res < 0) {
-      xerrno = errno;
+  pr_trace_msg(trace_channel, 8, "using %s close() for path '%s'", fs->fs_name,
+    fh->fh_path);
+  res = (fs->close)(fh, fh->fh_fd);
+  xerrno = errno;
 
-      pr_log_pri(PR_LOG_INFO, "renaming '%s' to '%s' failed: %s", tmpl_path,
-        path, strerror(xerrno));
+  if (res == 0) {
+    pr_fs_clear_cache2(fh->fh_path);
+  }
 
-      (void) rmdir(tmpl_path);
+  /* Make sure to scrub any buffered memory, too. */
+  if (fh->fh_buf != NULL) {
+    pr_buffer_t *pbuf;
 
-#ifdef ENOTEMPTY
-      if (xerrno == ENOTEMPTY) {
-        /* If the rename(2) failed with "Directory not empty" (ENOTEMPTY),
-         * then change the errno to "File exists" (EEXIST), so that the
-         * error reported to the client is more indicative of the actual
-         * cause.
-         */
-        xerrno = EEXIST;
-      }
-#endif /* ENOTEMPTY */
- 
-      errno = xerrno;
-      return -1;
-    }
+    pbuf = fh->fh_buf;
+    pr_memscrub(pbuf->buf, pbuf->buflen);
   }
 
-  return 0;
+  if (fh->fh_pool != NULL) {
+    destroy_pool(fh->fh_pool);
+    fh->fh_pool = NULL;
+  }
+
+  errno = xerrno;
+  return res;
 }
 
-int pr_fsio_rmdir(const char *path) {
+int pr_fsio_read(pr_fh_t *fh, char *buf, size_t size) {
   int res;
   pr_fs_t *fs;
 
-  fs = lookup_dir_fs(path, FSIO_DIR_RMDIR);
-  if (fs == NULL) {
+  if (fh == NULL ||
+      buf == NULL ||
+      size == 0) {
+    errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom rmdir handler.  If there are none,
-   * use the system rmdir.
+  /* Find the first non-NULL custom read handler.  If there are none,
+   * use the system read.
    */
-  while (fs && fs->fs_next && !fs->rmdir)
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->read) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s rmdir() for path '%s'", fs->fs_name,
-    path);
-  res = (fs->rmdir)(fs, path);
+  pr_trace_msg(trace_channel, 8, "using %s read() for path '%s' (%lu bytes)",
+    fs->fs_name, fh->fh_path, (unsigned long) size);
+  res = (fs->read)(fh, fh->fh_fd, buf, size);
 
   return res;
 }
 
-int pr_fsio_stat_canon(const char *path, struct stat *sbuf) {
-  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_STAT);
+int pr_fsio_write(pr_fh_t *fh, const char *buf, size_t size) {
+  int res;
+  pr_fs_t *fs;
+
+  if (fh == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  /* Find the first non-NULL custom stat handler.  If there are none,
-   * use the system stat.
+  /* Find the first non-NULL custom write handler.  If there are none,
+   * use the system write.
    */
-  while (fs && fs->fs_next && !fs->stat)
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->write) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s stat() for path '%s'",
-    fs ? fs->fs_name : "system", path);
-  return fs_cache_stat(fs ? fs : root_fs, path, sbuf);
+  pr_trace_msg(trace_channel, 8, "using %s write() for path '%s' (%lu bytes)",
+    fs->fs_name, fh->fh_path, (unsigned long) size);
+  res = (fs->write)(fh, fh->fh_fd, buf, size);
+
+  return res;
 }
 
-int pr_fsio_stat(const char *path, struct stat *sbuf) {
+off_t pr_fsio_lseek(pr_fh_t *fh, off_t offset, int whence) {
+  off_t res;
   pr_fs_t *fs;
 
-  fs = lookup_file_fs(path, NULL, FSIO_FILE_STAT);
-  if (fs == NULL) {
+  if (fh == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom stat handler.  If there are none,
-   * use the system stat.
+  /* Find the first non-NULL custom lseek handler.  If there are none,
+   * use the system lseek.
    */
-  while (fs && fs->fs_next && !fs->stat)
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->lseek) {
     fs = fs->fs_next;
+  }
+
+  pr_trace_msg(trace_channel, 8, "using %s lseek() for path '%s'", fs->fs_name,
+    fh->fh_path);
+  res = (fs->lseek)(fh, fh->fh_fd, offset, whence);
 
-  pr_trace_msg(trace_channel, 8, "using %s stat() for path '%s'", fs->fs_name,
-    path);
-  return fs_cache_stat(fs ? fs : root_fs, path, sbuf);
+  return res;
 }
 
-int pr_fsio_fstat(pr_fh_t *fh, struct stat *sbuf) {
+int pr_fsio_link_canon(const char *target_path, const char *link_path) {
   int res;
-  pr_fs_t *fs;
+  pr_fs_t *target_fs, *link_fs, *fs;
 
-  if (!fh) {
+  if (target_path == NULL ||
+      link_path == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom fstat handler.  If there are none,
-   * use the system fstat.
-   */
-  fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->fstat)
-    fs = fs->fs_next;
+  target_fs = lookup_file_fs(target_path, NULL, FSIO_FILE_LINK);
+  if (target_fs == NULL) {
+    return -1;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s fstat() for path '%s'", fs->fs_name,
-    fh->fh_path);
-  res = (fs->fstat)(fh, fh->fh_fd, sbuf);
+  link_fs = lookup_file_fs(link_path, NULL, FSIO_FILE_LINK);
+  if (link_fs == NULL) {
+    return -1;
+  }
 
-  return res;
-}
+  if (target_fs->allow_xdev_link == FALSE ||
+      link_fs->allow_xdev_link == FALSE) {
+    if (target_fs != link_fs) {
+      errno = EXDEV;
+      return -1;
+    }
+  }
 
-int pr_fsio_lstat_canon(const char *path, struct stat *sbuf) {
-  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_LSTAT);
+  fs = link_fs;
 
-  /* Find the first non-NULL custom lstat handler.  If there are none,
-   * use the system lstat.
+  /* Find the first non-NULL custom link handler.  If there are none,
+   * use the system link.
    */
-  while (fs && fs->fs_next && !fs->lstat)
+  while (fs && fs->fs_next && !fs->link) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s lstat() for path '%s'",
-    fs ? fs->fs_name : "system", path);
-  return fs_cache_lstat(fs ? fs : root_fs, path, sbuf);
+  pr_trace_msg(trace_channel, 8, "using %s link() for paths '%s', '%s'",
+    fs->fs_name, target_path, link_path);
+  res = (fs->link)(fs, target_path, link_path);
+  if (res == 0) {
+    pr_fs_clear_cache2(link_path);
+  }
+
+  return res;
 }
 
-int pr_fsio_lstat(const char *path, struct stat *sbuf) {
-  pr_fs_t *fs;
+int pr_fsio_link(const char *target_path, const char *link_path) {
+  int res;
+  pr_fs_t *target_fs, *link_fs, *fs;
 
-  fs = lookup_file_fs(path, NULL, FSIO_FILE_LSTAT);
-  if (fs == NULL) {
+  if (target_path == NULL ||
+      link_path == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom lstat handler.  If there are none,
-   * use the system lstat.
-   */
-  while (fs && fs->fs_next && !fs->lstat)
-    fs = fs->fs_next;
+  target_fs = lookup_file_fs(target_path, NULL, FSIO_FILE_LINK);
+  if (target_fs == NULL) {
+    return -1;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s lstat() for path '%s'", fs->fs_name,
-    path);
-  return fs_cache_lstat(fs ? fs : root_fs, path, sbuf);
-}
+  link_fs = lookup_file_fs(link_path, NULL, FSIO_FILE_LINK);
+  if (link_fs == NULL) {
+    return -1;
+  }
 
-int pr_fsio_readlink_canon(const char *path, char *buf, size_t buflen) {
-  int res;
-  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_READLINK);
+  if (target_fs->allow_xdev_link == FALSE ||
+      link_fs->allow_xdev_link == FALSE) {
+    if (target_fs != link_fs) {
+      errno = EXDEV;
+      return -1;
+    }
+  }
 
-  /* Find the first non-NULL custom readlink handler.  If there are none,
-   * use the system readlink.
+  fs = link_fs;
+
+  /* Find the first non-NULL custom link handler.  If there are none,
+   * use the system link.
    */
-  while (fs && fs->fs_next && !fs->readlink)
+  while (fs && fs->fs_next && !fs->link) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s readlink() for path '%s'",
-    fs->fs_name, path);
-  res = (fs->readlink)(fs, path, buf, buflen);
+  pr_trace_msg(trace_channel, 8, "using %s link() for paths '%s', '%s'",
+    fs->fs_name, target_path, link_path);
+  res = (fs->link)(fs, target_path, link_path);
+  if (res == 0) {
+    pr_fs_clear_cache2(link_path);
+  }
 
   return res;
 }
 
-int pr_fsio_readlink(const char *path, char *buf, size_t buflen) {
+int pr_fsio_symlink_canon(const char *target_path, const char *link_path) {
   int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_fs(path, NULL, FSIO_FILE_READLINK);
+  if (target_path == NULL ||
+      link_path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  fs = lookup_file_canon_fs(link_path, NULL, FSIO_FILE_SYMLINK);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom readlink handler.  If there are none,
-   * use the system readlink.
+  /* Find the first non-NULL custom symlink handler.  If there are none,
+   * use the system symlink
    */
-  while (fs && fs->fs_next && !fs->readlink)
+  while (fs && fs->fs_next && !fs->symlink) {
     fs = fs->fs_next;
-
-  pr_trace_msg(trace_channel, 8, "using %s readlink() for path '%s'",
-    fs->fs_name, path);
-  res = (fs->readlink)(fs, path, buf, buflen);
-
-  return res;
-}
-
-/* pr_fs_glob() is just a wrapper for glob(3), setting the various gl_
- * callbacks to our fs functions.
- */
-int pr_fs_glob(const char *pattern, int flags,
-    int (*errfunc)(const char *, int), glob_t *pglob) {
-
-  if (pglob) {
-    flags |= GLOB_ALTDIRFUNC;
-
-    pglob->gl_closedir = (void (*)(void *)) pr_fsio_closedir;
-    pglob->gl_readdir = pr_fsio_readdir;
-    pglob->gl_opendir = pr_fsio_opendir;
-    pglob->gl_lstat = pr_fsio_lstat;
-    pglob->gl_stat = pr_fsio_stat;
   }
 
-  return glob(pattern, flags, errfunc, pglob);
-}
+  pr_trace_msg(trace_channel, 8, "using %s symlink() for path '%s'",
+    fs->fs_name, link_path);
+  res = (fs->symlink)(fs, target_path, link_path);
+  if (res == 0) {
+    pr_fs_clear_cache2(link_path);
+  }
 
-void pr_fs_globfree(glob_t *pglob) {
-  globfree(pglob);
+  return res;
 }
 
-int pr_fsio_rename_canon(const char *rfrom, const char *rto) {
+int pr_fsio_symlink(const char *target_path, const char *link_path) {
   int res;
-  pr_fs_t *from_fs, *to_fs, *fs;
-
-  from_fs = lookup_file_canon_fs(rfrom, NULL, FSIO_FILE_RENAME);
-  to_fs = lookup_file_canon_fs(rto, NULL, FSIO_FILE_RENAME);
+  pr_fs_t *fs;
 
-  if (from_fs->allow_xdev_rename == FALSE ||
-      to_fs->allow_xdev_rename == FALSE) {
-    if (from_fs != to_fs) {
-      errno = EXDEV;
-      return -1;
-    }
+  if (target_path == NULL ||
+      link_path == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  fs = to_fs;
+  fs = lookup_file_fs(link_path, NULL, FSIO_FILE_SYMLINK);
+  if (fs == NULL) {
+    return -1;
+  }
 
-  /* Find the first non-NULL custom rename handler.  If there are none,
-   * use the system rename.
+  /* Find the first non-NULL custom symlink handler.  If there are none,
+   * use the system symlink.
    */
-  while (fs && fs->fs_next && !fs->rename)
+  while (fs && fs->fs_next && !fs->symlink) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s rename() for paths '%s', '%s'",
-    fs->fs_name, rfrom, rto);
-  res = (fs->rename)(fs, rfrom, rto);
+  pr_trace_msg(trace_channel, 8, "using %s symlink() for path '%s'",
+    fs->fs_name, link_path);
+  res = (fs->symlink)(fs, target_path, link_path);
+  if (res == 0) {
+    pr_fs_clear_cache2(link_path);
+  }
 
   return res;
 }
 
-int pr_fsio_rename(const char *rnfm, const char *rnto) {
+int pr_fsio_ftruncate(pr_fh_t *fh, off_t len) {
   int res;
-  pr_fs_t *from_fs, *to_fs, *fs;
+  pr_fs_t *fs;
 
-  from_fs = lookup_file_fs(rnfm, NULL, FSIO_FILE_RENAME);
-  if (from_fs == NULL) {
+  if (fh == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  to_fs = lookup_file_fs(rnto, NULL, FSIO_FILE_RENAME);
-  if (to_fs == NULL) {
-    return -1;
+  /* Find the first non-NULL custom ftruncate handler.  If there are none,
+   * use the system ftruncate.
+   */
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->ftruncate) {
+    fs = fs->fs_next;
   }
 
-  if (from_fs->allow_xdev_rename == FALSE ||
-      to_fs->allow_xdev_rename == FALSE) {
-    if (from_fs != to_fs) {
-      errno = EXDEV;
-      return -1;
+  pr_trace_msg(trace_channel, 8, "using %s ftruncate() for path '%s'",
+    fs->fs_name, fh->fh_path);
+  res = (fs->ftruncate)(fh, fh->fh_fd, len);
+  if (res == 0) {
+    pr_fs_clear_cache2(fh->fh_path);
+
+    /* Clear any read buffer. */
+    if (fh->fh_buf != NULL) {
+      fh->fh_buf->current = fh->fh_buf->buf;
+      fh->fh_buf->remaining = fh->fh_buf->buflen;
     }
   }
 
-  fs = to_fs;
-
-  /* Find the first non-NULL custom rename handler.  If there are none,
-   * use the system rename.
-   */
-  while (fs && fs->fs_next && !fs->rename)
-    fs = fs->fs_next;
-
-  pr_trace_msg(trace_channel, 8, "using %s rename() for paths '%s', '%s'",
-    fs->fs_name, rnfm, rnto);
-  res = (fs->rename)(fs, rnfm, rnto);
-
   return res;
 }
 
-int pr_fsio_unlink_canon(const char *name) {
+int pr_fsio_truncate_canon(const char *path, off_t len) {
   int res;
-  pr_fs_t *fs = lookup_file_canon_fs(name, NULL, FSIO_FILE_UNLINK);
+  pr_fs_t *fs;
 
-  /* Find the first non-NULL custom unlink handler.  If there are none,
-   * use the system unlink.
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_TRUNC);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom truncate handler.  If there are none,
+   * use the system truncate.
    */
-  while (fs && fs->fs_next && !fs->unlink)
+  while (fs && fs->fs_next && !fs->truncate) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s unlink() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->unlink)(fs, name);
+  pr_trace_msg(trace_channel, 8, "using %s truncate() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->truncate)(fs, path, len);
+  if (res == 0) {
+    pr_fs_clear_cache2(path);
+  }
 
   return res;
 }
-	
-int pr_fsio_unlink(const char *name) {
+
+int pr_fsio_truncate(const char *path, off_t len) {
   int res;
   pr_fs_t *fs;
+ 
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  fs = lookup_file_fs(name, NULL, FSIO_FILE_UNLINK);
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_TRUNC);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom unlink handler.  If there are none,
-   * use the system unlink.
+  /* Find the first non-NULL custom truncate handler.  If there are none,
+   * use the system truncate.
    */
-  while (fs && fs->fs_next && !fs->unlink)
+  while (fs && fs->fs_next && !fs->truncate) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s unlink() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->unlink)(fs, name);
-
+  pr_trace_msg(trace_channel, 8, "using %s truncate() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->truncate)(fs, path, len);
+  if (res == 0) {
+    pr_fs_clear_cache2(path);
+  }
+  
   return res;
 }
 
-pr_fh_t *pr_fsio_open_canon(const char *name, int flags) {
+int pr_fsio_chmod_canon(const char *name, mode_t mode) {
+  int res;
   char *deref = NULL;
-  pool *tmp_pool = NULL;
-  pr_fh_t *fh = NULL;
-
-  pr_fs_t *fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_OPEN);
+  pr_fs_t *fs;
 
-  /* Allocate a filehandle. */
-  tmp_pool = make_sub_pool(fs->fs_pool);
-  pr_pool_tag(tmp_pool, "pr_fsio_open_canon() subpool");
+  if (name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
-  fh->fh_pool = tmp_pool;
-  fh->fh_path = pstrdup(fh->fh_pool, name);
-  fh->fh_fd = -1;
-  fh->fh_buf = NULL;
-  fh->fh_fs = fs;
+  fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_CHMOD);
+  if (fs == NULL) {
+    return -1;
+  }
 
-  /* Find the first non-NULL custom open handler.  If there are none,
-   * use the system open.
+  /* Find the first non-NULL custom chmod handler.  If there are none,
+   * use the system chmod.
    */
-  while (fs && fs->fs_next && !fs->open) {
+  while (fs && fs->fs_next && !fs->chmod) {
     fs = fs->fs_next;
   }
 
-  pr_trace_msg(trace_channel, 8, "using %s open() for path '%s'", fs->fs_name,
-    name);
-  fh->fh_fd = (fs->open)(fh, deref, flags);
-
-  if (fh->fh_fd == -1) {
-    destroy_pool(fh->fh_pool);
-    return NULL;
-  }
-
-  if (fcntl(fh->fh_fd, F_SETFD, FD_CLOEXEC) < 0) {
-    if (errno != EBADF) {
-      pr_trace_msg(trace_channel, 1, "error setting CLOEXEC on file fd %d: %s",
-        fh->fh_fd, strerror(errno));
-    }
+  pr_trace_msg(trace_channel, 8, "using %s chmod() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->chmod)(fs, deref, mode);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
   }
 
-  return fh;
+  return res;
 }
 
-pr_fh_t *pr_fsio_open(const char *name, int flags) {
-  pool *tmp_pool = NULL;
-  pr_fh_t *fh = NULL;
-  pr_fs_t *fs = NULL;
+int pr_fsio_chmod(const char *name, mode_t mode) {
+  int res;
+  pr_fs_t *fs;
 
-  if (!name) {
+  if (name == NULL) {
     errno = EINVAL;
-    return NULL;
+    return -1;
   }
 
-  fs = lookup_file_fs(name, NULL, FSIO_FILE_OPEN);
+  fs = lookup_file_fs(name, NULL, FSIO_FILE_CHMOD);
   if (fs == NULL) {
-    return NULL;
+    return -1;
   }
 
-  /* Allocate a filehandle. */
-  tmp_pool = make_sub_pool(fs->fs_pool);
-  pr_pool_tag(tmp_pool, "pr_fsio_open() subpool");
-
-  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
-  fh->fh_pool = tmp_pool;
-  fh->fh_path = pstrdup(fh->fh_pool, name);
-  fh->fh_fd = -1;
-  fh->fh_buf = NULL;
-  fh->fh_fs = fs;
-
-  /* Find the first non-NULL custom open handler.  If there are none,
-   * use the system open.
+  /* Find the first non-NULL custom chmod handler.  If there are none,
+   * use the system chmod.
    */
-  while (fs && fs->fs_next && !fs->open) {
+  while (fs && fs->fs_next && !fs->chmod) {
     fs = fs->fs_next;
   }
 
-  pr_trace_msg(trace_channel, 8, "using %s open() for path '%s'", fs->fs_name,
-    name);
-  fh->fh_fd = (fs->open)(fh, name, flags);
+  pr_trace_msg(trace_channel, 8, "using %s chmod() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->chmod)(fs, name, mode);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
+  }
 
-  if (fh->fh_fd == -1) {
-    destroy_pool(fh->fh_pool);
-    return NULL;
+  return res;
+}
+
+int pr_fsio_fchmod(pr_fh_t *fh, mode_t mode) {
+  int res;
+  pr_fs_t *fs;
+
+  if (fh == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  if (fcntl(fh->fh_fd, F_SETFD, FD_CLOEXEC) < 0) {
-    if (errno != EBADF) {
-      pr_trace_msg(trace_channel, 1, "error setting CLOEXEC on file fd %d: %s",
-        fh->fh_fd, strerror(errno));
-    }
+  /* Find the first non-NULL custom fchmod handler.  If there are none, use
+   * the system fchmod.
+   */
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->fchmod) {
+    fs = fs->fs_next;
   }
 
-  return fh;
+  pr_trace_msg(trace_channel, 8, "using %s fchmod() for path '%s'",
+    fs->fs_name, fh->fh_path);
+  res = (fs->fchmod)(fh, fh->fh_fd, mode);
+  if (res == 0) {
+    pr_fs_clear_cache2(fh->fh_path);
+  }
+
+  return res;
 }
 
-pr_fh_t *pr_fsio_creat_canon(const char *name, mode_t mode) {
-  char *deref = NULL;
-  pool *tmp_pool = NULL;
-  pr_fh_t *fh = NULL;
+int pr_fsio_chown_canon(const char *name, uid_t uid, gid_t gid) {
+  int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_CREAT);
-  if (fs == NULL) {
-    return NULL;
+  if (name == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  /* Allocate a filehandle. */
-  tmp_pool = make_sub_pool(fs->fs_pool);
-  pr_pool_tag(tmp_pool, "pr_fsio_creat_canon() subpool");
-
-  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
-  fh->fh_pool = tmp_pool;
-  fh->fh_path = pstrdup(fh->fh_pool, name);
-  fh->fh_fd = -1;
-  fh->fh_buf = NULL;
-  fh->fh_fs = fs;
+  fs = lookup_file_canon_fs(name, NULL, FSIO_FILE_CHOWN);
+  if (fs == NULL) {
+    return -1;
+  }
 
-  /* Find the first non-NULL custom creat handler.  If there are none,
-   * use the system creat.
+  /* Find the first non-NULL custom chown handler.  If there are none,
+   * use the system chown.
    */
-  while (fs && fs->fs_next && !fs->creat)
+  while (fs && fs->fs_next && !fs->chown) {
     fs = fs->fs_next;
-
-  pr_trace_msg(trace_channel, 8, "using %s creat() for path '%s'", fs->fs_name,
-    name);
-  fh->fh_fd = (fs->creat)(fh, deref, mode);
-
-  if (fh->fh_fd == -1) {
-    destroy_pool(fh->fh_pool);
-    return NULL;
   }
 
-  if (fcntl(fh->fh_fd, F_SETFD, FD_CLOEXEC) < 0) {
-    if (errno != EBADF) {
-      pr_trace_msg(trace_channel, 1, "error setting CLOEXEC on file fd %d: %s",
-        fh->fh_fd, strerror(errno));
-    }
+  pr_trace_msg(trace_channel, 8, "using %s chown() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->chown)(fs, name, uid, gid);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
   }
 
-  return fh;
+  return res;
 }
 
-pr_fh_t *pr_fsio_creat(const char *name, mode_t mode) {
-  pool *tmp_pool = NULL;
-  pr_fh_t *fh = NULL;
+int pr_fsio_chown(const char *name, uid_t uid, gid_t gid) {
+  int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_fs(name, NULL, FSIO_FILE_CREAT);
-  if (fs == NULL) {
-    return NULL;
+  if (name == NULL) {
+    errno = EINVAL;
+    return -1;
   }
 
-  /* Allocate a filehandle. */
-  tmp_pool = make_sub_pool(fs->fs_pool);
-  pr_pool_tag(tmp_pool, "pr_fsio_creat() subpool");
-
-  fh = pcalloc(tmp_pool, sizeof(pr_fh_t));
-  fh->fh_pool = tmp_pool;
-  fh->fh_path = pstrdup(fh->fh_pool, name);
-  fh->fh_fd = -1;
-  fh->fh_buf = NULL;
-  fh->fh_fs = fs;
+  fs = lookup_file_fs(name, NULL, FSIO_FILE_CHOWN);
+  if (fs == NULL) {
+    return -1;
+  }
 
-  /* Find the first non-NULL custom creat handler.  If there are none,
-   * use the system creat.
+  /* Find the first non-NULL custom chown handler.  If there are none,
+   * use the system chown.
    */
-  while (fs && fs->fs_next && !fs->creat)
+  while (fs && fs->fs_next && !fs->chown) {
     fs = fs->fs_next;
-
-  pr_trace_msg(trace_channel, 8, "using %s creat() for path '%s'", fs->fs_name,
-    name);
-  fh->fh_fd = (fs->creat)(fh, name, mode);
-
-  if (fh->fh_fd == -1) {
-    destroy_pool(fh->fh_pool);
-    return NULL;
   }
 
-  if (fcntl(fh->fh_fd, F_SETFD, FD_CLOEXEC) < 0) {
-    if (errno != EBADF) {
-      pr_trace_msg(trace_channel, 1, "error setting CLOEXEC on file fd %d: %s",
-        fh->fh_fd, strerror(errno));
-    }
+  pr_trace_msg(trace_channel, 8, "using %s chown() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->chown)(fs, name, uid, gid);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
   }
 
-  return fh;
+  return res;
 }
 
-int pr_fsio_close(pr_fh_t *fh) {
-  int res = 0;
+int pr_fsio_fchown(pr_fh_t *fh, uid_t uid, gid_t gid) {
+  int res;
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (fh == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom close handler.  If there are none,
-   * use the system close.
+  /* Find the first non-NULL custom fchown handler.  If there are none, use
+   * the system fchown.
    */
   fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->close)
+  while (fs && fs->fs_next && !fs->fchown) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s close() for path '%s'", fs->fs_name,
-    fh->fh_path);
-  res = (fs->close)(fh, fh->fh_fd);
+  pr_trace_msg(trace_channel, 8, "using %s fchown() for path '%s'",
+    fs->fs_name, fh->fh_path);
+  res = (fs->fchown)(fh, fh->fh_fd, uid, gid);
+  if (res == 0) {
+    pr_fs_clear_cache2(fh->fh_path);
+  }
 
-  destroy_pool(fh->fh_pool);
   return res;
 }
 
-int pr_fsio_read(pr_fh_t *fh, char *buf, size_t size) {
+int pr_fsio_lchown(const char *name, uid_t uid, gid_t gid) {
   int res;
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom read handler.  If there are none,
-   * use the system read.
+  fs = lookup_file_fs(name, NULL, FSIO_FILE_CHOWN);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom lchown handler.  If there are none,
+   * use the system chown.
    */
-  fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->read)
+  while (fs && fs->fs_next && !fs->lchown) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s read() for path '%s' (%lu bytes)",
-    fs->fs_name, fh->fh_path, (unsigned long) size);
-  res = (fs->read)(fh, fh->fh_fd, buf, size);
+  pr_trace_msg(trace_channel, 8, "using %s lchown() for path '%s'",
+    fs->fs_name, name);
+  res = (fs->lchown)(fs, name, uid, gid);
+  if (res == 0) {
+    pr_fs_clear_cache2(name);
+  }
 
   return res;
 }
 
-int pr_fsio_write(pr_fh_t *fh, const char *buf, size_t size) {
-  int res;
+int pr_fsio_access(const char *path, int mode, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (path == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom write handler.  If there are none,
-   * use the system write.
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_ACCESS);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom access handler.  If there are none,
+   * use the system access.
    */
-  fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->write)
+  while (fs && fs->fs_next && !fs->access) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s write() for path '%s' (%lu bytes)",
-    fs->fs_name, fh->fh_path, (unsigned long) size);
-  res = (fs->write)(fh, fh->fh_fd, buf, size);
-
-  return res;
+  pr_trace_msg(trace_channel, 8, "using %s access() for path '%s'",
+    fs->fs_name, path);
+  return (fs->access)(fs, path, mode, uid, gid, suppl_gids);
 }
 
-off_t pr_fsio_lseek(pr_fh_t *fh, off_t offset, int whence) {
-  off_t res;
+int pr_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (fh == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom lseek handler.  If there are none,
-   * use the system lseek.
+  /* Find the first non-NULL custom faccess handler.  If there are none,
+   * use the system faccess.
    */
   fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->lseek)
+  while (fs && fs->fs_next && !fs->faccess) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s lseek() for path '%s'", fs->fs_name,
-    fh->fh_path);
-  res = (fs->lseek)(fh, fh->fh_fd, offset, whence);
-
-  return res;
+  pr_trace_msg(trace_channel, 8, "using %s faccess() for path '%s'",
+    fs->fs_name, fh->fh_path);
+  return (fs->faccess)(fh, mode, uid, gid, suppl_gids);
 }
 
-int pr_fsio_link_canon(const char *lfrom, const char *lto) {
+int pr_fsio_utimes(const char *path, struct timeval *tvs) {
   int res;
-  pr_fs_t *from_fs, *to_fs, *fs;
+  pr_fs_t *fs;
 
-  from_fs = lookup_file_fs(lfrom, NULL, FSIO_FILE_LINK);
-  if (from_fs == NULL) {
+  if (path == NULL ||
+      tvs == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  to_fs = lookup_file_fs(lto, NULL, FSIO_FILE_LINK);
-  if (to_fs == NULL) {
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_UTIMES);
+  if (fs == NULL) {
     return -1;
   }
 
-  if (from_fs->allow_xdev_link == FALSE ||
-      to_fs->allow_xdev_link == FALSE) {
-    if (from_fs != to_fs) {
-      errno = EXDEV;
-      return -1;
-    }
-  }
-
-  fs = to_fs;
-
-  /* Find the first non-NULL custom link handler.  If there are none,
-   * use the system link.
+  /* Find the first non-NULL custom utimes handler.  If there are none,
+   * use the system utimes.
    */
-  while (fs && fs->fs_next && !fs->link)
+  while (fs && fs->fs_next && !fs->utimes) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s link() for paths '%s', '%s'",
-    fs->fs_name, lfrom, lto);
-  res = (fs->link)(fs, lfrom, lto);
+  pr_trace_msg(trace_channel, 8, "using %s utimes() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->utimes)(fs, path, tvs);
+  if (res == 0) {
+    pr_fs_clear_cache2(path);
+  }
 
   return res;
 }
 
-int pr_fsio_link(const char *lfrom, const char *lto) {
-  int res;
-  pr_fs_t *from_fs, *to_fs, *fs;
+/* If the utimes(2) call fails because the process UID does not match the file
+ * UID, then check to see if the GIDs match (and that the file has group write
+ * permissions).
+ *
+ * This can be alleviated in two ways: a) if mod_cap is present, enable the
+ * CAP_FOWNER capability for the session, or b) use root privs.
+ */
+int pr_fsio_utimes_with_root(const char *path, struct timeval *tvs) {
+  int res, xerrno, matching_gid = FALSE;
+  struct stat st;
 
-  from_fs = lookup_file_fs(lfrom, NULL, FSIO_FILE_LINK);
-  if (from_fs == NULL) {
-    return -1;
-  }
+  res = pr_fsio_utimes(path, tvs);
+  xerrno = errno;
 
-  to_fs = lookup_file_fs(lto, NULL, FSIO_FILE_LINK);
-  if (to_fs == NULL) {
-    return -1;
+  if (res == 0) {
+    return 0;
   }
 
-  if (from_fs->allow_xdev_link == FALSE ||
-      to_fs->allow_xdev_link == FALSE) {
-    if (from_fs != to_fs) {
-      errno = EXDEV;
-      return -1;
-    }
+  /* We only try these workarounds for EPERM. */
+  if (xerrno != EPERM) {
+    return res;
   }
 
-  fs = to_fs;
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_stat(path, &st) < 0) {
+    errno = xerrno;
+    return -1;
+  }
 
-  /* Find the first non-NULL custom link handler.  If there are none,
-   * use the system link.
+  /* Be sure to check the primary and all the supplemental groups to which
+   * this session belongs.
    */
-  while (fs && fs->fs_next && !fs->link)
-    fs = fs->fs_next;
+  if (st.st_gid == session.gid) {
+    matching_gid = TRUE;
 
-  pr_trace_msg(trace_channel, 8, "using %s link() for paths '%s', '%s'",
-    fs->fs_name, lfrom, lto);
-  res = (fs->link)(fs, lfrom, lto);
+  } else if (session.gids != NULL) {
+    register unsigned int i;
+    gid_t *gids;
 
-  return res;
-}
+    gids = session.gids->elts;
+    for (i = 0; i < session.gids->nelts; i++) {
+      if (st.st_gid == gids[i]) {
+        matching_gid = TRUE;
+        break;
+      }
+    }
+  }
 
-int pr_fsio_symlink_canon(const char *lfrom, const char *lto) {
-  int res;
-  pr_fs_t *fs = lookup_file_canon_fs(lto, NULL, FSIO_FILE_SYMLINK);
+  if (matching_gid == TRUE &&
+      (st.st_mode & S_IWGRP)) {
 
-  /* Find the first non-NULL custom symlink handler.  If there are none,
-   * use the system symlink
-   */
-  while (fs && fs->fs_next && !fs->symlink)
-    fs = fs->fs_next;
+    /* Try the utimes(2) call again, this time with root privs. */
+    pr_signals_block();
+    PRIVS_ROOT
+    res = pr_fsio_utimes(path, tvs);
+    PRIVS_RELINQUISH
+    pr_signals_unblock();
 
-  pr_trace_msg(trace_channel, 8, "using %s symlink() for path '%s'",
-    fs->fs_name, lto);
-  res = (fs->symlink)(fs, lfrom, lto);
+    if (res == 0) {
+      return 0;
+    }
+  }
 
-  return res;
+  errno = xerrno;
+  return -1;
 }
 
-int pr_fsio_symlink(const char *lfrom, const char *lto) {
+int pr_fsio_futimes(pr_fh_t *fh, struct timeval *tvs) {
   int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_fs(lto, NULL, FSIO_FILE_SYMLINK);
-  if (fs == NULL) {
+  if (fh == NULL ||
+      tvs == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom symlink handler.  If there are none,
-   * use the system symlink.
+  /* Find the first non-NULL custom futimes handler.  If there are none,
+   * use the system futimes.
    */
-  while (fs && fs->fs_next && !fs->symlink)
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->futimes) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s symlink() for path '%s'",
-    fs->fs_name, lto);
-  res = (fs->symlink)(fs, lfrom, lto);
+  pr_trace_msg(trace_channel, 8, "using %s futimes() for path '%s'",
+    fs->fs_name, fh->fh_path);
+  res = (fs->futimes)(fh, fh->fh_fd, tvs);
+  if (res == 0) {
+    pr_fs_clear_cache2(fh->fh_path);
+  }
 
   return res;
 }
 
-int pr_fsio_ftruncate(pr_fh_t *fh, off_t len) {
+int pr_fsio_fsync(pr_fh_t *fh) {
   int res;
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (fh == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom ftruncate handler.  If there are none,
-   * use the system ftruncate.
+  /* Find the first non-NULL custom fsync handler.  If there are none,
+   * use the system fsync.
    */
   fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->ftruncate)
+  while (fs && fs->fs_next && !fs->fsync) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s ftruncate() for path '%s'",
+  pr_trace_msg(trace_channel, 8, "using %s fsync() for path '%s'",
     fs->fs_name, fh->fh_path);
-  res = (fs->ftruncate)(fh, fh->fh_fd, len);
+  res = (fs->fsync)(fh, fh->fh_fd);
+  if (res == 0) {
+    pr_fs_clear_cache2(fh->fh_path);
+  }
 
   return res;
 }
 
-int pr_fsio_truncate_canon(const char *path, off_t len) {
-  int res;
-  pr_fs_t *fs = lookup_file_canon_fs(path, NULL, FSIO_FILE_TRUNC);
-
-  /* Find the first non-NULL custom truncate handler.  If there are none,
-   * use the system truncate.
-   */
-  while (fs && fs->fs_next && !fs->truncate)
-    fs = fs->fs_next;
-
-  pr_trace_msg(trace_channel, 8, "using %s truncate() for path '%s'",
-    fs->fs_name, path);
-  res = (fs->truncate)(fs, path, len);
+ssize_t pr_fsio_getxattr(pool *p, const char *path, const char *name, void *val,
+    size_t valsz) {
+  ssize_t res;
+  pr_fs_t *fs;
 
-  return res;
-}
+  if (p == NULL ||
+      path == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-int pr_fsio_truncate(const char *path, off_t len) {
-  int res;
-  pr_fs_t *fs;
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
 
-  fs = lookup_file_fs(path, NULL, FSIO_FILE_TRUNC);
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_GETXATTR);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom truncate handler.  If there are none,
-   * use the system truncate.
+  /* Find the first non-NULL custom getxattr handler.  If there are none,
+   * use the system getxattr.
    */
-  while (fs && fs->fs_next && !fs->truncate)
+  while (fs && fs->fs_next && !fs->getxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s truncate() for path '%s'",
+  pr_trace_msg(trace_channel, 8, "using %s getxattr() for path '%s'",
     fs->fs_name, path);
-  res = (fs->truncate)(fs, path, len);
-  
+  res = (fs->getxattr)(p, fs, path, name, val, valsz);
   return res;
 }
 
-int pr_fsio_chmod_canon(const char *name, mode_t mode) {
-  int res;
-  char *deref = NULL;
+ssize_t pr_fsio_lgetxattr(pool *p, const char *path, const char *name,
+    void *val, size_t valsz) {
+  ssize_t res;
   pr_fs_t *fs;
 
-  fs = lookup_file_canon_fs(name, &deref, FSIO_FILE_CHMOD);
-  if (fs == NULL) {
+  if (p == NULL ||
+      path == NULL ||
+      name == NULL) {
+    errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom chmod handler.  If there are none,
-   * use the system chmod.
-   */
-  while (fs && fs->fs_next && !fs->chmod)
-    fs = fs->fs_next;
-
-  pr_trace_msg(trace_channel, 8, "using %s chmod() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->chmod)(fs, deref, mode);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
-  return res;
-}
-
-int pr_fsio_chmod(const char *name, mode_t mode) {
-  int res;
-  pr_fs_t *fs;
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
 
-  fs = lookup_file_fs(name, NULL, FSIO_FILE_CHMOD);
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_LGETXATTR);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom chmod handler.  If there are none,
-   * use the system chmod.
+  /* Find the first non-NULL custom lgetxattr handler.  If there are none,
+   * use the system lgetxattr.
    */
-  while (fs && fs->fs_next && !fs->chmod)
+  while (fs && fs->fs_next && !fs->lgetxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s chmod() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->chmod)(fs, name, mode);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  pr_trace_msg(trace_channel, 8, "using %s lgetxattr() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->lgetxattr)(p, fs, path, name, val, valsz);
   return res;
 }
 
-int pr_fsio_fchmod(pr_fh_t *fh, mode_t mode) {
-  int res;
+ssize_t pr_fsio_fgetxattr(pool *p, pr_fh_t *fh, const char *name, void *val,
+    size_t valsz) {
+  ssize_t res;
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (p == NULL ||
+      fh == NULL ||
+      name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom fchmod handler.  If there are none, use
-   * the system fchmod.
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  /* Find the first non-NULL custom fgetxattr handler.  If there are none,
+   * use the system fgetxattr.
    */
   fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->fchmod)
+  while (fs && fs->fs_next && !fs->fgetxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s fchmod() for path '%s'",
+  pr_trace_msg(trace_channel, 8, "using %s fgetxattr() for path '%s'",
     fs->fs_name, fh->fh_path);
-  res = (fs->fchmod)(fh, fh->fh_fd, mode);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  res = (fs->fgetxattr)(p, fh, fh->fh_fd, name, val, valsz);
   return res;
 }
 
-int pr_fsio_chown_canon(const char *name, uid_t uid, gid_t gid) {
+int pr_fsio_listxattr(pool *p, const char *path, array_header **names) {
   int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_canon_fs(name, NULL, FSIO_FILE_CHOWN);
+  if (p == NULL ||
+      path == NULL ||
+      names == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_LISTXATTR);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom chown handler.  If there are none,
-   * use the system chown.
+  /* Find the first non-NULL custom listxattr handler.  If there are none,
+   * use the system listxattr.
    */
-  while (fs && fs->fs_next && !fs->chown)
+  while (fs && fs->fs_next && !fs->listxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s chown() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->chown)(fs, name, uid, gid);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  pr_trace_msg(trace_channel, 8, "using %s listxattr() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->listxattr)(p, fs, path, names);
   return res;
 }
 
-int pr_fsio_chown(const char *name, uid_t uid, gid_t gid) {
+int pr_fsio_llistxattr(pool *p, const char *path, array_header **names) {
   int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_fs(name, NULL, FSIO_FILE_CHOWN);
+  if (p == NULL ||
+      path == NULL ||
+      names == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_LLISTXATTR);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom chown handler.  If there are none,
-   * use the system chown.
+  /* Find the first non-NULL custom llistxattr handler.  If there are none,
+   * use the system llistxattr.
    */
-  while (fs && fs->fs_next && !fs->chown)
+  while (fs && fs->fs_next && !fs->llistxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s chown() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->chown)(fs, name, uid, gid);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  pr_trace_msg(trace_channel, 8, "using %s llistxattr() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->llistxattr)(p, fs, path, names);
   return res;
 }
 
-int pr_fsio_fchown(pr_fh_t *fh, uid_t uid, gid_t gid) {
+int pr_fsio_flistxattr(pool *p, pr_fh_t *fh, array_header **names) {
   int res;
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (p == NULL ||
+      fh == NULL ||
+      names == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom fchown handler.  If there are none, use
-   * the system fchown.
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  /* Find the first non-NULL custom flistxattr handler.  If there are none,
+   * use the system flistxattr.
    */
   fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->fchown)
+  while (fs && fs->fs_next && !fs->flistxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s fchown() for path '%s'",
+  pr_trace_msg(trace_channel, 8, "using %s flistxattr() for path '%s'",
     fs->fs_name, fh->fh_path);
-  res = (fs->fchown)(fh, fh->fh_fd, uid, gid);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  res = (fs->flistxattr)(p, fh, fh->fh_fd, names);
   return res;
 }
 
-int pr_fsio_lchown(const char *name, uid_t uid, gid_t gid) {
+int pr_fsio_removexattr(pool *p, const char *path, const char *name) {
   int res;
   pr_fs_t *fs;
 
-  fs = lookup_file_fs(name, NULL, FSIO_FILE_CHOWN);
+  if (p == NULL ||
+      path == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_REMOVEXATTR);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom lchown handler.  If there are none,
-   * use the system chown.
+  /* Find the first non-NULL custom removexattr handler.  If there are none,
+   * use the system removexattr.
    */
-  while (fs && fs->fs_next && !fs->lchown) {
+  while (fs && fs->fs_next && !fs->removexattr) {
     fs = fs->fs_next;
   }
 
-  pr_trace_msg(trace_channel, 8, "using %s lchown() for path '%s'",
-    fs->fs_name, name);
-  res = (fs->lchown)(fs, name, uid, gid);
+  pr_trace_msg(trace_channel, 8, "using %s removexattr() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->removexattr)(p, fs, path, name);
+  return res;
+}
 
-  if (res == 0) {
-    pr_fs_clear_cache();
+int pr_fsio_lremovexattr(pool *p, const char *path, const char *name) {
+  int res;
+  pr_fs_t *fs;
+
+  if (p == NULL ||
+      path == NULL ||
+      name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_LREMOVEXATTR);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom lremovexattr handler.  If there are none,
+   * use the system lremovexattr.
+   */
+  while (fs && fs->fs_next && !fs->lremovexattr) {
+    fs = fs->fs_next;
   }
 
+  pr_trace_msg(trace_channel, 8, "using %s lremovexattr() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->lremovexattr)(p, fs, path, name);
   return res;
 }
 
-int pr_fsio_access(const char *path, int mode, uid_t uid, gid_t gid,
-    array_header *suppl_gids) {
+int pr_fsio_fremovexattr(pool *p, pr_fh_t *fh, const char *name) {
+  int res;
   pr_fs_t *fs;
 
-  if (!path) {
+  if (p == NULL ||
+      fh == NULL ||
+      name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  fs = lookup_file_fs(path, NULL, FSIO_FILE_ACCESS);
-  if (fs == NULL) {
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
     return -1;
   }
 
-  /* Find the first non-NULL custom access handler.  If there are none,
-   * use the system access.
+  /* Find the first non-NULL custom fremovexattr handler.  If there are none,
+   * use the system fremovexattr.
    */
-  while (fs && fs->fs_next && !fs->access)
+  fs = fh->fh_fs;
+  while (fs && fs->fs_next && !fs->fremovexattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s access() for path '%s'",
-    fs->fs_name, path);
-  return (fs->access)(fs, path, mode, uid, gid, suppl_gids);
+  pr_trace_msg(trace_channel, 8, "using %s fremovexattr() for path '%s'",
+    fs->fs_name, fh->fh_path);
+  res = (fs->fremovexattr)(p, fh, fh->fh_fd, name);
+  return res;
 }
 
-int pr_fsio_faccess(pr_fh_t *fh, int mode, uid_t uid, gid_t gid,
-    array_header *suppl_gids) {
+int pr_fsio_setxattr(pool *p, const char *path, const char *name, void *val,
+    size_t valsz, int flags) {
+  int res;
   pr_fs_t *fs;
 
-  if (!fh) {
+  if (p == NULL ||
+      path == NULL ||
+      name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom faccess handler.  If there are none,
-   * use the system faccess.
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_SETXATTR);
+  if (fs == NULL) {
+    return -1;
+  }
+
+  /* Find the first non-NULL custom setxattr handler.  If there are none,
+   * use the system setxattr.
    */
-  fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->faccess)
+  while (fs && fs->fs_next && !fs->setxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s faccess() for path '%s'",
-    fs->fs_name, fh->fh_path);
-  return (fs->faccess)(fh, mode, uid, gid, suppl_gids);
+  pr_trace_msg(trace_channel, 8, "using %s setxattr() for path '%s'",
+    fs->fs_name, path);
+  res = (fs->setxattr)(p, fs, path, name, val, valsz, flags);
+  return res;
 }
 
-int pr_fsio_utimes(const char *path, struct timeval *tvs) {
+int pr_fsio_lsetxattr(pool *p, const char *path, const char *name, void *val,
+    size_t valsz, int flags) {
   int res;
   pr_fs_t *fs;
 
-  if (path == NULL ||
-      tvs == NULL) {
+  if (p == NULL ||
+      path == NULL ||
+      name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  fs = lookup_file_fs(path, NULL, FSIO_FILE_UTIMES);
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  fs = lookup_file_fs(path, NULL, FSIO_FILE_LSETXATTR);
   if (fs == NULL) {
     return -1;
   }
 
-  /* Find the first non-NULL custom utimes handler.  If there are none,
-   * use the system utimes.
+  /* Find the first non-NULL custom lsetxattr handler.  If there are none,
+   * use the system lsetxattr.
    */
-  while (fs && fs->fs_next && !fs->utimes)
+  while (fs && fs->fs_next && !fs->lsetxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s utimes() for path '%s'",
+  pr_trace_msg(trace_channel, 8, "using %s lsetxattr() for path '%s'",
     fs->fs_name, path);
-  res = (fs->utimes)(fs, path, tvs);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  res = (fs->lsetxattr)(p, fs, path, name, val, valsz, flags);
   return res;
 }
 
-int pr_fsio_futimes(pr_fh_t *fh, struct timeval *tvs) {
+int pr_fsio_fsetxattr(pool *p, pr_fh_t *fh, const char *name, void *val,
+    size_t valsz, int flags) {
   int res;
   pr_fs_t *fs;
 
-  if (fh == NULL ||
-      tvs == NULL) {
+  if (p == NULL ||
+      fh == NULL ||
+      name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  /* Find the first non-NULL custom futimes handler.  If there are none,
-   * use the system futimes.
+  if (fsio_opts & PR_FSIO_OPT_IGNORE_XATTR) {
+    errno = ENOSYS;
+    return -1;
+  }
+
+  /* Find the first non-NULL custom fsetxattr handler.  If there are none,
+   * use the system fsetxattr.
    */
   fs = fh->fh_fs;
-  while (fs && fs->fs_next && !fs->futimes)
+  while (fs && fs->fs_next && !fs->fsetxattr) {
     fs = fs->fs_next;
+  }
 
-  pr_trace_msg(trace_channel, 8, "using %s futimes() for path '%s'",
+  pr_trace_msg(trace_channel, 8, "using %s fsetxattr() for path '%s'",
     fs->fs_name, fh->fh_path);
-  res = (fs->futimes)(fh, fh->fh_fd, tvs);
-
-  if (res == 0)
-    pr_fs_clear_cache();
-
+  res = (fs->fsetxattr)(p, fh, fh->fh_fd, name, val, valsz, flags);
   return res;
 }
 
@@ -4377,9 +6192,14 @@ int pr_fsio_futimes(pr_fh_t *fh, struct timeval *tvs) {
  * rewritten to reflect the new root.
  */
 int pr_fsio_chroot(const char *path) {
-  int res = 0;
+  int res = 0, xerrno = 0;
   pr_fs_t *fs;
 
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   fs = lookup_dir_fs(path, FSIO_DIR_CHROOT);
   if (fs == NULL) {
     return -1;
@@ -4388,12 +6208,15 @@ int pr_fsio_chroot(const char *path) {
   /* Find the first non-NULL custom chroot handler.  If there are none,
    * use the system chroot.
    */
-  while (fs && fs->fs_next && !fs->chroot)
+  while (fs && fs->fs_next && !fs->chroot) {
     fs = fs->fs_next;
+  }
 
   pr_trace_msg(trace_channel, 8, "using %s chroot() for path '%s'",
     fs->fs_name, path);
   res = (fs->chroot)(fs, path);
+  xerrno = errno;
+
   if (res == 0) {
     unsigned int iter_start = 0;
 
@@ -4406,8 +6229,9 @@ int pr_fsio_chroot(const char *path) {
 
     pr_pool_tag(map_pool, "FSIO Map Pool");
 
-    if (fs_map)
+    if (fs_map) {
       fs_objs = (pr_fs_t **) fs_map->elts;
+    }
 
     if (fs != root_fs) {
       if (strncmp(fs->fs_path, path, strlen(path)) == 0) {
@@ -4455,24 +6279,65 @@ int pr_fsio_chroot(const char *path) {
     qsort(new_map->elts, new_map->nelts, sizeof(pr_fs_t *), fs_cmp);
 
     /* Destroy the old map */
-    if (fs_map)
+    if (fs_map != NULL) {
       destroy_pool(fs_map->pool);
+    }
 
     fs_map = new_map;
     chk_fs_map = TRUE;
   }
 
+  errno = xerrno;
   return res;
 }
 
+char *pr_fsio_getpipebuf(pool *p, int fd, long *bufsz) {
+  char *buf = NULL;
+  long buflen;
+
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (fd < 0) {
+    errno = EBADF;
+    return NULL;
+  }
+
+#if defined(PIPE_BUF)
+  buflen = PIPE_BUF;
+
+#elif defined(HAVE_FPATHCONF)
+  /* Some platforms do not define a PIPE_BUF constant.  For them, we need
+   * to use fpathconf(2), if available.
+   */
+  buflen = fpathconf(fd, _PC_PIPE_BUF);
+  if (buflen < 0) {
+    return NULL;
+  }
+
+#else
+  errno = ENOSYS;
+  return NULL;
+#endif
+
+  if (bufsz != NULL) {
+    *bufsz = buflen;
+  }
+
+  buf = palloc(p, buflen);
+  return buf;
+}
+
 char *pr_fsio_gets(char *buf, size_t size, pr_fh_t *fh) {
   char *bp = NULL;
   int toread = 0;
   pr_buffer_t *pbuf = NULL;
 
   if (buf == NULL ||
-      fh  == NULL ||
-      size <= 0) {
+      fh == NULL ||
+      size == 0) {
     errno = EINVAL;
     return NULL;
   }
@@ -4555,19 +6420,29 @@ char *pr_fsio_gets(char *buf, size_t size, pr_fh_t *fh) {
  * file is being read in, so that errors can be reported with line numbers
  * correctly.
  */
-char *pr_fsio_getline(char *buf, int buflen, pr_fh_t *fh,
+char *pr_fsio_getline(char *buf, size_t buflen, pr_fh_t *fh,
     unsigned int *lineno) {
   int inlen;
-  char *start = buf;
+  char *start;
+
+  if (buf == NULL ||
+      fh == NULL ||
+      buflen == 0) {
+    errno = EINVAL;
+    return NULL;
+  }
 
-  while (pr_fsio_gets(buf, buflen, fh)) {
+  start = buf;
+  while (pr_fsio_gets(buf, buflen, fh) != NULL) {
     pr_signals_handle();
 
     inlen = strlen(buf);
 
     if (inlen >= 1) {
       if (buf[inlen - 1] == '\n') {
-        (*lineno)++;
+        if (lineno != NULL) {
+          (*lineno)++;
+        }
 
         if (inlen >= 2 && buf[inlen - 2] == '\\') {
           char *bufp;
@@ -4581,7 +6456,7 @@ char *pr_fsio_getline(char *buf, int buflen, pr_fh_t *fh,
           for (bufp = buf; *bufp && PR_ISSPACE(*bufp); bufp++);
 
           if (*bufp == '#') {
-             continue;
+            continue;
           }
  
         } else {
@@ -4600,7 +6475,7 @@ char *pr_fsio_getline(char *buf, int buflen, pr_fh_t *fh,
     buf[0] = 0;
   }
 
-  return (buf > start ? start : 0);
+  return (buf > start ? start : NULL);
 }
 
 /* Be generous in the maximum allowed number of dup fds, in our search for
@@ -4616,7 +6491,7 @@ char *pr_fsio_getline(char *buf, int buflen, pr_fh_t *fh,
  * until the new fd is not one of the big three.
  */
 int pr_fs_get_usable_fd(int fd) {
-  register unsigned int i;
+  register int i;
   int fdi, dup_fds[FSIO_MAX_DUPFDS], n; 
 
   if (fd > STDERR_FILENO) {
@@ -4633,7 +6508,7 @@ int pr_fs_get_usable_fd(int fd) {
 
     dup_fds[i] = dup(fdi);
     if (dup_fds[i] < 0) {
-      register unsigned int j;
+      register int j;
       int xerrno  = errno;
 
       /* Need to clean up any previously opened dups as well. */
@@ -4676,7 +6551,7 @@ int pr_fs_get_usable_fd(int fd) {
 
   /* Free up the fds we opened in our search. */
   for (i = 0; i < n; i++) {
-    close(dup_fds[i]);
+    (void) close(dup_fds[i]);
     dup_fds[i] = -1;
   }
 
@@ -4755,6 +6630,11 @@ static int fs_getsize(int fd, char *path, off_t *fs_size) {
   struct statvfs fs;
 #  endif /* LFS && !Solaris 2.5.1 && !Solaris 2.6 && !Solaris 2.7 */
 
+  if (fs_size == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (path != NULL) {
     pr_trace_msg(trace_channel, 18, "using statvfs() on '%s'", path);
 
@@ -4801,11 +6681,16 @@ static int fs_getsize(int fd, char *path, off_t *fs_size) {
     *fs_size = get_fs_size(fs.f_bavail, fs.f_bsize);
   }
 
-  return 0;
+  res = 0;
 
 # elif defined(HAVE_SYS_VFS_H)
   struct statfs fs;
 
+  if (fs_size == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (path != NULL) {
     pr_trace_msg(trace_channel, 18, "using statfs() on '%s'", path);
 
@@ -4852,11 +6737,16 @@ static int fs_getsize(int fd, char *path, off_t *fs_size) {
     *fs_size = get_fs_size(fs.f_bavail, fs.f_bsize);
   }
 
-  return 0;
+  res = 0;
 
 # elif defined(HAVE_STATFS)
   struct statfs fs;
 
+  if (fs_size == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (path != NULL) {
     pr_trace_msg(trace_channel, 18, "using statfs() on '%s'", path);
 
@@ -4903,11 +6793,14 @@ static int fs_getsize(int fd, char *path, off_t *fs_size) {
     *fs_size = get_fs_size(fs.f_bavail, fs.f_bsize);
   }
 
-  return 0;
+  res = 0;
 
+# else
+  errno = ENOSYS:
+  res = -1;
 # endif /* !HAVE_STATFS && !HAVE_SYS_STATVFS && !HAVE_SYS_VFS */
-  errno = ENOSYS;
-  return -1;
+
+  return res;
 }
 
 #if defined(HAVE_STATFS) || defined(HAVE_SYS_STATVFS_H) || \
@@ -4918,7 +6811,8 @@ off_t pr_fs_getsize(char *path) {
 
   res = pr_fs_getsize2(path, &fs_size);
   if (res < 0) {
-    fs_size = 0;
+    errno = EINVAL;
+    fs_size = -1;
   }
 
   return fs_size;
@@ -4934,6 +6828,137 @@ int pr_fs_fgetsize(int fd, off_t *fs_size) {
   return fs_getsize(fd, NULL, fs_size);
 }
 
+void pr_fs_fadvise(int fd, off_t offset, off_t len, int advice) {
+#if defined(HAVE_POSIX_ADVISE)
+  int res, posix_advice;
+  const char *advice_str;
+
+  /* Convert from our advice values to the ones from the header; the
+   * indirection is needed for platforms which do not provide posix_fadvise(3).
+   */
+  switch (advice) {
+    case PR_FS_FADVISE_NORMAL:
+      advice_str = "NORMAL";
+      posix_advice = POSIX_FADV_NORMAL;
+      break;
+
+    case PR_FS_FADVISE_RANDOM:
+      advice_str = "RANDOM";
+      posix_advice = POSIX_FADV_RANDOM;
+      break;
+
+    case PR_FS_FADVISE_SEQUENTIAL:
+      advice_str = "SEQUENTIAL";
+      posix_advice = POSIX_FADV_SEQUENTIAL;
+      break;
+
+    case PR_FS_FADVISE_WILLNEED:
+      advice_str = "WILLNEED";
+      posix_advice = POSIX_FADV_WILLNEED;
+      break;
+
+    case PR_FS_FADVISE_DONTNEED:
+      advice_str = "DONTNEED";
+      posix_advice = POSIX_FADV_DONTNEED;
+      break;
+
+    case PR_FS_FADVISE_NOREUSE:
+      advice_str = "NOREUSE";
+      posix_advice = POSIX_FADV_NOREUSE;
+      break;
+
+    default:
+      pr_trace_msg(trace_channel, 9,
+        "unknown/unsupported advice: %d", advice);
+      return;
+  }
+
+  res = posix_fadvise(fd, offset, len, posix_advice);
+  if (res < 0) {
+    pr_trace_msg(trace_channel, 9,
+      "posix_fadvise() error on fd %d (off %" PR_LU ", len %" PR_LU ", "
+      "advice %s): %s", fd, (pr_off_t) offset, (pr_off_t) len, advice_str,
+      strerror(errno));
+  }
+#endif
+
+  return;
+}
+
+int pr_fs_have_access(struct stat *st, int mode, uid_t uid, gid_t gid,
+    array_header *suppl_gids) {
+  mode_t mask;
+
+  if (st == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Root always succeeds. */
+  if (uid == PR_ROOT_UID) {
+    return 0;
+  }
+
+  /* Initialize mask to reflect the permission bits that are applicable for
+   * the given user. mask contains the user-bits if the user ID equals the
+   * ID of the file owner. mask contains the group bits if the group ID
+   * belongs to the group of the file. mask will always contain the other
+   * bits of the permission bits.
+   */
+  mask = S_IROTH|S_IWOTH|S_IXOTH;
+
+  if (st->st_uid == uid) {
+    mask |= S_IRUSR|S_IWUSR|S_IXUSR;
+  }
+
+  /* Check the current group, as well as all supplementary groups.
+   * Fortunately, we have this information cached, so accessing it is
+   * almost free.
+   */
+  if (st->st_gid == gid) {
+    mask |= S_IRGRP|S_IWGRP|S_IXGRP;
+
+  } else {
+    if (suppl_gids != NULL) {
+      register unsigned int i = 0;
+
+      for (i = 0; i < suppl_gids->nelts; i++) {
+        if (st->st_gid == ((gid_t *) suppl_gids->elts)[i]) {
+          mask |= S_IRGRP|S_IWGRP|S_IXGRP;
+          break;
+        }
+      }
+    }
+  }
+
+  mask &= st->st_mode;
+
+  /* Perform requested access checks. */
+  if (mode & R_OK) {
+    if (!(mask & (S_IRUSR|S_IRGRP|S_IROTH))) {
+      errno = EACCES;
+      return -1;
+    }
+  }
+
+  if (mode & W_OK) {
+    if (!(mask & (S_IWUSR|S_IWGRP|S_IWOTH))) {
+      errno = EACCES;
+      return -1;
+    }
+  }
+
+  if (mode & X_OK) {
+    if (!(mask & (S_IXUSR|S_IXGRP|S_IXOTH))) {
+      errno = EACCES;
+      return -1;
+    }
+  }
+
+  /* F_OK already checked by checking the return value of stat. */
+  return 0;
+}
+
 int pr_fs_is_nfs(const char *path) {
 #if defined(HAVE_STATFS_F_TYPE) || defined(HAVE_STATFS_F_FSTYPENAME)
   struct statfs fs;
@@ -4985,7 +7010,8 @@ int pr_fs_is_nfs(const char *path) {
 }
 
 int pr_fsio_puts(const char *buf, pr_fh_t *fh) {
-  if (!fh) {
+  if (fh == NULL ||
+      buf == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -5002,25 +7028,33 @@ int pr_fsio_set_block(pr_fh_t *fh) {
   }
 
   flags = fcntl(fh->fh_fd, F_GETFL);
-  res = fcntl(fh->fh_fd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+  if (flags < 0) {
+    return -1;
+  }
 
+  res = fcntl(fh->fh_fd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
   return res;
 }
 
 void pr_resolve_fs_map(void) {
   register unsigned int i = 0;
 
-  if (!fs_map)
+  if (fs_map == NULL) {
     return;
+  }
 
   for (i = 0; i < fs_map->nelts; i++) {
     char *newpath = NULL;
-    unsigned char add_slash = FALSE;
-    pr_fs_t *tmpfs = ((pr_fs_t **) fs_map->elts)[i];
+    int add_slash = FALSE;
+    pr_fs_t *fsi;
+
+    pr_signals_handle();
+    fsi = ((pr_fs_t **) fs_map->elts)[i];
 
     /* Skip if this fs is the root fs. */
-    if (tmpfs == root_fs)
+    if (fsi == root_fs) {
       continue;
+    }
 
     /* Note that dir_realpath() does _not_ handle "../blah" paths
      * well, so...at least for now, hope that such paths are screened
@@ -5029,22 +7063,26 @@ void pr_resolve_fs_map(void) {
      * to re-add that slash to the adjusted path -- these trailing slashes
      * are important!
      */
-    if ((strncmp(tmpfs->fs_path, "/", 2) != 0 &&
-        (tmpfs->fs_path)[strlen(tmpfs->fs_path) - 1] == '/'))
+    if ((strncmp(fsi->fs_path, "/", 2) != 0 &&
+        (fsi->fs_path)[strlen(fsi->fs_path) - 1] == '/')) {
       add_slash = TRUE;
+    }
 
-    newpath = dir_realpath(tmpfs->fs_pool, tmpfs->fs_path);
+    newpath = dir_realpath(fsi->fs_pool, fsi->fs_path);
+    if (newpath != NULL) {
 
-    if (add_slash)
-      newpath = pstrcat(tmpfs->fs_pool, newpath, "/", NULL);
+      if (add_slash) {
+        newpath = pstrcat(fsi->fs_pool, newpath, "/", NULL);
+      }
 
-    /* Note that this does cause a slightly larger memory allocation from
-     * the pr_fs_t's pool, as the original path value was also allocated
-     * from that pool, and that original pointer is being overwritten.
-     * However, as this function is only called once, and that pool
-     * is freed later, I think this may be acceptable.
-     */
-    tmpfs->fs_path = newpath;
+      /* Note that this does cause a slightly larger memory allocation from
+       * the pr_fs_t's pool, as the original path value was also allocated
+       * from that pool, and that original pointer is being overwritten.
+       * However, as this function is only called once, and that pool
+       * is freed later, I think this may be acceptable.
+       */
+      fsi->fs_path = newpath;
+    }
   }
 
   /* Resort the map */
@@ -5076,7 +7114,6 @@ int init_fs(void) {
   root_fs->rename = sys_rename;
   root_fs->unlink = sys_unlink;
   root_fs->open = sys_open;
-  root_fs->creat = sys_creat;
   root_fs->close = sys_close;
   root_fs->read = sys_read;
   root_fs->write = sys_write;
@@ -5095,6 +7132,20 @@ int init_fs(void) {
   root_fs->faccess = sys_faccess;
   root_fs->utimes = sys_utimes;
   root_fs->futimes = sys_futimes;
+  root_fs->fsync = sys_fsync;
+
+  root_fs->getxattr = sys_getxattr;
+  root_fs->lgetxattr = sys_lgetxattr;
+  root_fs->fgetxattr = sys_fgetxattr;
+  root_fs->listxattr = sys_listxattr;
+  root_fs->llistxattr = sys_llistxattr;
+  root_fs->flistxattr = sys_flistxattr;
+  root_fs->removexattr = sys_removexattr;
+  root_fs->lremovexattr = sys_lremovexattr;
+  root_fs->fremovexattr = sys_fremovexattr;
+  root_fs->setxattr = sys_setxattr;
+  root_fs->lsetxattr = sys_lsetxattr;
+  root_fs->fsetxattr = sys_fsetxattr;
 
   root_fs->chdir = sys_chdir;
   root_fs->chroot = sys_chroot;
@@ -5114,7 +7165,10 @@ int init_fs(void) {
   }
 
   /* Prepare the stat cache as well. */
-  memset(&statcache, '\0', sizeof(statcache));
+  statcache_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(statcache_pool, "FS Statcache Pool");
+  stat_statcache_tab = pr_table_alloc(statcache_pool, 0);
+  lstat_statcache_tab = pr_table_alloc(statcache_pool, 0);
 
   return 0;
 }
@@ -5124,92 +7178,121 @@ int init_fs(void) {
 static const char *get_fs_hooks_str(pool *p, pr_fs_t *fs) {
   char *hooks = "";
 
-  if (fs->stat)
+  if (fs->stat) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "stat(2)", NULL);
+  }
 
-  if (fs->lstat)
+  if (fs->lstat) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "lstat(2)", NULL);
+  }
 
-  if (fs->fstat)
+  if (fs->fstat) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "fstat(2)", NULL);
+  }
 
-  if (fs->rename)
+  if (fs->rename) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "rename(2)", NULL);
+  }
 
-  if (fs->link)
+  if (fs->link) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "link(2)", NULL);
+  }
 
-  if (fs->unlink)
+  if (fs->unlink) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "unlink(2)", NULL);
+  }
 
-  if (fs->open)
+  if (fs->open) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "open(2)", NULL);
+  }
 
-  if (fs->creat)
-    hooks = pstrcat(p, hooks, *hooks ? ", " : "", "creat(2)", NULL);
-
-  if (fs->close)
+  if (fs->close) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "close(2)", NULL);
+  }
 
-  if (fs->read)
+  if (fs->read) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "read(2)", NULL);
+  }
 
-  if (fs->lseek)
+  if (fs->lseek) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "lseek(2)", NULL);
+  }
 
-  if (fs->readlink)
+  if (fs->readlink) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "readlink(2)", NULL);
+  }
 
-  if (fs->symlink)
+  if (fs->symlink) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "symlink(2)", NULL);
+  }
 
-  if (fs->ftruncate)
+  if (fs->ftruncate) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "ftruncate(2)", NULL);
+  }
 
-  if (fs->truncate)
+  if (fs->truncate) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "truncate(2)", NULL);
+  }
 
-  if (fs->chmod)
+  if (fs->chmod) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "chmod(2)", NULL);
+  }
 
-  if (fs->chown)
+  if (fs->chown) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "chown(2)", NULL);
+  }
 
-  if (fs->fchown)
+  if (fs->fchown) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "fchown(2)", NULL);
+  }
 
-  if (fs->lchown)
+  if (fs->lchown) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "lchown(2)", NULL);
+  }
 
-  if (fs->access)
+  if (fs->access) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "access(2)", NULL);
+  }
 
-  if (fs->faccess)
+  if (fs->faccess) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "faccess(2)", NULL);
+  }
 
-  if (fs->utimes)
+  if (fs->utimes) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "utimes(2)", NULL);
+  }
+
+  if (fs->futimes) {
+    hooks = pstrcat(p, hooks, *hooks ? ", " : "", "futimes(2)", NULL);
+  }
 
-  if (fs->futimes)
-    hooks = pstrcat(p, hooks, *hooks ? ", " : "", "futimes(3)", NULL);
+  if (fs->fsync) {
+    hooks = pstrcat(p, hooks, *hooks ? ", " : "", "fsync(2)", NULL);
+  }
 
-  if (fs->chdir)
+  if (fs->chdir) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "chdir(2)", NULL);
+  }
 
-  if (fs->chroot)
+  if (fs->chroot) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "chroot(2)", NULL);
+  }
 
-  if (fs->opendir)
+  if (fs->opendir) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "opendir(3)", NULL);
+  }
 
-  if (fs->closedir)
+  if (fs->closedir) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "closedir(3)", NULL);
+  }
 
-  if (fs->readdir)
+  if (fs->readdir) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "readdir(3)", NULL);
+  }
 
-  if (fs->mkdir)
+  if (fs->mkdir) {
     hooks = pstrcat(p, hooks, *hooks ? ", " : "", "mkdir(2)", NULL);
+  }
 
   if (!*hooks) {
     return pstrdup(p, "(none)");
@@ -5226,21 +7309,39 @@ static void get_fs_info(pool *p, int depth, pr_fs_t *fs,
   dumpf("FS#%u:    %s", depth, get_fs_hooks_str(p, fs));
 }
 
+static void fs_printf(const char *fmt, ...) {
+  char buf[PR_TUNABLE_BUFFER_SIZE+1];
+  va_list msg;
+
+  memset(buf, '\0', sizeof(buf));
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf)-1, fmt, msg);
+  va_end(msg);
+
+  buf[sizeof(buf)-1] = '\0';
+  pr_trace_msg(trace_channel, 19, "%s", buf);
+}
+
 void pr_fs_dump(void (*dumpf)(const char *, ...)) {
   pool *p;
 
+  if (dumpf == NULL) {
+    dumpf = fs_printf;
+  }
+
   dumpf("FS#0: 'system' mounted at '/', implementing the following hooks:");
   dumpf("FS#0:    (all)");
 
   if (!fs_map ||
-      fs_map->nelts == 0)
+      fs_map->nelts == 0) {
     return;
+  }
 
   p = make_sub_pool(permanent_pool);
 
   if (fs_map->nelts > 0) {
     pr_fs_t **fs_objs = (pr_fs_t **) fs_map->elts;
-    register int i;
+    register unsigned int i;
 
     for (i = 0; i < fs_map->nelts; i++) {
       pr_fs_t *fsi = fs_objs[i];
diff --git a/src/ftpdctl.c b/src/ftpdctl.c
index c3f4723..521c7ab 100644
--- a/src/ftpdctl.c
+++ b/src/ftpdctl.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD Controls command-line client
- * $Id: ftpdctl.c,v 1.20 2013-07-16 21:36:49 castaglia Exp $
- */
+/* ProFTPD Controls command-line client */
 
 #include "conf.h"
 #include "privs.h"
@@ -67,6 +65,9 @@ int pr_event_listening(const char *event) {
   return -1;
 }
 
+void pr_fs_fadvise(int fd, off_t off, off_t len, int advice) {
+}
+
 int pr_fs_get_usable_fd(int fd) {
   return -1;
 }
@@ -93,7 +94,7 @@ pr_table_t *pr_table_alloc(pool *p, int flags) {
   return NULL;
 }
 
-int pr_table_add(pr_table_t *tab, const char *k, void *v, size_t sz) {
+int pr_table_add(pr_table_t *tab, const char *k, const void *v, size_t sz) {
   errno = EPERM;
   return -1;
 }
@@ -118,12 +119,17 @@ int pr_table_free(pr_table_t *tab) {
   return -1;
 }
 
-void *pr_table_get(pr_table_t *tab, const char *k, size_t *sz) {
+const void *pr_table_get(pr_table_t *tab, const char *k, size_t *sz) {
+  errno = EPERM;
+  return NULL;
+}
+
+const void *pr_table_remove(pr_table_t *tab, const char *k, size_t *sz) {
   errno = EPERM;
   return NULL;
 }
 
-int pr_table_set(pr_table_t *tab, const char *k, void *v, size_t sz) {
+int pr_table_set(pr_table_t *tab, const char *k, const void *v, size_t sz) {
   errno = EPERM;
   return -1;
 }
@@ -169,7 +175,7 @@ static RETSIGTYPE sig_pipe(int sig) {
 }
 
 static void usage(void) {
-  fprintf(stdout, "usage: %s [options]\n", program);
+  fprintf(stdout, "usage: %s [options] action [...]\n", program);
   fprintf(stdout, "  -h\tdisplays this message\n");
   fprintf(stdout, "  -s\tspecify an alternate local socket\n");
   fprintf(stdout, "  -v\tdisplays more verbose information\n");
@@ -189,12 +195,6 @@ int main(int argc, char *argv[]) {
   pool *ctl_pool = NULL;
   array_header *reqargv = NULL;
 
-  /* Make sure we were called with at least one argument. */
-  if (argc-1 < 1) {
-    fprintf(stdout, "%s: missing required arguments\n", program);
-    exit(1);
-  }
-
   /* Set the POSIXLY_CORRECT environment variable, so that control handlers
    * can themselves have optional flags.
    */
@@ -231,6 +231,12 @@ int main(int argc, char *argv[]) {
     }
   }
 
+  /* Make sure we were called with at least one argument. */
+  if (argv[optind] == NULL) {
+    fprintf(stdout, "%s: missing required action\n", program);
+    exit(1);
+  }
+
   signal(SIGPIPE, sig_pipe);
 
   /* Allocate some memory for proftpd objects. */
diff --git a/src/help.c b/src/help.c
index 56816bd..c1fd865 100644
--- a/src/help.c
+++ b/src/help.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2011 The ProFTPD Project team
+ * Copyright (c) 2004-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* HELP management code
- * $Id: help.c,v 1.7 2011-05-23 21:22:24 castaglia Exp $
- */
+/* HELP management code */
 
 #include "conf.h"
 
@@ -40,11 +38,13 @@ static array_header *help_list = NULL;
 void pr_help_add(const char *cmd, const char *syntax, int impl) {
   struct help_rec *help;
 
-  if (!cmd || !syntax)
+  if (cmd == NULL ||
+      syntax == NULL) {
     return;
+  }
 
   /* If no list has been allocated, create one. */
-  if (!help_pool) {
+  if (help_pool == NULL) {
     help_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(help_pool, "Help Pool");
     help_list = make_array(help_pool, 0, sizeof(struct help_rec));
@@ -59,7 +59,7 @@ void pr_help_add(const char *cmd, const char *syntax, int impl) {
     register unsigned int i = 0;
     struct help_rec *helps = help_list->elts;
 
-    for (i = 0; i < help_list->nelts; i++)
+    for (i = 0; i < help_list->nelts; i++) {
       if (strcmp(helps[i].cmd, cmd) == 0) {
         if (helps[i].impl == FALSE &&
             impl == TRUE) {
@@ -68,6 +68,7 @@ void pr_help_add(const char *cmd, const char *syntax, int impl) {
 
         return;
       }
+    }
   }
 
   help = push_array(help_list);
@@ -77,6 +78,11 @@ void pr_help_add(const char *cmd, const char *syntax, int impl) {
 }
 
 int pr_help_add_response(cmd_rec *cmd, const char *target) {
+  if (cmd == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (help_list) {
     register unsigned int i;
     struct help_rec *helps = help_list->elts;
@@ -84,7 +90,9 @@ int pr_help_add_response(cmd_rec *cmd, const char *target) {
     char buf[9] = {'\0'};
     int col = 0;
 
-    if (!target) {
+    if (target == NULL) {
+      const char *server_admin = "ftp-admin";
+
       pr_response_add(R_214,
         _("The following commands are recognized (* =>'s unimplemented):"));
 
@@ -102,7 +110,7 @@ int pr_help_add_response(cmd_rec *cmd, const char *target) {
 
         /* 8 rows */
         if ((i + 1) % 8 == 0 ||
-            helps[i+1].cmd == NULL) {
+            (i+1 == help_list->nelts)) {
           register unsigned int j;
 
           for (j = 0; j < 8; j++) {
@@ -116,31 +124,32 @@ int pr_help_add_response(cmd_rec *cmd, const char *target) {
             }
           }
 
-          if (*outstr)
+          if (*outstr) {
             pr_response_add(R_DUP, "%s", outstr);
+          }
 
           memset(outa, '\0', sizeof(outa));
           col = 0;
         }
       }
 
-      pr_response_add(R_DUP, _("Direct comments to %s"),
-        cmd->server->ServerAdmin ? cmd->server->ServerAdmin : "ftp-admin");
+      if (cmd->server != NULL &&
+          cmd->server->ServerAdmin != NULL) {
+        server_admin = cmd->server->ServerAdmin;
+      }
 
-    } else {
+      pr_response_add(R_DUP, _("Direct comments to %s"), server_admin);
+      return 0;
+    }
 
-      /* List the syntax for the given target command. */
-      for (i = 0; i < help_list->nelts; i++) {
-        if (strcasecmp(helps[i].cmd, target) == 0) {
-          pr_response_add(R_214, "Syntax: %s %s", helps[i].cmd,
-            helps[i].syntax);
-          return 0;
-        }
+    /* List the syntax for the given target command. */
+    for (i = 0; i < help_list->nelts; i++) {
+      if (strcasecmp(helps[i].cmd, target) == 0) {
+        pr_response_add(R_214, "Syntax: %s %s", helps[i].cmd,
+          helps[i].syntax);
+        return 0;
       }
     }
-
-    errno = ENOENT;
-    return -1;
   }
 
   errno = ENOENT;
diff --git a/src/ident.c b/src/ident.c
index 403c6e5..30da4b5 100644
--- a/src/ident.c
+++ b/src/ident.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2007 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,10 +23,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/*
- * Ident (RFC1413) protocol support
- * $Id: ident.c,v 1.23 2007-10-22 18:09:18 castaglia Exp $
- */
+/* Ident (RFC1413) protocol support */
 
 #include "conf.h"
 
diff --git a/src/inet.c b/src/inet.c
index 4a3e7df..b01230d 100644
--- a/src/inet.c
+++ b/src/inet.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -72,10 +72,15 @@ int pr_inet_set_default_family(pool *p, int family) {
 
 /* Find a service and return its port number. */
 int pr_inet_getservport(pool *p, const char *serv, const char *proto) {
-  struct servent *servent = getservbyname(serv, proto);
+  struct servent *servent;
+
+  servent = getservbyname(serv, proto);
+  if (servent == NULL) {
+    return -1;
+  }
 
   /* getservbyname returns the port in network byte order. */
-  return (servent ? ntohs(servent->s_port) : -1);
+  return ntohs(servent->s_port);
 }
 
 static void conn_cleanup_cb(void *cv) {
@@ -118,8 +123,14 @@ conn_t *pr_inet_copy_conn(pool *p, conn_t *c) {
   conn_t *res = NULL;
   pool *sub_pool = NULL;
 
+  if (p == NULL ||
+      c == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   sub_pool = make_sub_pool(p);
-  pr_pool_tag(sub_pool, "pr_inet_copy_conn() subpool");
+  pr_pool_tag(sub_pool, "inet_copy_conn pool");
 
   res = (conn_t *) pcalloc(sub_pool, sizeof(conn_t));
 
@@ -127,30 +138,35 @@ conn_t *pr_inet_copy_conn(pool *p, conn_t *c) {
   res->pool = sub_pool;
   res->instrm = res->outstrm = NULL;
 
-  if (c->local_addr) {
-    res->local_addr = pr_netaddr_alloc(res->pool);
+  if (c->local_addr != NULL) {
+    pr_netaddr_t *local_addr;
+
+    local_addr = pr_netaddr_alloc(res->pool);
 
-    if (pr_netaddr_set_family(res->local_addr,
+    if (pr_netaddr_set_family(local_addr,
         pr_netaddr_get_family(c->local_addr)) < 0) {
       destroy_pool(res->pool);
       return NULL;
     }
 
-    pr_netaddr_set_sockaddr(res->local_addr,
-      pr_netaddr_get_sockaddr(c->local_addr));
+    pr_netaddr_set_sockaddr(local_addr, pr_netaddr_get_sockaddr(c->local_addr));
+    res->local_addr = local_addr;
   }
 
-  if (c->remote_addr) {
-    res->remote_addr = pr_netaddr_alloc(res->pool);
+  if (c->remote_addr != NULL) {
+    pr_netaddr_t *remote_addr;
 
-    if (pr_netaddr_set_family(res->remote_addr,
+    remote_addr = pr_netaddr_alloc(res->pool);
+
+    if (pr_netaddr_set_family(remote_addr,
         pr_netaddr_get_family(c->remote_addr)) < 0) {
       destroy_pool(res->pool);
       return NULL;
     }
 
-    pr_netaddr_set_sockaddr(res->remote_addr,
+    pr_netaddr_set_sockaddr(remote_addr,
       pr_netaddr_get_sockaddr(c->remote_addr));
+    res->remote_addr = remote_addr;
   }
 
   if (c->remote_name) {
@@ -164,7 +180,7 @@ conn_t *pr_inet_copy_conn(pool *p, conn_t *c) {
 /* Initialize a new connection record, also creates a new subpool just for the
  * new connection.
  */
-static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
+static conn_t *init_conn(pool *p, int fd, const pr_netaddr_t *bind_addr,
     int port, int retry_bind, int reporting) {
   pool *sub_pool = NULL;
   conn_t *c;
@@ -172,6 +188,11 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
   int addr_family;
   int res = 0, one = 1, hold_errno;
 
+  if (p == NULL) {
+    errno = inet_errno = EINVAL;
+    return NULL;
+  }
+
   if (!inet_pool) {
     inet_pool = make_sub_pool(permanent_pool);
     pr_pool_tag(inet_pool, "Inet Pool");
@@ -181,7 +202,7 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
   pr_netaddr_clear(&na);
 
   sub_pool = make_sub_pool(p);
-  pr_pool_tag(sub_pool, "init_conn() subpool");
+  pr_pool_tag(sub_pool, "init_conn pool");
 
   c = (conn_t *) pcalloc(sub_pool, sizeof(conn_t));
   c->pool = sub_pool;
@@ -236,7 +257,7 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
     defined(__OpenBSD__) || defined(__NetBSD__) || \
     defined(DARWIN6) || defined(DARWIN7) || defined(DARWIN8) || \
     defined(DARWIN9) || defined(DARWIN10) || defined(DARWIN11) || \
-    defined(DARWIN12) || \
+    defined(DARWIN12) || defined(DARWIN13) || defined(DARWIN14) || \
     defined(SCO3) || defined(CYGWIN) || defined(SYSV4_2MP) || \
     defined(SYSV5SCO_SV6) || defined(SYSV5UNIXWARE7)
 # ifdef SOLARIS2
@@ -261,7 +282,7 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
     defined(__OpenBSD__) || defined(__NetBSD__) || \
     defined(DARWIN6) || defined(DARWIN7) || defined(DARWIN8) || \
     defined(DARWIN9) || defined(DARWIN10) || defined(DARWIN11) || \
-    defined(DARWIN12) || \
+    defined(DARWIN12) || defined(DARWIN13) || defined(DARWIN14) || \
     defined(SCO3) || defined(CYGWIN) || defined(SYSV4_2MP) || \
     defined(SYSV5SCO_SV6) || defined(SYSV5UNIXWARE7)
 # ifdef SOLARIS2
@@ -276,7 +297,6 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
     }
 
     if (fd == -1) {
-
       /* On failure, destroy the connection and return NULL. */
       if (reporting) {
         pr_log_pri(PR_LOG_WARNING,
@@ -303,9 +323,25 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
         strerror(errno));
     }
 
+#if defined(IP_FREEBIND)
+    /* Allow binding to an as-yet-nonexistent address. */
+    if (setsockopt(fd, SOL_IP, IP_FREEBIND, (void *) &one,
+        sizeof(one)) < 0) {
+      if (errno != ENOSYS) {
+        pr_log_pri(PR_LOG_INFO, "error setting IP_FREEBIND: %s",
+          strerror(errno));
+      }
+    }
+#endif /* IP_FREEBIND */
+
     memset(&na, 0, sizeof(na));
     if (pr_netaddr_set_family(&na, addr_family) < 0) {
+      int xerrno = errno;
+
       destroy_pool(c->pool);
+      (void) close(fd);
+
+      errno = xerrno;
       return NULL;
     }
 
@@ -454,14 +490,23 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
 
     salen = pr_netaddr_get_sockaddr_len(&na);
     if (getsockname(fd, pr_netaddr_get_sockaddr(&na), &salen) == 0) {
-      if (c->local_addr == NULL) {
-        c->local_addr = pr_netaddr_alloc(c->pool);
+      pr_netaddr_t *local_addr;
+
+      if (c->local_addr != NULL) {
+        local_addr = (pr_netaddr_t *) c->local_addr;
+
+      } else {
+        local_addr = pr_netaddr_alloc(c->pool);
       }
 
-      pr_netaddr_set_family(c->local_addr, pr_netaddr_get_family(&na));
-      pr_netaddr_set_sockaddr(c->local_addr, pr_netaddr_get_sockaddr(&na));
+      pr_netaddr_set_family(local_addr, pr_netaddr_get_family(&na));
+      pr_netaddr_set_sockaddr(local_addr, pr_netaddr_get_sockaddr(&na));
       c->local_port = ntohs(pr_netaddr_get_port(&na));
 
+      if (c->local_addr == NULL) {
+        c->local_addr = local_addr;
+      }
+
     } else {
       pr_log_debug(DEBUG3, "getsockname error on socket %d: %s", fd,
         strerror(errno));
@@ -483,7 +528,7 @@ static conn_t *init_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
   return c;
 }
 
-conn_t *pr_inet_create_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
+conn_t *pr_inet_create_conn(pool *p, int fd, const pr_netaddr_t *bind_addr,
     int port, int retry_bind) {
   conn_t *c = NULL;
 
@@ -498,13 +543,24 @@ conn_t *pr_inet_create_conn(pool *p, int fd, pr_netaddr_t *bind_addr,
 /* Attempt to create a connection bound to a given port range, returns NULL
  * if unable to bind to any port in the range.
  */
-conn_t *pr_inet_create_conn_portrange(pool *p, pr_netaddr_t *bind_addr,
+conn_t *pr_inet_create_conn_portrange(pool *p, const pr_netaddr_t *bind_addr,
     int low_port, int high_port) {
   int range_len, i;
   int *range, *ports;
   int attempt, random_index;
   conn_t *c = NULL;
 
+  if (low_port < 0 ||
+      high_port < 0) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (low_port >= high_port) {
+    errno = EPERM;
+    return NULL;
+  }
+
   /* Make sure the temporary inet work pool exists. */
   if (!inet_pool) {
     inet_pool = make_sub_pool(permanent_pool); 
@@ -516,16 +572,15 @@ conn_t *pr_inet_create_conn_portrange(pool *p, pr_netaddr_t *bind_addr,
   ports = (int *) pcalloc(inet_pool, range_len * sizeof(int));
 
   i = range_len;
-  while (i--)
+  while (i--) {
     range[i] = low_port + i;
+  }
 
   for (attempt = 3; attempt > 0 && !c; attempt--) {
     for (i = range_len - 1; i >= 0 && !c; i--) {
-
       /* If this is the first attempt through the range, randomize
        * the order of the port numbers used.
        */
-
       if (attempt == 3) {
 	/* Obtain a random index into the port array range. */
 	random_index = (int) ((1.0 * i * rand()) / (RAND_MAX+1.0));
@@ -538,7 +593,6 @@ conn_t *pr_inet_create_conn_portrange(pool *p, pr_netaddr_t *bind_addr,
 	/* Move non-selected numbers down so that the next randomly chosen
 	 * port will be from the range of as-yet untried ports.
 	 */
-
 	while (++random_index <= i) {
 	  range[random_index-1] = range[random_index];
         }
@@ -570,21 +624,30 @@ void pr_inet_close(pool *p, conn_t *c) {
    * Simply destroy the pool and all the dirty work gets done.
    */
 
-  destroy_pool(c->pool);
+  if (c->pool != NULL) {
+    destroy_pool(c->pool);
+    c->pool = NULL;
+  }
 }
 
 /* Perform shutdown/read on streams */
 void pr_inet_lingering_close(pool *p, conn_t *c, long linger) {
-  pr_inet_set_block(p, c);
+  if (c == NULL) {
+    return;
+  }
 
-  if (c->outstrm)
+  (void) pr_inet_set_block(p, c);
+
+  if (c->outstrm) {
     pr_netio_lingering_close(c->outstrm, linger);
+  }
 
   /* Only close the input stream if it is actually a different stream than
    * the output stream.
    */
-  if (c->instrm != c->outstrm)
+  if (c->instrm != c->outstrm) {
     pr_netio_close(c->instrm);
+  }
 
   c->outstrm = NULL;
   c->instrm = NULL;
@@ -594,10 +657,15 @@ void pr_inet_lingering_close(pool *p, conn_t *c, long linger) {
 
 /* Similar to a lingering close, perform a lingering abort. */
 void pr_inet_lingering_abort(pool *p, conn_t *c, long linger) {
-  pr_inet_set_block(p, c);
+  if (c == NULL) {
+    return;
+  }
+
+  (void) pr_inet_set_block(p, c);
 
-  if (c->instrm)
+  if (c->instrm) {
     pr_netio_lingering_abort(c->instrm, linger);
+  }
 
   /* Only close the output stream if it is actually a different stream
    * than the input stream.
@@ -606,8 +674,9 @@ void pr_inet_lingering_abort(pool *p, conn_t *c, long linger) {
    * since doing so would result in two 426 responses sent; we only
    * want and need one.
    */
-  if (c->outstrm != c->instrm)
+  if (c->outstrm != c->instrm) {
     pr_netio_close(c->outstrm);
+  }
 
   c->instrm = NULL;
   c->outstrm = NULL;
@@ -655,10 +724,16 @@ int pr_inet_set_proto_nodelay(pool *p, conn_t *conn, int nodelay) {
   int tcp_level = tcp_proto;
 # endif /* SOL_TCP */
 
+  if (conn == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (conn->rfd != -1) {
     res = setsockopt(conn->rfd, tcp_level, TCP_NODELAY, (void *) &nodelay,
       sizeof(nodelay));
-    if (res < 0) {
+    if (res < 0 &&
+        errno != EBADF) {
       pr_log_pri(PR_LOG_NOTICE, "error setting read fd %d TCP_NODELAY %d: %s",
        conn->rfd, nodelay, strerror(errno));
     }
@@ -667,7 +742,9 @@ int pr_inet_set_proto_nodelay(pool *p, conn_t *conn, int nodelay) {
   if (conn->wfd != -1) {
     res = setsockopt(conn->wfd, tcp_level, TCP_NODELAY, (void *) &nodelay,
       sizeof(nodelay));
-    if (res < 0) {
+    if (res < 0 &&
+        errno != EBADF &&
+        errno != EINVAL) {
       pr_log_pri(PR_LOG_NOTICE, "error setting write fd %d TCP_NODELAY %d: %s",
        conn->wfd, nodelay, strerror(errno));
     }
@@ -697,39 +774,54 @@ int pr_inet_set_proto_opts(pool *p, conn_t *c, int mss, int nodelay,
 #else
   int tcp_level = tcp_proto;
 #endif /* SOL_TCP */
+  unsigned char *no_delay = NULL;
 
   /* Some of these setsockopt() calls may fail when they operate on IPv6
    * sockets, rather than on IPv4 sockets.
    */
 
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
 #ifdef TCP_NODELAY
-  unsigned char *no_delay = get_param_ptr(main_server->conf, "tcpNoDelay",
-    FALSE);
 
-  if (!no_delay ||
-      *no_delay == TRUE) {
+  /* Note: main_server might be null when those code runs in the testsuite. */
+  if (main_server != NULL) {
+    no_delay = get_param_ptr(main_server->conf, "TCPNoDelay", FALSE);
+  }
 
+  if (no_delay == NULL ||
+      *no_delay == TRUE) {
     if (c->rfd != -1) {
       if (setsockopt(c->rfd, tcp_level, TCP_NODELAY, (void *) &nodelay,
           sizeof(nodelay)) < 0) {
-        pr_log_pri(PR_LOG_NOTICE, "error setting read fd %d TCP_NODELAY: %s",
-          c->rfd, strerror(errno));
+        if (errno != EBADF) {
+          pr_log_pri(PR_LOG_NOTICE, "error setting read fd %d TCP_NODELAY: %s",
+            c->rfd, strerror(errno));
+        }
       }
     }
 
     if (c->wfd != -1) {
       if (setsockopt(c->wfd, tcp_level, TCP_NODELAY, (void *) &nodelay,
           sizeof(nodelay)) < 0) {
-        pr_log_pri(PR_LOG_NOTICE, "error setting write fd %d TCP_NODELAY: %s",
-          c->wfd, strerror(errno));
+        if (errno != EBADF) {
+          pr_log_pri(PR_LOG_NOTICE, "error setting write fd %d TCP_NODELAY: %s",
+            c->wfd, strerror(errno));
+        }
       }
     }
 
     if (c->listen_fd != -1) {
       if (setsockopt(c->listen_fd, tcp_level, TCP_NODELAY, (void *) &nodelay,
           sizeof(nodelay)) < 0) {
-        pr_log_pri(PR_LOG_NOTICE, "error setting listen fd %d TCP_NODELAY: %s",
-          c->listen_fd, strerror(errno));
+        if (errno != EBADF) {
+          pr_log_pri(PR_LOG_NOTICE,
+            "error setting listen fd %d TCP_NODELAY: %s",
+            c->listen_fd, strerror(errno));
+        }
       }
     }
   }
@@ -737,7 +829,7 @@ int pr_inet_set_proto_opts(pool *p, conn_t *c, int mss, int nodelay,
 
 #ifdef TCP_MAXSEG
   if (c->listen_fd != -1 &&
-      mss) {
+      mss > 0) {
     if (setsockopt(c->listen_fd, tcp_level, TCP_MAXSEG, &mss,
         sizeof(mss)) < 0) {
       pr_log_pri(PR_LOG_NOTICE, "error setting listen fd TCP_MAXSEG(%d): %s",
@@ -770,6 +862,7 @@ int pr_inet_set_proto_opts(pool *p, conn_t *c, int mss, int nodelay,
         res = setsockopt(c->listen_fd, level, IPV6_TCLASS, (void *) &tos,
           sizeof(tos));
         if (res < 0
+            && errno != EINVAL
 #ifdef ENOPROTOOPT
             && errno != ENOPROTOOPT
 #endif /* !ENOPROTOOPT */
@@ -796,6 +889,11 @@ int pr_inet_set_proto_opts(pool *p, conn_t *c, int mss, int nodelay,
 int pr_inet_set_socket_opts(pool *p, conn_t *c, int rcvbuf, int sndbuf,
     struct tcp_keepalive *tcp_keepalive) {
 
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   /* Linux and "most" newer networking OSes probably use a highly adaptive
    * window size system, which generally wouldn't require user-space
    * modification at all.  Thus, check the current sndbuf and rcvbuf sizes
@@ -892,24 +990,30 @@ int pr_inet_set_socket_opts(pool *p, conn_t *c, int rcvbuf, int sndbuf,
 
     if (sndbuf > 0) {
       len = sizeof(csndbuf);
-      getsockopt(c->listen_fd, SOL_SOCKET, SO_SNDBUF, (void *) &csndbuf, &len);
+      if (getsockopt(c->listen_fd, SOL_SOCKET, SO_SNDBUF, (void *) &csndbuf,
+          &len) == 0) {
+        if (sndbuf > csndbuf) {
+          if (setsockopt(c->listen_fd, SOL_SOCKET, SO_SNDBUF, (void *) &sndbuf,
+              sizeof(sndbuf)) < 0) {
+            pr_log_pri(PR_LOG_NOTICE, "error setting listen fd SO_SNDBUF: %s",
+              strerror(errno));
 
-      if (sndbuf > csndbuf) {
-        if (setsockopt(c->listen_fd, SOL_SOCKET, SO_SNDBUF, (void *) &sndbuf,
-            sizeof(sndbuf)) < 0) {
-          pr_log_pri(PR_LOG_NOTICE, "error setting listen fd SO_SNDBUF: %s",
-            strerror(errno));
+          } else {
+            pr_trace_msg("data", 8,
+              "set socket sndbuf of %lu bytes", (unsigned long) sndbuf);
+          }
 
         } else {
           pr_trace_msg("data", 8,
-            "set socket sndbuf of %lu bytes", (unsigned long) sndbuf);
+            "socket %d has sndbuf of %lu bytes, ignoring "
+            "requested %lu bytes sndbuf", c->listen_fd, (unsigned long) csndbuf,
+            (unsigned long) sndbuf);
         }
 
       } else {
-        pr_trace_msg("data", 8,
-          "socket %d has sndbuf of %lu bytes, ignoring "
-          "requested %lu bytes sndbuf", c->listen_fd, (unsigned long) csndbuf,
-          (unsigned long) sndbuf);
+        pr_trace_msg("data", 3,
+          "error getting SO_SNDBUF on listen fd %d: %s", c->listen_fd,
+          strerror(errno));
       }
     }
 
@@ -917,24 +1021,30 @@ int pr_inet_set_socket_opts(pool *p, conn_t *c, int rcvbuf, int sndbuf,
 
     if (rcvbuf > 0) {
       len = sizeof(crcvbuf);
-      getsockopt(c->listen_fd, SOL_SOCKET, SO_RCVBUF, (void *) &crcvbuf, &len);
+      if (getsockopt(c->listen_fd, SOL_SOCKET, SO_RCVBUF, (void *) &crcvbuf,
+          &len) == 0) {
+        if (rcvbuf > crcvbuf) {
+          if (setsockopt(c->listen_fd, SOL_SOCKET, SO_RCVBUF, (void *) &rcvbuf,
+              sizeof(rcvbuf)) < 0) {
+            pr_log_pri(PR_LOG_NOTICE, "error setting listen fd SO_RCVFBUF: %s",
+              strerror(errno));
 
-      if (rcvbuf > crcvbuf) {
-        if (setsockopt(c->listen_fd, SOL_SOCKET, SO_RCVBUF, (void *) &rcvbuf,
-            sizeof(rcvbuf)) < 0) {
-          pr_log_pri(PR_LOG_NOTICE, "error setting listen fd SO_RCVFBUF: %s",
-            strerror(errno));
+          } else {
+            pr_trace_msg("data", 8,
+              "set socket rcvbuf of %lu bytes", (unsigned long) rcvbuf);
+          }
 
         } else {
           pr_trace_msg("data", 8,
-            "set socket rcvbuf of %lu bytes", (unsigned long) rcvbuf);
+           "socket %d has rcvbuf of %lu bytes, ignoring "
+            "requested %lu bytes rcvbuf", c->listen_fd, (unsigned long) crcvbuf,
+            (unsigned long) rcvbuf);
         }
 
       } else {
-        pr_trace_msg("data", 8,
-          "socket %d has rcvbuf of %lu bytes, ignoring "
-          "requested %lu bytes rcvbuf", c->listen_fd, (unsigned long) crcvbuf,
-          (unsigned long) rcvbuf);
+        pr_trace_msg("data", 3,
+          "error getting SO_RCVBUF on listen fd %d: %s", c->listen_fd,
+          strerror(errno));
       }
     }
 
@@ -947,42 +1057,57 @@ int pr_inet_set_socket_opts(pool *p, conn_t *c, int rcvbuf, int sndbuf,
 #ifdef SO_OOBINLINE
 static void set_oobinline(int fd) {
   int on = 1;
-  if (fd != -1)
-    if (setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, (void*)&on, sizeof(on)) < 0)
+  if (fd >= 0) {
+    if (setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, (void*)&on, sizeof(on)) < 0) {
       pr_log_pri(PR_LOG_NOTICE, "error setting SO_OOBINLINE: %s",
         strerror(errno));
+    }
+  }
 }
 #endif
 
 #ifdef F_SETOWN
-static void set_owner(int fd) {
-  if (fd != -1)
-    fcntl(fd, F_SETOWN, session.pid ? session.pid : getpid());
+static void set_socket_owner(int fd) {
+  if (fd >= 0) {
+    pid_t pid;
+
+    pid = session.pid ? session.pid : getpid();
+    if (fcntl(fd, F_SETOWN, pid) < 0) {
+      pr_trace_msg(trace_channel, 3,
+        "failed to SETOWN PID %lu on socket fd %d: %s", (unsigned long) pid,
+        fd, strerror(errno));
+    }
+  }
 }
 #endif
 
 /* Put a socket in async mode (so SIGURG is raised on OOB)
  */
 int pr_inet_set_async(pool *p, conn_t *c) {
+  if (p == NULL ||
+      c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
 #ifdef SO_OOBINLINE
   pr_trace_msg(trace_channel, 7,
-    "setting SO_OOBINLINE for listening socket %d",  c->listen_fd);
+    "setting SO_OOBINLINE for listening socket %d", c->listen_fd);
   set_oobinline(c->listen_fd);
 
   pr_trace_msg(trace_channel, 7,
-    "setting SO_OOBINLINE for reading socket %d",  c->rfd);
+    "setting SO_OOBINLINE for reading socket %d", c->rfd);
   set_oobinline(c->rfd);
 
   pr_trace_msg(trace_channel, 7,
-    "setting SO_OOBINLINE for writing socket %d",  c->wfd);
+    "setting SO_OOBINLINE for writing socket %d", c->wfd);
   set_oobinline(c->wfd);
 #endif
 
 #ifdef F_SETOWN
-  set_owner(c->listen_fd);
-  set_owner(c->rfd);
-  set_owner(c->wfd);
+  set_socket_owner(c->listen_fd);
+  set_socket_owner(c->rfd);
+  set_socket_owner(c->wfd);
 #endif
 
   return 0;
@@ -994,22 +1119,44 @@ int pr_inet_set_nonblock(pool *p, conn_t *c) {
   int flags;
   int res = -1;
 
+  (void) p;
+
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   errno = EBADF;		/* Default */
 
   if (c->mode == CM_LISTEN ||
       c->mode == CM_CONNECT) {
     flags = fcntl(c->listen_fd, F_GETFL);
-    res = fcntl(c->listen_fd, F_SETFL, flags|O_NONBLOCK);
+    if (flags >= 0) {
+      res = fcntl(c->listen_fd, F_SETFL, flags|O_NONBLOCK);
+
+    } else {
+      res = flags;
+    }
 
   } else {
     if (c->rfd != -1) {
       flags = fcntl(c->rfd, F_GETFL);
-      res = fcntl(c->rfd, F_SETFL, flags|O_NONBLOCK);
+      if (flags >= 0) {
+        res = fcntl(c->rfd, F_SETFL, flags|O_NONBLOCK);
+
+      } else {
+        res = flags;
+      }
     }
 
     if (c->wfd != -1) {
       flags = fcntl(c->wfd, F_GETFL);
-      res = fcntl(c->wfd, F_SETFL, flags|O_NONBLOCK);
+      if (flags >= 0) {
+        res = fcntl(c->wfd, F_SETFL, flags|O_NONBLOCK);
+
+      } else {
+        res = flags;
+      }
     }
   }
 
@@ -1020,36 +1167,64 @@ int pr_inet_set_block(pool *p, conn_t *c) {
   int flags;
   int res = -1;
 
+  (void) p;
+
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   errno = EBADF;		/* Default */
 
   if (c->mode == CM_LISTEN ||
       c->mode == CM_CONNECT) {
     flags = fcntl(c->listen_fd, F_GETFL);
-    res = fcntl(c->listen_fd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+    if (flags >= 0) {
+      res = fcntl(c->listen_fd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+
+    } else {
+      res = flags;
+    }
 
   } else {
     if (c->rfd != -1) {
       flags = fcntl(c->rfd, F_GETFL);
-      res = fcntl(c->rfd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+      if (flags >= 0) {
+        res = fcntl(c->rfd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+
+      } else {
+        res = flags;
+      }
     }
 
     if (c->wfd != -1) {
       flags = fcntl(c->wfd, F_GETFL);
-      res = fcntl(c->wfd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+      if (flags >= 0) {
+        res = fcntl(c->wfd, F_SETFL, flags & (U32BITS ^ O_NONBLOCK));
+
+      } else {
+        res = flags;
+      }
     }
   }
 
   return res;
 }
 
-/* Put a connection in listen mode
- */
+/* Put a connection in listen mode */
 int pr_inet_listen(pool *p, conn_t *c, int backlog, int flags) {
-  if (!c || c->mode == CM_LISTEN)
+  if (c == NULL) {
+    errno = EINVAL;
     return -1;
+  }
+
+  if (c->mode == CM_LISTEN) {
+    errno = EPERM;
+    return -1;
+  }
 
   while (TRUE) {
-    if (listen(c->listen_fd, backlog) == -1) {
+    if (listen(c->listen_fd, backlog) < 0) {
       int xerrno = errno;
 
       if (xerrno == EINTR) {
@@ -1066,10 +1241,9 @@ int pr_inet_listen(pool *p, conn_t *c, int backlog, int flags) {
 
       errno = xerrno;
       return -1;
-
-    } else {
-      break;
     }
+
+    break;
   }
 
   c->mode = CM_LISTEN;
@@ -1080,15 +1254,30 @@ int pr_inet_listen(pool *p, conn_t *c, int backlog, int flags) {
  * for safety.
  */
 int pr_inet_resetlisten(pool *p, conn_t *c) {
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   c->mode = CM_LISTEN;
-  pr_inet_set_block(c->pool, c);
+  if (pr_inet_set_block(c->pool, c) < 0) {
+    c->xerrno = errno;
+    return -1;
+  }
+
   return 0;
 }
 
-int pr_inet_connect(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
+int pr_inet_connect(pool *p, conn_t *c, const pr_netaddr_t *addr, int port) {
   pr_netaddr_t remote_na;
   int res = 0;
 
+  if (c == NULL ||
+      addr == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   c->mode = CM_CONNECT;
   if (pr_inet_set_block(p, c) < 0) {
     c->mode = CM_ERROR;
@@ -1104,16 +1293,18 @@ int pr_inet_connect(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
   pr_netaddr_set_port(&remote_na, htons(port));
 
   while (TRUE) {
-    if ((res = connect(c->listen_fd, pr_netaddr_get_sockaddr(&remote_na),
-        pr_netaddr_get_sockaddr_len(&remote_na))) == -1 && errno == EINTR) {
+    res = connect(c->listen_fd, pr_netaddr_get_sockaddr(&remote_na),
+      pr_netaddr_get_sockaddr_len(&remote_na));
+    if (res < 0 &&
+        errno == EINTR) {
       pr_signals_handle();
       continue;
+    }
 
-    } else
-      break;
+    break;
   }
 
-  if (res == -1) {
+  if (res < 0) {
     c->mode = CM_ERROR;
     c->xerrno = errno;
     return -1;
@@ -1127,7 +1318,6 @@ int pr_inet_connect(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
     return -1;
   }
 
-  pr_inet_set_block(c->pool, c);
   return 1;
 }
 
@@ -1135,9 +1325,16 @@ int pr_inet_connect(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
  * 0 if not connected, or -1 if error.  Only needs to be called once, and can
  * then be selected for writing.
  */
-int pr_inet_connect_nowait(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
+int pr_inet_connect_nowait(pool *p, conn_t *c, const pr_netaddr_t *addr,
+    int port) {
   pr_netaddr_t remote_na;
 
+  if (c == NULL ||
+      addr == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   c->mode = CM_CONNECT;
   if (pr_inet_set_nonblock(p, c) < 0) {
     c->mode = CM_ERROR;
@@ -1154,11 +1351,12 @@ int pr_inet_connect_nowait(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
 
   if (connect(c->listen_fd, pr_netaddr_get_sockaddr(&remote_na),
       pr_netaddr_get_sockaddr_len(&remote_na)) == -1) {
-    if (errno != EINPROGRESS && errno != EALREADY) {
+    if (errno != EINPROGRESS &&
+        errno != EALREADY) {
       c->mode = CM_ERROR;
       c->xerrno = errno;
 
-      pr_inet_set_block(c->pool, c);
+      (void) pr_inet_set_block(c->pool, c);
 
       errno = c->xerrno;
       return -1;
@@ -1172,12 +1370,16 @@ int pr_inet_connect_nowait(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
   if (pr_inet_get_conn_info(c, c->listen_fd) < 0) {
     c->xerrno = errno;
 
-    pr_inet_set_block(c->pool, c);
+    (void) pr_inet_set_block(c->pool, c);
     errno = c->xerrno;
     return -1;
   }
 
-  pr_inet_set_block(c->pool, c);
+  if (pr_inet_set_block(c->pool, c) < 0) {
+    c->xerrno = errno;
+    return -1;
+  }
+
   return 1;
 }
 
@@ -1190,9 +1392,17 @@ int pr_inet_connect_nowait(pool *p, conn_t *c, pr_netaddr_t *addr, int port) {
 int pr_inet_accept_nowait(pool *p, conn_t *c) {
   int fd;
 
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (c->mode == CM_LISTEN) {
     if (pr_inet_set_nonblock(c->pool, c) < 0) {
-      return -1;
+      if (errno != EBADF) {
+        pr_trace_msg(trace_channel, 3,
+          "error making connection nonblocking: %s", strerror(errno));
+      }
     }
   }
 
@@ -1207,8 +1417,9 @@ int pr_inet_accept_nowait(pool *p, conn_t *c) {
     fd = accept(c->listen_fd, NULL, NULL);
 
     if (fd == -1) {
-      if (errno == EINTR)
+      if (errno == EINTR) {
         continue;
+      }
 
       if (errno != EWOULDBLOCK) {
         c->mode = CM_ERROR;
@@ -1227,7 +1438,12 @@ int pr_inet_accept_nowait(pool *p, conn_t *c) {
   /* Leave the connection in CM_ACCEPT mode, so others can see
    * our state.  Re-enable blocking mode, however.
    */
-  pr_inet_set_block(c->pool, c);
+  if (pr_inet_set_block(c->pool, c) < 0) {
+    if (errno != EBADF) {
+      pr_trace_msg(trace_channel, 3,
+        "error making connection blocking: %s", strerror(errno));
+    }
+  }
 
   return fd;
 }
@@ -1238,12 +1454,17 @@ int pr_inet_accept_nowait(pool *p, conn_t *c) {
 conn_t *pr_inet_accept(pool *p, conn_t *d, conn_t *c, int rfd, int wfd,
     unsigned char resolve) {
   conn_t *res = NULL;
-  unsigned char *allow_foreign_addr = NULL;
-  int fd = -1;
-
+  unsigned char *foreign_addr = NULL;
+  int fd = -1, allow_foreign_address = FALSE;
   pr_netaddr_t na;
   socklen_t nalen;
 
+  if (c == NULL ||
+      d == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   /* Initialize the netaddr. */
   pr_netaddr_clear(&na);
 
@@ -1252,8 +1473,10 @@ conn_t *pr_inet_accept(pool *p, conn_t *d, conn_t *c, int rfd, int wfd,
 
   d->mode = CM_ACCEPT;
 
-  allow_foreign_addr = get_param_ptr(TOPLEVEL_CONF,
-    "AllowForeignAddress", FALSE);
+  foreign_addr = get_param_ptr(TOPLEVEL_CONF, "AllowForeignAddress", FALSE);
+  if (foreign_addr != NULL) {
+    allow_foreign_address = *foreign_addr;
+  }
 
   /* A directive could enforce only IPv4 or IPv6 connections here, by
    * actually using a sockaddr argument to accept(2), and checking the
@@ -1264,31 +1487,45 @@ conn_t *pr_inet_accept(pool *p, conn_t *d, conn_t *c, int rfd, int wfd,
     pr_signals_handle();
 
     fd = accept(d->listen_fd, pr_netaddr_get_sockaddr(&na), &nalen);
-    if (fd != -1) {
-      if ((!allow_foreign_addr || *allow_foreign_addr == FALSE) &&
-          (getpeername(fd, pr_netaddr_get_sockaddr(&na), &nalen) != -1)) {
-
-        if (pr_netaddr_cmp(&na, c->remote_addr) != 0) {
-          pr_log_pri(PR_LOG_NOTICE,
-            "SECURITY VIOLATION: Passive connection from %s rejected.",
-            pr_netaddr_get_ipstr(&na));
-          close(fd);
-          continue;
-        }
+    if (fd < 0) {
+      if (errno == EINTR) {
+        continue;
       }
 
-      d->mode = CM_OPEN;
-      res = pr_inet_openrw(p, d, NULL, PR_NETIO_STRM_DATA, fd, rfd, wfd,
-        resolve);
+      d->mode = CM_ERROR;
+      d->xerrno = errno;
+      break;
+    }
 
-    } else {
-      if (errno == EINTR)
+    if (allow_foreign_address == FALSE) {
+      /* If foreign addresses (i.e. IP addresses that do not match the
+       * control connection's remote IP address) are not allowed, we
+       * need to see just what our remote address IS.
+       */
+      if (getpeername(fd, pr_netaddr_get_sockaddr(&na), &nalen) < 0) {
+        /* If getpeername(2) fails, should we still allow this connection?
+         * Caution (and the AllowForeignAddress setting say "no".
+         */
+        pr_log_pri(PR_LOG_DEBUG, "rejecting passive connection; "
+          "failed to get address of remote peer: %s", strerror(errno));
+        (void) close(fd);
         continue;
+      }
 
-      d->mode = CM_ERROR;
-      d->xerrno = errno;
+      if (pr_netaddr_cmp(&na, c->remote_addr) != 0) {
+        pr_log_pri(PR_LOG_NOTICE, "SECURITY VIOLATION: Passive connection "
+          "from foreign IP address %s rejected (does not match client "
+          "IP address %s).", pr_netaddr_get_ipstr(&na),
+          pr_netaddr_get_ipstr(c->remote_addr));
+        (void) close(fd);
+        continue;
+      }
     }
 
+    d->mode = CM_OPEN;
+    res = pr_inet_openrw(p, d, NULL, PR_NETIO_STRM_DATA, fd, rfd, wfd,
+      resolve);
+
     break;
   }
 
@@ -1299,7 +1536,11 @@ int pr_inet_get_conn_info(conn_t *c, int fd) {
   pr_netaddr_t na;
   socklen_t nalen;
 
-  /* Sanity check. */
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   if (fd < 0) {
     errno = EBADF;
     return -1;
@@ -1321,20 +1562,35 @@ int pr_inet_get_conn_info(conn_t *c, int fd) {
   nalen = pr_netaddr_get_sockaddr_len(&na);
 
   if (getsockname(fd, pr_netaddr_get_sockaddr(&na), &nalen) == 0) {
-    if (!c->local_addr)
-      c->local_addr = pr_netaddr_alloc(c->pool);
+    pr_netaddr_t *local_addr;
+
+    if (c->local_addr != NULL) {
+      local_addr = (pr_netaddr_t *) c->local_addr;
+
+    } else {
+      local_addr = pr_netaddr_alloc(c->pool);
+    }
 
     /* getsockname(2) will read the local socket information into the struct
      * sockaddr * given.  Which means that the address family of the local
      * socket can be found in struct sockaddr *->sa_family, and not (yet)
      * via pr_netaddr_get_family().
      */
-    pr_netaddr_set_family(c->local_addr,
-      pr_netaddr_get_sockaddr(&na)->sa_family);
-    pr_netaddr_set_sockaddr(c->local_addr, pr_netaddr_get_sockaddr(&na));
+    pr_netaddr_set_family(local_addr, pr_netaddr_get_sockaddr(&na)->sa_family);
+    pr_netaddr_set_sockaddr(local_addr, pr_netaddr_get_sockaddr(&na));
     c->local_port = ntohs(pr_netaddr_get_port(&na));
 
+    if (c->local_addr == NULL) {
+      c->local_addr = local_addr;
+    }
+
   } else {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "getsockname(2) error on fd %d: %s", fd, strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
@@ -1357,16 +1613,26 @@ int pr_inet_get_conn_info(conn_t *c, int fd) {
       c->remote_addr = pr_netaddr_v6tov4(c->pool, &na);
 
     } else {
-      c->remote_addr = pr_netaddr_alloc(c->pool);
+      pr_netaddr_t *remote_addr;
+
+      remote_addr = pr_netaddr_alloc(c->pool);
 
-      pr_netaddr_set_family(c->remote_addr,
+      pr_netaddr_set_family(remote_addr,
         pr_netaddr_get_sockaddr(&na)->sa_family);
-      pr_netaddr_set_sockaddr(c->remote_addr, pr_netaddr_get_sockaddr(&na));
+      pr_netaddr_set_sockaddr(remote_addr, pr_netaddr_get_sockaddr(&na));
+
+      c->remote_addr = remote_addr;
     }
 
     c->remote_port = ntohs(pr_netaddr_get_port(&na));
 
   } else {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "getpeername(2) error on fd %d: %s", fd, strerror(xerrno));
+
+    errno = xerrno;
     return -1;
   }
 
@@ -1388,12 +1654,21 @@ int pr_inet_get_conn_info(conn_t *c, int fd) {
  * Important, do not call any log_* functions from inside of pr_inet_openrw()
  * or any functions it calls, as the possibility for fd overwriting occurs.
  */
-conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
-    int fd, int rfd, int wfd, int resolve) {
+conn_t *pr_inet_openrw(pool *p, conn_t *c, const pr_netaddr_t *addr,
+    int strm_type, int fd, int rfd, int wfd, int resolve) {
   conn_t *res = NULL;
   int close_fd = TRUE;
 
   res = pr_inet_copy_conn(p, c);
+  if (res == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error copying connection: %s", strerror(xerrno));
+
+    errno = xerrno;
+    return NULL;
+  }
 
   res->listen_fd = -1;
 
@@ -1404,18 +1679,22 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
    */
   if (pr_inet_get_conn_info(res, fd) < 0 &&
       errno != EBADF) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 3,
+      "error getting info for connection on fd %d: %s", fd, strerror(xerrno));
+
+    errno = xerrno;
     return NULL;
   }
 
-  if (addr) {
-    if (!res->remote_addr) {
-      res->remote_addr = pr_netaddr_alloc(res->pool);
+  if (addr != NULL) {
+    if (res->remote_addr == NULL) {
+      res->remote_addr = pr_netaddr_dup(res->pool, addr);
     }
-
-    memcpy(res->remote_addr, addr, sizeof(pr_netaddr_t));
   }
 
-  if (resolve &&
+  if (resolve == TRUE &&
       res->remote_addr != NULL) {
     res->remote_name = pr_netaddr_get_dnsstr(res->remote_addr);
   }
@@ -1423,9 +1702,16 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
   if (res->remote_name == NULL) {
     res->remote_name = pr_netaddr_get_ipstr(res->remote_addr);
     if (res->remote_name == NULL) {
+      int xerrno = errno;
+
       /* If we can't even get the IP address as a string, then something
        * is very wrong, and we should not contine to handle this connection.
        */
+
+      pr_trace_msg(trace_channel, 3,
+        "error getting IP address for client: %s", strerror(xerrno));
+ 
+      errno = xerrno;
       return NULL;
     }
   }
@@ -1435,7 +1721,7 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
     fd = c->listen_fd;
   }
 
-  if (rfd != -1) {
+  if (rfd > -1) {
     if (fd != rfd) {
       dup2(fd, rfd);
 
@@ -1444,10 +1730,13 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
     }
 
   } else {
-    rfd = dup(fd);
+    /* dup(2) cannot take a negative value. */
+    if (fd >= 0) {
+      rfd = dup(fd);
+    }
   }
 
-  if (wfd != -1) {
+  if (wfd > -1) {
     if (fd != wfd) {
       if (wfd == STDOUT_FILENO) {
         fflush(stdout);
@@ -1460,12 +1749,15 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
     }
 
   } else {
-    wfd = dup(fd);
+    /* dup(2) cannot take a negative value. */
+    if (fd >= 0) {
+      wfd = dup(fd);
+    }
   }
 
   /* Now discard the original socket */
-  if (rfd != -1 &&
-      wfd != -1 &&
+  if (rfd > -1 &&
+      wfd > -1 &&
       close_fd) {
     (void) close(fd);
   }
@@ -1478,7 +1770,7 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
 
   /* Set options on the sockets. */
   pr_inet_set_socket_opts(res->pool, res, 0, 0, NULL);
-  pr_inet_set_block(res->pool, res);
+  (void) pr_inet_set_block(res->pool, res);
 
   res->mode = CM_OPEN;
 
@@ -1504,7 +1796,7 @@ conn_t *pr_inet_openrw(pool *p, conn_t *c, pr_netaddr_t *addr, int strm_type,
 }
 
 int pr_inet_generate_socket_event(const char *event, server_rec *s,
-    pr_netaddr_t *addr, int fd) {
+    const pr_netaddr_t *addr, int fd) {
   pool *p;
   struct socket_ctx *sc;
 
@@ -1526,7 +1818,6 @@ int pr_inet_generate_socket_event(const char *event, server_rec *s,
   return 0;
 }
 
-
 void init_inet(void) {
   struct protoent *pr = NULL;
 
diff --git a/src/json.c b/src/json.c
new file mode 100644
index 0000000..233ad84
--- /dev/null
+++ b/src/json.c
@@ -0,0 +1,844 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* JSON implementation (pool-based wrapper around CCAN JSON) */
+
+#include "json.h"
+#include "ccan-json.h"
+
+struct json_list_st {
+  pool *pool;
+  JsonNode *array;
+  unsigned int item_count;
+};
+
+struct json_obj_st {
+  pool *pool;
+  JsonNode *object;
+  unsigned int member_count;
+};
+
+static const char *trace_channel = "json";
+
+static pr_json_array_t *alloc_array(pool *p) {
+  pool *sub_pool;
+  pr_json_array_t *json;
+
+  sub_pool = make_sub_pool(p);
+  pr_pool_tag(sub_pool, "JSON Array Pool");
+
+  json = pcalloc(sub_pool, sizeof(pr_json_array_t));
+  json->pool = sub_pool;
+
+  return json;
+}
+
+static pr_json_object_t *alloc_object(pool *p) {
+  pool *sub_pool;
+  pr_json_object_t *json;
+
+  sub_pool = make_sub_pool(p);
+  pr_pool_tag(sub_pool, "JSON Object Pool");
+
+  json = pcalloc(sub_pool, sizeof(pr_json_object_t));
+  json->pool = sub_pool;
+
+  return json;
+}
+
+static unsigned int get_count(JsonNode *json) {
+  unsigned int count;
+  JsonNode *node;
+
+  for (count = 0, node = json_first_child(json);
+       node != NULL;
+       node = node->next) {
+    count++;
+  }
+
+  return count;
+}
+
+static char *get_text(pool *p, JsonNode *json, const char *ident) {
+  char *str, *text = NULL;
+
+  if (p == NULL ||
+      ident == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  str = json_stringify(json, ident);
+  if (str != NULL) {
+    text = pstrdup(p, str);
+    free(str);
+  }
+
+  return text;
+}
+
+/* JSON Objects */
+
+pr_json_object_t *pr_json_object_alloc(pool *p) {
+  pr_json_object_t *json;
+
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  json = alloc_object(p); 
+  json->object = json_mkobject();
+
+  return json;
+}
+
+int pr_json_object_free(pr_json_object_t *json) {
+  if (json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  json_delete(json->object);
+  json->object = NULL;
+
+  destroy_pool(json->pool);
+  json->pool = NULL;
+
+  return 0;
+}
+
+pr_json_object_t *pr_json_object_from_text(pool *p, const char *text) {
+  JsonNode *node;
+  pr_json_object_t *json;
+
+  if (p == NULL ||
+      text == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (json_validate(text) == FALSE) {
+    pr_trace_msg(trace_channel, 9, "unable to parse invalid JSON text '%s'",
+      text);
+    errno = EPERM;
+    return NULL;
+  }
+
+  node = json_decode(text);
+  if (node->tag != JSON_OBJECT) {
+    json_delete(node);
+
+    pr_trace_msg(trace_channel, 9, "JSON text '%s' is not a JSON object", text);
+    errno = EEXIST;
+    return NULL;
+  }
+
+  json = alloc_object(p);
+  json->object = node;
+  json->member_count = get_count(node);
+
+  return json;
+}
+
+char *pr_json_object_to_text(pool *p, const pr_json_object_t *json,
+    const char *indent) {
+  if (json == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  return get_text(p, json->object, indent);
+}
+
+int pr_json_object_count(const pr_json_object_t *json) {
+  if (json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return json->member_count;
+}
+
+int pr_json_object_remove(pr_json_object_t *json, const char *key) {
+  JsonNode *node;
+
+  if (json == NULL ||
+      key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  node = json_find_member(json->object, key);
+  if (node != NULL) {
+    /* This CCAN JSON code automatically removes the node from its parent. */
+    json_delete(node);
+
+    if (json->member_count > 0) {
+      json->member_count--;
+    }
+  }
+
+  return 0;
+}
+
+int pr_json_object_exists(const pr_json_object_t *json, const char *key) {
+  JsonNode *node;
+
+  if (json == NULL ||
+      key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  node = json_find_member(json->object, key);
+  if (node == NULL) {
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+static int can_get_member(pool *p, const pr_json_object_t *json,
+    const char *key, JsonTag tag, void *val) {
+
+  if (p == NULL ||
+      json == NULL ||
+      key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (tag != JSON_NULL &&
+      val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int can_set_member(pool *p, const pr_json_object_t *json,
+    const char *key) {
+
+  if (p == NULL ||
+      json == NULL ||
+      key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int get_val_from_node(pool *p, JsonNode *node, JsonTag tag, void *val) {
+  switch (tag) {
+    case JSON_NULL:
+      break;
+
+    case JSON_BOOL:
+      *((int *) val) = node->bool_;
+      break;
+
+    case JSON_STRING:
+      /* Fortunately, valid JSON does not allow an empty element, or
+       * a member without a value.  Thus checking for NULL string_ here
+       * would be superfluous.  The only way for that to happen is if the
+       * caller were using the CCAN JSON API directly, in which case, they
+       * get what they paid for.
+       */
+      *((char **) val) = pstrdup(p, node->string_);
+      break;
+
+    case JSON_NUMBER:
+      *((double *) val) = node->number_;
+      break; 
+
+    case JSON_ARRAY: {
+      pr_json_array_t *array;
+
+      array = alloc_array(p);
+
+      /* Make a duplicate of the child array, rather than just copying
+       * its pointer.  Otherwise, freeing this array and then freeing
+       * the parent node would cause a double free.
+       *
+       * A convenient way to get a deep copy is to encode the node
+       * as a string, then decode it again.
+       */
+      if (node->children.head != NULL) {
+        array->array = json_decode(json_encode(node->children.head));
+
+      } else {
+        array->array = json_mkarray();
+      }
+      array->item_count = get_count(array->array);
+
+      *((pr_json_array_t **) val) = array;
+      break;
+    }
+
+    case JSON_OBJECT: {
+      pr_json_object_t *object;
+
+      object = alloc_object(p);
+
+      /* Make a duplicate of the child object, rather than just copying
+       * its pointer.  Otherwise, freeing this object and then freeing
+       * the parent node would cause a double free.
+       *
+       * A convenient way to get a deep copy is to encode the node
+       * as a string, then decode it again.
+       */
+      if (node->children.head != NULL) {
+        object->object = json_decode(json_encode(node->children.head));
+
+      } else {
+        object->object = json_mkobject();
+      }
+      object->member_count = get_count(object->object);
+
+      *((pr_json_object_t **) val) = object;
+      break;
+    }
+  }
+
+  return 0;
+}
+
+static int get_member(pool *p, const pr_json_object_t *json, const char *key,
+    JsonTag tag, void *val) {
+  JsonNode *node;
+
+  node = json_find_member(json->object, key);
+  if (node == NULL) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  if (node->tag != tag) {
+    errno = EEXIST;
+    return -1;
+  }
+
+  return get_val_from_node(p, node, tag, val);
+}
+
+static JsonNode *get_node_from_val(JsonTag tag, const void *val) {
+  JsonNode *node = NULL;
+
+  switch (tag) {
+    case JSON_NULL:
+      node = json_mknull();
+      break;
+
+    case JSON_BOOL:
+      node = json_mkbool(*((int *) val));
+      break;
+
+    case JSON_NUMBER:
+      node = json_mknumber(*((double *) val));
+      break;
+
+    case JSON_STRING:
+      node = json_mkstring(val);
+      break;
+
+    case JSON_ARRAY: {
+      const pr_json_array_t *array;
+
+      array = val;
+      node = array->array;
+      break;
+    }
+
+    case JSON_OBJECT: {
+      const pr_json_object_t *object;
+
+      object = val;
+      node = object->object;
+      break;
+    }
+  }
+
+  return node;
+}
+
+static int set_member(pool *p, pr_json_object_t *json, const char *key,
+    JsonTag tag, const void *val) {
+  JsonNode *node = NULL;
+
+  node = get_node_from_val(tag, val);
+  json_append_member(json->object, key, node);
+  json->member_count++;
+
+  return 0;
+}
+
+int pr_json_object_get_bool(pool *p, const pr_json_object_t *json,
+    const char *key, int *val) {
+  if (can_get_member(p, json, key, JSON_BOOL, val) < 0) {
+    return -1;
+  }
+
+  return get_member(p, json, key, JSON_BOOL, val);
+}
+
+int pr_json_object_set_bool(pool *p, pr_json_object_t *json, const char *key,
+    int val) {
+  if (can_set_member(p, json, key) < 0) {
+    return -1;
+  }
+
+  return set_member(p, json, key, JSON_BOOL, &val);
+}
+
+int pr_json_object_get_null(pool *p, const pr_json_object_t *json,
+    const char *key) {
+  if (can_get_member(p, json, key, JSON_NULL, NULL) < 0) {
+    return -1;
+  }
+
+  return get_member(p, json, key, JSON_NULL, NULL);
+}
+
+int pr_json_object_set_null(pool *p, pr_json_object_t *json, const char *key) {
+  if (can_set_member(p, json, key) < 0) {
+    return -1;
+  }
+
+  return set_member(p, json, key, JSON_NULL, NULL);
+}
+
+int pr_json_object_get_number(pool *p, const pr_json_object_t *json,
+    const char *key, double *val) {
+  if (can_get_member(p, json, key, JSON_NUMBER, val) < 0) {
+    return -1;
+  }
+
+  return get_member(p, json, key, JSON_NUMBER, val);
+}
+
+int pr_json_object_set_number(pool *p, pr_json_object_t *json, const char *key,
+    double val) {
+  if (can_set_member(p, json, key) < 0) {
+    return -1;
+  }
+
+  return set_member(p, json, key, JSON_NUMBER, &val);
+}
+
+int pr_json_object_get_string(pool *p, const pr_json_object_t *json,
+    const char *key, char **val) {
+  if (can_get_member(p, json, key, JSON_STRING, val) < 0) {
+    return -1;
+  }
+
+  return get_member(p, json, key, JSON_STRING, val);
+}
+
+int pr_json_object_set_string(pool *p, pr_json_object_t *json, const char *key,
+    const char *val) {
+  if (can_set_member(p, json, key) < 0) {
+    return -1;
+  }
+
+  if (val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return set_member(p, json, key, JSON_STRING, val);
+}
+
+int pr_json_object_get_array(pool *p, const pr_json_object_t *json,
+    const char *key, pr_json_array_t **val) {
+  if (can_get_member(p, json, key, JSON_ARRAY, val) < 0) {
+    return -1;
+  }
+
+  return get_member(p, json, key, JSON_ARRAY, val);
+}
+
+int pr_json_object_set_array(pool *p, pr_json_object_t *json, const char *key,
+    const pr_json_array_t *val) {
+  if (can_set_member(p, json, key) < 0) {
+    return -1;
+  }
+
+  if (val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return set_member(p, json, key, JSON_ARRAY, val);
+}
+
+int pr_json_object_get_object(pool *p, const pr_json_object_t *json,
+    const char *key, pr_json_object_t **val) {
+  if (can_get_member(p, json, key, JSON_OBJECT, val) < 0) {
+    return -1;
+  }
+
+  return get_member(p, json, key, JSON_OBJECT, val);
+}
+
+int pr_json_object_set_object(pool *p, pr_json_object_t *json, const char *key,
+    const pr_json_object_t *val) {
+  if (can_set_member(p, json, key) < 0) {
+    return -1;
+  }
+
+  if (val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return set_member(p, json, key, JSON_OBJECT, val);
+}
+
+/* JSON Arrays */
+
+pr_json_array_t *pr_json_array_alloc(pool *p) {
+  pr_json_array_t *json;
+
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  json = alloc_array(p);
+  json->array = json_mkarray();
+
+  return json;
+}
+
+int pr_json_array_free(pr_json_array_t *json) {
+  if (json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  json_delete(json->array);
+  json->array = NULL;
+
+  destroy_pool(json->pool);
+  json->pool = NULL;
+
+  return 0;
+}
+
+pr_json_array_t *pr_json_array_from_text(pool *p, const char *text) {
+  JsonNode *node;
+  pr_json_array_t *json;
+
+  if (p == NULL ||
+      text == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (json_validate(text) == FALSE) {
+    pr_trace_msg(trace_channel, 9, "unable to parse invalid JSON text '%s'",
+      text);
+    errno = EPERM;
+    return NULL;
+  }
+
+  node = json_decode(text);
+  if (node->tag != JSON_ARRAY) {
+    json_delete(node);
+
+    pr_trace_msg(trace_channel, 9, "JSON text '%s' is not a JSON array", text);
+    errno = EEXIST;
+    return NULL;
+  }
+
+  json = alloc_array(p);
+  json->array = node;
+  json->item_count = get_count(node);
+
+  return json;
+}
+
+char *pr_json_array_to_text(pool *p, const pr_json_array_t *json,
+    const char *indent) {
+  if (json == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  return get_text(p, json->array, indent);
+}
+
+int pr_json_array_count(const pr_json_array_t *json) {
+  if (json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return json->item_count;
+}
+
+int pr_json_array_remove(pr_json_array_t *json, unsigned int idx) {
+  JsonNode *node;
+
+  if (json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  node = json_find_element(json->array, idx);
+  if (node != NULL) {
+    /* This CCAN JSON code automatically removes the node from its parent. */
+    json_delete(node);
+
+    if (json->item_count > 0) {
+      json->item_count--;
+    }
+  }
+
+  return 0;
+}
+
+int pr_json_array_exists(const pr_json_array_t *json, unsigned int idx) {
+  JsonNode *node;
+
+  if (json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  node = json_find_element(json->array, idx);
+  if (node == NULL) {
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+static int can_get_item(pool *p, const pr_json_array_t *json, JsonTag tag,
+    void *val) {
+
+  if (p == NULL ||
+      json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (tag != JSON_NULL &&
+      val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int can_add_item(pool *p, const pr_json_array_t *json) {
+
+  if (p == NULL ||
+      json == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return 0;
+}
+
+static int get_item(pool *p, const pr_json_array_t *json, unsigned int idx,
+    JsonTag tag, void *val) {
+  JsonNode *node;
+
+  node = json_find_element(json->array, idx);
+  if (node == NULL) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  if (node->tag != tag) {
+    errno = EEXIST;
+    return -1;
+  }
+
+  return get_val_from_node(p, node, tag, val);
+}
+
+static int append_item(pool *p, pr_json_array_t *json, JsonTag tag,
+    const void *val) {
+  JsonNode *node = NULL;
+
+  node = get_node_from_val(tag, val);
+  json_append_element(json->array, node);
+  json->item_count++;
+
+  return 0;
+}
+
+int pr_json_array_append_bool(pool *p, pr_json_array_t *json, int val) {
+  if (can_add_item(p, json) < 0) {
+    return -1;
+  }
+
+  return append_item(p, json, JSON_BOOL, &val);
+}
+
+int pr_json_array_get_bool(pool *p, const pr_json_array_t *json,
+    unsigned int idx, int *val) {
+  if (can_get_item(p, json, JSON_BOOL, val) < 0) {
+    return -1;
+  }
+
+  return get_item(p, json, idx, JSON_BOOL, val);
+}
+
+int pr_json_array_append_null(pool *p, pr_json_array_t *json) {
+  if (can_add_item(p, json) < 0) {
+    return -1;
+  }
+
+  return append_item(p, json, JSON_NULL, NULL);
+}
+
+int pr_json_array_get_null(pool *p, const pr_json_array_t *json,
+    unsigned int idx) {
+  if (can_get_item(p, json, JSON_NULL, NULL) < 0) {
+    return -1;
+  }
+
+  return get_item(p, json, idx, JSON_NULL, NULL);
+}
+
+int pr_json_array_append_number(pool *p, pr_json_array_t *json, double val) {
+  if (can_add_item(p, json) < 0) {
+    return -1;
+  }
+
+  return append_item(p, json, JSON_NUMBER, &val);
+}
+
+int pr_json_array_get_number(pool *p, const pr_json_array_t *json,
+    unsigned int idx, double *val) {
+  if (can_get_item(p, json, JSON_NUMBER, val) < 0) {
+    return -1;
+  }
+
+  return get_item(p, json, idx, JSON_NUMBER, val);
+}
+
+int pr_json_array_append_string(pool *p, pr_json_array_t *json,
+    const char *val) {
+  if (can_add_item(p, json) < 0) {
+    return -1;
+  }
+
+  if (val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return append_item(p, json, JSON_STRING, val);
+}
+
+int pr_json_array_get_string(pool *p, const pr_json_array_t *json,
+    unsigned int idx, char **val) {
+  if (can_get_item(p, json, JSON_STRING, val) < 0) {
+    return -1;
+  }
+
+  return get_item(p, json, idx, JSON_STRING, val);
+}
+
+int pr_json_array_append_array(pool *p, pr_json_array_t *json,
+    const pr_json_array_t *val) {
+  if (can_add_item(p, json) < 0) {
+    return -1;
+  }
+
+  if (val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return append_item(p, json, JSON_ARRAY, val);
+}
+
+int pr_json_array_get_array(pool *p, const pr_json_array_t *json,
+    unsigned int idx, pr_json_array_t **val) {
+  if (can_get_item(p, json, JSON_ARRAY, val) < 0) {
+    return -1;
+  }
+
+  return get_item(p, json, idx, JSON_ARRAY, val);
+}
+
+int pr_json_array_append_object(pool *p, pr_json_array_t *json,
+    const pr_json_object_t *val) {
+  if (can_add_item(p, json) < 0) {
+    return -1;
+  }
+
+  if (val == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return append_item(p, json, JSON_OBJECT, val);
+}
+
+int pr_json_array_get_object(pool *p, const pr_json_array_t *json,
+    unsigned int idx, pr_json_object_t **val) {
+  if (can_get_item(p, json, JSON_OBJECT, val) < 0) {
+    return -1;
+  }
+
+  return get_item(p, json, idx, JSON_OBJECT, val);
+}
+
+int pr_json_text_validate(pool *p, const char *text) {
+  if (p == NULL ||
+      text == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  return json_validate(text);
+}
+
+static void json_oom(void) {
+  pr_log_pri(PR_LOG_ALERT, "%s", "Out of memory!");
+  exit(1);
+}
+
+
+int init_json(void) {
+  json_set_oom(json_oom);
+  return 0;
+}
+
+int finish_json(void) {
+  json_set_oom(NULL);
+  return 0;
+}
+
diff --git a/src/lastlog.c b/src/lastlog.c
index 3d2fd96..05735cb 100644
--- a/src/lastlog.c
+++ b/src/lastlog.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006-2013 The ProFTPD Project team
+ * Copyright (c) 2006-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,14 @@
  * OpenSSL in the source distribution.
  */
 
-/* Lastlog code
- * $Id: lastlog.c,v 1.5 2013-10-13 18:06:57 castaglia Exp $
- */
+/* Lastlog code */
 
 #include "conf.h"
 
 #ifdef PR_USE_LASTLOG
 
 int log_lastlog(uid_t uid, const char *user_name, const char *tty,
-    pr_netaddr_t *remote_addr) {
+    const pr_netaddr_t *remote_addr) {
   struct lastlog ll;
   struct stat st;
   int fd, res;
diff --git a/src/log.c b/src/log.c
index a9700df..b988905 100644
--- a/src/log.c
+++ b/src/log.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2014 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,25 +24,29 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD logging support.
- * $Id: log.c,v 1.121 2013-10-13 23:46:43 castaglia Exp $
- */
+/* ProFTPD logging support. */
 
 #include "conf.h"
 
-/* Max path length plus 64 bytes for additional info. */
-#define LOGBUFFER_SIZE		(PR_TUNABLE_PATH_MAX + 64)
+#ifdef HAVE_EXECINFO_H
+# include <execinfo.h>
+#endif
+
+#define LOGBUFFER_SIZE		(PR_TUNABLE_PATH_MAX * 4)
 
 static int syslog_open = FALSE;
 static int syslog_discard = FALSE;
 static int logstderr = TRUE;
-static int debug_level = DEBUG0;	/* Default is no debug logging */
+static int debug_level = DEBUG0;
+static int default_level = PR_LOG_NOTICE;
 static int facility = LOG_DAEMON;
 static int set_facility = -1;
 static char systemlog_fn[PR_TUNABLE_PATH_MAX] = {'\0'};
 static char systemlog_host[256] = {'\0'};
 static int systemlog_fd = -1;
 
+static const char *trace_channel = "log";
+
 int syslog_sockfd = -1;
 
 #ifdef PR_USE_NONBLOCKING_LOG_OPEN
@@ -59,12 +63,13 @@ static int fd_set_block(int fd) {
 int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
   int res;
   pool *tmp_pool = NULL;
-  char *tmp = NULL, *lf;
+  char *ptr = NULL, *lf;
   unsigned char have_stat = FALSE, *allow_log_symlinks = NULL;
   struct stat st;
 
   /* Sanity check */
-  if (!log_file || !log_fd) {
+  if (log_file == NULL ||
+      log_fd == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -74,8 +79,8 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
   pr_pool_tag(tmp_pool, "log_openfile() tmp pool");
   lf = pstrdup(tmp_pool, log_file);
 
-  tmp = strrchr(lf, '/');
-  if (tmp == NULL) {
+  ptr = strrchr(lf, '/');
+  if (ptr == NULL) {
     pr_log_debug(DEBUG0, "inappropriate log file: %s", lf);
     destroy_pool(tmp_pool);
 
@@ -86,7 +91,9 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
   /* Set the path separator to zero, in order to obtain the directory
    * name, so that checks of the directory may be made.
    */
-  *tmp = '\0';
+  if (ptr != lf) {
+    *ptr = '\0';
+  }
 
   if (stat(lf, &st) < 0) {
     int xerrno = errno;
@@ -117,7 +124,9 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
   /* Restore the path separator so that checks on the file itself may be
    * done.
    */
-  *tmp = '/';
+  if (ptr != lf) {
+    *ptr = '/';
+  }
 
   allow_log_symlinks = get_param_ptr(main_server->conf, "AllowLogSymlinks",
     FALSE);
@@ -214,8 +223,9 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
 
     /* Stat the file using the descriptor, not the path */
     if (!have_stat &&
-        fstat(*log_fd, &st) != -1)
+        fstat(*log_fd, &st) != -1) {
       have_stat = TRUE;
+    }
 
     if (!have_stat ||
         S_ISLNK(st.st_mode)) {
@@ -241,11 +251,44 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
 
     *log_fd = open(lf, flags, log_mode);
     if (*log_fd < 0) {
+      int xerrno = errno;
+
       destroy_pool(tmp_pool);
+      errno = xerrno;
       return -1;
     }
   }
 
+  /* Make sure we're dealing with an expected file type (i.e. NOT a
+   * directory).
+   */
+  if (fstat(*log_fd, &st) < 0) {
+    int xerrno = errno;
+
+    pr_log_debug(DEBUG0, "error: unable to stat %s (fd %d): %s", lf, *log_fd,
+      strerror(xerrno));
+
+    close(*log_fd);
+    *log_fd = -1;
+    destroy_pool(tmp_pool);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (S_ISDIR(st.st_mode)) {
+    int xerrno = EISDIR;
+
+    pr_log_debug(DEBUG0, "error: unable to use %s: %s", lf, strerror(xerrno));
+
+    close(*log_fd);
+    *log_fd = -1;
+    destroy_pool(tmp_pool);
+
+    errno = xerrno;
+    return -1;
+  }
+
   /* Find a usable fd for the just-opened log fd. */
   if (*log_fd <= STDERR_FILENO) {
     res = pr_fs_get_usable_fd(*log_fd);
@@ -264,9 +307,14 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
       *log_fd, strerror(errno));
   }
 
+  /* Advise the platform that we will be treating this log file as
+   * write-only data.
+   */
+  pr_fs_fadvise(*log_fd, 0, 0, PR_FS_FADVISE_DONTNEED);
+
 #ifdef PR_USE_NONBLOCKING_LOG_OPEN
   /* Return the fd to blocking mode. */
-  fd_set_block(*log_fd);
+  (void) fd_set_block(*log_fd);
 #endif /* PR_USE_NONBLOCKING_LOG_OPEN */
 
   destroy_pool(tmp_pool);
@@ -275,7 +323,7 @@ int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
 
 int pr_log_vwritefile(int logfd, const char *ident, const char *fmt,
     va_list msg) {
-  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
+  char buf[LOGBUFFER_SIZE] = {'\0'};
   struct timeval now;
   struct tm *tm = NULL;
   size_t buflen, len;
@@ -318,8 +366,11 @@ int pr_log_vwritefile(int logfd, const char *ident, const char *fmt,
     buf[buflen++] = '\n';
 
   } else {
+    buf[sizeof(buf)-5] = '.';
+    buf[sizeof(buf)-4] = '.';
+    buf[sizeof(buf)-3] = '.';
     buf[sizeof(buf)-2] = '\n';
-    buflen++;
+    buflen = sizeof(buf)-1;
   }
 
   pr_log_event_generate(PR_LOG_TYPE_UNSPEC, logfd, -1, buf, buflen);
@@ -369,8 +420,14 @@ int log_opensyslog(const char *fn) {
     pr_closelog(syslog_sockfd);
 
     syslog_sockfd = pr_openlog("proftpd", LOG_NDELAY|LOG_PID, facility);
-    if (syslog_sockfd < 0)
+    if (syslog_sockfd < 0) {
+      int xerrno = errno;
+
+      (void) pr_trace_msg(trace_channel, 1,
+        "error opening syslog fd: %s", strerror(xerrno));
+      errno = xerrno;
       return -1;
+    }
 
     /* Find a usable fd for the just-opened socket fd. */
     if (syslog_sockfd <= STDERR_FILENO) {
@@ -381,7 +438,7 @@ int log_opensyslog(const char *fn) {
       }
     }
 
-    fcntl(syslog_sockfd, F_SETFD, FD_CLOEXEC);
+    (void) fcntl(syslog_sockfd, F_SETFD, FD_CLOEXEC);
     systemlog_fd = -1;
 
   } else if ((res = pr_log_openfile(systemlog_fn, &systemlog_fd,
@@ -417,20 +474,24 @@ void log_discard(void) {
 }
 
 static void log_write(int priority, int f, char *s, int discard) {
-  unsigned int *max_priority = NULL;
+  int max_priority = 0, *ptr = NULL;
   char serverinfo[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
 
   memset(serverinfo, '\0', sizeof(serverinfo));
 
   if (main_server &&
       main_server->ServerFQDN) {
-    pr_netaddr_t *remote_addr = pr_netaddr_get_sess_remote_addr();
-    const char *remote_name = pr_netaddr_get_sess_remote_name();
+    const pr_netaddr_t *remote_addr;
+    const char *remote_name;
+
+    remote_addr = pr_netaddr_get_sess_remote_addr();
+    remote_name = pr_netaddr_get_sess_remote_name();
 
     snprintf(serverinfo, sizeof(serverinfo)-1, "%s", main_server->ServerFQDN);
     serverinfo[sizeof(serverinfo)-1] = '\0';
 
-    if (remote_addr && remote_name) {
+    if (remote_addr != NULL &&
+        remote_name != NULL) {
       size_t serverinfo_len;
 
       serverinfo_len = strlen(serverinfo);
@@ -486,6 +547,7 @@ static void log_write(int priority, int f, char *s, int discard) {
       buf, buflen);
 
     fprintf(stderr, "%s", buf);
+    fflush(stderr);
     return;
   }
 
@@ -497,10 +559,26 @@ static void log_write(int priority, int f, char *s, int discard) {
     }
   }
 
-  max_priority = get_param_ptr(main_server->conf, "SyslogLevel", FALSE);
-  if (max_priority != NULL &&
-      priority > *max_priority) {
+  if (main_server != NULL) {
+    ptr = get_param_ptr(main_server->conf, "SyslogLevel", FALSE);
+  }
+
+  if (ptr != NULL) {
+    max_priority = *ptr;
+
+  } else {
+    /* Default SyslogLevel is NOTICE.  Note, however, that for backward
+     * compatibility of debugging, if the DebugLevel is set higher
+     * than DEBUG0, we will automatically ASSUME that the admin wants
+     * the syslog level to be e.g. DEBUG.
+     */
+    max_priority = default_level;
+    if (debug_level != DEBUG0) {
+      max_priority = PR_LOG_DEBUG;
+    }
+  }
 
+  if (priority > max_priority) {
     /* Only return now if we don't have any log listeners. */
     if (pr_log_event_listening(PR_LOG_TYPE_SYSLOG) <= 0 &&
         pr_log_event_listening(PR_LOG_TYPE_SYSTEMLOG) <= 0) {
@@ -560,8 +638,7 @@ static void log_write(int priority, int f, char *s, int discard) {
       return;
     }
 
-    if (max_priority != NULL &&
-        priority > *max_priority) {
+    if (priority > max_priority) {
       return;
     }
 
@@ -571,7 +648,7 @@ static void log_write(int priority, int f, char *s, int discard) {
         continue;
       }
 
-      return;
+      break;
     }
 
     return;
@@ -580,11 +657,18 @@ static void log_write(int priority, int f, char *s, int discard) {
   pr_log_event_generate(PR_LOG_TYPE_SYSLOG, syslog_sockfd, priority, s,
     strlen(s));
 
-  if (set_facility != -1)
+  if (set_facility != -1) {
     f = set_facility;
+  }
 
   if (!syslog_open) {
     syslog_sockfd = pr_openlog("proftpd", LOG_NDELAY|LOG_PID, f);
+    if (syslog_sockfd < 0) {
+      (void) pr_trace_msg(trace_channel, 1,
+        "error opening syslog fd: %s", strerror(errno));
+      return;
+    }
+
     syslog_open = TRUE;
 
   } else if (f != facility) {
@@ -643,7 +727,7 @@ void log_stderr(int bool) {
   logstderr = bool;
 }
 
-/* Set the debug logging level, see log.h for constants.  Higher
+/* Set the debug logging level; see log.h for constants.  Higher
  * numbers mean print more, DEBUG0 (0) == print no debugging log
  * (default)
  */
@@ -653,6 +737,13 @@ int pr_log_setdebuglevel(int level) {
   return old_level;
 }
 
+/* Set the default logging level; see log.h for constants. */
+int pr_log_setdefaultlevel(int level) {
+  int old_level = default_level;
+  default_level = level;
+  return old_level;
+}
+
 /* Convert a string into the matching syslog level value.  Return -1
  * if no matching level is found.
  */
@@ -797,12 +888,83 @@ int pr_log_event_listening(unsigned int log_type) {
   return TRUE;
 }
 
+void pr_log_stacktrace(int log_fd, const char *name) {
+#if defined(HAVE_EXECINFO_H) && \
+    defined(HAVE_BACKTRACE) && \
+    defined(HAVE_BACKTRACE_SYMBOLS)
+  void *trace[PR_TUNABLE_CALLER_DEPTH];
+  int tracesz, use_fd = TRUE;
+
+  if (log_fd < 0 ||
+      name == NULL) {
+    use_fd = FALSE;
+  }
+
+  if (use_fd) {
+    (void) pr_log_writefile(log_fd, name, "%s", "-----BEGIN STACK TRACE-----");
+
+  } else {
+    (void) pr_log_pri(PR_LOG_WARNING, "-----BEGIN STACK TRACE-----");
+  }
+
+  tracesz = backtrace(trace, PR_TUNABLE_CALLER_DEPTH);
+  if (tracesz < 0) {
+    if (use_fd) {
+      (void) pr_log_writefile(log_fd, name, "backtrace(3) error: %s",
+        strerror(errno));
+
+    } else {
+      (void) pr_log_pri(PR_LOG_WARNING, "backtrace(3) error: %s",
+        strerror(errno));
+    }
+
+  } else {
+    char **strings;
+
+    strings = backtrace_symbols(trace, tracesz);
+    if (strings != NULL) {
+      register int i;
+
+      for (i = 1; i < tracesz; i++) {
+        if (use_fd) {
+          (void) pr_log_writefile(log_fd, name, "[%d] %s", i-1, strings[i]);
+
+        } else {
+          (void) pr_log_pri(PR_LOG_WARNING, "[%d] %s", i-1, strings[i]);
+        }
+      }
+
+      /* Prevent memory leaks. */
+      free(strings);
+
+    } else {
+      if (use_fd) {
+        (void) pr_log_writefile(log_fd, name,
+          "error obtaining backtrace symbols: %s", strerror(errno));
+
+      } else {
+        (void) pr_log_pri(PR_LOG_WARNING,
+          "error obtaining backtrace symbols: %s", strerror(errno));
+      }
+    }
+  }
+
+  if (use_fd) {
+    (void) pr_log_writefile(log_fd, name, "%s", "-----END STACK TRACE-----");
+
+  } else {
+    (void) pr_log_pri(PR_LOG_WARNING, "%s", "-----END STACK TRACE-----");
+  }
+#endif
+}
+
 void init_log(void) {
   char buf[256];
 
   memset(buf, '\0', sizeof(buf));
-  if (gethostname(buf, sizeof(buf)) == -1)
+  if (gethostname(buf, sizeof(buf)) < 0) {
     sstrncpy(buf, "localhost", sizeof(buf));
+  }
 
   sstrncpy(systemlog_host, (char *) pr_netaddr_validate_dns_str(buf),
     sizeof(systemlog_host));
diff --git a/src/main.c b/src/main.c
index 98f7526..1ead27f 100644
--- a/src/main.c
+++ b/src/main.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -36,18 +36,10 @@
 # include <libutil.h>
 #endif /* HAVE_LIBUTIL_H */
 
-#ifdef HAVE_EXECINFO_H
-# include <execinfo.h>
-#endif
-
 #ifdef HAVE_UNAME
 # include <sys/utsname.h>
 #endif
 
-#ifdef HAVE_UCONTEXT_H
-# include <ucontext.h>
-#endif
-
 #include "privs.h"
 
 int (*cmd_auth_chk)(cmd_rec *);
@@ -56,6 +48,7 @@ void (*cmd_handler)(server_rec *, conn_t *);
 /* From modules/module_glue.c */
 extern module *static_modules[];
 
+extern int have_dead_child;
 extern xaset_t *server_list;
 
 unsigned long max_connects = 0UL;
@@ -75,8 +68,6 @@ array_header *daemon_gids;
 static time_t shut = 0, deny = 0, disc = 0;
 static char shutmsg[81] = {'\0'};
 
-static unsigned char have_dead_child = FALSE;
-
 /* The default command buffer size SHOULD be large enough to handle the
  * maximum path length, plus 4 bytes for the FTP command, plus 1 for the
  * whitespace separating command from path, and 2 for the terminating CRLF.
@@ -86,50 +77,28 @@ static unsigned char have_dead_child = FALSE;
 /* From response.c */
 extern pr_response_t *resp_list, *resp_err_list;
 
-static int nodaemon  = 0;
-static int quiet     = 0;
-static int shutdownp = 0;
+int nodaemon = 0;
+
+static int no_forking = FALSE;
+static int quiet = 0;
+static int shutting_down = 0;
 static int syntax_check = 0;
 
 /* Command handling */
-static void cmd_loop(server_rec *, conn_t *);
-
-/* Signal handling */
-static RETSIGTYPE sig_disconnect(int);
-static RETSIGTYPE sig_evnt(int);
-static RETSIGTYPE sig_terminate(int);
-#ifdef PR_DEVEL_STACK_TRACE
-static void install_stacktrace_handler(void);
-#endif /* PR_DEVEL_STACK_TRACE */
-
-volatile unsigned int recvd_signal_flags = 0;
-
-/* Used to capture an "unknown" signal value that causes termination. */
-static int term_signo = 0;
-
-/* Signal processing functions */
-static void handle_abort(void);
-static void handle_chld(void);
-static void handle_evnt(void);
-#ifdef PR_DEVEL_STACK_TRACE
-static void handle_stacktrace_signal(int, siginfo_t *, void *);
-#endif /* PR_DEVEL_STACK_TRACE */
-static void handle_xcpu(void);
-static void handle_terminate(void);
-static void handle_terminate_other(void);
-static void finish_terminate(void);
-
-static cmd_rec *make_ftp_cmd(pool *, char *, int);
+static void cmd_loop(server_rec *s, conn_t *conn);
+
+static cmd_rec *make_ftp_cmd(pool *p, char *buf, size_t buflen, int flags);
 
 static const char *config_filename = PR_CONFIG_FILE_PATH;
 
 /* Add child semaphore fds into the rfd for selecting */
 static int semaphore_fds(fd_set *rfd, int maxfd) {
-
   if (child_count()) {
     pr_child_t *ch;
 
     for (ch = child_get(NULL); ch; ch = child_get(ch)) {
+      pr_signals_handle();
+
       if (ch->ch_pipefd != -1) {
         FD_SET(ch->ch_pipefd, rfd);
         if (ch->ch_pipefd > maxfd) {
@@ -175,12 +144,12 @@ void session_exit(int pri, void *lv, int exitval, void *dummy) {
   pr_session_end(0);
 }
 
-static void shutdown_exit(void *d1, void *d2, void *d3, void *d4) {
-  if (check_shutmsg(&shut, &deny, &disc, shutmsg, sizeof(shutmsg)) == 1) {
-    char *user;
+void shutdown_end_session(void *d1, void *d2, void *d3, void *d4) {
+  if (check_shutmsg(PR_SHUTMSG_PATH, &shut, &deny, &disc, shutmsg,
+      sizeof(shutmsg)) == 1) {
+    const char *user;
     time_t now;
-    char *msg;
-    const char *serveraddress;
+    const char *msg, *serveraddress;
     config_rec *c = NULL;
     unsigned char *authenticated = get_param_ptr(main_server->conf,
       "authenticated", FALSE);
@@ -189,11 +158,17 @@ static void shutdown_exit(void *d1, void *d2, void *d3, void *d4) {
       pr_netaddr_get_ipstr(session.c->local_addr) :
       main_server->ServerAddress;
 
-    if ((c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress",
-        FALSE)) != NULL) {
+    c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
+    if (c != NULL) {
+      pr_netaddr_t *masq_addr = NULL;
 
-      pr_netaddr_t *masq_addr = (pr_netaddr_t *) c->argv[0];
-      serveraddress = pr_netaddr_get_ipstr(masq_addr);
+      if (c->argv[0] != NULL) {
+        masq_addr = c->argv[0];
+      }
+
+      if (masq_addr != NULL) {
+        serveraddress = pr_netaddr_get_ipstr(masq_addr);
+      }
     }
 
     time(&now);
@@ -223,7 +198,7 @@ static void shutdown_exit(void *d1, void *d2, void *d3, void *d4) {
     pr_session_disconnect(NULL, PR_SESS_DISCONNECT_SERVER_SHUTDOWN, NULL);
   }
 
-  if (signal(SIGUSR1, sig_disconnect) == SIG_ERR) {
+  if (signal(SIGUSR1, pr_signals_handle_disconnect) == SIG_ERR) {
     pr_log_pri(PR_LOG_NOTICE,
       "unable to install SIGUSR1 (signal %d) handler: %s", SIGUSR1,
       strerror(errno));
@@ -232,11 +207,13 @@ static void shutdown_exit(void *d1, void *d2, void *d3, void *d4) {
 
 static int get_command_class(const char *name) {
   int idx = -1;
-  cmdtable *c = pr_stash_get_symbol(PR_SYM_CMD, name, NULL, &idx);
+  unsigned int hash = 0;
+  cmdtable *c;
 
+  c = pr_stash_get_symbol2(PR_SYM_CMD, name, NULL, &idx, &hash);
   while (c && c->cmd_type != CMD) {
     pr_signals_handle();
-    c = pr_stash_get_symbol(PR_SYM_CMD, name, c, &idx);
+    c = pr_stash_get_symbol2(PR_SYM_CMD, name, c, &idx, &hash);
   }
 
   /* By default, every command has a class of CL_ALL.  This insures that
@@ -246,14 +223,16 @@ static int get_command_class(const char *name) {
 }
 
 static int _dispatch(cmd_rec *cmd, int cmd_type, int validate, char *match) {
-  char *cmdargstr = NULL;
+  const char *cmdargstr = NULL;
   cmdtable *c;
   modret_t *mr;
   int success = 0, xerrno = 0;
   int send_error = 0;
   static int match_index_cache = -1;
+  static unsigned int match_hash_cache = 0;
   static char *last_match = NULL;
-  int *index_cache;
+  int *index_cache = NULL;
+  unsigned int *hash_cache = NULL;
 
   send_error = (cmd_type == PRE_CMD || cmd_type == CMD ||
     cmd_type == POST_CMD_ERR);
@@ -261,6 +240,7 @@ static int _dispatch(cmd_rec *cmd, int cmd_type, int validate, char *match) {
   if (!match) {
     match = cmd->argv[0];
     index_cache = &cmd->stash_index;
+    hash_cache = &cmd->stash_hash;
 
   } else {
     if (last_match != match) {
@@ -269,9 +249,10 @@ static int _dispatch(cmd_rec *cmd, int cmd_type, int validate, char *match) {
     }
 
     index_cache = &match_index_cache;
+    hash_cache = &match_hash_cache;
   }
 
-  c = pr_stash_get_symbol(PR_SYM_CMD, match, NULL, index_cache);
+  c = pr_stash_get_symbol2(PR_SYM_CMD, match, NULL, index_cache, hash_cache);
 
   while (c && !success) {
     size_t cmdargstrlen = 0;
@@ -284,15 +265,16 @@ static int _dispatch(cmd_rec *cmd, int cmd_type, int validate, char *match) {
     session.curr_phase = cmd_type;
 
     if (c->cmd_type == cmd_type) {
-      if (c->group)
+      if (c->group) {
         cmd->group = pstrdup(cmd->pool, c->group);
+      }
 
       if (c->requires_auth &&
           cmd_auth_chk &&
           !cmd_auth_chk(cmd)) {
         pr_trace_msg("command", 8,
           "command '%s' failed 'requires_auth' check for mod_%s.c",
-          cmd->argv[0], c->m->name);
+          (char *) cmd->argv[0], c->m->name);
         errno = EACCES;
         return -1;
       }
@@ -420,7 +402,7 @@ static int _dispatch(cmd_rec *cmd, int cmd_type, int validate, char *match) {
     }
 
     if (!success) {
-      c = pr_stash_get_symbol(PR_SYM_CMD, match, c, index_cache);
+      c = pr_stash_get_symbol2(PR_SYM_CMD, match, c, index_cache, hash_cache);
     }
   }
 
@@ -479,8 +461,8 @@ static size_t get_max_cmd_sz(void) {
 int pr_cmd_read(cmd_rec **res) {
   static long cmd_bufsz = -1;
   static char *cmd_buf = NULL;
-  char *cp;
-  size_t cmd_buflen;
+  int cmd_buflen;
+  char *ptr;
 
   if (res == NULL) {
     errno = EINVAL;
@@ -500,9 +482,9 @@ int pr_cmd_read(cmd_rec **res) {
 
     memset(cmd_buf, '\0', cmd_bufsz);
 
-    if (pr_netio_telnet_gets(cmd_buf, cmd_bufsz, session.c->instrm,
-        session.c->outstrm) == NULL) {
-
+    cmd_buflen = pr_netio_telnet_gets2(cmd_buf, cmd_bufsz, session.c->instrm,
+      session.c->outstrm);
+    if (cmd_buflen < 0) {
       if (errno == E2BIG) {
         /* The client sent a too-long command which was ignored; give
          * them another chance?
@@ -521,39 +503,34 @@ int pr_cmd_read(cmd_rec **res) {
     break;
   }
 
-  /* This strlen(3) is guaranteed to terminate; the last byte of buf is
-   * always NUL, since pr_netio_telnet_gets() is told that the buf size is
-   * one byte less than it really is.
-   *
-   * If the strlen(3) says that the length is less than the cmd_bufsz, then
-   * there is no need to truncate the buffer by inserting a NUL.
+  /* If the read length is less than the cmd_bufsz, then there is no need to
+   * truncate the buffer by inserting a NUL.
    */
-  cmd_buflen = strlen(cmd_buf);
   if (cmd_buflen > cmd_bufsz) {
-    pr_log_debug(DEBUG0, "truncating incoming command length (%lu bytes) to "
+    pr_log_debug(DEBUG0, "truncating incoming command length (%d bytes) to "
       "CommandBufferSize %lu; use the CommandBufferSize directive to increase "
-      "the allowed command length", (unsigned long) cmd_buflen,
-      (unsigned long) cmd_bufsz);
+      "the allowed command length", cmd_buflen, (unsigned long) cmd_bufsz);
     cmd_buf[cmd_bufsz-1] = '\0';
   }
 
-  if (cmd_buflen &&
+  if (cmd_buflen > 0 &&
       (cmd_buf[cmd_buflen-1] == '\n' || cmd_buf[cmd_buflen-1] == '\r')) {
     cmd_buf[cmd_buflen-1] = '\0';
     cmd_buflen--;
 
-    if (cmd_buflen &&
+    if (cmd_buflen > 0 &&
         (cmd_buf[cmd_buflen-1] == '\n' || cmd_buf[cmd_buflen-1] =='\r')) {
       cmd_buf[cmd_buflen-1] = '\0';
       cmd_buflen--;
     }
   }
 
-  cp = cmd_buf;
-  if (*cp == '\r')
-    cp++;
+  ptr = cmd_buf;
+  if (*ptr == '\r') {
+    ptr++;
+  }
 
-  if (*cp) {
+  if (*ptr) {
     int flags = 0;
     cmd_rec *cmd;
 
@@ -563,18 +540,22 @@ int pr_cmd_read(cmd_rec **res) {
      * command handlers themselves, via cmd->arg.  This small hack
      * reduces the burden on SITE module developers, however.
      */
-    if (strncasecmp(cp, C_SITE, 4) == 0) {
+    if (strncasecmp(ptr, C_SITE, 4) == 0) {
       flags |= PR_STR_FL_PRESERVE_WHITESPACE;
     }
 
-    cmd = make_ftp_cmd(session.pool, cp, flags);
-    if (cmd) {
+    cmd = make_ftp_cmd(session.pool, ptr, cmd_buflen, flags);
+    if (cmd != NULL) {
       *res = cmd;
 
       if (pr_cmd_is_http(cmd) == TRUE) {
         cmd->is_ftp = FALSE;
         cmd->protocol = "HTTP";
 
+      } else if (pr_cmd_is_ssh2(cmd) == TRUE) {
+        cmd->is_ftp = FALSE;
+        cmd->protocol = "SSH2";
+
       } else if (pr_cmd_is_smtp(cmd) == TRUE) {
         cmd->is_ftp = FALSE;
         cmd->protocol = "SMTP";
@@ -590,6 +571,29 @@ int pr_cmd_read(cmd_rec **res) {
   return 0;
 }
 
+static int set_cmd_start_ms(cmd_rec *cmd) {
+  void *v;
+  uint64_t start_ms;
+
+  if (cmd->notes == NULL) {
+    return 0;
+  }
+
+  v = (void *) pr_table_get(cmd->notes, "start_ms", NULL);
+  if (v != NULL) {
+    return 0;
+  }
+
+  if (pr_gettimeofday_millis(&start_ms) < 0) {
+    return -1;
+  }
+
+  v = palloc(cmd->pool, sizeof(uint64_t));
+  memcpy(v, &start_ms, sizeof(uint64_t));
+
+  return pr_table_add(cmd->notes, "start_ms", v, sizeof(uint64_t));
+}
+
 int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
   char *cp = NULL;
   int success = 0, xerrno = 0;
@@ -604,7 +608,8 @@ int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
 
   if (flags & PR_CMD_DISPATCH_FL_CLEAR_RESPONSE) {
     pr_trace_msg("response", 9,
-      "clearing response lists before dispatching command '%s'", cmd->argv[0]);
+      "clearing response lists before dispatching command '%s'",
+      (char *) cmd->argv[0]);
     pr_response_clear(&resp_list);
     pr_response_clear(&resp_err_list);
   }
@@ -634,8 +639,9 @@ int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
     cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
   }
 
+  set_cmd_start_ms(cmd);
+
   if (phase == 0) {
-        
     /* First, dispatch to wildcard PRE_CMD handlers. */
     success = _dispatch(cmd, PRE_CMD, FALSE, C_ANY);
 
@@ -653,7 +659,7 @@ int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
 
       xerrno = errno;
       pr_trace_msg("response", 9, "flushing error response list for '%s'",
-        cmd->argv[0]);
+        (char *) cmd->argv[0]);
       pr_response_flush(&resp_err_list);
 
       /* Restore any previous pool to the Response API. */
@@ -677,13 +683,12 @@ int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
 
       xerrno = errno;
       pr_trace_msg("response", 9, "flushing response list for '%s'",
-        cmd->argv[0]);
+        (char *) cmd->argv[0]);
       pr_response_flush(&resp_list);
 
       errno = xerrno;
 
     } else if (success < 0) {
-
       /* Allow for non-logging command handlers to be run if CMD fails. */
 
       success = _dispatch(cmd, POST_CMD_ERR, FALSE, C_ANY);
@@ -695,7 +700,7 @@ int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
 
       xerrno = errno;
       pr_trace_msg("response", 9, "flushing error response list for '%s'",
-        cmd->argv[0]);
+        (char *) cmd->argv[0]);
       pr_response_flush(&resp_err_list);
 
       errno = xerrno;
@@ -738,12 +743,12 @@ int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
 
       if (success == 1) {
         pr_trace_msg("response", 9, "flushing response list for '%s'",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         pr_response_flush(&resp_list);
 
       } else if (success < 0) {
         pr_trace_msg("response", 9, "flushing error response list for '%s'",
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         pr_response_flush(&resp_err_list);
       }
 
@@ -763,45 +768,94 @@ int pr_cmd_dispatch(cmd_rec *cmd) {
     PR_CMD_DISPATCH_FL_SEND_RESPONSE|PR_CMD_DISPATCH_FL_CLEAR_RESPONSE);
 }
 
-static cmd_rec *make_ftp_cmd(pool *p, char *buf, int flags) {
-  char *cp = buf, *wrd;
+static cmd_rec *make_ftp_cmd(pool *p, char *buf, size_t buflen, int flags) {
+  register unsigned int i, j;
+  char *arg, *ptr, *wrd;
+  size_t arg_len;
   cmd_rec *cmd;
   pool *subpool;
   array_header *tarr;
-  int str_flags = PR_STR_FL_PRESERVE_COMMENTS|flags;
+  int have_crnul = FALSE, str_flags = PR_STR_FL_PRESERVE_COMMENTS|flags;
 
   /* Be pedantic (and RFC-compliant) by not allowing leading whitespace
    * in an issued FTP command.  Will this cause troubles with many clients?
    */
   if (PR_ISSPACE(buf[0])) {
+    pr_trace_msg("ctrl", 5,
+      "command '%s' has illegal leading whitespace, rejecting", buf);
+    errno = EINVAL;
     return NULL;
   }
 
-  /* Nothing there...bail out. */
-  wrd = pr_str_get_word(&cp, str_flags);
+  ptr = buf;
+  wrd = pr_str_get_word(&ptr, str_flags);
   if (wrd == NULL) {
+    /* Nothing there...bail out. */
+    pr_trace_msg("ctrl", 5, "command '%s' is empty, ignoring", buf);
+    errno = ENOENT;
     return NULL;
   }
 
   subpool = make_sub_pool(p);
-  cmd = (cmd_rec *) pcalloc(subpool, sizeof(cmd_rec));
+  pr_pool_tag(subpool, "make_ftp_cmd pool");
+  cmd = pcalloc(subpool, sizeof(cmd_rec));
   cmd->pool = subpool;
   cmd->tmp_pool = NULL;
   cmd->stash_index = -1;
+  cmd->stash_hash = 0;
 
   tarr = make_array(cmd->pool, 2, sizeof(char *));
 
   *((char **) push_array(tarr)) = pstrdup(cmd->pool, wrd);
   cmd->argc++;
-  cmd->arg = pstrdup(cmd->pool, cp);
 
-  while ((wrd = pr_str_get_word(&cp, str_flags)) != NULL) {
+  /* Make a copy of the command argument; we need to scan through it,
+   * looking for any CR+NUL sequences, per RFC 2460, Section 3.1.
+   *
+   * Note for future readers that this scanning may cause problems for
+   * commands such as ADAT, ENC, and MIC.  Per RFC 2228, the arguments for
+   * these commands are base64-encoded Telnet strings, thus there is no
+   * chance of them containing CRNUL sequences.  Any modules which implement
+   * the translating of those arguments, e.g. mod_gss, will need to ensure
+   * it does the proper handling of CRNUL sequences itself.
+   */
+  arg_len = buflen - strlen(wrd);
+  arg = pcalloc(cmd->pool, arg_len + 1);
+
+  for (i = 0, j = 0; i < arg_len; i++) {
+    pr_signals_handle();
+    if (i > 1 &&
+        ptr[i] == '\0' &&
+        ptr[i-1] == '\r') {
+
+      /* Strip out the NUL by simply not copying it into the new buffer. */
+      have_crnul = TRUE;
+  
+    } else {
+      arg[j++] = ptr[i];
+    }
+  }
+
+  cmd->arg = arg;
+
+  if (have_crnul) {
+    char *dup_arg;
+
+    /* Now make a copy of the stripped argument; this is what we need to
+     * tokenize into words, for further command dispatching/processing.
+     */
+    dup_arg = pstrdup(cmd->pool, arg);
+    ptr = dup_arg;
+  }
+
+  while ((wrd = pr_str_get_word(&ptr, str_flags)) != NULL) {
+    pr_signals_handle();
     *((char **) push_array(tarr)) = pstrdup(cmd->pool, wrd);
     cmd->argc++;
   }
 
   *((char **) push_array(tarr)) = NULL;
-  cmd->argv = (char **) tarr->elts;
+  cmd->argv = tarr->elts;
 
   /* This table will not contain that many entries, so a low number
    * of chains should suffice.
@@ -847,7 +901,7 @@ static void cmd_loop(server_rec *server, conn_t *c) {
       if (cmd->is_ftp == FALSE) {
         pr_log_pri(PR_LOG_WARNING,
           "client sent %s command '%s', disconnecting", cmd->protocol,
-          cmd->argv[0]);
+          (char *) cmd->argv[0]);
         pr_event_generate("core.bad-protocol", cmd);
         pr_session_disconnect(NULL, PR_SESS_DISCONNECT_BAD_PROTOCOL,
           cmd->protocol);
@@ -866,7 +920,7 @@ static void cmd_loop(server_rec *server, conn_t *c) {
   }
 }
 
-static void core_restart_cb(void *d1, void *d2, void *d3, void *d4) {
+void restart_daemon(void *d1, void *d2, void *d3, void *d4) {
   if (is_master && mpid) {
     int maxfd;
     fd_set childfds;
@@ -919,6 +973,7 @@ static void core_restart_cb(void *d1, void *d2, void *d3, void *d4) {
     init_netaddr();
     init_class();
     init_config();
+    init_dirtree();
 
 #ifdef PR_USE_NLS
     encode_free();
@@ -931,13 +986,20 @@ static void core_restart_cb(void *d1, void *d2, void *d3, void *d4) {
     pr_event_generate("core.preparse", NULL);
 
     PRIVS_ROOT
-    if (pr_parser_parse_file(NULL, config_filename, NULL, 0) == -1) {
+    if (pr_parser_parse_file(NULL, config_filename, NULL, 0) < 0) {
       int xerrno = errno;
 
       PRIVS_RELINQUISH
-      pr_log_pri(PR_LOG_WARNING,
-        "fatal: unable to read configuration file '%s': %s", config_filename,
-        strerror(xerrno));
+
+      /* Note: EPERM is used to indicate the presence of unrecognized
+       * configuration directives in the parsed file(s).
+       */
+      if (xerrno != EPERM) {
+        pr_log_pri(PR_LOG_WARNING,
+          "fatal: unable to read configuration file '%s': %s", config_filename,
+          strerror(xerrno));
+      }
+
       pr_session_end(0);
     }
     PRIVS_RELINQUISH
@@ -985,29 +1047,6 @@ static void core_restart_cb(void *d1, void *d2, void *d3, void *d4) {
   }
 }
 
-#ifndef PR_DEVEL_NO_FORK
-static int dup_low_fd(int fd) {
-  int i, need_close[3] = {-1, -1, -1};
-
-  for (i = 0; i < 3; i++) {
-    if (fd == i) {
-      fd = dup(fd);
-      fcntl(fd, F_SETFD, FD_CLOEXEC);
-
-      need_close[i] = 1;
-    }
-  }
-
-  for (i = 0; i < 3; i++) {
-    if (need_close[i] > -1) {
-      (void) close(i);
-    }
-  }
-
-  return fd;
-}
-#endif /* PR_DEVEL_NO_FORK */
-
 static void set_server_privs(void) {
   uid_t server_uid, current_euid = geteuid();
   gid_t server_gid, current_egid = getegid();
@@ -1040,7 +1079,7 @@ static void set_server_privs(void) {
   }
 }
 
-static void fork_server(int fd, conn_t *l, unsigned char nofork) {
+static void fork_server(int fd, conn_t *l, unsigned char no_fork) {
   conn_t *conn = NULL;
   int i, rev;
   int semfds[2] = { -1, -1 };
@@ -1050,7 +1089,7 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
   pid_t pid;
   sigset_t sig_set;
 
-  if (!nofork) {
+  if (no_fork == FALSE) {
 
     /* A race condition exists on heavily loaded servers where the parent
      * catches SIGHUP and attempts to close/re-open the main listening
@@ -1068,9 +1107,7 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
     /* Need to make sure the child (writer) end of the pipe isn't
      * < 2 (stdio/stdout/stderr) as this will cause problems later.
      */
-    if (semfds[1] < 3) {
-      semfds[1] = dup_low_fd(semfds[1]);
-    }
+    semfds[1] = pr_fs_get_usable_fd(semfds[1]);
 
     /* Make sure we set the close-on-exec flag for the parent's read side
      * of the pipe.
@@ -1173,13 +1210,13 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
 #endif /* PR_DEVEL_NO_FORK */
 
   /* Child is running here */
-  if (signal(SIGUSR1, sig_disconnect) == SIG_ERR) {
+  if (signal(SIGUSR1, pr_signals_handle_disconnect) == SIG_ERR) {
     pr_log_pri(PR_LOG_NOTICE,
       "unable to install SIGUSR1 (signal %d) handler: %s", SIGUSR1,
       strerror(errno));
   }
 
-  if (signal(SIGUSR2, sig_evnt) == SIG_ERR) {
+  if (signal(SIGUSR2, pr_signals_handle_event) == SIG_ERR) {
     pr_log_pri(PR_LOG_NOTICE,
       "unable to install SIGUSR2 (signal %d) handler: %s", SIGUSR2,
       strerror(errno));
@@ -1255,6 +1292,7 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
     exit(1);
   }
 
+  pr_gettimeofday_millis(&session.connect_time_ms);
   pr_event_generate("core.connect", conn);
 
   /* Find the server for this connection. */
@@ -1307,15 +1345,14 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
 
   pr_netaddr_set_sess_addrs();
 
-  /* Check and see if we are shutdown */
-  if (shutdownp) {
+  /* Check and see if we are shutting down. */
+  if (shutting_down) {
     time_t now;
 
     time(&now);
     if (!deny || deny <= now) {
       config_rec *c = NULL;
-      char *reason = NULL;
-      const char *serveraddress;
+      const char *reason = NULL, *serveraddress;
 
       serveraddress = (session.c && session.c->local_addr) ?
         pr_netaddr_get_ipstr(session.c->local_addr) :
@@ -1324,8 +1361,15 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
       c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress",
         FALSE);
       if (c != NULL) {
-        pr_netaddr_t *masq_addr = (pr_netaddr_t *) c->argv[0];
-        serveraddress = pr_netaddr_get_ipstr(masq_addr);
+        pr_netaddr_t *masq_addr = NULL;
+
+        if (c->argv[0] != NULL) {
+          masq_addr = c->argv[0];
+        }
+
+        if (masq_addr != NULL) {
+          serveraddress = pr_netaddr_get_ipstr(masq_addr);
+        }
       }
 
       reason = sreplace(permanent_pool, shutmsg,
@@ -1382,6 +1426,12 @@ static void fork_server(int fd, conn_t *l, unsigned char nofork) {
     pr_log_pri(PR_LOG_NOTICE, "Connection from %s [%s] denied",
       session.c->remote_name,
       pr_netaddr_get_ipstr(session.c->remote_addr));
+
+    /* XXX Send DisplayConnect here? No chroot to worry about; modules have
+     * NOT been initialized, so generating an event would not work as
+     * expected.
+     */
+
     exit(0);
   }
 
@@ -1482,20 +1532,22 @@ static void daemon_loop(void) {
     maxfd = semaphore_fds(&listenfds, maxfd);
 
     /* Check for ftp shutdown message file */
-    switch (check_shutmsg(&shut, &deny, &disc, shutmsg, sizeof(shutmsg))) {
+    switch (check_shutmsg(PR_SHUTMSG_PATH, &shut, &deny, &disc, shutmsg,
+        sizeof(shutmsg))) {
       case 1:
-        if (!shutdownp)
+        if (!shutting_down) {
           disc_children();
-        shutdownp = 1;
+        }
+        shutting_down = TRUE;
         break;
 
-      case 0:
-        shutdownp = 0;
+      default:
+        shutting_down = FALSE;
         deny = disc = (time_t) 0;
         break;
     }
 
-    if (shutdownp) {
+    if (shutting_down) {
       tv.tv_sec = 5L;
       tv.tv_usec = 0L;
 
@@ -1506,10 +1558,10 @@ static void daemon_loop(void) {
     }
 
     /* If running (a flag signaling whether proftpd is just starting up)
-     * AND shutdownp (a flag signalling the present of /etc/shutmsg) are
+     * AND shutting_down (a flag signalling the present of /etc/shutmsg) are
      * true, then log an error stating this -- but don't stop the server.
      */
-    if (shutdownp && !running) {
+    if (shutting_down && !running) {
 
       /* Check the value of the deny time_t struct w/ the current time.
        * If the deny time has passed, log that all incoming connections
@@ -1612,8 +1664,9 @@ static void daemon_loop(void) {
         /* While we're looking, tally up the number of children forked in
          * the past interval.
          */
-        if (ch->ch_when >= (now - (unsigned long) max_connect_interval))
+        if (ch->ch_when >= (time_t) (now - (long) max_connect_interval)) {
           nconnects++;
+        }
       }
     }
 
@@ -1634,11 +1687,12 @@ static void daemon_loop(void) {
     if (listen_conn) {
 
       /* Check for exceeded MaxInstances. */
-      if (ServerMaxInstances && (child_count() >= ServerMaxInstances)) {
+      if (ServerMaxInstances > 0 &&
+          child_count() >= ServerMaxInstances) {
         pr_event_generate("core.max-instances", NULL);
         
         pr_log_pri(PR_LOG_WARNING,
-          "MaxInstances (%d) reached, new connection denied",
+          "MaxInstances (%lu) reached, new connection denied",
           ServerMaxInstances);
         close(fd);
 
@@ -1653,7 +1707,7 @@ static void daemon_loop(void) {
 
       /* Fork off a child to handle the connection. */
       } else {
-        PR_DEVEL_CLOCK(fork_server(fd, listen_conn, FALSE));
+        PR_DEVEL_CLOCK(fork_server(fd, listen_conn, no_forking));
       }
     }
 #ifdef PR_DEVEL_NO_DAEMON
@@ -1663,619 +1717,6 @@ static void daemon_loop(void) {
   }
 }
 
-/* This function is to handle the dispatching of actions based on
- * signals received by the signal handlers, to avoid signal handler-based
- * race conditions.
- */
-
-void pr_signals_handle(void) {
-  table_handling_signal(TRUE);
-
-  if (errno == EINTR &&
-      PR_TUNABLE_EINTR_RETRY_INTERVAL > 0) {
-    struct timeval tv;
-    unsigned long interval_usecs = PR_TUNABLE_EINTR_RETRY_INTERVAL * 1000000;
-
-    tv.tv_sec = (interval_usecs / 1000000);
-    tv.tv_usec = (interval_usecs - (tv.tv_sec * 1000000));
-
-    pr_trace_msg("signal", 18, "interrupted system call, "
-      "delaying for %lu %s, %lu %s",
-      (unsigned long) tv.tv_sec, tv.tv_sec != 1 ? "secs" : "sec",
-      (unsigned long) tv.tv_usec, tv.tv_usec != 1 ? "microsecs" : "microsec");
-
-    pr_timer_usleep(interval_usecs);
-
-    /* Clear the EINTR errno, now we've dealt with it. */
-    errno = 0;
-  }
-
-  while (recvd_signal_flags) {
-
-    if (recvd_signal_flags & RECEIVED_SIG_ALRM) {
-      recvd_signal_flags &= ~RECEIVED_SIG_ALRM;
-      pr_trace_msg("signal", 9, "handling SIGALRM (signal %d)", SIGALRM);
-      handle_alarm();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_CHLD) {
-      recvd_signal_flags &= ~RECEIVED_SIG_CHLD;
-      pr_trace_msg("signal", 9, "handling SIGCHLD (signal %d)", SIGCHLD);
-      handle_chld();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_EVENT) {
-      recvd_signal_flags &= ~RECEIVED_SIG_EVENT;
-
-      /* The "event" signal is SIGUSR2 in proftpd. */
-      pr_trace_msg("signal", 9, "handling SIGUSR2 (signal %d)", SIGUSR2);
-      handle_evnt();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_SEGV) {
-      recvd_signal_flags &= ~RECEIVED_SIG_SEGV;
-      pr_trace_msg("signal", 9, "handling SIGSEGV (signal %d)", SIGSEGV);
-      handle_terminate_other();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_TERMINATE) {
-      recvd_signal_flags &= ~RECEIVED_SIG_TERMINATE;
-      pr_trace_msg("signal", 9, "handling signal %d", term_signo);
-      handle_terminate();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_TERM_OTHER) {
-      recvd_signal_flags &= ~RECEIVED_SIG_TERM_OTHER;
-      pr_trace_msg("signal", 9, "handling signal %d", term_signo);
-      handle_terminate_other();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_XCPU) {
-      recvd_signal_flags &= ~RECEIVED_SIG_XCPU;
-      pr_trace_msg("signal", 9, "handling SIGXCPU (signal %d)", SIGXCPU);
-      handle_xcpu();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_ABORT) {
-      recvd_signal_flags &= ~RECEIVED_SIG_ABORT;
-      pr_trace_msg("signal", 9, "handling SIGABRT (signal %d)", SIGABRT);
-      handle_abort();
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_RESTART) {
-      recvd_signal_flags &= ~RECEIVED_SIG_RESTART;
-      pr_trace_msg("signal", 9, "handling SIGHUP (signal %d)", SIGHUP);
-
-      /* NOTE: should this be done here, rather than using a schedule? */
-      schedule(core_restart_cb, 0, NULL, NULL, NULL, NULL);
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_EXIT) {
-      recvd_signal_flags &= ~RECEIVED_SIG_EXIT;
-      pr_trace_msg("signal", 9, "handling SIGUSR1 (signal %d)", SIGUSR1);
-      pr_log_pri(PR_LOG_NOTICE, "%s", "Parent process requested shutdown");
-      pr_session_disconnect(NULL, PR_SESS_DISCONNECT_SERVER_SHUTDOWN, NULL);
-    }
-
-    if (recvd_signal_flags & RECEIVED_SIG_SHUTDOWN) {
-      recvd_signal_flags &= ~RECEIVED_SIG_SHUTDOWN;
-      pr_trace_msg("signal", 9, "handling SIGUSR1 (signal %d)", SIGUSR1);
-
-      /* NOTE: should this be done here, rather than using a schedule? */
-      schedule(shutdown_exit, 0, NULL, NULL, NULL, NULL);
-    }
-  }
-
-  table_handling_signal(FALSE);
-}
-
-/* sig_restart occurs in the master daemon when manually "kill -HUP"
- * in order to re-read configuration files, and is sent to all
- * children by the master.
- */
-static RETSIGTYPE sig_restart(int signo) {
-  recvd_signal_flags |= RECEIVED_SIG_RESTART;
-
-  if (signal(SIGHUP, sig_restart) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGHUP (signal %d) handler: %s", SIGHUP,
-      strerror(errno));
-  }
-}
-
-static RETSIGTYPE sig_evnt(int signo) {
-  recvd_signal_flags |= RECEIVED_SIG_EVENT;
-
-  if (signal(SIGUSR2, sig_evnt) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGUSR2 (signal %d) handler: %s", SIGUSR2,
-      strerror(errno));
-  }
-}
-
-/* sig_disconnect is called in children when the parent daemon
- * detects that shutmsg has been created and ftp sessions should
- * be destroyed.  If a file transfer is underway, the process simply
- * dies, otherwise a function is scheduled to attempt to display
- * the shutdown reason.
- */
-static RETSIGTYPE sig_disconnect(int signo) {
-
-  /* If this is an anonymous session, or a transfer is in progress,
-   * perform the exit a little later...
-   */
-  if ((session.sf_flags & SF_ANON) ||
-      (session.sf_flags & SF_XFER)) {
-    recvd_signal_flags |= RECEIVED_SIG_EXIT;
-
-  } else {
-    recvd_signal_flags |= RECEIVED_SIG_SHUTDOWN;
-  }
-
-  if (signal(SIGUSR1, SIG_IGN) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGUSR1 (signal %d) handler: %s", SIGUSR1,
-      strerror(errno));
-  }
-}
-
-static RETSIGTYPE sig_child(int signo) {
-  recvd_signal_flags |= RECEIVED_SIG_CHLD;
-
-  /* We make an exception here to the synchronous processing that is done
-   * for other signals; SIGCHLD is handled asynchronously.  This is made
-   * necessary by two things.
-   *
-   * First, we need to support non-POSIX systems.  Under POSIX, once a
-   * signal handler has been configured for a given signal, that becomes
-   * that signal's disposition, until explicitly changed later.  Non-POSIX
-   * systems, on the other hand, will restore the default disposition of
-   * a signal after a custom signal handler has been configured.  Thus,
-   * to properly support non-POSIX systems, a call to signal(2) is necessary
-   * as one of the last steps in our signal handlers.
-   *
-   * Second, SVR4 systems differ specifically in their semantics of signal(2)
-   * and SIGCHLD.  These systems will check for any unhandled SIGCHLD
-   * signals, waiting to be reaped via wait(2) or waitpid(2), whenever
-   * the disposition of SIGCHLD is changed.  This means that if our process
-   * handles SIGCHLD, but does not call wait(2) or waitpid(2), and then
-   * calls signal(2), another SIGCHLD is generated; this loop repeats,
-   * until the process runs out of stack space and terminates.
-   *
-   * Thus, in order to cover this interaction, we'll need to call handle_chld()
-   * here, asynchronously.  handle_chld() does the work of reaping dead
-   * child processes, and does not seem to call any non-reentrant functions,
-   * so it should be safe.
-   */
-
-  handle_chld();
-
-  if (signal(SIGCHLD, sig_child) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGCHLD (signal %d) handler: %s", SIGCHLD,
-      strerror(errno));
-  }
-}
-
-#ifdef PR_DEVEL_COREDUMP
-static char *prepare_core(void) {
-  static char dir[256];
-
-  memset(dir, '\0', sizeof(dir));
-  snprintf(dir, sizeof(dir)-1, "%s/proftpd-core-%lu", PR_CORE_DIR,
-    (unsigned long) getpid());
-
-  if (mkdir(dir, 0700) < 0) {
-    pr_log_pri(PR_LOG_WARNING, "unable to create directory '%s' for "
-      "coredump: %s", dir, strerror(errno));
-
-  } else {
-    chdir(dir);
-  }
-
-  return dir;
-}
-#endif /* PR_DEVEL_COREDUMP */
-
-static RETSIGTYPE sig_abort(int signo) {
-  recvd_signal_flags |= RECEIVED_SIG_ABORT;
-
-  if (signal(SIGABRT, SIG_DFL) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGABRT (signal %d) handler: %s", SIGABRT,
-      strerror(errno));
-  }
-
-#ifdef PR_DEVEL_COREDUMP
-  pr_log_pri(PR_LOG_NOTICE, "ProFTPD received SIGABRT signal, generating core "
-    "file in %s", prepare_core());
-  pr_session_end(PR_SESS_END_FL_NOEXIT);
-  abort();
-#endif /* PR_DEVEL_COREDUMP */
-}
-
-static void handle_abort(void) {
-  pr_log_pri(PR_LOG_NOTICE, "ProFTPD received SIGABRT signal, no core dump");
-  finish_terminate();
-}
-
-#ifdef PR_DEVEL_STACK_TRACE
-static void handle_stacktrace_signal(int signo, siginfo_t *info, void *ptr) {
-  register unsigned i;
-  ucontext_t *uc = (ucontext_t *) ptr;
-  void *trace[PR_TUNABLE_CALLER_DEPTH];
-  char **strings;
-  int tracesz;
-
-  /* Call the "normal" signal handler. */
-  table_handling_signal(TRUE);
-  sig_terminate(signo);
-
-  pr_log_pri(PR_LOG_ERR, "-----BEGIN STACK TRACE-----");
-
-  tracesz = backtrace(trace, PR_TUNABLE_CALLER_DEPTH);
-  if (tracesz < 0) {
-    pr_log_pri(PR_LOG_ERR, "backtrace(3) error: %s", strerror(errno));
-  }
-
-  /* Overwrite sigaction with caller's address */
-#if defined(REG_EIP)
-  trace[1] = (void *) uc->uc_mcontext.gregs[REG_EIP];
-#elif defined(REG_RIP)
-  trace[1] = (void *) uc->uc_mcontext.gregs[REG_RIP];
-#endif
-
-  strings = backtrace_symbols(trace, tracesz);
-  if (strings == NULL) {
-    pr_log_pri(PR_LOG_ERR, "backtrace_symbols(3) error: %s", strerror(errno));
-  }
-
-  /* Skip first stack frame; it just points here. */
-  for (i = 1; i < tracesz; ++i) {
-    pr_log_pri(PR_LOG_ERR, "[%u] %s", i-1, strings[i]);
-  }
-
-  pr_log_pri(PR_LOG_ERR, "-----END STACK TRACE-----");
-
-  finish_terminate();
-}
-#endif /* PR_DEVEL_STACK_TRACE */
-
-static RETSIGTYPE sig_terminate(int signo) {
-
-  /* Capture the signal number for later display purposes. */
-  term_signo = signo;
-
-  if (signo == SIGSEGV ||
-      signo == SIGXCPU
-#ifdef SIGBUS
-      || signo == SIGBUS) {
-#else
-     ) {
-#endif /* SIGBUS */
-
-    if (signo == SIGXCPU) {
-      recvd_signal_flags |= RECEIVED_SIG_XCPU;
-
-    } else {
-      recvd_signal_flags |= RECEIVED_SIG_SEGV;
-    }
-
-    /* This is probably not the safest thing to be doing, but since the
-     * process is terminating anyway, why not?  It helps when knowing/logging
-     * that a segfault happened...
-     */
-    pr_trace_msg("signal", 9, "handling %s (signal %d)",
-      signo == SIGSEGV ? "SIGSEGV" : 
-        signo == SIGXCPU ? "SIGXCPU" : "SIGBUS", signo);
-    pr_log_pri(PR_LOG_NOTICE, "ProFTPD terminating (signal %d)", signo);
-
-    pr_log_pri(PR_LOG_INFO, "%s session closed.",
-      pr_session_get_protocol(PR_SESS_PROTO_FL_LOGOUT));
-
-#ifdef PR_DEVEL_STACK_TRACE
-    install_stacktrace_handler();
-#endif /* PR_DEVEL_STACK_TRACE */
-
-  } else if (signo == SIGTERM) {
-    recvd_signal_flags |= RECEIVED_SIG_TERMINATE;
-
-  } else {
-    recvd_signal_flags |= RECEIVED_SIG_TERM_OTHER;
-  }
-
-  /* Ignore future occurrences of this signal; we'll be terminating anyway. */
-
-  if (signal(signo, SIG_IGN) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install handler for signal %d: %s", signo, strerror(errno));
-  }
-}
-
-static void handle_chld(void) {
-  sigset_t sig_set;
-  pid_t pid;
-
-  sigemptyset(&sig_set);
-  sigaddset(&sig_set, SIGTERM);
-  sigaddset(&sig_set, SIGCHLD);
-
-  pr_alarms_block();
-
-  /* Block SIGTERM in here, so we don't create havoc with the child list
-   * while modifying it.
-   */
-  if (sigprocmask(SIG_BLOCK, &sig_set, NULL) < 0) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to block signal set: %s", strerror(errno));
-  }
-
-  while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
-    if (child_remove(pid) == 0)
-      have_dead_child = TRUE;
-  }
-
-  if (sigprocmask(SIG_UNBLOCK, &sig_set, NULL) < 0) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to unblock signal set: %s", strerror(errno));
-  }
-
-  pr_alarms_unblock();
-}
-
-static void handle_evnt(void) {
-  pr_event_generate("core.signal.USR2", NULL);
-}
-
-static void handle_xcpu(void) {
-  pr_log_pri(PR_LOG_NOTICE, "ProFTPD CPU limit exceeded (signal %d)", SIGXCPU);
-  finish_terminate();
-}
-
-static void handle_terminate_other(void) {
-  pr_log_pri(PR_LOG_WARNING, "ProFTPD terminating (signal %d)", term_signo);
-  finish_terminate();
-}
-
-static void handle_terminate(void) {
-
-  /* Do not log if we are a child that has been terminated. */
-  if (is_master) {
-
-    /* Send a SIGTERM to all our children */
-    if (child_count()) {
-      PRIVS_ROOT
-      child_signal(SIGTERM);
-      PRIVS_RELINQUISH
-    }
-
-    pr_log_pri(PR_LOG_NOTICE, "ProFTPD killed (signal %d)", term_signo);
-  }
-
-  finish_terminate();
-}
-
-static void finish_terminate(void) {
-
-  if (is_master &&
-      mpid == getpid()) {
-    PRIVS_ROOT
-
-    /* Do not need the pidfile any longer. */
-    if (ServerType == SERVER_STANDALONE &&
-        !nodaemon)
-      pr_pidfile_remove();
-
-    /* Run any exit handlers registered in the master process here, so that
-     * they may have the benefit of root privs.  More than likely these
-     * exit handlers were registered by modules' module initialization
-     * functions, which also occur under root priv conditions.
-     *
-     * If an exit handler is registered after the fork(), it won't be run here;
-     * that registration occurs in a different process space.
-     */
-    pr_event_generate("core.exit", NULL);
-    pr_event_generate("core.shutdown", NULL);
-
-    /* Remove the registered exit handlers now, so that the ensuing
-     * pr_session_end() call (outside the root privs condition) does not call
-     * the exit handlers for the master process again.
-     */
-    pr_event_unregister(NULL, "core.exit", NULL);
-    pr_event_unregister(NULL, "core.shutdown", NULL);
-
-    PRIVS_RELINQUISH
-
-    if (ServerType == SERVER_STANDALONE) {
-      pr_log_pri(PR_LOG_NOTICE, "ProFTPD " PROFTPD_VERSION_TEXT
-        " standalone mode SHUTDOWN");
-
-      /* Clean up the scoreboard */
-      PRIVS_ROOT
-      pr_delete_scoreboard();
-      PRIVS_RELINQUISH
-    }
-  }
-
-  pr_session_disconnect(NULL, PR_SESS_DISCONNECT_SIGNAL, "Killed by signal");
-}
-
-#ifdef PR_DEVEL_STACK_TRACE
-static void install_stacktrace_handler(void) {
-  struct sigaction action;
-
-  memset(&action, 0, sizeof(action));
-  action.sa_sigaction = handle_stacktrace_signal;
-  action.sa_flags = SA_SIGINFO;
-
-  /* Ideally we would check the return value here. */
-  sigaction(SIGSEGV, &action, NULL);
-# ifdef SIGBUS
-  sigaction(SIGBUS, &action, NULL);
-# endif /* SIGBUS */
-  sigaction(SIGXCPU, &action, NULL);
-}
-#endif /* PR_DEVEL_STACK_TRACE */
-
-static void install_signal_handlers(void) {
-  sigset_t sig_set;
-
-  /* Should the master server (only applicable in standalone mode)
-   * kill off children if we receive a signal that causes termination?
-   * Hmmmm... maybe this needs to be rethought, but I've done it in
-   * such a way as to only kill off our children if we receive a SIGTERM,
-   * meaning that the admin wants us dead (and probably our kids too).
-   */
-
-  /* The sub-pool for the child list is created the first time we fork
-   * off a child.  To conserve memory, the pool and list is destroyed
-   * when our last child dies (to prevent the list from eating more and
-   * more memory on long uptimes).
-   */
-
-  sigemptyset(&sig_set);
-
-  sigaddset(&sig_set, SIGCHLD);
-  sigaddset(&sig_set, SIGINT);
-  sigaddset(&sig_set, SIGQUIT);
-  sigaddset(&sig_set, SIGILL);
-  sigaddset(&sig_set, SIGABRT);
-  sigaddset(&sig_set, SIGFPE);
-  sigaddset(&sig_set, SIGSEGV);
-  sigaddset(&sig_set, SIGALRM);
-  sigaddset(&sig_set, SIGTERM);
-  sigaddset(&sig_set, SIGHUP);
-  sigaddset(&sig_set, SIGUSR2);
-#ifdef SIGSTKFLT
-  sigaddset(&sig_set, SIGSTKFLT);
-#endif /* SIGSTKFLT */
-#ifdef SIGIO
-  sigaddset(&sig_set, SIGIO);
-#endif /* SIGIO */
-#ifdef SIGBUS
-  sigaddset(&sig_set, SIGBUS);
-#endif /* SIGBUS */
-
-  if (signal(SIGCHLD, sig_child) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGCHLD (signal %d) handler: %s", SIGCHLD,
-      strerror(errno));
-  }
-
-  if (signal(SIGHUP, sig_restart) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGHUP (signal %d) handler: %s", SIGHUP,
-      strerror(errno));
-  }
-
-  if (signal(SIGINT, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGINT (signal %d) handler: %s", SIGINT,
-      strerror(errno));
-  }
-
-  if (signal(SIGQUIT, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGQUIT (signal %d) handler: %s", SIGQUIT,
-      strerror(errno));
-  }
-
-  if (signal(SIGILL, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGILL (signal %d) handler: %s", SIGILL,
-      strerror(errno));
-  }
-
-  if (signal(SIGFPE, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGFPE (signal %d) handler: %s", SIGFPE,
-      strerror(errno));
-  }
-
-  if (signal(SIGABRT, sig_abort) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGABRT (signal %d) handler: %s", SIGABRT,
-      strerror(errno));
-  }
-
-#ifdef PR_DEVEL_STACK_TRACE
-  /* Installs stacktrace handlers for SIGSEGV, SIGXCPU, and SIGBUS. */
-  install_stacktrace_handler();
-#else
-  if (signal(SIGSEGV, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGSEGV (signal %d) handler: %s", SIGSEGV,
-      strerror(errno));
-  }
-
-  if (signal(SIGXCPU, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGXCPU (signal %d) handler: %s", SIGXCPU,
-      strerror(errno));
-  }
-
-# ifdef SIGBUS
-  if (signal(SIGBUS, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGBUS (signal %d) handler: %s", SIGBUS,
-      strerror(errno));
-  }
-# endif /* SIGBUS */
-#endif /* PR_DEVEL_STACK_TRACE */
-
-  /* Ignore SIGALRM; this will be changed when a timer is registered. But
-   * this will prevent SIGALRMs from killing us if we don't currently have
-   * any timers registered.
-    */
-  if (signal(SIGALRM, SIG_IGN) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGALRM (signal %d) handler: %s", SIGALRM,
-      strerror(errno));
-  }
-
-  if (signal(SIGTERM, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGTERM (signal %d) handler: %s", SIGTERM,
-      strerror(errno));
-  }
-
-  if (signal(SIGURG, SIG_IGN) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGURG (signal %d) handler: %s", SIGURG,
-      strerror(errno));
-  }
-
-#ifdef SIGSTKFLT
-  if (signal(SIGSTKFLT, sig_terminate) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGSTKFLT (signal %d) handler: %s", SIGSTKFLT,
-      strerror(errno));
-  }
-#endif /* SIGSTKFLT */
-
-#ifdef SIGIO
-  if (signal(SIGIO, SIG_IGN) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGIO (signal %d) handler: %s", SIGIO,
-      strerror(errno));
-  }
-#endif /* SIGIO */
-
-  if (signal(SIGUSR2, sig_evnt) == SIG_ERR) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to install SIGUSR2 (signal %d) handler: %s", SIGUSR2,
-      strerror(errno));
-  }
-
-  /* In case our parent left signals blocked (as happens under some
-   * poor inetd implementations)
-   */
-  if (sigprocmask(SIG_UNBLOCK, &sig_set, NULL) < 0) {
-    pr_log_pri(PR_LOG_NOTICE,
-      "unable to block signal set: %s", strerror(errno));
-  }
-}
-
 static void daemonize(void) {
 #ifndef HAVE_SETSID
   int ttyfd;
@@ -2357,12 +1798,17 @@ static void inetd_main(void) {
         pr_log_pri(PR_LOG_ERR, "error opening scoreboard: wrong version, "
           "writing new scoreboard");
 
-        /* Delete the scoreboard, then open it again (and assume that the
-         * open succeeds).
-         */
+        /* Delete the scoreboard, then open it again. */
         PRIVS_ROOT
         pr_delete_scoreboard();
-        pr_open_scoreboard(O_RDWR);
+        if (pr_open_scoreboard(O_RDWR) < 0) {
+          int xerrno = errno;
+
+          PRIVS_RELINQUISH
+          pr_log_pri(PR_LOG_ERR, "error opening scoreboard: %s",
+            strerror(xerrno));
+          return;
+        }
         break;
 
       default:
@@ -2379,8 +1825,10 @@ static void inetd_main(void) {
   init_bindings();
 
   /* Check our shutdown status */
-  if (check_shutmsg(&shut, &deny, &disc, shutmsg, sizeof(shutmsg)) == 1)
-    shutdownp = 1;
+  if (check_shutmsg(PR_SHUTMSG_PATH, &shut, &deny, &disc, shutmsg,
+      sizeof(shutmsg)) == 1) {
+    shutting_down = TRUE;
+  }
 
   /* Finally, call right into fork_server() to start servicing the
    * connection immediately.
@@ -2438,7 +1886,12 @@ static void standalone_main(void) {
   pr_log_pri(PR_LOG_NOTICE, "ProFTPD %s (built %s) standalone mode STARTUP",
     PROFTPD_VERSION_TEXT " " PR_STATUS, BUILD_STAMP);
 
-  pr_pidfile_write();
+  if (pr_pidfile_write() < 0) {
+    fprintf(stderr, "error opening PidFile '%s': %s\n", pr_pidfile_get(),
+      strerror(errno));
+    exit(1);
+  }
+
   daemon_loop();
 }
 
@@ -2498,6 +1951,7 @@ static void show_settings(void) {
   printf("%s", "  LDFLAGS: " PR_BUILD_LDFLAGS "\n");
   printf("%s", "  LIBS: " PR_BUILD_LIBS "\n");
 
+  /* Files/paths */
   printf("%s", "\n  Files:\n");
   printf("%s", "    Configuration File:\n");
   printf("%s", "      " PR_CONFIG_FILE_PATH "\n");
@@ -2512,6 +1966,24 @@ static void show_settings(void) {
   printf("%s", "      " PR_LIBEXEC_DIR "\n");
 #endif /* PR_USE_DSO */
 
+  /* Informational */
+  printf("%s", "\n  Info:\n");
+#if SIZEOF_UID_T == SIZEOF_INT
+  printf("    + Max supported UID: %u\n", UINT_MAX);
+#elif SIZEOF_UID_T == SIZEOF_LONG
+  printf("    + Max supported UID: %lu\n", ULONG_MAX);
+#elif SIZEOF_UID_T == SIZEOF_LONG_LONG
+  printf("    + Max supported UID: %llu\n", ULLONG_MAX);
+#endif
+
+#if SIZEOF_GID_T == SIZEOF_INT
+  printf("    + Max supported GID: %u\n", UINT_MAX);
+#elif SIZEOF_GID_T == SIZEOF_LONG
+  printf("    + Max supported GID: %lu\n", ULONG_MAX);
+#elif SIZEOF_GID_T == SIZEOF_LONG_LONG
+  printf("    + Max supported GID: %llu\n", ULLONG_MAX);
+#endif
+
   /* Feature settings */
   printf("%s", "\n  Features:\n");
 #ifdef PR_USE_AUTO_SHADOW
@@ -2582,6 +2054,18 @@ static void show_settings(void) {
   printf("%s", "    - NLS support\n");
 #endif /* PR_USE_NLS */
 
+#ifdef PR_USE_REDIS
+  printf("%s", "    + Redis support\n");
+#else
+  printf("%s", "    - Redis support\n");
+#endif /* PR_USE_REDIS */
+
+#ifdef PR_USE_SODIUM
+  printf("%s", "    + Sodium support\n");
+#else
+  printf("%s", "    - Sodium support\n");
+#endif /* PR_USE_SODIUM */
+
 #ifdef PR_USE_OPENSSL
 # ifdef PR_USE_OPENSSL_FIPS
     printf("%s", "    + OpenSSL support (FIPS enabled)\n");
@@ -2622,15 +2106,24 @@ static void show_settings(void) {
   printf("%s", "    - Trace support\n");
 #endif /* PR_USE_TRACE */
 
+#ifdef PR_USE_XATTR
+  printf("%s", "    + xattr support\n");
+#else
+  printf("%s", "    - xattr support\n");
+#endif /* PR_USE_XATTR */
+
   /* Tunable settings */
   printf("%s", "\n  Tunable Options:\n");
   printf("    PR_TUNABLE_BUFFER_SIZE = %u\n", PR_TUNABLE_BUFFER_SIZE);
   printf("    PR_TUNABLE_DEFAULT_RCVBUFSZ = %u\n", PR_TUNABLE_DEFAULT_RCVBUFSZ);
   printf("    PR_TUNABLE_DEFAULT_SNDBUFSZ = %u\n", PR_TUNABLE_DEFAULT_SNDBUFSZ);
+  printf("    PR_TUNABLE_ENV_MAX = %u\n", PR_TUNABLE_ENV_MAX);
   printf("    PR_TUNABLE_GLOBBING_MAX_MATCHES = %lu\n", PR_TUNABLE_GLOBBING_MAX_MATCHES);
   printf("    PR_TUNABLE_GLOBBING_MAX_RECURSION = %u\n", PR_TUNABLE_GLOBBING_MAX_RECURSION);
   printf("    PR_TUNABLE_HASH_TABLE_SIZE = %u\n", PR_TUNABLE_HASH_TABLE_SIZE);
+  printf("    PR_TUNABLE_LOGIN_MAX = %u\n", PR_TUNABLE_LOGIN_MAX);
   printf("    PR_TUNABLE_NEW_POOL_SIZE = %u\n", PR_TUNABLE_NEW_POOL_SIZE);
+  printf("    PR_TUNABLE_PATH_MAX = %u\n", PR_TUNABLE_PATH_MAX);
   printf("    PR_TUNABLE_SCOREBOARD_BUFFER_SIZE = %u\n",
     PR_TUNABLE_SCOREBOARD_BUFFER_SIZE);
   printf("    PR_TUNABLE_SCOREBOARD_SCRUB_TIMER = %u\n",
@@ -2692,6 +2185,9 @@ static struct option_help {
   { "--version-status", "-vv",
     "Print extended version information and exit" },
 
+  { "--nofork", "-X",
+    "Non-forking debug mode; exits after one session" },
+
   { "--ipv4", "-4",
     "Support IPv4 connections only" },
 
@@ -2719,7 +2215,7 @@ static void show_usage(int exit_code) {
 
 int main(int argc, char *argv[], char **envp) {
   int optc, show_version = 0;
-  const char *cmdopts = "D:NVc:d:hlnp:qS:tv46";
+  const char *cmdopts = "D:NVc:d:hlnp:qS:tvX46";
   mode_t *main_umask = NULL;
   socklen_t peerlen;
   struct sockaddr peer;
@@ -2775,6 +2271,8 @@ int main(int argc, char *argv[], char **envp) {
    * --configtest
    * -v                 report version number
    * --version
+   * -X
+   * --nofork           debug/non-fork mode
    * -4                 support IPv4 connections only
    * --ipv4
    * -6                 support IPv6 connections
@@ -2826,6 +2324,12 @@ int main(int argc, char *argv[], char **envp) {
         exit(1);
       }
       pr_log_setdebuglevel(atoi(optarg));
+
+      /* If the admin uses -d on the command-line, they explicitly WANT
+       * debug logging, thus make sure the default SyslogLevel is set to
+       * DEBUG (rather than NOTICE); see Bug#3983.
+       */
+      pr_log_setdefaultlevel(PR_LOG_DEBUG);
       break;
 
     case 'c':
@@ -2842,7 +2346,7 @@ int main(int argc, char *argv[], char **envp) {
       break;
 
     case 'l':
-      modules_list(PR_MODULES_LIST_FL_SHOW_STATIC);
+      modules_list2(NULL, PR_MODULES_LIST_FL_SHOW_STATIC);
       exit(0);
       break;
 
@@ -2884,6 +2388,10 @@ int main(int argc, char *argv[], char **envp) {
       show_version++;
       break;
 
+    case 'X':
+      no_forking = TRUE;
+      break;
+
     case 1:
       show_version = 2;
       break;
@@ -2921,10 +2429,8 @@ int main(int argc, char *argv[], char **envp) {
 
   mpid = getpid();
 
-  /* Install signal handlers */
-  install_signal_handlers();
-
   /* Initialize sub-systems */
+  init_signals();
   init_pools();
   init_privs();
   init_log();
@@ -2936,7 +2442,9 @@ int main(int argc, char *argv[], char **envp) {
   init_class();
   free_bindings();
   init_config();
+  init_dirtree();
   init_stash();
+  init_json();
 
 #ifdef PR_USE_CTRLS
   init_ctrls();
@@ -2982,10 +2490,16 @@ int main(int argc, char *argv[], char **envp) {
 
   pr_event_generate("core.preparse", NULL);
 
-  if (pr_parser_parse_file(NULL, config_filename, NULL, 0) == -1) {
-    pr_log_pri(PR_LOG_WARNING,
-      "fatal: unable to read configuration file '%s': %s", config_filename,
-      strerror(errno));
+  if (pr_parser_parse_file(NULL, config_filename, NULL, 0) < 0) {
+    /* Note: EPERM is used to indicate the presence of unrecognized
+     * configuration directives in the parsed file(s).
+     */
+    if (errno != EPERM) {
+      pr_log_pri(PR_LOG_WARNING,
+        "fatal: unable to read configuration file '%s': %s", config_filename,
+        strerror(errno));
+    }
+
     exit(1);
   }
 
@@ -3011,7 +2525,7 @@ int main(int argc, char *argv[], char **envp) {
     printf("  Scoreboard Version: %08x\n", PR_SCOREBOARD_VERSION); 
     printf("  Built: %s\n\n", BUILD_STAMP);
 
-    modules_list(PR_MODULES_LIST_FL_SHOW_VERSION);
+    modules_list2(NULL, PR_MODULES_LIST_FL_SHOW_VERSION);
     exit(0);
   }
 
@@ -3046,8 +2560,10 @@ int main(int argc, char *argv[], char **envp) {
     }
 
     if (set_groups(permanent_pool, daemon_gid, daemon_gids) < 0) {
-      pr_log_pri(PR_LOG_WARNING, "unable to set daemon groups: %s",
-        strerror(errno));
+      if (errno != ENOSYS) {
+        pr_log_pri(PR_LOG_WARNING, "unable to set daemon groups: %s",
+          strerror(errno));
+      }
     }
   }
 
@@ -3072,14 +2588,16 @@ int main(int argc, char *argv[], char **envp) {
    */
 
   if (geteuid() != daemon_uid) {
-    pr_log_pri(PR_LOG_ERR, "unable to set UID to %lu, current UID: %lu",
-      (unsigned long) daemon_uid, (unsigned long) geteuid());
+    pr_log_pri(PR_LOG_ERR, "unable to set UID to %s, current UID: %s",
+      pr_uid2str(permanent_pool, daemon_uid),
+      pr_uid2str(permanent_pool, geteuid()));
     exit(1);
   }
 
   if (getegid() != daemon_gid) {
-    pr_log_pri(PR_LOG_ERR, "unable to set GID to %lu, current GID: %lu",
-      (unsigned long) daemon_gid, (unsigned long) getegid());
+    pr_log_pri(PR_LOG_ERR, "unable to set GID to %s, current GID: %s",
+      pr_gid2str(permanent_pool, daemon_gid),
+      pr_gid2str(permanent_pool, getegid()));
     exit(1);
   }
 #endif /* PR_DEVEL_COREDUMP */
diff --git a/src/memcache.c b/src/memcache.c
index ad0cc9b..adeb29f 100644
--- a/src/memcache.c
+++ b/src/memcache.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2010-2013 The ProFTPD Project team
+ * Copyright (c) 2010-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,14 +22,13 @@
  * OpenSSL in the source distribution.
  */
 
-/* Memcache management
- * $Id: memcache.c,v 1.26 2013-01-28 01:21:05 castaglia Exp $
- */
+/* Memcache management */
 
 #include "conf.h"
 
 #ifdef PR_USE_MEMCACHE
 
+#include "hanson-tpl.h"
 #include <libmemcached/memcached.h>
 
 #if defined(LIBMEMCACHED_VERSION_HEX)
@@ -182,14 +181,37 @@ static int mcache_set_options(pr_memcache_t *mcache, unsigned long flags,
 
   /* Use the binary protocol by default, unless explicitly requested not to. */
   val = memcached_behavior_get(mcache->mc, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL);
+  pr_trace_msg(trace_channel, 16,
+    "found BINARY_PROTOCOL=%s default behavior (val %lu) for connection",
+    val != 1 ? "false" : "true", (unsigned long) val);
+
   if (val != 1) {
     if (!(flags & PR_MEMCACHE_FL_NO_BINARY_PROTOCOL)) {
       res = memcached_behavior_set(mcache->mc,
         MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);
       if (res != MEMCACHED_SUCCESS) {
         pr_trace_msg(trace_channel, 4,
-          "error setting BINARY_PROTOCOL behavior on connection: %s",
+          "error setting BINARY_PROTOCOL=true behavior on connection: %s",
           memcached_strerror(mcache->mc, res));
+
+      } else {
+        pr_trace_msg(trace_channel, 16, "%s",
+          "set BINARY_PROTOCOL=true for connection");
+      }
+    }
+
+  } else {
+    if (flags & PR_MEMCACHE_FL_NO_BINARY_PROTOCOL) {
+      res = memcached_behavior_set(mcache->mc,
+        MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 0);
+      if (res != MEMCACHED_SUCCESS) {
+        pr_trace_msg(trace_channel, 4,
+          "error setting BINARY_PROTOCOL=false behavior on connection: %s",
+          memcached_strerror(mcache->mc, res));
+
+      } else {
+        pr_trace_msg(trace_channel, 16, "%s",
+          "set BINARY_PROTOCOL=false for connection");
       }
     }
   }
@@ -297,7 +319,6 @@ static int mcache_ping_servers(pr_memcache_t *mcache) {
 #endif
 
   server_count = memcached_server_count(clone);
-
   pr_trace_msg(trace_channel, 16,
     "pinging %lu memcached %s", (unsigned long) server_count,
     server_count != 1 ? "servers" : "server");
@@ -413,7 +434,6 @@ static int mcache_stat_servers(pr_memcache_t *mcache) {
             pr_trace_msg(trace_channel, 3,
               "error requesting memcached stats: system error: %s",
               strerror(errno));
-            break;
 
           } else {
             /* We know that we're not using nonblocking IO; this value usually
@@ -423,6 +443,7 @@ static int mcache_stat_servers(pr_memcache_t *mcache) {
              */
             res = MEMCACHED_CONNECTION_FAILURE;
           }
+          break;
 
           case MEMCACHED_SOME_ERRORS:
           case MEMCACHED_SERVER_MARKED_DEAD:
@@ -435,6 +456,8 @@ static int mcache_stat_servers(pr_memcache_t *mcache) {
                 "unable to connect to %s:%d", memcached_server_name(server),
                 memcached_server_port(server));
             }
+
+            break;
           }
 
         default:
@@ -500,10 +523,10 @@ pr_memcache_t *pr_memcache_conn_new(pool *p, module *m, unsigned long flags,
     return NULL;
   }
 
-  sub_pool = pr_pool_create_sz(p, 128);
+  sub_pool = make_sub_pool(p);
   pr_pool_tag(sub_pool, "Memcache connection pool");
 
-  mcache = palloc(sub_pool, sizeof(pr_memcache_t));
+  mcache = pcalloc(sub_pool, sizeof(pr_memcache_t));
   mcache->pool = sub_pool;
   mcache->owner = m;
   mcache->mc = mc;
@@ -577,6 +600,31 @@ int pr_memcache_conn_close(pr_memcache_t *mcache) {
   return 0;
 }
 
+int pr_memcache_conn_clone(pool *p, pr_memcache_t *mcache) {
+  memcached_st *old_mc = NULL, *new_mc = NULL;
+
+  if (p == NULL ||
+      mcache == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  memcached_quit(mcache->mc);
+  old_mc = mcache->mc;
+
+  new_mc = memcached_clone(NULL, old_mc);
+  if (new_mc == NULL) {
+    errno = ENOMEM;
+    return -1;
+  }
+
+  /* Now free up the previous context; we don't need it anymore. */
+  memcached_free(old_mc);
+
+  mcache->mc = new_mc;
+  return 0;
+}
+
 static int modptr_cmp_cb(const void *k1, size_t ksz1, const void *k2,
     size_t ksz2) {
 
@@ -865,16 +913,15 @@ static void mcache_set_module_namespace(pr_memcache_t *mcache, module *m) {
 
   } else {
     if (mcache->namespace_tab != NULL) {
-      void *v;
+      const char *v;
 
       v = pr_table_kget(mcache->namespace_tab, m, sizeof(module *), NULL);
-      if (v) {
+      if (v != NULL) {
         pr_trace_msg(trace_channel, 25,
-          "using namespace prefix '%s' for module 'mod_%s.c'", (const char *) v,
-          m->name);
+          "using namespace prefix '%s' for module 'mod_%s.c'", v, m->name);
 
         res = memcached_callback_set(mcache->mc, MEMCACHED_CALLBACK_PREFIX_KEY,
-          v);
+          (void *) v);
       }
 
     } else {
@@ -883,9 +930,16 @@ static void mcache_set_module_namespace(pr_memcache_t *mcache, module *m) {
   }
 
   if (res != MEMCACHED_SUCCESS) {
-    pr_trace_msg(trace_channel, 9,
-      "unable to set MEMCACHED_CALLBACK_PREFIX_KEY for module 'mod_%s.c': %s",
-      m->name, memcached_strerror(mcache->mc, res));
+    if (m != NULL) {
+      pr_trace_msg(trace_channel, 9,
+        "unable to set MEMCACHED_CALLBACK_PREFIX_KEY for module 'mod_%s.c': %s",
+        m->name, memcached_strerror(mcache->mc, res));
+
+    } else {
+      pr_trace_msg(trace_channel, 9,
+        "unable to clear MEMCACHED_CALLBACK_PREFIX_KEY: %s",
+        memcached_strerror(mcache->mc, res));
+    }
   }
 }
 
@@ -921,7 +975,6 @@ int pr_memcache_kadd(pr_memcache_t *mcache, module *m, const char *key,
           (unsigned long) keysz, (unsigned long) valuesz, strerror(xerrno));
 
         errno = xerrno;
-        break;
 
       } else {
         /* We know that we're not using nonblocking IO; this value usually
@@ -931,6 +984,7 @@ int pr_memcache_kadd(pr_memcache_t *mcache, module *m, const char *key,
          */
         res = MEMCACHED_CONNECTION_FAILURE;
       }
+      break;
 
     case MEMCACHED_SERVER_MARKED_DEAD:
     case MEMCACHED_CONNECTION_FAILURE: {
@@ -942,6 +996,8 @@ int pr_memcache_kadd(pr_memcache_t *mcache, module *m, const char *key,
           "unable to connect to %s:%d", memcached_server_name(server),
           memcached_server_port(server));
       }
+
+      break;
     }
 
     default:
@@ -950,6 +1006,7 @@ int pr_memcache_kadd(pr_memcache_t *mcache, module *m, const char *key,
         (unsigned long) keysz, (unsigned long) valuesz,
         memcached_strerror(mcache->mc, res));
       errno = EPERM;
+      break;
   }
 
   return -1;
@@ -967,6 +1024,10 @@ int pr_memcache_kdecr(pr_memcache_t *mcache, module *m, const char *key,
     return -1;
   }
 
+  /* Note: libmemcached automatically handles the case where value might be
+   * NULL.
+   */
+
   mcache_set_module_namespace(mcache, m);
   res = memcached_decrement(mcache->mc, key, keysz, decr, value);
   mcache_set_module_namespace(mcache, NULL);
@@ -984,7 +1045,6 @@ int pr_memcache_kdecr(pr_memcache_t *mcache, module *m, const char *key,
           (unsigned long) keysz, (unsigned long) decr, strerror(xerrno));
 
         errno = xerrno;
-        break;
 
       } else {
         /* We know that we're not using nonblocking IO; this value usually
@@ -994,6 +1054,7 @@ int pr_memcache_kdecr(pr_memcache_t *mcache, module *m, const char *key,
          */
         res = MEMCACHED_CONNECTION_FAILURE;
       }
+      break;
 
     case MEMCACHED_SERVER_MARKED_DEAD:
     case MEMCACHED_CONNECTION_FAILURE: {
@@ -1005,6 +1066,8 @@ int pr_memcache_kdecr(pr_memcache_t *mcache, module *m, const char *key,
           "unable to connect to %s:%d", memcached_server_name(server),
           memcached_server_port(server));
       }
+
+      break;
     }
 
     default:
@@ -1013,6 +1076,7 @@ int pr_memcache_kdecr(pr_memcache_t *mcache, module *m, const char *key,
         (unsigned long) keysz, (unsigned long) decr,
         memcached_strerror(mcache->mc, res));
       errno = EPERM;
+      break;
   }
 
   return -1;
@@ -1023,6 +1087,7 @@ void *pr_memcache_kget(pr_memcache_t *mcache, module *m, const char *key,
   char *data = NULL;
   void *ptr = NULL;
   memcached_return res;
+  int xerrno = 0;
 
   if (mcache == NULL ||
       m == NULL ||
@@ -1035,6 +1100,7 @@ void *pr_memcache_kget(pr_memcache_t *mcache, module *m, const char *key,
 
   mcache_set_module_namespace(mcache, m);
   data = memcached_get(mcache->mc, key, keysz, valuesz, flags, &res);
+  xerrno = errno;
   mcache_set_module_namespace(mcache, NULL);
 
   if (data == NULL) {
@@ -1047,13 +1113,11 @@ void *pr_memcache_kget(pr_memcache_t *mcache, module *m, const char *key,
 
       case MEMCACHED_ERRNO:
         if (errno != EINPROGRESS) {
-          int xerrno = errno;
           pr_trace_msg(trace_channel, 3,
             "no data found for key (%lu bytes): system error: %s",
             (unsigned long) keysz, strerror(xerrno));
 
           errno = xerrno;
-          break;
 
         } else {
           /* We know that we're not using nonblocking IO; this value usually
@@ -1063,6 +1127,7 @@ void *pr_memcache_kget(pr_memcache_t *mcache, module *m, const char *key,
            */
           res = MEMCACHED_CONNECTION_FAILURE;
         }
+        break;
 
       case MEMCACHED_SERVER_MARKED_DEAD:
       case MEMCACHED_CONNECTION_FAILURE: {
@@ -1074,7 +1139,9 @@ void *pr_memcache_kget(pr_memcache_t *mcache, module *m, const char *key,
             "unable to connect to %s:%d", memcached_server_name(server),
             memcached_server_port(server));
         }
-      } 
+
+        break;
+      }
 
       default:
         pr_trace_msg(trace_channel, 6,
@@ -1103,6 +1170,7 @@ char *pr_memcache_kget_str(pr_memcache_t *mcache, module *m, const char *key,
   char *data = NULL, *ptr = NULL;
   size_t valuesz = 0;
   memcached_return res;
+  int xerrno = 0;
 
   if (mcache == NULL ||
       m == NULL ||
@@ -1114,6 +1182,7 @@ char *pr_memcache_kget_str(pr_memcache_t *mcache, module *m, const char *key,
 
   mcache_set_module_namespace(mcache, m);
   data = memcached_get(mcache->mc, key, keysz, &valuesz, flags, &res);
+  xerrno = errno;
   mcache_set_module_namespace(mcache, NULL);
 
   if (data == NULL) {
@@ -1126,14 +1195,11 @@ char *pr_memcache_kget_str(pr_memcache_t *mcache, module *m, const char *key,
 
       case MEMCACHED_ERRNO:
         if (errno != EINPROGRESS) {
-          int xerrno = errno;
-
           pr_trace_msg(trace_channel, 3,
             "no data found for key (%lu bytes): system error: %s",
             (unsigned long) keysz, strerror(xerrno));
 
           errno = xerrno;
-          break;
 
         } else {
           /* We know that we're not using nonblocking IO; this value usually
@@ -1143,6 +1209,7 @@ char *pr_memcache_kget_str(pr_memcache_t *mcache, module *m, const char *key,
            */
           res = MEMCACHED_CONNECTION_FAILURE;
         }
+        break;
 
       case MEMCACHED_SERVER_MARKED_DEAD:
       case MEMCACHED_CONNECTION_FAILURE: {
@@ -1154,6 +1221,8 @@ char *pr_memcache_kget_str(pr_memcache_t *mcache, module *m, const char *key,
             "unable to connect to %s:%d", memcached_server_name(server),
             memcached_server_port(server));
         }
+
+        break;
       }
 
       default:
@@ -1190,6 +1259,10 @@ int pr_memcache_kincr(pr_memcache_t *mcache, module *m, const char *key,
     return -1;
   }
 
+  /* Note: libmemcached automatically handles the case where value might be
+   * NULL.
+   */
+
   mcache_set_module_namespace(mcache, m);
   res = memcached_increment(mcache->mc, key, keysz, incr, value);
   mcache_set_module_namespace(mcache, NULL);
@@ -1217,7 +1290,6 @@ int pr_memcache_kincr(pr_memcache_t *mcache, module *m, const char *key,
           (unsigned long) keysz, (unsigned long) incr, strerror(xerrno));
 
         errno = xerrno;
-        break;
 
       } else {
         /* We know that we're not using nonblocking IO; this value usually
@@ -1227,6 +1299,7 @@ int pr_memcache_kincr(pr_memcache_t *mcache, module *m, const char *key,
          */
         res = MEMCACHED_CONNECTION_FAILURE;
       }
+      break;
 
     case MEMCACHED_SERVER_MARKED_DEAD:
     case MEMCACHED_CONNECTION_FAILURE: {
@@ -1238,6 +1311,8 @@ int pr_memcache_kincr(pr_memcache_t *mcache, module *m, const char *key,
           "unable to connect to %s:%d", memcached_server_name(server),
           memcached_server_port(server));
       }
+
+      break;
     }
 
     default:
@@ -1246,6 +1321,7 @@ int pr_memcache_kincr(pr_memcache_t *mcache, module *m, const char *key,
         (unsigned long) keysz, (unsigned long) incr,
         memcached_strerror(mcache->mc, res));
       errno = EPERM;
+      break;
   }
 
   return -1;
@@ -1279,7 +1355,6 @@ int pr_memcache_kremove(pr_memcache_t *mcache, module *m, const char *key,
           (unsigned long) keysz, strerror(xerrno));
 
         errno = xerrno;
-        break;
 
       } else {
         /* We know that we're not using nonblocking IO; this value usually
@@ -1289,6 +1364,7 @@ int pr_memcache_kremove(pr_memcache_t *mcache, module *m, const char *key,
          */
         res = MEMCACHED_CONNECTION_FAILURE;
       }
+      break;
 
     case MEMCACHED_SERVER_MARKED_DEAD:
     case MEMCACHED_CONNECTION_FAILURE: {
@@ -1300,6 +1376,8 @@ int pr_memcache_kremove(pr_memcache_t *mcache, module *m, const char *key,
           "unable to connect to %s:%d", memcached_server_name(server),
           memcached_server_port(server));
       }
+
+      break;
     }
 
     default:
@@ -1307,6 +1385,7 @@ int pr_memcache_kremove(pr_memcache_t *mcache, module *m, const char *key,
         "error removing key (%lu bytes): %s", (unsigned long) keysz,
         memcached_strerror(mcache->mc, res));
       errno = EPERM;
+      break;
   }
 
   return -1;
@@ -1328,7 +1407,8 @@ int pr_memcache_kset(pr_memcache_t *mcache, module *m, const char *key,
   }
 
   mcache_set_module_namespace(mcache, m);
-  res = memcached_set(mcache->mc, key, keysz, value, valuesz, expires, flags); 
+  res = memcached_set(mcache->mc, key, keysz, (const char *) value, valuesz,
+    expires, flags);
   mcache_set_module_namespace(mcache, NULL);
 
   switch (res) {
@@ -1344,7 +1424,6 @@ int pr_memcache_kset(pr_memcache_t *mcache, module *m, const char *key,
           (unsigned long) keysz, (unsigned long) valuesz, strerror(xerrno));
 
         errno = xerrno;
-        break;
 
       } else {
         /* We know that we're not using nonblocking IO; this value usually
@@ -1354,6 +1433,7 @@ int pr_memcache_kset(pr_memcache_t *mcache, module *m, const char *key,
          */
         res = MEMCACHED_CONNECTION_FAILURE;
       }
+      break;
 
     case MEMCACHED_SERVER_MARKED_DEAD:
     case MEMCACHED_CONNECTION_FAILURE: {
@@ -1365,6 +1445,8 @@ int pr_memcache_kset(pr_memcache_t *mcache, module *m, const char *key,
           "unable to connect to %s:%d", memcached_server_name(server),
           memcached_server_port(server));
       }
+
+      break;
     }
 
     default:
@@ -1373,6 +1455,7 @@ int pr_memcache_kset(pr_memcache_t *mcache, module *m, const char *key,
         (unsigned long) keysz, (unsigned long) valuesz,
         memcached_strerror(mcache->mc, res));
       errno = EPERM;
+      break;
   }
 
   return -1;
@@ -1479,6 +1562,11 @@ int pr_memcache_conn_close(pr_memcache_t *mcache) {
   return -1;
 }
 
+int pr_memcache_conn_clone(pool *p, pr_memcache_t *mcache) {
+  errno = ENOSYS;
+  return -1;
+}
+
 int pr_memcache_conn_set_namespace(pr_memcache_t *mcache, module *m,
     const char *prefix) {
   errno = ENOSYS;
diff --git a/src/mkhome.c b/src/mkhome.c
index b34adad..c03315e 100644
--- a/src/mkhome.c
+++ b/src/mkhome.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2013 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Home-on-demand support
- * $Id: mkhome.c,v 1.23 2013-10-09 05:21:06 castaglia Exp $
- */
+/* Home-on-demand support */
 
 #include "conf.h"
 #include "privs.h"
@@ -37,7 +35,7 @@ static int create_dir(const char *dir, uid_t uid, gid_t gid,
   struct stat st;
   int res = -1;
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(dir);
   res = pr_fsio_stat(dir, &st);
 
   if (res == -1 &&
@@ -98,7 +96,7 @@ static int create_path(pool *p, const char *path, const char *user,
   char *currpath = NULL, *tmppath = NULL;
   struct stat st;
 
-  pr_fs_clear_cache();
+  pr_fs_clear_cache2(path);
   if (pr_fsio_stat(path, &st) == 0) {
     /* Path already exists, nothing to be done. */
     errno = EEXIST;
@@ -183,8 +181,8 @@ static int copy_symlink(pool *p, const char *src_dir, const char *src_path,
 
   /* Make sure the new symlink has the proper ownership. */
   if (pr_fsio_chown(dst_path, uid, gid) < 0) {
-    pr_log_pri(PR_LOG_WARNING, "CreateHome: error chown'ing '%s' to %u/%u: %s",
-      dst_path, (unsigned int) uid, (unsigned int) gid, strerror(errno));
+    pr_log_pri(PR_LOG_WARNING, "CreateHome: error chown'ing '%s' to %s/%s: %s",
+      dst_path, pr_uid2str(p, uid), pr_gid2str(p, gid), strerror(errno));
   }
 
   return 0; 
@@ -256,7 +254,7 @@ static int copy_dir(pool *p, const char *src_dir, const char *dst_dir,
       /* Make sure the destination file has the proper ownership and mode. */
       if (pr_fsio_chown(dst_path, uid, gid) < 0) {
         pr_log_pri(PR_LOG_WARNING, "CreateHome: error chown'ing '%s' "
-          "to %u/%u: %s", dst_path, (unsigned int) uid, (unsigned int) gid,
+          "to %s/%s: %s", dst_path, pr_uid2str(p, uid), pr_gid2str(p, gid),
           strerror(errno));
       }
 
@@ -310,7 +308,7 @@ int create_home(pool *p, const char *home, const char *user, uid_t uid,
   flags = *((unsigned long *) c->argv[7]);
 
   dst_uid = uid;
-  dst_gid = (home_gid == -1) ? gid : home_gid;
+  dst_gid = (home_gid == (gid_t) -1) ? gid : home_gid;
 
   dst_mode = *((mode_t *) c->argv[1]);
 
diff --git a/src/modules.c b/src/modules.c
index 80f9f1c..7d91e64 100644
--- a/src/modules.c
+++ b/src/modules.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,9 +23,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Module handling routines
- * $Id: modules.c,v 1.64 2013-10-07 05:51:30 castaglia Exp $
- */
+/* Module handling routines */
 
 #include "conf.h"
 
@@ -37,6 +35,8 @@ module *curr_module = NULL;
   
 /* Used to track the priority for loaded modules. */
 static unsigned int curr_module_pri = 0;
+
+static const char *trace_channel = "module";
   
 modret_t *pr_module_call(module *m, modret_t *(*func)(cmd_rec *),
     cmd_rec *cmd) {
@@ -50,7 +50,7 @@ modret_t *pr_module_call(module *m, modret_t *(*func)(cmd_rec *),
     return NULL;
   }
 
-  if (!cmd->tmp_pool) {
+  if (cmd->tmp_pool == NULL) {
     cmd->tmp_pool = make_sub_pool(cmd->pool);
     pr_pool_tag(cmd->tmp_pool, "Module call tmp_pool");
   }
@@ -68,24 +68,35 @@ modret_t *pr_module_call(module *m, modret_t *(*func)(cmd_rec *),
 modret_t *mod_create_data(cmd_rec *cmd, void *d) {
   modret_t *res;
 
+  if (cmd == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   res = pcalloc(cmd->tmp_pool, sizeof(modret_t));
   res->data = d;
 
   return res;
 }
 
-modret_t *mod_create_ret(cmd_rec *cmd, unsigned char err, char *n, char *m) {
+modret_t *mod_create_ret(cmd_rec *cmd, unsigned char err, const char *n,
+    const char *m) {
   modret_t *res;
 
+  if (cmd == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   res = pcalloc(cmd->tmp_pool, sizeof(modret_t));
   res->mr_handler_module = curr_module;
   res->mr_error = err;
 
-  if (n) {
+  if (n != NULL) {
     res->mr_numeric = pstrdup(cmd->tmp_pool, n);
   }
 
-  if (m) {
+  if (m != NULL) {
     res->mr_message = pstrdup(cmd->tmp_pool, m);
   }
 
@@ -95,6 +106,11 @@ modret_t *mod_create_ret(cmd_rec *cmd, unsigned char err, char *n, char *m) {
 modret_t *mod_create_error(cmd_rec *cmd, int mr_errno) {
   modret_t *res;
 
+  if (cmd == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   res = pcalloc(cmd->tmp_pool, sizeof(modret_t));
   res->mr_handler_module = curr_module;
   res->mr_error = mr_errno;
@@ -109,11 +125,18 @@ int modules_session_init(void) {
   module *prev_module = curr_module, *m;
 
   for (m = loaded_modules; m; m = m->next) {
-    if (m && m->sess_init) {
+    if (m->sess_init) {
       curr_module = m;
+
+      pr_trace_msg(trace_channel, 12,
+        "invoking sess_init callback on mod_%s.c", m->name);
       if (m->sess_init() < 0) {
+        int xerrno = errno;
+
         pr_log_pri(PR_LOG_WARNING, "mod_%s.c: error initializing session: %s",
-          m->name, strerror(errno));
+          m->name, strerror(xerrno));
+
+        errno = xerrno;
         return -1;
       }
     }
@@ -123,13 +146,15 @@ int modules_session_init(void) {
   return 0;
 }
 
-unsigned char command_exists(char *name) {
+unsigned char command_exists(const char *name) {
   int idx = -1;
-  cmdtable *cmdtab = pr_stash_get_symbol(PR_SYM_CMD, name, NULL, &idx);
+  unsigned int hash = 0;
+  cmdtable *cmdtab;
 
+  cmdtab = pr_stash_get_symbol2(PR_SYM_CMD, name, NULL, &idx, &hash);
   while (cmdtab && cmdtab->cmd_type != CMD) {
     pr_signals_handle();
-    cmdtab = pr_stash_get_symbol(PR_SYM_CMD, name, cmdtab, &idx);
+    cmdtab = pr_stash_get_symbol2(PR_SYM_CMD, name, cmdtab, &idx, &hash);
   }
 
   return (cmdtab ? TRUE : FALSE);
@@ -143,7 +168,7 @@ module *pr_module_get(const char *name) {
   char buf[80] = {'\0'};
   module *m;
 
-  if (!name) {
+  if (name == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -154,59 +179,71 @@ module *pr_module_get(const char *name) {
     snprintf(buf, sizeof(buf), "mod_%s.c", m->name);
     buf[sizeof(buf)-1] = '\0';
 
-    if (strcmp(buf, name) == 0)
+    if (strcmp(buf, name) == 0) {
       return m;
+    }
   }
 
   errno = ENOENT;
   return NULL;
 }
 
-void modules_list(int flags) {
+void modules_list2(int (*listf)(const char *, ...), int flags) {
+  if (listf == NULL) {
+    listf = printf;
+  }
 
   if (flags & PR_MODULES_LIST_FL_SHOW_STATIC) {
     register unsigned int i = 0;
 
-    printf("Compiled-in modules:\n");
+    listf("Compiled-in modules:\n");
     for (i = 0; static_modules[i]; i++) {
       module *m = static_modules[i];
 
       if (flags & PR_MODULES_LIST_FL_SHOW_VERSION) {
-        char *version = m->module_version;
-        if (version) {
-          printf("  %s\n", version);
+        const char *version;
+
+        version = m->module_version;
+        if (version != NULL) {
+          listf("  %s\n", version);
 
         } else {
-          printf("  mod_%s.c\n", m->name);
+          listf("  mod_%s.c\n", m->name);
         }
 
       } else {
-        printf("  mod_%s.c\n", m->name);
+        listf("  mod_%s.c\n", m->name);
       }
     }
 
   } else {
     module *m;
 
-    printf("Loaded modules:\n");
+    listf("Loaded modules:\n");
     for (m = loaded_modules; m; m = m->next) {
 
       if (flags & PR_MODULES_LIST_FL_SHOW_VERSION) {
-        char *version = m->module_version;
-        if (version) {
-          printf("  %s\n", version);
+        const char *version;
+
+        version = m->module_version;
+        if (version != NULL) {
+          listf("  %s\n", version);
 
         } else {  
-          printf("  mod_%s.c\n", m->name);
+          listf("  mod_%s.c\n", m->name);
         }
 
       } else {
-        printf("  mod_%s.c\n", m->name);
+        listf("  mod_%s.c\n", m->name);
       }
     }
   }
 }
 
+void modules_list(int flags) {
+  modules_list2(NULL, flags);
+}
+
 int pr_module_load_authtab(module *m) {
   if (m == NULL ||
       m->name == NULL) {
diff --git a/src/netacl.c b/src/netacl.c
index b9eccaa..938114d 100644
--- a/src/netacl.c
+++ b/src/netacl.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2013 The ProFTPD Project team
+ * Copyright (c) 2003-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Network ACL routines
- * $Id: netacl.c,v 1.27 2013-02-15 22:39:00 castaglia Exp $
- */
+/* Network ACL routines */
 
 #include "conf.h"
 
@@ -36,21 +34,22 @@ struct pr_netacl_t {
 
   char *pattern;
   int negated;
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   unsigned int masklen;
 };
 
 static const char *trace_channel = "netacl";
 
-pr_netacl_type_t pr_netacl_get_type(pr_netacl_t *acl) {
+pr_netacl_type_t pr_netacl_get_type(const pr_netacl_t *acl) {
   return acl->type;
 }
 
 /* Returns 1 if there was a positive match, -1 if there was a negative
  * match, -2 if there was an error, and zero if there was no match at all.
  */
-int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
+int pr_netacl_match(const pr_netacl_t *acl, const pr_netaddr_t *addr) {
   pool *tmp_pool;
+  int res = 0;
 
   if (acl == NULL ||
       addr == NULL) {
@@ -64,14 +63,14 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
     case PR_NETACL_TYPE_ALL:
       pr_trace_msg(trace_channel, 10, "addr '%s' matched rule 'ALL' ('%s')",
         pr_netaddr_get_ipstr(addr), pr_netacl_get_str(tmp_pool, acl));
-      destroy_pool(tmp_pool);
-      return 1;
+      res = 1;
+      break;
 
     case PR_NETACL_TYPE_NONE:
       pr_trace_msg(trace_channel, 10, "addr '%s' matched rule 'NONE'",
         pr_netaddr_get_ipstr(addr));
-      destroy_pool(tmp_pool);
-      return -1;
+      res = -1;
+      break;
 
     case PR_NETACL_TYPE_IPMASK:
       pr_trace_msg(trace_channel, 10,
@@ -81,17 +80,17 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
       if (pr_netaddr_ncmp(addr, acl->addr, acl->masklen) == 0) {
         pr_trace_msg(trace_channel, 10, "addr '%s' matched IP mask rule '%s'",
           pr_netaddr_get_ipstr(addr), acl->aclstr);
-        destroy_pool(tmp_pool);
 
-        if (acl->negated)
-          return -1;
+        if (acl->negated) {
+          res = -1;
 
-        return 1;
+        } else {
+          res = 1;
+        }
 
       } else {
         if (acl->negated) {
-          destroy_pool(tmp_pool);
-          return 1;
+          res = 1;
         }
       }
       break;
@@ -105,17 +104,17 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
         pr_trace_msg(trace_channel, 10,
           "addr '%s' matched IP address rule '%s'",
           pr_netaddr_get_ipstr(addr), acl->aclstr);
-        destroy_pool(tmp_pool);
 
-        if (acl->negated)
-          return -1;
+        if (acl->negated) {
+          res = -1;
 
-        return 1;
+        } else {
+          res = 1;
+        }
 
       } else {
         if (acl->negated) {
-          destroy_pool(tmp_pool);
-          return 1;
+          res = 1;
         }
       }
       break;
@@ -130,17 +129,17 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
           "addr '%s' (%s) matched DNS name rule '%s'",
           pr_netaddr_get_ipstr(addr), pr_netaddr_get_dnsstr(addr),
           acl->aclstr);
-        destroy_pool(tmp_pool);
 
-        if (acl->negated)
-          return -1;
+        if (acl->negated) {
+          res = -1;
 
-        return 1;
+        } else {
+          res = 1;
+        }
 
       } else {
         if (acl->negated) {
-          destroy_pool(tmp_pool);
-          return 1;
+          res = 1;
         }
       }
       break;
@@ -155,17 +154,17 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
         pr_trace_msg(trace_channel, 10,
           "addr '%s' matched IP glob rule '%s'",
           pr_netaddr_get_ipstr(addr), acl->aclstr);
-        destroy_pool(tmp_pool);
 
-        if (acl->negated)
-          return -1;
+        if (acl->negated) {
+          res = -1;
 
-        return 1;
+        } else {
+          res = 1;
+        }
 
       } else {
         if (acl->negated) {
-          destroy_pool(tmp_pool);
-          return 1;
+          res = 1;
         }
       }
       break;
@@ -182,17 +181,17 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
             "addr '%s' (%s) matched DNS glob rule '%s'",
             pr_netaddr_get_ipstr(addr), pr_netaddr_get_dnsstr(addr),
             acl->aclstr);
-          destroy_pool(tmp_pool);
 
-          if (acl->negated)
-            return -1;
+          if (acl->negated) {
+            res = -1;
 
-          return 1;
+          } else {
+            res = 1;
+          }
 
         } else {
           if (acl->negated) {
-            destroy_pool(tmp_pool);
-            return 1;
+            res = 1;
           }
         }
 
@@ -206,7 +205,7 @@ int pr_netacl_match(pr_netacl_t *acl, pr_netaddr_t *addr) {
   }
 
   destroy_pool(tmp_pool);
-  return 0;
+  return res;
 }
 
 pr_netacl_t *pr_netacl_create(pool *p, char *aclstr) {
@@ -259,8 +258,9 @@ pr_netacl_t *pr_netacl_create(pool *p, char *aclstr) {
     }
 
     acl->addr = pr_netaddr_get_addr(p, aclstr, NULL);
-    if (!acl->addr)
+    if (acl->addr == NULL) {
       return NULL;
+    }
 
     /* Determine what the given bitmask is. */
     acl->masklen = strtol(cp + 1, &tmp, 10);
@@ -392,10 +392,6 @@ pr_netacl_t *pr_netacl_create(pool *p, char *aclstr) {
       acl->type = PR_NETACL_TYPE_DNSGLOB;
       acl->pattern = pstrdup(p, aclstr);
 
-    } else if (*aclstr == '.') {
-      acl->type = PR_NETACL_TYPE_DNSGLOB;
-      acl->pattern = pstrcat(p, "*", aclstr, NULL);
-
     } else {
       acl->type = PR_NETACL_TYPE_DNSMATCH;
       acl->pattern = pstrdup(p, aclstr);
@@ -487,10 +483,11 @@ pr_netacl_t *pr_netacl_create(pool *p, char *aclstr) {
   return acl;
 }
 
-pr_netacl_t *pr_netacl_dup(pool *p, pr_netacl_t *acl) {
+pr_netacl_t *pr_netacl_dup(pool *p, const pr_netacl_t *acl) {
   pr_netacl_t *acl2;
 
-  if (!p || !acl) {
+  if (p == NULL ||
+      acl == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -500,28 +497,33 @@ pr_netacl_t *pr_netacl_dup(pool *p, pr_netacl_t *acl) {
   /* A simple memcpy(3) won't suffice; we need a deep copy. */
   acl2->type = acl->type;
 
-  if (acl->pattern)
+  if (acl->pattern != NULL) {
     acl2->pattern = pstrdup(p, acl->pattern);
+  }
 
   acl2->negated = acl->negated;
 
-  if (acl->addr) {
-    acl2->addr = pr_netaddr_alloc(p);
+  if (acl->addr != NULL) {
+    pr_netaddr_t *addr;
+
+    addr = pr_netaddr_alloc(p);
+    pr_netaddr_set_family(addr, pr_netaddr_get_family(acl->addr));
+    pr_netaddr_set_sockaddr(addr, pr_netaddr_get_sockaddr(acl->addr));
 
-    pr_netaddr_set_family(acl2->addr, pr_netaddr_get_family(acl->addr));
-    pr_netaddr_set_sockaddr(acl2->addr, pr_netaddr_get_sockaddr(acl->addr));
+    acl2->addr = addr;
   }
 
   acl2->masklen = acl->masklen;
 
-  if (acl->aclstr)
+  if (acl->aclstr != NULL) {
     acl2->aclstr = pstrdup(p, acl->aclstr);
+  }
 
   return acl2;
 }
 
-int pr_netacl_get_negated(pr_netacl_t *acl) {
-  if (!acl) {
+int pr_netacl_get_negated(const pr_netacl_t *acl) {
+  if (acl == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -529,10 +531,11 @@ int pr_netacl_get_negated(pr_netacl_t *acl) {
   return acl->negated;
 }
 
-const char *pr_netacl_get_str(pool *p, pr_netacl_t *acl) {
+const char *pr_netacl_get_str2(pool *p, const pr_netacl_t *acl, int flags) {
   char *res = "";
 
-  if (!p || !acl) {
+  if (p == NULL ||
+      acl == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -541,51 +544,73 @@ const char *pr_netacl_get_str(pool *p, pr_netacl_t *acl) {
   switch (acl->type) {
     case PR_NETACL_TYPE_ALL:
       res = pstrcat(p, res, acl->aclstr, NULL);
-      res = pstrcat(p, res, " <all>", NULL);
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        res = pstrcat(p, res, " <all>", NULL);
+      }
       return res;
 
     case PR_NETACL_TYPE_NONE:
       res = pstrcat(p, res, acl->aclstr, NULL);
-      res = pstrcat(p, res, " <none>", NULL);
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        res = pstrcat(p, res, " <none>", NULL);
+      }
       return res;
 
     case PR_NETACL_TYPE_IPMASK: {
-      char masklenstr[64];
-
       res = pstrcat(p, res, acl->aclstr, NULL);
-      memset(masklenstr, '\0', sizeof(masklenstr));
-      snprintf(masklenstr, sizeof(masklenstr)-1, "%u", acl->masklen);
-      res = pstrcat(p, res, " <IP address mask, ", masklenstr, "-bit mask",
-        NULL);
+
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        char masklenstr[64];
+
+        memset(masklenstr, '\0', sizeof(masklenstr));
+        snprintf(masklenstr, sizeof(masklenstr)-1, "%u", acl->masklen);
+        res = pstrcat(p, res, " <IP address mask, ", masklenstr, "-bit mask",
+          NULL);
+      }
       break;
     }
 
     case PR_NETACL_TYPE_IPMATCH:
       res = pstrcat(p, res, acl->aclstr, NULL);
-      res = pstrcat(p, res, " <IP address match", NULL);
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        res = pstrcat(p, res, " <IP address match", NULL);
+      }
       break;
 
     case PR_NETACL_TYPE_DNSMATCH:
       res = pstrcat(p, res, acl->aclstr, NULL);
-      res = pstrcat(p, res, " <DNS hostname match", NULL);
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        res = pstrcat(p, res, " <DNS hostname match", NULL);
+      }
       break;
 
     case PR_NETACL_TYPE_IPGLOB:
       res = pstrcat(p, res, acl->pattern, NULL);
-      res = pstrcat(p, res, " <IP address glob", NULL);
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        res = pstrcat(p, res, " <IP address glob", NULL);
+      }
       break;
 
     case PR_NETACL_TYPE_DNSGLOB:
       res = pstrcat(p, res, acl->pattern, NULL);
-      res = pstrcat(p, res, " <DNS hostname glob", NULL);
+      if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+        res = pstrcat(p, res, " <DNS hostname glob", NULL);
+      }
       break;
   }
 
-  if (!acl->negated)
-    res = pstrcat(p, res, ">", NULL);
-  else
-    res = pstrcat(p, res, ", inverted>", NULL);
+  if (!(flags & PR_NETACL_FL_STR_NO_DESC)) {
+    if (!acl->negated) {
+      res = pstrcat(p, res, ">", NULL);
+
+    } else {
+      res = pstrcat(p, res, ", inverted>", NULL);
+    }
+  }
 
   return res;
 }
 
+const char *pr_netacl_get_str(pool *p, const pr_netacl_t *acl) {
+  return pr_netacl_get_str2(p, acl, 0);
+}
diff --git a/src/netaddr.c b/src/netaddr.c
index 8c6474f..beb6c5a 100644
--- a/src/netaddr.c
+++ b/src/netaddr.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2014 The ProFTPD Project team
+ * Copyright (c) 2003-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Network address routines
- * $Id: netaddr.c,v 1.98 2013-12-23 17:53:42 castaglia Exp $
- */
+/* Network address routines */
 
 #include "conf.h"
 
@@ -70,10 +68,12 @@ static const char *trace_channel = "dns";
 static array_header *netaddr_dnscache_get(pool *p, const char *ip_str) {
   array_header *res = NULL;
 
-  if (netaddr_dnstab) {
-    void *v = pr_table_get(netaddr_dnstab, ip_str, NULL);
-    if (v) {
-      res = v;
+  if (netaddr_dnstab != NULL) {
+    const void *v;
+
+    v = pr_table_get(netaddr_dnstab, ip_str, NULL);
+    if (v != NULL) {
+      res = (array_header *) v;
 
       pr_trace_msg(trace_channel, 4,
         "using %d DNS %s from netaddr DNS cache for IP address '%s'",
@@ -153,10 +153,12 @@ static void netaddr_dnscache_set(const char *ip_str, const char *dns_name) {
 static pr_netaddr_t *netaddr_ipcache_get(pool *p, const char *name) {
   pr_netaddr_t *res = NULL;
 
-  if (netaddr_iptab) {
-    void *v = pr_table_get(netaddr_iptab, name, NULL);
-    if (v) {
-      res = v;
+  if (netaddr_iptab != NULL) {
+    const void *v;
+
+    v = pr_table_get(netaddr_iptab, name, NULL);
+    if (v != NULL) {
+      res = (pr_netaddr_t *) v;
       pr_trace_msg(trace_channel, 4,
         "using IP address '%s' from netaddr IP cache for name '%s'",
         pr_netaddr_get_ipstr(res), name);
@@ -164,7 +166,7 @@ static pr_netaddr_t *netaddr_ipcache_get(pool *p, const char *name) {
       /* We return a copy of the cache's netaddr_t, if the caller provided
        * a pool for duplication.
        */
-      if (p) {
+      if (p != NULL) {
         pr_netaddr_t *dup_res = NULL;
 
         dup_res = pr_netaddr_dup(p, res);
@@ -186,8 +188,8 @@ static pr_netaddr_t *netaddr_ipcache_get(pool *p, const char *name) {
   return NULL;
 }
 
-static int netaddr_ipcache_set(const char *name, pr_netaddr_t *na) {
-  if (netaddr_iptab) {
+static int netaddr_ipcache_set(const char *name, const pr_netaddr_t *na) {
+  if (netaddr_iptab != NULL) {
     int count = 0;
     void *v = NULL;
 
@@ -472,10 +474,11 @@ void pr_netaddr_clear(pr_netaddr_t *na) {
   memset(na, 0, sizeof(pr_netaddr_t));
 }
 
-pr_netaddr_t *pr_netaddr_dup(pool *p, pr_netaddr_t *na) {
+pr_netaddr_t *pr_netaddr_dup(pool *p, const pr_netaddr_t *na) {
   pr_netaddr_t *dup_na;
 
-  if (!p || !na) {
+  if (p == NULL ||
+      na == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -622,6 +625,10 @@ static pr_netaddr_t *get_addr_by_name(pool *p, const char *name,
               "unable to resolve '%s' to an IPv6 address: %s", name,
               pr_gai_strerror(res));
 
+            if (res == EAI_NONAME) {
+              xerrno = ENOENT;
+            }
+
           } else {
             pr_trace_msg(trace_channel, 1,
               "IPv6 getaddrinfo '%s' system error: [%d] %s", name,
@@ -632,10 +639,17 @@ static pr_netaddr_t *get_addr_by_name(pool *p, const char *name,
       } else {
         pr_trace_msg(trace_channel, 1, "IPv4 getaddrinfo '%s' error: %s",
           name, pr_gai_strerror(res));
+
+        if (res == EAI_NONAME) {
+          xerrno = ENOENT;
+        }
       }
 #else
       pr_trace_msg(trace_channel, 1, "IPv4 getaddrinfo '%s' error: %s",
         name, pr_gai_strerror(res));
+      if (res == EAI_NONAME) {
+        xerrno = ENOENT;
+      }
 #endif /* PR_USE_IPV6 */
 
     } else {
@@ -650,7 +664,7 @@ static pr_netaddr_t *get_addr_by_name(pool *p, const char *name,
     }
   }
 
-  if (info) {
+  if (info != NULL) {
     na = (pr_netaddr_t *) pcalloc(p, sizeof(pr_netaddr_t));
 
     /* Copy the first returned addr into na, as the return value. */
@@ -671,6 +685,33 @@ static pr_netaddr_t *get_addr_by_name(pool *p, const char *name,
         pr_netaddr_get_ipstr(na), strerror(errno));
     }
 
+    if (addrs != NULL) {
+      struct addrinfo *next_info = NULL;
+
+      /* Copy any other addrs into the list. */
+      if (*addrs == NULL) {
+        *addrs = make_array(p, 0, sizeof(pr_netaddr_t *));
+      }
+
+      next_info = info->ai_next;
+      while (next_info != NULL) {
+        pr_netaddr_t **elt;
+
+        pr_signals_handle();
+        elt = push_array(*addrs);
+
+        *elt = pcalloc(p, sizeof(pr_netaddr_t));
+        pr_netaddr_set_family(*elt, next_info->ai_family);
+        pr_netaddr_set_sockaddr(*elt, next_info->ai_addr);
+
+        pr_trace_msg(trace_channel, 7, "resolved '%s' to %s address %s", name,
+          next_info->ai_family == AF_INET ? "IPv4" : "IPv6",
+          pr_netaddr_get_ipstr(*elt));
+
+        next_info = next_info->ai_next;
+      }
+    }
+
     pr_freeaddrinfo(info);
   }
 
@@ -714,20 +755,31 @@ static pr_netaddr_t *get_addr_by_name(pool *p, const char *name,
        * address; we don't want to have duplicate addresses in the
        * returned list of additional addresses.
        */
-      if (info &&
-          info->ai_family != pr_netaddr_get_family(na)) {
-        pr_netaddr_t **elt;
+      if (info != NULL) {
+        struct addrinfo *next_info = NULL;
 
-        *addrs = make_array(p, 0, sizeof(pr_netaddr_t *));
-        elt = push_array(*addrs);
+        /* Copy any other addrs into the list. */
+        if (*addrs == NULL) {
+          *addrs = make_array(p, 0, sizeof(pr_netaddr_t *));
+        }
 
-        *elt = pcalloc(p, sizeof(pr_netaddr_t));
-        pr_netaddr_set_family(*elt, info->ai_family);
-        pr_netaddr_set_sockaddr(*elt, info->ai_addr);
+        next_info = info->ai_next;
+        while (next_info != NULL) {
+          pr_netaddr_t **elt;
 
-        pr_trace_msg(trace_channel, 7, "resolved '%s' to %s address %s", name,
-          info->ai_family == AF_INET ? "IPv4" : "IPv6",
-          pr_netaddr_get_ipstr(*elt));
+          pr_signals_handle();
+          elt = push_array(*addrs);
+
+          *elt = pcalloc(p, sizeof(pr_netaddr_t));
+          pr_netaddr_set_family(*elt, next_info->ai_family);
+          pr_netaddr_set_sockaddr(*elt, next_info->ai_addr);
+
+          pr_trace_msg(trace_channel, 7, "resolved '%s' to %s address %s", name,
+            next_info->ai_family == AF_INET ? "IPv4" : "IPv6",
+            pr_netaddr_get_ipstr(*elt));
+
+          next_info = next_info->ai_next;
+        }
 
         pr_freeaddrinfo(info);
       }
@@ -836,7 +888,7 @@ static pr_netaddr_t *get_addr_by_device(pool *p, const char *name,
   return NULL;
 }
 
-pr_netaddr_t *pr_netaddr_get_addr2(pool *p, const char *name,
+const pr_netaddr_t *pr_netaddr_get_addr2(pool *p, const char *name,
     array_header **addrs, unsigned int flags) {
   pr_netaddr_t *na = NULL;
 
@@ -907,13 +959,13 @@ pr_netaddr_t *pr_netaddr_get_addr2(pool *p, const char *name,
   return NULL;
 }
 
-pr_netaddr_t *pr_netaddr_get_addr(pool *p, const char *name,
+const pr_netaddr_t *pr_netaddr_get_addr(pool *p, const char *name,
     array_header **addrs) {
   return pr_netaddr_get_addr2(p, name, addrs, 0);
 }
 
 int pr_netaddr_get_family(const pr_netaddr_t *na) {
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -955,7 +1007,7 @@ int pr_netaddr_set_family(pr_netaddr_t *na, int family) {
 }
 
 size_t pr_netaddr_get_sockaddr_len(const pr_netaddr_t *na) {
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -976,7 +1028,7 @@ size_t pr_netaddr_get_sockaddr_len(const pr_netaddr_t *na) {
 }
 
 size_t pr_netaddr_get_inaddr_len(const pr_netaddr_t *na) {
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -996,7 +1048,7 @@ size_t pr_netaddr_get_inaddr_len(const pr_netaddr_t *na) {
 }
 
 struct sockaddr *pr_netaddr_get_sockaddr(const pr_netaddr_t *na) {
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -1017,7 +1069,8 @@ struct sockaddr *pr_netaddr_get_sockaddr(const pr_netaddr_t *na) {
 }
 
 int pr_netaddr_set_sockaddr(pr_netaddr_t *na, struct sockaddr *addr) {
-  if (!na || !addr) {
+  if (na == NULL ||
+      addr == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -1042,7 +1095,7 @@ int pr_netaddr_set_sockaddr(pr_netaddr_t *na, struct sockaddr *addr) {
 }
 
 int pr_netaddr_set_sockaddr_any(pr_netaddr_t *na) {
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -1077,7 +1130,7 @@ int pr_netaddr_set_sockaddr_any(pr_netaddr_t *na) {
 }
 
 void *pr_netaddr_get_inaddr(const pr_netaddr_t *na) {
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -1151,14 +1204,20 @@ int pr_netaddr_cmp(const pr_netaddr_t *na1, const pr_netaddr_t *na2) {
   pr_netaddr_t *a, *b;
   int res;
 
-  if (na1 && !na2)
+  if (na1 != NULL &&
+      na2 == NULL) {
     return 1;
+  }
 
-  if (!na1 && na2)
+  if (na1 == NULL &&
+      na2 != NULL) {
     return -1;
+  }
 
-  if (!na1 && !na2)
+  if (na1 == NULL &&
+      na2 == NULL) {
     return 0;
+  }
 
   if (pr_netaddr_get_family(na1) != pr_netaddr_get_family(na2)) {
 
@@ -1265,6 +1324,23 @@ static int addr_ncmp(const unsigned char *aptr, const unsigned char *bptr,
   unsigned char nbits, nbytes;
   int res;
 
+  /* These null checks are unlikely to happen.  But be prepared, eh? */
+
+  if (aptr != NULL &&
+      bptr == NULL) {
+    return 1;
+  }
+
+  if (aptr == NULL &&
+      bptr != NULL) {
+    return -1;
+  }
+
+  if (aptr == NULL &&
+      bptr == NULL) {
+    return 0;
+  }
+
   nbytes = masklen / 8;
   nbits = masklen % 8;
 
@@ -1300,15 +1376,18 @@ int pr_netaddr_ncmp(const pr_netaddr_t *na1, const pr_netaddr_t *na2,
   const unsigned char *in1, *in2;
   int res;
 
-  if (na1 && !na2) {
+  if (na1 != NULL &&
+      na2 == NULL) {
     return 1;
   }
 
-  if (!na1 && na2) {
+  if (na1 == NULL &&
+      na2 != NULL) {
     return -1;
   }
 
-  if (!na1 && !na2) {
+  if (na1 == NULL &&
+      na2 == NULL) {
     return 0;
   }
 
@@ -1408,7 +1487,7 @@ int pr_netaddr_ncmp(const pr_netaddr_t *na1, const pr_netaddr_t *na2,
   return res;
 }
 
-int pr_netaddr_fnmatch(pr_netaddr_t *na, const char *pattern, int flags) {
+int pr_netaddr_fnmatch(const pr_netaddr_t *na, const char *pattern, int flags) {
 
   /* Note: I'm still not sure why proftpd bundles an fnmatch(3)
    * implementation rather than using the system library's implementation.
@@ -1419,14 +1498,16 @@ int pr_netaddr_fnmatch(pr_netaddr_t *na, const char *pattern, int flags) {
    */
   int match_flags = PR_FNM_NOESCAPE|PR_FNM_CASEFOLD;
 
-  if (!na || !pattern) {
+  if (na == NULL ||
+      pattern == NULL) {
     errno = EINVAL;
     return -1;
   }
 
   if (flags & PR_NETADDR_MATCH_DNS) {
-    const char *dnsstr = pr_netaddr_get_dnsstr(na);
+    const char *dnsstr;
 
+    dnsstr = pr_netaddr_get_dnsstr(na);
     if (pr_fnmatch(pattern, dnsstr, match_flags) == 0) {
       pr_trace_msg(trace_channel, 6, "DNS name '%s' matches pattern '%s'",
         dnsstr, pattern);
@@ -1435,8 +1516,9 @@ int pr_netaddr_fnmatch(pr_netaddr_t *na, const char *pattern, int flags) {
   }
 
   if (flags & PR_NETADDR_MATCH_IP) {
-    const char *ipstr = pr_netaddr_get_ipstr(na);
+    const char *ipstr;
 
+    ipstr = pr_netaddr_get_ipstr(na);
     if (pr_fnmatch(pattern, ipstr, match_flags) == 0) {
       pr_trace_msg(trace_channel, 6, "IP address '%s' matches pattern '%s'",
         ipstr, pattern);
@@ -1475,15 +1557,16 @@ int pr_netaddr_fnmatch(pr_netaddr_t *na, const char *pattern, int flags) {
   return FALSE;
 }
 
-const char *pr_netaddr_get_ipstr(pr_netaddr_t *na) {
+const char *pr_netaddr_get_ipstr(const pr_netaddr_t *na) {
 #ifdef PR_USE_IPV6
   char buf[INET6_ADDRSTRLEN];
 #else
   char buf[INET_ADDRSTRLEN];
 #endif /* PR_USE_IPV6 */
   int res = 0, xerrno;
+  pr_netaddr_t *addr;
   
-  if (!na) {
+  if (na == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -1491,8 +1574,9 @@ const char *pr_netaddr_get_ipstr(pr_netaddr_t *na) {
   /* If this pr_netaddr_t has already been resolved to an IP string, return the
    * cached string.
    */
-  if (na->na_have_ipstr)
+  if (na->na_have_ipstr) {
     return na->na_ipstr;
+  }
 
   memset(buf, '\0', sizeof(buf));
   res = pr_getnameinfo(pr_netaddr_get_sockaddr(na),
@@ -1529,17 +1613,21 @@ const char *pr_netaddr_get_ipstr(pr_netaddr_t *na) {
 #endif /* PR_USE_IPV6 */
 
   /* Copy the string into the pr_netaddr_t cache as well, so we only
-   * have to do this once for this pr_netaddr_t.
+   * have to do this once for this pr_netaddr_t.  But to do this, we need
+   * let the compiler know that the pr_netaddr_t is not really const at this
+   * point.
    */
-  memset(na->na_ipstr, '\0', sizeof(na->na_ipstr));
-  sstrncpy(na->na_ipstr, buf, sizeof(na->na_ipstr));
-  na->na_have_ipstr = TRUE;
+  addr = (pr_netaddr_t *) na;
+  memset(addr->na_ipstr, '\0', sizeof(addr->na_ipstr));
+  sstrncpy(addr->na_ipstr, buf, sizeof(addr->na_ipstr));
+  addr->na_have_ipstr = TRUE;
 
   return na->na_ipstr;
 }
 
 #if defined(HAVE_GETADDRINFO) && !defined(HAVE_GETHOSTBYNAME2)
-static int netaddr_get_dnsstr_getaddrinfo(pr_netaddr_t *na, const char *name) {
+static int netaddr_get_dnsstr_getaddrinfo(const pr_netaddr_t *na,
+    const char *name) {
   struct addrinfo hints, *info = NULL;
   int family, flags = 0, res = 0, ok = FALSE;
   void *inaddr = pr_netaddr_get_inaddr(na);
@@ -1677,14 +1765,20 @@ static int netaddr_get_dnsstr_getaddrinfo(pr_netaddr_t *na, const char *name) {
 #endif /* HAVE_GETADDRINFO and not HAVE_GETHOSTBYNAME2 */
 
 #ifdef HAVE_GETHOSTBYNAME2
-static int netaddr_get_dnsstr_gethostbyname(pr_netaddr_t *na,
+static int netaddr_get_dnsstr_gethostbyname(const pr_netaddr_t *na,
     const char *name) {
   char **checkaddr;
   struct hostent *hent = NULL;
-  int ok = FALSE;
-  int family = pr_netaddr_get_family(na);
-  void *inaddr = pr_netaddr_get_inaddr(na);
-    
+  int family, ok = FALSE;
+  void *inaddr;
+
+  family = pr_netaddr_get_family(na);
+  if (family < 0) {
+    return -1;
+  }
+
+  inaddr = pr_netaddr_get_inaddr(na);
+
   if (pr_netaddr_is_v4mappedv6(na) == TRUE) {
     family = AF_INET;
     inaddr = get_v4inaddr(na);
@@ -1762,9 +1856,9 @@ static int netaddr_get_dnsstr_gethostbyname(pr_netaddr_t *na,
  * returns a string of the numeric form of the given network address, whereas
  * this function returns a string of the DNS name (if present).
  */
-const char *pr_netaddr_get_dnsstr(pr_netaddr_t *na) {
-  char *name = NULL;
-  pr_netaddr_t *cache = NULL;
+const char *pr_netaddr_get_dnsstr(const pr_netaddr_t *na) {
+  char dns_buf[1024], *name = NULL;
+  pr_netaddr_t *addr = NULL, *cache = NULL;
 
   if (na == NULL) {
     errno = EINVAL;
@@ -1774,9 +1868,10 @@ const char *pr_netaddr_get_dnsstr(pr_netaddr_t *na) {
   cache = netaddr_ipcache_get(NULL, pr_netaddr_get_ipstr(na));
   if (cache &&
       cache->na_have_dnsstr) {
-    memset(na->na_dnsstr, '\0', sizeof(na->na_dnsstr));
-    sstrncpy(na->na_dnsstr, cache->na_dnsstr, sizeof(na->na_dnsstr));
-    na->na_have_dnsstr = TRUE;
+    addr = (pr_netaddr_t *) na;
+    memset(addr->na_dnsstr, '\0', sizeof(addr->na_dnsstr));
+    sstrncpy(addr->na_dnsstr, cache->na_dnsstr, sizeof(addr->na_dnsstr));
+    addr->na_have_dnsstr = TRUE;
 
     return na->na_dnsstr;
   }
@@ -1789,17 +1884,17 @@ const char *pr_netaddr_get_dnsstr(pr_netaddr_t *na) {
   }
 
   if (reverse_dns) {
-    char buf[256];
     int res = 0;
 
     pr_trace_msg(trace_channel, 3,
       "verifying DNS name for IP address %s via reverse DNS lookup",
       pr_netaddr_get_ipstr(na));
 
-    memset(buf, '\0', sizeof(buf));
+    memset(dns_buf, '\0', sizeof(dns_buf));
     res = pr_getnameinfo(pr_netaddr_get_sockaddr(na),
-      pr_netaddr_get_sockaddr_len(na), buf, sizeof(buf), NULL, 0, NI_NAMEREQD);
-    buf[sizeof(buf)-1] = '\0';
+      pr_netaddr_get_sockaddr_len(na), dns_buf, sizeof(dns_buf), NULL, 0,
+      NI_NAMEREQD);
+    dns_buf[sizeof(dns_buf)-1] = '\0';
 
     if (res == 0) {
       /* Some older glibc's getaddrinfo(3) does not appear to handle IPv6
@@ -1807,12 +1902,12 @@ const char *pr_netaddr_get_dnsstr(pr_netaddr_t *na) {
        * which have it, for such older systems.
        */
 #ifdef HAVE_GETHOSTBYNAME2
-      res = netaddr_get_dnsstr_gethostbyname(na, buf);
+      res = netaddr_get_dnsstr_gethostbyname(na, dns_buf);
 #else
-      res = netaddr_get_dnsstr_getaddrinfo(na, buf);
+      res = netaddr_get_dnsstr_getaddrinfo(na, dns_buf);
 #endif /* HAVE_GETHOSTBYNAME2 */
       if (res == 0) {
-        name = buf;
+        name = dns_buf;
         pr_trace_msg(trace_channel, 8,
           "using DNS name '%s' for IP address '%s'", name,
           pr_netaddr_get_ipstr(na));
@@ -1838,11 +1933,14 @@ const char *pr_netaddr_get_dnsstr(pr_netaddr_t *na) {
   }
 
   /* Copy the string into the pr_netaddr_t cache as well, so we only
-   * have to do this once for this pr_netaddr_t.
+   * have to do this once for this pr_netaddr_t.  But to do this, we need
+   * let the compiler know that the pr_netaddr_t is not really const at this
+   * point.
    */
-  memset(na->na_dnsstr, '\0', sizeof(na->na_dnsstr));
-  sstrncpy(na->na_dnsstr, name, sizeof(na->na_dnsstr));
-  na->na_have_dnsstr = TRUE;
+  addr = (pr_netaddr_t *) na;
+  memset(addr->na_dnsstr, '\0', sizeof(addr->na_dnsstr));
+  sstrncpy(addr->na_dnsstr, name, sizeof(addr->na_dnsstr));
+  addr->na_have_dnsstr = TRUE;
 
   /* Update the netaddr object in the cache with the resolved DNS names. */
   netaddr_ipcache_set(name, na);
@@ -1851,7 +1949,7 @@ const char *pr_netaddr_get_dnsstr(pr_netaddr_t *na) {
   return na->na_dnsstr;
 }
 
-array_header *pr_netaddr_get_dnsstr_list(pool *p, pr_netaddr_t *na) {
+array_header *pr_netaddr_get_dnsstr_list(pool *p, const pr_netaddr_t *na) {
   array_header *res;
 
   if (p == NULL ||
@@ -1878,6 +1976,7 @@ array_header *pr_netaddr_get_dnsstr_list(pool *p, pr_netaddr_t *na) {
 /* Return the hostname (wrapper for gethostname(2), except returns FQDN). */
 const char *pr_netaddr_get_localaddr_str(pool *p) {
   char buf[256];
+  int res, xerrno;
 
   if (p == NULL) {
     errno = EINVAL;
@@ -1889,7 +1988,10 @@ const char *pr_netaddr_get_localaddr_str(pool *p) {
   }
 
   memset(buf, '\0', sizeof(buf));
-  if (gethostname(buf, sizeof(buf)-1) != -1) {
+  res = gethostname(buf, sizeof(buf)-1);
+  xerrno = errno;
+
+  if (res >= 0) {
     struct hostent *host;
 
     buf[sizeof(buf)-1] = '\0';
@@ -1898,14 +2000,28 @@ const char *pr_netaddr_get_localaddr_str(pool *p) {
      * that function, for it is possible that the configured hostname for
      * a machine only resolves to an IPv6 address.
      */
+#ifdef HAVE_GETHOSTBYNAME2
+    host = gethostbyname2(buf, AF_INET);
+    if (host == NULL &&
+        h_errno == HOST_NOT_FOUND) {
+# ifdef AF_INET6
+      host = gethostbyname2(buf, AF_INET6);
+# endif /* AF_INET6 */
+    }
+#else
     host = gethostbyname(buf);
-    if (host)
+#endif
+    if (host != NULL) {
       return pr_netaddr_validate_dns_str(pstrdup(p, host->h_name));
+    }
 
+    pr_trace_msg(trace_channel, 14,
+      "gethostbyname() failed for '%s': %s", buf, hstrerror(h_errno));
     return pr_netaddr_validate_dns_str(pstrdup(p, buf));
   }
 
-  pr_trace_msg(trace_channel, 1, "gethostname(2) error: %s", strerror(errno));
+  pr_trace_msg(trace_channel, 1, "gethostname(2) error: %s", strerror(xerrno));
+  errno = xerrno;
   return NULL;
 }
 
@@ -2201,7 +2317,36 @@ pr_netaddr_t *pr_netaddr_v6tov4(pool *p, const pr_netaddr_t *na) {
   return res;
 }
 
-pr_netaddr_t *pr_netaddr_get_sess_local_addr(void) {
+pr_netaddr_t *pr_netaddr_v4tov6(pool *p, const pr_netaddr_t *na) {
+  pr_netaddr_t *res;
+
+  if (p == NULL ||
+      na == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (pr_netaddr_get_family(na) != AF_INET) {
+    errno = EPERM;
+    return NULL;
+  }
+
+#ifdef PR_USE_IPV6
+  res = (pr_netaddr_t *) pr_netaddr_get_addr(p,
+    pstrcat(p, "::ffff:", pr_netaddr_get_ipstr(na), NULL), NULL);
+  if (res != NULL) {
+    pr_netaddr_set_port(res, pr_netaddr_get_port(na));
+  }
+
+#else
+  errno = EPERM;
+  res = NULL;
+#endif /* PR_USE_IPV6 */
+
+  return res;
+}
+
+const pr_netaddr_t *pr_netaddr_get_sess_local_addr(void) {
   if (have_sess_local_addr) {
     return &sess_local_addr;
   }
@@ -2210,7 +2355,7 @@ pr_netaddr_t *pr_netaddr_get_sess_local_addr(void) {
   return NULL;
 }
 
-pr_netaddr_t *pr_netaddr_get_sess_remote_addr(void) {
+const pr_netaddr_t *pr_netaddr_get_sess_remote_addr(void) {
   if (have_sess_remote_addr) {
     return &sess_remote_addr;
   }
@@ -2286,6 +2431,18 @@ void pr_netaddr_clear_cache(void) {
   }
 }
 
+void pr_netaddr_clear_dnscache(const char *ip_str) {
+  if (netaddr_dnstab != NULL) {
+    (void) pr_table_remove(netaddr_dnstab, ip_str, NULL);
+  }
+}
+
+void pr_netaddr_clear_ipcache(const char *name) {
+  if (netaddr_iptab != NULL) {
+    (void) pr_table_remove(netaddr_iptab, name, NULL);
+  }
+}
+
 void init_netaddr(void) {
   if (netaddr_pool) {
     pr_netaddr_clear_cache();
diff --git a/src/netio.c b/src/netio.c
index 88170df..d0adec1 100644
--- a/src/netio.c
+++ b/src/netio.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2014 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* NetIO routines
- * $Id: netio.c,v 1.63 2014-01-06 06:57:16 castaglia Exp $
- */
+/* NetIO routines */
 
 #include "conf.h"
 
@@ -88,6 +86,7 @@ static pr_netio_stream_t *netio_stream_alloc(pool *parent_pool) {
   }
 
   netio_pool = make_sub_pool(parent_pool);
+  pr_pool_tag(netio_pool, "netio stream pool");
   nstrm = pcalloc(netio_pool, sizeof(pr_netio_stream_t));
 
   nstrm->strm_pool = netio_pool;
@@ -193,11 +192,13 @@ static int core_netio_poll_cb(pr_netio_stream_t *nstrm) {
   while (res < 0) {
     int xerrno = errno;
 
-    /* Watch for EAGAIN, and handle it by delaying temporarily. */
-    if (xerrno == EAGAIN) {
-      errno = EINTR;
-      pr_signals_handle();
-      continue;
+    if (!(nstrm->strm_flags & PR_NETIO_SESS_INTR)) {
+      /* Watch for EAGAIN, and handle it by delaying temporarily. */
+      if (xerrno == EAGAIN) {
+        errno = EINTR;
+        pr_signals_handle();
+        continue;
+      }
     }
 
     errno = nstrm->strm_errno = xerrno;
@@ -238,30 +239,78 @@ static int core_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
   return write(nstrm->strm_fd, buf, buflen);
 }
 
-/* NetIO API wrapper functions.
- */
+static const char *netio_stream_mode(int strm_mode) {
+  const char *modestr = "(unknown)";
+
+  switch (strm_mode) {
+    case PR_NETIO_IO_RD:
+      modestr = "reading";
+      break;
+
+    case PR_NETIO_IO_WR:
+      modestr = "writing";
+      break;
+
+    default:
+      break;
+  }
+
+  return modestr;
+}
+
+/* NetIO API wrapper functions. */
 
 void pr_netio_abort(pr_netio_stream_t *nstrm) {
+  const char *nstrm_mode;
 
   if (nstrm == NULL) {
     errno = EINVAL;
     return;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   switch (nstrm->strm_type) {
     case PR_NETIO_STRM_CTRL:
-      ctrl_netio ? (ctrl_netio->abort)(nstrm) :
+      if (ctrl_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s abort() for control %s stream",
+          ctrl_netio->owner_name, nstrm_mode);
+        (ctrl_netio->abort)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s abort() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
         (default_ctrl_netio->abort)(nstrm);
+      }
+
       break;
 
     case PR_NETIO_STRM_DATA:
-      data_netio ? (data_netio->abort)(nstrm) :
+      if (data_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s abort() for data %s stream",
+          data_netio->owner_name, nstrm_mode);
+        (data_netio->abort)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s abort() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
         (default_data_netio->abort)(nstrm);
+      }
       break;
 
     case PR_NETIO_STRM_OTHR:
-      othr_netio ? (othr_netio->abort)(nstrm) :
+      if (othr_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s abort() for other %s stream",
+          othr_netio->owner_name, nstrm_mode);
+        (othr_netio->abort)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s abort() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
         (default_othr_netio->abort)(nstrm);
+      }
       break;
 
     default:
@@ -273,51 +322,107 @@ void pr_netio_abort(pr_netio_stream_t *nstrm) {
 }
 
 int pr_netio_close(pr_netio_stream_t *nstrm) {
-  int res = -1;
+  int res = -1, xerrno = 0;
+  const char *nstrm_mode;
 
   if (nstrm == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   switch (nstrm->strm_type) {
     case PR_NETIO_STRM_CTRL:
-      res = ctrl_netio ? (ctrl_netio->close)(nstrm) :
-        (default_ctrl_netio->close)(nstrm);
-      destroy_pool(nstrm->strm_pool);
-      return res;
+      if (ctrl_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s close() for control %s stream", ctrl_netio->owner_name,
+          nstrm_mode);
+        res = (ctrl_netio->close)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s close() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
+        res = (default_ctrl_netio->close)(nstrm);
+      }
+      xerrno = errno;
+      break;
 
     case PR_NETIO_STRM_DATA:
-      res = data_netio ? (data_netio->close)(nstrm) :
-        (default_data_netio->close)(nstrm);
-      destroy_pool(nstrm->strm_pool);
-      return res;
+      if (data_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
+          data_netio->owner_name, nstrm_mode);
+        res = (data_netio->close)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
+        res = (default_data_netio->close)(nstrm);
+      }
+      xerrno = errno;
+      break;
 
     case PR_NETIO_STRM_OTHR:
-      res = othr_netio ? (othr_netio->close)(nstrm) :
-        (default_othr_netio->close)(nstrm);
-      destroy_pool(nstrm->strm_pool);
-      return res;
+      if (othr_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
+          othr_netio->owner_name, nstrm_mode);
+        res = (othr_netio->close)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
+        res = (default_othr_netio->close)(nstrm);
+      }
+      xerrno = errno;
+      break;
+
+    default:
+      errno = EPERM;
+      return -1;
   }
 
-  errno = EPERM;
+  /* Make sure to scrub any buffered memory, too. */
+  if (nstrm->strm_buf != NULL) {
+    pr_buffer_t *pbuf;
+
+    pbuf = nstrm->strm_buf;
+    pr_memscrub(pbuf->buf, pbuf->buflen);
+  }
+
+  destroy_pool(nstrm->strm_pool);
+  errno = xerrno;
   return res;
 }
 
 static int netio_lingering_close(pr_netio_stream_t *nstrm, long linger,
     int flags) {
   int res;
+  const char *nstrm_mode;
 
   if (nstrm == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  switch (nstrm->strm_type) {
+    case PR_NETIO_STRM_CTRL:
+    case PR_NETIO_STRM_DATA:
+    case PR_NETIO_STRM_OTHR:
+      break;
+
+    default:
+      errno = EPERM;
+      return -1;
+  }
+
   if (nstrm->strm_fd < 0) {
     /* Already closed. */
     return 0;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   if (!(flags & NETIO_LINGERING_CLOSE_FL_NO_SHUTDOWN)) {
     pr_netio_shutdown(nstrm, 1);
   }
@@ -341,7 +446,8 @@ static int netio_lingering_close(pr_netio_stream_t *nstrm, long linger,
       FD_SET(nstrm->strm_fd, &rfds);
 
       pr_trace_msg(trace_channel, 8,
-        "lingering %lu secs before closing fd %d", (unsigned long) tv.tv_sec,
+        "lingering %lu %s before closing fd %d",
+        (unsigned long) tv.tv_sec, tv.tv_sec != 1 ? "secs" : "sec",
         nstrm->strm_fd);
 
       res = select(nstrm->strm_fd+1, &rfds, NULL, NULL, &tv);
@@ -381,16 +487,45 @@ static int netio_lingering_close(pr_netio_stream_t *nstrm, long linger,
 
   switch (nstrm->strm_type) {
     case PR_NETIO_STRM_CTRL:
-      return ctrl_netio ? (ctrl_netio->close)(nstrm) :
-        (default_ctrl_netio->close)(nstrm);
+      if (ctrl_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s close() for control %s stream", ctrl_netio->owner_name,
+          nstrm_mode);
+        res = (ctrl_netio->close)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s close() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
+        res = (default_ctrl_netio->close)(nstrm);
+      }
+      return res;
 
     case PR_NETIO_STRM_DATA:
-      return data_netio ? (data_netio->close)(nstrm) :
-        (default_data_netio->close)(nstrm);
+      if (data_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
+          data_netio->owner_name, nstrm_mode);
+        res = (data_netio->close)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s close() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
+        res = (default_data_netio->close)(nstrm);
+      }
+      return res;
 
     case PR_NETIO_STRM_OTHR:
-      return othr_netio ? (othr_netio->close)(nstrm) :
-        (default_othr_netio->close)(nstrm);
+      if (othr_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
+          othr_netio->owner_name, nstrm_mode);
+        res = (othr_netio->close)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s close() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
+        res = (default_othr_netio->close)(nstrm);
+      }
+      return res;
   }
 
   errno = EPERM;
@@ -405,6 +540,17 @@ int pr_netio_lingering_abort(pr_netio_stream_t *nstrm, long linger) {
     return -1;
   }
 
+  switch (nstrm->strm_type) {
+    case PR_NETIO_STRM_CTRL:
+    case PR_NETIO_STRM_DATA:
+    case PR_NETIO_STRM_OTHR:
+      break;
+
+    default:
+      errno = EPERM;
+      return -1;
+  }
+
   /* Send an appropriate response code down the stream asychronously. */
   pr_response_send_async(R_426, _("Transfer aborted. Data connection closed."));
 
@@ -444,6 +590,8 @@ int pr_netio_lingering_abort(pr_netio_stream_t *nstrm, long linger) {
     }
   }
 
+  nstrm->strm_flags |= PR_NETIO_SESS_ABORT;
+
   /* Now continue with a normal lingering close. */
   return netio_lingering_close(nstrm, linger,
     NETIO_LINGERING_CLOSE_FL_NO_SHUTDOWN);  
@@ -456,6 +604,7 @@ int pr_netio_lingering_close(pr_netio_stream_t *nstrm, long linger) {
 pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
     int mode) {
   pr_netio_stream_t *nstrm = NULL;
+  const char *nstrm_mode;
 
   if (parent_pool == NULL) {
     errno = EINVAL;
@@ -464,6 +613,7 @@ pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
 
   /* Create a new stream object, then pass that the NetIO open handler. */
   nstrm = netio_stream_alloc(parent_pool);
+  nstrm_mode = netio_stream_mode(mode);
 
   switch (strm_type) {
     case PR_NETIO_STRM_CTRL:
@@ -471,13 +621,25 @@ pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
       nstrm->strm_mode = mode;
 
       if (ctrl_netio != NULL) {
-        pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
-          ctrl_netio, sizeof(pr_netio_t *));
+        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
+            ctrl_netio, sizeof(pr_netio_t *)) < 0) {
+          pr_trace_msg(trace_channel, 9,
+            "error stashing 'core.netio' note for ctrl stream: %s",
+            strerror(errno));
+        }
+        pr_trace_msg(trace_channel, 19, "using %s open() for control %s stream",
+          ctrl_netio->owner_name, nstrm_mode);
         return (ctrl_netio->open)(nstrm, fd, mode);
 
       } else {
-        pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
-          default_ctrl_netio, sizeof(pr_netio_t *));
+        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
+            default_ctrl_netio, sizeof(pr_netio_t *)) < 0) {
+          pr_trace_msg(trace_channel, 9,
+            "error stashing 'core.netio' note for ctrl stream: %s",
+            strerror(errno));
+        }
+        pr_trace_msg(trace_channel, 19, "using %s open() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
         return (default_ctrl_netio->open)(nstrm, fd, mode);
       }
 
@@ -486,13 +648,25 @@ pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
       nstrm->strm_mode = mode;
 
       if (data_netio != NULL) {
-        pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
-          data_netio, sizeof(pr_netio_t *));
+        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
+            data_netio, sizeof(pr_netio_t *)) < 0) {
+          pr_trace_msg(trace_channel, 9,
+            "error stashing 'core.netio' note for data stream: %s",
+            strerror(errno));
+        }
+        pr_trace_msg(trace_channel, 19, "using %s open() for data %s stream",
+          data_netio->owner_name, nstrm_mode);
         return (data_netio->open)(nstrm, fd, mode);
 
       } else {
-        pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
-          default_data_netio, sizeof(pr_netio_t *));
+        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
+            default_data_netio, sizeof(pr_netio_t *)) < 0) {
+          pr_trace_msg(trace_channel, 9,
+            "error stashing 'core.netio' note for data stream: %s",
+            strerror(errno));
+        }
+        pr_trace_msg(trace_channel, 19, "using %s open() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
         return (default_data_netio->open)(nstrm, fd, mode);
       }
 
@@ -501,13 +675,25 @@ pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
       nstrm->strm_mode = mode;
 
       if (othr_netio != NULL) {
-        pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
-          othr_netio, sizeof(pr_netio_t *));
+        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
+            othr_netio, sizeof(pr_netio_t *)) < 0) {
+          pr_trace_msg(trace_channel, 9,
+            "error stashing 'core.netio' note for othr stream: %s",
+            strerror(errno));
+        }
+        pr_trace_msg(trace_channel, 19, "using %s open() for other %s stream",
+          othr_netio->owner_name, nstrm_mode);
         return (othr_netio->open)(nstrm, fd, mode);
 
       } else {
-        pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
-          default_othr_netio, sizeof(pr_netio_t *));
+        if (pr_table_add(nstrm->notes, pstrdup(nstrm->strm_pool, "core.netio"),
+            default_othr_netio, sizeof(pr_netio_t *)) < 0) {
+          pr_trace_msg(trace_channel, 9,
+            "error stashing 'core.netio' note for othr stream: %s",
+            strerror(errno));
+        }
+        pr_trace_msg(trace_channel, 19, "using %s open() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
         return (default_othr_netio->open)(nstrm, fd, mode);
       }
   }
@@ -520,24 +706,57 @@ pr_netio_stream_t *pr_netio_open(pool *parent_pool, int strm_type, int fd,
 }
 
 pr_netio_stream_t *pr_netio_reopen(pr_netio_stream_t *nstrm, int fd, int mode) {
+  pr_netio_stream_t *res;
+  const char *nstrm_mode;
 
   if (nstrm == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
+  nstrm_mode = netio_stream_mode(mode);
+
   switch (nstrm->strm_type) {
     case PR_NETIO_STRM_CTRL:
-      return ctrl_netio ? (ctrl_netio->reopen)(nstrm, fd, mode) :
-        (default_ctrl_netio->reopen)(nstrm, fd, mode);
+      if (ctrl_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s reopen() for control %s stream", ctrl_netio->owner_name,
+          nstrm_mode);
+        res = (ctrl_netio->reopen)(nstrm, fd, mode);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s reopen() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
+        res = (default_ctrl_netio->reopen)(nstrm, fd, mode);
+      }
+      return res;
 
     case PR_NETIO_STRM_DATA:
-      return data_netio ? (data_netio->reopen)(nstrm, fd, mode) :
-        (default_data_netio->reopen)(nstrm, fd, mode);
+      if (data_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s reopen() for data %s stream",
+          data_netio->owner_name, nstrm_mode);
+        res = (data_netio->reopen)(nstrm, fd, mode);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s reopen() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
+        res = (default_data_netio->reopen)(nstrm, fd, mode);
+      }
+      return res;
 
     case PR_NETIO_STRM_OTHR:
-      return othr_netio ? (othr_netio->reopen)(nstrm, fd, mode) :
-        (default_othr_netio->reopen)(nstrm, fd, mode);
+      if (othr_netio != NULL) {
+        pr_trace_msg(trace_channel, 19, "using %s reopen() for other %s stream",
+          othr_netio->owner_name, nstrm_mode);
+        res = (othr_netio->reopen)(nstrm, fd, mode);
+
+      } else {
+        pr_trace_msg(trace_channel, 19, "using %s reopen() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
+        res = (default_othr_netio->reopen)(nstrm, fd, mode);
+      }
+      return res;
   }
 
   errno = EPERM;
@@ -555,9 +774,7 @@ void pr_netio_reset_poll_interval(pr_netio_stream_t *nstrm) {
 }
 
 void pr_netio_set_poll_interval(pr_netio_stream_t *nstrm, unsigned int secs) {
-
   if (nstrm == NULL) {
-    errno = EINVAL;
     return;
   }
 
@@ -567,6 +784,7 @@ void pr_netio_set_poll_interval(pr_netio_stream_t *nstrm, unsigned int secs) {
 
 int pr_netio_poll(pr_netio_stream_t *nstrm) {
   int res = 0, xerrno = 0;
+  const char *nstrm_mode;
 
   /* Sanity checks. */
   if (nstrm == NULL) {
@@ -585,106 +803,179 @@ int pr_netio_poll(pr_netio_stream_t *nstrm) {
     return 1;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   while (TRUE) {
     run_schedule();
     pr_signals_handle();
 
     switch (nstrm->strm_type) {
       case PR_NETIO_STRM_CTRL:
-        res = ctrl_netio ? (ctrl_netio->poll)(nstrm) :
-          (default_ctrl_netio->poll)(nstrm);
-        break;
+        if (ctrl_netio != NULL) {
+          pr_trace_msg(trace_channel, 19,
+            "using %s poll() for control %s stream", ctrl_netio->owner_name,
+            nstrm_mode);
+          res = (ctrl_netio->poll)(nstrm);
 
-      case PR_NETIO_STRM_DATA:
-        res = data_netio ? (data_netio->poll)(nstrm) :
-          (default_data_netio->poll)(nstrm);
-        break;
-
-      case PR_NETIO_STRM_OTHR:
-        res = othr_netio ? (othr_netio->poll)(nstrm) :
-          (default_othr_netio->poll)(nstrm);
+        } else {
+          pr_trace_msg(trace_channel, 19,
+            "using %s poll() for control %s stream",
+            default_ctrl_netio->owner_name, nstrm_mode);
+          res = (default_ctrl_netio->poll)(nstrm);
+        }
         break;
-    }
 
-    switch (res) {
-      case -1:
-        xerrno = errno;
-        if (xerrno == EINTR) {
-          if (nstrm->strm_flags & PR_NETIO_SESS_ABORT) {
-            nstrm->strm_flags &= ~PR_NETIO_SESS_ABORT;
-            return 1;
-          }
+      case PR_NETIO_STRM_DATA:
+        if (data_netio != NULL) {
+          pr_trace_msg(trace_channel, 19,
+            "using %s poll() for data %s stream", data_netio->owner_name,
+            nstrm_mode);
+          res = (data_netio->poll)(nstrm);
 
-	  /* Otherwise, restart the call */
-          pr_signals_handle();
-          continue;
+        } else {
+          pr_trace_msg(trace_channel, 19,
+            "using %s poll() for data %s stream",
+            default_data_netio->owner_name, nstrm_mode);
+          res = (default_data_netio->poll)(nstrm);
         }
+        break;
 
-        /* Some other error occured */
-        nstrm->strm_errno = xerrno;
+      case PR_NETIO_STRM_OTHR:
+        if (othr_netio != NULL) {
+          pr_trace_msg(trace_channel, 19,
+            "using %s poll() for other %s stream", othr_netio->owner_name,
+            nstrm_mode);
+          res = (othr_netio->poll)(nstrm);
 
-        /* If this is the control stream, and the error indicates a
-         * broken pipe (i.e. the client went away), AND there is a data
-         * transfer is progress, abort the transfer.
-         */
-        if (xerrno == EPIPE &&
-            nstrm->strm_type == PR_NETIO_STRM_CTRL &&
-            (session.sf_flags & SF_XFER)) {
-          pr_trace_msg(trace_channel, 5,
-            "received EPIPE on control connection, setting 'aborted' "
-            "session flag");
-          session.sf_flags |= SF_ABORT;
+        } else {
+          pr_trace_msg(trace_channel, 19,
+            "using %s poll() for other %s stream",
+            default_othr_netio->owner_name, nstrm_mode);
+          res = (default_othr_netio->poll)(nstrm);
         }
+        break;
+    }
 
-        errno = nstrm->strm_errno;
-        return -1;
-
-      case 0:
-        /* In case the kernel doesn't support interrupted syscalls. */
+    if (res == -1) {
+      xerrno = errno;
+      if (xerrno == EINTR) {
         if (nstrm->strm_flags & PR_NETIO_SESS_ABORT) {
           nstrm->strm_flags &= ~PR_NETIO_SESS_ABORT;
           return 1;
         }
 
-        /* If the stream has been marked as "interruptible", AND the
-         * poll interval is zero seconds (meaning a true poll, not blocking),
-         * then return here.
-         */
-        if ((nstrm->strm_flags & PR_NETIO_SESS_INTR) &&
-            nstrm->strm_interval == 0) {
-          errno = EOF;
+        if (nstrm->strm_flags & PR_NETIO_SESS_INTR) {
+          errno = nstrm->strm_errno = xerrno;
           return -1;
         }
 
+        /* Otherwise, restart the call */
+        pr_signals_handle();
         continue;
+      }
 
-      default:
-        return 0;
+      /* Some other error occured */
+      nstrm->strm_errno = xerrno;
+
+      /* If this is the control stream, and the error indicates a
+       * broken pipe (i.e. the client went away), AND there is a data
+       * transfer is progress, abort the transfer.
+       */
+      if (xerrno == EPIPE &&
+          nstrm->strm_type == PR_NETIO_STRM_CTRL &&
+          (session.sf_flags & SF_XFER)) {
+        pr_trace_msg(trace_channel, 5,
+          "received EPIPE on control connection, setting 'aborted' "
+          "session flag");
+        session.sf_flags |= SF_ABORT;
+      }
+
+      errno = nstrm->strm_errno;
+      return -1;
     }
+
+    if (res == 0) {
+      /* In case the kernel doesn't support interrupted syscalls. */
+      if (nstrm->strm_flags & PR_NETIO_SESS_ABORT) {
+        nstrm->strm_flags &= ~PR_NETIO_SESS_ABORT;
+        return 1;
+      }
+
+      /* If the stream has been marked as "interruptible", AND the
+       * poll interval is zero seconds (meaning a true poll, not blocking),
+       * then return here.
+       */
+      if ((nstrm->strm_flags & PR_NETIO_SESS_INTR) &&
+          nstrm->strm_interval == 0) {
+        errno = EOF;
+        return -1;
+      }
+
+      continue;
+    }
+
+    break;
   }
 
-  /* This will never be reached. */
-  return -1;
+  return 0;
 }
 
 int pr_netio_postopen(pr_netio_stream_t *nstrm) {
+  int res;
+  const char *nstrm_mode;
+
   if (nstrm == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   switch (nstrm->strm_type) {
     case PR_NETIO_STRM_CTRL:
-      return ctrl_netio ? (ctrl_netio->postopen)(nstrm) :
-        (default_ctrl_netio->postopen)(nstrm);
+      if (ctrl_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s postopen() for control %s stream", ctrl_netio->owner_name,
+          nstrm_mode);
+        res = (ctrl_netio->postopen)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s postopen() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
+        res = (default_ctrl_netio->postopen)(nstrm);
+      }
+      return res;
 
     case PR_NETIO_STRM_DATA:
-      return data_netio ? (data_netio->postopen)(nstrm) :
-        (default_data_netio->postopen)(nstrm);
+      if (data_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s postopen() for data %s stream", data_netio->owner_name,
+          nstrm_mode);
+        res = (data_netio->postopen)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s postopen() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
+        res = (default_data_netio->postopen)(nstrm);
+      }
+      return res;
 
     case PR_NETIO_STRM_OTHR:
-      return othr_netio ? (othr_netio->postopen)(nstrm) :
-        (default_othr_netio->postopen)(nstrm);
+      if (othr_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s postopen() for other %s stream", othr_netio->owner_name,
+          nstrm_mode);
+        res = (othr_netio->postopen)(nstrm);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s postopen() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
+        res = (default_othr_netio->postopen)(nstrm);
+      }
+      return res;
   }
 
   errno = EPERM;
@@ -735,11 +1026,14 @@ int pr_netio_printf_async(pr_netio_stream_t *nstrm, char *fmt, ...) {
 
 int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
   int bwritten = 0, total = 0;
+  const char *nstrm_mode;
   pr_buffer_t *pbuf;
-  pool *sub_pool;
+  pool *tmp_pool;
 
   /* Sanity check */
-  if (!nstrm) {
+  if (nstrm == NULL ||
+      buf == NULL ||
+      buflen == 0) {
     errno = EINVAL;
     return -1;
   }
@@ -749,6 +1043,8 @@ int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
     return -1;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   /* Before we send out the data to the client, generate an event
    * for any listeners which may want to examine this data.  To do this, we
    * need to allocate a pr_buffer_t for sending the buffer data to the
@@ -760,8 +1056,8 @@ int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
    * pr_buffer_t out of that.  Then simply destroy the subpool when done.
    */
 
-  sub_pool = pr_pool_create_sz(nstrm->strm_pool, 64);
-  pbuf = pcalloc(sub_pool, sizeof(pr_buffer_t));
+  tmp_pool = make_sub_pool(nstrm->strm_pool);
+  pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
   pbuf->buf = buf;
   pbuf->buflen = buflen;
   pbuf->current = pbuf->buf;
@@ -784,12 +1080,14 @@ int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
   /* The event listeners may have changed the data to write out. */
   buf = pbuf->buf;
   buflen = pbuf->buflen - pbuf->remaining;
-  destroy_pool(sub_pool);
+  destroy_pool(tmp_pool);
 
   while (buflen) {
 
     switch (pr_netio_poll(nstrm)) {
       case 1:
+        /* pr_netio_poll() returns 1 only if the stream has been aborted. */
+        errno = ECONNABORTED;
         return -2;
 
       case -1:
@@ -803,21 +1101,52 @@ int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
 
           switch (nstrm->strm_type) {
             case PR_NETIO_STRM_CTRL:
-              bwritten = ctrl_netio ? (ctrl_netio->write)(nstrm, buf, buflen) :
-                (default_ctrl_netio->write)(nstrm, buf, buflen);
-                break;
+              if (ctrl_netio != NULL) {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s write() for control %s stream",
+                  ctrl_netio->owner_name, nstrm_mode);
+                bwritten = (ctrl_netio->write)(nstrm, buf, buflen);
+
+              } else {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s write() for control %s stream",
+                  default_ctrl_netio->owner_name, nstrm_mode);
+                bwritten = (default_ctrl_netio->write)(nstrm, buf, buflen);
+              }
+              break;
 
             case PR_NETIO_STRM_DATA:
-              if (XFER_ABORTED)
+              if (XFER_ABORTED) {
                 break;
-
-              bwritten = data_netio ? (data_netio->write)(nstrm, buf, buflen) :
-                (default_data_netio->write)(nstrm, buf, buflen);
+              }
+
+              if (data_netio != NULL) {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s write() for data %s stream", data_netio->owner_name,
+                  nstrm_mode);
+                bwritten = (data_netio->write)(nstrm, buf, buflen);
+
+              } else {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s write() for data %s stream",
+                  default_data_netio->owner_name, nstrm_mode);
+                bwritten = (default_data_netio->write)(nstrm, buf, buflen);
+              }
               break;
 
             case PR_NETIO_STRM_OTHR:
-              bwritten = othr_netio ? (othr_netio->write)(nstrm, buf, buflen) :
-                (default_othr_netio->write)(nstrm, buf, buflen);
+              if (othr_netio != NULL) {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s write() for other %s stream",
+                  othr_netio->owner_name, nstrm_mode);
+                bwritten = (othr_netio->write)(nstrm, buf, buflen);
+
+              } else {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s write() for other %s stream",
+                  default_othr_netio->owner_name, nstrm_mode);
+                bwritten = (default_othr_netio->write)(nstrm, buf, buflen);
+              }
               break;
           }
 
@@ -840,12 +1169,13 @@ int pr_netio_write(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
 }
 
 int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
-  int flags = 0;
-  int bwritten = 0, total = 0;
+  int bwritten = 0, flags = 0, total = 0;
+  const char *nstrm_mode;
   pr_buffer_t *pbuf;
+  pool *tmp_pool;
 
   /* Sanity check */
-  if (!nstrm) {
+  if (nstrm == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -856,17 +1186,23 @@ int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
   }
 
   /* Prepare the descriptor for nonblocking IO. */
-  if ((flags = fcntl(nstrm->strm_fd, F_GETFL)) == -1)
+  flags = fcntl(nstrm->strm_fd, F_GETFL);
+  if (flags < 0) {
     return -1;
+  }
 
-  if (fcntl(nstrm->strm_fd, F_SETFL, flags|O_NONBLOCK) == -1)
+  if (fcntl(nstrm->strm_fd, F_SETFL, flags|O_NONBLOCK) < 0) {
     return -1;
+  }
+
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
 
   /* Before we send out the data to the client, generate an event
    * for any listeners which may want to examine this data.
    */
 
-  pbuf = pcalloc(nstrm->strm_pool, sizeof(pr_buffer_t));
+  tmp_pool = make_sub_pool(nstrm->strm_pool);
+  pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
   pbuf->buf = buf;
   pbuf->buflen = buflen;
   pbuf->current = pbuf->buf;
@@ -889,6 +1225,7 @@ int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
   /* The event listeners may have changed the data to write out. */
   buf = pbuf->buf;
   buflen = pbuf->buflen - pbuf->remaining;
+  destroy_pool(tmp_pool);
 
   while (buflen) {
     do {
@@ -904,18 +1241,48 @@ int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
 
       switch (nstrm->strm_type) {
         case PR_NETIO_STRM_CTRL:
-          bwritten = ctrl_netio ? (ctrl_netio->write)(nstrm, buf, buflen) :
-            (default_ctrl_netio->write)(nstrm, buf, buflen);
+          if (ctrl_netio != NULL) {
+            pr_trace_msg(trace_channel, 19,
+              "using %s write() for control %s stream", ctrl_netio->owner_name,
+              nstrm_mode);
+            bwritten = (ctrl_netio->write)(nstrm, buf, buflen);
+
+          } else {
+            pr_trace_msg(trace_channel, 19,
+              "using %s write() for control %s stream",
+              default_ctrl_netio->owner_name, nstrm_mode);
+            bwritten = (default_ctrl_netio->write)(nstrm, buf, buflen);
+          }
           break;
 
         case PR_NETIO_STRM_DATA:
-          bwritten = data_netio ? (data_netio->write)(nstrm, buf, buflen) :
-            (default_data_netio->write)(nstrm, buf, buflen);
+          if (data_netio != NULL) {
+            pr_trace_msg(trace_channel, 19,
+              "using %s write() for data %s stream", data_netio->owner_name,
+              nstrm_mode);
+            bwritten = (data_netio->write)(nstrm, buf, buflen);
+
+          } else {
+            pr_trace_msg(trace_channel, 19,
+              "using %s write() for data %s stream",
+              default_data_netio->owner_name, nstrm_mode);
+            bwritten = (default_data_netio->write)(nstrm, buf, buflen);
+          }
           break;
 
         case PR_NETIO_STRM_OTHR:
-          bwritten = othr_netio ? (othr_netio->write)(nstrm, buf, buflen) :
-            (default_othr_netio->write)(nstrm, buf, buflen);
+          if (othr_netio != NULL) {
+            pr_trace_msg(trace_channel, 19,
+              "using %s write() for other %s stream", othr_netio->owner_name,
+              nstrm_mode);
+            bwritten = (othr_netio->write)(nstrm, buf, buflen);
+
+          } else {
+            pr_trace_msg(trace_channel, 19,
+              "using %s write() for other %s stream",
+              default_othr_netio->owner_name, nstrm_mode);
+            bwritten = (default_othr_netio->write)(nstrm, buf, buflen);
+          }
           break;
       }
 
@@ -923,11 +1290,12 @@ int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
 
     if (bwritten < 0) {
       nstrm->strm_errno = errno;
-      fcntl(nstrm->strm_fd, F_SETFL, flags);
+      (void) fcntl(nstrm->strm_fd, F_SETFL, flags);
 
-      if (nstrm->strm_errno == EWOULDBLOCK)
+      if (nstrm->strm_errno == EWOULDBLOCK) {
         /* Give up ... */
         return total;
+      }
 
       return -1;
     }
@@ -937,16 +1305,21 @@ int pr_netio_write_async(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
     buflen -= bwritten;
   }
 
-  fcntl(nstrm->strm_fd, F_SETFL, flags);
+  (void) fcntl(nstrm->strm_fd, F_SETFL, flags);
   return total;
 }
 
 int pr_netio_read(pr_netio_stream_t *nstrm, char *buf, size_t buflen,
     int bufmin) {
   int bread = 0, total = 0;
+  const char *nstrm_mode;
+  pr_buffer_t *pbuf;
+  pool *tmp_pool;
 
   /* Sanity check. */
-  if (!nstrm) {
+  if (nstrm == NULL ||
+      buf == NULL ||
+      buflen == 0) {
     errno = EINVAL;
     return -1;
   }
@@ -956,11 +1329,16 @@ int pr_netio_read(pr_netio_stream_t *nstrm, char *buf, size_t buflen,
     return -1;
   }
 
-  if (bufmin < 1)
+  if (bufmin < 1) {
     bufmin = 1;
+  }
 
-  if (bufmin > buflen)
+  if (bufmin > 0 &&
+      (size_t) bufmin > buflen) {
     bufmin = buflen;
+  }
+
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
 
   while (bufmin > 0) {
     polling:
@@ -980,21 +1358,52 @@ int pr_netio_read(pr_netio_stream_t *nstrm, char *buf, size_t buflen,
 
           switch (nstrm->strm_type) {
             case PR_NETIO_STRM_CTRL:
-              bread = ctrl_netio ? (ctrl_netio->read)(nstrm, buf, buflen) :
-                (default_ctrl_netio->read)(nstrm, buf, buflen);
-                break;
+              if (ctrl_netio != NULL) {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s read() for control %s stream",
+                  ctrl_netio->owner_name, nstrm_mode);
+                bread = (ctrl_netio->read)(nstrm, buf, buflen);
+
+              } else {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s read() for control %s stream",
+                  default_ctrl_netio->owner_name, nstrm_mode);
+                bread = (default_ctrl_netio->read)(nstrm, buf, buflen);
+              }
+              break;
 
             case PR_NETIO_STRM_DATA:
-              if (XFER_ABORTED)
+              if (XFER_ABORTED) {
                 break;
-
-              bread = data_netio ? (data_netio->read)(nstrm, buf, buflen) :
-                (default_data_netio->read)(nstrm, buf, buflen);
+              }
+
+              if (data_netio != NULL) {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s read() for data %s stream", data_netio->owner_name,
+                  nstrm_mode);
+                bread = (data_netio->read)(nstrm, buf, buflen);
+
+              } else {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s read() for data %s stream",
+                  default_data_netio->owner_name, nstrm_mode);
+                bread = (default_data_netio->read)(nstrm, buf, buflen);
+              }
               break;
 
             case PR_NETIO_STRM_OTHR:
-              bread = othr_netio ? (othr_netio->read)(nstrm, buf, buflen) :
-                (default_othr_netio->read)(nstrm, buf, buflen);
+              if (othr_netio != NULL) {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s read() for other %s stream",
+                  othr_netio->owner_name, nstrm_mode);
+                bread = (othr_netio->read)(nstrm, buf, buflen);
+
+              } else {
+                pr_trace_msg(trace_channel, 19,
+                  "using %s read() for other %s stream",
+                  default_othr_netio->owner_name, nstrm_mode);
+                bread = (default_othr_netio->read)(nstrm, buf, buflen);
+              }
               break;
           }
 
@@ -1040,6 +1449,43 @@ int pr_netio_read(pr_netio_stream_t *nstrm, char *buf, size_t buflen,
       break;
     }
 
+    /* Before we provide the data from the client, generate an event
+     * for any listeners which may want to examine this data.  To do this, we
+     * need to allocate a pr_buffer_t for sending the buffer data to the
+     * listeners.
+     *
+     * We could just use nstrm->strm_pool, but for a long-lived control
+     * connection, this would amount to a slow memory increase.  So instead,
+     * we create a subpool from the stream's pool, and allocate the
+     * pr_buffer_t out of that.  Then simply destroy the subpool when done.
+     */
+
+    tmp_pool = make_sub_pool(nstrm->strm_pool);
+    pbuf = pcalloc(tmp_pool, sizeof(pr_buffer_t));
+    pbuf->buf = buf;
+    pbuf->buflen = bread;
+    pbuf->current = pbuf->buf;
+    pbuf->remaining = 0;
+
+    switch (nstrm->strm_type) {
+      case PR_NETIO_STRM_CTRL:
+        pr_event_generate("core.ctrl-read", pbuf);
+        break;
+
+      case PR_NETIO_STRM_DATA:
+        pr_event_generate("core.data-read", pbuf);
+        break;
+
+      case PR_NETIO_STRM_OTHR:
+        pr_event_generate("core.othr-read", pbuf);
+        break;
+    }
+
+    /* The event listeners may have changed the data read in out. */
+    buf = pbuf->buf;
+    bread = pbuf->buflen - pbuf->remaining;
+    destroy_pool(tmp_pool);
+
     buf += bread;
     total += bread;
     bufmin -= bread;
@@ -1052,26 +1498,59 @@ int pr_netio_read(pr_netio_stream_t *nstrm, char *buf, size_t buflen,
 
 int pr_netio_shutdown(pr_netio_stream_t *nstrm, int how) {
   int res = -1;
+  const char *nstrm_mode;
 
-  if (!nstrm) {
+  if (nstrm == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  nstrm_mode = netio_stream_mode(nstrm->strm_mode);
+
   switch (nstrm->strm_type) {
     case PR_NETIO_STRM_CTRL:
-      res = ctrl_netio ? (ctrl_netio->shutdown)(nstrm, how) :
-        (default_ctrl_netio->shutdown)(nstrm, how);
+      if (ctrl_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s shutdown() for control %s stream", ctrl_netio->owner_name,
+          nstrm_mode);
+        res = (ctrl_netio->shutdown)(nstrm, how);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s shutdown() for control %s stream",
+          default_ctrl_netio->owner_name, nstrm_mode);
+        res = (default_ctrl_netio->shutdown)(nstrm, how);
+      }
       return res;
 
     case PR_NETIO_STRM_DATA:
-      res = data_netio ? (data_netio->shutdown)(nstrm, how) :
-        (default_data_netio->shutdown)(nstrm, how);
+      if (data_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s shutdown() for data %s stream", data_netio->owner_name,
+          nstrm_mode);
+        res = (data_netio->shutdown)(nstrm, how);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s shutdown() for data %s stream",
+          default_data_netio->owner_name, nstrm_mode);
+        res = (default_data_netio->shutdown)(nstrm, how);
+      }
       return res;
 
     case PR_NETIO_STRM_OTHR:
-      res = othr_netio ? (othr_netio->shutdown)(nstrm, how) :
-        (default_othr_netio->shutdown)(nstrm, how);
+      if (othr_netio != NULL) {
+        pr_trace_msg(trace_channel, 19,
+          "using %s shutdown() for other %s stream", othr_netio->owner_name,
+          nstrm_mode);
+        res = (othr_netio->shutdown)(nstrm, how);
+
+      } else {
+        pr_trace_msg(trace_channel, 19,
+          "using %s shutdown() for other %s stream",
+          default_othr_netio->owner_name, nstrm_mode);
+        res = (default_othr_netio->shutdown)(nstrm, how);
+      }
       return res;
   }
 
@@ -1084,7 +1563,9 @@ char *pr_netio_gets(char *buf, size_t buflen, pr_netio_stream_t *nstrm) {
   int toread;
   pr_buffer_t *pbuf = NULL;
 
-  if (buflen == 0) {
+  if (nstrm == NULL ||
+      buf == NULL ||
+      buflen == 0) {
     errno = EINVAL;
     return NULL;
   }
@@ -1111,9 +1592,9 @@ char *pr_netio_gets(char *buf, size_t buflen, pr_netio_stream_t *nstrm) {
         if (bp != buf) {
           *bp = '\0';
           return buf;
+        }
 
-        } else
-          return NULL;
+        return NULL;
       }
 
       pbuf->remaining = pbuf->buflen - toread;
@@ -1132,10 +1613,10 @@ char *pr_netio_gets(char *buf, size_t buflen, pr_netio_stream_t *nstrm) {
     toread = pbuf->buflen - pbuf->remaining;
 
     while (buflen && *pbuf->current != '\n' && toread--) {
-      if (*pbuf->current & 0x80)
+      if (*pbuf->current & 0x80) {
         pbuf->current++;
 
-      else {
+      } else {
         *bp++ = *pbuf->current++;
         buflen--;
       }
@@ -1150,8 +1631,9 @@ char *pr_netio_gets(char *buf, size_t buflen, pr_netio_stream_t *nstrm) {
       break;
     }
 
-    if (!toread)
+    if (!toread) {
       pbuf->current = NULL;
+    }
   }
 
   *bp = '\0';
@@ -1160,18 +1642,19 @@ char *pr_netio_gets(char *buf, size_t buflen, pr_netio_stream_t *nstrm) {
 
 static int telnet_mode = 0;
 
-char *pr_netio_telnet_gets(char *buf, size_t buflen,
+int pr_netio_telnet_gets2(char *buf, size_t bufsz,
     pr_netio_stream_t *in_nstrm, pr_netio_stream_t *out_nstrm) {
   char *bp = buf;
   unsigned char cp;
   int toread, handle_iac = TRUE, saw_newline = FALSE;
   pr_buffer_t *pbuf = NULL;
+  size_t buflen = bufsz;
 
   if (buflen == 0 ||
       in_nstrm == NULL ||
       out_nstrm == NULL) {
     errno = EINVAL;
-    return NULL;
+    return -1;
   }
 
 #ifdef PR_USE_NLS
@@ -1200,10 +1683,10 @@ char *pr_netio_telnet_gets(char *buf, size_t buflen,
       if (toread <= 0) {
         if (bp != buf) {
           *bp = '\0';
-          return buf;
+          return (bufsz - buflen - 1);
         }
 
-        return NULL;
+        return -1;
       }
 
       pbuf->remaining = pbuf->buflen - toread;
@@ -1220,7 +1703,8 @@ char *pr_netio_telnet_gets(char *buf, size_t buflen,
 
     while (buflen > 0 &&
            toread > 0 &&
-           *pbuf->current != '\n' &&
+           (*pbuf->current != '\n' ||
+            (*pbuf->current == '\n' && *(pbuf->current - 1) != '\r')) &&
            toread--) {
       pr_signals_handle();
 
@@ -1313,11 +1797,25 @@ char *pr_netio_telnet_gets(char *buf, size_t buflen,
     if (buflen > 0 &&
         toread > 0 &&
         *pbuf->current == '\n') {
-      buflen--;
-      toread--;
-      *bp++ = *pbuf->current++;
-      pbuf->remaining++;
 
+      /* If the current character is LF, and the previous character we
+       * copied was a CR, then strip the CR by overwriting it with the LF,
+       * turning the copied data from Telnet CRLF line termination to
+       * Unix LF line termination.
+       */
+      if (*(bp-1) == '\r') {
+        /* We already decrement the buffer length for the CR; no need to
+         * do it again since we are overwriting that CR.
+         */
+        *(bp-1) = *pbuf->current++;
+
+      } else {
+        *bp++ = *pbuf->current++;
+        buflen--;
+      }
+
+      pbuf->remaining++;
+      toread--;
       saw_newline = TRUE;
       break;
     }
@@ -1338,18 +1836,30 @@ char *pr_netio_telnet_gets(char *buf, size_t buflen,
 
     properly_terminated_prev_command = FALSE;
     errno = E2BIG;
-    return NULL;
+    return -1;
   }
 
   if (!properly_terminated_prev_command) {
     properly_terminated_prev_command = TRUE;
     pr_log_pri(PR_LOG_NOTICE, "client sent too-long command, ignoring");
     errno = E2BIG;
-    return NULL;
+    return -1;
   }
 
   properly_terminated_prev_command = TRUE;
   *bp = '\0';
+  return (bufsz - buflen - 1);
+}
+
+char *pr_netio_telnet_gets(char *buf, size_t bufsz,
+    pr_netio_stream_t *in_nstrm, pr_netio_stream_t *out_nstrm) {
+  int res;
+
+  res = pr_netio_telnet_gets2(buf, bufsz, in_nstrm, out_nstrm);
+  if (res < 0) {
+    return NULL;
+  }
+
   return buf;
 }
 
@@ -1363,17 +1873,17 @@ int pr_register_netio(pr_netio_t *netio, int strm_types) {
      */
     if (default_ctrl_netio == NULL) {
       default_ctrl_netio = default_netio = pr_alloc_netio2(permanent_pool,
-        NULL);
+        NULL, NULL);
     }
 
     if (default_data_netio == NULL) {
       default_data_netio = default_netio ? default_netio :
-        (default_netio = pr_alloc_netio2(permanent_pool, NULL));
+        (default_netio = pr_alloc_netio2(permanent_pool, NULL, NULL));
     }
 
     if (default_othr_netio == NULL) {
       default_othr_netio = default_netio ? default_netio :
-        (default_netio = pr_alloc_netio2(permanent_pool, NULL));
+        (default_netio = pr_alloc_netio2(permanent_pool, NULL, NULL));
     }
 
     return 0;
@@ -1454,7 +1964,8 @@ pr_netio_t *pr_get_netio(int strm_type) {
 
 extern pid_t mpid;
 
-pr_netio_t *pr_alloc_netio2(pool *parent_pool, module *owner) {
+pr_netio_t *pr_alloc_netio2(pool *parent_pool, module *owner,
+    const char *owner_name) {
   pr_netio_t *netio = NULL;
   pool *netio_pool = NULL;
 
@@ -1485,7 +1996,20 @@ pr_netio_t *pr_alloc_netio2(pool *parent_pool, module *owner) {
   netio->owner = owner;
 
   if (owner != NULL) {
-    netio->owner_name = pstrdup(netio_pool, owner->name);
+    if (owner_name != NULL) {
+      netio->owner_name = pstrdup(netio_pool, owner_name);
+
+    } else {
+      netio->owner_name = pstrdup(netio_pool, owner->name);
+    }
+
+  } else {
+    if (owner_name != NULL) {
+      netio->owner_name = owner_name;
+
+    } else {
+      netio->owner_name = "default";
+    }
   }
 
   /* Set the default NetIO handlers to the core handlers. */
@@ -1503,7 +2027,7 @@ pr_netio_t *pr_alloc_netio2(pool *parent_pool, module *owner) {
 }
 
 pr_netio_t *pr_alloc_netio(pool *parent_pool) {
-  return pr_alloc_netio2(parent_pool, NULL);
+  return pr_alloc_netio2(parent_pool, NULL, NULL);
 }
 
 void init_netio(void) {
@@ -1512,4 +2036,3 @@ void init_netio(void) {
 
   pr_register_netio(NULL, 0);
 }
-
diff --git a/src/parser.c b/src/parser.c
index 662598e..e319aaf 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2013 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,19 @@
  * for OpenSSL in the source distribution.
  */
 
-/* Configuration parser
- * $Id: parser.c,v 1.40 2013-12-30 06:38:59 castaglia Exp $
- */
+/* Configuration parser */
 
 #include "conf.h"
+#include "privs.h"
+
+/* Maximum depth of Include patterns/files. */
+#define PR_PARSER_INCLUDE_MAX_DEPTH	64
 
 extern xaset_t *server_list;
 extern pool *global_config_pool;
 
 static pool *parser_pool = NULL;
+static unsigned long parser_include_opts = 0UL;
 
 static array_header *parser_confstack = NULL;
 static config_rec **parser_curr_config = NULL;
@@ -64,16 +67,6 @@ static struct config_src *parser_sources = NULL;
 /* Private functions
  */
 
-static void add_config_ctxt(config_rec *c) {
-  if (!*parser_curr_config) {
-    *parser_curr_config = c;
-
-  } else {
-    parser_curr_config = (config_rec **) push_array(parser_confstack);
-    *parser_curr_config = c;
-  }
-}
-
 static struct config_src *add_config_source(pr_fh_t *fh) {
   pool *p = pr_pool_create_sz(parser_pool, PARSER_CONFIG_SRC_POOL_SZ);
   struct config_src *cs = pcalloc(p, sizeof(struct config_src));
@@ -162,7 +155,7 @@ static char *get_config_word(pool *p, char *word) {
         continue;
       }
 
-      word = sreplace(p, word, var, env, NULL);
+      word = (char *) sreplace(p, word, var, env, NULL);
       ptr = strstr(word, "%{env:");
     }
   }
@@ -251,8 +244,9 @@ config_rec *pr_parser_config_ctxt_close(int *empty) {
 }
 
 config_rec *pr_parser_config_ctxt_get(void) {
-  if (parser_curr_config)
+  if (parser_curr_config) {
     return *parser_curr_config;
+  }
 
   errno = ENOENT;
   return NULL;
@@ -263,7 +257,7 @@ config_rec *pr_parser_config_ctxt_open(const char *name) {
   pool *c_pool = NULL, *parent_pool = NULL;
   xaset_t **set = NULL;
 
-  if (!name) {
+  if (name == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -285,7 +279,7 @@ config_rec *pr_parser_config_ctxt_open(const char *name) {
    * prematurely, and helps to avoid memory leaks.
    */
   if (strncasecmp(name, "<Global>", 9) == 0) {
-    if (!global_config_pool) {
+    if (global_config_pool == NULL) {
       global_config_pool = make_sub_pool(permanent_pool);
       pr_pool_tag(global_config_pool, "<Global> Pool");
     }
@@ -312,18 +306,69 @@ config_rec *pr_parser_config_ctxt_open(const char *name) {
   c->name = pstrdup(c->pool, name);
 
   if (parent) {
-    if (parent->config_type == CONF_DYNDIR)
+    if (parent->config_type == CONF_DYNDIR) {
       c->flags |= CF_DYNAMIC;
+    }
   }
 
-  add_config_ctxt(c);
+  (void) pr_parser_config_ctxt_push(c);
   return c;
 }
 
+int pr_parser_config_ctxt_push(config_rec *c) {
+  if (c == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (parser_confstack == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  if (!*parser_curr_config) {
+    *parser_curr_config = c;
+
+  } else {
+    parser_curr_config = (config_rec **) push_array(parser_confstack);
+    *parser_curr_config = c;
+  }
+
+  return 0;
+}
+
 unsigned int pr_parser_get_lineno(void) {
   return parser_curr_lineno;
 }
 
+/* Return an array of all supported/known configuration directives. */
+static array_header *get_all_directives(pool *p) {
+  array_header *names;
+  conftable *tab;
+  int idx;
+  unsigned int hash;
+
+  names = make_array(p, 1, sizeof(const char *));
+
+  idx = -1;
+  hash = 0;
+  tab = pr_stash_get_symbol2(PR_SYM_CONF, NULL, NULL, &idx, &hash);
+  while (idx != -1) {
+    pr_signals_handle();
+
+    if (tab != NULL) {
+      *((const char **) push_array(names)) = pstrdup(p, tab->directive);
+
+    } else {
+      idx++;
+    }
+
+    tab = pr_stash_get_symbol2(PR_SYM_CONF, NULL, tab, &idx, &hash);
+  }
+
+  return names;
+}
+
 int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
     int flags) {
   pr_fh_t *fh;
@@ -331,22 +376,30 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
   struct config_src *cs;
   cmd_rec *cmd;
   pool *tmp_pool;
-  char *report_path;
+  char *buf, *report_path;
+  size_t bufsz;
 
   if (path == NULL) {
     errno = EINVAL;
     return -1;
   }
 
+  if (parser_servstack == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
   tmp_pool = make_sub_pool(p ? p : permanent_pool);
   pr_pool_tag(tmp_pool, "parser file pool");
 
   report_path = (char *) path;
-  if (session.chroot_path)
+  if (session.chroot_path) {
     report_path = pdircat(tmp_pool, session.chroot_path, path, NULL);
+  }
 
-  if (!(flags & PR_PARSER_FL_DYNAMIC_CONFIG))
+  if (!(flags & PR_PARSER_FL_DYNAMIC_CONFIG)) {
     pr_trace_msg(trace_channel, 3, "parsing '%s' configuration", report_path);
+  }
 
   fh = pr_fsio_open(path, O_RDONLY);
   if (fh == NULL) {
@@ -378,6 +431,11 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
     return -1;
   }
 
+  /* Advise the platform that we will be only reading this file
+   * sequentially.
+   */
+  pr_fs_fadvise(PR_FH_FD(fh), 0, 0, PR_FS_FADVISE_SEQUENTIAL);
+
   /* Check for world-writable files (and later, files in world-writable
    * directories).
    *
@@ -395,12 +453,21 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
    */
   cs = add_config_source(fh);
 
-  if (start) 
-    add_config_ctxt(start);
+  if (start != NULL) {
+    (void) pr_parser_config_ctxt_push(start);
+  }
+
+  bufsz = PR_TUNABLE_PARSER_BUFFER_SIZE;
+  buf = pcalloc(tmp_pool, bufsz + 1);
 
-  while ((cmd = pr_parser_parse_line(tmp_pool)) != NULL) {
+  while (pr_parser_read_line(buf, bufsz) != NULL) {
     pr_signals_handle();
 
+    cmd = pr_parser_parse_line(tmp_pool, buf, 0);
+    if (cmd == NULL) {
+      continue;
+    }
+
     if (cmd->argc) {
       conftable *conftab;
       char found = FALSE;
@@ -408,10 +475,9 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
       cmd->server = *parser_curr_server;
       cmd->config = *parser_curr_config;
 
-      conftab = pr_stash_get_symbol(PR_SYM_CONF, cmd->argv[0], NULL,
-        &cmd->stash_index);
-
-      while (conftab) {
+      conftab = pr_stash_get_symbol2(PR_SYM_CONF, cmd->argv[0], NULL,
+        &cmd->stash_index, &cmd->stash_hash);
+      while (conftab != NULL) {
         modret_t *mr;
 
         pr_signals_handle();
@@ -425,16 +491,16 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
         mr = pr_module_call(conftab->m, conftab->handler, cmd);
         if (mr != NULL) {
           if (MODRET_ISERROR(mr)) {
-
             if (!(flags & PR_PARSER_FL_DYNAMIC_CONFIG)) {
               pr_log_pri(PR_LOG_WARNING, "fatal: %s on line %u of '%s'",
                 MODRET_ERRMSG(mr), cs->cs_lineno, report_path);
-              exit(1);
-
-            } else {
-              pr_log_pri(PR_LOG_WARNING, "warning: %s on line %u of '%s'",
-                MODRET_ERRMSG(mr), cs->cs_lineno, report_path);
+              destroy_pool(tmp_pool);
+              errno = EPERM;
+              return -1;
             }
+
+            pr_log_pri(PR_LOG_WARNING, "warning: %s on line %u of '%s'",
+              MODRET_ERRMSG(mr), cs->cs_lineno, report_path);
           }
         }
 
@@ -442,30 +508,97 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
           found = TRUE;
         }
 
-        conftab = pr_stash_get_symbol(PR_SYM_CONF, cmd->argv[0], conftab,
-          &cmd->stash_index);
+        conftab = pr_stash_get_symbol2(PR_SYM_CONF, cmd->argv[0], conftab,
+          &cmd->stash_index, &cmd->stash_hash);
       }
 
-      if (cmd->tmp_pool)
+      if (cmd->tmp_pool) {
         destroy_pool(cmd->tmp_pool);
+      }
+
+      if (found == FALSE) {
+        register unsigned int i;
+        char *name;
+        size_t namelen;
+        int non_ascii = FALSE;
+
+        /* I encountered a case where a particular configuration file had
+         * what APPEARED to be a valid directive, but the parser kept reporting
+         * that the directive was unknown.  I now suspect that the file in
+         * question had embedded UTF8 characters (spaces, perhaps), which
+         * would appear as normal spaces in e.g. UTF8-aware editors/terminals,
+         * but which the parser would rightly refuse.
+         *
+         * So to indicate that this might be the case, check for any non-ASCII
+         * characters in the "unknown" directive name, and if found, log
+         * about them.
+         */
+
+        name = cmd->argv[0];
+        namelen = strlen(name);
 
-      if (!found) {
+        for (i = 0; i < namelen; i++) {
+          if (!isascii((int) name[i])) {
+            non_ascii = TRUE;
+            break;
+          }
+        }
 
         if (!(flags & PR_PARSER_FL_DYNAMIC_CONFIG)) {
           pr_log_pri(PR_LOG_WARNING, "fatal: unknown configuration directive "
-            "'%s' on line %u of '%s'", cmd->argv[0], cs->cs_lineno,
-            report_path);
-          exit(1);
-
-        } else {
-          pr_log_pri(PR_LOG_WARNING, "warning: unknown configuration directive "
-            "'%s' on line %u of '%s'", cmd->argv[0], cs->cs_lineno,
-            report_path);
+            "'%s' on line %u of '%s'", name, cs->cs_lineno, report_path);
+          if (non_ascii) {
+            pr_log_pri(PR_LOG_WARNING, "fatal: malformed directive name "
+              "'%s' (contains non-ASCII characters)", name);
+
+          } else {
+            array_header *directives, *similars;
+
+            directives = get_all_directives(tmp_pool);
+            similars = pr_str_get_similars(tmp_pool, name, directives, 0,
+              PR_STR_FL_IGNORE_CASE);
+            if (similars != NULL &&
+                similars->nelts > 0) {
+              unsigned int nelts;
+              const char **names, *msg;
+
+              names = similars->elts;
+              nelts = similars->nelts;
+              if (nelts > 4) {
+                nelts = 4;
+              }
+
+              msg = "fatal: Did you mean:";
+
+              if (nelts == 1) {
+                msg = pstrcat(tmp_pool, msg, " ", names[0], NULL);
+
+              } else {
+                for (i = 0; i < nelts; i++) {
+                  msg = pstrcat(tmp_pool, msg, "\n  ", names[i], NULL);
+                }
+              }
+
+              pr_log_pri(PR_LOG_WARNING, "%s", msg);
+            }
+          }
+
+          destroy_pool(tmp_pool);
+          errno = EPERM;
+          return -1;
+        }
+
+        pr_log_pri(PR_LOG_WARNING, "warning: unknown configuration directive "
+          "'%s' on line %u of '%s'", name, cs->cs_lineno, report_path);
+        if (non_ascii) {
+          pr_log_pri(PR_LOG_WARNING, "warning: malformed directive name "
+            "'%s' (contains non-ASCII characters)", name);
         }
       }
     }
 
     destroy_pool(cmd->pool);
+    memset(buf, '\0', bufsz);
   }
 
   /* Pop this configuration stream from the stack. */
@@ -477,105 +610,113 @@ int pr_parser_parse_file(pool *p, const char *path, config_rec *start,
   return 0;
 }
 
-cmd_rec *pr_parser_parse_line(pool *p) {
+cmd_rec *pr_parser_parse_line(pool *p, const char *text, size_t text_len) {
   register unsigned int i;
-  char buf[PR_TUNABLE_BUFFER_SIZE+1], *arg = "", *word = NULL;
+  char *arg = "", *ptr, *word = NULL;
   cmd_rec *cmd = NULL;
   pool *sub_pool = NULL;
   array_header *arr = NULL;
 
-  if (p == NULL) {
+  if (p == NULL ||
+      text == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
-  memset(buf, '\0', sizeof(buf));
-  
-  while (pr_parser_read_line(buf, sizeof(buf)-1) != NULL) {
-    char *bufp = buf;
+  if (text_len == 0) {
+    text_len = strlen(text);
+  }
 
-    pr_signals_handle();
+  if (text_len == 0) {
+    errno = ENOENT;
+    return NULL;
+  }
 
-    /* Build a new pool for the command structure and array */
-    sub_pool = make_sub_pool(p);
-    pr_pool_tag(sub_pool, "parser cmd subpool");
+  ptr = (char *) text;
 
-    cmd = pcalloc(sub_pool, sizeof(cmd_rec));
-    cmd->pool = sub_pool;
-    cmd->stash_index = -1;
+  /* Build a new pool for the command structure and array */
+  sub_pool = make_sub_pool(p);
+  pr_pool_tag(sub_pool, "parser cmd subpool");
 
-    /* Add each word to the array */
-    arr = make_array(cmd->pool, 4, sizeof(char **));
-    while ((word = pr_str_get_word(&bufp, 0)) != NULL) {
-      char *tmp;
+  cmd = pcalloc(sub_pool, sizeof(cmd_rec));
+  cmd->pool = sub_pool;
+  cmd->stash_index = -1;
+  cmd->stash_hash = 0;
 
-      tmp = get_config_word(cmd->pool, word);
+  /* Add each word to the array */
+  arr = make_array(cmd->pool, 4, sizeof(char **));
+  while ((word = pr_str_get_word(&ptr, 0)) != NULL) {
+    char *ptr2;
 
-      *((char **) push_array(arr)) = tmp;
-      cmd->argc++;
-    }
+    pr_signals_handle();
+    ptr2 = get_config_word(cmd->pool, word);
+    *((char **) push_array(arr)) = ptr2;
+    cmd->argc++;
+  }
 
-    /* Terminate the array with a NULL. */
-    *((char **) push_array(arr)) = NULL;
+  /* Terminate the array with a NULL. */
+  *((char **) push_array(arr)) = NULL;
 
-    /* The array header's job is done, we can forget about it and
-     * it will get purged when the command's pool is destroyed.
-     */
+  /* The array header's job is done, we can forget about it and
+   * it will get purged when the command's pool is destroyed.
+   */
 
-    cmd->argv = (char **) arr->elts;
+  cmd->argv = (void **) arr->elts;
 
-    /* Perform a fixup on configuration directives so that:
-     *
-     *   -argv[0]--  -argv[1]-- ----argv[2]-----
-     *   <Option     /etc/adir  /etc/anotherdir>
-     *
-     *  becomes:
-     *
-     *   -argv[0]--  -argv[1]-  ----argv[2]----
-     *   <Option>    /etc/adir  /etc/anotherdir
-     */
+  /* Perform a fixup on configuration directives so that:
+   *
+   *   -argv[0]--  -argv[1]-- ----argv[2]-----
+   *   <Option     /etc/adir  /etc/anotherdir>
+   *
+   *  becomes:
+   *
+   *   -argv[0]--  -argv[1]-  ----argv[2]----
+   *   <Option>    /etc/adir  /etc/anotherdir
+   */
 
-    if (cmd->argc &&
-        *(cmd->argv[0]) == '<') {
-      char *cp = cmd->argv[cmd->argc-1];
+  if (cmd->argc &&
+      *((char *) cmd->argv[0]) == '<') {
+    char *cp;
+    size_t cp_len;
 
-      if (*(cp + strlen(cp)-1) == '>' &&
-          cmd->argc > 1) {
+    cp = cmd->argv[cmd->argc-1];
+    cp_len = strlen(cp);
 
-        if (strncmp(cp, ">", 2) == 0) {
-          cmd->argv[cmd->argc-1] = NULL;
-          cmd->argc--;
+    if (*(cp + cp_len-1) == '>' &&
+        cmd->argc > 1) {
 
-        } else {
-          *(cp + strlen(cp)-1) = '\0';
-        }
+      if (strncmp(cp, ">", 2) == 0) {
+        cmd->argv[cmd->argc-1] = NULL;
+        cmd->argc--;
 
-        cp = cmd->argv[0];
-        if (*(cp + strlen(cp)-1) != '>') {
-          cmd->argv[0] = pstrcat(cmd->pool, cp, ">", NULL);
-        }
+      } else {
+        *(cp + cp_len-1) = '\0';
       }
-    }
 
-    if (cmd->argc < 2) {
-      arg = pstrdup(cmd->pool, arg);
+      cp = cmd->argv[0];
+      cp_len = strlen(cp);
+      if (*(cp + cp_len-1) != '>') {
+        cmd->argv[0] = pstrcat(cmd->pool, cp, ">", NULL);
+      }
     }
+  }
 
-    for (i = 1; i < cmd->argc; i++) {
-      arg = pstrcat(cmd->pool, arg, *arg ? " " : "", cmd->argv[i], NULL);
-    }
+  if (cmd->argc < 2) {
+    arg = pstrdup(cmd->pool, arg);
+  }
 
-    cmd->arg = arg;
-    return cmd;
+  for (i = 1; i < cmd->argc; i++) {
+    arg = pstrcat(cmd->pool, arg, *arg ? " " : "", cmd->argv[i], NULL);
   }
 
-  return NULL;
+  cmd->arg = arg;
+  return cmd;
 }
 
 int pr_parser_prepare(pool *p, xaset_t **parsed_servers) {
 
-  if (!p) {
-    if (!parser_pool) {
+  if (p == NULL) {
+    if (parser_pool == NULL) {
       parser_pool = make_sub_pool(permanent_pool);
       pr_pool_tag(parser_pool, "Parser Pool");
     }
@@ -614,12 +755,13 @@ char *pr_parser_read_line(char *buf, size_t bufsz) {
   /* Always use the config stream at the top of the stack. */
   cs = parser_sources;
 
-  if (!buf) {
+  if (buf == NULL ||
+      cs == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
-  if (!cs->cs_fh) {
+  if (cs->cs_fh == NULL) {
     errno = EPERM;
     return NULL;
   }
@@ -631,8 +773,11 @@ char *pr_parser_read_line(char *buf, size_t bufsz) {
   while ((pr_fsio_getline(buf, bufsz, cs->cs_fh, &(cs->cs_lineno))) != NULL) {
     int have_eol = FALSE;
     char *bufp = NULL;
-    size_t buflen = strlen(buf);
+    size_t buflen;
 
+    pr_signals_handle();
+
+    buflen = strlen(buf);
     parser_curr_lineno = cs->cs_lineno;
 
     /* Trim off the trailing newline, if present. */
@@ -643,14 +788,13 @@ char *pr_parser_read_line(char *buf, size_t bufsz) {
       buflen--;
     }
 
-    while (buflen &&
-           buf[buflen - 1] == '\r') {
-      pr_signals_handle();
+    if (buflen &&
+        buf[buflen - 1] == '\r') {
       buf[buflen-1] = '\0';
       buflen--;
     }
 
-    if (!have_eol) {
+    if (have_eol == FALSE) {
       pr_log_pri(PR_LOG_WARNING,
         "warning: handling possibly truncated configuration data at "
         "line %u of '%s'", cs->cs_lineno, cs->cs_fh->fh_path);
@@ -699,13 +843,31 @@ server_rec *pr_parser_server_ctxt_close(void) {
 }
 
 server_rec *pr_parser_server_ctxt_get(void) {
-  if (parser_curr_server)
+  if (parser_curr_server) {
     return *parser_curr_server;
+  }
 
   errno = ENOENT;
   return NULL;
 }
 
+int pr_parser_server_ctxt_push(server_rec *s) {
+  if (s == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (parser_servstack == NULL) {
+    errno = EPERM;
+    return -1;
+  }
+
+  parser_curr_server = (server_rec **) push_array(parser_servstack);
+  *parser_curr_server = s;
+
+  return 0;
+}
+
 server_rec *pr_parser_server_ctxt_open(const char *addrstr) {
   server_rec *s;
   pool *p;
@@ -738,8 +900,448 @@ server_rec *pr_parser_server_ctxt_open(const char *addrstr) {
   /* Default server port */
   s->ServerPort = pr_inet_getservport(s->pool, "ftp", "tcp");
 
-  parser_curr_server = (server_rec **) push_array(parser_servstack);
-  *parser_curr_server = s;
-
+  (void) pr_parser_server_ctxt_push(s);
   return s;
 }
+
+unsigned long pr_parser_set_include_opts(unsigned long opts) {
+  unsigned long prev_opts;
+
+  prev_opts = parser_include_opts;
+  parser_include_opts = opts;
+
+  return prev_opts;
+}
+
+static const char *tmpfile_patterns[] = {
+  "*~",
+  "*.sw?",
+  NULL
+};
+
+static int is_tmp_file(const char *file) {
+  register unsigned int i;
+
+  for (i = 0; tmpfile_patterns[i]; i++) {
+    if (pr_fnmatch(tmpfile_patterns[i], file, PR_FNM_PERIOD) == 0) {
+      return TRUE;
+    }
+  }
+
+  return FALSE;
+}
+
+static int config_filename_cmp(const void *a, const void *b) {
+  return strcmp(*((char **) a), *((char **) b));
+}
+
+static int parse_wildcard_config_path(pool *p, const char *path,
+    unsigned int depth) {
+  register unsigned int i;
+  int res, xerrno;
+  pool *tmp_pool;
+  array_header *globbed_dirs = NULL;
+  const char *component = NULL, *parent_path = NULL, *suffix_path = NULL;
+  struct stat st;
+  size_t path_len, component_len;
+  char *name_pattern = NULL;
+  void *dirh = NULL;
+  struct dirent *dent = NULL;
+
+  if (depth > PR_PARSER_INCLUDE_MAX_DEPTH) {
+    pr_log_pri(PR_LOG_WARNING, "error: resolving wildcard pattern in '%s' "
+      "exceeded maximum filesystem depth (%u)", path,
+      (unsigned int) PR_PARSER_INCLUDE_MAX_DEPTH);
+    errno = EINVAL;
+    return -1;
+  }
+
+  path_len = strlen(path);
+  if (path_len < 2) {
+    pr_trace_msg(trace_channel, 7, "path '%s' too short to be wildcard path",
+      path);
+
+    /* The first character must be a slash, and we need at least one more
+     * character in the path as a glob character.
+     */
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "Include sub-pool");
+
+  /* We need to find the first component of the path which contains glob
+   * characters.  We then use the path up to the previous component as the
+   * parent directory to open, and the glob-bearing component as the filter
+   * for directories within the parent.
+   */
+
+  component = path + 1;
+  while (TRUE) {
+    int last_component = FALSE;
+    char *ptr;
+
+    pr_signals_handle();
+
+    ptr = strchr(component, '/');
+    if (ptr != NULL) {
+      component_len = ptr - component;
+
+    } else {
+      component_len = strlen(component);
+      last_component = TRUE;
+    }
+
+    if (memchr(component, (int) '*', component_len) != NULL ||
+        memchr(component, (int) '?', component_len) != NULL ||
+        memchr(component, (int) '[', component_len) != NULL) {
+
+      name_pattern = pstrndup(tmp_pool, component, component_len);
+
+      if (parent_path == NULL) {
+        parent_path = pstrndup(tmp_pool, "/", 1);
+      }
+
+      if (ptr != NULL) {
+        suffix_path = pstrdup(tmp_pool, ptr + 1);
+      }
+
+      break;
+    }
+
+    if (parent_path != NULL) {
+      parent_path = pdircat(tmp_pool, parent_path,
+        pstrndup(tmp_pool, component, component_len), NULL);
+
+    } else {
+      parent_path = pstrndup(tmp_pool, "/", 1);
+    }
+
+    if (last_component) {
+      break;
+    }
+
+    component = ptr + 1;
+  }
+
+  if (name_pattern == NULL) {
+    pr_trace_msg(trace_channel, 4,
+      "unable to process invalid, non-globbed path '%s'", path);
+    errno = ENOENT;
+    return -1;
+  }
+
+  pr_fs_clear_cache2(parent_path);
+  if (pr_fsio_lstat(parent_path, &st) < 0) {
+    xerrno = errno;
+
+    pr_log_pri(PR_LOG_WARNING,
+      "error: failed to check configuration path '%s': %s", parent_path,
+      strerror(xerrno));
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (S_ISLNK(st.st_mode) &&
+      !(parser_include_opts & PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS)) {
+    pr_log_pri(PR_LOG_WARNING,
+      "error: cannot read configuration path '%s': Symbolic link", parent_path);
+    destroy_pool(tmp_pool);
+    errno = ENOTDIR;
+    return -1;
+  }
+
+  pr_log_pri(PR_LOG_DEBUG,
+    "processing configuration directory '%s' using pattern '%s', suffix '%s'",
+    parent_path, name_pattern, suffix_path);
+
+  dirh = pr_fsio_opendir(parent_path);
+  if (dirh == NULL) {
+    pr_log_pri(PR_LOG_WARNING,
+      "error: unable to open configuration directory '%s': %s", parent_path,
+      strerror(errno));
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  globbed_dirs = make_array(tmp_pool, 0, sizeof(char *));
+
+  while ((dent = pr_fsio_readdir(dirh)) != NULL) {
+    pr_signals_handle();
+
+    if (strncmp(dent->d_name, ".", 2) == 0 ||
+        strncmp(dent->d_name, "..", 3) == 0) {
+      continue;
+    }
+
+    if (parser_include_opts & PR_PARSER_INCLUDE_OPT_IGNORE_TMP_FILES) {
+      if (is_tmp_file(dent->d_name) == TRUE) {
+        pr_trace_msg(trace_channel, 19,
+          "ignoring temporary file '%s' found in directory '%s'", dent->d_name,
+          parent_path);
+        continue;
+      }
+    }
+
+    if (pr_fnmatch(name_pattern, dent->d_name, PR_FNM_PERIOD) == 0) {
+      pr_trace_msg(trace_channel, 17,
+        "matched '%s' path with wildcard pattern '%s'", dent->d_name,
+        name_pattern);
+
+      *((char **) push_array(globbed_dirs)) = pdircat(tmp_pool, parent_path,
+        dent->d_name, suffix_path, NULL);
+    }
+  }
+
+  pr_fsio_closedir(dirh);
+
+  if (globbed_dirs->nelts == 0) {
+    pr_log_pri(PR_LOG_WARNING,
+      "error: no matches found for wildcard directory '%s'", path);
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return -1;
+  }
+
+  depth++;
+
+  qsort((void *) globbed_dirs->elts, globbed_dirs->nelts, sizeof(char *),
+    config_filename_cmp);
+
+  for (i = 0; i < globbed_dirs->nelts; i++) {
+    const char *globbed_dir;
+
+    globbed_dir = ((const char **) globbed_dirs->elts)[i];
+    res = parse_config_path2(p, globbed_dir, depth);
+    if (res < 0) {
+      xerrno = errno;
+
+      pr_trace_msg(trace_channel, 7, "error parsing wildcard path '%s': %s",
+        globbed_dir, strerror(xerrno));
+
+      destroy_pool(tmp_pool);
+      errno = xerrno;
+      return -1;
+    }
+  }
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int parse_config_path2(pool *p, const char *path, unsigned int depth) {
+  struct stat st;
+  int have_glob;
+  void *dirh;
+  struct dirent *dent;
+  array_header *file_list;
+  char *dup_path, *ptr;
+  pool *tmp_pool;
+
+  if (p == NULL ||
+      path == NULL ||
+      (depth > PR_PARSER_INCLUDE_MAX_DEPTH)) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (pr_fs_valid_path(path) < 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  have_glob = pr_str_is_fnmatch(path);
+  if (have_glob) {
+    /* Even though the path may be valid, it also may not be a filesystem
+     * path; consider custom FSIO modules.  Thus if the path does not start
+     * with a slash, it should not be treated as having globs.
+     */
+    if (*path != '/') {
+      have_glob = FALSE;
+    }
+  }
+
+  pr_fs_clear_cache2(path);
+
+  if (have_glob) {
+    pr_trace_msg(trace_channel, 19, "parsing '%s' as a globbed path", path);
+  }
+
+  if (!have_glob &&
+      pr_fsio_lstat(path, &st) < 0) {
+    return -1;
+  }
+
+  /* If path is not a glob pattern, and is a symlink OR is not a directory,
+   * then use the normal parsing function for the file.
+   */
+  if (have_glob == FALSE &&
+      (S_ISLNK(st.st_mode) ||
+       !S_ISDIR(st.st_mode))) {
+    int res, xerrno;
+
+    PRIVS_ROOT
+    res = pr_parser_parse_file(p, path, NULL, 0);
+    xerrno = errno;
+    PRIVS_RELINQUISH
+
+    errno = xerrno;
+    return res;
+  }
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "Include sub-pool");
+
+  /* Handle the glob/directory. */
+  dup_path = pstrdup(tmp_pool, path);
+
+  ptr = strrchr(dup_path, '/');
+
+  if (have_glob) {
+    int have_glob_dir;
+
+    /* Note that we know, by definition, that ptr CANNOT be null here; dup_path
+     * is a duplicate of path, and the first character (if nothing else) of
+     * path MUST be a slash, per earlier checks.
+     */
+    *ptr = '\0';
+
+    /* We just changed ptr, thus we DO need to check whether the now-modified
+     * path contains fnmatch(3) characters again.
+     */
+    have_glob_dir = pr_str_is_fnmatch(dup_path);
+    if (have_glob_dir) {
+      const char *glob_dir;
+
+      if (parser_include_opts & PR_PARSER_INCLUDE_OPT_IGNORE_WILDCARDS) {
+        pr_log_pri(PR_LOG_WARNING, "error: wildcard patterns not allowed in "
+          "configuration directory name '%s'", dup_path);
+        destroy_pool(tmp_pool);
+        errno = EINVAL;
+        return -1;
+      }
+
+      *ptr = '/';
+      glob_dir = pstrdup(p, dup_path);
+      destroy_pool(tmp_pool);
+
+      return parse_wildcard_config_path(p, glob_dir, depth);
+    }
+
+    ptr++;
+
+    /* Check the directory component. */
+    pr_fs_clear_cache2(dup_path);
+    if (pr_fsio_lstat(dup_path, &st) < 0) {
+      int xerrno = errno;
+
+      pr_log_pri(PR_LOG_WARNING,
+        "error: failed to check configuration path '%s': %s", dup_path,
+        strerror(xerrno));
+
+      destroy_pool(tmp_pool);
+      errno = xerrno;
+      return -1;
+    }
+
+    if (S_ISLNK(st.st_mode) &&
+        !(parser_include_opts & PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS)) {
+      pr_log_pri(PR_LOG_WARNING,
+        "error: cannot read configuration path '%s': Symbolic link", path);
+      destroy_pool(tmp_pool);
+      errno = ENOTDIR;
+      return -1;
+    }
+
+    if (have_glob_dir == FALSE &&
+        pr_str_is_fnmatch(ptr) == FALSE) {
+      pr_log_pri(PR_LOG_WARNING,
+        "error: wildcard pattern required for file '%s'", ptr);
+      destroy_pool(tmp_pool);
+      errno = EINVAL;
+      return -1;
+    }
+  }
+
+  pr_log_pri(PR_LOG_DEBUG, "processing configuration directory '%s'", dup_path);
+
+  dirh = pr_fsio_opendir(dup_path);
+  if (dirh == NULL) {
+    pr_log_pri(PR_LOG_WARNING,
+      "error: unable to open configuration directory '%s': %s", dup_path,
+      strerror(errno));
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  file_list = make_array(tmp_pool, 0, sizeof(char *));
+
+  while ((dent = pr_fsio_readdir(dirh)) != NULL) {
+    pr_signals_handle();
+
+    if (strncmp(dent->d_name, ".", 2) == 0 ||
+        strncmp(dent->d_name, "..", 3) == 0) {
+      continue;
+    }
+
+    if (parser_include_opts & PR_PARSER_INCLUDE_OPT_IGNORE_TMP_FILES) {
+      if (is_tmp_file(dent->d_name) == TRUE) {
+        pr_trace_msg(trace_channel, 19,
+          "ignoring temporary file '%s' found in directory '%s'", dent->d_name,
+          dup_path);
+        continue;
+      }
+    }
+
+    if (have_glob == FALSE ||
+        (ptr != NULL &&
+         pr_fnmatch(ptr, dent->d_name, PR_FNM_PERIOD) == 0)) {
+      *((char **) push_array(file_list)) = pdircat(tmp_pool, dup_path,
+        dent->d_name, NULL);
+    }
+  }
+
+  pr_fsio_closedir(dirh);
+
+  if (file_list->nelts) {
+    register unsigned int i;
+
+    qsort((void *) file_list->elts, file_list->nelts, sizeof(char *),
+      config_filename_cmp);
+
+    for (i = 0; i < file_list->nelts; i++) {
+      int res, xerrno;
+      char *file;
+
+      file = ((char **) file_list->elts)[i];
+
+      /* Make sure we always parse the files with root privs.  The
+       * previously parsed file might have had root privs relinquished
+       * (e.g. by its directive handlers), but when we first start up,
+       * we have root privs.  See Bug#3855.
+       */
+      PRIVS_ROOT
+      res = pr_parser_parse_file(tmp_pool, file, NULL, 0);
+      xerrno = errno;
+      PRIVS_RELINQUISH
+
+      if (res < 0) {
+        pr_log_pri(PR_LOG_WARNING,
+          "error: unable to open parse file '%s': %s", file,
+          strerror(xerrno));
+      }
+    }
+  }
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int parse_config_path(pool *p, const char *path) {
+  return parse_config_path2(p, path, 0);
+}
diff --git a/src/pidfile.c b/src/pidfile.c
index 9b8f33c..e21dbe7 100644
--- a/src/pidfile.c
+++ b/src/pidfile.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2009 The ProFTPD Project team
+ * Copyright (c) 2007-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,41 +22,54 @@
  * OpenSSL in the source distribution.
  */
 
-/* Pidfile management
- * $Id: pidfile.c,v 1.5 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Pidfile management */
 
 #include "conf.h"
 #include "privs.h"
 
 static const char *pidfile_path = PR_PID_FILE_PATH;
 
-void pr_pidfile_write(void) {
-  FILE *fh = NULL;
-  const char *path = NULL;
+const char *pr_pidfile_get(void) {
+  return pidfile_path;
+}
 
-  path = get_param_ptr(main_server->conf, "PidFile", FALSE);
-  if (path != NULL &&
-      *path) {
-    pidfile_path = pstrdup(permanent_pool, path);
+int pr_pidfile_set(const char *path) {
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-  } else {
-    path = pidfile_path;
+  /* Do not allow relative paths. */
+  if (*path != '/') {
+    errno = EINVAL;
+    return -1;
   }
 
+  pidfile_path = pstrdup(permanent_pool, path);
+  return 0;
+}
+
+int pr_pidfile_write(void) {
+  int xerrno;
+  FILE *fh = NULL;
+
   PRIVS_ROOT
-  fh = fopen(path, "w");
+  fh = fopen(pidfile_path, "w");
+  xerrno = errno;
   PRIVS_RELINQUISH
 
   if (fh == NULL) {
-    fprintf(stderr, "error opening PidFile '%s': %s\n", path, strerror(errno));
-    exit(1);
+    errno = xerrno;
+    return -1;
   }
 
   fprintf(fh, "%lu\n", (unsigned long) getpid());
   if (fclose(fh) < 0) {
-    fprintf(stderr, "error writing PidFile '%s': %s\n", path, strerror(errno));
+    fprintf(stderr, "error writing PidFile '%s': %s\n", pidfile_path,
+      strerror(errno));
   }
+
+  return 0;
 }
 
 int pr_pidfile_remove(void) {
diff --git a/src/pool.c b/src/pool.c
index f620694..d7ade94 100644
--- a/src/pool.c
+++ b/src/pool.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Resource allocation code
- * $Id: pool.c,v 1.72 2013-10-07 05:51:30 castaglia Exp $
- */
+/* Resource allocation code */
 
 #include "conf.h"
 
@@ -52,9 +50,9 @@ union block_hdr {
 
   /* Actual header */
   struct {
-    char *endp;
+    void *endp;
     union block_hdr *next;
-    char *first_avail;
+    void *first_avail;
   } h;
 };
 
@@ -65,6 +63,10 @@ static unsigned int stat_malloc = 0;	/* incr when malloc required */
 static unsigned int stat_freehit = 0;	/* incr when freelist used */
 
 #ifdef PR_USE_DEVEL
+static const char *trace_channel = "pool";
+#endif /* PR_USE_DEVEL */
+
+#ifdef PR_USE_DEVEL
 /* Debug flags */
 static int debug_flags = 0;
 
@@ -128,7 +130,7 @@ static union block_hdr *malloc_block(size_t size) {
 
   blok->h.next = NULL;
   blok->h.first_avail = (char *) (blok + 1);
-  blok->h.endp = size + blok->h.first_avail;
+  blok->h.endp = size + (char *) blok->h.first_avail;
 
   return blok;
 }
@@ -200,17 +202,16 @@ static union block_hdr *new_block(int minsz, int exact) {
   /* Check if we have anything of the requested size on our free list first...
    */
   while (blok) {
-    if (minsz <= blok->h.endp - blok->h.first_avail) {
+    if (minsz <= ((char *) blok->h.endp - (char *) blok->h.first_avail)) {
       *lastptr = blok->h.next;
       blok->h.next = NULL;
 
       stat_freehit++;
       return blok;
-
-    } else {
-      lastptr = &blok->h.next;
-      blok = blok->h.next;
     }
+
+    lastptr = &blok->h.next;
+    blok = blok->h.next;
   }
 
   /* Nope...damn.  Have to malloc() a new one. */
@@ -264,7 +265,7 @@ static unsigned long bytes_in_block_list(union block_hdr *blok) {
   unsigned long size = 0;
 
   while (blok) {
-    size += blok->h.endp - (char *) (blok + 1);
+    size += ((char *) blok->h.endp - (char *) (blok + 1));
     blok = blok->h.next;
   }
 
@@ -289,7 +290,7 @@ static unsigned int subpools_in_pool(pool *p) {
 /* Walk all pools, starting with top level permanent pool, displaying a
  * tree.
  */
-static long walk_pools(pool *p, int level,
+static long walk_pools(pool *p, unsigned long level,
     void (*debugf)(const char *, ...)) {
   char _levelpad[80] = "";
   long total = 0;
@@ -352,17 +353,39 @@ static void debug_pool_info(void (*debugf)(const char *, ...)) {
   debugf("%u blocks reused", stat_freehit);
 }
 
+static void pool_printf(const char *fmt, ...) {
+  char buf[PR_TUNABLE_BUFFER_SIZE];
+  va_list msg;
+
+  memset(buf, '\0', sizeof(buf));
+
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf), fmt, msg);
+  va_end(msg);
+
+  buf[sizeof(buf)-1] = '\0';
+  pr_trace_msg(trace_channel, 5, "%s", buf);
+}
+
 void pr_pool_debug_memory(void (*debugf)(const char *, ...)) {
+  if (debugf == NULL) {
+    debugf = pool_printf;
+  }
+
   debugf("Memory pool allocation:");
   debugf("Total %lu bytes allocated", walk_pools(permanent_pool, 0, debugf));
   debug_pool_info(debugf);
 }
 
 int pr_pool_debug_set_flags(int flags) {
+  if (flags < 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
   debug_flags = flags;
   return 0;
 }
-
 #endif /* PR_USE_DEVEL */
 
 void pr_pool_tag(pool *p, const char *tag) {
@@ -398,7 +421,7 @@ struct pool_rec *make_sub_pool(struct pool_rec *p) {
   blok = new_block(0, FALSE);
 
   new_pool = (pool *) blok->h.first_avail;
-  blok->h.first_avail += POOL_HDR_BYTES;
+  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;
 
   memset(new_pool, 0, sizeof(struct pool_rec));
   new_pool->free_first_avail = blok->h.first_avail;
@@ -428,7 +451,7 @@ struct pool_rec *pr_pool_create_sz(struct pool_rec *p, size_t sz) {
   blok = new_block(sz + POOL_HDR_BYTES, TRUE);
 
   new_pool = (pool *) blok->h.first_avail;
-  blok->h.first_avail += POOL_HDR_BYTES;
+  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;
 
   memset(new_pool, 0, sizeof(struct pool_rec));
   new_pool->free_first_avail = blok->h.first_avail;
@@ -533,18 +556,23 @@ void destroy_pool(pool *p) {
  */
 
 static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {
-
   /* Round up requested size to an even number of aligned units */
   size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);
   size_t sz = nclicks * CLICK_SZ;
+  union block_hdr *blok;
+  char *first_avail, *new_first_avail;
 
   /* For performance, see if space is available in the most recently
    * allocated block.
    */
 
-  union block_hdr *blok = p->last;
-  char *first_avail = blok->h.first_avail;
-  char *new_first_avail;
+  blok = p->last;
+  if (blok == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  first_avail = blok->h.first_avail;
 
   if (reqsz == 0) {
     /* Don't try to allocate memory of zero length.
@@ -558,7 +586,7 @@ static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {
 
   new_first_avail = first_avail + sz;
 
-  if (new_first_avail <= blok->h.endp) {
+  if (new_first_avail <= (char *) blok->h.endp) {
     blok->h.first_avail = new_first_avail;
     return (void *) first_avail;
   }
@@ -571,7 +599,7 @@ static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {
   p->last = blok;
 
   first_avail = blok->h.first_avail;
-  blok->h.first_avail += sz;
+  blok->h.first_avail = sz + (char *) blok->h.first_avail;
 
   pr_alarms_unblock();
   return (void *) first_avail;
@@ -655,25 +683,29 @@ void *push_array(array_header *arr) {
   return ((char *) arr->elts) + (arr->elt_size * (arr->nelts - 1));
 }
 
-void array_cat(array_header *dst, const array_header *src) {
+int array_cat2(array_header *dst, const array_header *src) {
   size_t elt_size;
 
   if (dst == NULL ||
       src == NULL) {
-    return;
+    errno = EINVAL;
+    return -1;
   }
 
   elt_size = dst->elt_size;
 
   if (dst->nelts + src->nelts > dst->nalloc) {
-    int new_size = dst->nalloc * 2;
+    size_t new_size;
     char *new_data;
 
-    if (new_size == 0)
+    new_size = dst->nalloc * 2;
+    if (new_size == 0) {
       ++new_size;
+    }
 
-    while ((dst->nelts + src->nelts) > new_size)
+    while ((dst->nelts + src->nelts) > new_size) {
       new_size *= 2;
+    }
 
     new_data = pcalloc(dst->pool, elt_size * new_size);
     memcpy(new_data, dst->elts, dst->nalloc * elt_size);
@@ -685,6 +717,12 @@ void array_cat(array_header *dst, const array_header *src) {
   memcpy(((char *) dst->elts) + (dst->nelts * elt_size), (char *) src->elts,
          elt_size * src->nelts);
   dst->nelts += src->nelts;
+
+  return 0;
+}
+
+void array_cat(array_header *dst, const array_header *src) {
+  (void) array_cat2(dst, src);
 }
 
 array_header *copy_array(pool *p, const array_header *arr) {
@@ -771,7 +809,13 @@ typedef struct cleanup {
 
 void register_cleanup(pool *p, void *data, void (*plain_cleanup_cb)(void*),
     void (*child_cleanup_cb)(void *)) {
-  cleanup_t *c = pcalloc(p, sizeof(cleanup_t));
+  cleanup_t *c;
+
+  if (p == NULL) {
+    return;
+  }
+
+  c = pcalloc(p, sizeof(cleanup_t));
   c->data = data;
   c->plain_cleanup_cb = plain_cleanup_cb;
   c->child_cleanup_cb = child_cleanup_cb;
@@ -782,12 +826,18 @@ void register_cleanup(pool *p, void *data, void (*plain_cleanup_cb)(void*),
 }
 
 void unregister_cleanup(pool *p, void *data, void (*cleanup_cb)(void *)) {
-  cleanup_t *c = p->cleanups;
-  cleanup_t **lastp = &p->cleanups;
+  cleanup_t *c, **lastp;
+
+  if (p == NULL) {
+    return;
+  }
+
+  c = p->cleanups;
+  lastp = &p->cleanups;
 
   while (c) {
     if (c->data == data &&
-        c->plain_cleanup_cb == cleanup_cb) {
+        (c->plain_cleanup_cb == cleanup_cb || cleanup_cb == NULL)) {
 
       /* Remove the given cleanup by pointing the previous next pointer to
        * the matching cleanup's next pointer.
@@ -803,7 +853,10 @@ void unregister_cleanup(pool *p, void *data, void (*cleanup_cb)(void *)) {
 
 static void run_cleanups(cleanup_t *c) {
   while (c) {
-    (*c->plain_cleanup_cb)(c->data);
+    if (c->plain_cleanup_cb) {
+      (*c->plain_cleanup_cb)(c->data);
+    }
+
     c = c->next;
   }
 }
diff --git a/src/privs.c b/src/privs.c
index 32dcd2b..8a86947 100644
--- a/src/privs.c
+++ b/src/privs.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2014 The ProFTPD Project team
+ * Copyright (c) 2009-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@
  * 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.
- *
- * $Id: privs.c,v 1.6 2012-12-29 00:45:04 castaglia Exp $
  */
 
 #include "conf.h"
@@ -234,8 +232,8 @@ int pr_privs_user(const char *file, int lineno) {
     return 0;
   }
 
-  pr_log_debug(DEBUG9, "USER PRIVS %lu at %s:%d",
-    (unsigned long) session.login_uid, file, lineno);
+  pr_log_debug(DEBUG9, "USER PRIVS %s at %s:%d",
+    pr_uid2str(NULL, session.login_uid), file, lineno);
 
   if (user_privs > 0) {
     pr_trace_msg(trace_channel, 9, "user privs count = %u, ignoring PRIVS_USER",
@@ -485,10 +483,26 @@ int pr_privs_revoke(const char *file, int lineno) {
   return 0;
 }
 
+/* Returns the previous value, or -1 on error. */
+int set_nonroot_daemon(int nonroot) {
+  int was_nonroot;
+
+  if (nonroot != TRUE &&
+      nonroot != FALSE) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  was_nonroot = nonroot_daemon;
+  nonroot_daemon = nonroot;
+
+  return was_nonroot;
+}
+
 int init_privs(void) {
   /* Check to see if we have real root privs. */
   if (getuid() != PR_ROOT_UID) {
-    nonroot_daemon = TRUE;
+    set_nonroot_daemon(TRUE);
   }
 
   return 0;
diff --git a/src/proctitle.c b/src/proctitle.c
index dcace13..ec41aaf 100644
--- a/src/proctitle.c
+++ b/src/proctitle.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2007-2012 The ProFTPD Project team
+ * Copyright (c) 2007-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Proctitle management
- * $Id: proctitle.c,v 1.13 2012-04-15 18:04:15 castaglia Exp $
- */
+/* Proctitle management */
 
 #include "conf.h"
 
@@ -52,10 +50,8 @@ extern char *__progname, *__progname_full;
 #endif /* HAVE___PROGNAME */
 extern char **environ;
 
-#ifndef PR_DEVEL_STACK_TRACE
 static char **prog_argv = NULL;
 static char *prog_last_argv = NULL;
-#endif /* PR_DEVEL_STACK_TRACE */
 
 static int prog_argc = -1;
 static char proc_title_buf[BUFSIZ];
@@ -64,16 +60,17 @@ static unsigned int proc_flags = 0;
 #define PR_PROCTITLE_FL_USE_STATIC		0x001
 
 void pr_proctitle_init(int argc, char *argv[], char *envp[]) {
-#ifndef PR_DEVEL_STACK_TRACE
   register int i;
   register size_t envpsize;
   char **p;
 
   /* Move the environment so setproctitle can use the space. */
-  for (i = envpsize = 0; envp[i] != NULL; i++)
+  for (i = envpsize = 0; envp[i] != NULL; i++) {
     envpsize += strlen(envp[i]) + 1;
+  }
 
-  if ((p = (char **) malloc((i + 1) * sizeof(char *))) != NULL) {
+  p = (char **) malloc((i + 1) * sizeof(char *));
+  if (p != NULL) {
     environ = p;
 
     for (i = 0; envp[i] != NULL; i++) {
@@ -97,63 +94,56 @@ void pr_proctitle_init(int argc, char *argv[], char *envp[]) {
   prog_argc = argc;
 
   for (i = 0; i < prog_argc; i++) {
-    if (!i || (prog_last_argv + 1 == argv[i]))
+    if (!i || (prog_last_argv + 1 == argv[i])) {
       prog_last_argv = argv[i] + strlen(argv[i]);
+    }
   }
 
   for (i = 0; envp[i] != NULL; i++) {
-    if ((prog_last_argv + 1) == envp[i])
+    if ((prog_last_argv + 1) == envp[i]) {
       prog_last_argv = envp[i] + strlen(envp[i]);
+    }
   }
 
-# ifdef HAVE___PROGNAME
+#ifdef HAVE___PROGNAME
   /* Set the __progname and __progname_full variables so glibc and company
    * don't go nuts.
    */
   __progname = strdup("proftpd");
   __progname_full = strdup(argv[0]);
-# endif /* HAVE___PROGNAME */
-#else
-  /* Silence compiler warning about unused variable when stacktrace
-   * developer mode is configured.
-   */
-  prog_argc = -1;
-#endif /* !PR_DEVEL_STACK_TRACE */
+#endif /* HAVE___PROGNAME */
   memset(proc_title_buf, '\0', sizeof(proc_title_buf));
 }
 
 void pr_proctitle_free(void) {
 #ifdef PR_USE_DEVEL
-# ifndef PR_DEVEL_STACK_TRACE
   if (environ) {
     register unsigned int i;
 
-    for (i = 0; environ[i] != NULL; i++)
+    for (i = 0; environ[i] != NULL; i++) {
       free(environ[i]);
+    }
     free(environ);
     environ = NULL;
   }
 
-#  ifdef HAVE___PROGNAME
+# ifdef HAVE___PROGNAME
   free(__progname);
   __progname = NULL;
   free(__progname_full);
   __progname_full = NULL;
-#  endif /* HAVE___PROGNAME */
-# endif /* !PR_DEVEL_STACK_TRACE */
+# endif /* HAVE___PROGNAME */
 #endif /* PR_USE_DEVEL */
 }
 
 void pr_proctitle_set_str(const char *str) {
-#ifndef PR_DEVEL_STACK_TRACE
-
-# ifndef HAVE_SETPROCTITLE
+#ifndef HAVE_SETPROCTITLE
   char *p;
   int i, procbuflen, maxlen = (prog_last_argv - prog_argv[0]) - 2;
 
-#  if PF_ARGV_TYPE == PF_ARGV_PSTAT
+# if PF_ARGV_TYPE == PF_ARGV_PSTAT
   union pstun pst;
-#  endif /* PF_ARGV_PSTAT */
+# endif /* PF_ARGV_PSTAT */
 
   if (proc_flags & PR_PROCTITLE_FL_USE_STATIC) {
     return;
@@ -162,130 +152,129 @@ void pr_proctitle_set_str(const char *str) {
   sstrncpy(proc_title_buf, str, sizeof(proc_title_buf));
   procbuflen = strlen(proc_title_buf);
 
-#  if PF_ARGV_TYPE == PF_ARGV_NEW
+# if PF_ARGV_TYPE == PF_ARGV_NEW
   /* We can just replace argv[] arguments.  Nice and easy. */
   prog_argv[0] = proc_title_buf;
   for (i = 1; i < prog_argc; i++) {
     prog_argv[i] = "";
   }
-#  endif /* PF_ARGV_NEW */
+# endif /* PF_ARGV_NEW */
 
-#  if PF_ARGV_TYPE == PF_ARGV_WRITEABLE
+# if PF_ARGV_TYPE == PF_ARGV_WRITEABLE
   /* We can overwrite individual argv[] arguments.  Semi-nice. */
   snprintf(prog_argv[0], maxlen, "%s", proc_title_buf);
   p = &prog_argv[0][procbuflen];
 
-  while (p < prog_last_argv)
+  while (p < prog_last_argv) {
     *p++ = '\0';
+  }
 
   for (i = 1; i < prog_argc; i++) {
     prog_argv[i] = "";
   }
 
-#  endif /* PF_ARGV_WRITEABLE */
+# endif /* PF_ARGV_WRITEABLE */
 
-#  if PF_ARGV_TYPE == PF_ARGV_PSSTRINGS
+# if PF_ARGV_TYPE == PF_ARGV_PSSTRINGS
   PS_STRINGS->ps_nargvstr = 1;
   PS_STRINGS->ps_argvstr = proc_title_buf;
-#  endif /* PF_ARGV_PSSTRINGS */
+# endif /* PF_ARGV_PSSTRINGS */
 
-# else
+#else
   if (proc_flags & PR_PROCTITLE_FL_USE_STATIC) {
     return;
   }
 
   setproctitle("%s", str);
-# endif /* HAVE_SETPROCTITLE */
-#endif /* PR_DEVEL_STACK_TRACE */
+#endif /* HAVE_SETPROCTITLE */
 }
 
 void pr_proctitle_set(const char *fmt, ...) {
-#ifndef PR_DEVEL_STACK_TRACE
   va_list msg;
 
-# ifndef HAVE_SETPROCTITLE
-#  if PF_ARGV_TYPE == PF_ARGV_PSTAT
+#ifndef HAVE_SETPROCTITLE
+# if PF_ARGV_TYPE == PF_ARGV_PSTAT
   union pstun pst;
-#  endif /* PF_ARGV_PSTAT */
+# endif /* PF_ARGV_PSTAT */
   char *p;
   int i, procbuflen, maxlen = (prog_last_argv - prog_argv[0]) - 2;
-# endif /* HAVE_SETPROCTITLE */
+#endif /* HAVE_SETPROCTITLE */
 
   if (proc_flags & PR_PROCTITLE_FL_USE_STATIC) {
     return;
   }
 
-  if (!fmt)
+  if (fmt == NULL) {
     return;
+  }
 
   va_start(msg, fmt);
 
   memset(proc_title_buf, 0, sizeof(proc_title_buf));
 
-# ifdef HAVE_SETPROCTITLE
-#  if __FreeBSD__ >= 4 && !defined(FREEBSD4_0) && !defined(FREEBSD4_1)
+#ifdef HAVE_SETPROCTITLE
+# if __FreeBSD__ >= 4 && !defined(FREEBSD4_0) && !defined(FREEBSD4_1)
   /* FreeBSD's setproctitle() automatically prepends the process name. */
   vsnprintf(proc_title_buf, sizeof(proc_title_buf), fmt, msg);
 
-#  else /* FREEBSD4 */
+# else /* FREEBSD4 */
   /* Manually append the process name for non-FreeBSD platforms. */
   snprintf(proc_title_buf, sizeof(proc_title_buf), "%s", "proftpd: ");
   vsnprintf(proc_title_buf + strlen(proc_title_buf),
     sizeof(proc_title_buf) - strlen(proc_title_buf), fmt, msg);
 
-#  endif /* FREEBSD4 */
+# endif /* FREEBSD4 */
   setproctitle("%s", proc_title_buf);
 
-# else /* HAVE_SETPROCTITLE */
+#else /* HAVE_SETPROCTITLE */
   /* Manually append the process name for non-setproctitle() platforms. */
   snprintf(proc_title_buf, sizeof(proc_title_buf), "%s", "proftpd: ");
   vsnprintf(proc_title_buf + strlen(proc_title_buf),
     sizeof(proc_title_buf) - strlen(proc_title_buf), fmt, msg);
 
-# endif /* HAVE_SETPROCTITLE */
+#endif /* HAVE_SETPROCTITLE */
 
   va_end(msg);
 
-# ifdef HAVE_SETPROCTITLE
+#ifdef HAVE_SETPROCTITLE
   return;
-# else
+#else
   procbuflen = strlen(proc_title_buf);
 
-#  if PF_ARGV_TYPE == PF_ARGV_NEW
+# if PF_ARGV_TYPE == PF_ARGV_NEW
   /* We can just replace argv[] arguments.  Nice and easy. */
   prog_argv[0] = proc_title_buf;
   for (i = 1; i < prog_argc; i++) {
     prog_argv[i] = "";
   }
-#  endif /* PF_ARGV_NEW */
+# endif /* PF_ARGV_NEW */
 
-#  if PF_ARGV_TYPE == PF_ARGV_WRITEABLE
+# if PF_ARGV_TYPE == PF_ARGV_WRITEABLE
   /* We can overwrite individual argv[] arguments.  Semi-nice. */
   snprintf(prog_argv[0], maxlen, "%s", proc_title_buf);
   p = &prog_argv[0][procbuflen];
 
-  while (p < prog_last_argv)
+  while (p < prog_last_argv) {
     *p++ = '\0';
+  }
 
   for (i = 1; i < prog_argc; i++) {
     prog_argv[i] = "";
   }
 
-#  endif /* PF_ARGV_WRITEABLE */
+# endif /* PF_ARGV_WRITEABLE */
 
-#  if PF_ARGV_TYPE == PF_ARGV_PSTAT
+# if PF_ARGV_TYPE == PF_ARGV_PSTAT
   pst.pst_command = proc_title_buf;
   pstat(PSTAT_SETCMD, pst, procbuflen, 0, 0);
 
-#  endif /* PF_ARGV_PSTAT */
+# endif /* PF_ARGV_PSTAT */
 
-#  if PF_ARGV_TYPE == PF_ARGV_PSSTRINGS
+# if PF_ARGV_TYPE == PF_ARGV_PSSTRINGS
   PS_STRINGS->ps_nargvstr = 1;
   PS_STRINGS->ps_argvstr = proc_title_buf;
-#  endif /* PF_ARGV_PSSTRINGS */
-
-# endif /* HAVE_SETPROCTITLE */
-#endif /* !PR_DEVEL_STACK_TRACE */
+# endif /* PF_ARGV_PSSTRINGS */
+#endif /* HAVE_SETPROCTITLE */
 }
 
 void pr_proctitle_set_static_str(const char *buf) {
diff --git a/src/proftpd.8.in b/src/proftpd.8.in
index 49de980..909692d 100644
--- a/src/proftpd.8.in
+++ b/src/proftpd.8.in
@@ -78,9 +78,6 @@ than the default configuration file.  The default configuration file is
 .BI \-N,\--nocollision
 Disables address/port collision checking.
 .TP
-.BI \-V,\--settings
-Displays various compile-time settings and exits.
-.TP
 .BI \-S,\--serveraddr
 Specifies an IP address for the host machine, avoiding an DNS lookup of the hostname
 .TP
@@ -96,6 +93,12 @@ the \fBPersistentPasswd\fP directive.
 .BI \-l,\--list
 Lists all modules compiled into proftpd.
 .TP
+.BI \-V,\--settings
+Displays various compile-time settings and exits.
+.TP
+.BI \-X,\--nofork
+Debug mode (do not fork a session process); exits after one session.
+.TP
 .BI \-4,\--ipv4
 Support IPv4 functionality \fBonly\fP, regardless of whether the
 \fB--enable-ipv6\fP configure option was used.
diff --git a/src/redis.c b/src/redis.c
new file mode 100644
index 0000000..92cb25a
--- /dev/null
+++ b/src/redis.c
@@ -0,0 +1,5708 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Redis management */
+
+#include "conf.h"
+
+#ifdef PR_USE_REDIS
+
+#include <hiredis/hiredis.h>
+
+struct redis_rec {
+  pool *pool;
+  module *owner;
+  redisContext *ctx;
+
+  /* For tracking the number of "opens"/"closes" on a shared redis_rec,
+   * as the same struct might be used by multiple modules in the same
+   * session, each module doing a conn_get()/conn_close().
+   */
+  unsigned int refcount;
+
+  /* Table mapping modules to their namespaces */
+  pr_table_t *namespace_tab;
+};
+
+static const char *redis_server = NULL;
+static int redis_port = -1;
+static const char *redis_password = NULL;
+
+static pr_redis_t *sess_redis = NULL;
+
+static unsigned long redis_connect_millis = 500;
+static unsigned long redis_io_millis = 500;
+
+static const char *trace_channel = "redis";
+
+static void millis2timeval(struct timeval *tv, unsigned long millis) {
+  tv->tv_sec = (millis / 1000);
+  tv->tv_usec = (millis - (tv->tv_sec * 1000)) * 1000;
+}
+
+static const char *redis_strerror(pool *p, pr_redis_t *redis, int rerrno) {
+  const char *err;
+
+  switch (redis->ctx->err) {
+    case REDIS_ERR_IO:
+      err = pstrcat(p, "[io] ", strerror(rerrno), NULL);
+      break;
+
+    case REDIS_ERR_EOF:
+      err = pstrcat(p, "[eof] ", redis->ctx->errstr, NULL);
+      break;
+
+    case REDIS_ERR_PROTOCOL:
+      err = pstrcat(p, "[protocol] ", redis->ctx->errstr, NULL);
+      break;
+
+    case REDIS_ERR_OOM:
+      err = pstrcat(p, "[oom] ", redis->ctx->errstr, NULL);
+      break;
+
+    case REDIS_ERR_OTHER:
+      err = pstrcat(p, "[other] ", redis->ctx->errstr, NULL);
+      break;
+
+    case REDIS_OK:
+    default:
+      err = "OK";
+      break;
+  }
+
+  return err;
+}
+
+static int ping_server(pr_redis_t *redis) {
+  const char *cmd;
+  redisReply *reply;
+
+  cmd = "PING";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s", cmd);
+  if (reply == NULL) {
+    int xerrno;
+    pool *tmp_pool;
+
+    xerrno = errno;
+    tmp_pool = make_sub_pool(redis->pool);
+    pr_trace_msg(trace_channel, 2, "error sending %s command: %s", cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  /* We COULD assert a "PONG" response here, but really, anything is OK. */
+  pr_trace_msg(trace_channel, 7, "%s reply: %s", cmd, reply->str);
+  freeReplyObject(reply);
+  return 0;
+}
+
+static int stat_server(pr_redis_t *redis, const char *section) {
+  const char *cmd;
+  redisReply *reply;
+
+  cmd = "INFO";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %s", cmd, section);
+  if (reply == NULL) {
+    int xerrno;
+    pool *tmp_pool;
+
+    xerrno = errno;
+    tmp_pool = make_sub_pool(redis->pool);
+    pr_trace_msg(trace_channel, 2, "error sending %s command: %s", cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (pr_trace_get_level(trace_channel) >= 25) {
+    pr_trace_msg(trace_channel, 25, "%s reply: %s", cmd, reply->str);
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: (text, %lu bytes)", cmd,
+      (unsigned long) reply->len);
+  }
+
+  freeReplyObject(reply);
+  return 0;
+}
+
+pr_redis_t *pr_redis_conn_get(pool *p) {
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (sess_redis != NULL) {
+    sess_redis->refcount++;
+    return sess_redis;
+  }
+
+  return pr_redis_conn_new(p, NULL, 0UL);
+}
+
+static int set_conn_options(pr_redis_t *redis, unsigned long flags) {
+  int res, xerrno;
+  struct timeval tv;
+  pool *tmp_pool;
+
+  tmp_pool = make_sub_pool(redis->pool);
+
+  millis2timeval(&tv, redis_io_millis);
+  res = redisSetTimeout(redis->ctx, tv);
+  if (res == REDIS_ERR) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 4,
+      "error setting %lu ms timeout: %s", redis_io_millis,
+      redis_strerror(tmp_pool, redis, xerrno));
+  }
+
+#if HIREDIS_MAJOR >= 0 && \
+    HIREDIS_MINOR >= 12
+  res = redisEnableKeepAlive(redis->ctx);
+  if (res == REDIS_ERR) {
+    xerrno = errno;
+
+    pr_trace_msg(trace_channel, 4,
+      "error setting keepalive: %s", redis_strerror(tmp_pool, redis, xerrno));
+  }
+#endif /* HiRedis 0.12.0 and later */
+
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+static void sess_redis_cleanup(void *data) {
+  sess_redis = NULL;
+}
+
+pr_redis_t *pr_redis_conn_new(pool *p, module *m, unsigned long flags) {
+  int uses_ip = TRUE, res, xerrno;
+  pr_redis_t *redis;
+  pool *sub_pool;
+  redisContext *ctx;
+  struct timeval tv;
+
+  if (p == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (redis_server == NULL) {
+    pr_trace_msg(trace_channel, 9, "%s",
+      "unable to create new Redis connection: No server configured");
+    errno = EPERM;
+    return NULL;
+  }
+
+  millis2timeval(&tv, redis_connect_millis); 
+
+  /* If the given redis "server" string starts with a '/' character, assume
+   * that it is a Unix socket path.
+   */
+  if (*redis_server == '/') {
+    uses_ip = FALSE;
+    ctx = redisConnectUnixWithTimeout(redis_server, tv);
+
+  } else {
+    ctx = redisConnectWithTimeout(redis_server, redis_port, tv);
+  }
+
+  xerrno = errno;
+
+  if (ctx == NULL) {
+    errno = ENOMEM;
+    return NULL;
+  }
+
+  if (ctx->err != 0) {
+    const char *err_type, *err_msg;
+
+    switch (ctx->err) {
+      case REDIS_ERR_IO:
+        err_type = "io";
+        err_msg = strerror(xerrno);
+        break;
+
+      case REDIS_ERR_EOF:
+        err_type = "eof";
+        err_msg = ctx->errstr;
+        break;
+
+      case REDIS_ERR_PROTOCOL:
+        err_type = "protocol";
+        err_msg = ctx->errstr;
+        break;
+
+      case REDIS_ERR_OOM:
+        err_type = "oom";
+        err_msg = ctx->errstr;
+        break;
+
+      case REDIS_ERR_OTHER:
+        err_type = "other";
+        err_msg = ctx->errstr;
+        break;
+
+      default:
+        err_type = "unknown";
+        err_msg = ctx->errstr;
+        break;
+    }
+
+    if (uses_ip == TRUE) {
+      pr_trace_msg(trace_channel, 3,
+        "error connecting to %s#%d: [%s] %s", redis_server, redis_port,
+        err_type, err_msg);
+
+    } else {
+      pr_trace_msg(trace_channel, 3,
+        "error connecting to '%s': [%s] %s", redis_server, err_type, err_msg);
+    }
+
+    redisFree(ctx);
+    errno = EIO;
+    return NULL;
+  }
+
+  sub_pool = make_sub_pool(p);
+  pr_pool_tag(sub_pool, "Redis connection pool");
+
+  redis = pcalloc(sub_pool, sizeof(pr_redis_t));
+  redis->pool = sub_pool;
+  redis->owner = m;
+  redis->ctx = ctx;
+  redis->refcount = 1;
+
+  /* The namespace table is null; it will be created if/when callers
+   * configure namespace prefixes.
+   */
+  redis->namespace_tab = NULL;
+
+  /* Set some of the desired behavior flags on the connection */
+  res = set_conn_options(redis, flags);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_redis_conn_destroy(redis);
+    errno = xerrno;
+    return NULL;    
+  }
+
+  res = ping_server(redis);
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_redis_conn_destroy(redis);
+    errno = xerrno;
+    return NULL;
+  }
+
+  /* Make sure we are connected to the configured server by querying
+   * some stats/info from it.
+   */
+  res = stat_server(redis, "server");
+  if (res < 0) {
+    xerrno = errno;
+
+    pr_redis_conn_destroy(redis);
+    errno = xerrno;
+    return NULL;    
+  }
+
+  if (sess_redis == NULL) {
+    sess_redis = redis;
+
+    /* Register a cleanup on this redis, so that when it is destroyed, we
+     * clear this sess_redis pointer, lest it remaining dangling.
+     */
+    register_cleanup(redis->pool, NULL, sess_redis_cleanup, NULL);
+  }
+
+  return redis;
+}
+
+/* Return TRUE if we actually closed the connection, FALSE if we simply
+ * decremented the refcount.
+ */
+int pr_redis_conn_close(pr_redis_t *redis) {
+  int closed = FALSE;
+
+  if (redis == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (redis->refcount > 0) {
+    redis->refcount--;
+  }
+
+  if (redis->refcount == 0) {
+    if (redis->ctx != NULL) {
+      const char *cmd = NULL;
+      redisReply *reply;
+
+      cmd = "QUIT";
+      pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+      reply = redisCommand(redis->ctx, "%s", cmd);
+      if (reply != NULL) {
+        freeReplyObject(reply);
+      }
+
+      redisFree(redis->ctx);
+      redis->ctx = NULL;
+    }
+
+    if (redis->namespace_tab != NULL) {
+      (void) pr_table_empty(redis->namespace_tab);
+      (void) pr_table_free(redis->namespace_tab);
+      redis->namespace_tab = NULL;
+    }
+
+    closed = TRUE;
+  }
+
+  return closed;
+}
+
+/* Return TRUE if we actually closed the connection, FALSE if we simply
+ * decremented the refcount.
+ */
+int pr_redis_conn_destroy(pr_redis_t *redis) {
+  int closed, destroyed = FALSE;
+
+  if (redis == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  closed = pr_redis_conn_close(redis);
+  if (closed < 0) {
+    return -1;
+  }
+
+  if (closed == TRUE) {
+    if (redis == sess_redis) {
+      sess_redis = NULL;
+    }
+
+    destroy_pool(redis->pool);
+    destroyed = TRUE;
+  }
+
+  return destroyed;
+}
+
+static int modptr_cmp_cb(const void *k1, size_t ksz1, const void *k2,
+    size_t ksz2) {
+
+  /* Return zero to indicate a match, non-zero otherwise. */
+  return (((module *) k1) == ((module *) k2) ? 0 : 1);
+}
+
+static unsigned int modptr_hash_cb(const void *k, size_t ksz) {
+  unsigned int key = 0;
+
+  /* XXX Yes, this is a bit hacky for "hashing" a pointer value. */
+
+  memcpy(&key, k, sizeof(key));
+  key ^= (key >> 16);
+
+  return key;
+}
+
+int pr_redis_conn_set_namespace(pr_redis_t *redis, module *m,
+    const void *prefix, size_t prefixsz) {
+
+  if (redis == NULL ||
+      m == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (prefix != NULL &&
+      prefixsz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (redis->namespace_tab == NULL) {
+    pr_table_t *tab;
+
+    tab = pr_table_alloc(redis->pool, 0);
+
+    (void) pr_table_ctl(tab, PR_TABLE_CTL_SET_KEY_CMP, modptr_cmp_cb);
+    (void) pr_table_ctl(tab, PR_TABLE_CTL_SET_KEY_HASH, modptr_hash_cb);
+    redis->namespace_tab = tab;
+  }
+
+  if (prefix != NULL) {
+    int count;
+    void *val;
+    size_t valsz;
+
+    valsz = prefixsz;
+    val = palloc(redis->pool, valsz);
+    memcpy(val, prefix, prefixsz);
+
+    count = pr_table_kexists(redis->namespace_tab, m, sizeof(module *));
+    if (count <= 0) {
+      (void) pr_table_kadd(redis->namespace_tab, m, sizeof(module *), val,
+        valsz);
+
+    } else {
+      (void) pr_table_kset(redis->namespace_tab, m, sizeof(module *), val,
+        valsz);
+    }
+
+  } else {
+    /* A NULL prefix means the caller is removing their namespace maping. */
+    (void) pr_table_kremove(redis->namespace_tab, m, sizeof(module *), NULL);
+  }
+
+  return 0;
+}
+
+int pr_redis_add(pr_redis_t *redis, module *m, const char *key, void *value,
+    size_t valuesz, time_t expires) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_kadd(redis, m, key, strlen(key), value, valuesz, expires);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error adding key '%s', value (%lu bytes): %s", key,
+      (unsigned long) valuesz, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_decr(pr_redis_t *redis, module *m, const char *key, uint32_t decr,
+    uint64_t *value) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_kdecr(redis, m, key, strlen(key), decr, value);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error decrementing key '%s' by %lu: %s", key,
+      (unsigned long) decr, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+void *pr_redis_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t *valuesz) {
+  void *ptr = NULL;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  ptr = pr_redis_kget(p, redis, m, key, strlen(key), valuesz);
+  if (ptr == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting data for key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return NULL;
+  }
+
+  return ptr;
+}
+
+char *pr_redis_get_str(pool *p, pr_redis_t *redis, module *m, const char *key) {
+  char *ptr = NULL;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  ptr = pr_redis_kget_str(p, redis, m, key, strlen(key));
+  if (ptr == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting data for key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno; 
+    return NULL;
+  }
+
+  return ptr;
+}
+
+int pr_redis_incr(pr_redis_t *redis, module *m, const char *key, uint32_t incr,
+    uint64_t *value) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_kincr(redis, m, key, strlen(key), incr, value);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing key '%s' by %lu: %s", key,
+      (unsigned long) incr, strerror(xerrno));
+ 
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_remove(pr_redis_t *redis, module *m, const char *key) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_kremove(redis, m, key, strlen(key));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error removing key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_rename(pr_redis_t *redis, module *m, const char *from,
+    const char *to) {
+  int res;
+
+  if (from == NULL ||
+      to == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_krename(redis, m, from, strlen(from), to, strlen(to));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error renaming key '%s' to '%s': %s", from, to, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_set(pr_redis_t *redis, module *m, const char *key, void *value,
+    size_t valuesz, time_t expires) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_kset(redis, m, key, strlen(key), value, valuesz, expires);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting key '%s', value (%lu bytes): %s", key,
+      (unsigned long) valuesz, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+/* Hash operations */
+int pr_redis_hash_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kcount(redis, m, key, strlen(key), count);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error counting hash using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_delete(pr_redis_t *redis, module *m, const char *key,
+    const char *field) {
+  int res;
+
+  if (key == NULL ||
+      field == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kdelete(redis, m, key, strlen(key), field, strlen(field));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error deleting field from hash using key '%s', field '%s': %s", key,
+      field, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_exists(pr_redis_t *redis, module *m, const char *key,
+    const char *field) {
+  int res;
+
+  if (key == NULL ||
+      field == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kexists(redis, m, key, strlen(key), field, strlen(field));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error checking existence of hash using key '%s', field '%s': %s", key,
+      field, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_hash_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+    const char *field, void **value, size_t *valuesz) {
+  int res;
+
+  if (key == NULL ||
+      field == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kget(p, redis, m, key, strlen(key), field, strlen(field),
+    value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting field from hash using key '%s', field '%s': %s", key,
+      field, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_getall(pool *p, pr_redis_t *redis, module *m,
+    const char *key, pr_table_t **hash) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kgetall(p, redis, m, key, strlen(key), hash);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error entire hash using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_incr(pr_redis_t *redis, module *m, const char *key,
+    const char *field, int32_t incr, int64_t *value) {
+  int res;
+
+  if (key == NULL ||
+      field == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kincr(redis, m, key, strlen(key), field, strlen(field),
+    incr, value);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing field in hash using key '%s', field '%s': %s", key,
+      field, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_keys(pool *p, pr_redis_t *redis, module *m, const char *key,
+    array_header **fields) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kkeys(p, redis, m, key, strlen(key), fields);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error obtaining keys from hash using key '%s': %s", key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_remove(pr_redis_t *redis, module *m, const char *key) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kremove(redis, m, key, strlen(key));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error removing hash using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_set(pr_redis_t *redis, module *m, const char *key,
+    const char *field, void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL ||
+      field == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kset(redis, m, key, strlen(key), field, strlen(field),
+    value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting field in hash using key '%s', field '%s': %s", key, field,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_setall(pr_redis_t *redis, module *m, const char *key,
+    pr_table_t *hash) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_ksetall(redis, m, key, strlen(key), hash);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting hash using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_values(pool *p, pr_redis_t *redis, module *m,
+    const char *key, array_header **values) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_hash_kvalues(p, redis, m, key, strlen(key), values);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting values of hash using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+/* List operations */
+int pr_redis_list_append(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kappend(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error appending to list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kcount(redis, m, key, strlen(key), count);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error counting list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_delete(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (value == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kdelete(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error deleting item from list using key '%s': %s", key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_exists(pr_redis_t *redis, module *m, const char *key,
+    unsigned int idx) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kexists(redis, m, key, strlen(key), idx);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error checking item at index %u in list using key '%s': %s", idx, key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_list_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+    unsigned int idx, void **value, size_t *valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kget(p, redis, m, key, strlen(key), idx, value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting item at index %u in list using key '%s': %s", idx, key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_list_getall(pool *p, pr_redis_t *redis, module *m, const char *key,
+    array_header **values, array_header **valueszs) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kgetall(p, redis, m, key, strlen(key), values, valueszs);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting items in list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_list_pop(pool *p, pr_redis_t *redis, module *m, const char *key,
+    void **value, size_t *valuesz, int flags) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kpop(p, redis, m, key, strlen(key), value, valuesz,
+    flags);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error popping item from list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_list_push(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, int flags) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kpush(redis, m, key, strlen(key), value, valuesz, flags);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error pushing item into list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_remove(pr_redis_t *redis, module *m, const char *key) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kremove(redis, m, key, strlen(key));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error removing list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_rotate(pool *p, pr_redis_t *redis, module *m,
+    const char *key, void **value, size_t *valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_krotate(p, redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error rotating list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_set(pr_redis_t *redis, module *m, const char *key,
+    unsigned int idx, void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_kset(redis, m, key, strlen(key), idx, value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting item in list using key '%s', index %u: %s", key, idx,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_setall(pr_redis_t *redis, module *m, const char *key,
+    array_header *values, array_header *valueszs) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_list_ksetall(redis, m, key, strlen(key), values, valueszs);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting items in list using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+/* Set operations */
+int pr_redis_set_add(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_kadd(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error adding item to set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_set_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_kcount(redis, m, key, strlen(key), count);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error counting set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_set_delete(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_kdelete(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error deleting item from set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_set_exists(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_kexists(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error checking item in set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_set_getall(pool *p, pr_redis_t *redis, module *m, const char *key,
+    array_header **values, array_header **valueszs) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_kgetall(p, redis, m, key, strlen(key), values, valueszs);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting items in set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_set_remove(pr_redis_t *redis, module *m, const char *key) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_kremove(redis, m, key, strlen(key));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error removing set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_set_setall(pr_redis_t *redis, module *m, const char *key,
+    array_header *values, array_header *valueszs) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_set_ksetall(redis, m, key, strlen(key), values, valueszs);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting items in set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+/* Sorted Set operations */
+int pr_redis_sorted_set_add(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float score) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kadd(redis, m, key, strlen(key), value, valuesz,
+    score);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error adding item with score %0.3f to sorted set using key '%s': %s",
+      score, key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_sorted_set_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kcount(redis, m, key, strlen(key), count);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error counting sorted set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_sorted_set_delete(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kdelete(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error deleting item from sorted set using key '%s': %s", key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_sorted_set_exists(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kexists(redis, m, key, strlen(key), value, valuesz);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error checking item in sorted set using key '%s': %s", key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_sorted_set_getn(pool *p, pr_redis_t *redis, module *m,
+    const char *key, unsigned int offset, unsigned int len,
+    array_header **values, array_header **valueszs, int flags) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kgetn(p, redis, m, key, strlen(key), offset, len,
+    values, valueszs, flags);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting %u %s from sorted set using key '%s': %s", len,
+      len != 1 ? "items" : "item", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_sorted_set_incr(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float incr, float *score) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kincr(redis, m, key, strlen(key), value, valuesz,
+    incr, score);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing item by %0.3f in sorted set using key '%s': %s",
+      incr, key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_sorted_set_remove(pr_redis_t *redis, module *m, const char *key) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kremove(redis, m, key, strlen(key));
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error removing sorted set using key '%s': %s", key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_sorted_set_score(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float *score) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kscore(redis, m, key, strlen(key), value, valuesz,
+    score);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error getting score for item in sorted set using key '%s': %s", key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
+int pr_redis_sorted_set_set(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float score) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_kset(redis, m, key, strlen(key), value, valuesz,
+    score);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting item to score %0.3f in sorted set using key '%s': %s",
+      score, key, strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_sorted_set_setall(pr_redis_t *redis, module *m, const char *key,
+    array_header *values, array_header *valueszs, array_header *scores) {
+  int res;
+
+  if (key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_redis_sorted_set_ksetall(redis, m, key, strlen(key), values,
+    valueszs, scores);
+  if (res < 0) {
+    int xerrno = errno;
+
+    pr_trace_msg(trace_channel, 2,
+      "error setting items in sorted set using key '%s': %s", key,
+      strerror(xerrno));
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return 0;
+}
+
+static const char *get_namespace_key(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t *keysz) {
+
+  if (m != NULL &&
+      redis->namespace_tab != NULL) {
+    const char *prefix = NULL;
+    size_t prefixsz = 0;
+
+    prefix = pr_table_kget(redis->namespace_tab, m, sizeof(module *),
+      &prefixsz);
+    if (prefix != NULL) {
+      char *new_key;
+      size_t new_keysz;
+
+      pr_trace_msg(trace_channel, 25,
+        "using namespace prefix '%s' for module 'mod_%s.c'", prefix, m->name);
+
+      /* Since the given key may not be text, we cannot simply use pstrcat()
+       * to prepend our namespace value.
+       */
+      new_keysz = prefixsz + *keysz;
+      new_key = palloc(p, new_keysz);
+      memcpy(new_key, prefix, prefixsz);
+      memcpy(new_key + prefixsz, key, *keysz);
+
+      key = new_key;
+      *keysz = new_keysz;
+    }
+  }
+
+  return key;
+}
+
+static const char *get_reply_type(int reply_type) {
+  const char *type_name;
+
+  switch (reply_type) {
+    case REDIS_REPLY_STRING:
+      type_name = "STRING";
+      break;
+
+    case REDIS_REPLY_ARRAY:
+      type_name = "ARRAY";
+      break;
+
+    case REDIS_REPLY_INTEGER:
+      type_name = "INTEGER";
+      break;
+
+    case REDIS_REPLY_NIL:
+      type_name = "NIL";
+      break;
+
+    case REDIS_REPLY_STATUS:
+      type_name = "STATUS";
+      break;
+
+    case REDIS_REPLY_ERROR:
+      type_name = "ERROR";
+      break;
+
+    default:
+      type_name = "unknown";
+  }
+
+  return type_name;
+}
+
+int pr_redis_command(pr_redis_t *redis, const array_header *args,
+    int reply_type) {
+  register unsigned int i;
+  int xerrno;
+  pool *tmp_pool = NULL;
+  array_header *arglens;
+  const char *cmd = NULL;
+  redisReply *reply;
+  int redis_reply_type;
+
+  if (redis == NULL ||
+      args == NULL ||
+      args->nelts == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  switch (reply_type) {
+    case PR_REDIS_REPLY_TYPE_STRING:
+      redis_reply_type = REDIS_REPLY_STRING;
+      break;
+
+    case PR_REDIS_REPLY_TYPE_INTEGER:
+      redis_reply_type = REDIS_REPLY_INTEGER;
+      break;
+
+    case PR_REDIS_REPLY_TYPE_NIL:
+      redis_reply_type = REDIS_REPLY_NIL;
+      break;
+
+    case PR_REDIS_REPLY_TYPE_ARRAY:
+      redis_reply_type = REDIS_REPLY_ARRAY;
+      break;
+
+    case PR_REDIS_REPLY_TYPE_STATUS:
+      redis_reply_type = REDIS_REPLY_STATUS;
+      break;
+
+    case PR_REDIS_REPLY_TYPE_ERROR:
+      redis_reply_type = REDIS_REPLY_ERROR;
+      break;
+
+    default:
+      errno = EINVAL;
+      return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis Command pool");
+
+  arglens = make_array(tmp_pool, args->nelts, sizeof(size_t));
+  for (i = 0; i < args->nelts; i++) {
+    pr_signals_handle();
+    *((size_t *) push_array(arglens)) = strlen(((char **) args->elts)[i]);
+  }
+
+  cmd = ((char **) args->elts)[0];
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommandArgv(redis->ctx, args->nelts, args->elts, arglens->elts);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error executing command '%s': %s", cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  if (reply->type != redis_reply_type) {
+    pr_trace_msg(trace_channel, 2,
+      "expected %s reply for %s, got %s", get_reply_type(redis_reply_type), cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  switch (reply->type) {
+    case REDIS_REPLY_STRING:
+    case REDIS_REPLY_STATUS:
+    case REDIS_REPLY_ERROR:
+      pr_trace_msg(trace_channel, 7, "%s %s reply: %.*s", cmd,
+        get_reply_type(reply->type), (int) reply->len, reply->str);
+      break;
+
+    case REDIS_REPLY_INTEGER:
+      pr_trace_msg(trace_channel, 7, "%s INTEGER reply: %lld", cmd,
+        reply->integer);
+      break;
+
+    case REDIS_REPLY_NIL:
+      pr_trace_msg(trace_channel, 7, "%s NIL reply", cmd);
+      break;
+
+    case REDIS_REPLY_ARRAY:
+      pr_trace_msg(trace_channel, 7, "%s ARRAY reply: (%lu %s)", cmd,
+        (unsigned long) reply->elements,
+        reply->elements != 1 ? "elements" : "element");
+      break;
+
+    default:
+      break;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_auth(pr_redis_t *redis, const char *password) {
+  int xerrno = 0;
+  const char *cmd;
+  pool *tmp_pool;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      password == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis AUTH pool");
+
+  cmd = "AUTH";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %s", cmd, password);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error authenticating client: %s", redis_strerror(tmp_pool, redis,
+        xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_STATUS) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or STATUS reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_kadd(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+    void *value, size_t valuesz, time_t expires) {
+  return pr_redis_kset(redis, m, key, keysz, value, valuesz, expires);
+}
+
+int pr_redis_kdecr(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+    uint32_t decr, uint64_t *value) {
+  int xerrno;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      decr == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis DECRBY pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "DECRBY";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %lu", cmd, key, keysz,
+    (unsigned long) decr);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error decrementing key (%lu bytes) by %lu using %s: %s",
+      (unsigned long) keysz, (unsigned long) decr, cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Note: DECRBY will automatically set the key value to zero if it does
+   * not already exist.  To detect a nonexistent key, then, we look to
+   * see if the return value is exactly our requested decrement.  If so,
+   * REMOVE the auto-created key, and return ENOENT.
+   */
+  if ((decr * -1) == (uint32_t) reply->integer) {
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    (void) pr_redis_kremove(redis, m, key, keysz);
+    errno = ENOENT;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  if (value != NULL) {
+    *value = (uint64_t) reply->integer;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+void *pr_redis_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, size_t *valuesz) {
+  int xerrno = 0;
+  const char *cmd;
+  pool *tmp_pool;
+  redisReply *reply;
+  char *data = NULL;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      valuesz == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis GET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "GET";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting data for key (%lu bytes) using %s: %s",
+      (unsigned long) keysz, cmd, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return NULL;
+  }
+
+  if (reply->type == REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING reply for %s, got %s", cmd, get_reply_type(reply->type));
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  if (valuesz != NULL) {
+    *valuesz = (uint64_t) reply->len;
+  }
+
+  data = palloc(p, reply->len);
+  memcpy(data, reply->str, reply->len);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return data;
+}
+
+char *pr_redis_kget_str(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  int xerrno = 0;
+  const char *cmd;
+  pool *tmp_pool;
+  redisReply *reply;
+  char *data = NULL;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis GET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "GET";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting data for key (%lu bytes) using %s: %s",
+      (unsigned long) keysz, cmd, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return NULL;
+  }
+
+  if (reply->type == REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = ENOENT;
+    return NULL;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING reply for %s, got %s", cmd, get_reply_type(reply->type));
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return NULL;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  data = pstrndup(p, reply->str, reply->len);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return data;
+}
+
+int pr_redis_kincr(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+    uint32_t incr, uint64_t *value) {
+  int xerrno;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      incr == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis INCRBY pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "INCRBY";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %lu", cmd, key, keysz,
+    (unsigned long) incr);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing key (%lu bytes) by %lu using %s: %s",
+      (unsigned long) keysz, (unsigned long) incr, cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Note: INCRBY will automatically set the key value to zero if it does
+   * not already exist.  To detect a nonexistent key, then, we look to
+   * see if the return value is exactly our requested increment.  If so,
+   * REMOVE the auto-created key, and return ENOENT.
+   */
+  if (incr == (uint32_t) reply->integer) {
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    (void) pr_redis_kremove(redis, m, key, keysz);
+    errno = ENOENT;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  if (value != NULL) {
+    *value = (uint64_t) reply->integer;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+  long long count;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis DEL pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "DEL";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error removing key (%lu bytes): %s", (unsigned long) keysz,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  count = reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  if (count == 0) {
+    /* No keys removed. */
+    errno = ENOENT;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_krename(pr_redis_t *redis, module *m, const char *from,
+    size_t fromsz, const char *to, size_t tosz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      from == NULL ||
+      fromsz == 0 ||
+      to == NULL ||
+      tosz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis RENAME pool");
+
+  from = get_namespace_key(tmp_pool, redis, m, from, &fromsz);
+  to = get_namespace_key(tmp_pool, redis, m, to, &tosz);
+
+  cmd = "RENAME";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, from, fromsz, to, tosz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error renaming key (from %lu bytes, to %lu bytes): %s",
+      (unsigned long) fromsz, (unsigned long) tosz,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_STATUS) {
+    xerrno = EINVAL;
+
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or STATUS reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+
+      /* Note: In order to provide ENOENT semantics here, we have to be
+       * naughty, and assume the contents of this error message.
+       */
+      if (strstr(reply->str, "no such key") != NULL) {
+        xerrno = ENOENT;
+      }
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_kset(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+    void *value, size_t valuesz, time_t expires) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  /* XXX Should we allow null values to be added, thus allowing use of keys
+   * as sentinels?
+   */
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      value == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  if (expires > 0) {
+    cmd = "SETEX";
+    pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+    reply = redisCommand(redis->ctx, "%s %b %lu %b", cmd, key, keysz,
+      (unsigned long) expires, value, valuesz);
+
+  } else {
+    cmd = "SET";
+    pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+    reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value,
+      valuesz);
+  }
+
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error adding key (%lu bytes), value (%lu bytes) using %s: %s",
+      (unsigned long) keysz, (unsigned long) valuesz, cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %s", cmd, reply->str);
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_hash_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      count == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HLEN pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HLEN";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting count of hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  *count = reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_hash_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      field == NULL ||
+      fieldsz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HDEL pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HDEL";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, field, fieldsz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting count of hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  exists = reply->integer ? TRUE : FALSE;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  if (exists == FALSE) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      field == NULL ||
+      fieldsz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HEXISTS pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HEXISTS";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, field, fieldsz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting count of hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  exists = reply->integer ? TRUE : FALSE;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return exists;
+}
+
+int pr_redis_hash_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz, void **value,
+    size_t *valuesz) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      field == NULL ||
+      fieldsz == 0 ||
+      value == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HGET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HGET";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, field, fieldsz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting item for field in hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or NIL reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->type == REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 7, "%s reply: (%lu bytes)", cmd,
+      (unsigned long) reply->len);
+
+    *value = palloc(p, reply->len);
+    memcpy(*value, reply->str, reply->len);
+
+    if (valuesz != NULL) {
+      *valuesz = reply->len;
+    }
+
+    exists = TRUE;
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  if (exists == FALSE) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_hash_kgetall(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, pr_table_t **hash) {
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      hash == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HGETALL pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HGETALL";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_ARRAY) {
+    pr_trace_msg(trace_channel, 2,
+      "expected ARRAY reply for %s, got %s", cmd, get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->elements > 0) {
+    register unsigned int i;
+
+    pr_trace_msg(trace_channel, 7, "%s reply: %lu %s", cmd,
+      (unsigned long) reply->elements,
+      reply->elements != 1 ? "elements" : "element");
+
+    *hash = pr_table_alloc(p, 0);
+
+    for (i = 0; i < reply->elements; i += 2) {
+      redisReply *key_elt, *value_elt;
+      void *key_data = NULL, *value_data = NULL;
+      size_t key_datasz = 0, value_datasz = 0;
+
+      key_elt = reply->element[i];
+      if (key_elt->type == REDIS_REPLY_STRING) {
+        key_datasz = key_elt->len;
+        key_data = palloc(p, key_datasz);
+        memcpy(key_data, key_elt->str, key_datasz);
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i,
+          get_reply_type(key_elt->type));
+      }
+
+      value_elt = reply->element[i+1];
+      if (value_elt->type == REDIS_REPLY_STRING) {
+        value_datasz = value_elt->len;
+        value_data = palloc(p, value_datasz);
+        memcpy(value_data, value_elt->str, value_datasz);
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i + 2,
+          get_reply_type(value_elt->type));
+      }
+
+      if (key_data != NULL &&
+          value_data != NULL) {
+        if (pr_table_kadd(*hash, key_data, key_datasz, value_data,
+            value_datasz) < 0) {
+          pr_trace_msg(trace_channel, 2,
+            "error adding key (%lu bytes), value (%lu bytes) to hash: %s",
+            (unsigned long) key_datasz, (unsigned long) value_datasz,
+            strerror(errno));
+        }
+      }
+    }
+
+    res = 0;
+
+  } else {
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_hash_kincr(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz, int32_t incr,
+    int64_t *value) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      field == NULL ||
+      fieldsz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  exists = pr_redis_hash_kexists(redis, m, key, keysz, field, fieldsz);
+  if (exists == FALSE) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HINCRBY pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HINCRBY";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b %d", cmd, key, keysz, field,
+    fieldsz, incr);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing field in hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  if (value != NULL) {
+    *value = (int64_t) reply->integer;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_hash_kkeys(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header **fields) {
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      fields == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HKEYS pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HKEYS";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting fields of hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_ARRAY) {
+    pr_trace_msg(trace_channel, 2,
+      "expected ARRAY reply for %s, got %s", cmd, get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->elements > 0) {
+    register unsigned int i;
+
+    pr_trace_msg(trace_channel, 7, "%s reply: %lu %s", cmd,
+      (unsigned long) reply->elements,
+      reply->elements != 1 ? "elements" : "element");
+
+    *fields = make_array(p, reply->elements, sizeof(char *));
+    for (i = 0; i < reply->elements; i++) {
+      redisReply *elt;
+
+      elt = reply->element[i];
+      if (elt->type == REDIS_REPLY_STRING) {
+        char *field;
+
+        field = pstrndup(p, elt->str, elt->len);
+        *((char **) push_array(*fields)) = field;
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i,
+          get_reply_type(elt->type));
+      }
+    }
+
+    res = 0;
+
+  } else {
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_hash_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+
+  /* Note: We can actually use just DEL here. */
+  return pr_redis_kremove(redis, m, key, keysz);
+}
+
+int pr_redis_hash_kset(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz, void *value,
+    size_t valuesz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      field == NULL ||
+      fieldsz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HSET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HSET";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b %b", cmd, key, keysz, field,
+    fieldsz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting item for field in hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_hash_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, pr_table_t *hash) {
+  int count, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  array_header *args, *arglens;
+  redisReply *reply;
+  const void *key_data;
+  size_t key_datasz;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      hash == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Skip any empty hashes. */
+  count = pr_table_count(hash);
+  if (count <= 0) {
+    pr_trace_msg(trace_channel, 9, "skipping empty table");
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HMSET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HMSET";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+
+  args = make_array(tmp_pool, count + 1, sizeof(char *));
+  arglens = make_array(tmp_pool, count + 1, sizeof(size_t));
+
+  *((char **) push_array(args)) = pstrdup(tmp_pool, cmd);
+  *((size_t *) push_array(arglens)) = strlen(cmd);
+
+  *((char **) push_array(args)) = (char *) key;
+  *((size_t *) push_array(arglens)) = keysz;
+
+  pr_table_rewind(hash);
+  key_data = pr_table_knext(hash, &key_datasz);
+  while (key_data != NULL) {
+    const void *value_data;
+    size_t value_datasz;
+
+    pr_signals_handle();
+
+    value_data = pr_table_kget(hash, key_data, key_datasz, &value_datasz);
+    if (value_data != NULL) {
+      char *key_dup, *value_dup;
+
+      key_dup = palloc(tmp_pool, key_datasz);
+      memcpy(key_dup, key_data, key_datasz);
+      *((char **) push_array(args)) = key_dup;
+      *((size_t *) push_array(arglens)) = key_datasz;
+
+      value_dup = palloc(tmp_pool, value_datasz);
+      memcpy(value_dup, value_data, value_datasz);
+      *((char **) push_array(args)) = value_dup;
+      *((size_t *) push_array(arglens)) = value_datasz;
+    }
+
+    key_data = pr_table_knext(hash, &key_datasz);
+  }
+
+  reply = redisCommandArgv(redis->ctx, args->nelts, args->elts, arglens->elts);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_STATUS) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or STATUS reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_hash_kvalues(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, array_header **values) {
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis HVALS pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "HVALS";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting values of hash using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_ARRAY) {
+    pr_trace_msg(trace_channel, 2,
+      "expected ARRAY reply for %s, got %s", cmd, get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->elements > 0) {
+    register unsigned int i;
+
+    pr_trace_msg(trace_channel, 7, "%s reply: %lu %s", cmd,
+      (unsigned long) reply->elements,
+      reply->elements != 1 ? "elements" : "element");
+
+    *values = make_array(p, reply->elements, sizeof(char *));
+    for (i = 0; i < reply->elements; i++) {
+      redisReply *elt;
+
+      elt = reply->element[i];
+      if (elt->type == REDIS_REPLY_STRING) {
+        char *value;
+
+        value = pcalloc(p, reply->len + 1);
+        memcpy(value, reply->str, reply->len);
+        *((char **) push_array(*values)) = value;
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i,
+          get_reply_type(elt->type));
+      }
+    }
+
+    res = 0;
+
+  } else {
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_list_kappend(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  return pr_redis_list_kpush(redis, m, key, keysz, value, valuesz,
+    PR_REDIS_LIST_FL_RIGHT);
+}
+
+int pr_redis_list_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      count == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis LLEN pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "LLEN";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting count of list using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  *count = (uint64_t) reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_list_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+  long long count = 0;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis LREM pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "LREM";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b 0 %b", cmd, key, keysz, value,
+    valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error deleting item from set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  count = reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  if (count == 0) {
+    /* No items removed. */
+    errno = ENOENT;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_list_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, unsigned int idx) {
+  pool *tmp_pool;
+  int res, xerrno = 0;
+  void *val = NULL;
+  size_t valsz = 0;
+
+  tmp_pool = make_sub_pool(NULL);
+  res = pr_redis_list_kget(tmp_pool, redis, m, key, keysz, idx, &val, &valsz);
+  xerrno = errno;
+  destroy_pool(tmp_pool);
+
+  if (res < 0) {
+    if (xerrno != ENOENT) {
+      errno = xerrno;
+      return -1;
+    }
+
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+int pr_redis_list_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, unsigned int idx, void **value, size_t *valuesz) {
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+  uint64_t count;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (pr_redis_list_kcount(redis, m, key, keysz, &count) == 0) {
+    if (count > 0 &&
+        idx > 0 &&
+        idx >= count) {
+      pr_trace_msg(trace_channel, 14,
+        "requested index %u exceeds list length %lu", idx,
+        (unsigned long) count);
+      errno = ERANGE;
+      return -1;
+    }
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis LINDEX pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "LINDEX";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %u", cmd, key, keysz, idx);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting item at index %u of list using key (%lu bytes): %s", idx,
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or NIL reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->type == REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+      reply->str);
+    *valuesz = reply->len;
+    *value = palloc(p, reply->len);
+    memcpy(*value, reply->str, reply->len);
+    res = 0;
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_list_kgetall(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, array_header **values,
+    array_header **valueszs) {
+  int res = 0, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL ||
+      valueszs == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis LRANGE pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "LRANGE";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b 0 -1", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting items in list using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_ARRAY) {
+    pr_trace_msg(trace_channel, 2,
+      "expected ARRAY reply for %s, got %s", cmd, get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->elements > 0) {
+    register unsigned int i;
+
+    pr_trace_msg(trace_channel, 7, "%s reply: %lu %s", cmd,
+      (unsigned long) reply->elements,
+      reply->elements != 1 ? "elements" : "element");
+
+    *values = make_array(p, 0, sizeof(void *));
+    *valueszs = make_array(p, 0, sizeof(size_t));
+
+    for (i = 0; i < reply->elements; i++) {
+      redisReply *value_elt;
+      void *value_data = NULL;
+      size_t value_datasz = 0;
+
+      value_elt = reply->element[i];
+      if (value_elt->type == REDIS_REPLY_STRING) {
+        value_datasz = value_elt->len;
+        value_data = palloc(p, value_datasz);
+        memcpy(value_data, value_elt->str, value_datasz);
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i + 1,
+          get_reply_type(value_elt->type));
+      }
+
+      if (value_data != NULL) {
+        *((void **) push_array(*values)) = value_data;
+        *((size_t *) push_array(*valueszs)) = value_datasz;
+      }
+    }
+
+    res = 0;
+
+  } else {
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_list_kpop(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void **value, size_t *valuesz, int flags) {
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+
+  switch (flags) {
+    case PR_REDIS_LIST_FL_RIGHT:
+      pr_pool_tag(tmp_pool, "Redis RPOP pool");
+      cmd = "RPOP";
+      break;
+
+    case PR_REDIS_LIST_FL_LEFT:
+      pr_pool_tag(tmp_pool, "Redis LPOP pool");
+      cmd = "LPOP";
+      break;
+
+    default:
+      destroy_pool(tmp_pool);
+      errno = EINVAL;
+      return -1;
+  }
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error popping item from list using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or NIL reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->type == REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+      reply->str);
+    *valuesz = reply->len;
+    *value = palloc(p, reply->len);
+    memcpy(*value, reply->str, reply->len);
+    res = 0;
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_list_kpush(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, int flags) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+
+  switch (flags) {
+    case PR_REDIS_LIST_FL_RIGHT:
+      pr_pool_tag(tmp_pool, "Redis RPUSH pool");
+      cmd = "RPUSH";
+      break;
+
+    case PR_REDIS_LIST_FL_LEFT:
+      pr_pool_tag(tmp_pool, "Redis LPUSH pool");
+      cmd = "LPUSH";
+      break;
+
+    default:
+      destroy_pool(tmp_pool);
+      errno = EINVAL;
+      return -1;
+  }
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error pushing to list using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_list_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+
+  /* Note: We can actually use just DEL here. */
+  return pr_redis_kremove(redis, m, key, keysz);
+}
+
+int pr_redis_list_krotate(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, void **value, size_t *valuesz) {
+  int res = 0, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis RPOPLPUSH pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "RPOPLPUSH";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error rotating list using key (%lu bytes): %s", (unsigned long) keysz,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or NIL reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->type == REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+      reply->str);
+    *valuesz = reply->len;
+    *value = palloc(p, reply->len);
+    memcpy(*value, reply->str, reply->len);
+    res = 0;
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_list_kset(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, unsigned int idx, void *value, size_t valuesz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis LSET pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "LSET";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %u %b", cmd, key, keysz, idx, value,
+    valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting item at index %u in list using key (%lu bytes): %s", idx,
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_STATUS) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or STATUS reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_list_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header *values, array_header *valueszs) {
+  register unsigned int i;
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  array_header *args, *arglens;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL ||
+      values->nelts == 0 ||
+      valueszs == NULL ||
+      valueszs->nelts == 0 ||
+      values->nelts != valueszs->nelts) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* First, delete any existing list at this key; a set operation, in my mind,
+   * is a complete overwrite.
+   */
+  res = pr_redis_list_kremove(redis, m, key, keysz);
+  if (res < 0 &&
+      errno != ENOENT) {
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis RPUSH pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "RPUSH";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+
+  args = make_array(tmp_pool, 0, sizeof(char *));
+  arglens = make_array(tmp_pool, 0, sizeof(size_t));
+
+  *((char **) push_array(args)) = pstrdup(tmp_pool, cmd);
+  *((size_t *) push_array(arglens)) = strlen(cmd);
+
+  *((char **) push_array(args)) = (char *) key;
+  *((size_t *) push_array(arglens)) = keysz;
+
+  for (i = 0; i < values->nelts; i++) {
+    pr_signals_handle();
+
+    *((char **) push_array(args)) = ((char **) values->elts)[i];
+    *((size_t *) push_array(arglens)) = ((size_t *) valueszs->elts)[i];
+  }
+
+  reply = redisCommandArgv(redis->ctx, args->nelts, args->elts, arglens->elts);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting items in list using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_set_kadd(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  exists = pr_redis_set_kexists(redis, m, key, keysz, value, valuesz);
+  if (exists == TRUE) {
+    errno = EEXIST;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SADD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "SADD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error adding to set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_set_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      count == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SCARD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "SCARD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting count of set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  *count = (uint64_t) reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_set_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+  long long count = 0;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SREM pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "SREM";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error deleting item from set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  count = reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  if (count == 0) {
+    /* No items removed. */
+    errno = ENOENT;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_set_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SISMEMBER pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "SISMEMBER";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error checking item in set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  exists = reply->integer ? TRUE : FALSE;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return exists;
+}
+
+int pr_redis_set_kgetall(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header **values, array_header **valueszs) {
+  int res = 0, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL ||
+      valueszs == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SMEMBERS pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "SMEMBERS";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting items in set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_ARRAY) {
+    pr_trace_msg(trace_channel, 2,
+      "expected ARRAY reply for %s, got %s", cmd, get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->elements > 0) {
+    register unsigned int i;
+
+    pr_trace_msg(trace_channel, 7, "%s reply: %lu %s", cmd,
+      (unsigned long) reply->elements,
+      reply->elements != 1 ? "elements" : "element");
+
+    *values = make_array(p, 0, sizeof(void *));
+    *valueszs = make_array(p, 0, sizeof(size_t));
+
+    for (i = 0; i < reply->elements; i++) {
+      redisReply *value_elt;
+      void *value_data = NULL;
+      size_t value_datasz = 0;
+
+      value_elt = reply->element[i];
+      if (value_elt->type == REDIS_REPLY_STRING) {
+        value_datasz = value_elt->len;
+        value_data = palloc(p, value_datasz);
+        memcpy(value_data, value_elt->str, value_datasz);
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i + 1,
+          get_reply_type(value_elt->type));
+      }
+
+      if (value_data != NULL) {
+        *((void **) push_array(*values)) = value_data;
+        *((size_t *) push_array(*valueszs)) = value_datasz;
+      }
+    }
+
+    res = 0;
+
+  } else {
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_set_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+
+  /* Note: We can actually use just DEL here. */
+  return pr_redis_kremove(redis, m, key, keysz);
+}
+
+int pr_redis_set_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header *values, array_header *valueszs) {
+  register unsigned int i;
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  array_header *args, *arglens;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL ||
+      values->nelts == 0 ||
+      valueszs == NULL ||
+      valueszs->nelts == 0 ||
+      values->nelts != valueszs->nelts) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* First, delete any existing set at this key; a set operation, in my mind,
+   * is a complete overwrite.
+   */
+  res = pr_redis_set_kremove(redis, m, key, keysz);
+  if (res < 0 &&
+      errno != ENOENT) {
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis SADD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "SADD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+
+  args = make_array(tmp_pool, 0, sizeof(char *));
+  arglens = make_array(tmp_pool, 0, sizeof(size_t));
+
+  *((char **) push_array(args)) = pstrdup(tmp_pool, cmd);
+  *((size_t *) push_array(arglens)) = strlen(cmd);
+
+  *((char **) push_array(args)) = (char *) key;
+  *((size_t *) push_array(arglens)) = keysz;
+
+  for (i = 0; i < values->nelts; i++) {
+    pr_signals_handle();
+
+    *((char **) push_array(args)) = ((char **) values->elts)[i];
+    *((size_t *) push_array(arglens)) = ((size_t *) valueszs->elts)[i];
+  }
+
+  reply = redisCommandArgv(redis->ctx, args->nelts, args->elts, arglens->elts);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting items in set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_sorted_set_kadd(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float score) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Note: We should probably detect the server version, and instead of using
+   * a separate existence check, if server >= 3.0.2, use the NX/XX flags of
+   * the ZADD command.
+   */
+  exists = pr_redis_sorted_set_kexists(redis, m, key, keysz, value, valuesz);
+  if (exists == TRUE) {
+    errno = EEXIST;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZADD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZADD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %f %b", cmd, key, keysz, score,
+    value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error adding to sorted set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_sorted_set_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      count == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZCARD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZCARD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b", cmd, key, keysz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting count of sorted set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  *count = (uint64_t) reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int pr_redis_sorted_set_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  int xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+  long long count = 0;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZREM pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZREM";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error deleting item from sorted set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+  count = reply->integer;
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  if (count == 0) {
+    /* No items removed. */
+    errno = ENOENT;
+    return -1;
+  }
+
+  return 0;
+}
+
+int pr_redis_sorted_set_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZRANK pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZRANK";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error checking item in sorted set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER &&
+      reply->type != REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER or NIL reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->type == REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+    exists = TRUE;
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    exists = FALSE;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return exists;
+}
+
+int pr_redis_sorted_set_kgetn(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, unsigned int offset, unsigned int len,
+    array_header **values, array_header **valueszs, int flags) {
+  int res = 0, xerrno = 0;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (p == NULL ||
+      redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL ||
+      valueszs == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+
+  switch (flags) {
+    case PR_REDIS_SORTED_SET_FL_ASC:
+      pr_pool_tag(tmp_pool, "Redis ZRANGE pool");
+      cmd = "ZRANGE";
+      break;
+
+    case PR_REDIS_SORTED_SET_FL_DESC:
+      pr_pool_tag(tmp_pool, "Redis ZREVRANGE pool");
+      cmd = "ZREVRANGE";
+      break;
+
+    default:
+      errno = EINVAL;
+      return -1;
+  }
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+
+  /* Since the the range is [start, stop] inclusive, and the function takes
+   * a length, we need to subtract one for whose items these are.  Consider
+   * an offset of 0, and a len of 1 -- to get just one item.  In that case,
+   * stop would be 0 as well.
+   */
+  reply = redisCommand(redis->ctx, "%s %b %u %u", cmd, key, keysz, offset,
+    offset + len - 1);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error getting %u %s in sorted set using key (%lu bytes): %s", len,
+      len != 1 ? "items" : "item", (unsigned long) keysz,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_ARRAY) {
+    pr_trace_msg(trace_channel, 2,
+      "expected ARRAY reply for %s, got %s", cmd, get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->elements > 0) {
+    register unsigned int i;
+
+    pr_trace_msg(trace_channel, 7, "%s reply: %lu %s", cmd,
+      (unsigned long) reply->elements,
+      reply->elements != 1 ? "elements" : "element");
+
+    *values = make_array(p, 0, sizeof(void *));
+    *valueszs = make_array(p, 0, sizeof(size_t));
+
+    for (i = 0; i < reply->elements; i++) {
+      redisReply *value_elt;
+      void *value_data = NULL;
+      size_t value_datasz = 0;
+
+      value_elt = reply->element[i];
+      if (value_elt->type == REDIS_REPLY_STRING) {
+        value_datasz = value_elt->len;
+        value_data = palloc(p, value_datasz);
+        memcpy(value_data, value_elt->str, value_datasz);
+
+      } else {
+        pr_trace_msg(trace_channel, 2,
+          "expected STRING element at index %u, got %s", i + 1,
+          get_reply_type(value_elt->type));
+      }
+
+      if (value_data != NULL) {
+        *((void **) push_array(*values)) = value_data;
+        *((size_t *) push_array(*valueszs)) = value_datasz;
+      }
+    }
+
+    res = 0;
+
+  } else {
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_sorted_set_kincr(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float incr, float *score) {
+  int res, xerrno, exists;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0 ||
+      score == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  exists = pr_redis_sorted_set_kexists(redis, m, key, keysz, value, valuesz);
+  if (exists == FALSE) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZINCRBY pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZINCRBY";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %f %b", cmd, key, keysz, incr,
+    value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error incrementing key (%lu bytes) by %0.3f in sorted set using %s: %s",
+      (unsigned long) keysz, incr, cmd,
+      redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+    reply->str);
+
+  res = sscanf(reply->str, "%f", score);
+  if (res != 1) {
+    pr_trace_msg(trace_channel, 3, "error parsing '%.*s' as float",
+      (int) reply->len, reply->str);
+    xerrno = EINVAL;
+    res = -1;
+
+  } else {
+    res = 0;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_sorted_set_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+
+  /* Note: We can actually use just DEL here. */
+  return pr_redis_kremove(redis, m, key, keysz);
+}
+
+int pr_redis_sorted_set_kscore(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float *score) {
+  int res, xerrno;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0 ||
+      score == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZSCORE pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZSCORE";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %b", cmd, key, keysz, value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error gettin score for key (%lu bytes) using %s: %s",
+      (unsigned long) keysz, cmd, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = EIO;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_STRING &&
+      reply->type != REDIS_REPLY_NIL) {
+    pr_trace_msg(trace_channel, 2,
+      "expected STRING or NIL reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (reply->type == REDIS_REPLY_STRING) {
+    pr_trace_msg(trace_channel, 7, "%s reply: %.*s", cmd, (int) reply->len,
+      reply->str);
+
+    res = sscanf(reply->str, "%f", score);
+    if (res != 1) {
+      pr_trace_msg(trace_channel, 3, "error parsing '%.*s' as float",
+        (int) reply->len, reply->str);
+      xerrno = EINVAL;
+      res = -1;
+
+    } else {
+      res = 0;
+    }
+
+  } else {
+    pr_trace_msg(trace_channel, 7, "%s reply: nil", cmd);
+    xerrno = ENOENT;
+    res = -1;
+  }
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+
+  errno = xerrno;
+  return res;
+}
+
+int pr_redis_sorted_set_kset(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float score) {
+  int xerrno = 0, exists = FALSE;
+  pool *tmp_pool = NULL;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      value == NULL ||
+      valuesz == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Note: We should probably detect the server version, and instead of using
+   * a separate existence check, if server >= 3.0.2, use the NX/XX flags of
+   * the ZADD command.
+   */
+  exists = pr_redis_sorted_set_kexists(redis, m, key, keysz, value, valuesz);
+  if (exists == FALSE) {
+    errno = ENOENT;
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZADD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZADD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+  reply = redisCommand(redis->ctx, "%s %b %f %b", cmd, key, keysz, score,
+    value, valuesz);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting item in sorted set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+static char *f2s(pool *p, float num, size_t *len) {
+  int res;
+  char *s;
+  size_t sz;
+
+  sz = 32;
+  s = pcalloc(p, sz + 1);
+  res = snprintf(s, sz, "%0.3f", num);
+
+  *len = res;
+  return s;
+}
+
+int pr_redis_sorted_set_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header *values, array_header *valueszs,
+    array_header *scores) {
+  register unsigned int i;
+  int res, xerrno = 0;
+  pool *tmp_pool = NULL;
+  array_header *args, *arglens;
+  const char *cmd = NULL;
+  redisReply *reply;
+
+  if (redis == NULL ||
+      m == NULL ||
+      key == NULL ||
+      keysz == 0 ||
+      values == NULL ||
+      values->nelts == 0 ||
+      valueszs == NULL ||
+      valueszs->nelts == 0 ||
+      scores == NULL ||
+      scores->nelts == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (values->nelts != valueszs->nelts ||
+      values->nelts != scores->nelts) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* First, delete any existing sorted set at this key; a set operation,
+   * in my mind, is a complete overwrite.
+   */
+  res = pr_redis_sorted_set_kremove(redis, m, key, keysz);
+  if (res < 0 &&
+      errno != ENOENT) {
+    return -1;
+  }
+
+  tmp_pool = make_sub_pool(redis->pool);
+  pr_pool_tag(tmp_pool, "Redis ZADD pool");
+
+  key = get_namespace_key(tmp_pool, redis, m, key, &keysz);
+
+  cmd = "ZADD";
+  pr_trace_msg(trace_channel, 7, "sending command: %s", cmd);
+
+  args = make_array(tmp_pool, 0, sizeof(char *));
+  arglens = make_array(tmp_pool, 0, sizeof(size_t));
+
+  *((char **) push_array(args)) = pstrdup(tmp_pool, cmd);
+  *((size_t *) push_array(arglens)) = strlen(cmd);
+
+  *((char **) push_array(args)) = (char *) key;
+  *((size_t *) push_array(arglens)) = keysz;
+
+  for (i = 0; i < values->nelts; i++) {
+    size_t scoresz = 0;
+
+    pr_signals_handle();
+
+    *((char **) push_array(args)) = f2s(tmp_pool, ((float *) scores->elts)[i],
+      &scoresz);
+    *((size_t *) push_array(arglens)) = scoresz;
+
+    *((char **) push_array(args)) = ((char **) values->elts)[i];
+    *((size_t *) push_array(arglens)) = ((size_t *) valueszs->elts)[i];
+  }
+
+  reply = redisCommandArgv(redis->ctx, args->nelts, args->elts, arglens->elts);
+  xerrno = errno;
+
+  if (reply == NULL) {
+    pr_trace_msg(trace_channel, 2,
+      "error setting items in sorted set using key (%lu bytes): %s",
+      (unsigned long) keysz, redis_strerror(tmp_pool, redis, xerrno));
+    destroy_pool(tmp_pool);
+    errno = xerrno;
+    return -1;
+  }
+
+  if (reply->type != REDIS_REPLY_INTEGER) {
+    pr_trace_msg(trace_channel, 2,
+      "expected INTEGER reply for %s, got %s", cmd,
+      get_reply_type(reply->type));
+
+    if (reply->type == REDIS_REPLY_ERROR) {
+      pr_trace_msg(trace_channel, 2, "%s error: %s", cmd, reply->str);
+    }
+    freeReplyObject(reply);
+    destroy_pool(tmp_pool);
+    errno = EINVAL;
+    return -1;
+  }
+
+  pr_trace_msg(trace_channel, 7, "%s reply: %lld", cmd, reply->integer);
+
+  freeReplyObject(reply);
+  destroy_pool(tmp_pool);
+  return 0;
+}
+
+int redis_set_server(const char *server, int port, const char *password) {
+  if (server == NULL ||
+      port < 1) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  redis_server = server;
+  redis_port = port;
+  redis_password = password;
+  return 0;
+}
+
+int redis_set_timeouts(unsigned long connect_millis, unsigned long io_millis) {
+  redis_connect_millis = connect_millis;
+  redis_io_millis = io_millis;
+
+  return 0;
+}
+
+int redis_clear(void) {
+  if (sess_redis != NULL) {
+    pr_redis_conn_destroy(sess_redis);
+    sess_redis = NULL;
+  }
+
+  return 0;
+}
+
+int redis_init(void) {
+  return 0;
+}
+
+#else
+
+pr_redis_t *pr_redis_conn_get(pool *p) {
+  errno = ENOSYS;
+  return NULL;
+}
+
+pr_redis_t *pr_redis_conn_new(pool *p, module *m, unsigned long flags) {
+  errno = ENOSYS;
+  return NULL;
+}
+
+int pr_redis_conn_close(pr_redis_t *redis) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_conn_destroy(pr_redis_t *redis) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_conn_set_namespace(pr_redis_t *redis, module *m,
+    const void *prefix, size_t prefixsz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_auth(pr_redis_t *redis, const char *password) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_command(pr_redis_t *redis, const array_header *args,
+    int reply_type) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_add(pr_redis_t *redis, module *m, const char *key, void *value,
+    size_t valuesz, time_t expires) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_decr(pr_redis_t *redis, module *m, const char *key, uint32_t decr,
+    uint64_t *value) {
+  errno = ENOSYS;
+  return -1;
+}
+
+void *pr_redis_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t *valuesz) {
+  errno = ENOSYS;
+  return NULL;
+}
+
+char *pr_redis_get_str(pool *p, pr_redis_t *redis, module *m, const char *key) {
+  errno = ENOSYS;
+  return NULL;
+}
+
+int pr_redis_incr(pr_redis_t *redis, module *m, const char *key, uint32_t incr,
+    uint64_t *value) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_remove(pr_redis_t *redis, module *m, const char *key) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_rename(pr_redis_t *redis, module *m, const char *from,
+    const char *to) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set(pr_redis_t *redis, module *m, const char *key, void *value,
+    size_t valuesz, time_t expires) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_delete(pr_redis_t *redis, module *m, const char *key,
+    const char *field) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_exists(pr_redis_t *redis, module *m, const char *key,
+    const char *field) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+    const char *field, void **value, size_t *valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_getall(pool *p, pr_redis_t *redis, module *m,
+    const char *key, pr_table_t **hash) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_incr(pr_redis_t *redis, module *m, const char *key,
+    const char *field, int32_t incr, int64_t *value) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_keys(pool *p, pr_redis_t *redis, module *m, const char *key,
+    array_header **fields) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_remove(pr_redis_t *redis, module *m, const char *key) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_set(pr_redis_t *redis, module *m, const char *key,
+    const char *field, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_setall(pr_redis_t *redis, module *m, const char *key,
+    pr_table_t *hash) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_values(pool *p, pr_redis_t *redis, module *m,
+    const char *key, array_header **values) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_append(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_delete(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_exists(pr_redis_t *redis, module *m, const char *key,
+    unsigned int idx) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_get(pool *p, pr_redis_t *redis, module *m, const char *key,
+    unsigned int idx, void **value, size_t *valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_getall(pool *p, pr_redis_t *redis, module *m, const char *key,
+    array_header **values, array_header **valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_pop(pool *p, pr_redis_t *redis, module *m, const char *key,
+    void **value, size_t *valuesz, int flags) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_push(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, int flags) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_remove(pr_redis_t *redis, module *m, const char *key) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_rotate(pool *p, pr_redis_t *redis, module *m,
+    const char *key, void **value, size_t *valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_set(pr_redis_t *redis, module *m, const char *key,
+    unsigned int idx, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_setall(pr_redis_t *redis, module *m, const char *key,
+    array_header *values, array_header *valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_add(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_delete(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_exists(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_getall(pool *p, pr_redis_t *redis, module *m, const char *key,
+    array_header **values, array_header **valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_remove(pr_redis_t *redis, module *m, const char *key) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_setall(pr_redis_t *redis, module *m, const char *key,
+    array_header *values, array_header *valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_add(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_count(pr_redis_t *redis, module *m, const char *key,
+    uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_delete(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_exists(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_getn(pool *p, pr_redis_t *redis, module *m,
+    const char *key, unsigned int offset, unsigned int len,
+    array_header **values, array_header **valueszs, int flags) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_incr(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float incr, float *score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_remove(pr_redis_t *redis, module *m, const char *key) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_score(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float *score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_set(pr_redis_t *redis, module *m, const char *key,
+    void *value, size_t valuesz, float score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_setall(pr_redis_t *redis, module *m, const char *key,
+    array_header *values, array_header *valueszs, array_header *scores) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_kadd(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+    void *value, size_t valuesz, time_t expires) {
+  errno = ENOSYS;
+  return -1;
+}
+
+void *pr_redis_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, size_t *valuesz) {
+  errno = ENOSYS;
+  return NULL;
+}
+
+char *pr_redis_kget_str(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  errno = ENOSYS;
+  return NULL;
+}
+
+int pr_redis_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_krename(pr_redis_t *redis, module *m, const char *from,
+    size_t fromsz, const char *to, size_t tosz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_kset(pr_redis_t *redis, module *m, const char *key, size_t keysz,
+    void *value, size_t valuesz, time_t expires) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz, void **value,
+    size_t *valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kgetall(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, pr_table_t **hash) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kincr(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz, int32_t incr,
+    int64_t *value) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kkeys(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header **fields) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kset(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, const char *field, size_t fieldsz, void *value,
+    size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, pr_table_t *hash) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_hash_kvalues(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, array_header **values) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kappend(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, unsigned int idx) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kget(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, unsigned int idx, void **value, size_t *valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kgetall(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, array_header **values,
+    array_header **valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kpop(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void **value, size_t *valuesz, int flags) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kpush(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, int flags) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_krotate(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, void **value, size_t *valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_kset(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, unsigned int idx, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_list_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header *values, array_header *valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_kadd(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_kgetall(pool *p, pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header **values, array_header **valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_set_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header *values, array_header *valueszs) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kadd(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kcount(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, uint64_t *count) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kdelete(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kexists(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kgetn(pool *p, pr_redis_t *redis, module *m,
+    const char *key, size_t keysz, unsigned int offset, unsigned int len,
+    array_header **values, array_header **valueszs, int flags) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kincr(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float incr, float *score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kremove(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kscore(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float *score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_kset(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, void *value, size_t valuesz, float score) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int pr_redis_sorted_set_ksetall(pr_redis_t *redis, module *m, const char *key,
+    size_t keysz, array_header *values, array_header *valueszs,
+    array_header *scores) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int redis_set_server(const char *server, int port, const char *password) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int redis_set_timeouts(unsigned long conn_millis, unsigned long io_millis) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int redis_clear(void) {
+  errno = ENOSYS;
+  return -1;
+}
+
+int redis_init(void) {
+  errno = ENOSYS;
+  return -1;
+}
+
+#endif /* PR_USE_REDIS */
diff --git a/src/regexp.c b/src/regexp.c
index 63e9103..3ee8eef 100644
--- a/src/regexp.c
+++ b/src/regexp.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,15 +24,15 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Regex management code
- * $Id: regexp.c,v 1.20 2013-03-14 21:49:19 castaglia Exp $
- */
+/* Regex management code. */
 
 #include "conf.h"
 
 #ifdef PR_USE_REGEX
 
 #ifdef PR_USE_PCRE
+#include <pcre.h>
+
 struct regexp_rec {
   pool *regex_pool;
 
@@ -45,7 +45,7 @@ struct regexp_rec {
   /* For callers wishing to use POSIX REs */
   regex_t *re;
 
-  /* For calles wishing to use PCRE REs */
+  /* For callers wishing to use PCRE REs */
   pcre *pcre;
   pcre_extra *pcre_extra;
 
@@ -76,6 +76,28 @@ static array_header *regexp_list = NULL;
 
 static const char *trace_channel = "regexp";
 
+static void regexp_free(pr_regex_t *pre) {
+#ifdef PR_USE_PCRE
+  if (pre->pcre != NULL) {
+# if defined(HAVE_PCRE_PCRE_FREE_STUDY)
+    pcre_free_study(pre->pcre_extra);
+# endif /* HAVE_PCRE_PCRE_FREE_STUDY */
+    pre->pcre_extra = NULL;
+    pcre_free(pre->pcre);
+    pre->pcre = NULL;
+  }
+#endif /* PR_USE_PCRE */
+
+  if (pre->re != NULL) {
+    /* This frees memory associated with this pointer by regcomp(3). */
+    regfree(pre->re);
+    pre->re = NULL;
+  }
+
+  pre->pattern = NULL;
+  destroy_pool(pre->regex_pool);
+}
+
 static void regexp_cleanup(void) {
   /* Only perform this cleanup if necessary */
   if (regexp_pool) {
@@ -84,23 +106,8 @@ static void regexp_cleanup(void) {
 
     for (i = 0; i < regexp_list->nelts; i++) {
       if (pres[i] != NULL) {
-
-#ifdef PR_USE_PCRE
-        if (pres[i]->pcre != NULL) {
-          /* This frees memory associated with this pointer by regcomp(3). */
-          pcre_free(pres[i]->pcre);
-          pres[i]->pcre = NULL;
-        }
-#endif /* PR_USE_PCRE */
-
-        if (pres[i]->re != NULL) {
-          /* This frees memory associated with this pointer by regcomp(3). */
-          regfree(pres[i]->re);
-          pres[i]->re = NULL;
-        }
-
-        /* This frees the memory allocated for the object itself. */
-        destroy_pool(pres[i]->regex_pool);
+        regexp_free(pres[i]);
+        pres[i] = NULL;
       }
     }
 
@@ -163,25 +170,7 @@ void pr_regexp_free(module *m, pr_regex_t *pre) {
 
     if ((pre != NULL && pres[i] == pre) ||
         (m != NULL && pres[i]->m == m)) {
-
-#ifdef PR_USE_PCRE
-      if (pres[i]->pcre != NULL) {
-        /* This frees memory associated with this pointer by regcomp(3). */
-        pcre_free(pres[i]->pcre);
-        pres[i]->pcre = NULL;
-      }
-#endif /* PR_USE_PCRE */
-
-      if (pres[i]->re != NULL) {
-        /* This frees memory associated with this pointer by regcomp(3). */
-        regfree(pres[i]->re);
-        pres[i]->re = NULL;
-      }
-
-      pres[i]->pattern = NULL;
-
-      /* This frees the memory allocated for the object itself. */
-      destroy_pool(pres[i]->regex_pool);
+      regexp_free(pres[i]);
       pres[i] = NULL;
     }
   }
@@ -190,7 +179,7 @@ void pr_regexp_free(module *m, pr_regex_t *pre) {
 #ifdef PR_USE_PCRE
 static int regexp_compile_pcre(pr_regex_t *pre, const char *pattern,
     int flags) {
-  int err_offset;
+  int err_offset, study_flags = 0;
 
   if (pre == NULL ||
       pattern == NULL) {
@@ -204,7 +193,6 @@ static int regexp_compile_pcre(pr_regex_t *pre, const char *pattern,
 
   pre->pcre = pcre_compile(pattern, flags, &(pre->pcre_errstr), &err_offset,
     NULL);
-
   if (pre->pcre == NULL) {
     pr_trace_msg(trace_channel, 4,
       "error compiling pattern '%s' into PCRE regex: %s", pattern,
@@ -213,9 +201,20 @@ static int regexp_compile_pcre(pr_regex_t *pre, const char *pattern,
   }
 
   /* Study the pattern as well, just in case. */
+#ifdef PCRE_STUDY_JIT_COMPILE
+  study_flags = PCRE_STUDY_JIT_COMPILE;
+#endif /* PCRE_STUDY_JIT_COMPILE */
   pr_trace_msg(trace_channel, 9, "studying pattern '%s' for PCRE extra data",
     pattern);
-  pre->pcre_extra = pcre_study(pre->pcre, 0, &(pre->pcre_errstr));
+  pre->pcre_extra = pcre_study(pre->pcre, study_flags, &(pre->pcre_errstr));
+  if (pre->pcre_extra == NULL) {
+    if (pre->pcre_errstr != NULL) {
+      pr_trace_msg(trace_channel, 4,
+        "error studying pattern '%s' for PCRE regex: %s", pattern,
+        pre->pcre_errstr);
+    }
+  }
+
   return 0;
 }
 #endif /* PR_USE_PCRE */
@@ -263,7 +262,8 @@ int pr_regexp_compile(pr_regex_t *pre, const char *pattern, int flags) {
 }
 
 size_t pr_regexp_error(int errcode, const pr_regex_t *pre, char *buf,
-  size_t bufsz) {
+    size_t bufsz) {
+  size_t res = 0;
 
   if (pre == NULL ||
       buf == NULL ||
@@ -281,10 +281,10 @@ size_t pr_regexp_error(int errcode, const pr_regex_t *pre, char *buf,
   if (pre->re != NULL) {
     /* Make sure the given buffer is always zeroed out first. */
     memset(buf, '\0', bufsz);
-    return regerror(errcode, pre->re, buf, bufsz-1);
+    res = regerror(errcode, pre->re, buf, bufsz-1);
   }
 
-  return 0;
+  return res;
 }
 
 const char *pr_regexp_get_pattern(const pr_regex_t *pre) {
@@ -305,11 +305,6 @@ const char *pr_regexp_get_pattern(const pr_regex_t *pre) {
 static int regexp_exec_pcre(pr_regex_t *pre, const char *str,
     size_t nmatches, regmatch_t *matches, int flags, unsigned long match_limit,
     unsigned long match_limit_recursion) {
-  if (pre == NULL ||
-      str == NULL) {
-    errno = EINVAL;
-    return -1;
-  }
 
   if (pre->pcre != NULL) {
     int res;
@@ -448,6 +443,14 @@ int pr_regexp_exec(pr_regex_t *pre, const char *str, size_t nmatches,
 #endif /* PR_USE_PCRE */
 
   res = regexp_exec_posix(pre, str, nmatches, matches, flags);
+
+  /* Make sure that we return a negative value to indicate a failed match;
+   * PCRE already does this.
+   */
+  if (res == REG_NOMATCH) {
+    res = -1;
+  }
+
   return res;
 }
 
diff --git a/src/response.c b/src/response.c
index 83d2fbb..9b4395f 100644
--- a/src/response.c
+++ b/src/response.c
@@ -22,22 +22,19 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Command response routines
- * $Id: response.c,v 1.22 2011-08-13 19:24:06 castaglia Exp $
- */
+/* Command response routines. */
 
 #include "conf.h"
 
 pr_response_t *resp_list = NULL, *resp_err_list = NULL;
 
 static int resp_blocked = FALSE;
-
 static pool *resp_pool = NULL;
 
 static char resp_buf[PR_RESPONSE_BUFFER_SIZE] = {'\0'};
 
-static char *resp_last_response_code = NULL;
-static char *resp_last_response_msg = NULL;
+static const char *resp_last_response_code = NULL;
+static const char *resp_last_response_msg = NULL;
 
 static char *(*resp_handler_cb)(pool *, const char *, ...) = NULL;
 
@@ -86,21 +83,22 @@ void pr_response_set_pool(pool *p) {
 
   /* Copy any old "last" values out of the new pool. */
   if (resp_last_response_code != NULL) {
-    char *ptr;
+    const char *ptr;
 
     ptr = resp_last_response_code;
     resp_last_response_code = pstrdup(p, ptr);
   }
 
   if (resp_last_response_msg != NULL) {
-    char *ptr;
+    const char *ptr;
   
     ptr = resp_last_response_msg;
     resp_last_response_msg = pstrdup(p, ptr);
   }
 }
 
-int pr_response_get_last(pool *p, char **response_code, char **response_msg) {
+int pr_response_get_last(pool *p, const char **response_code,
+    const char **response_msg) {
   if (p == NULL) {
     errno = EINVAL;
     return -1;
@@ -149,7 +147,7 @@ void pr_response_clear(pr_response_t **head) {
 
 void pr_response_flush(pr_response_t **head) {
   unsigned char ml = FALSE;
-  char *last_numeric = NULL;
+  const char *last_numeric = NULL;
   pr_response_t *resp = NULL;
 
   if (head == NULL) {
@@ -157,6 +155,9 @@ void pr_response_flush(pr_response_t **head) {
   }
 
   if (resp_blocked) {
+    pr_trace_msg(trace_channel, 19,
+      "responses blocked, not flushing response chain");
+    pr_response_clear(head);
     return;
   }
 
@@ -164,6 +165,7 @@ void pr_response_flush(pr_response_t **head) {
     /* Not sure what happened to the control connection, but since it's gone,
      * there's no need to flush any messages.
      */
+    pr_response_clear(head);
     return;
   }
 
@@ -210,20 +212,27 @@ void pr_response_flush(pr_response_t **head) {
 
 void pr_response_add_err(const char *numeric, const char *fmt, ...) {
   pr_response_t *resp = NULL, **head = NULL;
+  int res;
   va_list msg;
 
+  if (fmt == NULL) {
+    return;
+  }
+
   va_start(msg, fmt);
-  vsnprintf(resp_buf, sizeof(resp_buf), fmt, msg);
+  res = vsnprintf(resp_buf, sizeof(resp_buf), fmt, msg);
   va_end(msg);
   
   resp_buf[sizeof(resp_buf) - 1] = '\0';
   
   resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));
   resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);
-  resp->msg = pstrdup(resp_pool, resp_buf);
+  resp->msg = pstrndup(resp_pool, resp_buf, res);
 
-  resp_last_response_code = pstrdup(resp_pool, resp->num);
-  resp_last_response_msg = pstrdup(resp_pool, resp->msg);
+  if (numeric != R_DUP) {
+    resp_last_response_code = pstrdup(resp_pool, resp->num);
+  }
+  resp_last_response_msg = pstrndup(resp_pool, resp->msg, res);
 
   pr_trace_msg(trace_channel, 7, "error response added to pending list: %s %s",
     resp->num ? resp->num : "(null)", resp->msg);
@@ -247,6 +256,7 @@ void pr_response_add_err(const char *numeric, const char *fmt, ...) {
     *head &&
     (!numeric || !(*head)->num || strcmp((*head)->num, numeric) <= 0) &&
     !(numeric && !(*head)->num && head == &resp_err_list);
+
   head = &(*head)->next);
 
   resp->next = *head;
@@ -255,20 +265,27 @@ void pr_response_add_err(const char *numeric, const char *fmt, ...) {
 
 void pr_response_add(const char *numeric, const char *fmt, ...) {
   pr_response_t *resp = NULL, **head = NULL;
+  int res;
   va_list msg;
 
+  if (fmt == NULL) {
+    return;
+  }
+
   va_start(msg, fmt);
-  vsnprintf(resp_buf, sizeof(resp_buf), fmt, msg);
+  res = vsnprintf(resp_buf, sizeof(resp_buf), fmt, msg);
   va_end(msg);
 
   resp_buf[sizeof(resp_buf) - 1] = '\0';
   
   resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));
   resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);
-  resp->msg = pstrdup(resp_pool, resp_buf);
+  resp->msg = pstrndup(resp_pool, resp_buf, res);
 
-  resp_last_response_code = pstrdup(resp_pool, resp->num);
-  resp_last_response_msg = pstrdup(resp_pool, resp->msg);
+  if (numeric != R_DUP) {
+    resp_last_response_code = pstrdup(resp_pool, resp->num);
+  }
+  resp_last_response_msg = pstrndup(resp_pool, resp->msg, res);
 
   pr_trace_msg(trace_channel, 7, "response added to pending list: %s %s",
     resp->num ? resp->num : "(null)", resp->msg);
@@ -299,11 +316,14 @@ void pr_response_add(const char *numeric, const char *fmt, ...) {
 }
 
 void pr_response_send_async(const char *resp_numeric, const char *fmt, ...) {
+  int res;
   char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
   va_list msg;
-  int maxlen;
+  size_t len, max_len;
 
   if (resp_blocked) {
+    pr_trace_msg(trace_channel, 19,
+      "responses blocked, not sending async response");
     return;
   }
 
@@ -315,20 +335,22 @@ void pr_response_send_async(const char *resp_numeric, const char *fmt, ...) {
   }
 
   sstrncpy(buf, resp_numeric, sizeof(buf));
-  sstrcat(buf, " ", sizeof(buf));
+
+  len = strlen(resp_numeric);
+  sstrcat(buf + len, " ", sizeof(buf) - len);
   
-  maxlen = sizeof(buf) - strlen(buf) - 1;
+  max_len = sizeof(buf) - len;
   
   va_start(msg, fmt);
-  vsnprintf(buf + strlen(buf), maxlen, fmt, msg);
+  res = vsnprintf(buf + len + 1, max_len, fmt, msg);
   va_end(msg);
   
   buf[sizeof(buf) - 1] = '\0';
 
   resp_last_response_code = pstrdup(resp_pool, resp_numeric);
-  resp_last_response_msg = pstrdup(resp_pool, buf + strlen(resp_numeric) + 1);
+  resp_last_response_msg = pstrdup(resp_pool, buf + len + 1);
 
-  sstrcat(buf, "\r\n", sizeof(buf));
+  sstrcat(buf + res, "\r\n", sizeof(buf));
   RESPONSE_WRITE_STR_ASYNC(session.c->outstrm, "%s", buf)
 }
 
@@ -336,6 +358,7 @@ void pr_response_send(const char *resp_numeric, const char *fmt, ...) {
   va_list msg;
 
   if (resp_blocked) {
+    pr_trace_msg(trace_channel, 19, "responses blocked, not sending response");
     return;
   }
 
@@ -363,6 +386,8 @@ void pr_response_send_raw(const char *fmt, ...) {
   va_list msg;
 
   if (resp_blocked) {
+    pr_trace_msg(trace_channel, 19,
+      "responses blocked, not sending raw response");
     return;
   }
 
@@ -381,4 +406,3 @@ void pr_response_send_raw(const char *fmt, ...) {
 
   RESPONSE_WRITE_STR(session.c->outstrm, "%s\r\n", resp_buf)
 }
-
diff --git a/src/rlimit.c b/src/rlimit.c
index 915680f..6297641 100644
--- a/src/rlimit.c
+++ b/src/rlimit.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2013 The ProFTPD Project team
+ * Copyright (c) 2013-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Resource limits implementation
- * $Id: rlimit.c,v 1.4 2013-06-06 20:26:11 castaglia Exp $
- */
+/* Resource limits implementation */
 
 #include "conf.h"
 
@@ -40,6 +38,11 @@ static int get_rlimit(int resource, rlim_t *current, rlim_t *max) {
 
   res = getrlimit(resource, &rlim);
   if (res < 0) {
+    /* Some libcs use EPERM instead of ENOSYS; weird. */
+    if (errno == EPERM) {
+      errno = ENOSYS;
+    }
+
     return res;
   }
 
diff --git a/src/scoreboard.c b/src/scoreboard.c
index 282177c..3733a2d 100644
--- a/src/scoreboard.c
+++ b/src/scoreboard.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2014 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* ProFTPD scoreboard support.
- * $Id: scoreboard.c,v 1.81 2013-12-09 19:16:14 castaglia Exp $
- */
+/* ProFTPD scoreboard support. */
 
 #include "conf.h"
 #include "privs.h"
@@ -130,25 +128,32 @@ static const char *get_lock_type(struct flock *lock) {
       break;
 
     default:
-      lock_type = "[unknown]";
+      errno = EINVAL;
+      lock_type = NULL;
   }
 
   return lock_type;
 }
 
-static int rlock_scoreboard(void) {
+int pr_lock_scoreboard(int mutex_fd, int lock_type) {
   struct flock lock;
   unsigned int nattempts = 1;
+  const char *lock_label;
 
-  lock.l_type = F_RDLCK;
+  lock.l_type = lock_type;
   lock.l_whence = SEEK_SET;
   lock.l_start = 0;
   lock.l_len = 0;
 
-  pr_trace_msg("lock", 9, "attempt #%u to read-lock scoreboard mutex fd %d",
-    nattempts, scoreboard_mutex_fd);
+  lock_label = get_lock_type(&lock);
+  if (lock_label == NULL) {
+    return -1;
+  }
+
+  pr_trace_msg("lock", 9, "attempt #%u to %s-lock scoreboard mutex fd %d",
+    nattempts, lock_label, mutex_fd);
 
-  while (fcntl(scoreboard_mutex_fd, F_SETLK, &lock) < 0) {
+  while (fcntl(mutex_fd, F_SETLK, &lock) < 0) {
     int xerrno = errno;
 
     if (xerrno == EINTR) {
@@ -157,16 +162,16 @@ static int rlock_scoreboard(void) {
     }
 
     pr_trace_msg("lock", 3,
-      "read-lock (attempt #%u) of scoreboard mutex fd %d failed: %s",
-      nattempts, scoreboard_mutex_fd, strerror(xerrno));
+      "%s-lock (attempt #%u) of scoreboard mutex fd %d failed: %s",
+      lock_label, nattempts, mutex_fd, strerror(xerrno));
     if (xerrno == EACCES) {
       struct flock locker;
 
       /* Get the PID of the process blocking this lock. */
-      if (fcntl(scoreboard_mutex_fd, F_GETLK, &locker) == 0) {
+      if (fcntl(mutex_fd, F_GETLK, &locker) == 0) {
         pr_trace_msg("lock", 3, "process ID %lu has blocking %s lock on "
           "scoreboard mutex fd %d", (unsigned long) locker.l_pid,
-          get_lock_type(&locker), scoreboard_mutex_fd);
+          get_lock_type(&locker), mutex_fd);
       }
     }
 
@@ -185,14 +190,14 @@ static int rlock_scoreboard(void) {
 
         errno = 0;
         pr_trace_msg("lock", 9,
-          "attempt #%u to read-lock scoreboard mutex fd %d", nattempts,
-          scoreboard_mutex_fd);
+          "attempt #%u to %s-lock scoreboard mutex fd %d", nattempts,
+          lock_label, mutex_fd);
         continue;
       }
 
-      pr_trace_msg("lock", 9, "unable to acquire read-lock on "
-        "scoreboard mutex fd %d after %u %s: %s", scoreboard_mutex_fd,
-        nattempts, nattempts != 1 ? "attempts" : "attempt", strerror(xerrno));
+      pr_trace_msg("lock", 9, "unable to acquire %s-lock on "
+        "scoreboard mutex fd %d after %u attempts: %s", lock_label, mutex_fd,
+        nattempts, strerror(xerrno));
     }
 
     errno = xerrno;
@@ -200,146 +205,60 @@ static int rlock_scoreboard(void) {
   }
 
   pr_trace_msg("lock", 9,
-    "read-lock of scoreboard mutex fd %d successful after %u %s",
-    scoreboard_mutex_fd, nattempts, nattempts != 1 ? "attempts" : "attempt");
+    "%s-lock of scoreboard mutex fd %d successful after %u %s", lock_label,
+    mutex_fd, nattempts, nattempts != 1 ? "attempts" : "attempt");
 
-  scoreboard_read_locked = TRUE;
   return 0;
 }
 
-static int unlock_entry(int fd) {
-  unsigned int nattempts = 1;
-
-  entry_lock.l_type = F_UNLCK;
-  entry_lock.l_whence = SEEK_CUR;
-  entry_lock.l_len = sizeof(pr_scoreboard_entry_t);
-
-  pr_trace_msg("lock", 9, "attempting to unlock scoreboard fd %d entry, "
-    "offset %" PR_LU, fd, (pr_off_t) entry_lock.l_start);
-
-  while (fcntl(fd, F_SETLK, &entry_lock) < 0) {
-    int xerrno = errno;
-
-    if (xerrno == EINTR) {
-      pr_signals_handle();
-      continue;
-    }
-
-    if (xerrno == EAGAIN) {
-      /* Treat this as an interrupted call, call pr_signals_handle() (which
-       * will delay for a few msecs because of EINTR), and try again.
-       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
-       */
-
-      nattempts++;
-      if (nattempts <= SCOREBOARD_MAX_LOCK_ATTEMPTS) {
-        errno = EINTR;
+static int rlock_scoreboard(void) {
+  int res;
 
-        pr_signals_handle();
+  res = pr_lock_scoreboard(scoreboard_mutex_fd, F_RDLCK);
+  if (res == 0) {
+    scoreboard_read_locked = TRUE;
+  }
 
-        errno = 0;
-        pr_trace_msg("lock", 9,
-          "attempt #%u to to unlock scoreboard fd %d entry, offset %" PR_LU,
-          nattempts, fd, (pr_off_t) entry_lock.l_start);
-        continue;
-      }
-    }
+  return res;
+}
 
-    pr_trace_msg("lock", 3, "unlock of scoreboard fd %d entry failed: %s", fd,
-      strerror(xerrno));
+static int wlock_scoreboard(void) {
+  int res;
 
-    errno = xerrno;
-    return -1;
+  res = pr_lock_scoreboard(scoreboard_mutex_fd, F_WRLCK);
+  if (res == 0) {
+    scoreboard_write_locked = TRUE;
   }
 
-  pr_trace_msg("lock", 9, "unlock of scoreboard fd %d entry, "
-    "offset %" PR_LU " succeeded", fd, (pr_off_t) entry_lock.l_start);
-
-  return 0;
+  return res;
 }
 
 static int unlock_scoreboard(void) {
-  struct flock lock;
-  unsigned int nattempts = 1;
-
-  lock.l_type = F_UNLCK;
-  lock.l_whence = SEEK_SET;
-  lock.l_start = 0;
-  lock.l_len = 0;
-
-  pr_trace_msg("lock", 9, "attempt #%u to unlock scoreboard mutex fd %d",
-    nattempts, scoreboard_mutex_fd);
-
-  while (fcntl(scoreboard_mutex_fd, F_SETLK, &lock) < 0) {
-    int xerrno = errno;
-
-    if (errno == EINTR) {
-      pr_signals_handle();
-      continue;
-    }
-
-    pr_trace_msg("lock", 3,
-      "unlock (attempt #%u) of scoreboard mutex fd %d failed: %s",
-      nattempts, scoreboard_mutex_fd, strerror(xerrno));
-    if (xerrno == EACCES) {
-      struct flock locker;
-
-      /* Get the PID of the process blocking this lock. */
-      if (fcntl(scoreboard_mutex_fd, F_GETLK, &locker) == 0) {
-        pr_trace_msg("lock", 3, "process ID %lu has blocking %s lock on "
-          "scoreboard mutex fd %d", (unsigned long) locker.l_pid,
-          get_lock_type(&locker), scoreboard_mutex_fd);
-      }
-    }
-
-    if (xerrno == EAGAIN ||
-        xerrno == EACCES) {
-      /* Treat this as an interrupted call, call pr_signals_handle() (which
-       * will delay for a few msecs because of EINTR), and try again.
-       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
-       */
-
-      nattempts++;
-      if (nattempts <= SCOREBOARD_MAX_LOCK_ATTEMPTS) {
-        errno = EINTR;
-
-        pr_signals_handle();
-
-        errno = 0;
-
-        pr_trace_msg("lock", 9,
-          "attempt #%u to unlock scoreboard mutex fd %d", nattempts,
-          scoreboard_mutex_fd);
-        continue;
-      }
-
-      pr_trace_msg("lock", 9,
-        "unable to unlock scoreboard mutex fd %d after %u %s: %s",
-        scoreboard_mutex_fd, nattempts, nattempts != 1 ? "attempts" : "attempt",
-        strerror(xerrno));
-    }
+  int res;
 
-    errno = xerrno;
-    return -1;
+  res = pr_lock_scoreboard(scoreboard_mutex_fd, F_UNLCK);
+  if (res == 0) {
+    scoreboard_read_locked = scoreboard_write_locked = FALSE;
   }
 
-  pr_trace_msg("lock", 9,
-    "unlock of scoreboard mutex fd %d successful after %u %s",
-    scoreboard_mutex_fd, nattempts, nattempts != 1 ? "attempts" : "attempt");
-
-  scoreboard_read_locked = scoreboard_write_locked = FALSE;
-  return 0;
+  return res;
 }
 
-static int wlock_entry(int fd) {
+int pr_scoreboard_entry_lock(int fd, int lock_type) {
   unsigned int nattempts = 1;
+  const char *lock_label;
 
-  entry_lock.l_type = F_WRLCK;
+  entry_lock.l_type = lock_type;
   entry_lock.l_whence = SEEK_CUR;
   entry_lock.l_len = sizeof(pr_scoreboard_entry_t);
 
-  pr_trace_msg("lock", 9, "attempting to write-lock scoreboard fd %d entry, "
-    "offset %" PR_LU, fd, (pr_off_t) entry_lock.l_start);
+  lock_label = get_lock_type(&entry_lock);
+  if (lock_label == NULL) {
+    return -1;
+  }
+
+  pr_trace_msg("lock", 9, "attempting to %s scoreboard fd %d entry, "
+    "offset %" PR_LU, lock_label, fd, (pr_off_t) entry_lock.l_start);
 
   while (fcntl(fd, F_SETLK, &entry_lock) < 0) {
     int xerrno = errno;
@@ -363,94 +282,38 @@ static int wlock_entry(int fd) {
 
         errno = 0;
         pr_trace_msg("lock", 9,
-          "attempt #%u to write-lock scoreboard fd %d entry, offset %" PR_LU,
-          nattempts, fd, (pr_off_t) entry_lock.l_start);
+          "attempt #%u to to %s scoreboard fd %d entry, offset %" PR_LU,
+          nattempts, lock_label, fd, (pr_off_t) entry_lock.l_start);
         continue;
       }
     }
 
-    pr_trace_msg("lock", 3, "write-lock of scoreboard fd %d entry failed: %s",
-      fd, strerror(xerrno));
+    pr_trace_msg("lock", 3, "%s of scoreboard fd %d entry failed: %s",
+      lock_label, fd, strerror(xerrno));
 
     errno = xerrno;
     return -1;
   }
 
-  pr_trace_msg("lock", 9, "write-lock of scoreboard fd %d entry, "
-    "offset %" PR_LU " succeeded", fd, (pr_off_t) entry_lock.l_start);
+  pr_trace_msg("lock", 9, "%s of scoreboard fd %d entry, "
+    "offset %" PR_LU " succeeded", lock_label, fd,
+    (pr_off_t) entry_lock.l_start);
 
   return 0;
 }
 
-static int wlock_scoreboard(void) {
-  struct flock lock;
-  unsigned int nattempts = 1;
-
-  lock.l_type = F_WRLCK;
-  lock.l_whence = 0;
-  lock.l_start = 0;
-  lock.l_len = 0;
-
-  pr_trace_msg("lock", 9, "attempt #%u to write-lock scoreboard mutex fd %d",
-    nattempts, scoreboard_mutex_fd);
-
-  while (fcntl(scoreboard_mutex_fd, F_SETLK, &lock) < 0) {
-    int xerrno = errno;
-
-    if (xerrno == EINTR) {
-      pr_signals_handle();
-      continue;
-    }
-
-    pr_trace_msg("lock", 3,
-      "write-lock (attempt #%u) of scoreboard mutex fd %d failed: %s",
-      nattempts, scoreboard_mutex_fd, strerror(xerrno));
-    if (xerrno == EACCES) {
-      struct flock locker;
-
-      /* Get the PID of the process blocking this lock. */
-      if (fcntl(scoreboard_mutex_fd, F_GETLK, &locker) == 0) {
-        pr_trace_msg("lock", 3, "process ID %lu has blocking %s lock on "
-          "scoreboard mutex fd %d", (unsigned long) locker.l_pid,
-          get_lock_type(&locker), scoreboard_mutex_fd);
-      }
-    }
-
-    if (xerrno == EAGAIN ||
-        xerrno == EACCES) {
-      /* Treat this as an interrupted call, call pr_signals_handle() (which
-       * will delay for a few msecs because of EINTR), and try again.
-       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
-       */
-
-      nattempts++;
-      if (nattempts <= SCOREBOARD_MAX_LOCK_ATTEMPTS) {
-        errno = EINTR;
-
-        pr_signals_handle();
-
-        errno = 0;
-        pr_trace_msg("lock", 9,
-          "attempt #%u to write-lock scoreboard mutex fd %d", nattempts,
-          scoreboard_mutex_fd);
-        continue;
-      }
-
-      pr_trace_msg("lock", 9, "unable to acquire write-lock on "
-        "scoreboard mutex fd %d after %u %s: %s", scoreboard_mutex_fd,
-        nattempts, nattempts != 1 ? "attempts" : "attempt", strerror(xerrno));
-    }
+static int unlock_entry(int fd) {
+  int res;
 
-    errno = xerrno;
-    return -1;
-  }
+  res = pr_scoreboard_entry_lock(fd, F_UNLCK); 
+  return res;
+}
 
-  pr_trace_msg("lock", 9,
-    "write-lock of scoreboard mutex fd %d successful after %u %s",
-    scoreboard_mutex_fd, nattempts, nattempts != 1 ? "attempts" : "attempt");
+static int wlock_entry(int fd) {
+  int res;
 
-  scoreboard_write_locked = TRUE;
-  return 0;
+  res = pr_scoreboard_entry_lock(fd, F_UNLCK); 
+  return res;
 }
 
 static int write_entry(int fd) {
@@ -943,6 +806,11 @@ int pr_set_scoreboard(const char *path) {
 }
 
 int pr_set_scoreboard_mutex(const char *path) {
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
   sstrncpy(scoreboard_mutex, path, sizeof(scoreboard_mutex));
   return 0;
 }
@@ -1100,8 +968,9 @@ pr_scoreboard_entry_t *pr_scoreboard_entry_read(void) {
 
     /* Do not proceed if we cannot lock the scoreboard. */
     res = rlock_scoreboard();
-    if (res < 0)
+    if (res < 0) {
       return NULL; 
+    }
   }
 
   pr_trace_msg(trace_channel, 5, "reading scoreboard entry");
@@ -1124,12 +993,10 @@ pr_scoreboard_entry_t *pr_scoreboard_entry_read(void) {
     if (scan_entry.sce_pid) {
       unlock_scoreboard();
       return &scan_entry;
-
-    } else
-      continue;
+    }
   }
 
-  unlock_scoreboard();
+  /* Technically we never reach this. */
   return NULL;
 }
 
@@ -1473,11 +1340,14 @@ int pr_scoreboard_entry_update(pid_t pid, ...) {
         break;
 
       default:
+        va_end(ap);
         errno = ENOENT;
         return -1;
     }
   }
 
+  va_end(ap);
+
   /* Write-lock this entry */
   wlock_entry(scoreboard_fd);
   if (write_entry(scoreboard_fd) < 0) {
@@ -1570,6 +1440,16 @@ int pr_scoreboard_scrub(void) {
  
   /* Skip past the scoreboard header. */
   curr_offset = lseek(fd, (off_t) sizeof(pr_scoreboard_header_t), SEEK_SET);
+  if (curr_offset < 0) {
+    xerrno = errno;
+
+    unlock_scoreboard();
+    (void) close(fd);
+
+    errno = xerrno;
+    return -1;
+  }
+
   entry_lock.l_start = curr_offset;
  
   PRIVS_ROOT
@@ -1673,8 +1553,11 @@ int pr_scoreboard_scrub(void) {
 
       /* Mark the current offset. */
       curr_offset = lseek(fd, (off_t) 0, SEEK_CUR);
-      entry_lock.l_start = curr_offset;
+      if (curr_offset < 0) {
+        break;
+      }
 
+      entry_lock.l_start = curr_offset;
     }
   }
 
diff --git a/src/session.c b/src/session.c
index 2c80947..30e479a 100644
--- a/src/session.c
+++ b/src/session.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2013 The ProFTPD Project team
+ * Copyright (c) 2009-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@
  * 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.
- *
- * $Id: session.c,v 1.16 2013-08-25 21:16:39 castaglia Exp $
  */
 
 #include "conf.h"
@@ -95,6 +93,7 @@ static void sess_cleanup(int flags) {
 
 void pr_session_disconnect(module *m, int reason_code,
     const char *details) {
+  int flags = 0;
 
   session.disconnect_reason = reason_code;
   session.disconnect_module = m;
@@ -113,7 +112,11 @@ void pr_session_disconnect(module *m, int reason_code,
     }
   }
 
-  pr_session_end(0);
+  if (reason_code == PR_SESS_DISCONNECT_SEGFAULT) {
+    flags |= PR_SESS_END_FL_ERROR;
+  }
+
+  pr_session_end(flags);
 }
 
 void pr_session_end(int flags) {
@@ -125,6 +128,10 @@ void pr_session_end(int flags) {
     return;
   }
 
+  if (flags & PR_SESS_END_FL_ERROR) {
+    exitcode = 1;
+  }
+
 #ifdef PR_USE_DEVEL
   destroy_pool(session.pool);
 
@@ -146,7 +153,7 @@ void pr_session_end(int flags) {
 #endif /* PR_DEVEL_PROFILE */
 }
 
-const char *pr_session_get_disconnect_reason(char **details) {
+const char *pr_session_get_disconnect_reason(const char **details) {
   const char *reason_str = NULL;
 
   switch (session.disconnect_reason) {
@@ -257,8 +264,37 @@ void pr_session_send_banner(server_rec *s, int flags) {
 
   masq = find_config(s->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
   if (masq != NULL) {
-    pr_netaddr_t *masq_addr = (pr_netaddr_t *) masq->argv[0];
-    serveraddress = pr_netaddr_get_ipstr(masq_addr);
+    const pr_netaddr_t *masq_addr = NULL;
+
+    if (masq->argv[0] != NULL) {
+      masq_addr = masq->argv[0];
+
+    } else {
+      const char *name;
+
+      /* Here we do a delayed lookup, to see if the configured name
+       * can be resolved yet (e.g. the network is now up); see Bug#4104.
+       */
+
+      name = masq->argv[1];
+
+      pr_log_debug(DEBUG10,
+        "performing delayed resolution of MasqueradeAddress '%s'", name);
+      masq_addr = pr_netaddr_get_addr(session.pool, name, NULL);
+      if (masq_addr != NULL) {
+        /* Stash the resolved pr_netaddr_t in the config_rec, so that other
+         * code paths will find it (within this session process).
+         */
+        masq->argv[0] = (void *) masq_addr;
+
+      } else {
+        pr_log_debug(DEBUG5, "unable to resolve '%s'", name);
+      }
+    }
+
+    if (masq_addr != NULL) {
+      serveraddress = pr_netaddr_get_ipstr(masq_addr);
+    }
   }
 
   c = find_config(s->conf, CONF_PARAM, "ServerIdent", FALSE);
@@ -270,7 +306,9 @@ void pr_session_send_banner(server_rec *s, int flags) {
 
     if (c &&
         c->argc > 1) {
-      char *server_ident = c->argv[1];
+      const char *server_ident;
+
+      server_ident = c->argv[1];
 
       if (strstr(server_ident, "%L") != NULL) {
         server_ident = sreplace(session.pool, server_ident, "%L",
@@ -287,6 +325,11 @@ void pr_session_send_banner(server_rec *s, int flags) {
           main_server->ServerName, NULL);
       }
 
+      if (strstr(server_ident, "%{version}") != NULL) {
+        server_ident = sreplace(session.pool, server_ident, "%{version}",
+          PROFTPD_VERSION_TEXT, NULL);
+      }
+
       if (flags & PR_DISPLAY_FL_SEND_NOW) {
         pr_response_send(R_220, "%s", server_ident);
 
@@ -298,22 +341,20 @@ void pr_session_send_banner(server_rec *s, int flags) {
                *defer_welcome == TRUE) {
 
       if (flags & PR_DISPLAY_FL_SEND_NOW) {
-        pr_response_send(R_220, "ProFTPD " PROFTPD_VERSION_TEXT
-          " Server ready.");
+        pr_response_send(R_220, _("ProFTPD Server ready."));
 
       } else {
-        pr_response_add(R_220, "ProFTPD " PROFTPD_VERSION_TEXT
-          " Server ready.");
+        pr_response_add(R_220, _("ProFTPD Server ready."));
       }
 
     } else {
       if (flags & PR_DISPLAY_FL_SEND_NOW) {
-        pr_response_send(R_220, "ProFTPD " PROFTPD_VERSION_TEXT
-          " Server (%s) [%s]", s->ServerName, serveraddress);
+        pr_response_send(R_220, _("ProFTPD Server (%s) [%s]"), s->ServerName,
+          serveraddress);
 
       } else {
-        pr_response_add(R_220, "ProFTPD " PROFTPD_VERSION_TEXT
-          " Server (%s) [%s]", s->ServerName, serveraddress);
+        pr_response_add(R_220, _("ProFTPD Server (%s) [%s]"), s->ServerName,
+          serveraddress);
       }
     }
 
@@ -328,7 +369,7 @@ void pr_session_send_banner(server_rec *s, int flags) {
 }
 
 int pr_session_set_idle(void) {
-  char *user = NULL;
+  const char *user = NULL;
 
   pr_scoreboard_entry_update(session.pid,
     PR_SCORE_BEGIN_IDLE, time(NULL),
@@ -349,7 +390,7 @@ int pr_session_set_idle(void) {
 }
 
 int pr_session_set_protocol(const char *sess_proto) {
-  int count, res;
+  int count, res, xerrno;
 
   if (sess_proto == NULL) {
     errno = EINVAL;
@@ -360,25 +401,24 @@ int pr_session_set_protocol(const char *sess_proto) {
   if (count > 0) {
     res = pr_table_set(session.notes, pstrdup(session.pool, "protocol"),
       pstrdup(session.pool, sess_proto), 0);
+    xerrno = errno;
 
-    if (res == 0) {
-      /* Update the scoreboard entry for this session with the protocol. */
-      pr_scoreboard_entry_update(session.pid, PR_SCORE_PROTOCOL, sess_proto,
-        NULL);
-    }
+    /* Update the scoreboard entry for this session with the protocol. */
+    pr_scoreboard_entry_update(session.pid, PR_SCORE_PROTOCOL, sess_proto,
+      NULL);
 
+    errno = xerrno;
     return res;
   }
 
   res = pr_table_add(session.notes, pstrdup(session.pool, "protocol"),
     pstrdup(session.pool, sess_proto), 0);
+  xerrno = errno;
 
-  if (res == 0) {
-    /* Update the scoreboard entry for this session with the protocol. */
-    pr_scoreboard_entry_update(session.pid, PR_SCORE_PROTOCOL, sess_proto,
-      NULL);
-  }
+  /* Update the scoreboard entry for this session with the protocol. */
+  pr_scoreboard_entry_update(session.pid, PR_SCORE_PROTOCOL, sess_proto, NULL);
 
+  errno = xerrno;
   return res;
 }
 
diff --git a/src/sets.c b/src/sets.c
index d38aabf..3681ee2 100644
--- a/src/sets.c
+++ b/src/sets.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2008 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,9 +23,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Generic set manipulation
- * $Id: sets.c,v 1.16 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Generic set manipulation */
 
 #include "conf.h"
 
diff --git a/src/signals.c b/src/signals.c
new file mode 100644
index 0000000..8e0592c
--- /dev/null
+++ b/src/signals.c
@@ -0,0 +1,720 @@
+/*
+ * ProFTPD - FTP server daemon
+ * Copyright (c) 2014-2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, the ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Signal handling. */
+
+#include "conf.h"
+#include "privs.h"
+
+#ifdef HAVE_EXECINFO_H
+# include <execinfo.h>
+#endif
+
+#ifdef HAVE_UCONTEXT_H
+# include <ucontext.h>
+#endif
+
+/* From src/main.c */
+extern unsigned char is_master;
+extern pid_t mpid;
+extern int nodaemon;
+
+int have_dead_child = FALSE;
+volatile unsigned int recvd_signal_flags = 0;
+
+static RETSIGTYPE sig_terminate(int);
+static void install_stacktrace_handler(void);
+
+/* Used to capture an "unknown" signal value that causes termination. */
+static int term_signo = 0;
+
+static void finish_terminate(int signo) {
+  int reason_code = PR_SESS_DISCONNECT_SIGNAL;
+
+  if (is_master &&
+      mpid == getpid()) {
+    PRIVS_ROOT
+
+    /* Do not need the pidfile any longer. */
+    if (ServerType == SERVER_STANDALONE &&
+        !nodaemon) {
+      pr_pidfile_remove();
+    }
+
+    /* Run any exit handlers registered in the master process here, so that
+     * they may have the benefit of root privs.  More than likely these
+     * exit handlers were registered by modules' module initialization
+     * functions, which also occur under root priv conditions.
+     *
+     * If an exit handler is registered after the fork(), it won't be run here;
+     * that registration occurs in a different process space.
+     */
+    pr_event_generate("core.exit", NULL);
+    pr_event_generate("core.shutdown", NULL);
+
+    /* Remove the registered exit handlers now, so that the ensuing
+     * pr_session_end() call (outside the root privs condition) does not call
+     * the exit handlers for the master process again.
+     */
+    pr_event_unregister(NULL, "core.exit", NULL);
+    pr_event_unregister(NULL, "core.shutdown", NULL);
+
+    PRIVS_RELINQUISH
+
+    if (ServerType == SERVER_STANDALONE) {
+      pr_log_pri(PR_LOG_NOTICE, "ProFTPD " PROFTPD_VERSION_TEXT
+        " standalone mode SHUTDOWN");
+
+      /* Clean up the scoreboard */
+      PRIVS_ROOT
+      pr_delete_scoreboard();
+      PRIVS_RELINQUISH
+    }
+  }
+
+  if (signo == SIGSEGV) {
+    reason_code = PR_SESS_DISCONNECT_SEGFAULT;
+  }
+
+  pr_session_disconnect(NULL, reason_code, "Killed by signal");
+}
+
+static void handle_abort(void) {
+  pr_log_pri(PR_LOG_NOTICE, "ProFTPD received SIGABRT signal, no core dump");
+  finish_terminate(SIGABRT);
+}
+
+static void handle_chld(void) {
+  sigset_t sig_set;
+  pid_t pid;
+
+  sigemptyset(&sig_set);
+  sigaddset(&sig_set, SIGTERM);
+  sigaddset(&sig_set, SIGCHLD);
+
+  pr_alarms_block();
+
+  /* Block SIGTERM in here, so we don't create havoc with the child list
+   * while modifying it.
+   */
+  if (sigprocmask(SIG_BLOCK, &sig_set, NULL) < 0) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to block signal set: %s", strerror(errno));
+  }
+
+  while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
+    if (child_remove(pid) == 0) {
+      have_dead_child = TRUE;
+    }
+  }
+
+  if (sigprocmask(SIG_UNBLOCK, &sig_set, NULL) < 0) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to unblock signal set: %s", strerror(errno));
+  }
+
+  pr_alarms_unblock();
+}
+
+static void handle_evnt(void) {
+  pr_event_generate("core.signal.USR2", NULL);
+}
+
+static void handle_terminate_with_kids(void) {
+  /* Do not log if we are a child that has been terminated. */
+  if (is_master == TRUE) {
+
+    /* Send a SIGTERM to all our children */
+    if (child_count()) {
+      PRIVS_ROOT
+      child_signal(SIGTERM);
+      PRIVS_RELINQUISH
+    }
+
+    pr_log_pri(PR_LOG_NOTICE, "ProFTPD killed (signal %d)", term_signo);
+  }
+
+  finish_terminate(term_signo);
+}
+
+static void handle_terminate_without_kids(void) {
+  pr_log_pri(PR_LOG_WARNING, "ProFTPD terminating (signal %d)", term_signo);
+  finish_terminate(term_signo);
+}
+
+static void handle_stacktrace_signal(int signo, siginfo_t *info, void *ptr) {
+#ifdef HAVE_BACKTRACE
+  register int i;
+# if defined(HAVE_UCONTEXT_H)
+  ucontext_t *uc = NULL;
+# endif /* !HAVE_UCONTEXT_H */
+  void *trace[PR_TUNABLE_CALLER_DEPTH];
+  char **strings = NULL;
+  int tracesz;
+#endif /* HAVE_BACKTRACE */
+
+  /* Call the "normal" signal handler. */
+  table_handling_signal(TRUE);
+
+  pr_log_pri(PR_LOG_ERR, "-----BEGIN STACK TRACE-----");
+
+#ifdef HAVE_BACKTRACE
+  tracesz = backtrace(trace, PR_TUNABLE_CALLER_DEPTH);
+  if (tracesz < 0) {
+    pr_log_pri(PR_LOG_ERR, "backtrace(3) error: %s", strerror(errno));
+  }
+
+# if defined(HAVE_UCONTEXT_H)
+  /* Overwrite sigaction with caller's address */
+  uc = (ucontext_t *) ptr;
+#  if defined(REG_EIP)
+  trace[1] = (void *) uc->uc_mcontext.gregs[REG_EIP];
+#  elif defined(REG_RIP)
+  trace[1] = (void *) uc->uc_mcontext.gregs[REG_RIP];
+#  endif
+# endif /* !HAVE_UCONTEXT_H */
+
+# ifdef HAVE_BACKTRACE_SYMBOLS
+  strings = backtrace_symbols(trace, tracesz);
+  if (strings == NULL) {
+    pr_log_pri(PR_LOG_ERR, "backtrace_symbols(3) error: %s", strerror(errno));
+  }
+# endif /* HAVE_BACKTRACE_SYMBOLS */
+
+  if (strings != NULL) {
+    /* Skip first stack frame; it just points here. */
+    for (i = 1; i < tracesz; ++i) {
+      pr_log_pri(PR_LOG_ERR, "[%u] %s", i-1, strings[i]);
+    }
+  }
+#else
+  pr_log_pri(PR_LOG_ERR, " backtrace(3) unavailable");
+#endif /* HAVE_BACKTRACE */
+  pr_log_pri(PR_LOG_ERR, "-----END STACK TRACE-----");
+
+  sig_terminate(signo);
+  finish_terminate(signo);
+}
+
+static void handle_xcpu(void) {
+  pr_log_pri(PR_LOG_NOTICE, "ProFTPD CPU limit exceeded (signal %d)", SIGXCPU);
+  finish_terminate(SIGXCPU);
+}
+
+#ifdef SIGXFSZ
+static void handle_xfsz(void) {
+  pr_log_pri(PR_LOG_NOTICE, "ProFTPD File size limit exceeded (signal %d)",
+    SIGXFSZ);
+  finish_terminate(SIGXFSZ);
+}
+#endif /* SIGXFSZ */
+
+static RETSIGTYPE sig_child(int signo) {
+  recvd_signal_flags |= RECEIVED_SIG_CHLD;
+
+  /* We make an exception here to the synchronous processing that is done
+   * for other signals; SIGCHLD is handled asynchronously.  This is made
+   * necessary by two things.
+   *
+   * First, we need to support non-POSIX systems.  Under POSIX, once a
+   * signal handler has been configured for a given signal, that becomes
+   * that signal's disposition, until explicitly changed later.  Non-POSIX
+   * systems, on the other hand, will restore the default disposition of
+   * a signal after a custom signal handler has been configured.  Thus,
+   * to properly support non-POSIX systems, a call to signal(2) is necessary
+   * as one of the last steps in our signal handlers.
+   *
+   * Second, SVR4 systems differ specifically in their semantics of signal(2)
+   * and SIGCHLD.  These systems will check for any unhandled SIGCHLD
+   * signals, waiting to be reaped via wait(2) or waitpid(2), whenever
+   * the disposition of SIGCHLD is changed.  This means that if our process
+   * handles SIGCHLD, but does not call wait(2) or waitpid(2), and then
+   * calls signal(2), another SIGCHLD is generated; this loop repeats,
+   * until the process runs out of stack space and terminates.
+   *
+   * Thus, in order to cover this interaction, we'll need to call handle_chld()
+   * here, asynchronously.  handle_chld() does the work of reaping dead
+   * child processes, and does not seem to call any non-reentrant functions,
+   * so it should be safe.
+   */
+
+  handle_chld();
+
+  if (signal(SIGCHLD, sig_child) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGCHLD (signal %d) handler: %s", SIGCHLD,
+      strerror(errno));
+  }
+}
+
+#ifdef PR_DEVEL_COREDUMP
+static char *prepare_core(void) {
+  static char dir[256];
+
+  memset(dir, '\0', sizeof(dir));
+  snprintf(dir, sizeof(dir)-1, "%s/proftpd-core-%lu", PR_CORE_DIR,
+    (unsigned long) getpid());
+
+  if (mkdir(dir, 0700) < 0) {
+    pr_log_pri(PR_LOG_WARNING, "unable to create directory '%s' for "
+      "coredump: %s", dir, strerror(errno));
+
+  } else {
+    chdir(dir);
+  }
+
+  return dir;
+}
+#endif /* PR_DEVEL_COREDUMP */
+
+static RETSIGTYPE sig_abort(int signo) {
+  recvd_signal_flags |= RECEIVED_SIG_ABORT;
+
+  if (signal(SIGABRT, SIG_DFL) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGABRT (signal %d) handler: %s", SIGABRT,
+      strerror(errno));
+  }
+
+#ifdef PR_DEVEL_COREDUMP
+  pr_log_pri(PR_LOG_NOTICE, "ProFTPD received SIGABRT signal, generating core "
+    "file in %s", prepare_core());
+  pr_session_end(PR_SESS_END_FL_NOEXIT);
+  abort();
+#endif /* PR_DEVEL_COREDUMP */
+}
+
+static RETSIGTYPE sig_terminate(int signo) {
+  const char *signame = "(unsupported)";
+  int log_signal = TRUE, log_stacktrace = TRUE;
+
+  /* Capture the signal number for later display purposes. */
+  term_signo = signo;
+
+  /* Some terminating signals get more special treatment than others. */
+
+  switch (signo) {
+    case SIGSEGV:
+      recvd_signal_flags |= RECEIVED_SIG_SEGV;
+      signame = "SIGSEGV";
+      break;
+
+    case SIGXCPU:
+      recvd_signal_flags |= RECEIVED_SIG_XCPU;
+      signame = "SIGXCPU";
+      break;
+
+#ifdef SIGXFSZ
+      recvd_signal_flags |= RECEIVED_SIG_XFSZ;
+      signame = "SIGXFSZ";
+      break;
+#endif /* SIGXFSZ */
+
+    case SIGTERM:
+      /* Since SIGTERM is more common, we do not want to log as much for it. */
+      log_signal = log_stacktrace = FALSE;
+      recvd_signal_flags |= RECEIVED_SIG_TERMINATE;
+      signame = "SIGTERM";
+      break;
+
+#ifdef SIGBUS
+    case SIGBUS:
+      recvd_signal_flags |= RECEIVED_SIG_TERMINATE;
+      signame = "SIGBUS";
+      break;
+#endif /* SIGBUS */
+
+    case SIGILL:
+      recvd_signal_flags |= RECEIVED_SIG_TERMINATE;
+      signame = "SIGILL";
+      break;
+
+    case SIGINT:
+      recvd_signal_flags |= RECEIVED_SIG_TERMINATE;
+      signame = "SIGINT";
+      break;
+
+    default:
+      /* Note that we do NOT want to automatically set the
+       * RECEIVED_SIG_TERMINATE here by as a fallback for unspecified signals;
+       * that flag causes the daemon to terminate all of its child processes.
+       * And not every signal should have that effect; it's on a case-by-case
+       * basis.
+       */
+      break;
+  }
+
+  if (log_signal == TRUE) {
+    /* This is probably not the safest thing to be doing, but since the
+     * process is terminating anyway, why not?  It helps when knowing/logging
+     * that a segfault (or other unusual event) happened.
+     */
+    pr_trace_msg("signal", 9, "handling %s (signal %d)", signame, signo);
+    pr_log_pri(PR_LOG_NOTICE, "ProFTPD terminating (signal %d)", signo);
+
+    if (!is_master) {
+      pr_log_pri(PR_LOG_INFO, "%s session closed.",
+        pr_session_get_protocol(PR_SESS_PROTO_FL_LOGOUT));
+    }
+  }
+
+  if (log_stacktrace == TRUE) {
+    install_stacktrace_handler();
+  }
+
+  /* Ignore future occurrences of this signal; we'll be terminating anyway. */
+  if (signal(signo, SIG_IGN) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install handler for signal %d: %s", signo, strerror(errno));
+  }
+}
+
+static void install_stacktrace_handler(void) {
+  struct sigaction action;
+
+  memset(&action, 0, sizeof(action));
+  action.sa_sigaction = handle_stacktrace_signal;
+  action.sa_flags = SA_SIGINFO;
+
+  if (sigaction(SIGSEGV, &action, NULL) < 0) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGSEGV stacktrace signal handler: %s",
+      strerror(errno));
+  }
+#ifdef SIGBUS
+  if (sigaction(SIGBUS, &action, NULL) < 0) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGBUS stacktrace signal handler: %s",
+      strerror(errno));
+  }
+#endif /* SIGBUS */
+  if (sigaction(SIGXCPU, &action, NULL) < 0) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGXCPU stacktrace signal handler: %s",
+      strerror(errno));
+  }
+}
+
+/* This function is to handle the dispatching of actions based on
+ * signals received by the signal handlers, to avoid signal handler-based
+ * race conditions.
+ */
+void pr_signals_handle(void) {
+  table_handling_signal(TRUE);
+
+  if (errno == EINTR &&
+      PR_TUNABLE_EINTR_RETRY_INTERVAL > 0) {
+    struct timeval tv;
+    unsigned long interval_usecs = PR_TUNABLE_EINTR_RETRY_INTERVAL * 1000000;
+
+    tv.tv_sec = (interval_usecs / 1000000);
+    tv.tv_usec = (interval_usecs - (tv.tv_sec * 1000000));
+
+    pr_trace_msg("signal", 18, "interrupted system call, "
+      "delaying for %lu %s, %lu %s",
+      (unsigned long) tv.tv_sec, tv.tv_sec != 1 ? "secs" : "sec",
+      (unsigned long) tv.tv_usec, tv.tv_usec != 1 ? "microsecs" : "microsec");
+
+    pr_timer_usleep(interval_usecs);
+
+    /* Clear the EINTR errno, now that we've dealt with it. */
+    errno = 0;
+  }
+
+  while (recvd_signal_flags) {
+    if (recvd_signal_flags & RECEIVED_SIG_ALRM) {
+      recvd_signal_flags &= ~RECEIVED_SIG_ALRM;
+      pr_trace_msg("signal", 9, "handling SIGALRM (signal %d)", SIGALRM);
+      handle_alarm();
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_CHLD) {
+      recvd_signal_flags &= ~RECEIVED_SIG_CHLD;
+      pr_trace_msg("signal", 9, "handling SIGCHLD (signal %d)", SIGCHLD);
+      handle_chld();
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_EVENT) {
+      recvd_signal_flags &= ~RECEIVED_SIG_EVENT;
+
+      /* The "event" signal is SIGUSR2 in proftpd. */
+      pr_trace_msg("signal", 9, "handling SIGUSR2 (signal %d)", SIGUSR2);
+      handle_evnt();
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_SEGV) {
+      recvd_signal_flags &= ~RECEIVED_SIG_SEGV;
+      pr_trace_msg("signal", 9, "handling SIGSEGV (signal %d)", SIGSEGV);
+      handle_terminate_without_kids();
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_TERMINATE) {
+      recvd_signal_flags &= ~RECEIVED_SIG_TERMINATE;
+      pr_trace_msg("signal", 9, "handling signal %d", term_signo);
+      handle_terminate_with_kids();
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_XCPU) {
+      recvd_signal_flags &= ~RECEIVED_SIG_XCPU;
+      pr_trace_msg("signal", 9, "handling SIGXCPU (signal %d)", SIGXCPU);
+      handle_xcpu();
+    }
+
+#ifdef SIGXFSZ
+    if (recvd_signal_flags & RECEIVED_SIG_XFSZ) {
+      recvd_signal_flags &= ~RECEIVED_SIG_XFSZ;
+      pr_trace_msg("signal", 9, "handling SIGXFSZ (signal %d)", SIGXFSZ);
+      handle_xfsz();
+    }
+#endif /* SIGXFSZ */
+
+    if (recvd_signal_flags & RECEIVED_SIG_ABORT) {
+      recvd_signal_flags &= ~RECEIVED_SIG_ABORT;
+      pr_trace_msg("signal", 9, "handling SIGABRT (signal %d)", SIGABRT);
+      handle_abort();
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_RESTART) {
+      recvd_signal_flags &= ~RECEIVED_SIG_RESTART;
+      pr_trace_msg("signal", 9, "handling SIGHUP (signal %d)", SIGHUP);
+
+      /* NOTE: should this be done here, rather than using a schedule? */
+      schedule(restart_daemon, 0, NULL, NULL, NULL, NULL);
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_EXIT) {
+      recvd_signal_flags &= ~RECEIVED_SIG_EXIT;
+      pr_trace_msg("signal", 9, "handling SIGUSR1 (signal %d)", SIGUSR1);
+      pr_log_pri(PR_LOG_NOTICE, "%s", "Parent process requested shutdown");
+      pr_session_disconnect(NULL, PR_SESS_DISCONNECT_SERVER_SHUTDOWN, NULL);
+    }
+
+    if (recvd_signal_flags & RECEIVED_SIG_SHUTDOWN) {
+      recvd_signal_flags &= ~RECEIVED_SIG_SHUTDOWN;
+      pr_trace_msg("signal", 9, "handling SIGUSR1 (signal %d)", SIGUSR1);
+
+      /* NOTE: should this be done here, rather than using a schedule? */
+      schedule(shutdown_end_session, 0, NULL, NULL, NULL, NULL);
+    }
+  }
+
+  table_handling_signal(FALSE);
+}
+
+/* sig_restart occurs in the master daemon when manually "kill -HUP"
+ * in order to re-read configuration files, and is sent to all
+ * children by the master.
+ */
+static RETSIGTYPE sig_restart(int signo) {
+  recvd_signal_flags |= RECEIVED_SIG_RESTART;
+
+  if (signal(SIGHUP, sig_restart) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGHUP (signal %d) handler: %s", SIGHUP,
+      strerror(errno));
+  }
+}
+
+/* pr_signals_handle_disconnect is called in children when the parent daemon
+ * detects that shutmsg has been created and that client sessions should be
+ * destroyed.  If a file transfer is underway, the process simply dies,
+ * otherwise a function is scheduled to attempt to display the shutdown reason.
+ */
+RETSIGTYPE pr_signals_handle_disconnect(int signo) {
+
+  /* If this is an anonymous session, or a transfer is in progress,
+   * perform the exit a little later...
+   */
+  if ((session.sf_flags & SF_ANON) ||
+      (session.sf_flags & SF_XFER)) {
+    recvd_signal_flags |= RECEIVED_SIG_EXIT;
+
+  } else {
+    recvd_signal_flags |= RECEIVED_SIG_SHUTDOWN;
+  }
+
+  if (signal(SIGUSR1, SIG_IGN) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGUSR1 (signal %d) handler: %s", SIGUSR1,
+      strerror(errno));
+  }
+}
+
+/* "Events", in this case, are SIGUSR2 signals. */
+RETSIGTYPE pr_signals_handle_event(int signo) {
+  recvd_signal_flags |= RECEIVED_SIG_EVENT;
+
+  if (signal(SIGUSR2, pr_signals_handle_event) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGUSR2 (signal %d) handler: %s", SIGUSR2,
+      strerror(errno));
+  }
+}
+
+int init_signals(void) {
+  sigset_t sig_set;
+
+  /* Should the master server (only applicable in standalone mode)
+   * kill off children if we receive a signal that causes termination?
+   * Hmmmm... maybe this needs to be rethought, but I've done it in
+   * such a way as to only kill off our children if we receive a SIGTERM,
+   * meaning that the admin wants us dead (and probably our kids too).
+   */
+
+  /* The sub-pool for the child list is created the first time we fork
+   * off a child.  To conserve memory, the pool and list is destroyed
+   * when our last child dies (to prevent the list from eating more and
+   * more memory on long uptimes).
+   */
+
+  sigemptyset(&sig_set);
+
+  sigaddset(&sig_set, SIGCHLD);
+  sigaddset(&sig_set, SIGINT);
+  sigaddset(&sig_set, SIGQUIT);
+  sigaddset(&sig_set, SIGILL);
+  sigaddset(&sig_set, SIGABRT);
+  sigaddset(&sig_set, SIGFPE);
+  sigaddset(&sig_set, SIGSEGV);
+  sigaddset(&sig_set, SIGALRM);
+  sigaddset(&sig_set, SIGTERM);
+  sigaddset(&sig_set, SIGHUP);
+  sigaddset(&sig_set, SIGUSR2);
+#ifdef SIGSTKFLT
+  sigaddset(&sig_set, SIGSTKFLT);
+#endif /* SIGSTKFLT */
+#ifdef SIGIO
+  sigaddset(&sig_set, SIGIO);
+#endif /* SIGIO */
+#ifdef SIGBUS
+  sigaddset(&sig_set, SIGBUS);
+#endif /* SIGBUS */
+
+  if (signal(SIGCHLD, sig_child) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGCHLD (signal %d) handler: %s", SIGCHLD,
+      strerror(errno));
+  }
+
+  if (signal(SIGHUP, sig_restart) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGHUP (signal %d) handler: %s", SIGHUP,
+      strerror(errno));
+  }
+
+  if (signal(SIGINT, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGINT (signal %d) handler: %s", SIGINT,
+      strerror(errno));
+  }
+
+  if (signal(SIGQUIT, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGQUIT (signal %d) handler: %s", SIGQUIT,
+      strerror(errno));
+  }
+
+  if (signal(SIGILL, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGILL (signal %d) handler: %s", SIGILL,
+      strerror(errno));
+  }
+
+  if (signal(SIGFPE, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGFPE (signal %d) handler: %s", SIGFPE,
+      strerror(errno));
+  }
+
+#ifdef SIGXFSZ
+  if (signal(SIGXFSZ, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGXFSZ (signal %d) handler: %s", SIGXFSZ,
+      strerror(errno));
+  }
+#endif /* SIGXFSZ */
+
+  if (signal(SIGABRT, sig_abort) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGABRT (signal %d) handler: %s", SIGABRT,
+      strerror(errno));
+  }
+
+  /* Installs stacktrace handlers for SIGSEGV, SIGXCPU, and SIGBUS. */
+  install_stacktrace_handler();
+
+  /* Ignore SIGALRM; this will be changed when a timer is registered. But
+   * this will prevent SIGALRMs from killing us if we don't currently have
+   * any timers registered.
+    */
+  if (signal(SIGALRM, SIG_IGN) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGALRM (signal %d) handler: %s", SIGALRM,
+      strerror(errno));
+  }
+
+  if (signal(SIGTERM, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGTERM (signal %d) handler: %s", SIGTERM,
+      strerror(errno));
+  }
+
+  if (signal(SIGURG, SIG_IGN) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGURG (signal %d) handler: %s", SIGURG,
+      strerror(errno));
+  }
+
+#ifdef SIGSTKFLT
+  if (signal(SIGSTKFLT, sig_terminate) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGSTKFLT (signal %d) handler: %s", SIGSTKFLT,
+      strerror(errno));
+  }
+#endif /* SIGSTKFLT */
+
+#ifdef SIGIO
+  if (signal(SIGIO, SIG_IGN) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGIO (signal %d) handler: %s", SIGIO,
+      strerror(errno));
+  }
+#endif /* SIGIO */
+
+  if (signal(SIGUSR2, pr_signals_handle_event) == SIG_ERR) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to install SIGUSR2 (signal %d) handler: %s", SIGUSR2,
+      strerror(errno));
+  }
+
+  /* In case our parent left signals blocked (as happens under some
+   * poor inetd implementations)
+   */
+  if (sigprocmask(SIG_UNBLOCK, &sig_set, NULL) < 0) {
+    pr_log_pri(PR_LOG_NOTICE,
+      "unable to block signal set: %s", strerror(errno));
+  }
+
+  return 0;
+}
diff --git a/src/stash.c b/src/stash.c
index 004de58..f609cf3 100644
--- a/src/stash.c
+++ b/src/stash.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2010-2014 The ProFTPD Project team
+ * Copyright (c) 2010-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,19 @@
  * the source distribution.
  */
 
-/* Symbol table hashes
- * $Id: stash.c,v 1.12 2014-02-11 15:17:04 castaglia Exp $
- */
+/* Symbol table hashes */
 
 #include "conf.h"
 
+#ifndef PR_SYM_POOL_SIZE
+# define PR_SYM_POOL_SIZE		128
+#endif /* PR_SYM_POOL_SIZE */
+
 /* This local structure vastly speeds up symbol lookups. */
 struct stash {
   struct stash *next, *prev;
   pool *sym_pool;
+  unsigned int sym_hash;
   const char *sym_name;
   size_t sym_namelen;
   pr_stash_type_t sym_type;
@@ -46,10 +49,20 @@ struct stash {
   } ptr;
 };
 
-/* Symbol hashes for each type */
-static xaset_t *symbol_table[PR_TUNABLE_HASH_TABLE_SIZE];
 static pool *symbol_pool = NULL;
-static struct stash *curr_sym = NULL;
+
+/* Symbol hashes for each type */
+static xaset_t *conf_symbol_table[PR_TUNABLE_HASH_TABLE_SIZE];
+static struct stash *conf_curr_sym = NULL;
+
+static xaset_t *cmd_symbol_table[PR_TUNABLE_HASH_TABLE_SIZE];
+static struct stash *cmd_curr_sym = NULL;
+
+static xaset_t *auth_symbol_table[PR_TUNABLE_HASH_TABLE_SIZE];
+static struct stash *auth_curr_sym = NULL;
+
+static xaset_t *hook_symbol_table[PR_TUNABLE_HASH_TABLE_SIZE];
+static struct stash *hook_curr_sym = NULL;
 
 /* Symbol stash lookup code and management */
 
@@ -59,9 +72,9 @@ static struct stash *sym_alloc(void) {
 
   /* XXX Use a smaller pool size, since there are lots of sub-pools allocated
    * for Stash symbols.  The default pool size (PR_TUNABLE_POOL_SIZE, 512
-   * by default) is a bit large for symbols.
+   * bytes by default) is a bit large for symbols.
    */
-  sub_pool = pr_pool_create_sz(symbol_pool, 128);
+  sub_pool = pr_pool_create_sz(symbol_pool, PR_SYM_POOL_SIZE);
 
   sym = pcalloc(sub_pool, sizeof(struct stash));
   sym->sym_pool = sub_pool; 
@@ -72,7 +85,11 @@ static struct stash *sym_alloc(void) {
 
 static int sym_cmp(struct stash *s1, struct stash *s2) {
   int res;
-  size_t namelen;
+  size_t checked_len = 0, namelen;
+
+  if (s1->sym_hash != s2->sym_hash) {
+    return s1->sym_hash < s2->sym_hash ? -1 : 1;
+  }
 
   if (s1->sym_namelen != s2->sym_namelen) {
     return s1->sym_namelen < s2->sym_namelen ? -1 : 1;
@@ -81,7 +98,7 @@ static int sym_cmp(struct stash *s1, struct stash *s2) {
   namelen = s1->sym_namelen;
 
   /* Try to avoid strncmp(3) if we can. */
-  if (namelen >= 1) {
+  if (namelen >= 2) {
     char c1, c2;
 
     c1 = s1->sym_name[0];
@@ -91,31 +108,22 @@ static int sym_cmp(struct stash *s1, struct stash *s2) {
       return c1 < c2 ? -1 : 1;
     }
 
-    /* Special case (unlikely, but possible) */
-    if (namelen == 1 &&
-        c1 == '\0') {
-      return 0;
-    }
-  }
-
-  if (namelen >= 2) {
-    char c1, c2;
+    checked_len++;
 
-    c1 = s1->sym_name[1];
-    c2 = s2->sym_name[1];
+    if (namelen >= 3) {
+      c1 = s1->sym_name[1];
+      c2 = s2->sym_name[1];
 
-    if (c1 != c2) {
-      return c1 < c2 ? -1 : 1;
-    }
+      if (c1 != c2) {
+        return c1 < c2 ? -1 : 1;
+      }
 
-    /* Special case */
-    if (namelen == 2 &&
-        c1 == '\0') {
-      return 0;
+      checked_len++;
     }
   }
 
-  res = strncmp(s1->sym_name + 2, s2->sym_name + 2, namelen - 2);
+  res = strncmp(s1->sym_name + checked_len, s2->sym_name + checked_len,
+    namelen - checked_len);
 
   /* Higher priority modules must go BEFORE lower priority in the
    * hash tables.
@@ -153,29 +161,62 @@ static int sym_cmp(struct stash *s1, struct stash *s2) {
   return res;
 }
 
-static int symtab_hash(const char *name, size_t namelen) {
+static unsigned int symtab_hash(const char *name, size_t namelen) {
   register unsigned int i;
-  int total = 0;
+  unsigned int h = 0;
 
-  if (name == NULL)
+  if (name == NULL) {
     return 0;
+  }
 
   for (i = 0; i < namelen; i++) {
-    unsigned char *cp;
+    const char *cp;
+
+    cp = (const char *) &(name[i]);
+    h = (h * 33) + *cp;
+  }
+
+  return h;
+}
+
+static unsigned int sym_type_hash(pr_stash_type_t sym_type, const char *name,
+    size_t namelen) {
+  unsigned int hash;
+
+  /* XXX Ugly hack to support mixed cases of directives in config files. */
+  if (sym_type != PR_SYM_CONF) {
+    hash = symtab_hash(name, namelen);
+
+  } else {
+    register unsigned int i;
+    char *buf;
 
-    cp = (unsigned char *) &(name[i]);
-    total += (int) *cp;
+    buf = malloc(namelen+1);
+    if (buf == NULL) {
+      pr_log_pri(PR_LOG_ALERT, "Out of memory!");
+      exit(1);
+    }
+
+    buf[namelen] = '\0';
+    for (i = 0; i < namelen; i++) {
+      buf[i] = tolower((int) name[i]);
+    }
+
+    hash = symtab_hash(buf, namelen);
+    free(buf);
   }
 
-  return (total < PR_TUNABLE_HASH_TABLE_SIZE ? total :
-    (total % PR_TUNABLE_HASH_TABLE_SIZE));
+  return hash;
 }
 
 int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
   struct stash *sym = NULL;
+  unsigned int hash;
   int idx = 0;
+  xaset_t **symbol_table;
+  size_t sym_namelen = 0;
 
-  if (!data) {
+  if (data == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -187,6 +228,7 @@ int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
       sym->sym_name = ((conftable *) data)->directive;
       sym->sym_module = ((conftable *) data)->m;
       sym->ptr.sym_conf = data;
+      symbol_table = conf_symbol_table;
       break;
 
     case PR_SYM_CMD:
@@ -195,6 +237,7 @@ int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
       sym->sym_name = ((cmdtable *) data)->command;
       sym->sym_module = ((cmdtable *) data)->m;
       sym->ptr.sym_cmd = data;
+      symbol_table = cmd_symbol_table;
       break;
 
     case PR_SYM_AUTH:
@@ -203,6 +246,7 @@ int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
       sym->sym_name = ((authtable *) data)->name;
       sym->sym_module = ((authtable *) data)->m;
       sym->ptr.sym_auth = data;
+      symbol_table = auth_symbol_table;
       break;
 
     case PR_SYM_HOOK:
@@ -211,10 +255,11 @@ int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
       sym->sym_name = ((cmdtable *) data)->command;
       sym->sym_module = ((cmdtable *) data)->m;
       sym->ptr.sym_hook = data;
+      symbol_table = hook_symbol_table;
       break;
 
     default:
-      errno = ENOENT;
+      errno = EINVAL;
       return -1;
   }
 
@@ -225,28 +270,19 @@ int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
     return -1;
   }
 
-  /* Don't forget to include one for the terminating NUL. */
-  sym->sym_namelen = strlen(sym->sym_name) + 1;
-
-  /* XXX Ugly hack to support mixed cases of directives in config files. */
-  if (sym_type != PR_SYM_CONF) {
-    idx = symtab_hash(sym->sym_name, sym->sym_namelen);
-
-  } else {
-    register unsigned int i;
-    char buf[1024];
-    size_t buflen;
-
-    memset(buf, '\0', sizeof(buf));
-    sstrncpy(buf, sym->sym_name, sizeof(buf)-1);
+  sym_namelen = strlen(sym->sym_name);
+  if (sym_namelen == 0) {
+    destroy_pool(sym->sym_pool);
+    errno = EPERM;
+    return -1;
+  }
 
-    buflen = strlen(buf);
-    for (i = 0; i < buflen; i++) {
-      buf[i] = tolower((int) buf[i]);
-    }
+  /* Don't forget to include one for the terminating NUL. */
+  sym->sym_namelen = sym_namelen + 1;
 
-    idx = symtab_hash(buf, buflen + 1);
-  }
+  hash = sym_type_hash(sym_type, sym->sym_name, sym->sym_namelen);
+  idx = hash % PR_TUNABLE_HASH_TABLE_SIZE;
+  sym->sym_hash = hash;
 
   if (!symbol_table[idx]) {
     symbol_table[idx] = xaset_create(symbol_pool, (XASET_COMPARE) sym_cmp);
@@ -257,63 +293,88 @@ int pr_stash_add_symbol(pr_stash_type_t sym_type, void *data) {
 }
 
 static struct stash *stash_lookup(pr_stash_type_t sym_type,
-    const char *name, size_t namelen, int idx) {
+    const char *name, size_t namelen, int idx, unsigned int hash) {
   struct stash *sym = NULL;
+  xaset_t **symbol_table = NULL;
+
+  switch (sym_type) {
+    case PR_SYM_CONF:
+      symbol_table = conf_symbol_table;
+      break;
+
+    case PR_SYM_CMD:
+      symbol_table = cmd_symbol_table;
+      break;
+
+    case PR_SYM_AUTH:
+      symbol_table = auth_symbol_table;
+      break;
+
+    case PR_SYM_HOOK:
+      symbol_table = hook_symbol_table;
+      break;
+
+    default:
+      errno = EINVAL;
+      return NULL;
+  }
 
   if (symbol_table[idx]) {
     for (sym = (struct stash *) symbol_table[idx]->xas_list; sym;
         sym = sym->next) {
-      if (sym->sym_type == sym_type) {
-        int res;
+      int res;
 
-        if (name == NULL) {
-          break;
-        }
+      if (name == NULL) {
+        break;
+      }
 
-        if (sym->sym_namelen != namelen) {
-          continue;
-        }
+      if (sym->sym_hash != hash) {
+        continue;
+      }
 
-        /* Try to avoid strncmp(3) if we can. */
-        if (namelen >= 1) {
-          char c1, c2;
+      if (sym->sym_namelen != namelen) {
+        continue;
+      }
 
-          c1 = tolower((int) sym->sym_name[0]);
-          c2 = tolower((int) name[0]);
+      /* Try to avoid strncmp(3) if we can. */
+      if (namelen >= 1) {
+        char c1, c2;
 
-          if (c1 != c2) {
-            continue;
-          }
+        c1 = tolower((int) sym->sym_name[0]);
+        c2 = tolower((int) name[0]);
 
-          /* Special case (unlikely, but possible) */
-          if (namelen == 1 &&
-              c1 == '\0') {
-            break;
-          }
+        if (c1 != c2) {
+          continue;
         }
 
-        if (namelen >= 2) {
-          char c1, c2;
+        /* Special case (unlikely, but possible) */
+        if (namelen == 1 &&
+            c1 == '\0') {
+          break;
+        }
+      }
 
-          c1 = tolower((int) sym->sym_name[1]);
-          c2 = tolower((int) name[1]);
+      if (namelen >= 2) {
+        char c1, c2;
 
-          if (c1 != c2) {
-            continue;
-          }
+        c1 = tolower((int) sym->sym_name[1]);
+        c2 = tolower((int) name[1]);
 
-          /* Special case */
-          if (namelen == 2 &&
-              c1 == '\0') {
-            break;
-          }
+        if (c1 != c2) {
+          continue;
         }
 
-        res = strncasecmp(sym->sym_name + 2, name + 2, namelen - 2);
-        if (res == 0) {
+        /* Special case */
+        if (namelen == 2 &&
+            c1 == '\0') {
           break;
         }
       }
+
+      res = strncasecmp(sym->sym_name + 2, name + 2, namelen - 2);
+      if (res == 0) {
+        break;
+      }
     }
   }
 
@@ -321,21 +382,47 @@ static struct stash *stash_lookup(pr_stash_type_t sym_type,
 }
 
 static struct stash *stash_lookup_next(pr_stash_type_t sym_type,
-    const char *name, size_t namelen, int idx, void *prev) {
+    const char *name, size_t namelen, int idx, unsigned int hash, void *prev) {
   struct stash *sym = NULL;
   int last_hit = 0;
+  xaset_t **symbol_table = NULL;
+
+  switch (sym_type) {
+    case PR_SYM_CONF:
+      symbol_table = conf_symbol_table;
+      break;
+
+    case PR_SYM_CMD:
+      symbol_table = cmd_symbol_table;
+      break;
+
+    case PR_SYM_AUTH:
+      symbol_table = auth_symbol_table;
+      break;
+
+    case PR_SYM_HOOK:
+      symbol_table = hook_symbol_table;
+      break;
+
+    default:
+      errno = EINVAL;
+      return NULL;
+  }
 
   if (symbol_table[idx]) {
     for (sym = (struct stash *) symbol_table[idx]->xas_list; sym;
         sym = sym->next) {
-      if (last_hit &&
-          sym->sym_type == sym_type) {
+      if (last_hit) {
         int res;
 
         if (name == NULL) {
           break;
         }
 
+        if (sym->sym_hash != hash) {
+          continue;
+        }
+
         if (sym->sym_namelen != namelen) {
           continue;
         }
@@ -390,12 +477,21 @@ static struct stash *stash_lookup_next(pr_stash_type_t sym_type,
   return sym;
 }
 
-void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
-    void *prev, int *idx_cache) {
+void *pr_stash_get_symbol2(pr_stash_type_t sym_type, const char *name,
+    void *prev, int *idx_cache, unsigned int *hash_cache) {
   int idx;
+  unsigned int hash = 0;
   struct stash *sym = NULL;
   size_t namelen = 0;
 
+  if (sym_type != PR_SYM_CONF &&
+      sym_type != PR_SYM_CMD &&
+      sym_type != PR_SYM_AUTH &&
+      sym_type != PR_SYM_HOOK) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   if (name != NULL) {
     /* Don't forget to include one for the terminating NUL. */
     namelen = strlen(name) + 1;
@@ -405,31 +501,28 @@ void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
       *idx_cache != -1) {
     idx = *idx_cache;
 
-  } else {
-
-    /* XXX Ugly hack to support mixed cases of directives in config files. */
-    if (sym_type != PR_SYM_CONF) {
-      idx = symtab_hash(name, namelen);
-
-    } else {
-      register unsigned int i;
-      char buf[1024];
-      size_t buflen;
-
-      memset(buf, '\0', sizeof(buf));
-      sstrncpy(buf, name, sizeof(buf)-1);
-
-      buflen = strlen(buf);
-      for (i = 0; i < buflen; i++) {
-        buf[i] = tolower((int) buf[i]);
+    if (hash_cache != NULL) {
+      hash = *hash_cache;
+      if (hash == 0) {
+        hash = sym_type_hash(sym_type, name, namelen);
+        *hash_cache = hash;
       }
 
-      idx = symtab_hash(buf, buflen + 1);
+    } else {
+      hash = sym_type_hash(sym_type, name, namelen);
     }
 
+  } else {
+    hash = sym_type_hash(sym_type, name, namelen);
+    idx = hash % PR_TUNABLE_HASH_TABLE_SIZE;
+
     if (idx_cache != NULL) {
       *idx_cache = idx;
     }
+
+    if (hash_cache != NULL) {
+      *hash_cache = hash;
+    }
   }
 
   if (idx >= PR_TUNABLE_HASH_TABLE_SIZE) {
@@ -437,19 +530,24 @@ void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
       *idx_cache = -1;
     }
 
+    if (hash_cache != NULL) {
+      *hash_cache = 0;
+    }
+
     errno = EINVAL;
     return NULL;
   }
 
   if (prev) {
-    curr_sym = sym = stash_lookup_next(sym_type, name, namelen, idx, prev);
+    sym = stash_lookup_next(sym_type, name, namelen, idx, hash, prev);
 
   } else {
-    curr_sym = sym = stash_lookup(sym_type, name, namelen, idx);
+    sym = stash_lookup(sym_type, name, namelen, idx, hash);
   }
 
   switch (sym_type) {
     case PR_SYM_CONF:
+      conf_curr_sym = sym;
       if (sym) {
         return sym->ptr.sym_conf;
       }
@@ -458,6 +556,7 @@ void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
       return NULL;
 
     case PR_SYM_CMD:
+      cmd_curr_sym = sym;
       if (sym) {
         return sym->ptr.sym_cmd;
       }
@@ -466,6 +565,7 @@ void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
       return NULL;
 
     case PR_SYM_AUTH:
+      auth_curr_sym = sym;
       if (sym) {
         return sym->ptr.sym_auth;
       }
@@ -474,6 +574,7 @@ void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
       return NULL;
 
     case PR_SYM_HOOK:
+      hook_curr_sym = sym;
       if (sym) {
         return sym->ptr.sym_hook;
       }
@@ -486,148 +587,224 @@ void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
   return NULL;
 }
 
-int pr_stash_remove_symbol(pr_stash_type_t sym_type, const char *sym_name,
-    module *sym_module) {
-  int count = 0, symtab_idx = 0;
-  size_t sym_namelen = 0;
+void *pr_stash_get_symbol(pr_stash_type_t sym_type, const char *name,
+    void *prev, int *idx_cache) {
+  return pr_stash_get_symbol2(sym_type, name, prev, idx_cache, NULL);
+}
 
-  if (sym_name == NULL) {
+int pr_stash_remove_conf(const char *directive_name, module *m) {
+  int count = 0, prev_idx, symtab_idx = 0;
+  size_t directive_namelen = 0;
+  unsigned int hash;
+  conftable *tab = NULL;
+
+  if (directive_name == NULL) {
     errno = EINVAL;
     return -1;
   }
 
   /* Don't forget to include one for the terminating NUL. */
-  sym_namelen = strlen(sym_name) + 1;
-
-  /* XXX Ugly hack to support mixed cases of directives in config files. */
-  if (sym_type != PR_SYM_CONF) {
-    symtab_idx = symtab_hash(sym_name, sym_namelen);
-
-  } else {
-    register unsigned int i;
-    char buf[1024];
-    size_t buflen;
-
-    memset(buf, '\0', sizeof(buf));
-    sstrncpy(buf, sym_name, sizeof(buf)-1);
-
-    buflen = strlen(buf);
-    for (i = 0; i < buflen; i++) {
-      buf[i] = tolower((int) buf[i]);
+  directive_namelen = strlen(directive_name) + 1;
+
+  hash = sym_type_hash(PR_SYM_CONF, directive_name, directive_namelen);
+  symtab_idx = hash % PR_TUNABLE_HASH_TABLE_SIZE;
+  prev_idx = -1;
+
+  tab = pr_stash_get_symbol2(PR_SYM_CONF, directive_name, NULL, &prev_idx,
+    &hash);
+  while (tab) {
+    pr_signals_handle();
+
+    /* Note: this works because of a hack: the symbol lookup functions set a
+     * static pointer, conf_curr_sym, to point to the struct stash just looked
+     * up.  conf_curr_sym will not be NULL if pr_stash_get_symbol2() returns
+     * non-NULL.
+     */
+
+    if (m == NULL ||
+        conf_curr_sym->sym_module == m) {
+      xaset_remove(conf_symbol_table[symtab_idx],
+        (xasetmember_t *) conf_curr_sym);
+      destroy_pool(conf_curr_sym->sym_pool);
+      conf_curr_sym = NULL;
+      tab = NULL;
+      count++;
     }
 
-    symtab_idx = symtab_hash(buf, buflen + 1);
+    tab = pr_stash_get_symbol2(PR_SYM_CONF, directive_name, tab, &prev_idx,
+      &hash);
   }
 
-  switch (sym_type) {
-    case PR_SYM_CONF: {
-      int idx = -1;
-      conftable *tab;
-
-      tab = pr_stash_get_symbol(PR_SYM_CONF, sym_name, NULL, &idx);
-
-      while (tab) {
-        pr_signals_handle();
-
-        /* Note: this works because of a hack: the symbol lookup functions
-         * set a static pointer, curr_sym, to point to the struct stash
-         * just looked up.  curr_sym will not be NULL if pr_stash_get_symbol()
-         * returns non-NULL.
-         */
-
-        if (!sym_module ||
-            curr_sym->sym_module == sym_module) {
-          xaset_remove(symbol_table[symtab_idx], (xasetmember_t *) curr_sym);
-          destroy_pool(curr_sym->sym_pool);
-          curr_sym = NULL;
-          tab = NULL;
-          count++;
-        }
+  return count;
+}
 
-        tab = pr_stash_get_symbol(PR_SYM_CONF, sym_name, tab, &idx);
-      }
+/* Sentinel values:
+ *
+ *  cmd_type = 0
+ *  cmd_group = NULL
+ *  cmd_class = -1
+ */
+int pr_stash_remove_cmd(const char *cmd_name, module *m,
+    unsigned char cmd_type, const char *cmd_group, int cmd_class) {
+  int count = 0, prev_idx, symtab_idx = 0;
+  size_t cmd_namelen = 0;
+  unsigned int hash;
+  cmdtable *tab = NULL;
+
+  if (cmd_name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-      break;
+  /* Don't forget to include one for the terminating NUL. */
+  cmd_namelen = strlen(cmd_name) + 1;
+
+  hash = sym_type_hash(PR_SYM_CMD, cmd_name, cmd_namelen);
+  symtab_idx = hash % PR_TUNABLE_HASH_TABLE_SIZE;
+  prev_idx = -1;
+
+  tab = pr_stash_get_symbol2(PR_SYM_CMD, cmd_name, NULL, &prev_idx, &hash);
+  while (tab) {
+    cmdtable *cmd_sym;
+
+    pr_signals_handle();
+
+    /* Note: this works because of a hack: the symbol lookup functions set a
+     * static pointer, cmd_curr_sym, to point to the struct stash just looked
+     * up.  cmd_curr_sym will not be NULL if pr_stash_get_symbol2() returns
+     * non-NULL.
+     */
+
+    cmd_sym = cmd_curr_sym->ptr.sym_cmd;
+    if ((m == NULL || cmd_curr_sym->sym_module == m) &&
+        (cmd_type == 0 || cmd_sym->cmd_type == cmd_type) &&
+        (cmd_group == NULL ||
+         (cmd_group != NULL &&
+          cmd_sym->group != NULL &&
+          strcmp(cmd_sym->group, cmd_group) == 0)) &&
+        (cmd_class == -1 || cmd_sym->cmd_class == cmd_class)) {
+      xaset_remove(cmd_symbol_table[symtab_idx],
+        (xasetmember_t *) cmd_curr_sym);
+      destroy_pool(cmd_curr_sym->sym_pool);
+      cmd_curr_sym = NULL;
+      tab = NULL;
+      count++;
     }
 
-    case PR_SYM_CMD: {
-      int idx = -1;
-      cmdtable *tab;
-
-      tab = pr_stash_get_symbol(PR_SYM_CMD, sym_name, NULL, &idx);
-
-      while (tab) {
-        pr_signals_handle();
+    tab = pr_stash_get_symbol2(PR_SYM_CMD, cmd_name, tab, &prev_idx, &hash);
+  }
 
-        /* Note: this works because of a hack: the symbol lookup functions
-         * set a static pointer, curr_sym, to point to the struct stash
-         * just looked up.  
-         */
+  return count;
+}
 
-        if (!sym_module ||
-            curr_sym->sym_module == sym_module) {
-          xaset_remove(symbol_table[symtab_idx], (xasetmember_t *) curr_sym);
-          destroy_pool(curr_sym->sym_pool);
-          tab = NULL;
-          count++;
-        }
+int pr_stash_remove_auth(const char *api_name, module *m) {
+  int count = 0, prev_idx, symtab_idx = 0;
+  size_t api_namelen = 0;
+  unsigned int hash;
+  authtable *tab = NULL;
 
-        tab = pr_stash_get_symbol(PR_SYM_CMD, sym_name, tab, &idx);
-      }
+  if (api_name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-      break;
+  /* Don't forget to include one for the terminating NUL. */
+  api_namelen = strlen(api_name) + 1;
+
+  hash = sym_type_hash(PR_SYM_AUTH, api_name, api_namelen);
+  symtab_idx = hash % PR_TUNABLE_HASH_TABLE_SIZE;
+  prev_idx = -1;
+
+  tab = pr_stash_get_symbol2(PR_SYM_AUTH, api_name, NULL, &prev_idx, &hash);
+  while (tab) {
+    pr_signals_handle();
+
+    /* Note: this works because of a hack: the symbol lookup functions set a
+     * static pointer, auth_curr_sym, to point to the struct stash just looked
+     * up.  auth_curr_sym will not be NULL if pr_stash_get_symbol2() returns
+     * non-NULL.
+     */
+
+    if (m == NULL ||
+        auth_curr_sym->sym_module == m) {
+      xaset_remove(auth_symbol_table[symtab_idx],
+        (xasetmember_t *) auth_curr_sym);
+      destroy_pool(auth_curr_sym->sym_pool);
+      auth_curr_sym = NULL;
+      tab = NULL;
+      count++;
     }
 
-    case PR_SYM_AUTH: {
-      int idx = -1;
-      authtable *tab;
-
-      tab = pr_stash_get_symbol(PR_SYM_AUTH, sym_name, NULL, &idx);
-
-      while (tab) {
-        pr_signals_handle();
+    tab = pr_stash_get_symbol2(PR_SYM_AUTH, api_name, tab, &prev_idx, &hash);
+  }
 
-        /* Note: this works because of a hack: the symbol lookup functions
-         * set a static pointer, curr_sym, to point to the struct stash
-         * just looked up.  
-         */
+  return count;
+}
 
-        if (!sym_module ||
-            curr_sym->sym_module == sym_module) {
-          xaset_remove(symbol_table[symtab_idx], (xasetmember_t *) curr_sym);
-          destroy_pool(curr_sym->sym_pool);
-          tab = NULL;
-          count++;
-        }
+int pr_stash_remove_hook(const char *hook_name, module *m) {
+  int count = 0, prev_idx, symtab_idx = 0;
+  size_t hook_namelen = 0;
+  unsigned int hash;
+  cmdtable *tab = NULL;
 
-        tab = pr_stash_get_symbol(PR_SYM_AUTH, sym_name, tab, &idx);
-      }
+  if (hook_name == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
 
-      break;
+  /* Don't forget to include one for the terminating NUL. */
+  hook_namelen = strlen(hook_name) + 1;
+
+  hash = sym_type_hash(PR_SYM_HOOK, hook_name, hook_namelen);
+  symtab_idx = hash % PR_TUNABLE_HASH_TABLE_SIZE;
+  prev_idx = -1;
+
+  tab = pr_stash_get_symbol2(PR_SYM_HOOK, hook_name, NULL, &prev_idx, &hash);
+  while (tab) {
+    pr_signals_handle();
+
+    /* Note: this works because of a hack: the symbol lookup functions set a
+     * static pointer, hook_curr_sym, to point to the struct stash just looked
+     * up.  hook_curr_sym will not be NULL if pr_stash_get_symbol2() returns
+     * non-NULL.
+     */
+
+    if (m == NULL ||
+        hook_curr_sym->sym_module == m) {
+      xaset_remove(hook_symbol_table[symtab_idx],
+        (xasetmember_t *) hook_curr_sym);
+      destroy_pool(hook_curr_sym->sym_pool);
+      hook_curr_sym = NULL;
+      tab = NULL;
+      count++;
     }
 
-    case PR_SYM_HOOK: {
-      int idx = -1;
-      cmdtable *tab;
+    tab = pr_stash_get_symbol2(PR_SYM_HOOK, hook_name, tab, &prev_idx, &hash);
+  }
+
+  return count;
+}
 
-      tab = pr_stash_get_symbol(PR_SYM_HOOK, sym_name, NULL, &idx);
+int pr_stash_remove_symbol(pr_stash_type_t sym_type, const char *sym_name,
+    module *sym_module) {
+  int count = 0;
 
-      while (tab) {
-        pr_signals_handle();
+  switch (sym_type) {
+    case PR_SYM_CONF:
+      count = pr_stash_remove_conf(sym_name, sym_module);
+      break;
 
-        if (!sym_module ||
-            curr_sym->sym_module == sym_module) {
-          xaset_remove(symbol_table[symtab_idx], (xasetmember_t *) curr_sym);
-          destroy_pool(curr_sym->sym_pool);
-          tab = NULL;
-          count++;
-        }
+    case PR_SYM_CMD:
+      count = pr_stash_remove_cmd(sym_name, sym_module, 0, NULL, -1);
+      break;
 
-        tab = pr_stash_get_symbol(PR_SYM_HOOK, sym_name, tab, &idx);
-      }
+    case PR_SYM_AUTH:
+      count = pr_stash_remove_auth(sym_name, sym_module);
+      break;
 
+    case PR_SYM_HOOK:
+      count = pr_stash_remove_hook(sym_name, sym_module);
       break;
-    }
 
     default:
       errno = EINVAL;
@@ -652,53 +829,32 @@ static void stash_dumpf(const char *fmt, ...) {
 }
 #endif
 
-void pr_stash_dump(void (*dumpf)(const char *, ...)) {
 #ifdef PR_USE_DEVEL
+static unsigned int stash_dump_syms(xaset_t **symbol_table, const char *type,
+    void (*dumpf)(const char *, ...)) {
   register unsigned int i;
-  unsigned int nsyms = 0, nconf_syms = 0, ncmd_syms = 0, nauth_syms = 0,
-    nhook_syms = 0;
-
-  if (dumpf == NULL) {
-    dumpf = stash_dumpf;
-  }
+  unsigned int count = 0;
 
   for (i = 0; i < PR_TUNABLE_HASH_TABLE_SIZE; i++) {
     unsigned int nrow_syms = 0;
     struct stash *sym;
+    xaset_t *syms;
+
+    pr_signals_handle();
 
-    xaset_t *syms = symbol_table[i];
+    syms = symbol_table[i];
+    if (syms == NULL) {
+      continue;
+    }
 
     for (sym = (struct stash *) syms->xas_list; sym; sym = sym->next) {
       nrow_syms++;
-      nsyms++;
     }
 
-    dumpf("stab index %u: %u symbols", i, nrow_syms);
+    dumpf("%s stab index %u: %u symbols", type, i, nrow_syms);
 
     for (sym = (struct stash *) syms->xas_list; sym; sym = sym->next) {
-      const char *type = "<unknown>";
-
-      switch (sym->sym_type) {
-        case PR_SYM_CONF:
-          nconf_syms++;
-          type = "CONF";
-          break;
-
-        case PR_SYM_CMD:
-          ncmd_syms++;
-          type = "CMD";
-          break;
-
-        case PR_SYM_AUTH:
-          nauth_syms++;
-          type = "AUTH";
-          break;
-
-        case PR_SYM_HOOK:
-          nhook_syms++;
-          type = "HOOK";
-          break;
-      }
+      count++;
 
       if (sym->sym_module != NULL) {
         dumpf(" + %s symbol: %s (mod_%s.c)", type, sym->sym_name,
@@ -710,6 +866,25 @@ void pr_stash_dump(void (*dumpf)(const char *, ...)) {
     }
   }
 
+  return count;
+}
+#endif /* PR_USE_DEVEL */
+
+void pr_stash_dump(void (*dumpf)(const char *, ...)) {
+#ifdef PR_USE_DEVEL
+  unsigned int nsyms = 0, nconf_syms = 0, ncmd_syms = 0, nauth_syms = 0,
+    nhook_syms = 0;
+
+  if (dumpf == NULL) {
+    dumpf = stash_dumpf;
+  }
+
+  nconf_syms = stash_dump_syms(conf_symbol_table, "CONF", dumpf);
+  ncmd_syms = stash_dump_syms(cmd_symbol_table, "CMD", dumpf);
+  nauth_syms = stash_dump_syms(auth_symbol_table, "AUTH", dumpf);
+  nhook_syms = stash_dump_syms(hook_symbol_table, "HOOK", dumpf);
+  nsyms = nconf_syms + ncmd_syms + nauth_syms + nhook_syms;
+ 
   dumpf("stab: %u total symbols: %u CONF, %u CMD, %u AUTH, %u HOOK", nsyms,
     nconf_syms, ncmd_syms, nauth_syms, nhook_syms);
 
@@ -723,7 +898,11 @@ int init_stash(void) {
 
   symbol_pool = make_sub_pool(permanent_pool); 
   pr_pool_tag(symbol_pool, "Stash Pool");
-  memset(symbol_table, '\0', sizeof(symbol_table));
+
+  memset(conf_symbol_table, '\0', sizeof(conf_symbol_table));
+  memset(cmd_symbol_table, '\0', sizeof(cmd_symbol_table));
+  memset(auth_symbol_table, '\0', sizeof(auth_symbol_table));
+  memset(hook_symbol_table, '\0', sizeof(hook_symbol_table));
 
   return 0;
 }
diff --git a/src/str.c b/src/str.c
index eb7924c..eeed096 100644
--- a/src/str.c
+++ b/src/str.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,18 +22,17 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* String manipulation functions
- * $Id: str.c,v 1.21 2013-11-24 00:45:30 castaglia Exp $
- */
+/* String manipulation functions. */
 
 #include "conf.h"
 
 /* Maximum number of matches that we will do in a given string. */
 #define PR_STR_MAX_MATCHES			128
 
-static char *str_vreplace(pool *p, unsigned int max_replaces, char *s,
-    va_list args) {
-  char *m, *r, *src, *cp;
+static const char *str_vreplace(pool *p, unsigned int max_replaces,
+    const char *s, va_list args) {
+  const char *src;
+  char *m, *r, *cp;
   char *matches[PR_STR_MAX_MATCHES+1], *replaces[PR_STR_MAX_MATCHES+1];
   char buf[PR_TUNABLE_PATH_MAX] = {'\0'}, *pbuf = NULL;
   size_t nmatches = 0, rlen = 0;
@@ -51,7 +50,7 @@ static char *str_vreplace(pool *p, unsigned int max_replaces, char *s,
   while ((m = va_arg(args, char *)) != NULL &&
          nmatches < PR_STR_MAX_MATCHES) {
     char *tmp = NULL;
-    int count = 0;
+    unsigned int count = 0;
 
     r = va_arg(args, char *);
     if (r == NULL) {
@@ -169,9 +168,24 @@ static char *str_vreplace(pool *p, unsigned int max_replaces, char *s,
   return pbuf;
 }
 
-char *pr_str_replace(pool *p, unsigned int max_replaces, char *s, ...) {
+const char *pr_str_quote(pool *p, const char *str) {
+  if (p == NULL ||
+      str == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  return sreplace(p, str, "\"", "\"\"", NULL);
+}
+
+const char *quote_dir(pool *p, char *path) {
+  return pr_str_quote(p, path);
+}
+
+const char *pr_str_replace(pool *p, unsigned int max_replaces,
+    const char *s, ...) {
   va_list args;
-  char *res = NULL;
+  const char *res = NULL;
 
   if (p == NULL ||
       s == NULL ||
@@ -187,9 +201,9 @@ char *pr_str_replace(pool *p, unsigned int max_replaces, char *s, ...) {
   return res;
 }
 
-char *sreplace(pool *p, char *s, ...) {
+const char *sreplace(pool *p, const char *s, ...) {
   va_list args;
-  char *res = NULL;
+  const char *res = NULL;
 
   if (p == NULL ||
       s == NULL) {
@@ -251,7 +265,8 @@ char *pstrdup(pool *p, const char *str) {
   char *res;
   size_t len;
 
-  if (!p || !str) {
+  if (p == NULL ||
+      str == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -259,7 +274,10 @@ char *pstrdup(pool *p, const char *str) {
   len = strlen(str) + 1;
 
   res = palloc(p, len);
-  sstrncpy(res, str, len);
+  if (res != NULL) {
+    sstrncpy(res, str, len);
+  }
+
   return res;
 }
 
@@ -424,10 +442,12 @@ int pr_strnrstr(const char *s, size_t slen, const char *suffix,
   return res;
 }
 
-char *pr_str_strip(pool *p, char *str) {
-  char c, *dupstr, *start, *finish;
+const char *pr_str_strip(pool *p, const char *str) {
+  const char *dup_str, *start, *finish;
+  size_t len = 0;
  
-  if (!p || !str) {
+  if (p == NULL ||
+      str == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -438,22 +458,16 @@ char *pr_str_strip(pool *p, char *str) {
   /* Now, find the non-whitespace end of the given string */
   for (finish = &str[strlen(str)-1]; PR_ISSPACE(*finish); finish--);
 
-  /* finish is now pointing to a non-whitespace character.  So advance one
-   * character forward, and set that to NUL.
-   */
-  c = *++finish;
-  *finish = '\0';
+  /* Include for the last byte, of course. */
+  len = finish - start + 1;
 
   /* The space-stripped string is, then, everything from start to finish. */
-  dupstr = pstrdup(p, start);
-
-  /* Restore the given string buffer contents. */
-  *finish = c;
+  dup_str = pstrndup(p, start, len);
 
-  return dupstr;
+  return dup_str;
 }
 
-char *pr_str_strip_end(char *s, char *ch) {
+char *pr_str_strip_end(char *s, const char *ch) {
   size_t len;
 
   if (s == NULL ||
@@ -474,9 +488,519 @@ char *pr_str_strip_end(char *s, char *ch) {
   return s;
 }
 
+#if defined(HAVE_STRTOULL) && \
+  (SIZEOF_UID_T == SIZEOF_LONG_LONG && SIZEOF_GID_T == SIZEOF_LONG_LONG)
+static int parse_ull(const char *val, unsigned long long *num) {
+  char *endp = NULL;
+  unsigned long long res;
+
+  res = strtoull(val, &endp, 10);
+  if (endp && *endp) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  *num = res;
+  return 0;
+}
+#endif /* HAVE_STRTOULL */
+
+static int parse_ul(const char *val, unsigned long *num) {
+  char *endp = NULL;
+  unsigned long res;
+
+  res = strtoul(val, &endp, 10);
+  if (endp && *endp) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  *num = res;
+  return 0;
+}
+
+char *pr_str_bin2hex(pool *p, const unsigned char *buf, size_t len, int flags) {
+  static const char *hex_lc = "0123456789abcdef", *hex_uc = "0123456789ABCDEF";
+  register unsigned int i;
+  const char *hex_vals;
+  char *hex, *ptr;
+  size_t hex_len;
+
+  if (p == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (len == 0) {
+    return pstrdup(p, "");
+  }
+
+  /* By default, we use lowercase hex values. */
+  hex_vals = hex_lc;
+  if (flags & PR_STR_FL_HEX_USE_UC) {
+    hex_vals = hex_uc;
+  }
+
+  hex_len = (len * 2) + 1;
+  hex = palloc(p, hex_len);
+
+  ptr = hex;
+  for (i = 0; i < len; i++) {
+    *ptr++ = hex_vals[buf[i] >> 4];
+    *ptr++ = hex_vals[buf[i] % 16];
+  }
+  *ptr = '\0';
+
+  return hex;
+}
+
+static int c2h(char c, unsigned char *h) {
+  if (c >= '0' &&
+      c <= '9') {
+    *h = c - '0';
+    return TRUE;
+  }
+
+  if (c >= 'a' &&
+      c <= 'f') {
+    *h = c - 'a' + 10;
+    return TRUE;
+  }
+
+  if (c >= 'A' &&
+      c <= 'F') {
+    *h = c - 'A' + 10;
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
+unsigned char *pr_str_hex2bin(pool *p, const unsigned char *hex, size_t hex_len,
+    size_t *len) {
+  register unsigned int i, j;
+  unsigned char *data;
+  size_t data_len;
+
+  if (p == NULL ||
+      hex == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (hex_len == 0) {
+    hex_len = strlen((char *) hex);
+  }
+
+  if (hex_len == 0) {
+    data = (unsigned char *) pstrdup(p, "");
+    return data;
+  }
+
+  data_len = hex_len / 2;
+  data = palloc(p, data_len);
+
+  for (i = 0, j = 0; i < hex_len; i += 2) {
+    unsigned char v1, v2;
+
+    if (c2h(hex[i], &v1) == FALSE) {
+      errno = ERANGE;
+      return NULL;
+    }
+
+    if (c2h(hex[i+1], &v2) == FALSE) {
+      errno = ERANGE;
+      return NULL;
+    }
+
+    data[j++] = ((v1 << 4) | v2);
+  }
+
+  if (len != NULL) {
+    *len = data_len;
+  }
+
+  return data;
+}
+
+/* Calculate the Damerau-Levenshtein distance between strings `a' and `b'.
+ * This implementation borrows from the git implementation; see
+ * git/src/levenshtein.c.
+ */
+int pr_str_levenshtein(pool *p, const char *a, const char *b, int swap_cost,
+    int subst_cost, int insert_cost, int del_cost, int flags) {
+  size_t alen, blen;
+  unsigned int i, j;
+  int *row0, *row1, *row2, res;
+  pool *tmp_pool;
+
+  if (p == NULL ||
+      a == NULL ||
+      b == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  alen = strlen(a);
+  blen = strlen(b);
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "Levenshtein Distance pool");
+
+  if (flags & PR_STR_FL_IGNORE_CASE) {
+    char *a2, *b2;
+
+    a2 = pstrdup(tmp_pool, a);
+    for (i = 0; i < alen; i++) {
+      a2[i] = tolower((int) a[i]);
+    }
+
+    b2 = pstrdup(tmp_pool, b);
+    for (i = 0; i < blen; i++) {
+      b2[i] = tolower((int) b[i]);
+    }
+
+    a = a2;
+    b = b2;
+  }
+
+  row0 = pcalloc(tmp_pool, sizeof(int) * (blen + 1));
+  row1 = pcalloc(tmp_pool, sizeof(int) * (blen + 1));
+  row2 = pcalloc(tmp_pool, sizeof(int) * (blen + 1));
+
+  for (j = 0; j <= blen; j++) {
+    row1[j] = j * insert_cost;
+  }
+
+  for (i = 0; i < alen; i++) {
+    int *ptr;
+
+    row2[0] = (i + 1) * del_cost;
+    for (j = 0; j < blen; j++) {
+      /* Substitution */
+      row2[j + 1] = row1[j] + (subst_cost * (a[i] != b[j]));
+
+      /* Swap */
+      if (i > 0 &&
+          j > 0 &&
+          a[i-1] == b[j] &&
+          a[i] == b[j-1] &&
+          row2[j+1] > (row0[j-1] + swap_cost)) {
+        row2[j+1] = row0[j-1] + swap_cost;
+      }
+
+      /* Deletion */
+      if (row2[j+1] > (row1[j+1] + del_cost)) {
+        row2[j+1] = row1[j+1] + del_cost;
+      }
+
+      /* Insertion */
+      if (row2[j+1] > (row2[j] + insert_cost)) {
+        row2[j+1] = row2[j] + insert_cost;
+      }
+    }
+
+    ptr = row0;
+    row0 = row1;
+    row1 = row2;
+    row2 = ptr;
+  }
+
+  res = row2[blen];
+
+  destroy_pool(tmp_pool);
+  return res;
+}
+
+/* For tracking the Levenshtein distance for a string. */
+struct candidate {
+  const char *s;
+  int distance;
+  int flags;
+};
+
+static int distance_cmp(const void *a, const void *b) {
+  const struct candidate *cand1, *cand2;
+  const char *s1, *s2;
+  int distance1, distance2;
+
+  cand1 = a;
+  s1 = cand1->s;
+  distance1 = cand1->distance;
+
+  cand2 = b;
+  s2 = cand2->s;
+  distance2 = cand2->distance;
+
+  if (distance1 != distance2) {
+    return distance1 - distance2;
+  }
+
+  if (cand1->flags & PR_STR_FL_IGNORE_CASE) {
+    return strcasecmp(s1, s2);
+  }
+
+  return strcmp(s1, s2);
+}
+
+array_header *pr_str_get_similars(pool *p, const char *s,
+    array_header *candidates, int max_distance, int flags) {
+  register unsigned int i;
+  size_t len;
+  array_header *similars;
+  struct candidate **distances;
+  pool *tmp_pool;
+
+  if (p == NULL ||
+      s == NULL ||
+      candidates == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (candidates->nelts == 0) {
+    errno = ENOENT;
+    return NULL;
+  }
+
+  if (max_distance <= 0) {
+    max_distance = PR_STR_DEFAULT_MAX_EDIT_DISTANCE;
+  }
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "Similar Strings pool");
+
+  /* In order to use qsort(3), we need a contiguous block of memory, not
+   * one of our array_headers.
+   */
+
+  distances = pcalloc(tmp_pool, candidates->nelts * sizeof(struct candidate *));
+
+  len = strlen(s);
+  for (i = 0; i < candidates->nelts; i++) {
+    const char *c;
+    struct candidate *cand;
+    int prefix_match = FALSE;
+
+    c = ((const char **) candidates->elts)[i];
+    cand = pcalloc(tmp_pool, sizeof(struct candidate));
+    cand->s = c;
+    cand->flags = flags;
+
+    /* Give prefix matches a higher score */
+    if (flags & PR_STR_FL_IGNORE_CASE) {
+      if (strncasecmp(c, s, len) == 0) {
+        prefix_match = TRUE;
+      }
+
+    } else {
+      if (strncmp(c, s, len) == 0) {
+        prefix_match = TRUE;
+      }
+    }
+
+    if (prefix_match == TRUE) {
+      cand->distance = 0;
+
+    } else {
+      /* Note: We arbitrarily add one to the edit distance, in order to
+       * distinguish a distance of zero from our prefix match "distances" of
+       * zero above.
+       */
+      cand->distance = pr_str_levenshtein(tmp_pool, s, c, 0, 2, 1, 3,
+        flags) + 1;
+    }
+
+    distances[i] = cand;
+  }
+
+  qsort(distances, candidates->nelts, sizeof(struct candidate *), distance_cmp);
+
+  similars = make_array(p, candidates->nelts, sizeof(const char *));
+  for (i = 0; i < candidates->nelts; i++) {
+    struct candidate *cand;
+
+    cand = distances[i];
+    if (cand->distance <= max_distance) {
+      *((const char **) push_array(similars)) = cand->s;
+    }
+  }
+
+  destroy_pool(tmp_pool);
+  return similars;
+}
+
+array_header *pr_str_text_to_array(pool *p, const char *text, char delimiter) {
+  char *ptr;
+  array_header *items;
+  size_t text_len;
+
+  if (p == NULL ||
+      text == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  text_len = strlen(text);
+  items = make_array(p, 1, sizeof(char *));
+
+  if (text_len == 0) {
+    return items;
+  }
+
+  ptr = memchr(text, delimiter, text_len);
+  while (ptr != NULL) {
+    size_t item_len;
+
+    pr_signals_handle();
+
+    item_len = ptr - text;
+    if (item_len > 0) {
+      char *item;
+
+      item = palloc(p, item_len + 1);
+      memcpy(item, text, item_len);
+      item[item_len] = '\0';
+      *((char **) push_array(items)) = item;
+    }
+
+    text = ++ptr;
+
+    /* Include one byte for the delimiter character being skipped over. */
+    text_len = text_len - item_len - 1;
+
+    if (text_len == 0) {
+      break;
+    }
+
+    ptr = memchr(text, delimiter, text_len);
+  }
+
+  if (text_len > 0) {
+    *((char **) push_array(items)) = pstrdup(p, text);
+  }
+
+  return items;
+}
+
+int pr_str2uid(const char *val, uid_t *uid) {
+#ifdef HAVE_STRTOULL
+  unsigned long long ull = 0ULL;
+#endif /* HAVE_STRTOULL */
+  unsigned long ul = 0UL;
+
+  if (val == NULL ||
+      uid == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+#if SIZEOF_UID_T == SIZEOF_LONG_LONG
+# ifdef HAVE_STRTOULL
+  if (parse_ull(val, &ull) < 0) {
+    return -1;
+  }
+  *uid = ull; 
+
+# else
+  if (parse_ul(val, &ul) < 0) {
+    return -1;
+  }
+  *uid = ul;
+# endif /* HAVE_STRTOULL */
+#else
+  (void) ull;
+  if (parse_ul(val, &ul) < 0) {
+    return -1;
+  }
+  *uid = ul;
+#endif /* sizeof(uid_t) != sizeof(long long) */
+
+  return 0;
+}
+
+int pr_str2gid(const char *val, gid_t *gid) {
+#ifdef HAVE_STRTOULL
+  unsigned long long ull = 0ULL;
+#endif /* HAVE_STRTOULL */
+  unsigned long ul = 0UL;
+
+  if (val == NULL ||
+      gid == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+#if SIZEOF_GID_T == SIZEOF_LONG_LONG
+# ifdef HAVE_STRTOULL
+  if (parse_ull(val, &ull) < 0) {
+    return -1;
+  }
+  *gid = ull; 
+
+# else
+  if (parse_ul(val, &ul) < 0) {
+    return -1;
+  }
+  *gid = ul;
+# endif /* HAVE_STRTOULL */
+#else
+  (void) ull;
+  if (parse_ul(val, &ul) < 0) {
+    return -1;
+  }
+  *gid = ul;
+#endif /* sizeof(gid_t) != sizeof(long long) */
+
+  return 0;
+}
+
+const char *pr_uid2str(pool *p, uid_t uid) {
+  static char buf[64];
+
+  memset(&buf, 0, sizeof(buf));
+  if (uid != (uid_t) -1) {
+#if SIZEOF_UID_T == SIZEOF_LONG_LONG
+    snprintf(buf, sizeof(buf)-1, "%llu", (unsigned long long) uid);
+#else
+    snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) uid);
+#endif /* sizeof(uid_t) != sizeof(long long) */
+  } else {
+    snprintf(buf, sizeof(buf)-1, "%d", -1);
+  }
+
+  if (p != NULL) {
+    return pstrdup(p, buf);
+  }
+
+  return buf;
+}
+
+const char *pr_gid2str(pool *p, gid_t gid) {
+  static char buf[64];
+
+  memset(&buf, 0, sizeof(buf));
+  if (gid != (gid_t) -1) {
+#if SIZEOF_GID_T == SIZEOF_LONG_LONG
+    snprintf(buf, sizeof(buf)-1, "%llu", (unsigned long long) gid);
+#else
+    snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) gid);
+#endif /* sizeof(gid_t) != sizeof(long long) */
+  } else {
+    snprintf(buf, sizeof(buf)-1, "%d", -1);
+  }
+
+  if (p != NULL) {
+    return pstrdup(p, buf);
+  }
+
+  return buf;
+}
+
 /* NOTE: Update mod_ban's ban_parse_timestr() to use this function. */
 int pr_str_get_duration(const char *str, int *duration) {
-  unsigned int hours, mins, secs;
+  int hours, mins, secs;
   int flags = PR_STR_FL_IGNORE_CASE, has_suffix = FALSE;
   size_t len;
   char *ptr = NULL;
@@ -486,10 +1010,10 @@ int pr_str_get_duration(const char *str, int *duration) {
     return -1;
   }
 
-  if (sscanf(str, "%2u:%2u:%2u", &hours, &mins, &secs) == 3) {
-    if (hours > INT_MAX ||
-        mins > INT_MAX ||
-        secs > INT_MAX) {
+  if (sscanf(str, "%2d:%2d:%2d", &hours, &mins, &secs) == 3) {
+    if (hours < 0 ||
+        mins < 0 ||
+        secs < 0) {
       errno = ERANGE;
       return -1;
     }
@@ -523,8 +1047,8 @@ int pr_str_get_duration(const char *str, int *duration) {
   if (has_suffix == TRUE) {
     /* Parse seconds */
 
-    if (sscanf(str, "%u", &secs) == 1) {
-      if (secs > INT_MAX) {
+    if (sscanf(str, "%d", &secs) == 1) {
+      if (secs < 0) {
         errno = ERANGE;
         return -1;
       }
@@ -547,8 +1071,8 @@ int pr_str_get_duration(const char *str, int *duration) {
   if (has_suffix == TRUE) {
     /* Parse minutes */
 
-    if (sscanf(str, "%u", &mins) == 1) {
-      if (mins > INT_MAX) {
+    if (sscanf(str, "%d", &mins) == 1) {
+      if (mins < 0) {
         errno = ERANGE;
         return -1;
       }
@@ -571,8 +1095,8 @@ int pr_str_get_duration(const char *str, int *duration) {
   if (has_suffix == TRUE) {
     /* Parse hours */
 
-    if (sscanf(str, "%u", &hours) == 1) {
-      if (hours > INT_MAX) {
+    if (sscanf(str, "%d", &hours) == 1) {
+      if (hours < 0) {
         errno = ERANGE;
         return -1;
       }
@@ -596,8 +1120,7 @@ int pr_str_get_duration(const char *str, int *duration) {
     return -1;
   }
 
-  if (secs < 0 ||
-      secs > INT_MAX) {
+  if (secs < 0) {
     errno = ERANGE;
     return -1;
   }
@@ -697,19 +1220,22 @@ char *pr_str_get_word(char **cp, int flags) {
 
   if (!(flags & PR_STR_FL_PRESERVE_WHITESPACE)) {
     while (**cp && PR_ISSPACE(**cp)) {
+      pr_signals_handle();
       (*cp)++;
     }
   }
 
-  if (!**cp)
+  if (!**cp) {
     return NULL;
+  }
 
   res = dst = *cp;
 
   if (!(flags & PR_STR_FL_PRESERVE_COMMENTS)) {
     /* Stop processing at start of an inline comment. */
-    if (**cp == '#')
+    if (**cp == '#') {
       return NULL;
+    }
   }
 
   if (**cp == '\"') {
@@ -718,21 +1244,25 @@ char *pr_str_get_word(char **cp, int flags) {
   }
 
   while (**cp && (quote_mode ? (**cp != '\"') : !PR_ISSPACE(**cp))) {
+    pr_signals_handle();
+
     if (**cp == '\\' && quote_mode) {
 
       /* Escaped char */
-      if (*((*cp)+1))
+      if (*((*cp)+1)) {
         *dst = *(++(*cp));
+      }
     }
 
     *dst++ = **cp;
     ++(*cp);
   }
 
-  if (**cp)
+  if (**cp) {
     (*cp)++;
-  *dst = '\0';
+  }
 
+  *dst = '\0';
   return res;
 }
 
@@ -825,6 +1355,10 @@ int pr_str_is_boolean(const char *str) {
 int pr_str_is_fnmatch(const char *str) {
   int have_bracket = 0;
 
+  if (str == NULL) {
+    return FALSE;
+  }
+
   while (*str) {
     switch (*str) {
       case '?':
@@ -833,8 +1367,9 @@ int pr_str_is_fnmatch(const char *str) {
 
       case '\\':
         /* If the next character is NUL, we've reached the end of the string. */
-        if (*(str+1) == '\0')
+        if (*(str+1) == '\0') {
           return FALSE;
+        }
 
         /* Skip past the escaped character, i.e. the next character. */
         str++;
diff --git a/src/support.c b/src/support.c
index 2341dce..45d2126 100644
--- a/src/support.c
+++ b/src/support.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2015 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -43,9 +43,9 @@ typedef struct sched_obj {
   struct sched_obj *next, *prev;
 
   pool *pool;
-  void (*f)(void*,void*,void*,void*);
-  int loops;
-  void *a1,*a2,*a3,*a4;
+  void (*cb)(void *, void *, void *, void *);
+  int nloops;
+  void *arg1, *arg2, *arg3, *arg4;
 } sched_t;
 
 static xaset_t *scheds = NULL;
@@ -116,11 +116,16 @@ void pr_signals_unblock(void) {
   sigs_nblocked--;
 }
 
-void schedule(void (*f)(void*,void*,void*,void*),int nloops, void *a1,
-    void *a2, void *a3, void *a4) {
+void schedule(void (*cb)(void *, void *, void *, void *), int nloops,
+    void *arg1, void *arg2, void *arg3, void *arg4) {
   pool *p, *sub_pool;
   sched_t *s;
 
+  if (cb == NULL ||
+      nloops < 0) {
+    return;
+  }
+
   if (scheds == NULL) {
     p = make_sub_pool(permanent_pool);
     pr_pool_tag(p, "Schedules Pool");
@@ -135,12 +140,12 @@ void schedule(void (*f)(void*,void*,void*,void*),int nloops, void *a1,
 
   s = pcalloc(sub_pool, sizeof(sched_t));
   s->pool = sub_pool;
-  s->f = f;
-  s->a1 = a1;
-  s->a2 = a2;
-  s->a3 = a3;
-  s->a4 = a4;
-  s->loops = nloops;
+  s->cb = cb;
+  s->arg1 = arg1;
+  s->arg2 = arg2;
+  s->arg3 = arg3;
+  s->arg4 = arg4;
+  s->nloops = nloops;
   xaset_insert(scheds, (xasetmember_t *) s);
 }
 
@@ -155,8 +160,10 @@ void run_schedule(void) {
   for (s = (sched_t *) scheds->xas_list; s; s = snext) {
     snext = s->next;
 
-    if (s->loops-- <= 0) {
-      s->f(s->a1, s->a2, s->a3, s->a4);
+    pr_signals_handle();
+
+    if (s->nloops-- <= 0) {
+      s->cb(s->arg1, s->arg2, s->arg3, s->arg4);
       xaset_remove(scheds, (xasetmember_t *) s);
       destroy_pool(s->pool);
     }
@@ -173,72 +180,115 @@ void run_schedule(void) {
  * Alas, current (Jul 2000) Linux systems define NAME_MAX anyway.
  * NB: NAME_MAX_GUESS is defined in support.h.
  */
-size_t get_name_max(char *dirname, int dir_fd) {
-  long res = 0;
-#if defined(HAVE_FPATHCONF) || defined(HAVE_PATHCONF)
-  char *msgfmt = "";
-
-# if defined(HAVE_FPATHCONF)
-  if (dir_fd > 0) {
-    res = fpathconf(dir_fd, _PC_NAME_MAX);
-    msgfmt = "fpathconf(%s, _PC_NAME_MAX) = %ld, errno = %d";
-  } else
-# endif
-# if defined(HAVE_PATHCONF)
-  if (dirname != NULL) {
-    res = pathconf(dirname, _PC_NAME_MAX);
-    msgfmt = "pathconf(%s, _PC_NAME_MAX) = %ld, errno = %d";
-  } else
-# endif
-  /* No data provided to use either pathconf() or fpathconf() */
+
+static int get_fpathconf_name_max(int fd, long *name_max) {
+#if defined(HAVE_FPATHCONF)
+  *name_max = fpathconf(fd, _PC_NAME_MAX);
+  return 0;
+#else
+  errno = ENOSYS;
   return -1;
+#endif /* HAVE_FPATHCONF */
+}
 
-  if (res < 0) {
-    /* NB: errno may not be set if the failure is due to a limit or option
-     * not being supported.
-     */
-    pr_log_debug(DEBUG1, msgfmt, dirname ? dirname : "(NULL)", res, errno);
+static int get_pathconf_name_max(char *dir, long *name_max) {
+#if defined(HAVE_PATHCONF)
+  *name_max = pathconf(dir, _PC_NAME_MAX);
+  return 0;
+#else
+  errno = ENOSYS;
+  return -1;
+#endif /* HAVE_PATHCONF */
+}
+
+long get_name_max(char *dir_name, int dir_fd) {
+  int res;
+  long name_max = 0;
+
+  if (dir_name == NULL &&
+      dir_fd < 0) {
+    errno = EINVAL;
+    return -1;
   }
 
-#else
-  res = NAME_MAX_GUESS;
-#endif /* HAVE_FPATHCONF or HAVE_PATHCONF */
+  /* Try the fd first. */
+  if (dir_fd >= 0) {
+    res = get_fpathconf_name_max(dir_fd, &name_max);
+    if (res == 0) {
+      if (name_max < 0) {
+        int xerrno = errno;
 
-  return (size_t) res;
-}
+        pr_log_debug(DEBUG5, "fpathconf() error for fd %d: %s", dir_fd,
+          strerror(xerrno));
+
+        errno = xerrno;
+        return -1;
+      }
 
+      return name_max;
+    }
+  }
+
+  /* Name, then. */
+  if (dir_name != NULL) {
+    res = get_pathconf_name_max(dir_name, &name_max);
+    if (res == 0) {
+      if (name_max < 0) {
+        int xerrno = errno;
+
+        pr_log_debug(DEBUG5, "pathconf() error for name '%s': %s", dir_name,
+          strerror(xerrno));
+
+        errno = xerrno;
+        return -1;
+      }
+
+      return name_max;
+    }
+  }
+
+  errno = ENOSYS;
+  return -1;
+}
 
 /* Interpolates a pathname, expanding ~ notation if necessary
  */
 char *dir_interpolate(pool *p, const char *path) {
   struct passwd *pw;
-  char *user,*tmp;
-  char *ret = (char *)path;
+  char *res = NULL;
 
-  if (!ret)
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
     return NULL;
+  }
 
-  if (*ret == '~') {
-    user = pstrdup(p, ret+1);
-    tmp = strchr(user, '/');
+  if (*path == '~') {
+    char *ptr, *user;
 
-    if (tmp)
-      *tmp++ = '\0';
+    user = pstrdup(p, path + 1);
+    ptr = strchr(user, '/');
+    if (ptr != NULL) {
+      *ptr++ = '\0';
+    }
 
-    if (!*user)
-      user = session.user;
+    if (!*user) {
+      user = (char *) session.user;
+    }
 
     pw = pr_auth_getpwnam(p, user);
-
-    if (!pw) {
+    if (pw == NULL) {
       errno = ENOENT;
       return NULL;
     }
 
-    ret = pdircat(p, pw->pw_dir, tmp, NULL);
+    res = pdircat(p, pw->pw_dir, ptr, NULL);
+
+  } else {
+    res = pstrdup(p, path);
   }
 
-  return ret;
+  return res;
 }
 
 /* dir_best_path() creates the "most" fully canonicalized path possible
@@ -250,49 +300,63 @@ char *dir_best_path(pool *p, const char *path) {
   char *target = NULL, *ntarget;
   int fini = 0;
 
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   if (*path == '~') {
     if (pr_fs_interpolate(path, workpath, sizeof(workpath)-1) != 1) {
-      if (pr_fs_dircat(workpath, sizeof(workpath), pr_fs_getcwd(), path) < 0)
+      if (pr_fs_dircat(workpath, sizeof(workpath), pr_fs_getcwd(), path) < 0) {
         return NULL;
+      }
     }
 
   } else {
-    if (pr_fs_dircat(workpath, sizeof(workpath), pr_fs_getcwd(), path) < 0)
+    if (pr_fs_dircat(workpath, sizeof(workpath), pr_fs_getcwd(), path) < 0) {
       return NULL;
+    }
   }
 
   pr_fs_clean_path(pstrdup(p, workpath), workpath, sizeof(workpath)-1);
 
   while (!fini && *workpath) {
     if (pr_fs_resolve_path(workpath, realpath_buf,
-        sizeof(realpath_buf)-1, 0) != -1)
+        sizeof(realpath_buf)-1, 0) != -1) {
       break;
+    }
 
     ntarget = strrchr(workpath, '/');
     if (ntarget) {
       if (target) {
-        if (pr_fs_dircat(workpath, sizeof(workpath), workpath, target) < 0)
+        if (pr_fs_dircat(workpath, sizeof(workpath), workpath, target) < 0) {
           return NULL;
+        }
       }
 
       target = ntarget;
       *target++ = '\0';
 
-    } else
+    } else {
       fini++;
+    }
   }
 
   if (!fini && *workpath) {
     if (target) {
-      if (pr_fs_dircat(workpath, sizeof(workpath), realpath_buf, target) < 0)
+      if (pr_fs_dircat(workpath, sizeof(workpath), realpath_buf, target) < 0) {
         return NULL;
+      }
 
-    } else
+    } else {
       sstrncpy(workpath, realpath_buf, sizeof(workpath));
+    }
 
   } else {
-    if (pr_fs_dircat(workpath, sizeof(workpath), "/", target) < 0)
+    if (pr_fs_dircat(workpath, sizeof(workpath), "/", target) < 0) {
       return NULL;
+    }
   }
 
   return pstrdup(p, workpath);
@@ -302,15 +366,23 @@ char *dir_canonical_path(pool *p, const char *path) {
   char buf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
   char work[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
 
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   if (*path == '~') {
     if (pr_fs_interpolate(path, work, sizeof(work)-1) != 1) {
-      if (pr_fs_dircat(work, sizeof(work), pr_fs_getcwd(), path) < 0)
+      if (pr_fs_dircat(work, sizeof(work), pr_fs_getcwd(), path) < 0) {
         return NULL;
+      }
     }
 
   } else {
-    if (pr_fs_dircat(work, sizeof(work), pr_fs_getcwd(), path) < 0)
+    if (pr_fs_dircat(work, sizeof(work), pr_fs_getcwd(), path) < 0) {
       return NULL;
+    }
   }
 
   pr_fs_clean_path(work, buf, sizeof(buf)-1);
@@ -321,29 +393,217 @@ char *dir_canonical_vpath(pool *p, const char *path) {
   char buf[PR_TUNABLE_PATH_MAX + 1]  = {'\0'};
   char work[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
 
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
   if (*path == '~') {
     if (pr_fs_interpolate(path, work, sizeof(work)-1) != 1) {
-      if (pr_fs_dircat(work, sizeof(work), pr_fs_getvwd(), path) < 0)
+      if (pr_fs_dircat(work, sizeof(work), pr_fs_getvwd(), path) < 0) {
         return NULL;
+      }
     }
 
   } else {
-    if (pr_fs_dircat(work, sizeof(work), pr_fs_getvwd(), path) < 0)
+    if (pr_fs_dircat(work, sizeof(work), pr_fs_getvwd(), path) < 0) {
       return NULL;
+    }
   }
 
   pr_fs_clean_path(work, buf, sizeof(buf)-1);
   return pstrdup(p, buf);
 }
 
+/* Performs chroot-aware handling of symlinks. */
+int dir_readlink(pool *p, const char *path, char *buf, size_t bufsz,
+    int flags) {
+  int is_abs_dst, clean_flags, len, res = -1;
+  size_t chroot_pathlen = 0, adj_pathlen = 0;
+  char *dst_path, *adj_path;
+  pool *tmp_pool;
+
+  if (p == NULL ||
+      path == NULL ||
+      buf == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (bufsz == 0) {
+    return 0;
+  }
+
+  len = pr_fsio_readlink(path, buf, bufsz);
+  if (len < 0) {
+    return -1;
+  }
+
+  if (len == 0 ||
+      (size_t) len == bufsz) {
+    /* If we read nothing in, OR if the given buffer was completely
+     * filled WITHOUT terminating NUL, there's really nothing we can/should
+     * be doing.
+     */
+    return len;
+  }
+
+  is_abs_dst = FALSE;
+  if (*buf == '/') {
+    is_abs_dst = TRUE;
+  }
+
+  if (session.chroot_path != NULL) {
+    chroot_pathlen = strlen(session.chroot_path);
+  }
+
+  if (chroot_pathlen <= 1) {
+    char *ptr;
+
+    if (is_abs_dst == TRUE ||
+        !(flags & PR_DIR_READLINK_FL_HANDLE_REL_PATH)) {
+      return len;
+    }
+
+    /* Since we have a relative destination path, we will concat it
+     * with the source path's directory, then clean up that path.
+     */
+    ptr = strrchr(path, '/');
+    if (ptr != NULL &&
+        ptr != path) {
+      char *parent_dir;
+
+      tmp_pool = make_sub_pool(p);
+      pr_pool_tag(tmp_pool, "dir_readlink pool");
+
+      parent_dir = pstrndup(tmp_pool, path, (ptr - path));
+      dst_path = pdircat(tmp_pool, parent_dir, buf, NULL);
+
+      adj_pathlen = bufsz + 1;
+      adj_path = pcalloc(tmp_pool, adj_pathlen);
+
+      res = pr_fs_clean_path2(dst_path, adj_path, adj_pathlen-1, 0);
+      if (res == 0) {
+        pr_trace_msg("fsio", 19,
+          "cleaned symlink path '%s', yielding '%s'", dst_path, adj_path);
+        dst_path = adj_path;
+      }
+
+      pr_trace_msg("fsio", 19,
+        "adjusted relative symlink path '%s', yielding '%s'", buf, dst_path);
+
+      memset(buf, '\0', bufsz);
+      sstrncpy(buf, dst_path, bufsz);
+      len = strlen(buf);
+      destroy_pool(tmp_pool);
+    }
+
+    return len;
+  }
+
+  if (is_abs_dst == FALSE) {
+    /* If we are to ignore relative destination paths, return now. */
+    if (!(flags & PR_DIR_READLINK_FL_HANDLE_REL_PATH)) {
+      return len;
+    }
+  }
+
+  if (is_abs_dst == TRUE &&
+      (size_t) len < chroot_pathlen) {
+    /* If the destination path length is shorter than the chroot path,
+     * AND the destination path is absolute, then by definition it CANNOT
+     * point within the chroot.
+     */
+    return len;
+  }
+
+  tmp_pool = make_sub_pool(p);
+  pr_pool_tag(tmp_pool, "dir_readlink pool");
+
+  dst_path = pstrdup(tmp_pool, buf);
+  if (is_abs_dst == FALSE) {
+    char *ptr;
+
+    /* Since we have a relative destination path, we will concat it
+     * with the source path's directory, then clean up that path.
+     */
+
+    ptr = strrchr(path, '/');
+    if (ptr != NULL &&
+        ptr != path) {
+      char *parent_dir;
+
+      parent_dir = pstrndup(tmp_pool, path, (ptr - path));
+      dst_path = pdircat(tmp_pool, parent_dir, dst_path, NULL);
+
+    } else {
+      dst_path = pdircat(tmp_pool, path, dst_path, NULL);
+    }
+  }
+
+  adj_pathlen = bufsz + 1;
+  adj_path = pcalloc(tmp_pool, adj_pathlen);
+
+  clean_flags = PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH;
+  res = pr_fs_clean_path2(dst_path, adj_path, adj_pathlen-1, clean_flags);
+  if (res == 0) {
+    pr_trace_msg("fsio", 19,
+      "cleaned symlink path '%s', yielding '%s'", dst_path, adj_path);
+    dst_path = adj_path;
+
+    memset(buf, '\0', bufsz);
+    sstrncpy(buf, dst_path, bufsz);
+    len = strlen(dst_path);
+  }
+
+  if (strncmp(dst_path, session.chroot_path, chroot_pathlen) == 0 &&
+      *(dst_path + chroot_pathlen) == '/') {
+    char *ptr;
+
+    ptr = dst_path + chroot_pathlen;
+
+    if (is_abs_dst == FALSE &&
+        res == 0) {
+      /* If we originally had a relative destination path, AND we cleaned
+       * that adjusted path, then we should try to re-adjust the path
+       * back to being a relative path.  Within reason.
+       */
+      ptr = pstrcat(tmp_pool, ".", ptr, NULL);
+    }
+
+    /* Since we are making the destination path shorter, the given buffer
+     * (which was big enough for the original destination path) should
+     * always be large enough for this adjusted, shorter version.  Right?
+     */
+    pr_trace_msg("fsio", 19,
+      "adjusted symlink path '%s' for chroot '%s', yielding '%s'",
+      dst_path, session.chroot_path, ptr);
+
+    memset(buf, '\0', bufsz);
+    sstrncpy(buf, ptr, bufsz);
+    len = strlen(buf);
+  }
+
+  destroy_pool(tmp_pool);
+  return len;
+}
+
 /* dir_realpath() is needed to properly dereference symlinks (getcwd() may
  * not work if permissions cause problems somewhere up the tree).
  */
 char *dir_realpath(pool *p, const char *path) {
   char buf[PR_TUNABLE_PATH_MAX + 1] = {'\0'};
 
-  if (pr_fs_resolve_partial(path, buf, sizeof(buf)-1, 0) == -1)
+  if (p == NULL ||
+      path == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  if (pr_fs_resolve_partial(path, buf, sizeof(buf)-1, 0) < 0) {
     return NULL;
+  }
 
   return pstrdup(p, buf);
 }
@@ -355,7 +615,8 @@ char *dir_realpath(pool *p, const char *path) {
 char *dir_abs_path(pool *p, const char *path, int interpolate) {
   char *res = NULL;
 
-  if (path == NULL) {
+  if (p == NULL ||
+      path == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -409,78 +670,117 @@ char *dir_abs_path(pool *p, const char *path, int interpolate) {
  * PATH, or 0 if it doesn't exist. Catch symlink loops using LAST_INODE and
  * RCOUNT.
  */
-static mode_t _symlink(const char *path, ino_t last_inode, int rcount) {
+static mode_t _symlink(pool *p, const char *path, ino_t last_inode,
+    int rcount) {
   char buf[PR_TUNABLE_PATH_MAX + 1];
-  struct stat sbuf;
+  struct stat st;
   int i;
 
-  if (++rcount >= 32) {
+  if (++rcount >= PR_FSIO_MAX_LINK_COUNT) {
     errno = ELOOP;
     return 0;
   }
 
   memset(buf, '\0', sizeof(buf));
 
-  i = pr_fsio_readlink(path, buf, sizeof(buf) - 1);
-  if (i == -1)
-    return (mode_t)0;
+  if (p != NULL) {
+    i = dir_readlink(p, path, buf, sizeof(buf)-1,
+      PR_DIR_READLINK_FL_HANDLE_REL_PATH);
+  } else {
+    i = pr_fsio_readlink(path, buf, sizeof(buf)-1);
+  }
+
+  if (i < 0) {
+    return (mode_t) 0;
+  }
   buf[i] = '\0';
 
-  if (pr_fsio_lstat(buf, &sbuf) != -1) {
-    if (sbuf.st_ino && (ino_t) sbuf.st_ino == last_inode) {
+  pr_fs_clear_cache2(buf);
+  if (pr_fsio_lstat(buf, &st) >= 0) {
+    if (st.st_ino > 0 &&
+        (ino_t) st.st_ino == last_inode) {
       errno = ELOOP;
       return 0;
     }
 
-    if (S_ISLNK(sbuf.st_mode)) {
-      return _symlink(buf, (ino_t) sbuf.st_ino, rcount);
+    if (S_ISLNK(st.st_mode)) {
+      return _symlink(p, buf, (ino_t) st.st_ino, rcount);
     }
 
-    return sbuf.st_mode;
+    return st.st_mode;
   }
 
   return 0;
 }
 
-mode_t file_mode(const char *path) {
-  struct stat sbuf;
-  mode_t res = 0;
+mode_t symlink_mode2(pool *p, const char *path) {
+  if (path == NULL) {
+    errno = EINVAL;
+    return 0;
+  }
 
-  pr_fs_clear_cache();
-  if (pr_fsio_lstat(path, &sbuf) != -1) {
-    if (S_ISLNK(sbuf.st_mode)) {
-      res = _symlink(path, (ino_t) 0, 0);
+  return _symlink(p, path, (ino_t) 0, 0);
+}
 
-      if (res == 0) {
+mode_t symlink_mode(const char *path) {
+  return symlink_mode2(NULL, path);
+}
+
+mode_t file_mode2(pool *p, const char *path) {
+  struct stat st;
+  mode_t mode = 0;
+
+  if (path == NULL) {
+    errno = EINVAL;
+    return mode;
+  }
+
+  pr_fs_clear_cache2(path);
+  if (pr_fsio_lstat(path, &st) >= 0) {
+    if (S_ISLNK(st.st_mode)) {
+      mode = _symlink(p, path, (ino_t) 0, 0);
+      if (mode == 0) {
 	/* a dangling symlink, but it exists to rename or delete. */
-	res = sbuf.st_mode;
+	mode = st.st_mode;
       }
 
     } else {
-      res = sbuf.st_mode;
+      mode = st.st_mode;
     }
   }
 
-  return res;
+  return mode;
 }
 
-/* If DIRP == 1, fail unless PATH is an existing directory.
- * If DIRP == 0, fail unless PATH is an existing non-directory.
- * If DIRP == -1, fail unless PATH exists; the caller doesn't care whether
+mode_t file_mode(const char *path) {
+  return file_mode2(NULL, path);
+}
+
+/* If flags == 1, fail unless PATH is an existing directory.
+ * If flags == 0, fail unless PATH is an existing non-directory.
+ * If flags == -1, fail unless PATH exists; the caller doesn't care whether
  * PATH is a file or a directory.
  */
-static int _exists(const char *path, int dirp) {
-  mode_t fmode;
+static int _exists(pool *p, const char *path, int flags) {
+  mode_t mode;
 
-  fmode = file_mode(path);
-  if (fmode != 0) {
-    if (dirp == 1 &&
-        !S_ISDIR(fmode)) {
-      return FALSE;
+  mode = file_mode2(p, path);
+  if (mode != 0) {
+    switch (flags) {
+      case 1:
+        if (!S_ISDIR(mode)) {
+          return FALSE;
+        }
+        break;
+
+      case 0:
+        if (S_ISDIR(mode)) {
+          return FALSE;
+        }
+        break;
 
-    } else if (dirp == 0 &&
-               S_ISDIR(fmode)) {
-      return FALSE;
+      default:
+        break; 
     }
 
     return TRUE;
@@ -489,16 +789,28 @@ static int _exists(const char *path, int dirp) {
   return FALSE;
 }
 
+int file_exists2(pool *p, const char *path) {
+  return _exists(p, path, 0);
+}
+
 int file_exists(const char *path) {
-  return _exists(path, 0);
+  return file_exists2(NULL, path);
+}
+
+int dir_exists2(pool *p, const char *path) {
+  return _exists(p, path, 1);
 }
 
 int dir_exists(const char *path) {
-  return _exists(path, 1);
+  return dir_exists2(NULL, path);
+}
+
+int exists2(pool *p, const char *path) {
+  return _exists(p, path, -1);
 }
 
 int exists(const char *path) {
-  return _exists(path, -1);
+  return exists2(NULL, path);
 }
 
 /* safe_token tokenizes a string, and increments the pointer to
@@ -509,23 +821,29 @@ int exists(const char *path) {
 char *safe_token(char **s) {
   char *res = "";
 
-  if (!s || !*s)
+  if (s == NULL ||
+      !*s) {
     return res;
+  }
 
-  while (PR_ISSPACE(**s) && **s)
+  while (PR_ISSPACE(**s) && **s) {
     (*s)++;
+  }
 
   if (**s) {
     res = *s;
 
-    while (!PR_ISSPACE(**s) && **s)
+    while (!PR_ISSPACE(**s) && **s) {
       (*s)++;
+    }
 
-    if (**s)
+    if (**s) {
       *(*s)++ = '\0';
+    }
 
-    while (PR_ISSPACE(**s) && **s)
+    while (PR_ISSPACE(**s) && **s) {
       (*s)++;
+    }
   }
 
   return res;
@@ -535,58 +853,84 @@ char *safe_token(char **s) {
  * filled with the times to deny new connections and disconnect
  * existing ones.
  */
-int check_shutmsg(time_t *shut, time_t *deny, time_t *disc, char *msg,
-                  size_t msg_size) {
+int check_shutmsg(const char *path, time_t *shut, time_t *deny, time_t *disc,
+    char *msg, size_t msg_size) {
   FILE *fp;
-  char *deny_str,*disc_str,*cp, buf[PR_TUNABLE_BUFFER_SIZE+1] = {'\0'};
+  char *deny_str, *disc_str, *cp, buf[PR_TUNABLE_BUFFER_SIZE+1] = {'\0'};
   char hr[3] = {'\0'}, mn[3] = {'\0'};
-  time_t now,shuttime = (time_t)0;
-  struct tm tm;
+  time_t now, shuttime = (time_t) 0;
+  struct tm *tm;
 
-  if (file_exists(PR_SHUTMSG_PATH) && (fp = fopen(PR_SHUTMSG_PATH, "r"))) {
-    if ((cp = fgets(buf, sizeof(buf),fp)) != NULL) {
+  if (path == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  fp = fopen(path, "r");
+  if (fp != NULL) {
+    struct stat st;
+
+    if (fstat(fileno(fp), &st) == 0) {
+      if (S_ISDIR(st.st_mode)) {
+        fclose(fp);
+        errno = EISDIR;
+        return -1;
+      }
+    }
+
+    cp = fgets(buf, sizeof(buf), fp);
+    if (cp != NULL) {
       buf[sizeof(buf)-1] = '\0'; CHOP(cp);
 
       /* We use this to fill in dst, timezone, etc */
       time(&now);
-      tm = *(localtime(&now));
+      tm = pr_localtime(NULL, &now);
+      if (tm == NULL) {
+        fclose(fp);
+        return 0;
+      }
 
-      tm.tm_year = atoi(safe_token(&cp)) - 1900;
-      tm.tm_mon = atoi(safe_token(&cp)) - 1;
-      tm.tm_mday = atoi(safe_token(&cp));
-      tm.tm_hour = atoi(safe_token(&cp));
-      tm.tm_min = atoi(safe_token(&cp));
-      tm.tm_sec = atoi(safe_token(&cp));
+      tm->tm_year = atoi(safe_token(&cp)) - 1900;
+      tm->tm_mon = atoi(safe_token(&cp)) - 1;
+      tm->tm_mday = atoi(safe_token(&cp));
+      tm->tm_hour = atoi(safe_token(&cp));
+      tm->tm_min = atoi(safe_token(&cp));
+      tm->tm_sec = atoi(safe_token(&cp));
 
       deny_str = safe_token(&cp);
       disc_str = safe_token(&cp);
 
-      if ((shuttime = mktime(&tm)) == (time_t) - 1) {
+      shuttime = mktime(tm);
+      if (shuttime == (time_t) -1) {
         fclose(fp);
         return 0;
       }
 
-      if (deny) {
+      if (deny != NULL) {
         if (strlen(deny_str) == 4) {
-          sstrncpy(hr,deny_str,sizeof(hr)); hr[2] = '\0'; deny_str += 2;
-          sstrncpy(mn,deny_str,sizeof(mn)); mn[2] = '\0';
+          sstrncpy(hr, deny_str, sizeof(hr)); hr[2] = '\0'; deny_str += 2;
+          sstrncpy(mn, deny_str, sizeof(mn)); mn[2] = '\0';
 
           *deny = shuttime - ((atoi(hr) * 3600) + (atoi(mn) * 60));
-        } else
+
+        } else {
           *deny = shuttime;
+        }
       }
 
-      if (disc) {
+      if (disc != NULL) {
         if (strlen(disc_str) == 4) {
-          sstrncpy(hr,disc_str,sizeof(hr)); hr[2] = '\0'; disc_str += 2;
-          sstrncpy(mn,disc_str,sizeof(mn)); mn[2] = '\0';
+          sstrncpy(hr, disc_str, sizeof(hr)); hr[2] = '\0'; disc_str += 2;
+          sstrncpy(mn, disc_str, sizeof(mn)); mn[2] = '\0';
 
           *disc = shuttime - ((atoi(hr) * 3600) + (atoi(mn) * 60));
-        } else
+
+        } else {
           *disc = shuttime;
+        }
       }
 
-      if (fgets(buf, sizeof(buf),fp) && msg) {
+      if (fgets(buf, sizeof(buf), fp) && msg) {
         buf[sizeof(buf)-1] = '\0';
 	CHOP(buf);
         sstrncpy(msg, buf, msg_size-1);
@@ -594,12 +938,14 @@ int check_shutmsg(time_t *shut, time_t *deny, time_t *disc, char *msg,
     }
 
     fclose(fp);
-    if (shut)
+    if (shut != NULL) {
       *shut = shuttime;
+    }
+
     return 1;
   }
 
-  return 0;
+  return -1;
 }
 
 #if !defined(PR_USE_OPENSSL) || OPENSSL_VERSION_NUMBER <= 0x000907000L
@@ -642,8 +988,9 @@ void pr_memscrub(void *ptr, size_t ptrlen) {
     memscrub_ctr += (17 + (unsigned char)((intptr_t) p & 0xF));
   }
 
-  if (memchr(ptr, memscrub_ctr, ptrlen))
+  if (memchr(ptr, memscrub_ctr, ptrlen)) {
     memscrub_ctr += 63;
+  }
 #endif
 }
 
@@ -652,7 +999,8 @@ void pr_getopt_reset(void) {
     defined(FREEBSD7) || defined(FREEBSD8) || defined(FREEBSD9) || \
     defined(FREEBSD10) || \
     defined(DARWIN7) || defined(DARWIN8) || defined(DARWIN9) || \
-    defined(DARWIN10) || defined(DARWIN11) || defined(DARWIN12)
+    defined(DARWIN10) || defined(DARWIN11) || defined(DARWIN12) || \
+    defined(DARWIN13) || defined(DARWIN14)
   optreset = 1;
   opterr = 1;
   optind = 1;
@@ -671,10 +1019,18 @@ void pr_getopt_reset(void) {
   }
 }
 
-struct tm *pr_gmtime(pool *p, const time_t *t) {
+struct tm *pr_gmtime(pool *p, const time_t *now) {
   struct tm *sys_tm, *dup_tm;
 
-  sys_tm = gmtime(t);
+  if (now == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  sys_tm = gmtime(now);
+  if (sys_tm == NULL) {
+    return NULL;
+  }
 
   /* If the caller provided a pool, make a copy of the struct tm using that
    * pool.  Otherwise, return the struct tm as is.
@@ -690,7 +1046,7 @@ struct tm *pr_gmtime(pool *p, const time_t *t) {
   return dup_tm;
 }
 
-struct tm *pr_localtime(pool *p, const time_t *t) {
+struct tm *pr_localtime(pool *p, const time_t *now) {
   struct tm *sys_tm, *dup_tm;
 
 #ifdef HAVE_TZNAME
@@ -724,7 +1080,15 @@ struct tm *pr_localtime(pool *p, const time_t *t) {
   memcpy(&tzname_dup, tzname, sizeof(tzname_dup));
 #endif /* HAVE_TZNAME */
 
-  sys_tm = localtime(t);
+  if (now == NULL) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  sys_tm = localtime(now);
+  if (sys_tm == NULL) {
+    return NULL;
+  }
 
   if (p) {
     /* If the caller provided a pool, make a copy of the returned
@@ -773,13 +1137,170 @@ const char *pr_strtime2(time_t t, int use_gmtime) {
     snprintf(buf, sizeof(buf), "%s %s %02d %02d:%02d:%02d %d",
       days[tr->tm_wday], mons[tr->tm_mon], tr->tm_mday, tr->tm_hour,
       tr->tm_min, tr->tm_sec, tr->tm_year + 1900);
-
-  } else {
-    buf[0] = '\0';
   }
 
   buf[sizeof(buf)-1] = '\0';
-
   return buf;
 }
 
+int pr_timeval2millis(struct timeval *tv, uint64_t *millis) {
+  if (tv == NULL ||
+      millis == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* Make sure to use 64-bit multiplication to avoid overflow errors,
+   * as much as we can.
+   */
+  *millis = (tv->tv_sec * (uint64_t) 1000) + (tv->tv_usec / (uint64_t) 1000);
+  return 0;
+}
+
+int pr_gettimeofday_millis(uint64_t *millis) {
+  struct timeval tv;
+
+  if (gettimeofday(&tv, NULL) < 0) {
+    return -1;
+  }
+
+  if (pr_timeval2millis(&tv, millis) < 0) {
+    return -1;
+  }
+
+  return 0;
+}
+
+/* Substitute any appearance of the %u variable in the given string with
+ * the value.
+ */
+const char *path_subst_uservar(pool *path_pool, const char **path) {
+  const char *new_path = NULL, *substr_path = NULL;
+  char *substr = NULL;
+  size_t user_len = 0;
+
+  /* Sanity check. */
+  if (path_pool == NULL ||
+      path == NULL ||
+      !*path) {
+    errno = EINVAL;
+    return NULL;
+  }
+
+  /* If no %u string present, do nothing. */
+  if (strstr(*path, "%u") == NULL) {
+    return *path;
+  }
+
+  /* Same if there is no user set yet. */
+  if (session.user == NULL) {
+    return *path;
+  }
+
+  user_len = strlen(session.user);
+
+  /* First, deal with occurrences of "%u[index]" strings.  Note that
+   * with this syntax, the '[' and ']' characters become invalid in paths,
+   * but only if that '[' appears after a "%u" string -- certainly not
+   * a common phenomenon (I hope).  This means that in the future, an escape
+   * mechanism may be needed in this function.  Caveat emptor.
+   */
+
+  substr_path = *path;
+  substr = substr_path ? strstr(substr_path, "%u[") : NULL;
+  while (substr != NULL) {
+    long i = 0;
+    char *substr_end = NULL, *substr_dup = NULL, *endp = NULL;
+    char ref_char[2] = {'\0', '\0'};
+
+    pr_signals_handle();
+
+    /* Now, find the closing ']'. If not found, it is a syntax error;
+     * continue on without processing this occurrence.
+     */
+    substr_end = strchr(substr, ']');
+    if (substr_end == NULL) {
+      /* Just end here. */
+      break;
+    }
+
+    /* Make a copy of the entire substring. */
+    substr_dup = pstrdup(path_pool, substr);
+
+    /* The substr_end variable (used as an index) should work here, too
+     * (trying to obtain the entire substring).
+     */
+    substr_dup[substr_end - substr + 1] = '\0';
+
+    /* Advance the substring pointer by three characters, so that it is
+     * pointing at the character after the '['.
+     */
+    substr += 3;
+
+    /* If the closing ']' is the next character after the opening '[', it
+     * is a syntax error.
+     */
+    if (*substr == ']') {
+      substr_path = *path;
+      break;
+    }
+
+    /* Temporarily set the ']' to '\0', to make it easy for the string
+     * scanning below.
+     */
+    *substr_end = '\0';
+
+    /* Scan the index string into a number, watching for bad strings. */
+    i = strtol(substr, &endp, 10);
+    if (endp && *endp) {
+      *substr_end = ']';
+      pr_trace_msg("auth", 3,
+        "invalid index number syntax found in '%s', ignoring", substr);
+      return *path;
+    }
+
+    /* Make sure that index is within bounds. */
+    if (i < 0 ||
+        (size_t) i > user_len - 1) {
+
+      /* Put the closing ']' back. */
+      *substr_end = ']';
+
+      if (i < 0) {
+        pr_trace_msg("auth", 3,
+          "out-of-bounds index number (%ld) found in '%s', ignoring", i,
+          substr);
+
+      } else {
+        pr_trace_msg("auth", 3,
+          "out-of-bounds index number (%ld > %lu) found in '%s', ignoring", i,
+          (unsigned long) user_len-1, substr);
+      }
+
+      return *path;
+    }
+
+    ref_char[0] = session.user[i];
+
+    /* Put the closing ']' back. */
+    *substr_end = ']';
+
+    /* Now, to substitute the whole "%u[index]" substring with the
+     * referenced character/string.
+     */
+    substr_path = sreplace(path_pool, substr_path, substr_dup, ref_char, NULL);
+    substr = substr_path ? strstr(substr_path, "%u[") : NULL;
+  }
+
+  /* Check for any bare "%u", and handle those if present. */
+  if (substr_path &&
+      strstr(substr_path, "%u") != NULL) {
+    new_path = sreplace(path_pool, substr_path, "%u", session.user, NULL);
+
+  } else {
+    new_path = substr_path;
+  }
+
+  return new_path;
+}
+
diff --git a/src/table.c b/src/table.c
index 0695e86..92d6aa7 100644
--- a/src/table.c
+++ b/src/table.c
@@ -162,8 +162,9 @@ static unsigned int key_hash(const void *key, size_t keysz) {
 static void entry_insert(pr_table_entry_t **h, pr_table_entry_t *e) {
   pr_table_entry_t *ei;
 
-  if (*h == NULL)
+  if (*h == NULL) {
     return;
+  }
 
   for (ei = *h; ei != NULL && ei->next; ei = ei->next);
 
@@ -181,16 +182,10 @@ static void entry_remove(pr_table_entry_t **h, pr_table_entry_t *e) {
 
   if (e->prev) {
     e->prev->next = e->next;
-  }
-
-  if (e == *h) {
-    if (e->next == NULL) {
-      /* This entry is the head, and is the only entry in this chain. */
-      *h = NULL;
 
-    } else {
-      *h = e->next;
-    }
+  } else {
+    /* This entry is the head. */
+    *h = e->next;
   }
 
   e->prev = e->next = NULL;
@@ -237,8 +232,9 @@ static void tab_key_free(pr_table_t *tab, pr_table_key_t *k) {
 
     i->next = k;
 
-  } else
+  } else {
     tab->free_keys = k;
+  }
 }
 
 /* Table entry management
@@ -281,8 +277,9 @@ static void tab_entry_free(pr_table_t *tab, pr_table_entry_t *e) {
 
     i->next = e;
  
-  } else
+  } else {
     tab->free_ents = e;
+  }
 }
 
 static void tab_entry_insert(pr_table_t *tab, pr_table_entry_t *e) {
@@ -346,11 +343,10 @@ static void tab_entry_remove(pr_table_t *tab, pr_table_entry_t *e) {
   pr_table_entry_t *h;
 
   h = tab->chains[e->idx];
-
   tab->entremove(&h, e);
   tab->chains[e->idx] = h;
-  e->key->nents--;
 
+  e->key->nents--;
   if (e->key->nents == 0) {
     tab_key_free(tab, e->key);
     e->key = NULL;
@@ -394,7 +390,7 @@ static unsigned int tab_get_seed(void) {
  */
 
 int pr_table_kadd(pr_table_t *tab, const void *key_data, size_t key_datasz,
-    void *value_data, size_t value_datasz) {
+    const void *value_data, size_t value_datasz) {
   unsigned int h, idx;
   pr_table_entry_t *e, *n;
 
@@ -427,8 +423,7 @@ int pr_table_kadd(pr_table_t *tab, const void *key_data, size_t key_datasz,
 
   /* Find the current chain entry at this index. */
   e = tab->chains[idx];
-
-  if (e) {
+  if (e != NULL) {
     pr_table_entry_t *ei;
 
     /* There is a chain at this index.  Next step is to see if any entry
@@ -437,8 +432,9 @@ int pr_table_kadd(pr_table_t *tab, const void *key_data, size_t key_datasz,
      */
 
     for (ei = e; ei; ei = ei->next) {
-      if (ei->key->hash != h)
+      if (ei->key->hash != h) {
         continue;
+      }
 
       /* Hash collision.  Now check if the key data that was hashed
        * is identical.  If so, we have multiple values for the same key.
@@ -451,9 +447,9 @@ int pr_table_kadd(pr_table_t *tab, const void *key_data, size_t key_datasz,
         if (!(tab->flags & PR_TABLE_FL_MULTI_VALUE)) {
           errno = EEXIST;
           return -1;
+        }
 
-        } else
-          n->key = ei->key;
+        n->key = ei->key;
       }
     }
 
@@ -485,7 +481,8 @@ int pr_table_kexists(pr_table_t *tab, const void *key_data, size_t key_datasz) {
   unsigned int h, idx;
   pr_table_entry_t *head, *ent;
 
-  if (!tab || !key_data) {
+  if (tab == NULL ||
+      key_data == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -509,25 +506,25 @@ int pr_table_kexists(pr_table_t *tab, const void *key_data, size_t key_datasz) {
   h = tab->keyhash(key_data, key_datasz) + tab->seed;
 
   idx = h % tab->nchains;
-
   head = tab->chains[idx];
-
-  if (!head) {
+  if (head == NULL) {
     tab->cache_ent = NULL;
     return 0;
   }
 
   for (ent = head; ent; ent = ent->next) {
     if (ent->key == NULL ||
-        ent->key->hash != h)
+        ent->key->hash != h) {
       continue;
+    }
 
     /* Matching hashes.  Now to see if the keys themselves match. */
     if (tab->keycmp(ent->key->key_data, ent->key->key_datasz,
         key_data, key_datasz) == 0) {
 
-      if (tab->flags & PR_TABLE_FL_USE_CACHE)
+      if (tab->flags & PR_TABLE_FL_USE_CACHE) {
         tab->cache_ent = ent;
+      }
 
       return ent->key->nents;
     }
@@ -539,18 +536,18 @@ int pr_table_kexists(pr_table_t *tab, const void *key_data, size_t key_datasz) {
   return 0;
 }
 
-void *pr_table_kget(pr_table_t *tab, const void *key_data, size_t key_datasz,
-    size_t *value_datasz) {
+const void *pr_table_kget(pr_table_t *tab, const void *key_data,
+    size_t key_datasz, size_t *value_datasz) {
   unsigned int h;
   pr_table_entry_t *head, *ent;
 
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
   /* Use a NULL key as a way of rewinding the per-key lookup. */
-  if (!key_data) {
+  if (key_data == NULL) {
     tab->cache_ent = NULL;
     tab->val_iter_ent = NULL;
 
@@ -589,7 +586,7 @@ void *pr_table_kget(pr_table_t *tab, const void *key_data, size_t key_datasz,
     head = tab->chains[idx];
   }
 
-  if (!head) {
+  if (head == NULL) {
     tab->cache_ent = NULL;
     tab->val_iter_ent = NULL;
 
@@ -599,21 +596,25 @@ void *pr_table_kget(pr_table_t *tab, const void *key_data, size_t key_datasz,
 
   for (ent = head; ent; ent = ent->next) {
     if (ent->key == NULL ||
-        ent->key->hash != h)
+        ent->key->hash != h) {
       continue;
+    }
 
     /* Matching hashes.  Now to see if the keys themselves match. */
     if (tab->keycmp(ent->key->key_data, ent->key->key_datasz,
         key_data, key_datasz) == 0) {
 
-      if (tab->flags & PR_TABLE_FL_USE_CACHE) 
+      if (tab->flags & PR_TABLE_FL_USE_CACHE) {
         tab->cache_ent = ent;
+      }
 
-      if (tab->flags & PR_TABLE_FL_MULTI_VALUE)
+      if (tab->flags & PR_TABLE_FL_MULTI_VALUE) {
         tab->val_iter_ent = ent;
+      }
 
-      if (value_datasz)
+      if (value_datasz) {
         *value_datasz = ent->value_datasz;
+      }
 
       return ent->value_data;
     }
@@ -626,12 +627,13 @@ void *pr_table_kget(pr_table_t *tab, const void *key_data, size_t key_datasz,
   return NULL;
 }
 
-void *pr_table_kremove(pr_table_t *tab, const void *key_data,
+const void *pr_table_kremove(pr_table_t *tab, const void *key_data,
     size_t key_datasz, size_t *value_datasz) {
   unsigned int h, idx;
   pr_table_entry_t *head, *ent;
 
-  if (!tab || !key_data) {
+  if (tab == NULL ||
+      key_data == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -648,10 +650,12 @@ void *pr_table_kremove(pr_table_t *tab, const void *key_data,
   if ((tab->flags & PR_TABLE_FL_USE_CACHE) &&
       tab->cache_ent &&
       tab->cache_ent->key->key_data == key_data) {
-    void *value_data = tab->cache_ent->value_data;
+    const void *value_data;
 
-    if (value_datasz)
+    value_data = tab->cache_ent->value_data;
+    if (value_datasz) {
       *value_datasz = tab->cache_ent->value_datasz;
+    }
 
     tab_entry_remove(tab, tab->cache_ent);
     tab_entry_free(tab, tab->cache_ent);
@@ -664,10 +668,8 @@ void *pr_table_kremove(pr_table_t *tab, const void *key_data,
   h = tab->keyhash(key_data, key_datasz) + tab->seed;
 
   idx = h % tab->nchains;
-
   head = tab->chains[idx];
-
-  if (!head) {
+  if (head == NULL) {
     tab->cache_ent = NULL;
 
     errno = ENOENT;
@@ -676,16 +678,19 @@ void *pr_table_kremove(pr_table_t *tab, const void *key_data,
 
   for (ent = head; ent; ent = ent->next) {
     if (ent->key == NULL ||
-        ent->key->hash != h)
+        ent->key->hash != h) {
       continue;
+    }
 
     /* Matching hashes.  Now to see if the keys themselves match. */
     if (tab->keycmp(ent->key->key_data, ent->key->key_datasz,
         key_data, key_datasz) == 0) {
-      void *value_data = ent->value_data;
+      const void *value_data;
 
-      if (value_datasz)
+      value_data = ent->value_data;
+      if (value_datasz) {
         *value_datasz = ent->value_datasz;
+      }
 
       tab_entry_remove(tab, ent);
       tab_entry_free(tab, ent);
@@ -702,7 +707,7 @@ void *pr_table_kremove(pr_table_t *tab, const void *key_data,
 }
 
 int pr_table_kset(pr_table_t *tab, const void *key_data, size_t key_datasz,
-    void *value_data, size_t value_datasz) {
+    const void *value_data, size_t value_datasz) {
   unsigned int h;
   pr_table_entry_t *head, *ent;
 
@@ -740,11 +745,10 @@ int pr_table_kset(pr_table_t *tab, const void *key_data, size_t key_datasz,
 
   } else {
     unsigned int idx = h % tab->nchains;
-
     head = tab->chains[idx];
   }
 
-  if (!head) {
+  if (head == NULL) {
     tab->cache_ent = NULL;
     tab->val_iter_ent = NULL;
 
@@ -754,8 +758,9 @@ int pr_table_kset(pr_table_t *tab, const void *key_data, size_t key_datasz,
 
   for (ent = head; ent; ent = ent->next) {
     if (ent->key == NULL ||
-        ent->key->hash != h)
+        ent->key->hash != h) {
       continue;
+    }
 
     /* Matching hashes.  Now to see if the keys themselves match. */
     if (tab->keycmp(ent->key->key_data, ent->key->key_datasz,
@@ -769,11 +774,13 @@ int pr_table_kset(pr_table_t *tab, const void *key_data, size_t key_datasz,
       ent->value_data = value_data;
       ent->value_datasz = value_datasz;
 
-      if (tab->flags & PR_TABLE_FL_USE_CACHE)
+      if (tab->flags & PR_TABLE_FL_USE_CACHE) {
         tab->cache_ent = ent;
+      }
 
-      if (tab->flags & PR_TABLE_FL_MULTI_VALUE)
+      if (tab->flags & PR_TABLE_FL_MULTI_VALUE) {
         tab->val_iter_ent = ent;
+      }
 
       return 0;
     }
@@ -786,7 +793,7 @@ int pr_table_kset(pr_table_t *tab, const void *key_data, size_t key_datasz,
   return -1;
 }
 
-int pr_table_add(pr_table_t *tab, const char *key_data, void *value_data,
+int pr_table_add(pr_table_t *tab, const char *key_data, const void *value_data,
     size_t value_datasz) {
 
   if (tab == NULL ||
@@ -796,29 +803,34 @@ int pr_table_add(pr_table_t *tab, const char *key_data, void *value_data,
   }
 
   if (value_data &&
-      value_datasz == 0)
+      value_datasz == 0) {
     value_datasz = strlen((char *) value_data) + 1;
+  }
 
   return pr_table_kadd(tab, key_data, strlen(key_data) + 1, value_data,
     value_datasz);
 }
 
-int pr_table_add_dup(pr_table_t *tab, const char *key_data, void *value_data,
-    size_t value_datasz) {
+int pr_table_add_dup(pr_table_t *tab, const char *key_data,
+    const void *value_data, size_t value_datasz) {
   void *dup_data;
 
-  if (!tab || !key_data) {
+  if (tab == NULL ||
+      key_data == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  if (!value_data && value_datasz != 0) {
+  if (value_data == NULL &&
+      value_datasz != 0) {
     errno = EINVAL;
     return -1;
   }
 
-  if (value_data && value_datasz == 0)
-    value_datasz = strlen((char *) value_data) + 1;
+  if (value_data != NULL &&
+      value_datasz == 0) {
+    value_datasz = strlen((const char *) value_data) + 1;
+  }
 
   dup_data = pcalloc(tab->pool, value_datasz);
   memcpy(dup_data, value_data, value_datasz);
@@ -861,7 +873,7 @@ pr_table_t *pr_table_alloc(pool *p, int flags) {
 }
 
 int pr_table_count(pr_table_t *tab) {
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -870,24 +882,30 @@ int pr_table_count(pr_table_t *tab) {
 }
 
 int pr_table_do(pr_table_t *tab, int (*cb)(const void *key_data,
-    size_t key_datasz, void *value_data, size_t value_datasz, void *user_data),
-    void *user_data, int flags) {
+    size_t key_datasz, const void *value_data, size_t value_datasz,
+    void *user_data), void *user_data, int flags) {
   register unsigned int i;
 
-  if (!tab || !cb) {
+  if (tab == NULL ||
+      cb == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  if (tab->nents == 0)
+  if (tab->nents == 0) {
     return 0;
+  }
 
   for (i = 0; i < tab->nchains; i++) {
-    pr_table_entry_t *ent = tab->chains[i];
+    pr_table_entry_t *ent;
 
-    while (ent) {
+    ent = tab->chains[i];
+    while (ent != NULL) {
+      pr_table_entry_t *next_ent;
       int res;
 
+      next_ent = ent->next;
+
       if (!handling_signal) { 
         pr_signals_handle();
       }
@@ -900,7 +918,7 @@ int pr_table_do(pr_table_t *tab, int (*cb)(const void *key_data,
         return -1;
       }
 
-      ent = ent->next;
+      ent = next_ent;
     }
   }
 
@@ -910,18 +928,20 @@ int pr_table_do(pr_table_t *tab, int (*cb)(const void *key_data,
 int pr_table_empty(pr_table_t *tab) {
   register unsigned int i;
 
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return -1;
   }
 
-  if (tab->nents == 0)
+  if (tab->nents == 0) {
     return 0;
+  }
 
   for (i = 0; i < tab->nchains; i++) {
-    pr_table_entry_t *e = tab->chains[i];
+    pr_table_entry_t *e;
 
-    while (e) {
+    e = tab->chains[i];
+    while (e != NULL) {
       if (!handling_signal) {
         pr_signals_handle();
       }
@@ -939,7 +959,8 @@ int pr_table_empty(pr_table_t *tab) {
 }
 
 int pr_table_exists(pr_table_t *tab, const char *key_data) {
-  if (!tab || !key_data) {
+  if (tab == NULL ||
+      key_data == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -949,7 +970,7 @@ int pr_table_exists(pr_table_t *tab, const char *key_data) {
 
 int pr_table_free(pr_table_t *tab) {
 
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -963,25 +984,26 @@ int pr_table_free(pr_table_t *tab) {
   return 0;
 }
 
-void *pr_table_get(pr_table_t *tab, const char *key_data,
+const void *pr_table_get(pr_table_t *tab, const char *key_data,
     size_t *value_datasz) {
   size_t key_datasz = 0;
 
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return NULL;
   }
 
-  if (key_data)
+  if (key_data) {
     key_datasz = strlen(key_data) + 1;  
+  }
 
   return pr_table_kget(tab, key_data, key_datasz, value_datasz);
 }
 
-void *pr_table_next(pr_table_t *tab) {
+const void *pr_table_knext(pr_table_t *tab, size_t *key_datasz) {
   pr_table_entry_t *ent, *prev;
 
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -989,7 +1011,7 @@ void *pr_table_next(pr_table_t *tab) {
   prev = tab->tab_iter_ent;
 
   ent = tab_entry_next(tab);
-  while (ent) {
+  while (ent != NULL) {
     if (!handling_signal) {
       pr_signals_handle();
     }
@@ -1003,18 +1025,27 @@ void *pr_table_next(pr_table_t *tab) {
     break;
   }
 
-  if (!ent) {
+  if (ent == NULL) {
     errno = EPERM;
     return NULL;
   }
 
+  if (key_datasz != NULL) {
+    *key_datasz = ent->key->key_datasz;
+  }
+
   return ent->key->key_data;
 }
 
-void *pr_table_remove(pr_table_t *tab, const char *key_data,
+const void *pr_table_next(pr_table_t *tab) {
+  return pr_table_knext(tab, NULL);
+}
+
+const void *pr_table_remove(pr_table_t *tab, const char *key_data,
     size_t *value_datasz) {
 
-  if (!tab || !key_data) {
+  if (tab == NULL ||
+      key_data == NULL) {
     errno = EINVAL;
     return NULL;
   }
@@ -1023,7 +1054,7 @@ void *pr_table_remove(pr_table_t *tab, const char *key_data,
 }
 
 int pr_table_rewind(pr_table_t *tab) {
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -1032,17 +1063,19 @@ int pr_table_rewind(pr_table_t *tab) {
   return 0;
 }
 
-int pr_table_set(pr_table_t *tab, const char *key_data, void *value_data,
+int pr_table_set(pr_table_t *tab, const char *key_data, const void *value_data,
     size_t value_datasz) {
 
-  if (!tab || !key_data) {
+  if (tab == NULL ||
+      key_data == NULL) {
     errno = EINVAL;
     return -1;
   }
 
   if (value_data &&
-      value_datasz == 0)
-    value_datasz = strlen((char *) value_data) + 1;
+      value_datasz == 0) {
+    value_datasz = strlen((const char *) value_data) + 1;
+  }
 
   return pr_table_kset(tab, key_data, strlen(key_data) + 1, value_data,
     value_datasz);
@@ -1050,7 +1083,7 @@ int pr_table_set(pr_table_t *tab, const char *key_data, void *value_data,
 
 int pr_table_ctl(pr_table_t *tab, int cmd, void *arg) {
 
-  if (!tab) {
+  if (tab == NULL) {
     errno = EINVAL;
     return -1;
   }
@@ -1069,7 +1102,7 @@ int pr_table_ctl(pr_table_t *tab, int cmd, void *arg) {
       return 0;
 
     case PR_TABLE_CTL_SET_FLAGS:
-      if (!arg) {
+      if (arg == NULL) {
         errno = EINVAL;
         return -1;
       }
@@ -1150,7 +1183,7 @@ int pr_table_ctl(pr_table_t *tab, int cmd, void *arg) {
 }
 
 void *pr_table_pcalloc(pr_table_t *tab, size_t sz) {
-  if (!tab ||
+  if (tab == NULL ||
       sz == 0) {
     errno = EINVAL;
     return NULL;
@@ -1204,21 +1237,22 @@ void pr_table_dump(void (*dumpf)(const char *fmt, ...), pr_table_t *tab) {
       dumpf("%s", "[table flags]: MultiValue, UseCache");
 
     } else {
-      if (tab->flags & PR_TABLE_FL_MULTI_VALUE)
+      if (tab->flags & PR_TABLE_FL_MULTI_VALUE) {
         dumpf("%s", "[table flags]: MultiValue");
+      }
 
-      if (tab->flags & PR_TABLE_FL_USE_CACHE)
+      if (tab->flags & PR_TABLE_FL_USE_CACHE) {
         dumpf("%s", "[table flags]: UseCache");
+      }
     }
   }
 
   if (tab->nents == 0) {
     dumpf("[empty table]");
     return;
+  }
 
-  } else
-    dumpf("[table count]: %u", tab->nents);
-
+  dumpf("[table count]: %u", tab->nents);
   for (i = 0; i < tab->nchains; i++) {
     register unsigned int j = 0;
     pr_table_entry_t *ent = tab->chains[i];
diff --git a/src/throttle.c b/src/throttle.c
index e72deaa..e458976 100644
--- a/src/throttle.c
+++ b/src/throttle.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* TransferRate throttling
- * $Id: throttle.c,v 1.12 2012-12-28 02:56:32 castaglia Exp $
- */
+/* TransferRate throttling */
 
 #include "conf.h"
 
diff --git a/src/timers.c b/src/timers.c
index 331f17c..2ec81d5 100644
--- a/src/timers.c
+++ b/src/timers.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,14 +23,12 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Timer system, based on alarm() and SIGALRM
- * $Id: timers.c,v 1.39 2013-10-09 06:36:20 castaglia Exp $
- */
+/* Timer system, based on alarm() and SIGALRM. */
 
 #include "conf.h"
 
 /* From src/main.c */
-volatile extern unsigned int recvd_signal_flags;
+extern volatile unsigned int recvd_signal_flags;
 
 struct timer {
   struct timer *next, *prev;
@@ -62,12 +60,16 @@ static time_t _alarmed_time = 0;
 
 static pool *timer_pool = NULL;
 
+static const char *trace_channel = "timer";
+
 static int timer_cmp(struct timer *t1, struct timer *t2) {
-  if (t1->count < t2->count)
+  if (t1->count < t2->count) {
     return -1;
+  }
 
-  if (t1->count > t2->count)
+  if (t1->count > t2->count) {
     return 1;
+  }
 
   return 0;
 }
@@ -79,22 +81,38 @@ static int timer_cmp(struct timer *t1, struct timer *t2) {
  */
 static int process_timers(int elapsed) {
   struct timer *t = NULL, *next = NULL;
+  int res = 0;
 
-  if (!recycled)
+  if (recycled == NULL) {
     recycled = xaset_create(timer_pool, NULL);
+  }
 
-  if (!elapsed &&
-      !recycled->xas_list) {
-
-    if (!timers)
+  if (elapsed == 0 &&
+      recycled->xas_list == NULL) {
+    if (timers == NULL) {
       return 0;
+    }
 
-    return (timers->xas_list ? ((struct timer *) timers->xas_list)->count : 0);
+    if (timers->xas_list != NULL) {
+      /* The value we return is a proposed timeout, for the next call to
+       * alarm(3).  We start with the simple count of timers in our list.
+       *
+       * But then we reduce the number; some of the timers' intervals may
+       * less than the number of total timers.
+       */
+      res = ((struct timer *) timers->xas_list)->count;
+      if (res > 5) {
+        res = 5;
+      }
+    }
+
+    return res;
   }
 
   /* Critical code, no interruptions please */
-  if (_indispatch)
+  if (_indispatch) {
     return 0;
+  }
 
   pr_alarms_block();
   _indispatch++;
@@ -112,7 +130,7 @@ static int process_timers(int elapsed) {
       } else if ((t->count -= elapsed) <= 0) {
         /* This timer's interval has elapsed, so trigger its callback. */
 
-        pr_trace_msg("timer", 4,
+        pr_trace_msg(trace_channel, 4,
           "%ld %s for timer ID %d ('%s', for module '%s') elapsed, invoking "
           "callback (%p)", t->interval,
           t->interval != 1 ? "seconds" : "second", t->timerno,
@@ -132,8 +150,9 @@ static int process_timers(int elapsed) {
           /* A non-zero return value from a timer callback signals that
            * the timer should be reused/restarted.
            */
-          pr_trace_msg("timer", 6, "restarting timer ID %d ('%s'), as per "
-            "callback", t->timerno, t->desc ? t->desc : "<unknown>");
+          pr_trace_msg(trace_channel, 6,
+            "restarting timer ID %d ('%s'), as per callback", t->timerno,
+            t->desc ? t->desc : "<unknown>");
 
           xaset_remove(timers, (xasetmember_t *) t);
           t->count = t->interval;
@@ -144,9 +163,11 @@ static int process_timers(int elapsed) {
   }
 
   /* Put the recycled timers back into the main timer list. */
-  while ((t = (struct timer *) recycled->xas_list) != NULL) {
+  t = (struct timer *) recycled->xas_list;
+  while (t != NULL) {
     xaset_remove(recycled, (xasetmember_t *) t);
     xaset_insert_sort(timers, (xasetmember_t *) t, TRUE);
+    t = (struct timer *) recycled->xas_list;
   }
 
   _indispatch--;
@@ -155,7 +176,21 @@ static int process_timers(int elapsed) {
   /* If no active timers remain in the list, there is no reason to set the
    * SIGALRM handle.
    */
-  return (timers->xas_list ? ((struct timer *) timers->xas_list)->count : 0);
+
+  if (timers->xas_list != NULL) {
+    /* The value we return is a proposed timeout, for the next call to
+     * alarm(3).  We start with the simple count of timers in our list.
+     *
+     * But then we reduce the number; some of the timers' intervals may
+     * less than the number of total timers.
+     */
+    res = ((struct timer *) timers->xas_list)->count;
+    if (res > 5) {
+      res = 5;
+    }
+  }
+
+  return res;
 }
 
 static RETSIGTYPE sig_alarm(int signo) {
@@ -231,32 +266,39 @@ void handle_alarm(void) {
    */
 
   /* It's possible that alarms are blocked when this function is
-   * called, if so, increment alarm_pending and exit swiftly
+   * called, if so, increment alarm_pending and exit swiftly.
    */
   while (nalarms) {
     nalarms = 0;
 
     if (!alarms_blocked) {
       int alarm_elapsed;
+      time_t now;
 
+      /* Clear any pending ALRM signals. */
       alarm(0);
-      alarm_elapsed = _alarmed_time ? (int) time(NULL) - _alarmed_time : 0;
+
+      /* Determine how much time has elapsed since we last processed timers. */
+      time(&now);
+      alarm_elapsed = _alarmed_time > 0 ? (int) (now - _alarmed_time) : 0;
+
       new_timeout = _total_time + alarm_elapsed;
       _total_time = 0;
       new_timeout = process_timers(new_timeout);
 
-      _alarmed_time = time(NULL);
+      _alarmed_time = now;
       alarm(_current_timeout = new_timeout);
 
-    } else
+    } else {
       alarm_pending++;
+    }
   }
 }
 
 int pr_timer_reset(int timerno, module *mod) {
   struct timer *t = NULL;
 
-  if (!timers) {
+  if (timers == NULL) {
     errno = EPERM;
     return -1;
   }
@@ -268,8 +310,9 @@ int pr_timer_reset(int timerno, module *mod) {
 
   pr_alarms_block();
 
-  if (!recycled)
+  if (recycled == NULL) {
     recycled = xaset_create(timer_pool, NULL);
+  }
 
   for (t = (struct timer *) timers->xas_list; t; t = t->next) {
     if (t->timerno == timerno &&
@@ -290,13 +333,13 @@ int pr_timer_reset(int timerno, module *mod) {
 
   pr_alarms_unblock();
 
-  if (t) {
-    pr_trace_msg("timer", 7, "reset timer ID %d ('%s', for module '%s')",
+  if (t != NULL) {
+    pr_trace_msg(trace_channel, 7, "reset timer ID %d ('%s', for module '%s')",
       t->timerno, t->desc, t->mod ? t->mod->name : "[none]");
     return t->timerno;
   }
 
-  return (t ? t->timerno : 0);
+  return 0;
 }
 
 int pr_timer_remove(int timerno, module *mod) {
@@ -331,8 +374,9 @@ int pr_timer_remove(int timerno, module *mod) {
         handle_alarm();
       }
 
-      pr_trace_msg("timer", 7, "removed timer ID %d ('%s', for module '%s')",
-        t->timerno, t->desc, t->mod ? t->mod->name : "[none]");
+      pr_trace_msg(trace_channel, 7,
+        "removed timer ID %d ('%s', for module '%s')", t->timerno, t->desc,
+        t->mod ? t->mod->name : "[none]");
     }
 
     /* If we are removing a specific timer, break out of the loop now.
@@ -395,7 +439,6 @@ int pr_timer_add(int seconds, int timerno, module *mod, callback_t cb,
     xaset_remove(free_timers, (xasetmember_t *) t);
 
   } else {
-
     if (timer_pool == NULL) {
       timer_pool = make_sub_pool(permanent_pool);
       pr_pool_tag(timer_pool, "Timer Pool");
@@ -444,7 +487,7 @@ int pr_timer_add(int seconds, int timerno, module *mod, callback_t cb,
 
   pr_alarms_unblock();
 
-  pr_trace_msg("timer", 7, "added timer ID %d ('%s', for module '%s'), "
+  pr_trace_msg(trace_channel, 7, "added timer ID %d ('%s', for module '%s'), "
     "triggering in %ld %s", t->timerno, t->desc,
     t->mod ? t->mod->name : "[none]", t->interval,
     t->interval != 1 ? "seconds" : "second");
@@ -532,8 +575,9 @@ void timers_init(void) {
   free_timers = NULL;
 
   /* Reset the timer pool. */
-  if (timer_pool)
+  if (timer_pool) {
     destroy_pool(timer_pool);
+  }
 
   timer_pool = make_sub_pool(permanent_pool);
   pr_pool_tag(timer_pool, "Timer Pool");
diff --git a/src/trace.c b/src/trace.c
index ad0a958..1c29cc6 100644
--- a/src/trace.c
+++ b/src/trace.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006-2013 The ProFTPD Project team
+ * Copyright (c) 2006-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,10 +22,7 @@
  * in the source distribution.
  */
 
-/* Trace functions
- * $Id: trace.c,v 1.46 2013-02-25 19:40:43 castaglia Exp $
- */
-
+/* Trace functions. */
 
 #include "conf.h"
 #include "privs.h"
@@ -59,6 +56,8 @@ static const char *trace_channels[] = {
   "ident",
   "inet",
   "lock",
+  "log",
+  "module",
   "netacl",
   "netio",
   "pam",
@@ -93,13 +92,14 @@ static void trace_restart_ev(const void *event_data, void *user_data) {
 
 static int trace_write(const char *channel, int level, const char *msg,
     int discard) {
-  char buf[PR_TUNABLE_BUFFER_SIZE];
+  char buf[PR_TUNABLE_BUFFER_SIZE * 2];
   size_t buflen, len;
   struct tm *tm;
   int use_conn_ips = FALSE;
 
-  if (trace_logfd < 0)
+  if (trace_logfd < 0) {
     return 0;
+  }
 
   memset(buf, '\0', sizeof(buf));
 
@@ -168,7 +168,11 @@ static int trace_write(const char *channel, int level, const char *msg,
     buflen++;
 
   } else {
-    buf[sizeof(buf)-2] = '\n';
+    buf[sizeof(buf)-5] = '.';
+    buf[sizeof(buf)-4] = '.';
+    buf[sizeof(buf)-3] = '.';
+    buf[sizeof(buf)-2] = '.';
+    buflen = sizeof(buf)-1;
   }
 
   pr_log_event_generate(PR_LOG_TYPE_TRACELOG, trace_logfd, level, buf, buflen);
@@ -186,16 +190,16 @@ static int trace_write(const char *channel, int level, const char *msg,
 }
 
 pr_table_t *pr_trace_get_table(void) {
-  if (!trace_tab) {
-    errno = EPERM;
+  if (trace_tab == NULL) {
+    errno = ENOENT;
     return NULL;
   }
 
   return trace_tab;
 }
 
-static struct trace_levels *trace_get_levels(const char *channel) {
-  void *value;
+static const struct trace_levels *trace_get_levels(const char *channel) {
+  const void *value;
 
   if (channel == NULL) {
     errno = EINVAL;
@@ -222,21 +226,23 @@ int pr_trace_get_level(const char *channel) {
 }
 
 int pr_trace_get_max_level(const char *channel) {
-  struct trace_levels *levels;
+  const struct trace_levels *levels;
 
   levels = trace_get_levels(channel);
-  if (levels == NULL)
+  if (levels == NULL) {
     return -1;
+  }
 
   return levels->max_level;
 }
 
 int pr_trace_get_min_level(const char *channel) {
-  struct trace_levels *levels;
+  const struct trace_levels *levels;
 
   levels = trace_get_levels(channel);
-  if (levels == NULL)
+  if (levels == NULL) {
     return -1;
+  }
 
   return levels->min_level;
 }
@@ -259,6 +265,11 @@ int pr_trace_parse_levels(char *str, int *min_level, int *max_level) {
   }
 
   /* Check for a value range. */
+  if (*str == '-') {
+    errno = EINVAL;
+    return -1;
+  }
+
   ptr = strchr(str, '-');
   if (ptr == NULL) {
     /* Just a single value. */
@@ -315,15 +326,20 @@ int pr_trace_parse_levels(char *str, int *min_level, int *max_level) {
     return -1;
   }
 
+  if (high < low) {
+    errno = EINVAL;
+    return -1;
+  }
+
   *min_level = low;
   *max_level = high;
   return 0;
 }
 
 int pr_trace_set_file(const char *path) {
-  int res;
+  int res, xerrno;
 
-  if (!path) {
+  if (path == NULL) {
     if (trace_logfd < 0) {
       errno = EINVAL;
       return -1;
@@ -337,23 +353,27 @@ int pr_trace_set_file(const char *path) {
   pr_signals_block();
   PRIVS_ROOT
   res = pr_log_openfile(path, &trace_logfd, 0660);
+  xerrno = errno;
   PRIVS_RELINQUISH
   pr_signals_unblock();
 
   if (res < 0) {
     if (res == -1) {
       pr_log_debug(DEBUG1, "unable to open TraceLog '%s': %s", path,
-        strerror(errno));
+        strerror(xerrno));
+      errno = xerrno;
 
     } else if (res == PR_LOG_WRITABLE_DIR) {
       pr_log_debug(DEBUG1,
         "unable to open TraceLog '%s': parent directory is world-writable",
         path);
+      errno = EPERM;
 
     } else if (res == PR_LOG_SYMLINK) {
       pr_log_debug(DEBUG1,
         "unable to open TraceLog '%s': cannot log to a symbolic link",
         path);
+      errno = EPERM;
     }
 
     return res;
@@ -365,19 +385,11 @@ int pr_trace_set_file(const char *path) {
 int pr_trace_set_levels(const char *channel, int min_level, int max_level) {
 
   if (channel == NULL) {
-    void *v;
-
     if (trace_tab == NULL) {
       errno = EINVAL;
       return -1;
     }
 
-    v = pr_table_remove(trace_tab, channel, NULL);
-    if (v == NULL) {
-      errno = EINVAL;
-      return -1;
-    }
-
     return 0;
   }
 
@@ -464,6 +476,10 @@ int pr_trace_use_stderr(int use_stderr) {
     /* Avoid a file descriptor leak by closing any existing fd. */
     (void) close(trace_logfd);
     trace_logfd = res;
+
+  } else {
+    (void) close(trace_logfd);
+    trace_logfd = -1;
   }
 
   return 0;
@@ -473,6 +489,19 @@ int pr_trace_msg(const char *channel, int level, const char *fmt, ...) {
   int res;
   va_list msg;
 
+  if (channel == NULL ||
+      fmt == NULL ||
+      level <= 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  /* If no one's listening... */
+  if (trace_logfd < 0 &&
+      pr_log_event_listening(PR_LOG_TYPE_TRACELOG) <= 0) {
+    return 0;
+  }
+
   va_start(msg, fmt);
   res = pr_trace_vmsg(channel, level, fmt, msg);
   va_end(msg);
@@ -482,10 +511,10 @@ int pr_trace_msg(const char *channel, int level, const char *fmt, ...) {
 
 int pr_trace_vmsg(const char *channel, int level, const char *fmt,
     va_list msg) {
-  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
+  char buf[PR_TUNABLE_BUFFER_SIZE * 2];
   size_t buflen;
-  struct trace_levels *levels;
-  int discard = FALSE;
+  const struct trace_levels *levels;
+  int discard = FALSE, listening;
 
   /* Writing a trace message at level zero is NOT helpful; this makes it
    * impossible to quell messages to that trace channel by setting the level
@@ -505,12 +534,19 @@ int pr_trace_vmsg(const char *channel, int level, const char *fmt,
     return -1;
   }
 
+  /* If no one's listening... */
+  if (trace_logfd < 0) {
+    return 0;
+  }
+
+  listening = pr_log_event_listening(PR_LOG_TYPE_TRACELOG);
+
   levels = trace_get_levels(channel);
   if (levels == NULL) {
     discard = TRUE;
 
-    if (pr_log_event_listening(PR_LOG_TYPE_TRACELOG) <= 0) {
-      return -1;
+    if (listening <= 0) {
+      return 0;
     }
   }
 
@@ -518,7 +554,7 @@ int pr_trace_vmsg(const char *channel, int level, const char *fmt,
       level < levels->min_level) {
     discard = TRUE;
 
-    if (pr_log_event_listening(PR_LOG_TYPE_TRACELOG) <= 0) {
+    if (listening <= 0) {
       return 0;
     }
   }
@@ -527,16 +563,26 @@ int pr_trace_vmsg(const char *channel, int level, const char *fmt,
       level > levels->max_level) {
     discard = TRUE;
 
-    if (pr_log_event_listening(PR_LOG_TYPE_TRACELOG) <= 0) {
+    if (listening <= 0) {
       return 0;
     }
   }
 
-  buflen = vsnprintf(buf, sizeof(buf), fmt, msg);
+  buflen = vsnprintf(buf, sizeof(buf)-1, fmt, msg);
 
   /* Always make sure the buffer is NUL-terminated. */
   buf[sizeof(buf)-1] = '\0';
 
+  if (buflen < sizeof(buf)) {
+    buf[buflen] = '\0';
+
+  } else {
+    /* Note that vsnprintf() returns the number of characters _that would have
+     * been printed if buffer were unlimited_.  Be careful of this.
+     */
+    buflen = sizeof(buf)-1; 
+  }
+
   /* Trim trailing newlines. */
   while (buflen >= 1 &&
          buf[buflen-1] == '\n') {
diff --git a/src/utf8.c b/src/utf8.c
index 9583b07..84fd2cd 100644
--- a/src/utf8.c
+++ b/src/utf8.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2006-2008 The ProFTPD Project team
+ * Copyright (c) 2006-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* UTF8 encoding/decoding
- * $Id: utf8.c,v 1.6 2008-03-25 22:20:44 castaglia Exp $
- */
+/* UTF8 encoding/decoding */
 
 #include "conf.h"
 
diff --git a/src/var.c b/src/var.c
index d861487..e33f161 100644
--- a/src/var.c
+++ b/src/var.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2004-2013 The ProFTPD Project team
+ * Copyright (c) 2004-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Variables API implementation
- * $Id: var.c,v 1.8 2013-10-06 05:30:31 castaglia Exp $
- */
+/* Variables API implementation */
 
 #include "conf.h"
 
@@ -75,7 +73,7 @@ int pr_var_exists(const char *name) {
 }
 
 const char *pr_var_get(const char *name) {
-  struct var *v = NULL;
+  const struct var *v = NULL;
 
   if (var_tab == NULL) {
     errno = EPERM;
@@ -113,7 +111,7 @@ const char *pr_var_get(const char *name) {
 
 const char *pr_var_next(const char **desc) {
   const char *name;
-  struct var *v;
+  const struct var *v;
 
   if (var_tab == NULL) {
     errno = EPERM;
@@ -126,7 +124,8 @@ const char *pr_var_next(const char **desc) {
   }
 
   v = pr_table_get(var_tab, name, NULL);
-  if (v && desc) {
+  if (v != NULL &&
+      desc != NULL) {
     *desc = v->v_desc;
   }
 
diff --git a/src/version.c b/src/version.c
index 1b0216a..b94d50d 100644
--- a/src/version.c
+++ b/src/version.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2008 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Versioning
- * $Id: version.c,v 1.2 2011-05-23 21:22:24 castaglia Exp $
- */
+/* Versioning */
 
 #include "conf.h"
 
diff --git a/src/wtmp.c b/src/wtmp.c
index d411411..ca4cc85 100644
--- a/src/wtmp.c
+++ b/src/wtmp.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2009-2013 The ProFTPD Project team
+ * Copyright (c) 2009-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@
  * 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.
- *
- * $Id: wtmp.c,v 1.10 2013-12-09 19:16:15 castaglia Exp $
  */
 
 #include "conf.h"
@@ -32,7 +30,7 @@
  */
 
 int log_wtmp(const char *line, const char *name, const char *host,
-    pr_netaddr_t *ip) {
+    const pr_netaddr_t *ip) {
   struct stat buf;
   int res = 0;
 
diff --git a/src/xferlog.c b/src/xferlog.c
index 771139e..b00c6ec 100644
--- a/src/xferlog.c
+++ b/src/xferlog.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2003-2013 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* ProFTPD xferlog(5) logging support.
- * $Id: xferlog.c,v 1.12 2013-02-15 22:39:00 castaglia Exp $
- */
+/* ProFTPD xferlog(5) logging support. */
 
 #include "conf.h"
 
@@ -41,33 +39,37 @@ void xferlog_close(void) {
 
 int xferlog_open(const char *path) {
 
-  if (!path) {
-    if (xferlogfd != -1)
+  if (path == NULL) {
+    if (xferlogfd != -1) {
       xferlog_close();
+    }
 
     return 0;
   }
 
-  if (xferlogfd == -1) {
+  if (xferlogfd < 0) {
     pr_log_debug(DEBUG6, "opening TransferLog '%s'", path);
-    pr_log_openfile(path, &xferlogfd, PR_LOG_XFER_MODE);
+    pr_log_openfile(path, &xferlogfd, PR_TUNABLE_XFER_LOG_MODE);
 
     if (xferlogfd < 0) {
+      int xerrno = errno;
+
       pr_log_pri(PR_LOG_NOTICE, "unable to open TransferLog '%s': %s",
-        path, strerror(errno));
+        path, strerror(xerrno));
+
+      errno = xerrno;
     }
   }
 
   return xferlogfd;
 }
 
-int xferlog_write(long xfertime, const char *remhost, off_t fsize, char *fname,
-    char xfertype, char direction, char access_mode, char *user,
-    char abort_flag, const char *action_flags) {
-  const char *xfer_proto;
+int xferlog_write(long xfertime, const char *remhost, off_t fsize,
+    const char *fname, char xfertype, char direction, char access_mode,
+    const char *user, char abort_flag, const char *action_flags) {
+  const char *rfc1413_ident = NULL, *xfer_proto;
   char buf[LOGBUFFER_SIZE] = {'\0'}, fbuf[LOGBUFFER_SIZE] = {'\0'};
   int have_ident = FALSE, len;
-  char *rfc1413_ident = NULL;
   register unsigned int i = 0;
 
   if (xferlogfd == -1 ||
@@ -84,15 +86,16 @@ int xferlog_write(long xfertime, const char *remhost, off_t fsize, char *fname,
   fbuf[i] = '\0';
 
   rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident", NULL);
-  if (rfc1413_ident) {
+  if (rfc1413_ident != NULL) {
     have_ident = TRUE;
 
     /* If the retrieved identity is "UNKNOWN", then change the string to be
      * "*", since "*" is to be logged in the xferlog, as per the doc, when
      * the authenticated user ID is not available.
      */
-    if (strncmp(rfc1413_ident, "UNKNOWN", 8) == 0)
+    if (strncmp(rfc1413_ident, "UNKNOWN", 8) == 0) {
       rfc1413_ident = "*";
+    }
 
   } else {
     /* If an authenticated user ID is not available, log "*", as per the
diff --git a/tests/Makefile.in b/tests/Makefile.in
index d058377..ae37982 100644
--- a/tests/Makefile.in
+++ b/tests/Makefile.in
@@ -17,6 +17,7 @@ TEST_API_DEPS=\
   $(top_srcdir)/src/sets.o \
   $(top_srcdir)/src/timers.o \
   $(top_srcdir)/src/table.o \
+  $(top_srcdir)/src/support.o \
   $(top_srcdir)/src/var.o \
   $(top_srcdir)/src/event.o \
   $(top_srcdir)/src/env.o \
@@ -32,11 +33,25 @@ TEST_API_DEPS=\
   $(top_srcdir)/src/modules.o \
   $(top_srcdir)/src/cmd.o \
   $(top_srcdir)/src/response.o \
+  $(top_srcdir)/src/rlimit.o \
   $(top_srcdir)/src/fsio.o \
   $(top_srcdir)/src/netio.o \
-  $(top_srcdir)/src/encode.o
-
-TEST_API_LIBS=-lcheck
+  $(top_srcdir)/src/encode.o \
+  $(top_srcdir)/src/trace.o \
+  $(top_srcdir)/src/parser.o \
+  $(top_srcdir)/src/pidfile.o \
+  $(top_srcdir)/src/configdb.o \
+  $(top_srcdir)/src/auth.o \
+  $(top_srcdir)/src/filter.o \
+  $(top_srcdir)/src/inet.o \
+  $(top_srcdir)/src/data.o \
+  $(top_srcdir)/src/ascii.o \
+  $(top_srcdir)/src/help.o \
+  $(top_srcdir)/src/display.o \
+  $(top_srcdir)/src/json.o \
+  $(top_srcdir)/src/redis.o
+
+TEST_API_LIBS=-lcheck -lm
 
 TEST_API_OBJS=\
   api/pool.o \
@@ -62,10 +77,26 @@ TEST_API_OBJS=\
   api/response.o \
   api/fsio.o \
   api/netio.o \
+  api/trace.o \
+  api/parser.o \
+  api/pidfile.o \
+  api/configdb.o \
+  api/auth.o \
+  api/filter.o \
+  api/inet.o \
+  api/data.o \
+  api/ascii.o \
+  api/help.o \
+  api/rlimit.o \
+  api/encode.o \
+  api/privs.o \
+  api/display.o \
+  api/misc.o \
+  api/json.o \
+  api/redis.o \
   api/stubs.o \
   api/tests.o
 
-
 all:
 	@echo "Running make from top level directory."
 	cd ../; $(MAKE) all
@@ -79,7 +110,7 @@ api/.c.o:
 	$(CC) $(CPPFLAGS) $(CFLAGS) -c $<
 
 api-tests$(EXEEXT): $(TEST_API_OBJS) $(TEST_API_DEPS)
-	$(LIBTOOL) --mode=link --tag=CC $(CC) $(LDFLAGS) -o $@ $(TEST_API_DEPS) $(TEST_API_OBJS) $(LIBS) $(TEST_API_LIBS)
+	$(LIBTOOL) --mode=link --tag=CC $(CC) $(LDFLAGS) -o $@ $(TEST_API_DEPS) $(TEST_API_OBJS) $(TEST_API_LIBS) $(LIBS)
 	./$@
 
 running-tests:
@@ -105,4 +136,4 @@ check-utils:
 check: check-api running-tests
 
 clean:
-	$(LIBTOOL) --mode=clean $(RM) *.o api/*.o api-tests$(EXEEXT) api-tests.log
+	$(LIBTOOL) --mode=clean $(RM) *.o *.gcda *.gcno api/*.o api-tests$(EXEEXT) api-tests.log
diff --git a/tests/api/array.c b/tests/api/array.c
index 47e9307..9cddb9c 100644
--- a/tests/api/array.c
+++ b/tests/api/array.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Array API tests
- * $Id: array.c,v 1.2 2011-05-23 20:50:29 castaglia Exp $
- */
+/* Array API tests */
 
 #include "tests.h"
 
@@ -164,6 +162,62 @@ START_TEST (array_cat_test) {
 }
 END_TEST
 
+START_TEST (array_cat2_test) {
+  array_header *src, *dst;
+  int res;
+
+  mark_point();
+
+  res = array_cat2(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno EINVAL, got '%s' (%d)",
+    strerror(errno), errno);
+
+  dst = make_array(p, 0, 1);
+  mark_point();
+  res = array_cat2(dst, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno EINVAL, got '%s' (%d)",
+    strerror(errno), errno);
+
+  src = make_array(p, 0, 1);
+  mark_point();
+  res = array_cat2(NULL, src);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno EINVAL, got '%s' (%d)",
+    strerror(errno), errno);
+
+  mark_point();
+  res = array_cat2(dst, src);
+  fail_unless(res == 0, "Failed to concatenate arrays: %s", strerror(errno));
+
+  fail_unless(dst->nalloc == 1, "Wrong dst alloc count (expected %u, got %d)",
+    1, dst->nalloc);
+  fail_unless(dst->nelts == 0, "Wrong dst item count (expected %u, got %d)",
+    0, dst->nelts);
+
+  push_array(src);
+  res = array_cat2(dst, src);
+  fail_unless(res == 0, "Failed to concatenate arrays: %s", strerror(errno));
+
+  fail_unless(dst->nalloc == 1, "Wrong dst alloc count (expected %u, got %d)",
+    1, dst->nalloc);
+  fail_unless(dst->nelts == 1, "Wrong dst item count (expected %u, got %d)",
+    1, dst->nelts);
+
+  push_array(src);
+  push_array(src);
+  push_array(src);
+  res = array_cat2(dst, src);
+  fail_unless(res == 0, "Failed to concatenate arrays: %s", strerror(errno));
+
+  fail_unless(dst->nalloc == 8, "Wrong dst alloc count (expected %u, got %d)",
+    8, dst->nalloc);
+  fail_unless(dst->nelts == 5, "Wrong dst item count (expected %u, got %d)",
+    5, dst->nelts);
+}
+END_TEST
+
 START_TEST (clear_array_test) {
   array_header *list;
 
@@ -398,6 +452,7 @@ Suite *tests_get_array_suite(void) {
   tcase_add_test(testcase, make_array_test);
   tcase_add_test(testcase, push_array_test);
   tcase_add_test(testcase, array_cat_test);
+  tcase_add_test(testcase, array_cat2_test);
   tcase_add_test(testcase, clear_array_test);
   tcase_add_test(testcase, copy_array_test);
   tcase_add_test(testcase, copy_array_str_test);
diff --git a/tests/api/ascii.c b/tests/api/ascii.c
new file mode 100644
index 0000000..f9677e1
--- /dev/null
+++ b/tests/api/ascii.c
@@ -0,0 +1,351 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* ASCII API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = make_sub_pool(NULL);
+  }
+}
+
+static void tear_down(void) {
+  if (p) {
+    destroy_pool(p);
+    p = NULL;
+  } 
+}
+
+START_TEST (ascii_ftp_from_crlf_test) {
+  int res;
+  char *src, *dst, *expected;
+  size_t src_len, dst_len, expected_len;
+
+  pr_ascii_ftp_reset();
+  res = pr_ascii_ftp_from_crlf(NULL, NULL, 0, NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL ('%s' [%d]), got '%s' [%d]",
+    strerror(errno), errno);
+
+  /* Handle an empty input buffer. */
+  pr_ascii_ftp_reset();
+  src = "";
+  src_len = 0;
+  dst = pcalloc(p, 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle empty input buffer");
+  fail_unless(dst_len == src_len, "Failed to set output buffer length");
+
+  /* Handle an input buffer with no CRLFs. */
+  pr_ascii_ftp_reset();
+  src = "hello";
+  src_len = 5;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with no CRLFs");
+  expected = src;
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  res = strcmp(expected, dst);
+  fail_unless(res == 0, "Expected output buffer '%s', got '%s' (%d)", expected,
+    dst, res);
+
+  /* Handle an input buffer with CRs, no LFs. */
+  pr_ascii_ftp_reset();
+  src = "he\rl\rlo";
+  src_len = 7;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with CRs, no LFs");
+  expected = src;
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle an input buffer with LFs, no CRs. */
+  pr_ascii_ftp_reset();
+  src = "he\nl\nlo";
+  src_len = 7;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with LFs, no CRs");
+  expected = src;
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle an input buffer with several CRLFs. */
+  pr_ascii_ftp_reset();
+  src = "he\r\nl\r\nlo"; 
+  src_len = 9;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with CRLFs");
+  expected = "he\nl\nlo";
+  expected_len = 7;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle an input buffer ending with a CR. */
+  pr_ascii_ftp_reset();
+  src = "he\r\nl\r\nlo\r";
+  src_len = 10;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 1,
+    "Failed to handle input buffer with trailing CR: expected %d, got %d", 1,
+    res);
+  expected = "he\nl\nlo\r";
+  expected_len = 7;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle an input buffer of just an LF. */
+  pr_ascii_ftp_reset();
+  src = "\n";
+  src_len = 1;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0,
+    "Failed to handle input buffer of single LF: expected %d, got %d", 0, res);
+  expected = "\n";
+  expected_len = 1;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle an input buffer of just a CR. */
+  pr_ascii_ftp_reset();
+  src = "\r";
+  src_len = 1;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 1,
+    "Failed to handle input buffer of single CR: expected %d, got %d", 1, res);
+  expected = "\r";
+  expected_len = 0;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle an input buffer of just CRs. */
+  pr_ascii_ftp_reset();
+  src = "\r\r\r";
+  src_len = 3;
+  dst = pcalloc(p, src_len + 1);
+  dst_len = 0;
+  res = pr_ascii_ftp_from_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 1,
+    "Failed to handle input buffer of single CR: expected %d, got %d", 3, res);
+  expected = "\r\r\r";
+  expected_len = 2;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strcmp(dst, expected) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+}
+END_TEST
+
+START_TEST (ascii_ftp_to_crlf_test) {
+  int res;
+  char *src, *dst, *expected;
+  size_t src_len, dst_len, expected_len;
+
+  pr_ascii_ftp_reset();
+  res = pr_ascii_ftp_to_crlf(NULL, NULL, 0, NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL ('%s' [%d]), got '%s' [%d]",
+    strerror(errno), errno);
+
+  /* Handle empty input buffer. */
+  pr_ascii_ftp_reset();
+  src = "";
+  src_len = 0;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle empty input buffer");
+  fail_unless(dst_len == src_len, "Failed to set output buffer length");
+  fail_unless(dst == src, "Failed to set output buffer");
+
+  /* Handle input buffer with no CRLFs. */
+  pr_ascii_ftp_reset();
+  src = "hello";
+  src_len = 5;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with no CRLFs");
+  expected = src;
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle input buffer with CRs, no LFs. */
+  pr_ascii_ftp_reset();
+  src = "he\rl\rlo";
+  src_len = 7;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with CRs, no LFs");
+  expected = src; 
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle input buffer with LFs, no CRs. */
+  pr_ascii_ftp_reset();
+  src = "he\nl\nlo";
+  src_len = 7;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 2, "Failed to handle input buffer with CRs, no LFs");
+  expected = "he\r\nl\r\nlo";
+  expected_len = 9;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle input buffer CRLFs. */
+  pr_ascii_ftp_reset();
+  src = "he\r\nl\r\nlo";
+  src_len = 9;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with CRs, no LFs");
+  expected = src;
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle input buffer with leading LF. */
+  pr_ascii_ftp_reset();
+  src = "\nhello";
+  src_len = 6;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 1, "Failed to handle input buffer with leading LF");
+  expected = "\r\nhello";
+  expected_len = 7;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  /* Handle input buffer with trailing CR. */
+  pr_ascii_ftp_reset();
+  src = "hel\r";
+  src_len = 4;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 0, "Failed to handle input buffer with trailing CR");
+  expected = src;
+  expected_len = src_len;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+
+  src = "\nlo\n";
+  src_len = 4;
+  dst = NULL;
+  dst_len = 0;
+  res = pr_ascii_ftp_to_crlf(p, src, src_len, &dst, &dst_len);
+  fail_unless(res == 1, "Failed to handle next input buffer after trailing CR");
+  expected = "\nlo\r\n";
+  expected_len = 5;
+  fail_unless(dst_len == expected_len,
+    "Expected output buffer length %lu, got %lu", (unsigned long) expected_len,
+    (unsigned long) dst_len);
+  fail_unless(strncmp(dst, expected, dst_len) == 0,
+    "Expected output buffer '%s', got '%s'", expected, dst);
+}
+END_TEST
+
+Suite *tests_get_ascii_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("ascii");
+
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, ascii_ftp_from_crlf_test);
+  tcase_add_test(testcase, ascii_ftp_to_crlf_test);
+
+  suite_add_tcase(suite, testcase);
+
+  return suite;
+}
diff --git a/tests/api/auth.c b/tests/api/auth.c
new file mode 100644
index 0000000..a28083c
--- /dev/null
+++ b/tests/api/auth.c
@@ -0,0 +1,1915 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Auth API tests */
+
+#include "tests.h"
+
+#define PR_TEST_AUTH_NAME		"testsuite_user"
+#define PR_TEST_AUTH_NOBODY		"testsuite_nobody"
+#define PR_TEST_AUTH_NOBODY2		"testsuite_nobody2"
+#define PR_TEST_AUTH_NOGROUP		"testsuite_nogroup"
+#define PR_TEST_AUTH_UID		500
+#define PR_TEST_AUTH_UID_STR		"500"
+#define PR_TEST_AUTH_NOUID		666
+#define PR_TEST_AUTH_NOUID2		667
+#define PR_TEST_AUTH_GID		500
+#define PR_TEST_AUTH_GID_STR		"500"
+#define PR_TEST_AUTH_NOGID		666
+#define PR_TEST_AUTH_HOME		"/tmp"
+#define PR_TEST_AUTH_SHELL		"/bin/bash"
+#define PR_TEST_AUTH_PASSWD		"password"
+
+static pool *p = NULL;
+
+static struct passwd test_pwd;
+static struct group test_grp;
+
+static unsigned int setpwent_count = 0;
+static unsigned int endpwent_count = 0;
+static unsigned int getpwent_count = 0;
+static unsigned int getpwnam_count = 0;
+static unsigned int getpwuid_count = 0;
+static unsigned int name2uid_count = 0;
+static unsigned int uid2name_count = 0;
+
+static unsigned int setgrent_count = 0;
+static unsigned int endgrent_count = 0;
+static unsigned int getgrent_count = 0;
+static unsigned int getgrnam_count = 0;
+static unsigned int getgrgid_count = 0;
+static unsigned int name2gid_count = 0;
+static unsigned int gid2name_count = 0;
+static unsigned int getgroups_count = 0;
+
+static module testsuite_module = {
+  NULL, NULL,
+
+  /* Module API version */
+  0x20,
+
+  /* Module name */
+  "testsuite",
+
+  /* Module configuration directive table */
+  NULL,
+
+  /* Module command handler table */
+  NULL,
+
+  /* Module authentication handler table */
+  NULL,
+
+  /* Module initialization function */
+  NULL,
+
+  /* Session initialization function */
+  NULL
+};
+
+MODRET handle_setpwent(cmd_rec *cmd) {
+  setpwent_count++;
+  return PR_HANDLED(cmd);
+}
+
+MODRET handle_endpwent(cmd_rec *cmd) {
+  endpwent_count++;
+  return PR_HANDLED(cmd);
+}
+
+MODRET handle_getpwent(cmd_rec *cmd) {
+  getpwent_count++;
+
+  if (getpwent_count == 1) {
+    test_pwd.pw_uid = PR_TEST_AUTH_UID;
+    test_pwd.pw_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  if (getpwent_count == 2) {
+    test_pwd.pw_uid = (uid_t) -1;
+    test_pwd.pw_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  if (getpwent_count == 3) {
+    test_pwd.pw_uid = PR_TEST_AUTH_UID;
+    test_pwd.pw_gid = (gid_t) -1;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_getpwnam(cmd_rec *cmd) {
+  const char *name;
+
+  name = cmd->argv[0];
+  getpwnam_count++;
+
+  if (strcmp(name, PR_TEST_AUTH_NAME) == 0) {
+    test_pwd.pw_uid = PR_TEST_AUTH_UID;
+    test_pwd.pw_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  if (strcmp(name, PR_TEST_AUTH_NOBODY) == 0) {
+    test_pwd.pw_uid = (uid_t) -1;
+    test_pwd.pw_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  if (strcmp(name, PR_TEST_AUTH_NOBODY2) == 0) {
+    test_pwd.pw_uid = PR_TEST_AUTH_UID;
+    test_pwd.pw_gid = (gid_t) -1;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_getpwuid(cmd_rec *cmd) {
+  uid_t uid;
+
+  uid = *((uid_t *) cmd->argv[0]);
+  getpwuid_count++;
+
+  if (uid == PR_TEST_AUTH_UID) {
+    test_pwd.pw_uid = PR_TEST_AUTH_UID;
+    test_pwd.pw_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  if (uid == PR_TEST_AUTH_NOUID) {
+    test_pwd.pw_uid = (uid_t) -1;
+    test_pwd.pw_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  if (uid == PR_TEST_AUTH_NOUID2) {
+    test_pwd.pw_uid = PR_TEST_AUTH_UID;
+    test_pwd.pw_gid = (gid_t) -1;
+    return mod_create_data(cmd, &test_pwd);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET decline_name2uid(cmd_rec *cmd) {
+  name2uid_count++;
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_name2uid(cmd_rec *cmd) {
+  const char *name;
+
+  name = cmd->argv[0];
+  name2uid_count++;
+
+  if (strcmp(name, PR_TEST_AUTH_NAME) != 0) {
+    return PR_DECLINED(cmd);
+  }
+
+  return mod_create_data(cmd, (void *) &(test_pwd.pw_uid));
+}
+
+MODRET decline_uid2name(cmd_rec *cmd) {
+  uid2name_count++;
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_uid2name(cmd_rec *cmd) {
+  uid_t uid;
+
+  uid = *((uid_t *) cmd->argv[0]);
+  uid2name_count++;
+
+  if (uid != PR_TEST_AUTH_UID) {
+    return PR_DECLINED(cmd);
+  }
+
+  return mod_create_data(cmd, test_pwd.pw_name);
+}
+
+MODRET handle_setgrent(cmd_rec *cmd) {
+  setgrent_count++;
+  return PR_HANDLED(cmd);
+}
+
+MODRET handle_endgrent(cmd_rec *cmd) {
+  endgrent_count++;
+  return PR_HANDLED(cmd);
+}
+
+MODRET handle_getgrent(cmd_rec *cmd) {
+  getgrent_count++;
+
+  if (getgrent_count == 1) {
+    test_grp.gr_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_grp);
+  }
+
+  if (getgrent_count == 2) {
+    test_grp.gr_gid = (gid_t) -1;
+    return mod_create_data(cmd, &test_grp);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_getgrnam(cmd_rec *cmd) {
+  const char *name;
+
+  name = cmd->argv[0];
+  getgrnam_count++;
+
+  if (strcmp(name, PR_TEST_AUTH_NAME) == 0) {
+    test_grp.gr_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_grp);
+  }
+
+  if (strcmp(name, PR_TEST_AUTH_NOGROUP) == 0) {
+    test_grp.gr_gid = (gid_t) -1;
+    return mod_create_data(cmd, &test_grp);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_getgrgid(cmd_rec *cmd) {
+  gid_t gid;
+
+  gid = *((gid_t *) cmd->argv[0]);
+  getgrgid_count++;
+
+  if (gid == PR_TEST_AUTH_GID) {
+    test_grp.gr_gid = PR_TEST_AUTH_GID;
+    return mod_create_data(cmd, &test_grp);
+  }
+
+  if (gid == PR_TEST_AUTH_NOGID) {
+    test_grp.gr_gid = (gid_t) -1;
+    return mod_create_data(cmd, &test_grp);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET decline_name2gid(cmd_rec *cmd) {
+  name2gid_count++;
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_name2gid(cmd_rec *cmd) {
+  const char *name;
+
+  name = cmd->argv[0];
+  name2gid_count++;
+
+  if (strcmp(name, PR_TEST_AUTH_NAME) != 0) {
+    return PR_DECLINED(cmd);
+  }
+
+  return mod_create_data(cmd, (void *) &(test_grp.gr_gid));
+}
+
+MODRET decline_gid2name(cmd_rec *cmd) {
+  gid2name_count++;
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_gid2name(cmd_rec *cmd) {
+  gid_t gid;
+
+  gid = *((gid_t *) cmd->argv[0]);
+  gid2name_count++;
+
+  if (gid != PR_TEST_AUTH_GID) {
+    return PR_DECLINED(cmd);
+  }
+
+  return mod_create_data(cmd, test_grp.gr_name);
+}
+
+MODRET handle_getgroups(cmd_rec *cmd) {
+  const char *name;
+  array_header *gids = NULL, *names = NULL;
+
+  name = (char *) cmd->argv[0];
+
+  if (cmd->argv[1]) {
+    gids = (array_header *) cmd->argv[1];
+  }
+
+  if (cmd->argv[2]) {
+    names = (array_header *) cmd->argv[2];
+  }
+
+  getgroups_count++;
+
+  if (strcmp(name, PR_TEST_AUTH_NAME) != 0) {
+    return PR_DECLINED(cmd);
+  }
+
+  if (gids) { 
+    *((gid_t *) push_array(gids)) = PR_TEST_AUTH_GID;
+  }
+
+  if (names) {
+    *((char **) push_array(names)) = pstrdup(p, PR_TEST_AUTH_NAME);
+  }
+
+  return mod_create_data(cmd, (void *) &gids->nelts);
+}
+
+static int authn_rfc2228 = FALSE;
+
+MODRET handle_authn(cmd_rec *cmd) {
+  const char *user, *cleartext_passwd;
+
+  user = cmd->argv[0];
+  cleartext_passwd = cmd->argv[1];
+
+  if (strcmp(user, PR_TEST_AUTH_NAME) == 0) {
+    if (strcmp(cleartext_passwd, PR_TEST_AUTH_PASSWD) == 0) {
+      if (authn_rfc2228) {
+        authn_rfc2228 = FALSE;
+        return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
+      }
+
+      return PR_HANDLED(cmd);
+    }
+
+    return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_authz(cmd_rec *cmd) {
+  const char *user;
+
+  user = cmd->argv[0];
+
+  if (strcmp(user, PR_TEST_AUTH_NAME) == 0) {
+    return PR_HANDLED(cmd);
+  }
+
+  return PR_ERROR_INT(cmd, PR_AUTH_NOPWD);
+}
+
+static int check_rfc2228 = FALSE;
+
+MODRET handle_check(cmd_rec *cmd) {
+  const char *user, *cleartext_passwd, *ciphertext_passwd;
+
+  ciphertext_passwd = cmd->argv[0];
+  user = cmd->argv[1];
+  cleartext_passwd = cmd->argv[2];
+
+  if (strcmp(user, PR_TEST_AUTH_NAME) == 0) {
+    if (ciphertext_passwd != NULL &&
+        strcmp(ciphertext_passwd, cleartext_passwd) == 0) {
+      if (check_rfc2228) {
+        check_rfc2228 = FALSE;
+        return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
+      }
+
+      return PR_HANDLED(cmd);
+    }
+
+    return PR_ERROR_INT(cmd, PR_AUTH_BADPWD);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+MODRET handle_requires_pass(cmd_rec *cmd) {
+  const char *name;
+
+  name = cmd->argv[0];
+
+  if (strcmp(name, PR_TEST_AUTH_NAME) == 0) {
+    return mod_create_data(cmd, (void *) PR_AUTH_RFC2228_OK);
+  }
+
+  return PR_DECLINED(cmd);
+}
+
+/* Fixtures */
+
+static void set_up(void) {
+  server_rec *s = NULL;
+
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_stash();
+  init_auth();
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_DEFAULT);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("auth", 1, 20);
+  }
+
+  s = pcalloc(p, sizeof(server_rec));
+  tests_stubs_set_main_server(s);
+
+  test_pwd.pw_name = PR_TEST_AUTH_NAME;
+  test_pwd.pw_uid = PR_TEST_AUTH_UID;
+  test_pwd.pw_gid = PR_TEST_AUTH_GID;
+  test_pwd.pw_dir = PR_TEST_AUTH_HOME;
+  test_pwd.pw_shell = PR_TEST_AUTH_SHELL;
+
+  test_grp.gr_name = PR_TEST_AUTH_NAME;
+  test_grp.gr_gid = PR_TEST_AUTH_GID;
+
+  /* Reset counters. */
+  setpwent_count = 0;
+  endpwent_count = 0;
+  getpwent_count = 0;
+  getpwnam_count = 0;
+  getpwuid_count = 0;
+  name2uid_count = 0;
+  uid2name_count = 0;
+
+  setgrent_count = 0;
+  endgrent_count = 0;
+  getgrent_count = 0;
+  getgrnam_count = 0;
+  getgrgid_count = 0;
+  name2gid_count = 0;
+  gid2name_count = 0;
+  getgroups_count = 0;
+
+  pr_auth_cache_clear();
+}
+
+static void tear_down(void) {
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_DEFAULT);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("auth", 0, 0);
+  }
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  } 
+
+  tests_stubs_set_main_server(NULL);
+}
+
+/* Tests */
+
+START_TEST (auth_setpwent_test) {
+  int res;
+  authtable authtab;
+  char *sym_name = "setpwent";
+
+  pr_auth_setpwent(p);
+  fail_unless(setpwent_count == 0, "Expected call count 0, got %u",
+    setpwent_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_setpwent;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  pr_auth_setpwent(p);
+  fail_unless(setpwent_count == 1, "Expected call count 1, got %u",
+    setpwent_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_endpwent_test) {
+  int res;
+  authtable authtab;
+  char *sym_name = "endpwent";
+
+  pr_auth_endpwent(p);
+  fail_unless(endpwent_count == 0, "Expected call count 0, got %u",
+    endpwent_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_endpwent;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  pr_auth_endpwent(p);
+  fail_unless(endpwent_count == 1, "Expected call count 1, got %u",
+    endpwent_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getpwent_test) {
+  int res;
+  struct passwd *pw;
+  authtable authtab;
+  char *sym_name = "getpwent";
+
+  getpwent_count = 0;
+
+  pw = pr_auth_getpwent(NULL);
+  fail_unless(pw == NULL, "Found pwent unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  pw = pr_auth_getpwent(p);
+  fail_unless(pw == NULL, "Found pwent unexpectedly");
+  fail_unless(getpwent_count == 0, "Expected call count 0, got %u",
+    getpwent_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getpwent;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  pw = pr_auth_getpwent(p);
+  fail_unless(pw != NULL, "Failed to find pwent: %s", strerror(errno));
+  fail_unless(getpwent_count == 1, "Expected call count 1, got %u",
+    getpwent_count);
+
+  pw = pr_auth_getpwent(p);
+  fail_unless(pw == NULL, "Failed to avoid pwent with bad UID");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+  fail_unless(getpwent_count == 2, "Expected call count 2, got %u",
+    getpwent_count);
+
+  pw = pr_auth_getpwent(p);
+  fail_unless(pw == NULL, "Failed to avoid pwent with bad GID");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+  fail_unless(getpwent_count == 3, "Expected call count 3, got %u",
+    getpwent_count);
+
+  pr_auth_endpwent(p);
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getpwnam_test) {
+  int res;
+  struct passwd *pw;
+  authtable authtab;
+  char *sym_name = "getpwnam";
+
+  pw = pr_auth_getpwnam(NULL, NULL);
+  fail_unless(pw == NULL, "Found pwnam unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  pw = pr_auth_getpwnam(p, PR_TEST_AUTH_NAME);
+  fail_unless(pw == NULL, "Found pwnam unexpectedly");
+  fail_unless(getpwnam_count == 0, "Expected call count 0, got %u",
+    getpwnam_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getpwnam;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  pw = pr_auth_getpwnam(p, PR_TEST_AUTH_NOBODY);
+  fail_unless(pw == NULL, "Found user '%s' unexpectedly", PR_TEST_AUTH_NOBODY);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pw = pr_auth_getpwnam(p, PR_TEST_AUTH_NOBODY2);
+  fail_unless(pw == NULL, "Found user '%s' unexpectedly", PR_TEST_AUTH_NOBODY2);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pw = pr_auth_getpwnam(p, PR_TEST_AUTH_NAME);
+  fail_unless(pw != NULL, "Failed to find user '%s': %s", PR_TEST_AUTH_NAME,
+    strerror(errno));
+  fail_unless(getpwnam_count == 3, "Expected call count 3, got %u",
+    getpwnam_count);
+
+  mark_point();
+
+  pw = pr_auth_getpwnam(p, "other");
+  fail_unless(pw == NULL, "Found pwnam for user 'other' unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+  fail_unless(getpwnam_count == 4, "Expected call count 4, got %u",
+    getpwnam_count);
+
+  pr_auth_endpwent(p);
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getpwuid_test) {
+  int res;
+  struct passwd *pw;
+  authtable authtab;
+  char *sym_name = "getpwuid";
+
+  pw = pr_auth_getpwuid(NULL, -1);
+  fail_unless(pw == NULL, "Found pwuid unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  pw = pr_auth_getpwuid(p, PR_TEST_AUTH_UID);
+  fail_unless(pw == NULL, "Found pwuid unexpectedly");
+  fail_unless(getpwuid_count == 0, "Expected call count 0, got %u",
+    getpwuid_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getpwuid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  pw = pr_auth_getpwuid(p, PR_TEST_AUTH_UID);
+  fail_unless(pw != NULL, "Failed to find pwuid: %s", strerror(errno));
+  fail_unless(getpwuid_count == 1, "Expected call count 1, got %u",
+    getpwuid_count);
+
+  pw = pr_auth_getpwuid(p, PR_TEST_AUTH_NOUID);
+  fail_unless(pw == NULL, "Found pwuid for NOUID unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pw = pr_auth_getpwuid(p, PR_TEST_AUTH_NOUID2);
+  fail_unless(pw == NULL, "Found pwuid for NOUID2 unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+
+  pw = pr_auth_getpwuid(p, 5);
+  fail_unless(pw == NULL, "Found pwuid for UID 5 unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fail_unless(getpwuid_count == 4, "Expected call count 4, got %u",
+    getpwuid_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_name2uid_test) {
+  int res;
+  uid_t uid;
+  authtable authtab;
+  char *sym_name = "name2uid";
+
+  pr_auth_cache_set(FALSE, PR_AUTH_CACHE_FL_BAD_NAME2UID);
+
+  uid = pr_auth_name2uid(NULL, NULL);
+  fail_unless(uid == (uid_t) -1, "Found UID unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  uid = pr_auth_name2uid(p, PR_TEST_AUTH_NAME);
+  fail_unless(uid == (uid_t) -1, "Found UID unexpectedly");
+  fail_unless(name2uid_count == 0, "Expected call count 0, got %u",
+    name2uid_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_name2uid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  uid = pr_auth_name2uid(p, PR_TEST_AUTH_NAME);
+  fail_unless(uid == PR_TEST_AUTH_UID, "Expected UID %lu, got %lu",
+    (unsigned long) PR_TEST_AUTH_UID, (unsigned long) uid);
+  fail_unless(name2uid_count == 1, "Expected call count 1, got %u",
+    name2uid_count);
+
+  mark_point();
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  uid = pr_auth_name2uid(p, PR_TEST_AUTH_NAME);
+  fail_unless(uid == PR_TEST_AUTH_UID, "Expected UID %lu, got %lu",
+    (unsigned long) PR_TEST_AUTH_UID, (unsigned long) uid);
+  fail_unless(name2uid_count == 1, "Expected call count 1, got %u",
+    name2uid_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_uid2name_test) {
+  int res;
+  const char *name; 
+  authtable authtab;
+  char *sym_name = "uid2name";
+
+  pr_auth_cache_set(FALSE, PR_AUTH_CACHE_FL_BAD_UID2NAME);
+
+  name = pr_auth_uid2name(NULL, -1);
+  fail_unless(name == NULL, "Found name unexpectedly: %s", name);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+  mark_point();
+
+  name = pr_auth_uid2name(p, PR_TEST_AUTH_UID);
+  fail_unless(name != NULL, "Failed to find name for UID %lu: %s",
+    (unsigned long) PR_TEST_AUTH_UID, strerror(errno));
+  fail_unless(strcmp(name, PR_TEST_AUTH_UID_STR) == 0,
+     "Expected name '%s', got '%s'", PR_TEST_AUTH_UID_STR, name);
+  fail_unless(uid2name_count == 0, "Expected call count 0, got %u",
+    uid2name_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_uid2name;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  name = pr_auth_uid2name(p, PR_TEST_AUTH_UID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_NAME) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_NAME, name);
+  fail_unless(uid2name_count == 1, "Expected call count 1, got %u",
+    uid2name_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_setgrent_test) {
+  int res;
+  authtable authtab;
+  char *sym_name = "setgrent";
+
+  pr_auth_setgrent(p);
+  fail_unless(setgrent_count == 0, "Expected call count 0, got %u",
+    setgrent_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_setgrent;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  pr_auth_setgrent(p);
+  fail_unless(setgrent_count == 1, "Expected call count 1, got %u",
+    setgrent_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_endgrent_test) {
+  int res;
+  authtable authtab;
+  char *sym_name = "endgrent";
+
+  pr_auth_endgrent(p);
+  fail_unless(endgrent_count == 0, "Expected call count 0, got %u",
+    endgrent_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_endgrent;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  pr_auth_endgrent(p);
+  fail_unless(endgrent_count == 1, "Expected call count 1, got %u",
+    endgrent_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getgrent_test) {
+  int res;
+  struct group *gr;
+  authtable authtab;
+  char *sym_name = "getgrent";
+
+  gr = pr_auth_getgrent(NULL);
+  fail_unless(gr == NULL, "Found grent unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  gr = pr_auth_getgrent(p);
+  fail_unless(gr == NULL, "Found grent unexpectedly");
+  fail_unless(getgrent_count == 0, "Expected call count 0, got %u",
+    getgrent_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getgrent;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  gr = pr_auth_getgrent(p);
+  fail_unless(gr != NULL, "Failed to find grent: %s", strerror(errno));
+  fail_unless(getgrent_count == 1, "Expected call count 1, got %u",
+    getgrent_count);
+
+  gr = pr_auth_getgrent(p);
+  fail_unless(gr == NULL, "Failed to avoid grent with bad GID");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+  fail_unless(getgrent_count == 2, "Expected call count 2, got %u",
+    getgrent_count);
+
+  pr_auth_endgrent(p);
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getgrnam_test) {
+  int res;
+  struct group *gr;
+  authtable authtab;
+  char *sym_name = "getgrnam";
+
+  gr = pr_auth_getgrnam(NULL, NULL);
+  fail_unless(gr == NULL, "Found grnam unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  gr = pr_auth_getgrnam(p, PR_TEST_AUTH_NAME);
+  fail_unless(gr == NULL, "Found grnam unexpectedly");
+  fail_unless(getgrnam_count == 0, "Expected call count 0, got %u",
+    getgrnam_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getgrnam;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  gr = pr_auth_getgrnam(p, PR_TEST_AUTH_NOGROUP);
+  fail_unless(gr == NULL, "Found group '%s' unexpectedly",
+    PR_TEST_AUTH_NOGROUP);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  gr = pr_auth_getgrnam(p, PR_TEST_AUTH_NAME);
+  fail_unless(gr != NULL, "Failed to find grnam: %s", strerror(errno));
+  fail_unless(getgrnam_count == 2, "Expected call count 2, got %u",
+    getgrnam_count);
+
+  mark_point();
+
+  gr = pr_auth_getgrnam(p, "other");
+  fail_unless(gr == NULL, "Found grnam for user 'other' unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+  fail_unless(getgrnam_count == 3, "Expected call count 3, got %u",
+    getgrnam_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getgrgid_test) {
+  int res;
+  struct group *gr;
+  authtable authtab;
+  char *sym_name = "getgrgid";
+
+  gr = pr_auth_getgrgid(NULL, -1);
+  fail_unless(gr == NULL, "Found grgid unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  gr = pr_auth_getgrgid(p, PR_TEST_AUTH_GID);
+  fail_unless(gr == NULL, "Found grgid unexpectedly");
+  fail_unless(getgrgid_count == 0, "Expected call count 0, got %u",
+    getgrgid_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getgrgid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  gr = pr_auth_getgrgid(p, PR_TEST_AUTH_GID);
+  fail_unless(gr != NULL, "Failed to find grgid: %s", strerror(errno));
+  fail_unless(getgrgid_count == 1, "Expected call count 1, got %u",
+    getgrgid_count);
+
+  gr = pr_auth_getgrgid(p, PR_TEST_AUTH_NOGID);
+  fail_unless(gr == NULL, "Found grgid for NOGID unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+
+  gr = pr_auth_getgrgid(p, 5);
+  fail_unless(gr == NULL, "Found grgid for GID 5 unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fail_unless(getgrgid_count == 3, "Expected call count 3, got %u",
+    getgrgid_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_name2gid_test) {
+  int res;
+  gid_t gid;
+  authtable authtab;
+  char *sym_name = "name2gid";
+
+  pr_auth_cache_set(FALSE, PR_AUTH_CACHE_FL_BAD_NAME2GID);
+
+  gid = pr_auth_name2gid(NULL, NULL);
+  fail_unless(gid == (gid_t) -1, "Found GID unexpectedly");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  gid = pr_auth_name2gid(p, PR_TEST_AUTH_NAME);
+  fail_unless(gid == (gid_t) -1, "Found GID unexpectedly");
+  fail_unless(name2gid_count == 0, "Expected call count 0, got %u",
+    name2gid_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_name2gid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  gid = pr_auth_name2gid(p, PR_TEST_AUTH_NAME);
+  fail_unless(gid == PR_TEST_AUTH_GID, "Expected GID %lu, got %lu",
+    (unsigned long) PR_TEST_AUTH_GID, (unsigned long) gid);
+  fail_unless(name2gid_count == 1, "Expected call count 1, got %u",
+    name2gid_count);
+
+  mark_point();
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  gid = pr_auth_name2gid(p, PR_TEST_AUTH_NAME);
+  fail_unless(gid == PR_TEST_AUTH_GID, "Expected GID %lu, got %lu",
+    (unsigned long) PR_TEST_AUTH_GID, (unsigned long) gid);
+  fail_unless(name2gid_count == 1, "Expected call count 1, got %u",
+    name2gid_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_gid2name_test) {
+  int res;
+  const char *name; 
+  authtable authtab;
+  char *sym_name = "gid2name";
+
+  pr_auth_cache_set(FALSE, PR_AUTH_CACHE_FL_BAD_GID2NAME);
+
+  name = pr_auth_gid2name(NULL, -1);
+  fail_unless(name == NULL, "Found name unexpectedly: %s", name);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+  mark_point();
+
+  name = pr_auth_gid2name(p, PR_TEST_AUTH_GID);
+  fail_unless(name != NULL, "Failed to find name for GID %lu: %s",
+    (unsigned long) PR_TEST_AUTH_GID, strerror(errno));
+  fail_unless(strcmp(name, PR_TEST_AUTH_GID_STR) == 0,
+     "Expected name '%s', got '%s'", PR_TEST_AUTH_GID_STR, name);
+  fail_unless(gid2name_count == 0, "Expected call count 0, got %u",
+    gid2name_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_gid2name;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  name = pr_auth_gid2name(p, PR_TEST_AUTH_GID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_NAME) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_NAME, name);
+  fail_unless(gid2name_count == 1, "Expected call count 1, got %u",
+    gid2name_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_getgroups_test) {
+  int res;
+  array_header *gids = NULL, *names = NULL;
+  authtable authtab;
+  char *sym_name = "getgroups";
+
+  res = pr_auth_getgroups(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_auth_getgroups(p, PR_TEST_AUTH_NAME, &gids, NULL);
+  fail_unless(res < 0, "Found groups for '%s' unexpectedly", PR_TEST_AUTH_NAME);
+  fail_unless(getgroups_count == 0, "Expected call count 0, got %u",
+    getgroups_count);
+  mark_point();
+  
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_getgroups;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  res = pr_auth_getgroups(p, PR_TEST_AUTH_NAME, &gids, &names);
+  fail_unless(res > 0, "Expected group count 1 for '%s', got %d: %s",
+    PR_TEST_AUTH_NAME, res, strerror(errno));
+  fail_unless(getgroups_count == 1, "Expected call count 1, got %u",
+    getgroups_count);
+
+  res = pr_auth_getgroups(p, "other", &gids, &names);
+  fail_unless(res < 0, "Found groups for 'other' unexpectedly");
+  fail_unless(getgroups_count == 2, "Expected call count 2, got %u",
+    getgroups_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_uid2name_test) {
+  int res;
+  const char *name; 
+  authtable authtab;
+  char *sym_name = "uid2name";
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_uid2name;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  name = pr_auth_uid2name(p, PR_TEST_AUTH_UID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_NAME) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_NAME, name);
+  fail_unless(uid2name_count == 1, "Expected call count 1, got %u",
+    uid2name_count);
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  name = pr_auth_uid2name(p, PR_TEST_AUTH_UID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_NAME) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_NAME, name);
+  fail_unless(uid2name_count == 1, "Expected call count 1, got %u",
+    uid2name_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_gid2name_test) {
+  int res;
+  const char *name; 
+  authtable authtab;
+  char *sym_name = "gid2name";
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_gid2name;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  name = pr_auth_gid2name(p, PR_TEST_AUTH_GID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_NAME) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_NAME, name);
+  fail_unless(gid2name_count == 1, "Expected call count 1, got %u",
+    gid2name_count);
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  name = pr_auth_gid2name(p, PR_TEST_AUTH_GID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_NAME) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_NAME, name);
+  fail_unless(gid2name_count == 1, "Expected call count 1, got %u",
+    gid2name_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_uid2name_failed_test) {
+  int res;
+  const char *name; 
+  authtable authtab;
+  char *sym_name = "uid2name";
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = decline_uid2name;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  name = pr_auth_uid2name(p, PR_TEST_AUTH_UID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_UID_STR) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_UID_STR, name);
+  fail_unless(uid2name_count == 1, "Expected call count 1, got %u",
+    uid2name_count);
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  name = pr_auth_uid2name(p, PR_TEST_AUTH_UID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_UID_STR) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_UID_STR, name);
+  fail_unless(uid2name_count == 1, "Expected call count 1, got %u",
+    uid2name_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_gid2name_failed_test) {
+  int res;
+  const char *name; 
+  authtable authtab;
+  char *sym_name = "gid2name";
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = decline_gid2name;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  name = pr_auth_gid2name(p, PR_TEST_AUTH_GID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_GID_STR) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_GID_STR, name);
+  fail_unless(gid2name_count == 1, "Expected call count 1, got %u",
+    gid2name_count);
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  name = pr_auth_gid2name(p, PR_TEST_AUTH_GID);
+  fail_unless(name != NULL, "Expected name, got null");
+  fail_unless(strcmp(name, PR_TEST_AUTH_GID_STR) == 0,
+    "Expected name '%s', got '%s'", PR_TEST_AUTH_GID_STR, name);
+  fail_unless(gid2name_count == 1, "Expected call count 1, got %u",
+    gid2name_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_name2uid_failed_test) {
+  int res;
+  uid_t uid;
+  authtable authtab;
+  char *sym_name = "name2uid";
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = decline_name2uid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  uid = pr_auth_name2uid(p, PR_TEST_AUTH_NAME);
+  fail_unless(uid == (uid_t) -1, "Expected -1, got %lu", (unsigned long) uid);
+  fail_unless(name2uid_count == 1, "Expected call count 1, got %u",
+    name2uid_count);
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  uid = pr_auth_name2uid(p, PR_TEST_AUTH_NAME);
+  fail_unless(uid == (uid_t) -1, "Expected -1, got %lu", (unsigned long) uid);
+  fail_unless(name2uid_count == 1, "Expected call count 1, got %u",
+    name2uid_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_name2gid_failed_test) {
+  int res;
+  gid_t gid;
+  authtable authtab;
+  char *sym_name = "name2gid";
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = decline_name2gid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+
+  gid = pr_auth_name2gid(p, PR_TEST_AUTH_NAME);
+  fail_unless(gid == (gid_t) -1, "Expected -1, got %lu", (unsigned long) gid);
+  fail_unless(name2gid_count == 1, "Expected call count 1, got %u",
+    name2gid_count);
+
+  /* Call again; the call counter should NOT increment due to caching. */
+
+  gid = pr_auth_name2gid(p, PR_TEST_AUTH_NAME);
+  fail_unless(gid == (gid_t) -1, "Expected -1, got %lu", (unsigned long) gid);
+  fail_unless(name2gid_count == 1, "Expected call count 1, got %u",
+    name2gid_count);
+
+  pr_stash_remove_symbol(PR_SYM_AUTH, sym_name, &testsuite_module);
+}
+END_TEST
+
+START_TEST (auth_cache_clear_test) {
+  int res;
+  gid_t gid;
+  authtable authtab;
+  char *sym_name = "name2gid";
+
+  mark_point();
+  pr_auth_cache_clear();
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = decline_name2gid;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  mark_point();
+  gid = pr_auth_name2gid(p, PR_TEST_AUTH_NAME);
+  fail_unless(gid == (gid_t) -1, "Expected -1, got %lu", (unsigned long) gid);
+  fail_unless(name2gid_count == 1, "Expected call count 1, got %u",
+    name2gid_count);
+
+  mark_point();
+  pr_auth_cache_clear();
+}
+END_TEST
+
+START_TEST (auth_cache_set_test) {
+  int res;
+  unsigned int flags = PR_AUTH_CACHE_FL_UID2NAME|PR_AUTH_CACHE_FL_GID2NAME|PR_AUTH_CACHE_FL_AUTH_MODULE|PR_AUTH_CACHE_FL_NAME2UID|PR_AUTH_CACHE_FL_NAME2GID|PR_AUTH_CACHE_FL_BAD_UID2NAME|PR_AUTH_CACHE_FL_BAD_GID2NAME|PR_AUTH_CACHE_FL_BAD_NAME2UID|PR_AUTH_CACHE_FL_BAD_NAME2GID;
+
+  res = pr_auth_cache_set(-1, 0);
+  fail_unless(res < 0, "Failed to handle invalid setting");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_cache_set(TRUE, flags);
+  fail_unless(res == 0, "Failed to enable all auth cache settings: %s",
+    strerror(errno));
+
+  res = pr_auth_cache_set(FALSE, flags);
+  fail_unless(res == 0, "Failed to disable all auth cache settings: %s",
+    strerror(errno));
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_DEFAULT);
+}
+END_TEST
+
+START_TEST (auth_clear_auth_only_module_test) {
+  int res;
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_AUTH_MODULE);
+
+  res = pr_auth_clear_auth_only_modules();
+  fail_unless(res < 0, "Failed to handle no auth module list");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (auth_add_auth_only_module_test) {
+  int res;
+  const char *name = "foo.bar";
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_AUTH_MODULE);
+
+  res = pr_auth_add_auth_only_module(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_add_auth_only_module(name);
+  fail_unless(res == 0, "Failed to add auth-only module '%s': %s", name,
+    strerror(errno));
+
+  res = pr_auth_add_auth_only_module(name);
+  fail_unless(res < 0, "Failed to handle duplicate auth-only module");
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  res = pr_auth_clear_auth_only_modules();
+  fail_unless(res == 0, "Failed to clear auth-only modules: %s",
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (auth_remove_auth_only_module_test) {
+  int res;
+  const char *name = "foo.bar";
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_AUTH_MODULE);
+
+  res = pr_auth_remove_auth_only_module(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_remove_auth_only_module(name);
+  fail_unless(res < 0, "Failed to handle empty auth-only module list");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  res = pr_auth_add_auth_only_module(name);
+  fail_unless(res == 0, "Failed to add auth-only module '%s': %s", name,
+    strerror(errno));
+
+  res = pr_auth_remove_auth_only_module(name);
+  fail_unless(res == 0, "Failed to remove auth-only module '%s': %s", name,
+    strerror(errno));
+
+  (void) pr_auth_clear_auth_only_modules();
+}
+END_TEST
+
+START_TEST (auth_authenticate_test) {
+  int res;
+  authtable authtab;
+  char *sym_name = "auth";
+
+  res = pr_auth_authenticate(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_authenticate(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_authenticate(p, PR_TEST_AUTH_NAME, NULL);
+  fail_unless(res < 0, "Failed to handle null password");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_authn;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  res = pr_auth_authenticate(p, "other", "foobar");
+  fail_unless(res == PR_AUTH_NOPWD,
+    "Authenticated user 'other' unexpectedly (expected %d, got %d)",
+    PR_AUTH_NOPWD, res);
+
+  res = pr_auth_authenticate(p, PR_TEST_AUTH_NAME, "foobar");
+  fail_unless(res == PR_AUTH_BADPWD,
+    "Authenticated user '%s' unexpectedly (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_BADPWD, res);
+
+  res = pr_auth_authenticate(p, PR_TEST_AUTH_NAME, PR_TEST_AUTH_PASSWD);
+  fail_unless(res == PR_AUTH_OK,
+    "Failed to authenticate user '%s' (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_OK, res);
+
+  authtab.auth_flags |= PR_AUTH_FL_REQUIRED;
+  res = pr_auth_authenticate(p, PR_TEST_AUTH_NAME, PR_TEST_AUTH_PASSWD);
+  fail_unless(res == PR_AUTH_OK,
+    "Failed to authenticate user '%s' (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_OK, res);
+  authtab.auth_flags &= ~PR_AUTH_FL_REQUIRED;
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_AUTH_MODULE);
+
+  res = pr_auth_add_auth_only_module("foo.bar");
+  fail_unless(res == 0, "Failed to add auth-only module: %s", strerror(errno));
+
+  res = pr_auth_add_auth_only_module(testsuite_module.name);
+  fail_unless(res == 0, "Failed to add auth-only module: %s", strerror(errno));
+
+  res = pr_auth_authenticate(p, PR_TEST_AUTH_NAME, PR_TEST_AUTH_PASSWD);
+  fail_unless(res == PR_AUTH_OK,
+    "Failed to authenticate user '%s' (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_OK, res);
+
+  pr_auth_clear_auth_only_modules();
+
+  authn_rfc2228 = TRUE;
+  res = pr_auth_authenticate(p, PR_TEST_AUTH_NAME, PR_TEST_AUTH_PASSWD);
+  fail_unless(res == PR_AUTH_RFC2228_OK,
+    "Failed to authenticate user '%s' (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_RFC2228_OK, res);
+}
+END_TEST
+
+START_TEST (auth_authorize_test) {
+  int res;
+  authtable authtab;
+  char *sym_name = "authorize";
+
+  res = pr_auth_authorize(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_authorize(p, NULL);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_authorize(p, PR_TEST_AUTH_NAME);
+  fail_unless(res > 0, "Failed to handle missing handler");
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_authz;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  res = pr_auth_authorize(p, "other");
+  fail_unless(res == PR_AUTH_NOPWD,
+    "Authorized user 'other' unexpectedly (expected %d, got %d)",
+    PR_AUTH_NOPWD, res);
+
+  res = pr_auth_authorize(p, PR_TEST_AUTH_NAME);
+  fail_unless(res == PR_AUTH_OK,
+    "Failed to authorize user '%s' (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_OK, res);
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_AUTH_MODULE);
+
+  res = pr_auth_add_auth_only_module("foo.bar");
+  fail_unless(res == 0, "Failed to add auth-only module: %s", strerror(errno));
+
+  res = pr_auth_add_auth_only_module(testsuite_module.name);
+  fail_unless(res == 0, "Failed to add auth-only module: %s", strerror(errno));
+
+  res = pr_auth_authorize(p, PR_TEST_AUTH_NAME);
+  fail_unless(res == PR_AUTH_OK,
+    "Failed to authorize user '%s' (expected %d, got %d)",
+    PR_TEST_AUTH_NAME, PR_AUTH_OK, res);
+
+  (void) pr_auth_clear_auth_only_modules();
+}
+END_TEST
+
+START_TEST (auth_check_test) {
+  int res;
+  const char *cleartext_passwd, *ciphertext_passwd, *name;
+  authtable authtab;
+  char *sym_name = "check";
+
+  res = pr_auth_check(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_check(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = PR_TEST_AUTH_NAME;
+  res = pr_auth_check(p, NULL, name, NULL);
+  fail_unless(res < 0, "Failed to handle null cleartext password");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  cleartext_passwd = PR_TEST_AUTH_PASSWD;
+  res = pr_auth_check(p, NULL, name, cleartext_passwd);
+  fail_unless(res == PR_AUTH_BADPWD, "Expected %d, got %d", PR_AUTH_BADPWD,
+    res);
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_check;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  res = pr_auth_check(p, NULL, "other", cleartext_passwd);
+  fail_unless(res == PR_AUTH_BADPWD, "Expected %d, got %d", PR_AUTH_BADPWD,
+    res);
+
+  res = pr_auth_check(p, "foo", name, cleartext_passwd);
+  fail_unless(res == PR_AUTH_BADPWD, "Expected %d, got %d", PR_AUTH_BADPWD,
+    res);
+
+  res = pr_auth_check(p, NULL, name, cleartext_passwd);
+  fail_unless(res == PR_AUTH_BADPWD, "Expected %d, got %d", PR_AUTH_BADPWD,
+    res);
+
+  ciphertext_passwd = PR_TEST_AUTH_PASSWD;
+  res = pr_auth_check(p, ciphertext_passwd, name, cleartext_passwd);
+  fail_unless(res == PR_AUTH_OK, "Expected %d, got %d", PR_AUTH_OK, res);
+
+  (void) pr_auth_cache_set(TRUE, PR_AUTH_CACHE_FL_AUTH_MODULE);
+
+  res = pr_auth_add_auth_only_module("foo.bar");
+  fail_unless(res == 0, "Failed to add auth-only module: %s", strerror(errno));
+
+  res = pr_auth_add_auth_only_module(testsuite_module.name);
+  fail_unless(res == 0, "Failed to add auth-only module: %s", strerror(errno));
+
+  check_rfc2228 = TRUE;
+  res = pr_auth_check(p, ciphertext_passwd, name, cleartext_passwd);
+  fail_unless(res == PR_AUTH_RFC2228_OK,
+    "Failed to check user '%s' (expected %d, got %d)", name,
+    PR_AUTH_RFC2228_OK, res);
+
+  (void) pr_auth_clear_auth_only_modules();
+}
+END_TEST
+
+START_TEST (auth_requires_pass_test) {
+  int res;
+  const char *name;
+  authtable authtab;
+  char *sym_name = "requires_pass";
+
+  res = pr_auth_requires_pass(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_requires_pass(p, NULL);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "other";
+  res = pr_auth_requires_pass(p, name);
+  fail_unless(res == TRUE, "Unknown users should require passwords (got %d)",
+    res);
+
+  /* Load the appropriate AUTH symbol, and call it. */
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = sym_name;
+  authtab.handler = handle_requires_pass;
+  authtab.m = &testsuite_module;
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add '%s' AUTH symbol: %s", sym_name,
+    strerror(errno));
+
+  res = pr_auth_requires_pass(p, name);
+  fail_unless(res == TRUE, "Unknown users should require passwords (got %d)",
+    res);
+
+  name = PR_TEST_AUTH_NAME;
+  res = pr_auth_requires_pass(p, name);
+  fail_unless(res == FALSE, "Known users should NOT require passwords (got %d)",
+    res);
+}
+END_TEST
+
+START_TEST (auth_get_anon_config_test) {
+  config_rec *c;
+
+  c = pr_auth_get_anon_config(NULL, NULL, NULL, NULL);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+
+  /* XXX Need to exercise more of this function. */
+}
+END_TEST
+
+START_TEST (auth_chroot_test) {
+  int res;
+  const char *path;
+
+  res = pr_auth_chroot(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "tmp";
+  res = pr_auth_chroot(path);
+  fail_unless(res < 0, "Failed to chroot to '%s': %s", path, strerror(errno));
+  fail_unless(errno == EINVAL || errno == ENOENT,
+    "Expected EINVAL (%d) or ENOENT (%d), got %s (%d)", EINVAL, ENOENT,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_auth_chroot(path);
+  fail_unless(res < 0, "Failed to chroot to '%s': %s", path, strerror(errno));
+  fail_unless(errno == ENOENT || errno == EPERM || errno == EINVAL,
+    "Expected ENOENT (%d), EPERM (%d) or EINVAL (%d), got %s (%d)",
+    ENOENT, EPERM, EINVAL, strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (auth_banned_by_ftpusers_test) {
+  const char *name;
+  int res;
+  xaset_t *ctx;
+
+  res = pr_auth_banned_by_ftpusers(NULL, NULL);
+  fail_unless(res == FALSE, "Failed to handle null arguments");
+
+  ctx = xaset_create(p, NULL);
+  res = pr_auth_banned_by_ftpusers(ctx, NULL);
+  fail_unless(res == FALSE, "Failed to handle null user");
+
+  name = "testsuite";
+  res = pr_auth_banned_by_ftpusers(ctx, name);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+}
+END_TEST
+
+START_TEST (auth_is_valid_shell_test) {
+  const char *shell;
+  int res;
+  xaset_t *ctx;
+
+  res = pr_auth_is_valid_shell(NULL, NULL);
+  fail_unless(res == TRUE, "Failed to handle null arguments");
+
+  ctx = xaset_create(p, NULL);
+  res = pr_auth_is_valid_shell(ctx, NULL);
+  fail_unless(res == TRUE, "Failed to handle null shell");
+
+  shell = "/foo/bar";
+  res = pr_auth_is_valid_shell(ctx, shell);
+  fail_unless(res == FALSE, "Failed to handle invalid shell (got %d)", res);
+
+  shell = "/bin/bash";
+  res = pr_auth_is_valid_shell(ctx, shell);
+  fail_unless(res == TRUE, "Failed to handle valid shell (got %d)", res);
+}
+END_TEST
+
+START_TEST (auth_get_home_test) {
+  const char *home, *res;
+
+  res = pr_auth_get_home(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_auth_get_home(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null home");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  home = "/testsuite";
+  res = pr_auth_get_home(p, home);
+  fail_unless(res != NULL, "Failed to get home: %s", strerror(errno));
+  fail_unless(strcmp(home, res) == 0, "Expected '%s', got '%s'", home, res);  
+}
+END_TEST
+
+START_TEST (auth_set_max_password_len_test) {
+  int checked;
+  size_t res;
+
+  res = pr_auth_set_max_password_len(p, 1);
+  fail_unless(res == PR_TUNABLE_PASSWORD_MAX,
+    "Expected %lu, got %lu", (unsigned long) PR_TUNABLE_PASSWORD_MAX,
+    (unsigned long) res);
+
+  checked = pr_auth_check(p, NULL, PR_TEST_AUTH_NAME, PR_TEST_AUTH_PASSWD);
+  fail_unless(checked < 0, "Failed to reject too-long password");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  res = pr_auth_set_max_password_len(p, 0);
+  fail_unless(res == 1, "Expected %lu, got %lu", 1, (unsigned long) res);
+
+  res = pr_auth_set_max_password_len(p, 0);
+  fail_unless(res == PR_TUNABLE_PASSWORD_MAX,
+    "Expected %lu, got %lu", (unsigned long) PR_TUNABLE_PASSWORD_MAX,
+    (unsigned long) res);
+}
+END_TEST
+
+Suite *tests_get_auth_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("auth");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  /* pwent* et al */
+  tcase_add_test(testcase, auth_setpwent_test);
+  tcase_add_test(testcase, auth_endpwent_test);
+  tcase_add_test(testcase, auth_getpwent_test);
+  tcase_add_test(testcase, auth_getpwnam_test);
+  tcase_add_test(testcase, auth_getpwuid_test);
+  tcase_add_test(testcase, auth_name2uid_test);
+  tcase_add_test(testcase, auth_uid2name_test);
+
+  /* grent* et al */
+  tcase_add_test(testcase, auth_setgrent_test);
+  tcase_add_test(testcase, auth_endgrent_test);
+  tcase_add_test(testcase, auth_getgrent_test);
+  tcase_add_test(testcase, auth_getgrnam_test);
+  tcase_add_test(testcase, auth_getgrgid_test);
+  tcase_add_test(testcase, auth_gid2name_test);
+  tcase_add_test(testcase, auth_name2gid_test);
+  tcase_add_test(testcase, auth_getgroups_test);
+
+  /* Caching tests */
+  tcase_add_test(testcase, auth_cache_uid2name_test);
+  tcase_add_test(testcase, auth_cache_gid2name_test);
+  tcase_add_test(testcase, auth_cache_uid2name_failed_test);
+  tcase_add_test(testcase, auth_cache_gid2name_failed_test);
+  tcase_add_test(testcase, auth_cache_name2uid_failed_test);
+  tcase_add_test(testcase, auth_cache_name2gid_failed_test);
+  tcase_add_test(testcase, auth_cache_clear_test);
+  tcase_add_test(testcase, auth_cache_set_test);
+
+  /* Auth modules */
+  tcase_add_test(testcase, auth_clear_auth_only_module_test);
+  tcase_add_test(testcase, auth_add_auth_only_module_test);
+  tcase_add_test(testcase, auth_remove_auth_only_module_test);
+
+  /* Authorization */
+  tcase_add_test(testcase, auth_authenticate_test);
+  tcase_add_test(testcase, auth_authorize_test);
+  tcase_add_test(testcase, auth_check_test);
+  tcase_add_test(testcase, auth_requires_pass_test);
+
+  /* Misc */
+  tcase_add_test(testcase, auth_get_anon_config_test);
+  tcase_add_test(testcase, auth_chroot_test);
+  tcase_add_test(testcase, auth_banned_by_ftpusers_test);
+  tcase_add_test(testcase, auth_is_valid_shell_test);
+  tcase_add_test(testcase, auth_get_home_test);
+  tcase_add_test(testcase, auth_set_max_password_len_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/class.c b/tests/api/class.c
index fbdae7f..70883c7 100644
--- a/tests/api/class.c
+++ b/tests/api/class.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,10 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/*
- * Class API tests
- * $Id: class.c,v 1.2 2011-05-23 20:50:30 castaglia Exp $
- */
+/* Class API tests */
 
 #include "tests.h"
 
@@ -103,6 +100,32 @@ START_TEST (class_add_acl_test) {
 }
 END_TEST
 
+START_TEST (class_add_note_test) {
+  const char *k = NULL;
+  void *v = NULL;
+  size_t vsz = 0;
+  int res;
+
+  res = pr_class_add_note(k, v, vsz);
+  fail_unless(res == -1, "Failed to handle NULL argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  k = "KEY";
+  v = "VALUE";
+  vsz = 6;
+
+  res = pr_class_add_note(k, v, vsz);
+  fail_unless(res == -1, "Failed to handle unopened class");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM");
+
+  res = pr_class_open(p, "foo");
+  fail_unless(res == 0, "Failed to open class: %s", strerror(errno));
+
+  res = pr_class_add_note(k, v, vsz);
+  fail_unless(res == 0, "Failed to add note: %s", strerror(errno));
+}
+END_TEST
+
 START_TEST (class_close_test) {
   pr_netacl_t *acl;
   int res;
@@ -160,7 +183,7 @@ START_TEST (class_set_satisfy_test) {
 END_TEST
 
 START_TEST (class_get_test) {
-  pr_class_t *class;
+  const pr_class_t *class;
   int res;
   pr_netacl_t *acl;
 
@@ -195,7 +218,7 @@ START_TEST (class_get_test) {
 END_TEST
 
 START_TEST (class_find_test) {
-  pr_class_t *class;
+  const pr_class_t *class;
   pr_netacl_t *acl;
   int res;
 
@@ -235,9 +258,9 @@ START_TEST (class_find_test) {
 END_TEST
 
 START_TEST (class_match_addr_test) {
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
+  const pr_class_t *class;
   pr_netacl_t *acl;
-  pr_class_t *class;
   int res;
 
   class = pr_class_match_addr(NULL);
@@ -449,6 +472,7 @@ Suite *tests_get_class_suite(void) {
 
   tcase_add_test(testcase, class_open_test);
   tcase_add_test(testcase, class_add_acl_test);
+  tcase_add_test(testcase, class_add_note_test);
   tcase_add_test(testcase, class_close_test);
   tcase_add_test(testcase, class_set_satisfy_test);
   tcase_add_test(testcase, class_get_test);
diff --git a/tests/api/cmd.c b/tests/api/cmd.c
index 6600d6e..30d4429 100644
--- a/tests/api/cmd.c
+++ b/tests/api/cmd.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server API testsuite
- * Copyright (c) 2011-2014 The ProFTPD Project team
+ * Copyright (c) 2011-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Command API tests
- * $Id: cmd.c,v 1.4 2014-01-27 18:25:15 castaglia Exp $
- */
+/* Command API tests */
 
 #include "tests.h"
 
@@ -264,6 +262,15 @@ START_TEST (cmd_get_id_test) {
   fail_unless(res == PR_CMD_LANG_ID, "Expected cmd ID %d for %s, got %d",
     PR_CMD_LANG_ID, C_LANG, res); 
 
+  res = pr_cmd_get_id(C_HOST);
+  fail_unless(res == PR_CMD_HOST_ID, "Expected cmd ID %d for %s, got %d",
+    PR_CMD_HOST_ID, C_HOST, res); 
+
+  res = pr_cmd_get_id(C_CLNT);
+  fail_unless(res == PR_CMD_CLNT_ID, "Expected cmd ID %d for %s, got %d",
+    PR_CMD_CLNT_ID, C_CLNT, res); 
+
+  /* RFC 2228 commands */
   res = pr_cmd_get_id(C_ADAT);
   fail_unless(res == PR_CMD_ADAT_ID, "Expected cmd ID %d for %s, got %d",
     PR_CMD_ADAT_ID, C_ADAT, res); 
@@ -357,8 +364,9 @@ START_TEST (cmd_strcmp_test) {
 END_TEST
 
 START_TEST (cmd_get_displayable_str_test) {
-  char *ok, *res = NULL;
+  const char *ok, *res = NULL;
   cmd_rec *cmd = NULL;
+  size_t len = 0;
 
   res = pr_cmd_get_displayable_str(NULL, NULL);
   fail_unless(res == NULL, "Failed to handle null cmd_rec");
@@ -383,6 +391,11 @@ START_TEST (cmd_get_displayable_str_test) {
    */
   fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
 
+  if (pr_cmd_clear_cache(NULL) < 0) {
+    fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+      strerror(errno), errno);
+  }
+
   mark_point();
   pr_cmd_clear_cache(cmd);
   res = pr_cmd_get_displayable_str(cmd, NULL);
@@ -438,6 +451,172 @@ START_TEST (cmd_get_displayable_str_test) {
   ok = " bar baz";
   fail_if(res == NULL, "Expected string, got null");
   fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 2, C_PASS, "foo");
+  res = pr_cmd_get_displayable_str(cmd, &len);
+  ok = "PASS (hidden)";
+  fail_unless(res != NULL, "Expected displayable string, got null");
+  fail_unless(len == 13, "Expected len 13, got %lu", (unsigned long) len);
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 2, C_ADAT, "bar baz quxx");
+  res = pr_cmd_get_displayable_str(cmd, &len);
+  ok = "ADAT (hidden)";
+  fail_unless(res != NULL, "Expected displayable string, got null");
+  fail_unless(len == 13, "Expected len 13, got %lu", (unsigned long) len);
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+}
+END_TEST
+
+START_TEST (cmd_get_errno_test) {
+  int res, *xerrno = NULL;
+  cmd_rec *cmd = NULL;
+
+  res = pr_cmd_get_errno(NULL);
+  fail_unless(res == -1, "Failed to handle null cmd_rec");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  cmd = pr_cmd_alloc(p, 1, "foo");
+  res = pr_cmd_get_errno(cmd);
+  fail_unless(res == 0, "Expected errno 0, got %d", res);
+
+  (void) pr_table_remove(cmd->notes, "errno", NULL);
+  res = pr_cmd_get_errno(cmd);
+  fail_unless(res < 0, "Failed to handle missing 'errno' note");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  xerrno = pcalloc(cmd->pool, sizeof(int));
+  (void) pr_table_add(cmd->notes, "errno", xerrno, sizeof(int));
+
+  res = pr_cmd_set_errno(NULL, ENOENT);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_cmd_set_errno(cmd, ENOENT);
+  fail_unless(res == 0, "Failed to stash errno ENOENT: %s", strerror(errno));
+
+  res = pr_cmd_get_errno(cmd);
+  fail_unless(res == ENOENT, "Expected errno ENOENT, got %s (%d)",
+    strerror(res), res);
+}
+END_TEST
+
+START_TEST (cmd_set_name_test) {
+  int res;
+  cmd_rec *cmd;
+  const char *name;
+
+  res = pr_cmd_set_name(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  cmd = pr_cmd_alloc(p, 1, "foo");
+  res = pr_cmd_set_name(cmd, NULL);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "bar";
+  res = pr_cmd_set_name(cmd, name);
+  fail_unless(res == 0, "Failed to command name to '%s': %s", name,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (cmd_is_http_test) {
+  int res;
+  cmd_rec *cmd;
+
+  res = pr_cmd_is_http(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, C_SYST);
+  cmd->argv[0] = NULL;
+  res = pr_cmd_is_http(cmd);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd->argv[0] = C_SYST;
+  res = pr_cmd_is_http(cmd);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, "GET");
+  res = pr_cmd_is_http(cmd);
+  fail_unless(res == TRUE, "Expected TRUE (%d), got %d", TRUE, res);
+}
+END_TEST
+
+START_TEST (cmd_is_smtp_test) {
+  int res;
+  cmd_rec *cmd;
+
+  res = pr_cmd_is_smtp(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, C_SYST);
+  cmd->argv[0] = NULL;
+  res = pr_cmd_is_smtp(cmd);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd->argv[0] = C_SYST;
+  res = pr_cmd_is_smtp(cmd);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, "RCPT");
+  res = pr_cmd_is_smtp(cmd);
+  fail_unless(res == TRUE, "Expected TRUE (%d), got %d", TRUE, res);
+}
+END_TEST
+
+START_TEST (cmd_is_ssh2_test) {
+  int res;
+  cmd_rec *cmd;
+
+  res = pr_cmd_is_ssh2(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, C_SYST);
+  cmd->argv[0] = NULL;
+  res = pr_cmd_is_ssh2(cmd);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd->argv[0] = C_SYST;
+  res = pr_cmd_is_ssh2(cmd);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, "SSH-2.0-OpenSSH_5.6p1");
+  res = pr_cmd_is_ssh2(cmd);
+  fail_unless(res == TRUE, "Expected TRUE (%d), got %d", TRUE, res);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, "SSH-1.99-JSCH");
+  res = pr_cmd_is_ssh2(cmd);
+  fail_unless(res == TRUE, "Expected TRUE (%d), got %d", TRUE, res);
 }
 END_TEST
 
@@ -448,7 +627,6 @@ Suite *tests_get_cmd_suite(void) {
   suite = suite_create("cmd");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, cmd_alloc_test);
@@ -456,8 +634,12 @@ Suite *tests_get_cmd_suite(void) {
   tcase_add_test(testcase, cmd_cmp_test);
   tcase_add_test(testcase, cmd_strcmp_test);
   tcase_add_test(testcase, cmd_get_displayable_str_test);
+  tcase_add_test(testcase, cmd_get_errno_test);
+  tcase_add_test(testcase, cmd_set_name_test);
+  tcase_add_test(testcase, cmd_is_http_test);
+  tcase_add_test(testcase, cmd_is_smtp_test);
+  tcase_add_test(testcase, cmd_is_ssh2_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/configdb.c b/tests/api/configdb.c
new file mode 100644
index 0000000..04e9b80
--- /dev/null
+++ b/tests/api/configdb.c
@@ -0,0 +1,642 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Configuration API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_config();
+  pr_parser_prepare(p, NULL);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("config", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("config", 0, 0);
+  }
+
+  pr_parser_cleanup();
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  } 
+}
+
+START_TEST (config_init_config_test) {
+  mark_point();
+  init_config();
+
+  mark_point();
+  init_config();
+
+  mark_point();
+  init_config();
+}
+END_TEST
+
+START_TEST (config_add_config_test) {
+  int res;
+  const char *name = NULL;
+  config_rec *c = NULL;
+  server_rec *s = NULL;
+
+  s = pr_parser_server_ctxt_open("127.0.0.1");
+  fail_unless(s != NULL, "Failed to open server context: %s", strerror(errno));
+
+  name = "foo";
+
+  mark_point();
+  c = add_config(NULL, name);
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+  fail_unless(c->config_type == 0, "Expected config_type 0, got %d",
+    c->config_type);
+
+  mark_point();
+  pr_config_dump(NULL, s->conf, NULL);
+
+  c = add_config_param_set(&(c->subset), "bar", 1, "baz");
+
+  mark_point();
+  pr_config_dump(NULL, s->conf, NULL);
+
+  mark_point();
+  res = remove_config(s->conf, name, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (config_add_config_param_test) {
+  int res;
+  const char *name = NULL;
+  config_rec *c = NULL;
+  server_rec *s = NULL;
+
+  s = pr_parser_server_ctxt_open("127.0.0.1");
+  fail_unless(s != NULL, "Failed to open server context: %s", strerror(errno));
+ 
+  c = add_config_param(NULL, 0, NULL);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno); 
+
+  name = "foo";
+
+  mark_point();
+  c = add_config_param(name, 1, "bar");
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+  fail_unless(c->config_type == CONF_PARAM, "Expected config_type %d, got %d",
+    CONF_PARAM, c->config_type);
+
+  mark_point();
+  pr_config_dump(NULL, s->conf, NULL);
+
+  mark_point();
+  res = pr_config_remove(s->conf, name, PR_CONFIG_FL_PRESERVE_ENTRY, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (config_add_config_param_set_test) {
+  xaset_t *set = NULL;
+  const char *name = NULL;
+  config_rec *c = NULL;
+
+  name = "foo";
+
+  c = add_config_param_set(NULL, name, 0);
+  fail_unless(c == NULL, "Failed to handle null set argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  c = add_config_param_set(&set, name, 0);
+  fail_unless(c != NULL, "Failed to add config '%s' to set: %s", name,
+    strerror(errno));
+  fail_unless(c->config_type == CONF_PARAM, "Expected config_type %d, got %d",
+    CONF_PARAM, c->config_type);
+  fail_unless(c->argc == 0, "Expected argc 0, got %d", c->argc);
+
+  c = add_config_param_set(&set, name, 2, "bar", "baz");
+  fail_unless(c != NULL, "Failed to add config '%s' to set: %s", name,
+    strerror(errno));
+  fail_unless(c->config_type == CONF_PARAM, "Expected config_type %d, got %d",
+    CONF_PARAM, c->config_type);
+  fail_unless(c->argc == 2, "Expected argc 2, got %d", c->argc);
+  fail_unless(strcmp("bar", (char *) c->argv[0]) == 0,
+    "Expected argv[0] to be 'bar', got '%s'", (char *) c->argv[0]);
+  fail_unless(strcmp("baz", (char *) c->argv[1]) == 0,
+    "Expected argv[1] to be 'baz', got '%s'", (char *) c->argv[1]);
+  fail_unless(c->argv[2] == NULL, "Expected argv[2] to be null");
+}
+END_TEST
+
+START_TEST (config_add_config_param_str_test) {
+  int res;
+  const char *name = NULL;
+  config_rec *c = NULL, *c2;
+  server_rec *s = NULL;
+
+  s = pr_parser_server_ctxt_open("127.0.0.1");
+  fail_unless(s != NULL, "Failed to open server context: %s", strerror(errno));
+
+  name = "foo";
+
+  mark_point();
+  c = add_config_param_str(name, 1, "bar");
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+  fail_unless(c->config_type == CONF_PARAM, "Expected config_type %d, got %d",
+    CONF_PARAM, c->config_type);
+
+  c2 = add_config_param_str("foo2", 1, NULL);
+  fail_unless(c2 != NULL, "Failed to add config 'foo2': %s", strerror(errno));
+
+  mark_point();
+  pr_config_dump(NULL, s->conf, NULL);
+
+  mark_point();
+  res = remove_config(s->conf, name, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (config_add_server_config_param_str_test) {
+  const char *name;
+  config_rec *c;
+  server_rec *s;
+
+  mark_point();
+  c = pr_conf_add_server_config_param_str(NULL, NULL, 0);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno));
+
+  mark_point();
+  s = pr_parser_server_ctxt_open("127.0.0.2");
+  fail_unless(s != NULL, "Failed to open server context: %s", strerror(errno));
+
+  mark_point();
+  name = "foo";
+
+  c = pr_conf_add_server_config_param_str(s, name, 1, "bar");
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  (void) remove_config(s->conf, name, FALSE);
+}
+END_TEST
+
+START_TEST (config_add_config_set_test) {
+  int flags = PR_CONFIG_FL_INSERT_HEAD, res;
+  xaset_t *set = NULL;
+  const char *name = NULL;
+  config_rec *c = NULL;
+
+  res = remove_config(NULL, NULL, FALSE);
+  fail_unless(res == 0, "Failed to handle null arguments: %s", strerror(errno));
+
+  name = "foo";
+
+  c = add_config_set(NULL, name);
+  fail_unless(c == NULL, "Failed to handle null set argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  c = add_config_set(&set, name);
+  fail_unless(c != NULL, "Failed to add config '%s' to set: %s", name,
+    strerror(errno));
+  fail_unless(c->config_type == 0, "Expected config_type 0, got %d",
+    c->config_type);
+
+  res = remove_config(set, name, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+
+  name = "bar";
+  res = remove_config(set, name, FALSE);
+  fail_unless(res == 0, "Removed config '%s' unexpectedly", name,
+    strerror(errno));
+
+  c = pr_config_add_set(&set, name, flags);
+  fail_unless(c != NULL, "Failed to add config '%s' to set: %s", name,
+    strerror(errno));
+
+  /* XXX Note that calling this with recurse=TRUE yields a test timeout,
+   * suggestive of an infinite loop that needs to be tracked down and
+   * fixed.
+   *
+   * I suspect it's in find_config_next2() bit of code near the comment:
+   *
+   *  Restart the search at the previous level if required
+   *
+   * Given the "shallowness" of this particular set.
+   */
+  res = remove_config(set, name, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (config_find_config_test) {
+  int res;
+  config_rec *c;
+  xaset_t *set = NULL;
+  const char *name;
+
+  c = find_config(NULL, -1, NULL, FALSE);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  c = find_config_next(NULL, NULL, CONF_PARAM, NULL, FALSE);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+
+  name = "foo";
+  c = add_config_param_set(&set, name, 0);
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  name = "bar";
+  c = find_config(set, -1, name, FALSE);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  mark_point();
+
+  /* We expect to find "foo", but a 'next' should be empty. */
+
+  name = "foo";
+  c = find_config(set, -1, name, FALSE);
+  fail_unless(c != NULL, "Failed to find config '%s': %s", name,
+    strerror(errno));
+
+  mark_point();
+
+  c = find_config_next(c, c->next, -1, name, FALSE);
+  fail_unless(c == NULL, "Found next config unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  /* Now add another config, find "foo" again; this time, a 'next' should
+   * NOT be empty; it should find the 2nd config we added.
+   */
+
+  name = "foo2";
+  c = add_config_param_set(&set, name, 0);
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  name = NULL;
+  c = find_config(set, -1, name, FALSE);
+  fail_unless(c != NULL, "Failed to find any config: %s", strerror(errno));
+
+  mark_point();
+
+  c = find_config_next(c, c->next, -1, name, FALSE);
+  fail_unless(c != NULL, "Expected to find another config");
+
+  mark_point();
+
+  name = "foo";
+  res = remove_config(set, name, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+
+  mark_point();
+
+  c = find_config(set, -1, name, FALSE);
+  fail_unless(c == NULL, "Found config '%s' unexpectedly", name);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  name = "other";
+  c = find_config(set, -1, name, TRUE);
+  fail_unless(c == NULL, "Found config '%s' unexpectedly (recurse = true)",
+    name);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (config_find_config2_test) {
+  int res;
+  config_rec *c;
+  xaset_t *set = NULL;
+  const char *name;
+  unsigned long flags = 0;
+
+  c = find_config2(NULL, -1, NULL, FALSE, flags);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  mark_point();
+
+  name = "foo";
+  c = add_config_param_set(&set, name, 0);
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  name = "bar";
+  c = find_config2(set, -1, name, FALSE, flags);
+  fail_unless(c == NULL, "Failed to handle null arguments");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  mark_point();
+
+  /* We expect to find "foo", but a 'next' should be empty. */
+  name = "foo";
+  c = find_config2(set, -1, name, FALSE, flags);
+  fail_unless(c != NULL, "Failed to find config '%s': %s", name,
+    strerror(errno));
+
+  mark_point();
+
+  c = find_config_next2(c, c->next, -1, name, FALSE, flags);
+  fail_unless(c == NULL, "Found next config unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  /* Now add another config, find "foo" again; this time, a 'next' should
+   * NOT be empty; it should find the 2nd config we added.
+   */
+
+  name = "foo2";
+  c = add_config_param_set(&set, name, 0);
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  name = NULL;
+  c = find_config2(set, -1, name, FALSE, flags);
+  fail_unless(c != NULL, "Failed to find any config: %s", strerror(errno));
+
+  mark_point();
+
+  c = find_config_next2(c, c->next, -1, name, FALSE, flags);
+  fail_unless(c != NULL, "Expected to find another config");
+
+  mark_point();
+
+  name = "foo";
+  res = remove_config(set, name, FALSE);
+  fail_unless(res > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+
+  mark_point();
+
+  c = find_config2(set, -1, name, FALSE, flags);
+  fail_unless(c == NULL, "Found config '%s' unexpectedly", name);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+}
+END_TEST
+
+START_TEST (config_get_param_ptr_test) {
+  void *res;
+  int count;
+  xaset_t *set = NULL;
+  config_rec *c;
+  const char *name = NULL;
+
+  res = get_param_ptr(NULL, NULL, FALSE);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  mark_point();
+
+  name = "foo";
+  c = add_config_param_set(&set, name, 1, "bar");
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  name = "bar";
+  res = get_param_ptr(set, name, FALSE);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  mark_point();
+
+  /* We expect to find "foo", but a 'next' should be empty. Note that we
+   * need to reset the get_param_ptr tree.
+   */
+  get_param_ptr(NULL, NULL, FALSE);
+
+  name = "foo";
+  res = get_param_ptr(set, name, FALSE);
+  fail_unless(res != NULL, "Failed to find config '%s': %s", name,
+    strerror(errno));
+
+  mark_point();
+
+  res = get_param_ptr_next(name, FALSE);
+  fail_unless(res == NULL, "Found next config unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  /* Now add another config, find "foo" again; this time, a 'next' should
+   * NOT be empty; it should find the 2nd config we added.
+   */
+
+  name = "foo2";
+  c = add_config_param_set(&set, name, 1, "baz");
+  fail_unless(c != NULL, "Failed to add config '%s': %s", name,
+    strerror(errno));
+
+  get_param_ptr(NULL, NULL, FALSE);
+
+  name = NULL;
+  res = get_param_ptr(set, name, FALSE);
+  fail_unless(res != NULL, "Failed to find any config: %s", strerror(errno));
+
+  mark_point();
+
+  res = get_param_ptr_next(name, FALSE);
+  fail_unless(res != NULL, "Expected to find another config");
+
+  res = get_param_ptr_next(name, FALSE);
+  fail_unless(res == NULL, "Found another config unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+
+  name = "foo";
+  count = remove_config(set, name, FALSE);
+  fail_unless(count > 0, "Failed to remove config '%s': %s", name,
+    strerror(errno));
+
+  mark_point();
+
+  res = get_param_ptr(set, name, FALSE);
+  fail_unless(res == NULL, "Found config '%s' unexpectedly", name);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+}
+END_TEST
+
+START_TEST (config_set_get_id_test) {
+  unsigned int id, res;
+  const char *name;
+
+  res = pr_config_get_id(NULL);
+  fail_unless(res == 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_config_set_id(NULL);
+  fail_unless(res == 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  name = "foo";
+
+  id = pr_config_set_id(name);
+  fail_unless(id > 0, "Failed to set ID for config '%s': %s", name,
+    strerror(errno));
+
+  res = pr_config_get_id(name);
+  fail_unless(res == id, "Expected ID %u for config '%s', found %u", id,
+    name, res);
+}
+END_TEST
+
+START_TEST (config_merge_down_test) {
+  xaset_t *set;
+  config_rec *c, *src, *dst;
+  const char *name;
+
+  mark_point();
+  pr_config_merge_down(NULL, FALSE);
+
+  mark_point();
+  set = xaset_create(p, NULL);
+  pr_config_merge_down(set, FALSE);
+
+  name = "foo";
+  c = add_config_param_set(&set, name, 0);
+
+  mark_point();
+  pr_config_merge_down(set, FALSE);
+
+  name = "bar";
+  c = add_config_param_set(&set, name, 1, "baz");
+  c->flags |= CF_MERGEDOWN;
+
+  mark_point();
+  pr_config_merge_down(set, FALSE);
+
+  name = "BAZ";
+  c = add_config_param_set(&set, name, 2, "quxx", "Quzz");
+  c->flags |= CF_MERGEDOWN_MULTI;
+
+  mark_point();
+  pr_config_merge_down(set, FALSE);
+
+  /* Add a config to the subsets, with the same name and same args. */
+  name = "<Anonymous>";
+  src = add_config_param_set(&set, name, 0);
+  src->config_type = CONF_ANON;
+
+  mark_point();
+  pr_config_merge_down(set, FALSE);
+
+  name = "<Directory>";
+  dst = add_config_param_set(&set, name, 1, "/baz");
+  dst->config_type = CONF_DIR;
+
+  name = "foo";
+  c = add_config_param_set(&(src->subset), name, 1, "alef");
+  c->flags |= CF_MERGEDOWN;
+
+  c = add_config_param_set(&(dst->subset), name, 1, "alef");
+  c->flags |= CF_MERGEDOWN;
+
+  mark_point();
+  pr_config_merge_down(set, FALSE);
+
+  /* Add a config to the subsets, with the same name and diff args. */
+  name = "alef";
+  c = add_config_param_set(&(src->subset), name, 1, "alef");
+  c->flags |= CF_MERGEDOWN;
+
+  c = add_config_param_set(&(dst->subset), name, 2, "bet", "vet");
+  c->flags |= CF_MERGEDOWN;
+
+  c = add_config_param_set(&(src->subset), "Bet", 3, "1", "2", "3");
+  c->config_type = CONF_LIMIT;
+  c->flags |= CF_MERGEDOWN;
+
+  mark_point();
+  pr_config_merge_down(set, FALSE);
+}
+END_TEST
+
+Suite *tests_get_config_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("config");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, config_init_config_test);
+  tcase_add_test(testcase, config_add_config_test);
+  tcase_add_test(testcase, config_add_config_param_test);
+  tcase_add_test(testcase, config_add_config_param_set_test);
+  tcase_add_test(testcase, config_add_config_param_str_test);
+  tcase_add_test(testcase, config_add_server_config_param_str_test);
+  tcase_add_test(testcase, config_add_config_set_test);
+  tcase_add_test(testcase, config_find_config_test);
+  tcase_add_test(testcase, config_find_config2_test);
+  tcase_add_test(testcase, config_get_param_ptr_test);
+  tcase_add_test(testcase, config_set_get_id_test);
+  tcase_add_test(testcase, config_merge_down_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/data.c b/tests/api/data.c
new file mode 100644
index 0000000..e4442ab
--- /dev/null
+++ b/tests/api/data.c
@@ -0,0 +1,1097 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Data API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static const char *data_test_path = "/tmp/prt-data.dat";
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = session.pool = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_fs();
+  init_netio();
+  init_dirtree();
+
+  pr_response_set_pool(p);
+  (void) pr_fsio_unlink(data_test_path);
+
+  if (session.c != NULL) {
+    pr_inet_close(p, session.c);
+    session.c = NULL;
+  }
+
+  session.sf_flags = 0;
+
+  pr_trace_set_levels("timing", 1, 1);
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("data", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  pr_unregister_netio(PR_NETIO_STRM_CTRL|PR_NETIO_STRM_CTRL);
+  pr_unregister_netio(PR_NETIO_STRM_CTRL|PR_NETIO_STRM_DATA);
+  (void) pr_fsio_unlink(data_test_path);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("data", 0, 0);
+  }
+  pr_trace_set_levels("timing", 0, 0);
+
+  if (session.c != NULL) {
+    (void) pr_inet_close(p, session.c);
+
+    if (session.c == session.d) {
+      session.d = NULL;
+    }
+
+    session.c = NULL;
+  }
+
+  if (session.d != NULL) {
+    (void) pr_inet_close(p, session.d);
+    session.d = NULL;
+  }
+
+  pr_response_set_pool(NULL);
+
+  if (p) {
+    destroy_pool(p);
+    p = session.pool = session.xfer.p = permanent_pool = NULL;
+  } 
+}
+
+START_TEST (data_get_timeout_test) {
+  int res;
+
+  res = pr_data_get_timeout(-1);
+  fail_unless(res < 0, "Failed to handle invalid timeout ID");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_data_get_timeout(PR_DATA_TIMEOUT_IDLE);
+  fail_unless(res == PR_TUNABLE_TIMEOUTIDLE, "Expected %d, got %d",
+    PR_TUNABLE_TIMEOUTIDLE, res);
+
+  res = pr_data_get_timeout(PR_DATA_TIMEOUT_NO_TRANSFER);
+  fail_unless(res == PR_TUNABLE_TIMEOUTNOXFER, "Expected %d, got %d",
+    PR_TUNABLE_TIMEOUTNOXFER, res);
+
+  res = pr_data_get_timeout(PR_DATA_TIMEOUT_STALLED);
+  fail_unless(res == PR_TUNABLE_TIMEOUTSTALLED, "Expected %d, got %d",
+    PR_TUNABLE_TIMEOUTSTALLED, res);
+}
+END_TEST
+
+START_TEST (data_set_timeout_test) {
+  int res, timeout = 7;
+
+  pr_data_set_timeout(PR_DATA_TIMEOUT_IDLE, timeout);
+  res = pr_data_get_timeout(PR_DATA_TIMEOUT_IDLE);
+  fail_unless(res == timeout, "Expected %d, got %d", timeout, res);
+
+  pr_data_set_timeout(PR_DATA_TIMEOUT_NO_TRANSFER, timeout);
+  res = pr_data_get_timeout(PR_DATA_TIMEOUT_NO_TRANSFER);
+  fail_unless(res == timeout, "Expected %d, got %d", timeout, res);
+
+  pr_data_set_timeout(PR_DATA_TIMEOUT_STALLED, timeout);
+  res = pr_data_get_timeout(PR_DATA_TIMEOUT_STALLED);
+  fail_unless(res == timeout, "Expected %d, got %d", timeout, res);
+
+  /* Interestingly, the linger timeout has its own function. */
+  pr_data_set_linger(7L);
+}
+END_TEST
+
+START_TEST (data_ignore_ascii_test) {
+  int res;
+
+  res = pr_data_ignore_ascii(-1);
+  fail_unless(res < 0, "Failed to handle invalid argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_data_ignore_ascii(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_data_ignore_ascii(TRUE);
+  fail_unless(res == TRUE, "Expected TRUE (%d), got %d", TRUE, res);
+
+  res = pr_data_ignore_ascii(FALSE);
+  fail_unless(res == TRUE, "Expected TRUE (%d), got %d", TRUE, res);
+
+  res = pr_data_ignore_ascii(FALSE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+}
+END_TEST
+
+static int data_close_cb(pr_netio_stream_t *nstrm) {
+  return 0;
+}
+
+static int data_poll_cb(pr_netio_stream_t *nstrm) {
+  /* Always return >0, to indicate that we haven't timed out, AND that there
+   * is a writable fd available.
+   */
+  return 7;
+}
+
+static int data_read_eagain = FALSE;
+static int data_read_epipe = FALSE;
+static int data_read_dangling_cr = FALSE;
+
+static int data_read_cb(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
+  const char *data = "Hello,\r\n World!\r\n";
+  size_t sz;
+
+  if (data_read_eagain) {
+    data_read_eagain = FALSE;
+    errno = EAGAIN;
+    return -1;
+  }
+
+  if (data_read_epipe) {
+    data_read_epipe = FALSE;
+    errno = EPIPE;
+    return -1;
+  }
+
+  if (data_read_dangling_cr) {
+    data = "Hello,\r\n World!\r\n\r";
+  }
+
+  sz = strlen(data);
+  if (buflen < sz) {
+    sz = buflen;
+  }
+
+  memcpy(buf, data, sz);
+  return (int) sz;
+}
+
+static int data_write_eagain = FALSE;
+static int data_write_epipe = FALSE;
+
+static int data_write_cb(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
+  if (data_write_eagain) {
+    data_write_eagain = FALSE;
+    errno = EAGAIN;
+    return -1;
+  }
+
+  if (data_write_epipe) {
+    data_write_epipe = FALSE;
+    errno = EPIPE;
+    return -1;
+  }
+
+  return buflen;
+}
+
+static int data_open_streams(conn_t *conn, int strm_type) {
+  int fd = 2, res;
+  pr_netio_t *netio;
+  pr_netio_stream_t *nstrm;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->close = data_close_cb;
+  netio->poll = data_poll_cb;
+  netio->read = data_read_cb;
+  netio->write = data_write_cb;
+
+  res = pr_register_netio(netio, strm_type);
+  if (res < 0) {
+    return -1;
+  }
+
+  nstrm = pr_netio_open(p, strm_type, fd, PR_NETIO_IO_WR);
+  if (nstrm == NULL) {
+    return -1;
+  }
+
+  conn->outstrm = nstrm;
+
+  nstrm = pr_netio_open(p, strm_type, fd, PR_NETIO_IO_RD);
+  if (nstrm == NULL) {
+    return -1;
+  }
+
+  conn->instrm = nstrm;
+  return 0;
+}
+
+START_TEST (data_sendfile_test) {
+  int fd = -1, res;
+  off_t offset = 0;
+  pr_fh_t *fh;
+  const char *text;
+
+  res = (int) pr_data_sendfile(fd, NULL, 0);
+  if (res < 0 &&
+      errno == ENOSYS) {
+    return;
+  }
+
+  res = pr_data_sendfile(fd, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null offset");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_data_sendfile(fd, &offset, 0);
+  fail_unless(res < 0, "Failed to handle zero count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  session.xfer.direction = PR_NETIO_IO_RD;
+  res = pr_data_sendfile(fd, &offset, 1);
+  fail_unless(res < 0, "Failed to handle invalid transfer direction");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  session.xfer.direction = PR_NETIO_IO_WR;
+  res = pr_data_sendfile(fd, &offset, 1);
+  fail_unless(res < 0, "Failed to handle lack of data connection");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  mark_point();
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = data_open_streams(session.d, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to open streams: %s", strerror(errno));
+
+  mark_point();
+  res = pr_data_sendfile(fd, &offset, 1);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(data_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", data_test_path,
+    strerror(errno));
+
+  text = "Hello, World!\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_unless(res >= 0, "Failed to write to '%s': %s", data_test_path,
+    strerror(errno));
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to close '%s': %s", data_test_path,
+    strerror(errno));
+
+  fd = open(data_test_path, O_RDONLY);
+  fail_unless(fd >= 0, "Failed to open '%s': %s", data_test_path,
+    strerror(errno));
+
+  mark_point();
+  res = pr_data_sendfile(fd, &offset, strlen(text));
+  if (res < 0) {
+    fail_unless(errno == ENOTSOCK, "Expected ENOTSOCK (%d), got %s (%d)",
+      ENOTSOCK, strerror(errno), errno);
+  }
+
+  (void) close(fd);
+  (void) pr_netio_close(session.d->outstrm);
+  session.d->outstrm = NULL;
+  (void) pr_inet_close(p, session.d);
+  session.d = NULL;
+
+  pr_unregister_netio(PR_NETIO_STRM_DATA);
+}
+END_TEST
+
+START_TEST (data_init_test) {
+  int rd = PR_NETIO_IO_RD, wr = PR_NETIO_IO_WR;
+  char *filename = NULL;
+
+  mark_point();
+  pr_data_init(filename, 0);
+  fail_unless(session.xfer.direction == 0, "Expected xfer direction %d, got %d",
+    0, session.xfer.direction);
+  fail_unless(session.xfer.p != NULL, "Transfer pool not created as expected");
+  fail_unless(session.xfer.filename == NULL, "Expected null filename, got %s",
+    session.xfer.filename);
+
+  filename = "test.dat";
+  pr_data_clear_xfer_pool();
+
+  mark_point();
+  pr_data_init(filename, rd);
+  fail_unless(session.xfer.direction == rd,
+    "Expected xfer direction %d, got %d", rd, session.xfer.direction);
+  fail_unless(session.xfer.p != NULL, "Transfer pool not created as expected");
+  fail_unless(session.xfer.filename != NULL, "Missing transfer filename");
+  fail_unless(strcmp(session.xfer.filename, filename) == 0,
+    "Expected '%s', got '%s'", filename, session.xfer.filename);
+
+  mark_point();
+  pr_data_init("test2.dat", wr);
+  fail_unless(session.xfer.direction == wr,
+    "Expected xfer direction %d, got %d", wr, session.xfer.direction);
+  fail_unless(session.xfer.p != NULL, "Transfer pool not created as expected");
+  fail_unless(session.xfer.filename != NULL, "Missing transfer filename");
+
+  /* Even though we opened with a new filename, the previous filename should
+   * still be there, as we didn't actually clear/reset this transfer.
+   */
+  fail_unless(strcmp(session.xfer.filename, filename) == 0,
+    "Expected '%s', got '%s'", filename, session.xfer.filename);
+}
+END_TEST
+
+START_TEST (data_open_active_test) {
+  int dir = PR_NETIO_IO_RD, port = INPORT_ANY, sockfd = -1, res;
+  conn_t *conn;
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  /* Note: these tests REQUIRE that session.c be non-NULL */
+  session.c = conn;
+
+  /* Open a READing data transfer connection...*/
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Note: we also need session.c to have valid local/remote_addr, too! */
+  session.c->local_addr = session.c->remote_addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  fail_unless(session.c->remote_addr != NULL, "Failed to get address: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Opened active READ data connection unexpectedly");
+  fail_unless(errno == EADDRNOTAVAIL || errno == ECONNREFUSED,
+    "Expected EADDRNOTAVAIL (%d) or ECONNREFUSED (%d), got %s (%d)",
+    EADDRNOTAVAIL, ECONNREFUSED, strerror(errno), errno);
+
+  /* Open a WRITing data transfer connection...*/
+  dir = PR_NETIO_IO_WR;
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Opened active READ data connection unexpectedly");
+  fail_unless(errno == EADDRNOTAVAIL || errno == ECONNREFUSED,
+    "Expected EADDRNOTAVAIL (%d) or ECONNREFUSED (%d), got %s (%d)",
+    EADDRNOTAVAIL, ECONNREFUSED, strerror(errno), errno);
+
+  mark_point();
+  session.xfer.p = NULL;
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Opened active READ data connection unexpectedly");
+  fail_unless(errno == EADDRNOTAVAIL || errno == ECONNREFUSED,
+    "Expected EADDRNOTAVAIL (%d) or ECONNREFUSED (%d), got %s (%d)",
+    EADDRNOTAVAIL, ECONNREFUSED, strerror(errno), errno);
+
+  (void) pr_inet_close(p, session.c);
+  session.c = NULL;
+  if (session.d != NULL) {
+    (void) pr_inet_close(p, session.d);
+    session.d = NULL;
+  }
+}
+END_TEST
+
+START_TEST (data_open_passive_test) {
+  int dir = PR_NETIO_IO_RD, port = INPORT_ANY, sockfd = -1, res;
+
+  /* Set the session flags for a passive transfer data connection. */
+  session.sf_flags |= SF_PASSIVE;
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Note: these tests REQUIRE that session.c be non-NULL, AND that session.d
+   * be non-NULL.
+   */
+  session.c = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(session.c != NULL, "Failed to create conn: %s", strerror(errno));
+
+  session.d = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  /* Open a READing data transfer connection...*/
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Note: we also need session.c to have valid local/remote_addr, too! */
+  session.c->local_addr = session.c->remote_addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  fail_unless(session.c->remote_addr != NULL, "Failed to get address: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Opened passive READ data connection unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Open a WRITing data transfer connection...*/
+  dir = PR_NETIO_IO_WR;
+
+  mark_point();
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Opened passive READ data connection unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  session.xfer.p = NULL;
+  res = pr_data_open(NULL, NULL, dir, 0);
+  fail_unless(res < 0, "Opened passive READ data connection unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) pr_inet_close(p, session.c);
+  session.c = NULL;
+  if (session.d != NULL) {
+    (void) pr_inet_close(p, session.d);
+    session.d = NULL;
+  }
+}
+END_TEST
+
+START_TEST (data_close_test) {
+  session.sf_flags |= SF_PASSIVE;
+  pr_data_close(TRUE);
+  fail_unless(!(session.sf_flags & SF_PASSIVE),
+    "Failed to clear SF_PASSIVE session flag");
+
+  session.sf_flags |= SF_PASSIVE;
+  pr_data_close(FALSE);
+  fail_unless(!(session.sf_flags & SF_PASSIVE),
+    "Failed to clear SF_PASSIVE session flag");
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  pr_data_close(TRUE);
+  fail_unless(session.d == NULL, "Failed to close session.d");
+}
+END_TEST
+
+START_TEST (data_abort_test) {
+  session.sf_flags |= SF_PASSIVE;
+  pr_data_abort(EPERM, TRUE);
+  fail_unless(!(session.sf_flags & SF_PASSIVE),
+    "Failed to clear SF_PASSIVE session flag");
+
+  session.sf_flags |= SF_PASSIVE;
+  pr_data_abort(EPERM, FALSE);
+  fail_unless(!(session.sf_flags & SF_PASSIVE),
+    "Failed to clear SF_PASSIVE session flag");
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  pr_data_abort(ESPIPE, FALSE);
+  fail_unless(session.d == NULL, "Failed to close session.d");
+}
+END_TEST
+
+START_TEST (data_reset_test) {
+  mark_point();
+
+  /* Set a session flag, make sure it's cleared properly. */
+  session.sf_flags |= SF_PASSIVE;
+  pr_data_reset();
+  fail_unless(session.d == NULL, "Expected NULL session.d, got %p", session.d);
+  fail_unless(!(session.sf_flags & SF_PASSIVE),
+    "SF_PASSIVE session flag not cleared");
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  pr_data_reset();
+  fail_unless(session.d == NULL, "Expected NULL session.d, got %p", session.d);
+  fail_unless(!(session.sf_flags & SF_PASSIVE),
+    "SF_PASSIVE session flag not cleared");
+}
+END_TEST
+
+START_TEST (data_cleanup_test) {
+  mark_point();
+
+  /* Set a session flag, make sure it's cleared properly. */
+  session.sf_flags |= SF_PASSIVE;
+  pr_data_cleanup();
+  fail_unless(session.d == NULL, "Expected NULL session.d, got %p", session.d);
+  fail_unless(session.sf_flags & SF_PASSIVE,
+    "SF_PASSIVE session flag not preserved");
+  fail_unless(session.xfer.xfer_type == STOR_DEFAULT, "Expected %d, got %d",
+    STOR_DEFAULT, session.xfer.xfer_type);
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  pr_data_cleanup();
+  fail_unless(session.d == NULL, "Failed to close session.d");
+}
+END_TEST
+
+START_TEST (data_clear_xfer_pool_test) {
+  int xfer_type = 7;
+
+  mark_point();
+  pr_data_clear_xfer_pool();
+  fail_unless(session.xfer.p == NULL, "Failed to clear session.xfer.p");
+
+  session.xfer.xfer_type = xfer_type; 
+  session.xfer.p = make_sub_pool(p);
+
+  mark_point();
+  pr_data_clear_xfer_pool();
+  fail_unless(session.xfer.p == NULL, "Failed to clear session.xfer.p");
+  fail_unless(session.xfer.xfer_type == xfer_type, "Expected %d, got %d",
+    xfer_type, session.xfer.xfer_type);
+}
+END_TEST
+
+START_TEST (data_xfer_read_binary_test) {
+  int res;
+  char *buf, *expected;
+  size_t bufsz, expected_len;
+  cmd_rec *cmd;
+
+  pr_data_clear_xfer_pool();
+  pr_data_reset();
+
+  res = pr_data_xfer(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  bufsz = 1024;
+  buf = palloc(p, bufsz);
+
+  res = pr_data_xfer(buf, 0);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_data_xfer(buf, bufsz);
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == ECONNABORTED,
+    "Expected ECONNABORTED (%d), got %s (%d)", ECONNABORTED,
+    strerror(errno), errno);
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  /* read binary data */
+  session.xfer.direction = PR_NETIO_IO_RD;
+
+  /* Note: this string comes from the data_read_cb() we register with our
+   * DATA stream callback.
+   */
+  expected = "Hello,\r\n World!\r\n";
+  expected_len = strlen(expected);
+
+  mark_point();
+  data_write_eagain = TRUE;
+  session.xfer.buf = NULL;
+  session.xfer.buflen = 0;
+
+  res = pr_data_xfer(buf, bufsz);
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = data_open_streams(session.d, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to open streams on session.d: %s",
+    strerror(errno));
+
+  mark_point();
+  session.xfer.buf = NULL;
+  session.xfer.buflen = 0;
+
+  res = pr_data_xfer(buf, bufsz);
+  fail_unless((size_t) res == expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+
+  session.c = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.c != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = data_open_streams(session.c, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to open streams on session.c: %s",
+    strerror(errno));
+
+  mark_point();
+  session.xfer.buf = NULL;
+  session.xfer.buflen = 0;
+
+  res = pr_data_xfer(buf, bufsz);
+  fail_unless(res == (int) expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+  fail_unless(session.xfer.buflen == 0,
+    "Expected session.xfer.buflen 0, got %lu",
+    (unsigned long) session.xfer.buflen);
+
+  mark_point();
+  session.xfer.buf = NULL;
+  session.xfer.buflen = 0;
+  cmd = pr_cmd_alloc(p, 1, pstrdup(p, "syst"));
+  tests_stubs_set_next_cmd(cmd);
+  data_read_eagain = TRUE;
+
+  res = pr_data_xfer(buf, bufsz);
+  fail_unless(res == (int) expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+  fail_unless(session.xfer.buflen == 0,
+    "Expected session.xfer.buflen 0, got %lu",
+    (unsigned long) session.xfer.buflen);
+}
+END_TEST
+
+START_TEST (data_xfer_write_binary_test) {
+  int res;
+  char *buf;
+  size_t buflen;
+
+  pr_data_clear_xfer_pool();
+  pr_data_reset();
+
+  res = pr_data_xfer(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  buf = "Hello, World!\n";
+  buflen = strlen(buf);
+
+  res = pr_data_xfer(buf, 0);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_data_xfer(buf, buflen);
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == ECONNABORTED,
+    "Expected ECONNABORTED (%d), got %s (%d)", ECONNABORTED,
+    strerror(errno), errno);
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  /* write binary data */
+  session.xfer.direction = PR_NETIO_IO_WR;
+  session.xfer.p = make_sub_pool(p);
+  session.xfer.buflen = 1024;
+  session.xfer.buf = pcalloc(p, session.xfer.buflen);
+
+  mark_point();
+  data_write_eagain = TRUE;
+  res = pr_data_xfer(buf, buflen);
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = data_open_streams(session.d, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to open streams on session.d: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_data_xfer(buf, buflen);
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+
+  session.c = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.c != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = data_open_streams(session.c, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to open streams on session.c: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_data_xfer(buf, buflen);
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+  fail_unless(strncmp(session.xfer.buf, buf, buflen) == 0,
+    "Expected '%s', got '%.100s'", buf, session.xfer.buf);
+}
+END_TEST
+
+START_TEST (data_xfer_read_ascii_test) {
+  int res;
+  char *buf, *expected;
+  size_t bufsz, expected_len;
+  cmd_rec *cmd;
+
+  pr_data_clear_xfer_pool();
+  pr_data_reset();
+
+  res = pr_data_xfer(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  bufsz = 1024;
+  buf = palloc(p, bufsz);
+
+  res = pr_data_xfer(buf, 0);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_data_xfer(buf, bufsz);
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == ECONNABORTED,
+    "Expected ECONNABORTED (%d), got %s (%d)", ECONNABORTED,
+    strerror(errno), errno);
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  /* read ASCII data */
+  session.xfer.direction = PR_NETIO_IO_RD;
+  session.xfer.p = make_sub_pool(p);
+  session.xfer.bufsize = 1024;
+
+  /* Note: this string comes from the data_read_cb() we register with our
+   * DATA stream callback.
+   */
+  expected = "Hello,\n World!\n";
+  expected_len = strlen(expected);
+
+  mark_point();
+  data_write_eagain = TRUE;
+  pr_ascii_ftp_reset();
+  session.xfer.buf = pcalloc(p, session.xfer.bufsize);
+  session.xfer.buflen = 0;
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, bufsz);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = data_open_streams(session.d, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to open streams on session.d: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_ascii_ftp_reset();
+  session.xfer.buf = pcalloc(p, session.xfer.bufsize);
+  session.xfer.buflen = 0;
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, bufsz);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless((size_t) res == expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+
+  session.c = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.c != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = data_open_streams(session.c, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to open streams on session.c: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_ascii_ftp_reset();
+  session.xfer.buf = pcalloc(p, session.xfer.bufsize);
+  session.xfer.buflen = 0;
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, bufsz);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless(res == (int) expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+  fail_unless(session.xfer.buflen == 0,
+    "Expected session.xfer.buflen 0, got %lu",
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(buf, expected, expected_len) == 0,
+    "Expected '%s', got '%.100s'", expected, buf);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, pstrdup(p, "noop"));
+  tests_stubs_set_next_cmd(cmd);
+  pr_ascii_ftp_reset();
+  session.xfer.buf = pcalloc(p, session.xfer.bufsize);
+  session.xfer.buflen = 0;
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, bufsz);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless(res == (int) expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+  fail_unless(session.xfer.buflen == 0,
+    "Expected session.xfer.buflen 0, got %lu",
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(buf, expected, expected_len) == 0,
+    "Expected '%s', got '%.100s'", expected, buf);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, pstrdup(p, "pasv"));
+  tests_stubs_set_next_cmd(cmd);
+  pr_ascii_ftp_reset();
+  session.xfer.buf = pcalloc(p, session.xfer.bufsize);
+  session.xfer.buflen = 0;
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, bufsz);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless(res == (int) expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+  fail_unless(session.xfer.buflen == 0,
+    "Expected session.xfer.buflen 0, got %lu",
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(buf, expected, expected_len) == 0,
+    "Expected '%s', got '%.100s'", expected, buf);
+
+  /* Bug#4237 happened because of insufficient testing of the edge case
+   * where the LAST character in the buffer is a CR.
+   *
+   * Note that to properly test this, we need to KEEP the same session.xfer.buf
+   * in place, and do the read TWICE (at least; maybe more).
+   */
+
+  mark_point();
+  pr_ascii_ftp_reset();
+  session.xfer.buf = pcalloc(p, session.xfer.bufsize);
+  session.xfer.buflen = 0;
+  data_read_dangling_cr = TRUE;
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, bufsz);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless(res == (int) expected_len, "Expected %lu, got %d",
+    (unsigned long) expected_len, res);
+  fail_unless(session.xfer.buflen == 1,
+    "Expected session.xfer.buflen 1, got %lu",
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(buf, expected, expected_len) == 0,
+    "Expected '%s', got '%.100s'", expected, buf);
+}
+END_TEST
+
+START_TEST (data_xfer_write_ascii_test) {
+  int res;
+  char *buf, *ascii_buf;
+  size_t buflen, ascii_buflen;
+  cmd_rec *cmd;
+
+  pr_data_clear_xfer_pool();
+  pr_data_reset();
+
+  res = pr_data_xfer(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  buf = "Hello,\n World\n";
+  buflen = strlen(buf);
+
+  ascii_buf = "Hello,\r\n World\r\n";
+  ascii_buflen = strlen(ascii_buf);
+
+  res = pr_data_xfer(buf, 0);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_data_xfer(buf, buflen);
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == ECONNABORTED,
+    "Expected ECONNABORTED (%d), got %s (%d)", ECONNABORTED,
+    strerror(errno), errno);
+
+  session.d = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.d != NULL, "Failed to create conn: %s", strerror(errno));
+
+  /* write ASCII data */
+  session.xfer.direction = PR_NETIO_IO_WR;
+  session.xfer.p = make_sub_pool(p);
+  session.xfer.buflen = 1024;
+  session.xfer.buf = pcalloc(p, session.xfer.buflen);
+
+  mark_point();
+  data_write_eagain = TRUE;
+  pr_ascii_ftp_reset();
+  session.sf_flags |= SF_ASCII_OVERRIDE;
+  res = pr_data_xfer(buf, buflen);
+  session.sf_flags &= ~SF_ASCII_OVERRIDE;
+
+  fail_unless(res < 0, "Transfered data unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = data_open_streams(session.d, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to open streams on session.d: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_ascii_ftp_reset();
+  session.sf_flags |= SF_ASCII_OVERRIDE;
+  res = pr_data_xfer(buf, buflen);
+  session.sf_flags &= ~SF_ASCII_OVERRIDE;
+
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+
+  session.c = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.c != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = data_open_streams(session.c, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to open streams on session.c: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_ascii_ftp_reset();
+  session.xfer.buflen = 1024;
+  session.xfer.buf = pcalloc(p, session.xfer.buflen);
+
+  session.sf_flags |= SF_ASCII_OVERRIDE;
+  res = pr_data_xfer(buf, buflen);
+  session.sf_flags &= ~SF_ASCII_OVERRIDE;
+
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+  fail_unless(session.xfer.buflen == ascii_buflen,
+    "Expected session.xfer.buflen %lu, got %lu", (unsigned long) ascii_buflen,
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(session.xfer.buf, ascii_buf, ascii_buflen) == 0,
+    "Expected '%s', got '%.100s'", ascii_buf, session.xfer.buf);
+
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, pstrdup(p, "noop"));
+  tests_stubs_set_next_cmd(cmd);
+  pr_ascii_ftp_reset();
+  session.xfer.buflen = 1024;
+  session.xfer.buf = pcalloc(p, session.xfer.buflen);
+
+  session.sf_flags |= SF_ASCII_OVERRIDE;
+  res = pr_data_xfer(buf, buflen);
+  session.sf_flags &= ~SF_ASCII_OVERRIDE;
+
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+  fail_unless(session.xfer.buflen == ascii_buflen,
+    "Expected session.xfer.buflen %lu, got %lu", (unsigned long) ascii_buflen,
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(session.xfer.buf, ascii_buf, ascii_buflen) == 0,
+    "Expected '%s', got '%.100s'", ascii_buf, session.xfer.buf);
+
+  session.xfer.p = make_sub_pool(p);
+  mark_point();
+  cmd = pr_cmd_alloc(p, 1, pstrdup(p, "pasv"));
+  tests_stubs_set_next_cmd(cmd);
+  pr_ascii_ftp_reset();
+  session.xfer.buflen = 1024;
+  session.xfer.buf = pcalloc(p, session.xfer.buflen);
+
+  session.sf_flags |= SF_ASCII_OVERRIDE;
+  res = pr_data_xfer(buf, buflen);
+  session.sf_flags &= ~SF_ASCII_OVERRIDE;
+
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+  fail_unless(session.xfer.buflen == ascii_buflen,
+    "Expected session.xfer.buflen %lu, got %lu", (unsigned long) ascii_buflen,
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(session.xfer.buf, ascii_buf, ascii_buflen) == 0,
+    "Expected '%s', got '%.100s'", ascii_buf, session.xfer.buf);
+
+  mark_point();
+  pr_ascii_ftp_reset();
+  session.xfer.buflen = 1024;
+  session.xfer.buf = pcalloc(p, session.xfer.buflen);
+
+  session.sf_flags |= SF_ASCII;
+  res = pr_data_xfer(buf, buflen);
+  session.sf_flags &= ~SF_ASCII;
+
+  fail_unless(res == (int) buflen, "Expected %lu, got %d",
+    (unsigned long) buflen, res);
+  fail_unless(session.xfer.buflen == ascii_buflen,
+    "Expected session.xfer.buflen %lu, got %lu", (unsigned long) ascii_buflen,
+    (unsigned long) session.xfer.buflen);
+  fail_unless(strncmp(session.xfer.buf, ascii_buf, ascii_buflen) == 0,
+    "Expected '%s', got '%.100s'", ascii_buf, session.xfer.buf);
+}
+END_TEST
+
+Suite *tests_get_data_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("data");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, data_get_timeout_test);
+  tcase_add_test(testcase, data_set_timeout_test);
+  tcase_add_test(testcase, data_ignore_ascii_test);
+  tcase_add_test(testcase, data_sendfile_test);
+
+  tcase_add_test(testcase, data_init_test);
+  tcase_add_test(testcase, data_open_active_test);
+  tcase_add_test(testcase, data_open_passive_test);
+  tcase_add_test(testcase, data_close_test);
+  tcase_add_test(testcase, data_abort_test);
+  tcase_add_test(testcase, data_reset_test);
+  tcase_add_test(testcase, data_cleanup_test);
+  tcase_add_test(testcase, data_clear_xfer_pool_test);
+  tcase_add_test(testcase, data_xfer_read_binary_test);
+  tcase_add_test(testcase, data_xfer_write_binary_test);
+  tcase_add_test(testcase, data_xfer_read_ascii_test);
+  tcase_add_test(testcase, data_xfer_write_ascii_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/display.c b/tests/api/display.c
new file mode 100644
index 0000000..951c02a
--- /dev/null
+++ b/tests/api/display.c
@@ -0,0 +1,296 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015-2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Display API tests
+ */
+
+#include "tests.h"
+
+extern pr_response_t *resp_list, *resp_err_list;
+
+static pool *p = NULL;
+
+static const char *display_test_file = "/tmp/prt-display.txt";
+
+static const char *display_lines[] = {
+  "Hello, %U\n",
+  "Environment: %{env:FOO} (%{env:NO_FOO})\n",
+  "Variable: %{BAR}\n",
+  "Time: %{time:%Y%m%d}\n",
+  NULL
+};
+
+/* Fixtures */
+
+static void set_up(void) {
+  (void) unlink(display_test_file);
+
+  if (p == NULL) {
+    p = session.pool = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_dirtree();
+  init_fs();
+  init_netio();
+  init_inet();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("netio", 1, 20);
+    pr_trace_set_levels("response", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  pr_response_register_handler(NULL);
+
+  if (session.c != NULL) {
+    pr_inet_close(p, session.c);
+    session.c = NULL;
+  }
+
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("netio", 0, 0);
+    pr_trace_set_levels("response", 0, 0);
+  }
+
+  (void) unlink(display_test_file);
+
+  if (p) {
+    destroy_pool(p);
+    p = session.pool = permanent_pool = NULL;
+  }
+}
+
+static int write_file(const char *path, const char **lines,
+    unsigned int nlines) {
+  register unsigned int i;
+  FILE *fh;
+  int res;
+
+  /* Write out a test Display file. */
+  fh = fopen(path, "w+");
+  if (fh == NULL) {
+    return -1;
+  }
+
+  for (i = 0; i < nlines; i++) {
+    const char *line;
+    size_t line_len;
+
+    line = lines[i];
+    line_len = strlen(line);
+    fwrite(line, line_len, 1, fh);
+  }
+
+  res = fclose(fh);
+  return res; 
+}
+
+/* Tests */
+
+START_TEST (display_file_test) {
+  int res;
+  const char *path = NULL, *resp_code = NULL;
+  const char *last_resp_code = NULL, *last_resp_msg = NULL;
+  pr_class_t *conn_class;
+
+  res = pr_display_file(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = display_test_file;
+  res = pr_display_file(path, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null resp_code argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  resp_code = R_200;
+  path = "/";
+  res = pr_display_file(path, NULL, resp_code, 0);
+  fail_unless(res < 0, "Failed to handle directory");
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  mark_point();
+  path = display_test_file;
+  res = pr_display_file(path, NULL, resp_code, 0);
+  fail_unless(res < 0, "Failed to handle nonexistent file");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  res = write_file(path, display_lines, 4);
+  fail_unless(res == 0, "Failed to write display file: %s", strerror(errno));
+
+  pr_response_set_pool(p);
+
+  conn_class = pcalloc(p, sizeof(pr_class_t));
+  conn_class->cls_pool = p;
+  conn_class->cls_name = "foo.bar";
+  session.conn_class = conn_class;
+
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code, 0);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  mark_point();
+  res = pr_response_get_last(p, &last_resp_code, &last_resp_msg);
+  fail_unless(res == 0, "Failed to get last response values: %s (%d)",
+    strerror(errno), errno);
+
+  fail_unless(last_resp_code != NULL,
+    "Last response code is unexpectedly null");
+  fail_unless(strcmp(last_resp_code, resp_code) == 0,
+    "Expected response code '%s', got '%s'", resp_code, last_resp_code);
+
+  fail_unless(last_resp_msg != NULL,
+    "Last response message is unexpectedly null");
+
+  /* Send the display file NOW */
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code, PR_DISPLAY_FL_SEND_NOW);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  mark_point();
+  res = pr_response_get_last(p, &last_resp_code, &last_resp_msg);
+  fail_unless(res == 0, "Failed to get last response values: %s (%d)",
+    strerror(errno), errno);
+
+  fail_unless(last_resp_code != NULL,
+    "Last response code is unexpectedly null");
+  fail_unless(strcmp(last_resp_code, resp_code) == 0,
+    "Expected response code '%s', got '%s'", resp_code, last_resp_code);
+
+  fail_unless(last_resp_msg != NULL,
+    "Last response message is unexpectedly null");
+
+  /* Send the display file NOW, with no EOM */
+
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code,
+    PR_DISPLAY_FL_SEND_NOW|PR_DISPLAY_FL_NO_EOM);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  mark_point();
+  res = pr_response_get_last(p, &last_resp_code, &last_resp_msg);
+  fail_unless(res == 0, "Failed to get last response values: %s (%d)",
+    strerror(errno), errno);
+
+  fail_unless(last_resp_code != NULL,
+    "Last response code is unexpectedly null");
+  fail_unless(strcmp(last_resp_code, resp_code) == 0,
+    "Expected response code '%s', got '%s'", resp_code, last_resp_code);
+
+  fail_unless(last_resp_msg != NULL,
+    "Last response message is unexpectedly null");
+
+  /* With MultilineRFC2228 on */
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code,
+    PR_DISPLAY_FL_SEND_NOW|PR_DISPLAY_FL_NO_EOM);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code, PR_DISPLAY_FL_SEND_NOW);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  /* With session.auth_mech */
+  session.auth_mech = "testsuite";
+
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code,
+    PR_DISPLAY_FL_SEND_NOW|PR_DISPLAY_FL_NO_EOM);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  mark_point();
+  res = pr_display_file(path, NULL, resp_code, PR_DISPLAY_FL_SEND_NOW);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (display_fh_test) {
+  pr_fh_t *fh;
+  int res;
+  const char *path = NULL, *resp_code = NULL;
+  const char *last_resp_code = NULL, *last_resp_msg = NULL;
+
+  res = pr_display_fh(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) unlink(display_test_file);
+  res = write_file(display_test_file, display_lines, 4);
+  fail_unless(res == 0, "Failed to write display file: %s", strerror(errno));
+
+  path = display_test_file;
+  fh = pr_fsio_open(path, O_RDONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", path, strerror(errno));
+
+  mark_point();
+  res = pr_display_fh(fh, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null resp_code argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  resp_code = R_200;
+  pr_response_set_pool(p);
+
+  mark_point();
+  res = pr_display_fh(fh, NULL, resp_code, 0);
+  fail_unless(res == 0, "Failed to display file: %s", strerror(errno));
+
+  mark_point();
+  res = pr_response_get_last(p, &last_resp_code, &last_resp_msg);
+  fail_unless(res == 0, "Failed to get last response values: %s (%d)",
+    strerror(errno), errno);
+
+  fail_unless(last_resp_code != NULL,
+    "Last response code is unexpectedly null");
+  fail_unless(strcmp(last_resp_code, resp_code) == 0,
+    "Expected response code '%s', got '%s'", resp_code, last_resp_code);
+
+  fail_unless(last_resp_msg != NULL,
+    "Last response message is unexpectedly null");
+}
+END_TEST
+
+Suite *tests_get_display_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("display");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, display_file_test);
+  tcase_add_test(testcase, display_fh_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/encode.c b/tests/api/encode.c
new file mode 100644
index 0000000..6f40d35
--- /dev/null
+++ b/tests/api/encode.c
@@ -0,0 +1,303 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Encode API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+#ifdef PR_USE_NLS
+  encode_init();
+#endif /* PR_USE_NLS */
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("encode", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("encode", 0, 0);
+  }
+
+#ifdef PR_USE_NLS
+  encode_free();
+#endif /* PR_USE_NLS */
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  }
+}
+
+#ifdef PR_USE_NLS
+START_TEST (encode_encode_str_test) {
+  char *res;
+  const char *in_str, junk[1024];
+  size_t in_len, out_len = 0;
+
+  res = pr_encode_str(NULL, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_encode_str(p, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null input string");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  in_str = "OK";
+  in_len = 2;
+  res = pr_encode_str(p, in_str, in_len, NULL);
+  fail_unless(res == NULL, "Failed to handle null output string len");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_encode_str(p, in_str, in_len, &out_len);
+  fail_unless(res != NULL, "Failed to encode '%s': %s", in_str,
+    strerror(errno));
+  fail_unless(strcmp(res, in_str) == 0, "Expected '%s', got '%s'", in_str,
+    res);
+
+  in_str = junk;
+  in_len = sizeof(junk);
+  res = pr_encode_str(p, in_str, in_len, &out_len);
+  fail_unless(res == NULL, "Failed to handle bad input");
+  fail_unless(errno == EILSEQ, "Expected EILSEQ (%d), got %s (%d)", EILSEQ,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (encode_decode_str_test) {
+  char *res;
+  const char *in_str, junk[1024];
+  size_t in_len, out_len = 0;
+
+  res = pr_decode_str(NULL, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_decode_str(p, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null input string");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  in_str = "OK";
+  in_len = 2;
+  res = pr_decode_str(p, in_str, in_len, NULL);
+  fail_unless(res == NULL, "Failed to handle null output string len");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_decode_str(p, in_str, in_len, &out_len);
+  fail_unless(res != NULL, "Failed to decode '%s': %s", in_str,
+    strerror(errno));
+  fail_unless(strcmp(res, in_str) == 0, "Expected '%s', got '%s'", in_str,
+    res);
+
+  in_str = junk;
+  in_len = sizeof(junk);
+  res = pr_encode_str(p, in_str, in_len, &out_len);
+  fail_unless(res == NULL, "Failed to handle bad input");
+  fail_unless(errno == EILSEQ, "Expected EILSEQ (%d), got %s (%d)", EILSEQ,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (encode_charset_test) {
+  int res;
+  const char *charset, *encoding;
+
+  charset = pr_encode_get_charset();
+  fail_unless(charset != NULL, "Failed to get current charset: %s",
+    strerror(errno));
+
+  res = pr_encode_is_utf8(NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  charset = "utf8";
+  res = pr_encode_is_utf8(charset);
+  fail_unless(res == TRUE, "Expected TRUE for '%s', got %d", charset, res);
+
+  charset = "utf-8";
+  res = pr_encode_is_utf8(charset);
+  fail_unless(res == TRUE, "Expected TRUE for '%s', got %d", charset, res);
+
+  charset = "ascii";
+  res = pr_encode_is_utf8(charset);
+  fail_unless(res == FALSE, "Expected FALSE for '%s', got %d", charset, res);
+
+  res = pr_encode_set_charset_encoding(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  charset = "us-ascii";
+  res = pr_encode_set_charset_encoding(charset, NULL);
+  fail_unless(res < 0, "Failed to handle null encoding");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  encoding = "utf-8";
+  res = pr_encode_set_charset_encoding(charset, encoding);
+  fail_unless(res == 0, "Failed to set charset '%s', encoding '%s': %s",
+    charset, encoding, strerror(errno));
+
+  charset = "foo";
+  res = pr_encode_set_charset_encoding(charset, encoding);
+  fail_unless(res < 0, "Failed to handle bad charset '%s'", charset);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  charset = "us-ascii";
+  encoding = "foo";
+  res = pr_encode_set_charset_encoding(charset, encoding);
+  fail_unless(res < 0, "Failed to handle bad encoding '%s'", encoding);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (encode_encoding_test) {
+  int res;
+  const char *encoding;
+
+  res = pr_encode_enable_encoding(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  encoding = "utf-8";
+  res = pr_encode_enable_encoding(encoding);
+  fail_unless(res == 0, "Failed to enable encoding '%s': %s", encoding,
+    strerror(errno));
+
+  encoding = "iso-8859-1";
+  res = pr_encode_enable_encoding(encoding);
+  fail_unless(res == 0, "Failed to enable encoding '%s': %s", encoding,
+    strerror(errno));
+
+  encoding = "foo";
+  res = pr_encode_enable_encoding(encoding);
+  fail_unless(res < 0, "Failed to handle bad encoding '%s'", encoding);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  pr_encode_disable_encoding();
+
+  encoding = "utf-8";
+  res = pr_encode_enable_encoding(encoding);
+  fail_unless(res == 0, "Failed to enable encoding '%s': %s", encoding,
+    strerror(errno));
+
+  encoding = pr_encode_get_encoding();
+  fail_unless(encoding != NULL, "Failed to get encoding: %s", strerror(errno));
+  fail_unless(strcasecmp(encoding, "utf-8") == 0,
+    "Expected 'utf-8', got '%s'", encoding);
+}
+END_TEST
+
+START_TEST (encode_policy_test) {
+  unsigned long res;
+
+  res = pr_encode_get_policy();
+  fail_unless(res == 0, "Expected policy 0, got %lu", res);
+
+  res = pr_encode_set_policy(7);
+  fail_unless(res == 0, "Expected policy 0, got %lu", res);
+
+  res = pr_encode_get_policy();
+  fail_unless(res == 7, "Expected policy 7, got %lu", res);
+
+  (void) pr_encode_set_policy(0);
+}
+END_TEST
+
+START_TEST (encode_supports_telnet_iac_test) {
+  register unsigned int i;
+  int res;
+  const char *charset, *encoding;
+  const char *non_iac_encodings[] = {
+    "cp1251",
+    "cp866",
+    "iso-8859-1",
+    "koi8-r",
+    "windows-1251",
+    NULL
+  };
+
+  res = pr_encode_supports_telnet_iac();
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  charset = "us-ascii";
+
+  for (i = 0; non_iac_encodings[i]; i++) {
+    encoding = non_iac_encodings[i];
+
+    res = pr_encode_set_charset_encoding(charset, encoding);
+    fail_unless(res == 0, "Failed to set charset '%s', encoding '%s': %s",
+      charset, encoding, strerror(errno));
+
+    res = pr_encode_supports_telnet_iac();
+    fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+  }
+
+  encoding = "utf-8";
+  res = pr_encode_set_charset_encoding(charset, encoding);
+  fail_unless(res == 0, "Failed to set charset '%s', encoding '%s': %s",
+    charset, encoding, strerror(errno));
+}
+END_TEST
+#endif /* PR_USE_NLS */
+
+Suite *tests_get_encode_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("encode");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+#ifdef PR_USE_NLS
+  tcase_add_test(testcase, encode_encode_str_test);
+  tcase_add_test(testcase, encode_decode_str_test);
+  tcase_add_test(testcase, encode_charset_test);
+  tcase_add_test(testcase, encode_encoding_test);
+  tcase_add_test(testcase, encode_policy_test);
+  tcase_add_test(testcase, encode_supports_telnet_iac_test);
+#endif /* PR_USE_NLS */
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/env.c b/tests/api/env.c
index 4d960c9..ad12638 100644
--- a/tests/api/env.c
+++ b/tests/api/env.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Env API tests
- * $Id: env.c,v 1.2 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Env API tests */
 
 #include "tests.h"
 
@@ -40,7 +38,7 @@ static void tear_down(void) {
   if (p) {
     destroy_pool(p);
     p = NULL;
-  } 
+  }
 }
 
 START_TEST (env_get_test) {
diff --git a/tests/api/etc/str/utf8-space.txt b/tests/api/etc/str/utf8-space.txt
new file mode 100644
index 0000000..e9953f8
--- /dev/null
+++ b/tests/api/etc/str/utf8-space.txt
@@ -0,0 +1 @@
+foo bar
\ No newline at end of file
diff --git a/tests/api/event.c b/tests/api/event.c
index b04130b..cca4dfc 100644
--- a/tests/api/event.c
+++ b/tests/api/event.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Event API tests
- * $Id: event.c,v 1.3 2013-01-05 03:36:39 castaglia Exp $
- */
+/* Event API tests */
 
 #include "tests.h"
 
@@ -43,8 +41,7 @@ static void set_up(void) {
 static void tear_down(void) {
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
+    p = permanent_pool = NULL;
   } 
 }
 
@@ -59,6 +56,9 @@ static void event_cb(const void *event_data, void *user_data) {
 static void event_cb2(const void *event_data, void *user_data) {
 }
 
+static void event_cb3(const void *event_data, void *user_data) {
+}
+
 static unsigned int event_dumped = 0;
 
 static void event_dump(const char *fmt, ...) {
@@ -72,6 +72,7 @@ static void event_dump(const char *fmt, ...) {
 START_TEST (event_register_test) {
   int res;
   const char *event = "foo";
+  module m;
 
   res = pr_event_register(NULL, NULL, NULL, NULL);
   fail_unless(res == -1, "Failed to handle null arguments");
@@ -91,6 +92,23 @@ START_TEST (event_register_test) {
   res = pr_event_register(NULL, event, event_cb, NULL);
   fail_unless(res == -1, "Failed to handle duplicate registration");
   fail_unless(errno == EEXIST, "Failed to set errno to EEXIST");
+
+  memset(&m, 0, sizeof(m));
+  m.name = "testsuite";
+
+  (void) pr_event_unregister(NULL, event, NULL);
+
+  res = pr_event_register(&m, event, event_cb, NULL);
+  fail_unless(res == 0, "Failed to register event with module: %s",
+    strerror(errno));
+
+  res = pr_event_register(&m, event, event_cb2, NULL);
+  fail_unless(res == 0, "Failed to register event with module: %s",
+    strerror(errno));
+
+  pr_event_unregister(&m, event, event_cb2);
+  pr_event_unregister(&m, event, event_cb);
+  pr_event_unregister(&m, NULL, NULL);
 }
 END_TEST
 
@@ -129,6 +147,58 @@ START_TEST (event_unregister_test) {
 }
 END_TEST
 
+START_TEST (event_listening_test) {
+  const char *event = "foo", *event2 = "bar";
+  int res;
+
+  res = pr_event_listening(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_event_listening(event);
+  fail_unless(res == 0, "Failed to check for '%s' listeners: %s", event,
+    strerror(errno));
+
+  res = pr_event_register(NULL, event, event_cb2, NULL);
+  fail_unless(res == 0, "Failed to register event '%s: %s", event,
+    strerror(errno));
+
+  res = pr_event_register(NULL, event2, event_cb2, NULL);
+  fail_unless(res == 0, "Failed to register event '%s: %s", event2,
+    strerror(errno));
+
+  res = pr_event_listening(event);
+  fail_unless(res == 1, "Expected 1 listener, got %d", res);
+
+  res = pr_event_register(NULL, event, event_cb3, NULL);
+  fail_unless(res == 0, "Failed to register event '%s: %s", event,
+    strerror(errno));
+
+  res = pr_event_listening(event);
+  fail_unless(res == 2, "Expected 2 listeners, got %d", res);
+
+  /* Unregister our listener, and make sure that the API indicates there
+   * are no more listeners.
+   */
+  res = pr_event_unregister(NULL, event, NULL);
+  fail_unless(res == 0, "Failed to unregister event '%s': %s", event,
+    strerror(errno));
+
+  res = pr_event_listening(event);
+  fail_unless(res == 0, "Failed to check for '%s' listeners: %s", event,
+    strerror(errno));
+
+  res = pr_event_unregister(NULL, event2, NULL);
+  fail_unless(res == 0, "Failed to unregister event '%s': %s", event2,
+    strerror(errno));
+
+  res = pr_event_listening(event);
+  fail_unless(res == 0, "Failed to check for '%s' listeners: %s", event,
+    strerror(errno));
+}
+END_TEST
+
 START_TEST (event_generate_test) {
   int res;
   const char *event = "foo";
@@ -168,6 +238,7 @@ END_TEST
 START_TEST (event_dump_test) {
   int res;
   const char *event = "foo";
+  module m;
 
   pr_event_dump(NULL);
   fail_unless(event_dumped == 0, "Expected dumped count of %u, got %u",
@@ -177,7 +248,9 @@ START_TEST (event_dump_test) {
   fail_unless(event_dumped == 0, "Expected dumped count of %u, got %u",
     0, event_dumped);
 
-  res = pr_event_register(NULL, event, event_cb, NULL);
+  memset(&m, 0, sizeof(m));
+  m.name = "testsuite";
+  res = pr_event_register(&m, event, event_cb, NULL);
   fail_unless(res == 0, "Failed to register event '%s', callback %p: %s",
     event, event_cb, strerror(errno));
 
@@ -208,10 +281,11 @@ Suite *tests_get_event_suite(void) {
   suite = suite_create("event");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
+
   tcase_add_test(testcase, event_register_test);
   tcase_add_test(testcase, event_unregister_test);
+  tcase_add_test(testcase, event_listening_test);
   tcase_add_test(testcase, event_generate_test);
   tcase_add_test(testcase, event_dump_test);
 
diff --git a/tests/api/expr.c b/tests/api/expr.c
index ff6f21a..04dc7e0 100644
--- a/tests/api/expr.c
+++ b/tests/api/expr.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Expression API tests
- * $Id: expr.c,v 1.4 2011-12-11 02:14:43 castaglia Exp $
- */
+/* Expression API tests */
 
 #include "tests.h"
 
@@ -49,7 +47,7 @@ static void tear_down(void) {
 
 START_TEST (expr_create_test) {
   array_header *res;
-  int expr_argc = 2;
+  unsigned int expr_argc = 2;
   char *expr_argv[4] = { NULL, NULL, NULL, NULL };
   char **elts;
 
@@ -77,7 +75,7 @@ START_TEST (expr_create_test) {
   fail_unless(res == NULL, "Failed to handle empty argv argument");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  expr_argc = -1;
+  expr_argc = 0;
   expr_argv[0] = "foo";
   expr_argv[1] = "bar";
 
@@ -393,7 +391,6 @@ Suite *tests_get_expr_suite(void) {
   suite = suite_create("expr");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, expr_create_test);
@@ -405,6 +402,5 @@ Suite *tests_get_expr_suite(void) {
   tcase_add_test(testcase, expr_eval_user_or_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/feat.c b/tests/api/feat.c
index 9a1be75..2f05de9 100644
--- a/tests/api/feat.c
+++ b/tests/api/feat.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Feat API tests
- * $Id: feat.c,v 1.2 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Feat API tests */
 
 #include "tests.h"
 
diff --git a/tests/api/filter.c b/tests/api/filter.c
new file mode 100644
index 0000000..5bd83f7
--- /dev/null
+++ b/tests/api/filter.c
@@ -0,0 +1,164 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Filter API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = make_sub_pool(NULL);
+  }
+
+  init_config();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_use_stderr(TRUE);
+    pr_trace_set_levels("filter", 0, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_use_stderr(FALSE);
+  }
+
+  if (p) {
+    destroy_pool(p);
+    p = NULL;
+  }
+}
+
+START_TEST (filter_parse_flags_test) {
+  const char *flags_str = NULL;
+  int res;
+
+  res = pr_filter_parse_flags(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  res = pr_filter_parse_flags(p, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  res = pr_filter_parse_flags(NULL, flags_str);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  flags_str = "foo";
+  res = pr_filter_parse_flags(p, flags_str);
+  fail_unless(res < 0, "Failed to handle badly formatted flags '%s'",
+    flags_str);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  flags_str = "[foo]";
+  res = pr_filter_parse_flags(p, flags_str);
+  fail_unless(res == 0, "Expected %d, got %d", 0, res);
+
+  flags_str = "[NC]";
+  res = pr_filter_parse_flags(p, flags_str);
+  fail_unless(res == REG_ICASE, "Expected REG_ICASE (%d), got %d", REG_ICASE,
+    res);
+
+  flags_str = "[nocase]";
+  res = pr_filter_parse_flags(p, flags_str);
+  fail_unless(res == REG_ICASE, "Expected REG_ICASE (%d), got %d", REG_ICASE,
+    res);
+}
+END_TEST
+
+START_TEST (filter_allow_path_test) {
+  int res;
+  config_rec *c;
+  pr_regex_t *allow_pre, *deny_pre;
+  xaset_t *set = NULL;
+  const char *path = NULL;
+
+  res = pr_filter_allow_path(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  mark_point();
+  c = add_config_param_set(&set, "test", 1, "test");
+  fail_if(c == NULL, "Failed to add config param: %s", strerror(errno));
+
+  path = "/foo/bar";
+  res = pr_filter_allow_path(set, path);
+  fail_unless(res == 0, "Failed to allow path '%s' with no configured filters",
+    path);
+
+  /* First, let's add a PathDenyFilter. */
+  deny_pre = pr_regexp_alloc(NULL);
+  res = pr_regexp_compile(deny_pre, "/bar$", 0);
+  fail_unless(res == 0, "Error compiling deny filter");
+
+  c = add_config_param_set(&set, "PathDenyFilter", 1, deny_pre);
+  fail_if(c == NULL, "Failed to add config param: %s", strerror(errno));
+
+  mark_point();
+  res = pr_filter_allow_path(set, path);
+  fail_unless(res == PR_FILTER_ERR_FAILS_DENY_FILTER,
+    "Failed to reject path '%s' with matching PathDenyFilter", path);
+
+  mark_point();
+  path = "/foo/baz";
+  res = pr_filter_allow_path(set, path);
+  fail_unless(res == 0,
+    "Failed to allow path '%s' with non-matching PathDenyFilter", path);
+
+  /* Now, let's add a PathAllowFilter. */
+  allow_pre = pr_regexp_alloc(NULL);
+  res = pr_regexp_compile(allow_pre, "/baz$", 0);
+  fail_unless(res == 0, "Error compiling allow filter");
+
+  c = add_config_param_set(&set, "PathAllowFilter", 1, allow_pre);
+  fail_if(c == NULL, "Failed to add config param: %s", strerror(errno));
+
+  mark_point();
+  path = "/foo/quxx";
+  res = pr_filter_allow_path(set, path);
+  fail_unless(res == PR_FILTER_ERR_FAILS_ALLOW_FILTER,
+    "Failed to allow path '%s' with matching PathAllowFilter", path);
+}
+END_TEST
+
+Suite *tests_get_filter_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("filter");
+
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, filter_parse_flags_test);
+  tcase_add_test(testcase, filter_allow_path_test);
+
+  suite_add_tcase(suite, testcase);
+
+  return suite;
+}
diff --git a/tests/api/fsio.c b/tests/api/fsio.c
index 3b4b360..508ca46 100644
--- a/tests/api/fsio.c
+++ b/tests/api/fsio.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2015 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -28,195 +28,4623 @@
 
 static pool *p = NULL;
 
+static char *fsio_cwd = NULL;
+static const char *fsio_test_path = "/tmp/prt-foo.bar.baz";
+static const char *fsio_test2_path = "/tmp/prt-foo.bar.baz.quxx.quzz";
+static const char *fsio_unlink_path = "/tmp/prt-fsio-link.dat";
+static const char *fsio_link_path = "/tmp/prt-fsio-symlink.lnk";
+static const char *fsio_testdir_path = "/tmp/prt-fsio-test.d";
+
 /* Fixtures */
 
 static void set_up(void) {
+  (void) unlink(fsio_test_path);
+  (void) unlink(fsio_test2_path);
+  (void) unlink(fsio_link_path);
+  (void) unlink(fsio_unlink_path);
+  (void) rmdir(fsio_testdir_path);
+
+  if (fsio_cwd != NULL) {
+    free(fsio_cwd);
+  }
+
+  fsio_cwd = getcwd(NULL, 0);
+
   if (p == NULL) {
     p = permanent_pool = make_sub_pool(NULL);
   }
 
   init_fs();
+  pr_fs_statcache_set_policy(PR_TUNABLE_FS_STATCACHE_SIZE,
+    PR_TUNABLE_FS_STATCACHE_MAX_AGE, 0);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("fsio", 1, 20);
+    pr_trace_set_levels("fs.statcache", 1, 20);
+  }
+
 }
 
 static void tear_down(void) {
+  if (fsio_cwd != NULL) {
+    free(fsio_cwd);
+    fsio_cwd = NULL;
+  }
+
+  (void) pr_fsio_guard_chroot(FALSE);
+  pr_fs_statcache_set_policy(PR_TUNABLE_FS_STATCACHE_SIZE,
+    PR_TUNABLE_FS_STATCACHE_MAX_AGE, 0);
+
+  pr_unregister_fs("/testuite");
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("fsio", 0, 0);
+    pr_trace_set_levels("fs.statcache", 0, 0);
+  }
+
+  (void) unlink(fsio_test_path);
+  (void) unlink(fsio_test2_path);
+  (void) unlink(fsio_link_path);
+  (void) unlink(fsio_unlink_path);
+  (void) rmdir(fsio_testdir_path);
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
-  } 
+    p = permanent_pool = NULL;
+  }
 }
 
 /* Tests */
 
-START_TEST (fs_clean_path_test) {
-  char res[PR_TUNABLE_PATH_MAX+1], *path, *expected;
+START_TEST (fsio_sys_open_test) {
+  int flags;
+  pr_fh_t *fh;
 
-  res[sizeof(res)-1] = '\0';
-  path = "/test.txt";
-  pr_fs_clean_path(path, res, sizeof(res)-1);
-  fail_unless(strcmp(res, path) == 0, "Expected cleaned path '%s', got '%s'",
-    path, res);
+  mark_point();
+  flags = O_CREAT|O_EXCL|O_RDONLY;
+  fh = pr_fsio_open(NULL, flags);
+  fail_unless(fh == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-  res[sizeof(res)-1] = '\0';
-  path = "/./test.txt";
-  pr_fs_clean_path(path, res, sizeof(res)-1);
+  mark_point();
+  flags = O_RDONLY;
+  fh = pr_fsio_open(fsio_test_path, flags);
+  fail_unless(fh == NULL, "Failed to handle non-existent file '%s'",
+    fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), %s (%d)", ENOENT,
+    strerror(errno), errno);
 
-  expected = "/test.txt";
-  fail_unless(strcmp(res, expected) == 0,
-    "Expected cleaned path '%s', got '%s'", expected, res);
+  mark_point();
+  flags = O_RDONLY;
+  fh = pr_fsio_open("/etc/resolv.conf", flags);
+  fail_unless(fh != NULL, "Failed to /etc/resolv.conf: %s", strerror(errno));
 
-  res[sizeof(res)-1] = '\0';
-  path = "test.txt";
-  pr_fs_clean_path(path, res, sizeof(res)-1);
+  (void) pr_fsio_close(fh);
+}
+END_TEST
 
-  expected = "/test.txt";
-  fail_unless(strcmp(res, expected) == 0,
-    "Expected cleaned path '%s', got '%s'", path, res);
+START_TEST (fsio_sys_open_canon_test) {
+  int flags;
+  pr_fh_t *fh;
+
+  flags = O_CREAT|O_EXCL|O_RDONLY;
+  fh = pr_fsio_open_canon(NULL, flags);
+  fail_unless(fh == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  flags = O_RDONLY;
+  fh = pr_fsio_open_canon(fsio_test_path, flags);
+  fail_unless(fh == NULL, "Failed to handle non-existent file '%s'",
+    fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  flags = O_RDONLY;
+  fh = pr_fsio_open_canon("/etc/resolv.conf", flags);
+  fail_unless(fh != NULL, "Failed to /etc/resolv.conf: %s", strerror(errno));
+
+  (void) pr_fsio_close(fh);
 }
 END_TEST
 
-START_TEST (fs_clean_path2_test) {
-  char res[PR_TUNABLE_PATH_MAX+1], *path, *expected;
+START_TEST (fsio_sys_open_chroot_guard_test) {
+  int flags, res;
+  pr_fh_t *fh;
+  const char *path;
 
-  res[sizeof(res)-1] = '\0';
-  path = "test.txt";
-  pr_fs_clean_path2(path, res, sizeof(res)-1, 0);
-  fail_unless(strcmp(res, path) == 0, "Expected cleaned path '%s', got '%s'",
-    path, res);
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
 
-  res[sizeof(res)-1] = '\0';
-  path = "/./test.txt";
-  pr_fs_clean_path2(path, res, sizeof(res)-1, 0);
+  path = "/etc/resolv.conf";
+  flags = O_CREAT|O_RDONLY;
+  fh = pr_fsio_open(path, flags);
+  if (fh != NULL) {
+    (void) pr_fsio_close(fh);
+    fail("open(2) of %s succeeded unexpectedly", path);
+  }
 
-  expected = "/test.txt";
-  fail_unless(strcmp(res, expected) == 0,
-    "Expected cleaned path '%s', got '%s'", expected, res);
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
 
-  res[sizeof(res)-1] = '\0';
-  path = "test.d///test.txt";
-  pr_fs_clean_path2(path, res, sizeof(res)-1, 0);
+  path = "/&Z";
+  flags = O_WRONLY;
+  fh = pr_fsio_open(path, flags);
+  if (fh != NULL) {
+    (void) pr_fsio_close(fh);
+    fail("open(2) of %s succeeded unexpectedly", path);
+  }
 
-  expected = "test.d/test.txt";
-  fail_unless(strcmp(res, expected) == 0,
-    "Expected cleaned path '%s', got '%s'", expected, res);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno));
 
-  res[sizeof(res)-1] = '\0';
-  path = "/test.d///test.txt";
-  pr_fs_clean_path2(path, res, sizeof(res)-1,
-    PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH);
+  path = "/etc";
+  fh = pr_fsio_open(path, flags);
+  if (fh != NULL) {
+    (void) pr_fsio_close(fh);
+    fail("open(2) of %s succeeded unexpectedly", path);
+  }
 
-  expected = "/test.d/test.txt";
-  fail_unless(strcmp(res, expected) == 0,
-    "Expected cleaned path '%s', got '%s'", expected, res);
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno));
+
+  path = "/lib";
+  fh = pr_fsio_open(path, flags);
+  if (fh != NULL) {
+    (void) pr_fsio_close(fh);
+    fail("open(2) of %s succeeded unexpectedly", path);
+  }
+
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno));
 
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  path = "/etc/resolv.conf";
+  flags = O_RDONLY;
+  fh = pr_fsio_open(path, flags);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", path, strerror(errno));
+  (void) pr_fsio_close(fh);
 }
 END_TEST
 
-START_TEST (fs_dircat_test) {
-  char buf[PR_TUNABLE_PATH_MAX+1], *a, *b, *ok;
+START_TEST (fsio_sys_close_test) {
   int res;
+  pr_fh_t *fh;
 
-  res = pr_fs_dircat(NULL, 0, NULL, NULL);
-  fail_unless(res == -1, "Failed to handle null arguments");
-  fail_unless(errno == EINVAL,
-    "Failed to set errno to EINVAL for null arguments");
+  res = pr_fsio_close(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s %d", EINVAL,
+    strerror(errno), errno);
 
-  res = pr_fs_dircat(buf, 0, "foo", "bar");
-  fail_unless(res == -1, "Failed to handle zero-length buffer");
-  fail_unless(errno == EINVAL,
-    "Failed to set errno to EINVAL for zero-length buffer");
+  fh = pr_fsio_open("/etc/resolv.conf", O_RDONLY);
+  fail_unless(fh != NULL, "Failed to open /etc/resolv.conf: %s",
+    strerror(errno));
 
-  res = pr_fs_dircat(buf, -1, "foo", "bar");
-  fail_unless(res == -1, "Failed to handle negative-length buffer");
-  fail_unless(errno == EINVAL,
-    "Failed to set errno to EINVAL for negative-length buffer");
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to close file handle: %s", strerror(errno));
 
-  a = pcalloc(p, PR_TUNABLE_PATH_MAX);
-  memset(a, 'A', PR_TUNABLE_PATH_MAX-1);
+  mark_point();
 
-  b = "foo";
+  /* Deliberately try to close an already-closed handle, to make sure we
+   * don't segfault.
+   */
+  res = pr_fsio_close(fh);
+  fail_unless(res < 0, "Failed to handle already-closed file handle");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+}
+END_TEST
 
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == -1, "Failed to handle too-long paths");
-  fail_unless(errno == ENAMETOOLONG,
-    "Failed to set errno to ENAMETOOLONG for too-long paths");
+START_TEST (fsio_sys_unlink_test) {
+  int res;
+  pr_fh_t *fh;
 
-  a = "foo";
-  b = "/bar";
-  ok = b;
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == 0, "Failed to concatenate abs-path path second dir");
-  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
-    ok, buf);
- 
-  a = "foo";
-  b = "bar";
-  ok = "foo/bar";
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == 0, "Failed to concatenate two normal paths");
-  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
-    ok, buf);
- 
-  a = "foo/";
-  b = "bar";
-  ok = "foo/bar";
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == 0, "Failed to concatenate first dir with trailing slash");
-  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
-    ok, buf);
+  res = pr_fsio_unlink(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-  a = "";
-  b = "";
-  ok = "/";
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == 0, "Failed to concatenate two empty paths");
-  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
-    ok, buf);
+  fh = pr_fsio_open(fsio_unlink_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_unlink_path,
+    strerror(errno));
+  (void) pr_fsio_close(fh);
 
-  a = "/foo";
-  b = "";
-  ok = "/foo/";
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == 0, "Failed to concatenate two empty paths");
-  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
-    ok, buf);
+  res = pr_fsio_unlink(fsio_unlink_path);
+  fail_unless(res == 0, "Failed to unlink '%s': %s", fsio_unlink_path,
+    strerror(errno));
+}
+END_TEST
 
-  a = "";
-  b = "/bar";
-  ok = "/bar/";
-  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
-  fail_unless(res == 0, "Failed to concatenate two empty paths");
-  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
-    ok, buf);
+START_TEST (fsio_sys_unlink_canon_test) {
+  int res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_unlink_canon(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_unlink_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_unlink_path, 
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+
+  res = pr_fsio_unlink_canon(fsio_unlink_path);
+  fail_unless(res == 0, "Failed to unlink '%s': %s", fsio_unlink_path,
+    strerror(errno));
 }
 END_TEST
 
-START_TEST (fs_setcwd_test) {
+START_TEST (fsio_sys_unlink_chroot_guard_test) {
   int res;
 
-  /* Make sure that we don't segfault if we call pr_fs_setcwd() on the
-   * buffer that it is already using.
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_unlink("/etc/resolv.conf");
+  fail_unless(res < 0, "Deleted /etc/resolv.conf unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_unlink("/lib/foo.bar.baz");
+  fail_unless(res < 0, "Deleted /lib/foo.bar.baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_stat_test) {
+  int res;
+  struct stat st;
+  unsigned int cache_size = 3, max_age = 1, policy_flags = 0;
+
+  res = pr_fsio_stat(NULL, &st);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_stat("/", NULL);
+  fail_unless(res < 0, "Failed to handle null struct stat");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_stat("/", &st);
+  fail_unless(res == 0, "Unexpected stat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  /* Now, do the stat(2) again, and make sure we get the same information
+   * from the cache.
    */
-  res = pr_fs_setcwd(pr_fs_getcwd());
-  fail_unless(res == 0, "Failed to set cwd to '%s': %s", pr_fs_getcwd(),
+  res = pr_fsio_stat("/", &st);
+  fail_unless(res == 0, "Unexpected stat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  pr_fs_statcache_reset();
+  res = pr_fs_statcache_set_policy(cache_size, max_age, policy_flags);
+  fail_unless(res == 0, "Failed to set statcache policy: %s", strerror(errno));
+
+  res = pr_fsio_stat("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_stat("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Now wait for longer than 1 second (our configured max age) */
+  sleep(max_age + 1);
+
+  res = pr_fsio_stat("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Stat a symlink path */
+  res = pr_fsio_symlink("/tmp", fsio_link_path);
+  fail_unless(res == 0, "Failed to create symlink to '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  res = pr_fsio_stat(fsio_link_path, &st);
+  fail_unless(res == 0, "Failed to stat '%s': %s", fsio_link_path,
     strerror(errno));
+
+  (void) unlink(fsio_link_path);
 }
 END_TEST
 
-Suite *tests_get_fsio_suite(void) {
-  Suite *suite;
-  TCase *testcase;
+START_TEST (fsio_sys_stat_canon_test) {
+  int res;
+  struct stat st;
+  unsigned int cache_size = 3, max_age = 1, policy_flags = 0;
 
-  suite = suite_create("fsio");
+  res = pr_fsio_stat_canon(NULL, &st);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
 
-  testcase = tcase_create("base");
+  res = pr_fsio_stat_canon("/", NULL);
+  fail_unless(res < 0, "Failed to handle null struct stat");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
 
-  tcase_add_checked_fixture(testcase, set_up, tear_down);
+  res = pr_fsio_stat_canon("/", &st);
+  fail_unless(res == 0, "Unexpected stat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  /* Now, do the stat(2) again, and make sure we get the same information
+   * from the cache.
+   */
+  res = pr_fsio_stat_canon("/", &st);
+  fail_unless(res == 0, "Unexpected stat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  pr_fs_statcache_reset();
+  res = pr_fs_statcache_set_policy(cache_size, max_age, policy_flags);
+  fail_unless(res == 0, "Failed to set statcache policy: %s", strerror(errno));
+
+  res = pr_fsio_stat_canon("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_stat_canon("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Now wait for longer than 1 second (our configured max age) */
+  sleep(max_age + 1);
+
+  res = pr_fsio_stat_canon("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_fstat_test) {
+  int res;
+  pr_fh_t *fh;
+  struct stat st;
+
+  res = pr_fsio_fstat(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open("/etc/resolv.conf", O_RDONLY);
+  fail_unless(fh != NULL, "Failed to open /etc/resolv.conf: %s",
+    strerror(errno));
+
+  res = pr_fsio_fstat(fh, &st);
+  fail_unless(res == 0, "Failed to fstat /etc/resolv.conf: %s",
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+}
+END_TEST
+
+START_TEST (fsio_sys_read_test) {
+  int res;
+  pr_fh_t *fh;
+  char *buf;
+  size_t buflen;
+
+  res = pr_fsio_read(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open("/etc/resolv.conf", O_RDONLY);
+  fail_unless(fh != NULL, "Failed to open /etc/resolv.conf: %s",
+    strerror(errno));
+
+  res = pr_fsio_read(fh, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  buflen = 32;
+  buf = palloc(p, buflen);
+
+  res = pr_fsio_read(fh, buf, 0);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_read(fh, buf, 1);
+  fail_unless(res == 1, "Failed to read 1 byte: %s", strerror(errno));
+
+  (void) pr_fsio_close(fh);
+}
+END_TEST
+
+START_TEST (fsio_sys_write_test) {
+  int res;
+  pr_fh_t *fh;
+  char *buf;
+  size_t buflen;
+
+  res = pr_fsio_write(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", strerror(errno));
+
+  /* XXX What happens if we use NULL buffer, zero length? */
+  res = pr_fsio_write(fh, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  buflen = 32;
+  buf = palloc(p, buflen);
+  memset(buf, 'c', buflen);
+
+  res = pr_fsio_write(fh, buf, 0);
+  fail_unless(res == 0, "Failed to handle zero buffer length");
+
+  res = pr_fsio_write(fh, buf, buflen);
+  fail_unless((size_t) res == buflen, "Failed to write %lu bytes: %s",
+    (unsigned long) buflen, strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_lseek_test) {
+  int res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_lseek(NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open("/etc/resolv.conf", O_RDONLY);
+  fail_unless(fh != NULL, "Failed to open /etc/resolv.conf: %s",
+    strerror(errno));
+
+  res = pr_fsio_lseek(fh, 0, 0);
+  fail_unless(res == 0, "Failed to seek to byte 0: %s", strerror(errno));
+
+  (void) pr_fsio_close(fh);
+}
+END_TEST
+
+START_TEST (fsio_sys_link_test) {
+  int res;
+  const char *target_path, *link_path;
+  pr_fh_t *fh;
+
+  target_path = link_path = NULL;
+  res = pr_fsio_link(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = fsio_test_path;
+  link_path = NULL;
+  res = pr_fsio_link(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null link_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = NULL;
+  link_path = fsio_link_path;
+  res = pr_fsio_link(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null target_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+
+  /* Link a file (that exists) to itself */
+  link_path = target_path = fsio_test_path;
+  res = pr_fsio_link(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle same existing source/destination");
+  fail_unless(errno == EEXIST, "Expected EEXIST, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Create expected link */
+  link_path = fsio_link_path;
+  target_path = fsio_test_path;
+  res = pr_fsio_link(target_path, link_path);
+  fail_unless(res == 0, "Failed to create link from '%s' to '%s': %s",
+    link_path, target_path, strerror(errno));
+  (void) unlink(link_path);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_link_canon_test) {
+  int res;
+  const char *target_path, *link_path;
+  pr_fh_t *fh;
+
+  target_path = link_path = NULL;
+  res = pr_fsio_link_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = fsio_test_path;
+  link_path = NULL;
+  res = pr_fsio_link_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null link_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = NULL;
+  link_path = fsio_link_path;
+  res = pr_fsio_link_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null target_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+
+  /* Link a file (that exists) to itself */
+  link_path = target_path = fsio_test_path;
+  res = pr_fsio_link_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle same existing source/destination");
+  fail_unless(errno == EEXIST, "Expected EEXIST, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Create expected link */
+  link_path = fsio_link_path;
+  target_path = fsio_test_path;
+  res = pr_fsio_link_canon(target_path, link_path);
+  fail_unless(res == 0, "Failed to create link from '%s' to '%s': %s",
+    link_path, target_path, strerror(errno));
+  (void) unlink(link_path);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_link_chroot_guard_test) {
+  int res;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_link(fsio_link_path, "/etc/foo.bar.baz");
+  fail_unless(res < 0, "Linked /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  (void) pr_fsio_unlink(fsio_link_path);
+  res = pr_fsio_link(fsio_link_path, "/lib/foo/bar/baz");
+  fail_unless(res < 0, "Linked /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_symlink_test) {
+  int res;
+  const char *target_path, *link_path;
+
+  target_path = link_path = NULL;
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = "/tmp";
+  link_path = NULL;
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null link_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = NULL;
+  link_path = fsio_link_path;
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null target_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Symlink a file (that exists) to itself */
+  link_path = target_path = "/tmp";
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle same existing source/destination");
+  fail_unless(errno == EEXIST, "Expected EEXIST, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Create expected symlink */
+  link_path = fsio_link_path;
+  target_path = "/tmp";
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res == 0, "Failed to create symlink from '%s' to '%s': %s",
+    link_path, target_path, strerror(errno));
+  (void) unlink(link_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_symlink_canon_test) {
+  int res;
+  const char *target_path, *link_path;
+
+  target_path = link_path = NULL;
+  res = pr_fsio_symlink_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = "/tmp";
+  link_path = NULL;
+  res = pr_fsio_symlink_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null link_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  target_path = NULL;
+  link_path = fsio_link_path;
+  res = pr_fsio_symlink_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle null target_path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Symlink a file (that exists) to itself */
+  link_path = target_path = "/tmp";
+  res = pr_fsio_symlink_canon(target_path, link_path);
+  fail_unless(res < 0, "Failed to handle same existing source/destination");
+  fail_unless(errno == EEXIST, "Expected EEXIST, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Create expected symlink */
+  link_path = fsio_link_path;
+  target_path = "/tmp";
+  res = pr_fsio_symlink_canon(target_path, link_path);
+  fail_unless(res == 0, "Failed to create symlink from '%s' to '%s': %s",
+    link_path, target_path, strerror(errno));
+  (void) unlink(link_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_symlink_chroot_guard_test) {
+  int res;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_symlink(fsio_link_path, "/etc/foo.bar.baz");
+  fail_unless(res < 0, "Symlinked /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+  (void) pr_fsio_unlink(fsio_link_path);
+
+  res = pr_fsio_symlink(fsio_link_path, "/lib/foo/bar/baz");
+  fail_unless(res < 0, "Symlinked /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_readlink_test) {
+  int res;
+  char buf[PR_TUNABLE_BUFFER_SIZE];
+  const char *link_path, *target_path, *path;
+
+  res = pr_fsio_readlink(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Read a non-symlink file */
+  path = "/";
+  res = pr_fsio_readlink(path, buf, sizeof(buf)-1);
+  fail_unless(res < 0, "Failed to handle non-symlink path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Read a symlink file */
+  target_path = "/tmp";
+  link_path = fsio_link_path;
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res == 0, "Failed to create symlink from '%s' to '%s': %s",
+    link_path, target_path, strerror(errno));
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_fsio_readlink(link_path, buf, sizeof(buf)-1);
+  fail_unless(res > 0, "Failed to read symlink '%s': %s", link_path,
+    strerror(errno));
+  buf[res] = '\0';
+  fail_unless(strcmp(buf, target_path) == 0, "Expected '%s', got '%s'",
+    target_path, buf);
+
+  /* Read a symlink file using a zero-length buffer */
+  res = pr_fsio_readlink(link_path, buf, 0);
+  fail_unless(res <= 0, "Expected length <= 0, got %d", res);
+
+  (void) unlink(link_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_readlink_canon_test) {
+  int res;
+  char buf[PR_TUNABLE_BUFFER_SIZE];
+  const char *link_path, *target_path, *path;
+
+  res = pr_fsio_readlink_canon(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Read a non-symlink file */
+  path = "/";
+  res = pr_fsio_readlink_canon(path, buf, sizeof(buf)-1);
+  fail_unless(res < 0, "Failed to handle non-symlink path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Read a symlink file */
+  target_path = "/tmp";
+  link_path = fsio_link_path;
+  res = pr_fsio_symlink(target_path, link_path);
+  fail_unless(res == 0, "Failed to create symlink from '%s' to '%s': %s",
+    link_path, target_path, strerror(errno));
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_fsio_readlink_canon(link_path, buf, sizeof(buf)-1);
+  fail_unless(res > 0, "Failed to read symlink '%s': %s", link_path,
+    strerror(errno));
+  buf[res] = '\0';
+  fail_unless(strcmp(buf, target_path) == 0, "Expected '%s', got '%s'",
+    target_path, buf);
+
+  /* Read a symlink file using a zero-length buffer */
+  res = pr_fsio_readlink_canon(link_path, buf, 0);
+  fail_unless(res <= 0, "Expected length <= 0, got %d", res);
+
+  (void) unlink(link_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_lstat_test) {
+  int res;
+  struct stat st;
+  unsigned int cache_size = 3, max_age = 1, policy_flags = 0;
+
+  res = pr_fsio_lstat(NULL, &st);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_lstat("/", NULL);
+  fail_unless(res < 0, "Failed to handle null struct stat");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_lstat("/", &st);
+  fail_unless(res == 0, "Unexpected lstat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  /* Now, do the lstat(2) again, and make sure we get the same information
+   * from the cache.
+   */
+  res = pr_fsio_lstat("/", &st);
+  fail_unless(res == 0, "Unexpected lstat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  pr_fs_statcache_reset();
+  res = pr_fs_statcache_set_policy(cache_size, max_age, policy_flags);
+  fail_unless(res == 0, "Failed to set statcache policy: %s", strerror(errno));
+
+  res = pr_fsio_lstat("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_lstat("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Now wait for longer than 1 second (our configured max age) */
+  sleep(max_age + 1);
+
+  res = pr_fsio_lstat("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  /* lstat a symlink path */
+  res = pr_fsio_symlink("/tmp", fsio_link_path);
+  fail_unless(res == 0, "Failed to create symlink to '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  res = pr_fsio_lstat(fsio_link_path, &st);
+  fail_unless(res == 0, "Failed to lstat '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  (void) unlink(fsio_link_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_lstat_canon_test) {
+  int res;
+  struct stat st;
+  unsigned int cache_size = 3, max_age = 1, policy_flags = 0;
+
+  res = pr_fsio_lstat_canon(NULL, &st);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_lstat_canon("/", NULL);
+  fail_unless(res < 0, "Failed to handle null struct stat");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_lstat_canon("/", &st);
+  fail_unless(res == 0, "Unexpected lstat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  /* Now, do the lstat(2) again, and make sure we get the same information
+   * from the cache.
+   */
+  res = pr_fsio_lstat_canon("/", &st);
+  fail_unless(res == 0, "Unexpected lstat(2) error on '/': %s",
+    strerror(errno));
+  fail_unless(S_ISDIR(st.st_mode), "'/' is not a directory as expected");
+
+  pr_fs_statcache_reset();
+  res = pr_fs_statcache_set_policy(cache_size, max_age, policy_flags);
+  fail_unless(res == 0, "Failed to set statcache policy: %s", strerror(errno));
+
+  res = pr_fsio_lstat_canon("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_lstat_canon("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+
+  /* Now wait for longer than 1 second (our configured max age) */
+  sleep(max_age + 1);
+
+  res = pr_fsio_lstat_canon("/foo/bar/baz/quxx", &st);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT, got %s (%d)", strerror(errno),
+    errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_access_dir_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  mode_t perms;
+  array_header *suppl_gids;
+
+  res = pr_fsio_access(NULL, X_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL, got %s (%d)", strerror(errno),
+    errno);
+
+  res = pr_fsio_access("/baz/bar/foo", X_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Make the directory to check; we want it to have perms 771.*/
+  perms = (mode_t) 0771;
+  res = mkdir(fsio_testdir_path, perms);
+  fail_if(res < 0, "Unable to create directory '%s': %s", fsio_testdir_path,
+    strerror(errno));
+
+  /* Use chmod(2) to ensure that the directory has the perms we want,
+   * regardless of any umask settings.
+   */
+  res = chmod(fsio_testdir_path, perms);
+  fail_if(res < 0, "Unable to set perms %04o on directory '%s': %s", perms,
+    fsio_testdir_path, strerror(errno));
+
+  /* First, check that we ourselves can access our own directory. */
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, F_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for file access on directory: %s",
+    strerror(errno));
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, R_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for read access on directory: %s",
+    strerror(errno));
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, W_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for write access on directory: %s",
+    strerror(errno));
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, X_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for execute access on directory: %s",
+    strerror(errno));
+
+  suppl_gids = make_array(p, 1, sizeof(gid_t));
+  *((gid_t *) push_array(suppl_gids)) = gid;
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, X_OK, uid, gid, suppl_gids);
+  fail_unless(res == 0, "Failed to check for execute access on directory: %s",
+    strerror(errno));
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, R_OK, uid, gid, suppl_gids);
+  fail_unless(res == 0, "Failed to check for read access on directory: %s",
+    strerror(errno));
+
+  pr_fs_clear_cache2(fsio_testdir_path);
+  res = pr_fsio_access(fsio_testdir_path, W_OK, uid, gid, suppl_gids);
+  fail_unless(res == 0, "Failed to check for write access on directory: %s",
+    strerror(errno));
+
+  if (getenv("TRAVIS") == NULL) {
+    uid_t other_uid = 1000;
+    gid_t other_gid = 1000;
+
+    /* Next, check that others can access the directory. */
+    pr_fs_clear_cache2(fsio_testdir_path);
+    res = pr_fsio_access(fsio_testdir_path, F_OK, other_uid, other_gid,
+      NULL);
+    fail_unless(res == 0,
+      "Failed to check for other file access on directory: %s",
+      strerror(errno));
+
+    pr_fs_clear_cache2(fsio_testdir_path);
+    res = pr_fsio_access(fsio_testdir_path, R_OK, other_uid, other_gid,
+      NULL);
+    fail_unless(res < 0,
+      "other read access on directory succeeded unexpectedly");
+    fail_unless(errno == EACCES, "Expected EACCES, got %s (%d)",
+      strerror(errno), errno);
+
+    pr_fs_clear_cache2(fsio_testdir_path);
+    res = pr_fsio_access(fsio_testdir_path, W_OK, other_uid, other_gid,
+      NULL);
+    fail_unless(res < 0,
+      "other write access on directory succeeded unexpectedly");
+    fail_unless(errno == EACCES, "Expected EACCES, got %s (%d)",
+      strerror(errno), errno);
+
+    pr_fs_clear_cache2(fsio_testdir_path);
+    res = pr_fsio_access(fsio_testdir_path, X_OK, other_uid, other_gid,
+      NULL);
+    fail_unless(res == 0, "Failed to check for execute access on directory: %s",
+      strerror(errno));
+  }
+
+  (void) rmdir(fsio_testdir_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_access_file_test) {
+  int fd, res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  mode_t perms = 0665;
+  array_header *suppl_gids;
+
+  /* Make the file to check; we want it to have perms 664.*/
+  fd = open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY, S_IRUSR|S_IWUSR);
+  fail_if(fd < 0, "Unable to create file '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  /* Use chmod(2) to ensure that the file has the perms we want,
+   * regardless of any umask settings.
+   */
+  res = chmod(fsio_test_path, perms);
+  fail_if(res < 0, "Unable to set perms %04o on file '%s': %s", perms,
+    fsio_test_path, strerror(errno));
+
+  /* First, check that we ourselves can access our own file. */
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, F_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for file access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, R_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for read access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, W_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for write access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, X_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for execute access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  suppl_gids = make_array(p, 1, sizeof(gid_t));
+  *((gid_t *) push_array(suppl_gids)) = gid;
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, X_OK, uid, gid, suppl_gids);
+  fail_unless(res == 0, "Failed to check for execute access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, R_OK, uid, gid, suppl_gids);
+  fail_unless(res == 0, "Failed to check for read access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_access(fsio_test_path, W_OK, uid, gid, suppl_gids);
+  fail_unless(res == 0, "Failed to check for write access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  (void) unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_faccess_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  mode_t perms = 0664;
+  pr_fh_t *fh;
+
+  res = pr_fsio_faccess(NULL, F_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Unable to create file '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  /* Use chmod(2) to ensure that the file has the perms we want,
+   * regardless of any umask settings.
+   */
+  res = chmod(fsio_test_path, perms);
+  fail_if(res < 0, "Unable to set perms %04o on file '%s': %s", perms,
+    fsio_test_path, strerror(errno));
+
+  /* First, check that we ourselves can access our own file. */
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_faccess(fh, F_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for file access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_faccess(fh, R_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for read access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_faccess(fh, W_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to check for write access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  pr_fs_clear_cache2(fsio_test_path);
+  res = pr_fsio_faccess(fh, X_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to check for execute access on '%s': %s",
+    fsio_test_path, strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_truncate_test) {
+  int res;
+  off_t len = 0;
+  pr_fh_t *fh;
+
+  res = pr_fsio_truncate(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_truncate(fsio_test_path, 0);
+  fail_unless(res < 0, "Truncated '%s' unexpectedly", fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_truncate(fsio_test_path, len);
+  fail_unless(res == 0, "Failed to truncate '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_truncate_canon_test) {
+  int res;
+  off_t len = 0;
+  pr_fh_t *fh;
+
+  res = pr_fsio_truncate_canon(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_truncate_canon(fsio_test_path, 0);
+  fail_unless(res < 0, "Truncated '%s' unexpectedly", fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_truncate_canon(fsio_test_path, len);
+  fail_unless(res == 0, "Failed to truncate '%s': %s", fsio_test_path,
+    strerror(errno));
+  
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_truncate_chroot_guard_test) {
+  int res;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_truncate("/etc/foo.bar.baz", 0);
+  fail_unless(res < 0, "Truncated /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_truncate("/lib/foo/bar/baz", 0);
+  fail_unless(res < 0, "Truncated /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_ftruncate_test) {
+  int res;
+  off_t len = 0;
+  pr_fh_t *fh;
+  pr_buffer_t *buf;
+
+  res = pr_fsio_ftruncate(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  mark_point();
+  res = pr_fsio_ftruncate(fh, len);
+  fail_unless(res == 0, "Failed to truncate '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  /* Attach a read buffer to the handle, make sure it is cleared. */
+  buf = pcalloc(fh->fh_pool, sizeof(pr_buffer_t));
+  buf->buflen = 100;
+  buf->remaining = 1;
+
+  fh->fh_buf = buf;
+
+  mark_point();
+  res = pr_fsio_ftruncate(fh, len);
+  fail_unless(res == 0, "Failed to truncate '%s': %s", fsio_test_path,
+    strerror(errno));
+  fail_unless(buf->remaining == buf->buflen,
+    "Expected %lu, got %lu", (unsigned long) buf->buflen,
+    (unsigned long) buf->remaining);
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_chmod_test) {
+  int res;
+  mode_t mode = 0644;
+  pr_fh_t *fh;
+
+  res = pr_fsio_chmod(NULL, mode);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_chmod(fsio_test_path, 0);
+  fail_unless(res < 0, "Changed perms of '%s' unexpectedly", fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_chmod(fsio_test_path, mode);
+  fail_unless(res == 0, "Failed to set perms of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_chmod_canon_test) {
+  int res;
+  mode_t mode = 0644;
+  pr_fh_t *fh;
+
+  res = pr_fsio_chmod_canon(NULL, mode);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_chmod_canon(fsio_test_path, 0);
+  fail_unless(res < 0, "Changed perms of '%s' unexpectedly", fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_chmod_canon(fsio_test_path, mode);
+  fail_unless(res == 0, "Failed to set perms of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_chmod_chroot_guard_test) {
+  int res;
+  mode_t mode = 0644;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_chmod("/etc/foo.bar.baz", mode);
+  fail_unless(res < 0, "Set mode on /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_chmod("/lib/foo/bar/baz", mode);
+  fail_unless(res < 0, "Set mode on /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_fchmod_test) {
+  int res;
+  mode_t mode = 0644;
+  pr_fh_t *fh;
+
+  res = pr_fsio_fchmod(NULL, mode);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_fchmod(fh, mode);
+  fail_unless(res == 0, "Failed to set perms of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_chown_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  pr_fh_t *fh;
+
+  res = pr_fsio_chown(NULL, uid, gid);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_chown(fsio_test_path, uid, gid);
+  fail_unless(res < 0, "Changed ownership of '%s' unexpectedly",
+    fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_chown(fsio_test_path, uid, gid);
+  fail_unless(res == 0, "Failed to set ownership of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_chown_canon_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  pr_fh_t *fh;
+
+  res = pr_fsio_chown_canon(NULL, uid, gid);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_chown_canon(fsio_test_path, uid, gid);
+  fail_unless(res < 0, "Changed ownership of '%s' unexpectedly",
+    fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_chown_canon(fsio_test_path, uid, gid);
+  fail_unless(res == 0, "Failed to set ownership of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_chown_chroot_guard_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_chown("/etc/foo.bar.baz", uid, gid);
+  fail_unless(res < 0, "Set ownership on /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_chown("/lib/foo/bar/baz", uid, gid);
+  fail_unless(res < 0, "Set ownership on /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_fchown_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  pr_fh_t *fh;
+
+  res = pr_fsio_fchown(NULL, uid, gid);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_fchown(fh, uid, gid);
+  fail_unless(res == 0, "Failed to set ownership of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_lchown_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+  pr_fh_t *fh;
+
+  res = pr_fsio_lchown(NULL, uid, gid);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_lchown(fsio_test_path, uid, gid);
+  fail_unless(res < 0, "Changed ownership of '%s' unexpectedly",
+    fsio_test_path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_lchown(fsio_test_path, uid, gid);
+  fail_unless(res == 0, "Failed to set ownership of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_lchown_chroot_guard_test) {
+  int res;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_lchown("/etc/foo.bar.baz", uid, gid);
+  fail_unless(res < 0, "Set ownership on /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_lchown("/lib/foo/bar/baz", uid, gid);
+  fail_unless(res < 0, "Set ownership on /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_rename_test) {
+  int res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_rename(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_rename(fsio_test_path, NULL);
+  fail_unless(res < 0, "Failed to handle null dst argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_rename(fsio_test_path, fsio_test2_path);
+  fail_unless(res < 0, "Failed to handle non-existent files");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+
+  res = pr_fsio_rename(fsio_test_path, fsio_test2_path);
+  fail_unless(res == 0, "Failed to rename '%s' to '%s': %s", fsio_test_path,
+    fsio_test2_path, strerror(errno));
+
+  (void) pr_fsio_unlink(fsio_test_path);
+  (void) pr_fsio_unlink(fsio_test2_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_rename_canon_test) {
+  int res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_rename_canon(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_rename_canon(fsio_test_path, NULL);
+  fail_unless(res < 0, "Failed to handle null dst argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_rename_canon(fsio_test_path, fsio_test2_path);
+  fail_unless(res < 0, "Failed to handle non-existent files");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+
+  res = pr_fsio_rename_canon(fsio_test_path, fsio_test2_path);
+  fail_unless(res == 0, "Failed to rename '%s' to '%s': %s", fsio_test_path,
+    fsio_test2_path, strerror(errno));
+
+  (void) pr_fsio_unlink(fsio_test_path);
+  (void) pr_fsio_unlink(fsio_test2_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_rename_chroot_guard_test) {
+  int res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+  (void) pr_fsio_close(fh);
+
+  res = pr_fsio_rename(fsio_test_path, "/etc/foo.bar.baz");
+  fail_unless(res < 0, "Renamed '%s' unexpectedly", fsio_test_path);
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  res = pr_fsio_rename("/etc/foo.bar.baz", fsio_test_path);
+  fail_unless(res < 0, "Renamed '/etc/foo.bar.baz' unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_rename("/etc/foo/bar/baz", "/lib/quxx/quzz");
+  fail_unless(res < 0, "Renamed '/etc/foo/bar/baz' unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_utimes_test) {
+  int res;
+  struct timeval tvs[3];
+  pr_fh_t *fh;
+
+  res = pr_fsio_utimes(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_utimes(fsio_test_path, (struct timeval *) &tvs);
+  fail_unless(res < 0, "Changed times of '%s' unexpectedly", fsio_test_path);
+  fail_unless(errno == ENOENT || errno == EINVAL,
+    "Expected ENOENT (%d) or EINVAL (%d), got %s (%d)", ENOENT, EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  memset(&tvs, 0, sizeof(tvs));
+  res = pr_fsio_utimes(fsio_test_path, (struct timeval *) &tvs);
+  fail_unless(res == 0, "Failed to set times of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_utimes_chroot_guard_test) {
+  int res;
+  struct timeval tvs[3];
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+ 
+  res = pr_fsio_utimes("/etc/foo.bar.baz", (struct timeval *) &tvs);
+  fail_unless(res < 0, "Set times on /etc/foo.bar.baz unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_utimes("/lib/foo/bar/baz", (struct timeval *) &tvs);
+  fail_unless(res < 0, "Set times on /lib/foo/bar/baz unexpectedly");
+  fail_unless(errno == ENOENT || errno == EINVAL,
+    "Expected ENOENT (%d) or EINVAL (%d), got %s %d", ENOENT, EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_futimes_test) {
+  int res;
+  struct timeval tvs[3];
+  pr_fh_t *fh;
+  
+  res = pr_fsio_futimes(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to create '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  memset(&tvs, 0, sizeof(tvs));
+  res = pr_fsio_futimes(fh, (struct timeval *) &tvs);
+  fail_unless(res == 0, "Failed to set times of '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_fsync_test) {
+  int res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_fsync(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_fsync(fh);
+#ifdef HAVE_FSYNC
+  fail_unless(res == 0, "fsync of '%s' failed: %s", fsio_test_path,
+    strerror(errno));
+#else
+  fail_unless(res < 0, "fsync of '%s' succeeded unexpectedly", fsio_test_path);
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* HAVE_FSYNC */
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_getxattr_test) {
+  ssize_t res;
+  const char *path, *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_getxattr(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_getxattr(p, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_getxattr(p, path, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_getxattr(p, path, name, NULL, 0);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  (void) pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_getxattr(p, path, name, NULL, 0);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexist attribute '%s'", name);
+  fail_unless(errno == ENOENT || errno == ENOATTR || errno == ENOTSUP,
+    "Expected ENOENT (%d), ENOATTR (%d) or ENOTSUP (%d), got %s (%d)",
+    ENOENT, ENOATTR, ENOTSUP, strerror(errno), errno);
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_lgetxattr_test) {
+  ssize_t res;
+  const char *path, *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_lgetxattr(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_lgetxattr(p, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_lgetxattr(p, path, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null xattr name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_lgetxattr(p, path, name, NULL, 0);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_lgetxattr(p, path, name, NULL, 0);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexist attribute '%s'", name);
+  fail_unless(errno == ENOENT || errno == ENOATTR || errno == ENOTSUP,
+    "Expected ENOENT (%d), ENOATTR (%d) or ENOTSUP (%d), got %s (%d)",
+    ENOENT, ENOATTR, ENOTSUP, strerror(errno), errno);
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_fgetxattr_test) {
+  ssize_t res;
+  pr_fh_t *fh;
+  const char *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_fgetxattr(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_fgetxattr(p, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null file handle");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_RDWR);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_fgetxattr(p, fh, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null xattr name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_fgetxattr(p, fh, name, NULL, 0);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_fgetxattr(p, fh, name, NULL, 0);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexist attribute '%s'", name);
+  fail_unless(errno == ENOENT || errno == ENOATTR || errno == ENOTSUP,
+    "Expected ENOENT (%d), ENOATTR (%d) or ENOTSUP (%d), got %s (%d)",
+    ENOENT, ENOATTR, ENOTSUP, strerror(errno), errno);
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+
+  pr_fsio_close(fh);
+  (void) unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_listxattr_test) {
+  int res;
+  const char *path;
+  pr_fh_t *fh = NULL;
+  array_header *names = NULL;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_listxattr(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_listxattr(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_listxattr(p, path, NULL);
+  fail_unless(res < 0, "Failed to handle null array");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_listxattr(p, path, &names);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_listxattr(p, path, &names);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent path '%s'", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+  pr_fsio_close(fh);
+
+  res = pr_fsio_listxattr(p, path, &names);
+  fail_if(res < 0, "Failed to list xattrs for '%s': %s", path, strerror(errno));
+
+  pr_fsio_close(fh);
+  (void) unlink(fsio_test_path);
+#else
+  (void) fh;
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_llistxattr_test) {
+  int res;
+  const char *path;
+  pr_fh_t *fh = NULL;
+  array_header *names = NULL;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_llistxattr(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_llistxattr(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_llistxattr(p, path, NULL);
+  fail_unless(res < 0, "Failed to handle null array");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_llistxattr(p, path, &names);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_llistxattr(p, path, &names);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent path '%s'", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+  pr_fsio_close(fh);
+
+  res = pr_fsio_listxattr(p, path, &names);
+  fail_if(res < 0, "Failed to list xattrs for '%s': %s", path, strerror(errno));
+
+  pr_fsio_close(fh);
+  (void) unlink(fsio_test_path);
+#else
+  (void) fh;
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_flistxattr_test) {
+  int res;
+  pr_fh_t *fh;
+  array_header *names = NULL;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_flistxattr(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_flistxattr(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null file handle");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_RDWR);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_flistxattr(p, fh, NULL);
+  fail_unless(res < 0, "Failed to handle null array");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_flistxattr(p, fh, &names);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_flistxattr(p, fh, &names);
+#ifdef PR_USE_XATTR
+  fail_if(res < 0, "Failed to list xattrs for '%s': %s", fsio_test_path,
+    strerror(errno));
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+
+  pr_fsio_close(fh);
+  (void) unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_removexattr_test) {
+  int res;
+  const char *path, *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_removexattr(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_removexattr(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_removexattr(p, path, NULL);
+  fail_unless(res < 0, "Failed to handle null attribute name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_removexattr(p, path, name);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_removexattr(p, path, name);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent attribute '%s'", name);
+  fail_unless(errno == ENOENT || errno == ENOATTR || errno == ENOTSUP,
+    "Expected ENOENT (%d), ENOATTR (%d) or ENOTSUP (%d), got %s (%d)",
+    ENOENT, ENOATTR, ENOTSUP, strerror(errno), errno);
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_lremovexattr_test) {
+  int res;
+  const char *path, *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_lremovexattr(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_lremovexattr(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_lremovexattr(p, path, NULL);
+  fail_unless(res < 0, "Failed to handle null attribute name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_lremovexattr(p, path, name);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_lremovexattr(p, path, name);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent attribute '%s'", name);
+  fail_unless(errno == ENOENT || errno == ENOATTR || errno == ENOTSUP,
+    "Expected ENOENT (%d), ENOATTR (%d) or ENOTSUP (%d), got %s (%d)",
+    ENOENT, ENOATTR, ENOTSUP, strerror(errno), errno);
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_fremovexattr_test) {
+  int res;
+  pr_fh_t *fh;
+  const char *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_fremovexattr(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_fremovexattr(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_RDWR);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_fremovexattr(p, fh, NULL);
+  fail_unless(res < 0, "Failed to handle null attribute name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_fremovexattr(p, fh, name);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_fremovexattr(p, fh, name);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent attribute '%s'", name);
+  fail_unless(errno == ENOENT || errno == ENOATTR || errno == ENOTSUP,
+    "Expected ENOENT (%d), ENOATTR (%d) or ENOTSUP (%d), got %s (%d)",
+    ENOENT, ENOATTR, ENOTSUP, strerror(errno), errno);
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+
+  pr_fsio_close(fh);
+  (void) unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_setxattr_test) {
+  int res, flags;
+  const char *path, *name;
+  pr_fh_t *fh;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_setxattr(NULL, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_setxattr(p, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_setxattr(p, path, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null attribute name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+  flags = PR_FSIO_XATTR_FL_CREATE;
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_setxattr(p, path, name, NULL, 0, flags);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_setxattr(p, path, name, NULL, 0, flags);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent file '%s'", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+  pr_fsio_close(fh);
+
+  res = pr_fsio_setxattr(p, path, name, NULL, 0, flags);
+  if (res < 0) {
+    fail_unless(errno == ENOTSUP, "Expected ENOTSUP (%d), got %s (%d)", ENOTSUP,
+      strerror(errno), errno);
+  }
+
+  (void) unlink(fsio_test_path);
+#else
+  (void) fh;
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_lsetxattr_test) {
+  int res, flags;
+  const char *path, *name;
+  pr_fh_t *fh;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_lsetxattr(NULL, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_lsetxattr(p, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_test_path;
+  res = pr_fsio_lsetxattr(p, path, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null attribute name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+  flags = PR_FSIO_XATTR_FL_CREATE;
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_lsetxattr(p, path, name, NULL, 0, flags);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_lsetxattr(p, path, name, NULL, 0, flags);
+#ifdef PR_USE_XATTR
+  fail_unless(res < 0, "Failed to handle nonexistent file '%s'", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+  pr_fsio_close(fh);
+
+  res = pr_fsio_lsetxattr(p, path, name, NULL, 0, flags);
+  if (res < 0) {
+    fail_unless(errno == ENOTSUP, "Expected ENOTSUP (%d), got %s (%d)", ENOTSUP,
+      strerror(errno), errno);
+  }
+
+  (void) unlink(fsio_test_path);
+#else
+  (void) fh;
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+}
+END_TEST
+
+START_TEST (fsio_sys_fsetxattr_test) {
+  int res, flags;
+  pr_fh_t *fh;
+  const char *name;
+  unsigned long fsio_opts;
+
+  res = pr_fsio_fsetxattr(NULL, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_fsetxattr(p, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null file handle");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) unlink(fsio_test_path);
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_RDWR);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_fsetxattr(p, fh, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null attribute name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+  flags = PR_FSIO_XATTR_FL_CREATE;
+
+  fsio_opts = pr_fsio_set_options(PR_FSIO_OPT_IGNORE_XATTR);
+  res = pr_fsio_fsetxattr(p, fh, name, NULL, 0, flags);
+  fail_unless(res < 0, "Failed to handle disabled xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+
+  pr_fsio_set_options(fsio_opts);
+  res = pr_fsio_fsetxattr(p, fh, name, NULL, 0, flags);
+#ifdef PR_USE_XATTR
+  if (res < 0) {
+    fail_unless(errno == ENOTSUP, "Expected ENOTSUP (%d), got %s (%d)", ENOTSUP,
+      strerror(errno), errno);
+  }
+
+#else
+  fail_unless(res < 0, "Failed to handle --disable-xattr");
+  fail_unless(errno == ENOSYS, "Expected ENOSYS (%d), got %s (%d)", ENOSYS,
+    strerror(errno), errno);
+#endif /* PR_USE_XATTR */
+
+  pr_fsio_close(fh);
+  (void) unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_mkdir_test) {
+  int res;
+  mode_t mode = 0755;
+
+  res = pr_fsio_mkdir(NULL, mode);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_mkdir(fsio_testdir_path, mode);
+  fail_unless(res == 0, "Failed to create '%s': %s", fsio_testdir_path,
+    strerror(errno));
+
+  (void) pr_fsio_rmdir(fsio_testdir_path);
+}
+END_TEST
+
+START_TEST (fsio_sys_mkdir_chroot_guard_test) {
+  int res;
+  mode_t mode = 0755;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+  
+  res = pr_fsio_mkdir("/etc/foo.bar.baz.d", mode);
+  fail_unless(res < 0, "Created /etc/foo.bar.baz.d unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_mkdir("/lib/foo/bar/baz.d", mode);
+  fail_unless(res < 0, "Created /lib/foo/bar/baz.d unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_rmdir_test) {
+  int res;
+  mode_t mode = 0755;
+
+  res = pr_fsio_rmdir(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_rmdir(fsio_testdir_path);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  res = pr_fsio_mkdir(fsio_testdir_path, mode);
+  fail_unless(res == 0, "Failed to create '%s': %s", fsio_testdir_path,
+    strerror(errno));
+
+  res = pr_fsio_rmdir(fsio_testdir_path);
+  fail_unless(res == 0, "Failed to remove '%s': %s", fsio_testdir_path,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fsio_sys_rmdir_chroot_guard_test) {
+  int res;
+
+  res = pr_fsio_guard_chroot(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE (%d), got %d", FALSE, res);
+
+  res = pr_fsio_rmdir("/etc/foo.bar.baz.d");
+  fail_unless(res < 0, "Removed /etc/foo.bar.baz.d unexpectedly");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s %d", EACCES,
+    strerror(errno), errno);
+
+  (void) pr_fsio_guard_chroot(FALSE);
+
+  res = pr_fsio_rmdir("/lib/foo/bar/baz.d");
+  fail_unless(res < 0, "Removed /lib/etc/foo.bar.baz.d unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s %d", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_sys_chdir_test) {
+  int res;
+
+  res = pr_fsio_chdir(NULL, FALSE);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_chdir("/etc/resolv.conf", FALSE);
+  fail_unless(res < 0, "Failed to handle file argument");
+  fail_unless(errno == EINVAL || errno == ENOTDIR,
+    "Expected EINVAL (%d) or ENOTDIR (%d), got %s (%d)", EINVAL, ENOTDIR,
+    strerror(errno), errno);
+
+  res = pr_fsio_chdir("/tmp", FALSE);
+  fail_unless(res == 0, "Failed to chdir to '%s': %s", fsio_cwd,
+    strerror(errno));
+
+  res = pr_fsio_chdir(fsio_cwd, FALSE);
+  fail_unless(res == 0, "Failed to chdir to '%s': %s", fsio_cwd,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fsio_sys_chdir_canon_test) {
+  int res;
+
+  res = pr_fsio_chdir_canon(NULL, FALSE);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_chdir_canon("/tmp", FALSE);
+  fail_unless(res == 0, "Failed to chdir to '%s': %s", fsio_cwd,
+    strerror(errno));
+
+  res = pr_fsio_chdir_canon(fsio_cwd, FALSE);
+  fail_unless(res == 0, "Failed to chdir to '%s': %s", fsio_cwd,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fsio_sys_chroot_test) {
+  int res;
+
+  res = pr_fsio_chroot(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  if (getuid() != 0) {
+    res = pr_fsio_chroot("/tmp");
+    fail_unless(res < 0, "Failed to chroot without root privs");
+    fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+START_TEST (fsio_sys_opendir_test) {
+  void *res = NULL, *res2 = NULL;
+  const char *path;
+
+  mark_point();
+  res = pr_fsio_opendir(NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno); 
+
+  mark_point();
+  path = "/etc/resolv.conf";
+  res = pr_fsio_opendir(path);
+  fail_unless(res == NULL, "Failed to handle file argument");
+  fail_unless(errno == ENOTDIR, "Expected ENOTDIR (%d), got %s (%d)", ENOTDIR,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/tmp/";
+  res = pr_fsio_opendir(path);
+  fail_unless(res != NULL, "Failed to open '%s': %s", path, strerror(errno));
+
+  mark_point();
+  path = "/usr/";
+  res2 = pr_fsio_opendir(path);
+  fail_unless(res != NULL, "Failed to open '%s': %s", path, strerror(errno));
+
+  (void) pr_fsio_closedir(res);
+  (void) pr_fsio_closedir(res2);
+}
+END_TEST
+
+START_TEST (fsio_sys_readdir_test) {
+  void *dirh;
+  struct dirent *dent;
+
+  dent = pr_fsio_readdir(NULL);
+  fail_unless(dent == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  dent = pr_fsio_readdir("/etc/resolv.conf");
+  fail_unless(dent == NULL, "Failed to handle file argument");
+  fail_unless(errno == ENOTDIR, "Expected ENOTDIR (%d), got %s (%d)", ENOTDIR,
+    strerror(errno), errno);
+
+  mark_point();
+  dirh = pr_fsio_opendir("/tmp/");
+  fail_unless(dirh != NULL, "Failed to open '/tmp/': %s", strerror(errno));
+
+  dent = pr_fsio_readdir(dirh);
+  fail_unless(dent != NULL, "Failed to read directory entry: %s",
+    strerror(errno));
+
+  (void) pr_fsio_closedir(dirh);
+}
+END_TEST
+
+START_TEST (fsio_sys_closedir_test) {
+  void *dirh;
+  int res;
+
+  res = pr_fsio_closedir(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  dirh = pr_fsio_opendir("/tmp/");
+  fail_unless(dirh != NULL, "Failed to open '/tmp/': %s", strerror(errno));
+
+  res = pr_fsio_closedir(dirh);
+  fail_unless(res == 0, "Failed to close '/tmp/': %s", strerror(errno));
+
+  /* Closing an already-closed directory descriptor should fail. */
+  res = pr_fsio_closedir(dirh);
+  fail_unless(res < 0, "Failed to handle already-closed directory handle");
+  fail_unless(errno == ENOTDIR, "Expected ENOTDIR (%d), got %s (%d)", ENOTDIR,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (fsio_statcache_clear_cache_test) {
+  int expected, res;
+  struct stat st;
+  char *cwd;
+
+  mark_point();
+  pr_fs_clear_cache();
+
+  res = pr_fs_clear_cache2("/testsuite");
+  fail_unless(res == 0, "Failed to clear cache: %s", strerror(errno));
+
+  res = pr_fsio_stat("/tmp", &st);
+  fail_unless(res == 0, "Failed to stat '/tmp': %s", strerror(errno));
+
+  res = pr_fs_clear_cache2("/tmp");
+  expected = 1;
+  fail_unless(res == expected, "Expected %d, got %d", expected, res);
+
+  res = pr_fs_clear_cache2("/testsuite");
+  expected = 0;
+  fail_unless(res == expected, "Expected %d, got %d", expected, res);
+
+  res = pr_fsio_stat("/tmp", &st);
+  fail_unless(res == 0, "Failed to stat '/tmp': %s", strerror(errno));
+
+  res = pr_fsio_lstat("/tmp", &st);
+  fail_unless(res == 0, "Failed to lstat '/tmp': %s", strerror(errno));
+
+  res = pr_fs_clear_cache2("/tmp");
+  expected = 2;
+  fail_unless(res == expected, "Expected %d, got %d", expected, res);
+
+  res = pr_fsio_stat("/tmp", &st);
+  fail_unless(res == 0, "Failed to stat '/tmp': %s", strerror(errno));
+
+  res = pr_fsio_lstat("/tmp", &st);
+  fail_unless(res == 0, "Failed to lstat '/tmp': %s", strerror(errno));
+
+  cwd = getcwd(NULL, 0);
+  fail_unless(cwd != NULL, "Failed to get cwd: %s", strerror(errno));
+
+  res = pr_fs_setcwd("/");
+  fail_unless(res == 0, "Failed to set cwd to '/': %s", strerror(errno));
+
+  res = pr_fs_clear_cache2("tmp");
+  expected = 2;
+  fail_unless(res == expected, "Expected %d, got %d", expected, res);
+
+  res = pr_fs_setcwd(cwd);
+  fail_unless(res == 0, "Failed to set cwd to '%s': %s", cwd, strerror(errno)); 
+
+  free(cwd);
+}
+END_TEST
+
+START_TEST (fsio_statcache_cache_hit_test) {
+  int res;
+  struct stat st;
+
+  /* First is a cache miss...*/
+  res = pr_fsio_stat("/tmp", &st);
+  fail_unless(res == 0, "Failed to stat '/tmp': %s", strerror(errno));
+
+  /* This is a cache hit, hopefully. */
+  res = pr_fsio_stat("/tmp", &st);
+  fail_unless(res == 0, "Failed to stat '/tmp': %s", strerror(errno));
+
+  pr_fs_clear_cache();
+}
+END_TEST
+
+START_TEST (fsio_statcache_negative_cache_test) {
+  int res;
+  struct stat st;
+
+  /* First is a cache miss...*/
+  res = pr_fsio_stat("/foo.bar.baz.d", &st);
+  fail_unless(res < 0, "Check of '/foo.bar.baz.d' succeeded unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* This is a cache hit, hopefully. */
+  res = pr_fsio_stat("/foo.bar.baz.d", &st);
+  fail_unless(res < 0, "Check of '/foo.bar.baz.d' succeeded unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pr_fs_clear_cache();
+}
+END_TEST
+
+START_TEST (fsio_statcache_expired_test) {
+  unsigned int cache_size, max_age;
+  int res;
+  struct stat st;
+
+  cache_size = max_age = 1;
+  pr_fs_statcache_set_policy(cache_size, max_age, 0);
+
+  /* First is a cache miss...*/
+  res = pr_fsio_stat("/tmp", &st);
+  fail_unless(res == 0, "Failed to stat '/tmp': %s", strerror(errno));
+
+  /* Wait for that cached data to expire...*/
+  sleep(max_age + 1);
+
+  /* This is another cache miss, hopefully. */
+  res = pr_fsio_stat("/tmp2", &st);
+  fail_unless(res < 0, "Check of '/tmp2' succeeded unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pr_fs_clear_cache();
+}
+END_TEST
+
+START_TEST (fsio_statcache_dump_test) {
+  mark_point();
+  pr_fs_statcache_dump();
+}
+END_TEST
+
+START_TEST (fs_create_fs_test) {
+  pr_fs_t *fs;
+
+  fs = pr_create_fs(NULL, NULL);
+  fail_unless(fs == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_create_fs(p, NULL);
+  fail_unless(fs == NULL, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_create_fs(p, "testsuite");
+  fail_unless(fs != NULL, "Failed to create FS: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_insert_fs_test) {
+  pr_fs_t *fs, *fs2;
+  int res;
+
+  res = pr_insert_fs(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_create_fs(p, "testsuite");
+  fail_unless(fs != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs, NULL);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_insert_fs(fs, "/testsuite");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs, "/testsuite");
+  fail_unless(res == FALSE, "Failed to handle duplicate paths");
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  fs2 = pr_create_fs(p, "testsuite2");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs2, "/testsuite2");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  fs2 = pr_create_fs(p, "testsuite3");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  /* Push this FS on top of the previously registered path; FSes can be
+   * stacked like this.
+   */
+  res = pr_insert_fs(fs2, "/testsuite2");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  (void) pr_remove_fs("/testsuite");
+  (void) pr_remove_fs("/testsuite2");
+  (void) pr_remove_fs("/testsuite3");
+}
+END_TEST
+
+START_TEST (fs_get_fs_test) {
+  pr_fs_t *fs, *fs2, *fs3;
+  int exact_match = FALSE, res;
+
+  fs = pr_get_fs(NULL, NULL);
+  fail_unless(fs == NULL, "Failed to handle null arguments");
+
+  fs = pr_get_fs("/testsuite", &exact_match);
+  fail_unless(fs != NULL, "Failed to get FS: %s", strerror(errno));
+  fail_unless(exact_match == FALSE, "Expected FALSE, got TRUE");
+
+  fs2 = pr_create_fs(p, "testsuite");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs2, "/testsuite");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  fs = pr_get_fs("/testsuite", &exact_match);
+  fail_unless(fs != NULL, "Failed to get FS: %s", strerror(errno));
+  fail_unless(exact_match == TRUE, "Expected TRUE, got FALSE");
+
+  fs3 = pr_create_fs(p, "testsuite2");
+  fail_unless(fs3 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs3, "/testsuite2/");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  exact_match = FALSE;
+  fs = pr_get_fs("/testsuite2/foo/bar/baz", &exact_match);
+  fail_unless(fs != NULL, "Failed to get FS: %s", strerror(errno));
+  fail_unless(exact_match == FALSE, "Expected FALSE, got TRUE");
+
+  (void) pr_remove_fs("/testsuite2");
+  (void) pr_remove_fs("/testsuite");
+}
+END_TEST
+
+START_TEST (fs_unmount_fs_test) {
+  pr_fs_t *fs, *fs2;
+  int res;
+
+  fs = pr_unmount_fs(NULL, NULL);
+  fail_unless(fs == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_unmount_fs("/testsuite", NULL);
+  fail_unless(fs == NULL, "Failed to handle absent FS");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  fs2 = pr_create_fs(p, "testsuite");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs2, "/testsuite");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  fs = pr_unmount_fs("/testsuite", "foo bar");
+  fail_unless(fs == NULL, "Failed to mismatched path AND name");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fs = pr_unmount_fs("/testsuite2", NULL);
+  fail_unless(fs == NULL, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fs2 = pr_unmount_fs("/testsuite", NULL);
+  fail_unless(fs2 != NULL, "Failed to unmount '/testsuite': %s",
+    strerror(errno));
+
+  fs2 = pr_create_fs(p, "testsuite");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs2, "/testsuite");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  fs2 = pr_create_fs(p, "testsuite2");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs2, "/testsuite");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  fs2 = pr_unmount_fs("/testsuite", NULL);
+  fail_unless(fs2 != NULL, "Failed to unmount '/testsuite': %s",
+    strerror(errno));
+
+  fs2 = pr_unmount_fs("/testsuite", NULL);
+  fail_unless(fs2 != NULL, "Failed to unmount '/testsuite': %s",
+    strerror(errno));
+
+  (void) pr_remove_fs("/testsuite");
+  (void) pr_remove_fs("/testsuite");
+}
+END_TEST
+
+START_TEST (fs_remove_fs_test) {
+  pr_fs_t *fs, *fs2;
+  int res;
+
+  fs = pr_remove_fs(NULL);
+  fail_unless(fs == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_remove_fs("/testsuite");
+  fail_unless(fs == NULL, "Failed to handle absent FS");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  fs2 = pr_create_fs(p, "testsuite");
+  fail_unless(fs2 != NULL, "Failed to create FS: %s", strerror(errno));
+
+  res = pr_insert_fs(fs2, "/testsuite");
+  fail_unless(res == TRUE, "Failed to insert FS: %s", strerror(errno));
+
+  fs = pr_remove_fs("/testsuite2");
+  fail_unless(fs == NULL, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fs2 = pr_remove_fs("/testsuite");
+  fail_unless(fs2 != NULL, "Failed to remove '/testsuite': %s",
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_register_fs_test) {
+  pr_fs_t *fs, *fs2;
+
+  fs = pr_register_fs(NULL, NULL, NULL);
+  fail_unless(fs == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_register_fs(p, NULL, NULL);
+  fail_unless(fs == NULL, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_register_fs(p, "testsuite", NULL);
+  fail_unless(fs == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fs = pr_register_fs(p, "testsuite", "/testsuite");
+  fail_unless(fs != NULL, "Failed to register FS: %s", strerror(errno));
+
+  fs2 = pr_register_fs(p, "testsuite", "/testsuite");
+  fail_unless(fs2 == NULL, "Failed to handle duplicate names");
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  (void) pr_remove_fs("/testsuite");
+}
+END_TEST
+
+START_TEST (fs_unregister_fs_test) {
+  pr_fs_t *fs;
+  int res;
+
+  res = pr_unregister_fs(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_unregister_fs("/testsuite");
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fs = pr_register_fs(p, "testsuite", "/testsuite");
+  fail_unless(fs != NULL, "Failed to register FS: %s", strerror(errno));
+
+  res = pr_unregister_fs("/testsuite");
+  fail_unless(res == 0, "Failed to unregister '/testsuite': %s",
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_resolve_fs_map_test) {
+  pr_fs_t *fs;
+  int res;
+
+  mark_point();
+  pr_resolve_fs_map();
+
+  fs = pr_register_fs(p, "testsuite", "/testsuite");
+  fail_unless(fs != NULL, "Failed to register FS: %s", strerror(errno));
+
+  mark_point();
+  pr_resolve_fs_map();
+
+  res = pr_unregister_fs("/testsuite");
+  fail_unless(res == 0, "Failed to unregister '/testsuite': %s",
+    strerror(errno));
+
+  mark_point();
+  pr_resolve_fs_map();
+}
+END_TEST
+
+#if defined(PR_USE_DEVEL)
+START_TEST (fs_dump_fs_test) {
+  pr_fs_t *fs, *root_fs;
+
+  mark_point();
+  pr_fs_dump(NULL);
+
+  root_fs = pr_get_fs("/", NULL);
+  fs = pr_register_fs(p, "testsuite", "/testsuite");
+
+  mark_point();
+  pr_fs_dump(NULL);
+
+  fs->stat = root_fs->stat;
+  fs->fstat = root_fs->fstat;
+  fs->lstat = root_fs->lstat;
+  fs->rename = root_fs->rename;
+  fs->unlink = root_fs->unlink;
+  fs->open = root_fs->open;
+  fs->close = root_fs->close;
+  fs->read = root_fs->read;
+  fs->write = root_fs->write;
+  fs->lseek = root_fs->lseek;
+  fs->link = root_fs->link;
+  fs->readlink = root_fs->readlink;
+  fs->symlink = root_fs->symlink;
+  fs->ftruncate = root_fs->ftruncate;
+  fs->truncate = root_fs->truncate;
+  fs->chmod = root_fs->chmod;
+  fs->fchmod = root_fs->fchmod;
+  fs->chown = root_fs->chown;
+  fs->fchown = root_fs->fchown;
+  fs->lchown = root_fs->lchown;
+  fs->access = root_fs->access;
+  fs->faccess = root_fs->faccess;
+  fs->utimes = root_fs->utimes;
+  fs->futimes = root_fs->futimes;
+  fs->fsync = root_fs->fsync;
+  fs->chdir = root_fs->chdir;
+  fs->chroot = root_fs->chroot;
+  fs->opendir = root_fs->opendir;
+  fs->closedir = root_fs->closedir;
+  fs->readdir = root_fs->readdir;
+  fs->mkdir = root_fs->mkdir;
+  fs->rmdir = root_fs->rmdir;
+
+  mark_point();
+  pr_fs_dump(NULL);
+
+  pr_unregister_fs("/testsuite");
+}
+END_TEST
+#endif /* PR_USE_DEVEL */
+
+static int fsio_chroot_cb(pr_fs_t *fs, const char *path) {
+  return 0;
+}
+
+START_TEST (fsio_custom_chroot_test) {
+  pr_fs_t *fs;
+  int res;
+  const char *path;
+
+  fs = pr_register_fs(p, "custom", "/testsuite/");
+  fail_unless(fs != NULL, "Failed to register custom FS: %s", strerror(errno));
+
+  fs->chroot = fsio_chroot_cb;
+
+  mark_point();
+  pr_resolve_fs_map();
+
+  path = "/testsuite/foo/bar";
+  res = pr_fsio_chroot(path);
+  fail_unless(res == 0, "Failed to chroot (via custom FS) to '%s': %s", path,
+    strerror(errno));
+
+  pr_unregister_fs("/testsuite");
+}
+END_TEST
+
+START_TEST (fs_clean_path_test) {
+  char res[PR_TUNABLE_PATH_MAX+1], *path, *expected;
+
+  mark_point();
+  pr_fs_clean_path(NULL, NULL, 0);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+
+  mark_point();
+  pr_fs_clean_path(path, NULL, 0);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res[sizeof(res)-1] = '\0';
+
+  mark_point();
+  pr_fs_clean_path(path, res, 0);
+
+  pr_fs_clean_path(path, res, sizeof(res)-1);
+  fail_unless(strcmp(res, path) == 0, "Expected cleaned path '%s', got '%s'",
+    path, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "/test.txt";
+  pr_fs_clean_path(path, res, sizeof(res)-1);
+  fail_unless(strcmp(res, path) == 0, "Expected cleaned path '%s', got '%s'",
+    path, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "/test.txt";
+  pr_fs_clean_path(path, res, sizeof(res)-1);
+  fail_unless(strcmp(res, path) == 0, "Expected cleaned path '%s', got '%s'",
+    path, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "/./test.txt";
+  pr_fs_clean_path(path, res, sizeof(res)-1);
+  expected = "/test.txt";
+  fail_unless(strcmp(res, expected) == 0,
+    "Expected cleaned path '%s', got '%s'", expected, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "test.txt";
+  pr_fs_clean_path(path, res, sizeof(res)-1);
+  expected = "/test.txt";
+  fail_unless(strcmp(res, expected) == 0,
+    "Expected cleaned path '%s', got '%s'", path, res);
+}
+END_TEST
+
+START_TEST (fs_clean_path2_test) {
+  char res[PR_TUNABLE_PATH_MAX+1], *path, *expected;
+  int code;
+
+  mark_point();
+  code = pr_fs_clean_path2(NULL, NULL, 0, 0);
+  fail_unless(code < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+
+  mark_point();
+  code = pr_fs_clean_path2(path, NULL, 0, 0);
+  fail_unless(code < 0, "Failed to handle null buf");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res[sizeof(res)-1] = '\0';
+
+  mark_point();
+  code = pr_fs_clean_path2(path, res, 0, 0);
+  fail_unless(code == 0, "Failed to handle zero length buf: %s",
+    strerror(errno));
+
+  res[sizeof(res)-1] = '\0';
+  path = "test.txt";
+  code = pr_fs_clean_path2(path, res, sizeof(res)-1, 0);
+  fail_unless(code == 0, "Failed to clean path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected cleaned path '%s', got '%s'",
+    path, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "/./test.txt";
+  code = pr_fs_clean_path2(path, res, sizeof(res)-1, 0);
+  fail_unless(code == 0, "Failed to clean path '%s': %s", path,
+    strerror(errno));
+  expected = "/test.txt";
+  fail_unless(strcmp(res, expected) == 0,
+    "Expected cleaned path '%s', got '%s'", expected, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "test.d///test.txt";
+  code = pr_fs_clean_path2(path, res, sizeof(res)-1, 0);
+  fail_unless(code == 0, "Failed to clean path '%s': %s", path,
+    strerror(errno));
+  expected = "test.d/test.txt";
+  fail_unless(strcmp(res, expected) == 0,
+    "Expected cleaned path '%s', got '%s'", expected, res);
+
+  res[sizeof(res)-1] = '\0';
+  path = "/test.d///test.txt";
+  code = pr_fs_clean_path2(path, res, sizeof(res)-1,
+    PR_FSIO_CLEAN_PATH_FL_MAKE_ABS_PATH);
+  fail_unless(code == 0, "Failed to clean path '%s': %s", path,
+    strerror(errno));
+  expected = "/test.d/test.txt";
+  fail_unless(strcmp(res, expected) == 0,
+    "Expected cleaned path '%s', got '%s'", expected, res);
+}
+END_TEST
+
+START_TEST (fs_dircat_test) {
+  char buf[PR_TUNABLE_PATH_MAX+1], *a, *b, *ok;
+  int res;
+
+  res = pr_fs_dircat(NULL, 0, NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL,
+    "Failed to set errno to EINVAL for null arguments");
+
+  res = pr_fs_dircat(buf, 0, "foo", "bar");
+  fail_unless(res == -1, "Failed to handle zero-length buffer");
+  fail_unless(errno == EINVAL,
+    "Failed to set errno to EINVAL for zero-length buffer");
+
+  res = pr_fs_dircat(buf, -1, "foo", "bar");
+  fail_unless(res == -1, "Failed to handle negative-length buffer");
+  fail_unless(errno == EINVAL,
+    "Failed to set errno to EINVAL for negative-length buffer");
+
+  a = pcalloc(p, PR_TUNABLE_PATH_MAX);
+  memset(a, 'A', PR_TUNABLE_PATH_MAX-1);
+
+  b = "foo";
+
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == -1, "Failed to handle too-long paths");
+  fail_unless(errno == ENAMETOOLONG,
+    "Failed to set errno to ENAMETOOLONG for too-long paths");
+
+  a = "foo";
+  b = "/bar";
+  ok = b;
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == 0, "Failed to concatenate abs-path path second dir");
+  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
+    ok, buf);
+ 
+  a = "foo";
+  b = "bar";
+  ok = "foo/bar";
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == 0, "Failed to concatenate two normal paths");
+  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
+    ok, buf);
+ 
+  a = "foo/";
+  b = "bar";
+  ok = "foo/bar";
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == 0, "Failed to concatenate first dir with trailing slash");
+  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
+    ok, buf);
+
+  a = "";
+  b = "";
+  ok = "/";
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == 0, "Failed to concatenate two empty paths");
+  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
+    ok, buf);
+
+  a = "/foo";
+  b = "";
+  ok = "/foo/";
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == 0, "Failed to concatenate two empty paths");
+  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
+    ok, buf);
+
+  a = "";
+  b = "/bar";
+  ok = "/bar/";
+  res = pr_fs_dircat(buf, sizeof(buf)-1, a, b);
+  fail_unless(res == 0, "Failed to concatenate two empty paths");
+  fail_unless(strcmp(buf, ok) == 0, "Expected concatenated dir '%s', got '%s'",
+    ok, buf);
+}
+END_TEST
+
+START_TEST (fs_setcwd_test) {
+  int res;
+  const char *wd;
+
+  /* Make sure that we don't segfault if we call pr_fs_setcwd() on the
+   * buffer that it is already using.
+   */
+  res = pr_fs_setcwd(pr_fs_getcwd());
+  fail_unless(res == 0, "Failed to set cwd to '%s': %s", pr_fs_getcwd(),
+    strerror(errno));
+
+  wd = pr_fs_getcwd();
+  fail_unless(wd != NULL, "Failed to get working directory: %s",
+    strerror(errno));
+  fail_unless(strcmp(wd, fsio_cwd) == 0,
+    "Expected '%s', got '%s'", fsio_cwd, wd);
+
+  wd = pr_fs_getvwd();
+  fail_unless(wd != NULL, "Failed to get working directory: %s",
+    strerror(errno));
+  fail_unless(strcmp(wd, "/") == 0, "Expected '/', got '%s'", wd);
+}
+END_TEST
+
+START_TEST (fs_glob_test) {
+  glob_t pglob;
+  int res;
+
+  res = pr_fs_glob(NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_glob(NULL, 0, NULL, &pglob);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(&pglob, 0, sizeof(pglob));
+  res = pr_fs_glob("?", 0, NULL, &pglob);
+  fail_unless(res == 0, "Failed to glob: %s", strerror(errno));
+  fail_unless(pglob.gl_pathc > 0, "Expected >0, got %lu",
+    (unsigned long) pglob.gl_pathc);
+
+  mark_point();
+  pr_fs_globfree(NULL);
+  if (res == 0) {
+    pr_fs_globfree(&pglob);
+  }
+}
+END_TEST
+
+START_TEST (fs_copy_file_test) {
+  int res;
+  char *src_path, *dst_path, *text;
+  pr_fh_t *fh;
+
+  res = pr_fs_copy_file(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  src_path = "/tmp/prt-fs-src.dat";
+  res = pr_fs_copy_file(src_path, NULL);
+  fail_unless(res < 0, "Failed to handle null destination path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  dst_path = "/tmp/prt-fs-dst.dat";
+  res = pr_fs_copy_file(src_path, dst_path);
+  fail_unless(res < 0, "Failed to handle null destination path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  res = pr_fs_copy_file("/tmp", dst_path);
+  fail_unless(res < 0, "Failed to handle directory source path");
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(src_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", src_path, strerror(errno));
+
+  text = "Hello, World!\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s' to '%s': %s", text, src_path,
+    strerror(errno));
+
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to close '%s': %s", src_path, strerror(errno));
+
+  res = pr_fs_copy_file(src_path, "/tmp");
+  fail_unless(res < 0, "Failed to handle directory destination path");
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  res = pr_fs_copy_file(src_path, "/tmp/foo/bar/baz/quxx/quzz.dat");
+  fail_unless(res < 0, "Failed to handle nonexistent destination path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_copy_file(src_path, src_path);
+  fail_unless(res == 0, "Failed to copy file to itself: %s", strerror(errno));
+
+  mark_point();
+  res = pr_fs_copy_file(src_path, dst_path);
+  fail_unless(res == 0, "Failed to copy file: %s", strerror(errno));
+
+  (void) pr_fsio_unlink(src_path);
+  (void) pr_fsio_unlink(dst_path);
+}
+END_TEST
+
+static unsigned int copy_progress_iter = 0;
+static void copy_progress_cb(int nwritten) {
+  copy_progress_iter++;
+}
+
+START_TEST (fs_copy_file2_test) {
+  int res, flags;
+  char *src_path, *dst_path, *text;
+  pr_fh_t *fh;
+
+  src_path = "/tmp/prt-fs-src.dat";
+  dst_path = "/tmp/prt-fs-dst.dat";
+  flags = PR_FSIO_COPY_FILE_FL_NO_DELETE_ON_FAILURE;
+
+  fh = pr_fsio_open(src_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", src_path, strerror(errno));
+
+  text = "Hello, World!\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s' to '%s': %s", text, src_path,
+    strerror(errno));
+
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to close '%s': %s", src_path, strerror(errno));
+
+  copy_progress_iter = 0;
+
+  mark_point();
+  res = pr_fs_copy_file2(src_path, dst_path, flags, copy_progress_cb);
+  fail_unless(res == 0, "Failed to copy file: %s", strerror(errno));
+
+  (void) pr_fsio_unlink(src_path);
+  (void) pr_fsio_unlink(dst_path);
+
+  fail_unless(copy_progress_iter > 0, "Unexpected progress callback count (%u)",
+    copy_progress_iter);
+}
+END_TEST
+
+START_TEST (fs_interpolate_test) {
+  int res;
+  char buf[PR_TUNABLE_PATH_MAX], *path;
+
+  memset(buf, '\0', sizeof(buf));
+
+  res = pr_fs_interpolate(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_interpolate(path, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_interpolate(path, buf, 0);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_interpolate(path, buf, sizeof(buf)-1);
+  fail_unless(res == 1, "Failed to interpolate path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(buf, path) == 0, "Expected '%s', got '%s'", path, buf);
+
+  path = "~/foo/bar/baz/quzz/quzz.d";
+  res = pr_fs_interpolate(path, buf, sizeof(buf)-1);
+  fail_unless(res == 1, "Failed to interpolate path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(buf, path+1) == 0, "Expected '%s', got '%s'", path+1, buf);
+
+  path = "~";
+  res = pr_fs_interpolate(path, buf, sizeof(buf)-1);
+  fail_unless(res == 1, "Failed to interpolate path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(buf, "/") == 0, "Expected '/', got '%s'", buf);
+
+  session.chroot_path = "/tmp";
+  res = pr_fs_interpolate(path, buf, sizeof(buf)-1);
+  fail_unless(res == 1, "Failed to interpolate path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(buf, session.chroot_path) == 0, "Expected '%s', got '%s'",
+    session.chroot_path, buf);
+
+  session.chroot_path = NULL;
+
+  path = "~foo.bar.baz.quzz";
+  res = pr_fs_interpolate(path, buf, sizeof(buf)-1);
+  fail_unless(res < 0, "Interpolated '%s' unexpectedly", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  session.user = "testsuite";
+  path = "~/tmp.d/test.d/foo.d/bar.d";
+  res = pr_fs_interpolate(path, buf, sizeof(buf)-1);
+  fail_unless(res < 0, "Interpolated '%s' unexpectedly", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+  session.user = NULL;
+}
+END_TEST
+
+START_TEST (fs_resolve_partial_test) {
+  int op = FSIO_FILE_STAT, res;
+  char buf[PR_TUNABLE_PATH_MAX], *path;
+
+  res = pr_fs_resolve_partial(NULL, NULL, 0, op);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_resolve_partial(path, NULL, 0, op);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_fs_resolve_partial(path, buf, 0, op);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_resolve_partial(path, buf, sizeof(buf)-1, op);
+  fail_unless(res == 0, "Failed to resolve '%s': %s", path, strerror(errno));
+  if (strcmp(buf, path) != 0) {
+    /* Mac-specific hack */
+    const char *prefix = "/private";
+
+    if (strncmp(buf, prefix, strlen(prefix)) != 0) {
+      fail("Expected '%s', got '%s'", path, buf);
+    }
+  }
+
+  path = "/tmp/.////./././././.";
+  res = pr_fs_resolve_partial(path, buf, sizeof(buf)-1, op);
+  fail_unless(res == 0, "Failed to resolve '%s': %s", path, strerror(errno));
+  if (strcmp(buf, path) != 0) {
+    /* Mac-specific hack */
+    const char *prefix = "/private";
+
+    if (strncmp(buf, prefix, strlen(prefix)) != 0 &&
+        strcmp(buf, "/tmp/") != 0) {
+      fail("Expected '%s', got '%s'", path, buf);
+    }
+  }
+
+  path = "/../../../.././..///../";
+  res = pr_fs_resolve_partial(path, buf, sizeof(buf)-1, op);
+  fail_unless(res == 0, "Failed to resolve '%s': %s", path, strerror(errno));
+  if (strcmp(buf, "/") != 0) {
+    /* Mac-specific hack */
+    const char *prefix = "/private";
+
+    if (strncmp(buf, prefix, strlen(prefix)) != 0) {
+      fail("Expected '%s', got '%s'", path, buf);
+    }
+  }
+
+  path = "/tmp/.///../../..../";
+  res = pr_fs_resolve_partial(path, buf, sizeof(buf)-1, op);
+  fail_unless(res < 0, "Resolved '%s' unexpectedly", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  path = "~foo/.///../../..../";
+  res = pr_fs_resolve_partial(path, buf, sizeof(buf)-1, op);
+  fail_unless(res < 0, "Resolved '%s' unexpectedly", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  path = "../../..../";
+  res = pr_fs_resolve_partial(path, buf, sizeof(buf)-1, op);
+  fail_unless(res < 0, "Resolved '%s' unexpectedly", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Resolve a symlink path */
+  res = pr_fsio_symlink("/tmp", fsio_link_path);
+  fail_unless(res == 0, "Failed to create symlink to '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  res = pr_fs_resolve_partial(fsio_link_path, buf, sizeof(buf)-1, op);
+  fail_unless(res == 0, "Failed to resolve '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  (void) unlink(fsio_link_path);
+}
+END_TEST
+
+START_TEST (fs_resolve_path_test) {
+  int op = FSIO_FILE_STAT, res;
+  char buf[PR_TUNABLE_PATH_MAX], *path;
+
+  memset(buf, '\0', sizeof(buf));
+
+  res = pr_fs_resolve_path(NULL, NULL, 0, op);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_resolve_path(path, NULL, 0, op);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_resolve_path(path, buf, 0, op);
+  fail_unless(res < 0, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_resolve_path(path, buf, sizeof(buf)-1, op);
+  fail_unless(res == 0, "Failed to resolve path '%s': %s", path,
+    strerror(errno));
+  if (strcmp(buf, path) != 0) {
+    /* Mac-specific hack */
+    const char *prefix = "/private";
+
+    if (strncmp(buf, prefix, strlen(prefix)) != 0) {
+      fail("Expected '%s', got '%s'", path, buf);
+    }
+  }
+
+  /* Resolve a symlink path */
+  res = pr_fsio_symlink("/tmp", fsio_link_path);
+  fail_unless(res == 0, "Failed to create symlink to '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  res = pr_fs_resolve_path(fsio_link_path, buf, sizeof(buf)-1, op);
+  fail_unless(res == 0, "Failed to resolve '%s': %s", fsio_link_path,
+    strerror(errno));
+
+  (void) unlink(fsio_link_path);
+}
+END_TEST
+
+START_TEST (fs_use_encoding_test) {
+  int res;
+
+  res = pr_fs_use_encoding(-1);
+  fail_unless(res < 0, "Failed to handle invalid setting");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_use_encoding(TRUE);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  res = pr_fs_use_encoding(FALSE);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  res = pr_fs_use_encoding(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+}
+END_TEST
+
+START_TEST (fs_decode_path2_test) {
+  int flags = 0;
+  char junk[32], *res;
+  const char *path;
+
+  res = pr_fs_decode_path(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_decode_path(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_decode_path2(p, path, flags);
+  fail_unless(res != NULL, "Failed to decode path '%s': %s", path,
+    strerror(errno));
+
+  path = "/tmp";
+  res = pr_fs_decode_path2(p, path, flags);
+  fail_unless(res != NULL, "Failed to decode path '%s': %s", path,
+    strerror(errno));
+
+  /* Test a path that cannot be decoded, using junk data from the stack */
+  junk[sizeof(junk)-1] = '\0';
+  path = junk;
+  res = pr_fs_decode_path2(p, path, flags);
+  fail_unless(res != NULL, "Failed to decode path: %s", strerror(errno));
+
+  /* XXX Use the FSIO_DECODE_FL_TELL_ERRORS flags, AND set the encode
+   * policy to use PR_ENCODE_POLICY_FL_REQUIRE_VALID_ENCODING.
+   */
+}
+END_TEST
+
+START_TEST (fs_encode_path_test) {
+  char junk[32], *res;
+  const char *path;
+
+  res = pr_fs_encode_path(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_encode_path(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_encode_path(p, path);
+  fail_unless(res != NULL, "Failed to encode path '%s': %s", path,
+    strerror(errno));
+
+  /* Test a path that cannot be encoded, using junk data from the stack */
+  junk[sizeof(junk)-1] = '\0';
+  path = junk;
+  res = pr_fs_encode_path(p, path);
+  fail_unless(res != NULL, "Failed to encode path: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_split_path_test) {
+  array_header *res;
+  const char *path, *elt;
+
+  mark_point();
+  res = pr_fs_split_path(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_split_path(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res == NULL, "Failed to handle empty path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res != NULL, "Failed to split path '%s': %s", path,
+    strerror(errno));
+  fail_unless(res->nelts == 1, "Expected 1, got %u", res->nelts);
+  elt = ((char **) res->elts)[0];
+  fail_unless(strcmp(elt, "/") == 0, "Expected '/', got '%s'", elt);
+
+  path = "///";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res != NULL, "Failed to split path '%s': %s", path,
+    strerror(errno));
+  fail_unless(res->nelts == 1, "Expected 1, got %u", res->nelts);
+  elt = ((char **) res->elts)[0];
+  fail_unless(strcmp(elt, "/") == 0, "Expected '/', got '%s'", elt);
+
+  path = "/foo/bar/baz/";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res != NULL, "Failed to split path '%s': %s", path,
+    strerror(errno));
+  fail_unless(res->nelts == 4, "Expected 4, got %u", res->nelts);
+  elt = ((char **) res->elts)[0];
+  fail_unless(strcmp(elt, "/") == 0, "Expected '/', got '%s'", elt);
+  elt = ((char **) res->elts)[1];
+  fail_unless(strcmp(elt, "foo") == 0, "Expected 'foo', got '%s'", elt);
+  elt = ((char **) res->elts)[2];
+  fail_unless(strcmp(elt, "bar") == 0, "Expected 'bar', got '%s'", elt);
+  elt = ((char **) res->elts)[3];
+  fail_unless(strcmp(elt, "baz") == 0, "Expected 'baz', got '%s'", elt);
+
+  path = "/foo//bar//baz//";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res != NULL, "Failed to split path '%s': %s", path,
+    strerror(errno));
+  fail_unless(res->nelts == 4, "Expected 4, got %u", res->nelts);
+  elt = ((char **) res->elts)[0];
+  fail_unless(strcmp(elt, "/") == 0, "Expected '/', got '%s'", elt);
+  elt = ((char **) res->elts)[1];
+  fail_unless(strcmp(elt, "foo") == 0, "Expected 'foo', got '%s'", elt);
+  elt = ((char **) res->elts)[2];
+  fail_unless(strcmp(elt, "bar") == 0, "Expected 'bar', got '%s'", elt);
+  elt = ((char **) res->elts)[3];
+  fail_unless(strcmp(elt, "baz") == 0, "Expected 'baz', got '%s'", elt);
+
+  path = "/foo/bar/baz";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res != NULL, "Failed to split path '%s': %s", path,
+    strerror(errno));
+  fail_unless(res->nelts == 4, "Expected 4, got %u", res->nelts);
+  elt = ((char **) res->elts)[0];
+  fail_unless(strcmp(elt, "/") == 0, "Expected '/', got '%s'", elt);
+  elt = ((char **) res->elts)[1];
+  fail_unless(strcmp(elt, "foo") == 0, "Expected 'foo', got '%s'", elt);
+  elt = ((char **) res->elts)[2];
+  fail_unless(strcmp(elt, "bar") == 0, "Expected 'bar', got '%s'", elt);
+  elt = ((char **) res->elts)[3];
+  fail_unless(strcmp(elt, "baz") == 0, "Expected 'baz', got '%s'", elt);
+
+  path = "foo/bar/baz";
+
+  mark_point();
+  res = pr_fs_split_path(p, path);
+  fail_unless(res != NULL, "Failed to split path '%s': %s", path,
+    strerror(errno));
+  fail_unless(res->nelts == 3, "Expected 3, got %u", res->nelts);
+  elt = ((char **) res->elts)[0];
+  fail_unless(strcmp(elt, "foo") == 0, "Expected 'foo', got '%s'", elt);
+  elt = ((char **) res->elts)[1];
+  fail_unless(strcmp(elt, "bar") == 0, "Expected 'bar', got '%s'", elt);
+  elt = ((char **) res->elts)[2];
+  fail_unless(strcmp(elt, "baz") == 0, "Expected 'baz', got '%s'", elt);
+}
+END_TEST
+
+START_TEST (fs_join_path_test) {
+  char *path;
+  array_header *components;
+
+  mark_point();
+  path = pr_fs_join_path(NULL, NULL, 0);
+  fail_unless(path == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = pr_fs_join_path(p, NULL, 0);
+  fail_unless(path == NULL, "Failed to handle null components");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  components = make_array(p, 0, sizeof(char **));
+
+  mark_point();
+  path = pr_fs_join_path(p, components, 0);
+  fail_unless(path == NULL, "Failed to handle empty components");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((char **) push_array(components)) = pstrdup(p, "/");
+
+  mark_point();
+  path = pr_fs_join_path(p, components, 0);
+  fail_unless(path == NULL, "Failed to handle empty count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = pr_fs_join_path(p, components, 3);
+  fail_unless(path == NULL, "Failed to handle invalid count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = pr_fs_join_path(p, components, 1);
+  fail_unless(path != NULL, "Failed to join path: %s", strerror(errno));
+  fail_unless(strcmp(path, "/") == 0, "Expected '/', got '%s'", path);
+
+  *((char **) push_array(components)) = pstrdup(p, "foo");
+  *((char **) push_array(components)) = pstrdup(p, "bar");
+  *((char **) push_array(components)) = pstrdup(p, "baz");
+
+  mark_point();
+  path = pr_fs_join_path(p, components, 4);
+  fail_unless(path != NULL, "Failed to join path: %s", strerror(errno));
+  fail_unless(strcmp(path, "/foo/bar/baz") == 0,
+    "Expected '/foo/bar/baz', got '%s'", path);
+
+  mark_point();
+  path = pr_fs_join_path(p, components, 3);
+  fail_unless(path != NULL, "Failed to join path: %s", strerror(errno));
+  fail_unless(strcmp(path, "/foo/bar") == 0, "Expected '/foo/bar', got '%s'",
+    path);
+}
+END_TEST
+
+START_TEST (fs_virtual_path_test) {
+  const char *path;
+  char buf[PR_TUNABLE_PATH_MAX];
+
+  mark_point();
+  pr_fs_virtual_path(NULL, NULL, 0);
+
+  mark_point();
+  path = "/tmp";
+  pr_fs_virtual_path(path, NULL, 0);
+
+  mark_point();
+  memset(buf, '\0', sizeof(buf));
+  pr_fs_virtual_path(path, buf, 0);
+  fail_unless(*buf == '\0', "Expected empty buffer, got '%s'", buf);
+
+  mark_point();
+  memset(buf, '\0', sizeof(buf));
+  pr_fs_virtual_path(path, buf, sizeof(buf)-1);
+  fail_unless(strcmp(buf, path) == 0, "Expected '%s', got '%s'", path, buf);
+
+  mark_point();
+  memset(buf, '\0', sizeof(buf));
+  path = "tmp";
+  pr_fs_virtual_path(path, buf, sizeof(buf)-1);
+  fail_unless(strcmp(buf, "/tmp") == 0, "Expected '/tmp', got '%s'", path, buf);
+
+  mark_point();
+  memset(buf, '\0', sizeof(buf));
+  path = "/tmp/././";
+  pr_fs_virtual_path(path, buf, sizeof(buf)-1);
+  fail_unless(strcmp(buf, "/tmp") == 0 || strcmp(buf, "/tmp/") == 0,
+    "Expected '/tmp', got '%s'", path, buf);
+
+  mark_point();
+  memset(buf, '\0', sizeof(buf));
+  path = "tmp/../../";
+  pr_fs_virtual_path(path, buf, sizeof(buf)-1);
+  fail_unless(strcmp(buf, "/") == 0, "Expected '/', got '%s'", path, buf);
+}
+END_TEST
+
+START_TEST (fs_get_usable_fd_test) {
+  int fd, res;
+
+  fd = -1;
+  res = pr_fs_get_usable_fd(fd);
+  fail_unless(res < 0, "Failed to handle bad fd");
+  fail_unless(errno == EBADF || errno == EINVAL,
+    "Expected EBADF (%d) or EINVAL (%d), got %s (%d)", EBADF, EINVAL,
+    strerror(errno), errno);
+
+  fd = STDERR_FILENO + 1;
+  res = pr_fs_get_usable_fd(fd);
+  fail_unless(res == fd, "Expected %d, got %d", fd, res);
+
+  fd = STDERR_FILENO - 1;
+  res = pr_fs_get_usable_fd(fd);
+  fail_unless(res > STDERR_FILENO, "Failed to get usable fd for %d: %s", fd,
+    strerror(errno));
+  (void) close(res);
+}
+END_TEST
+
+START_TEST (fs_get_usable_fd2_test) {
+  int fd, res;
+
+  res = pr_fs_get_usable_fd2(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fd = -1;
+  res = pr_fs_get_usable_fd2(&fd);
+  fail_unless(res < 0, "Failed to handle bad fd");
+  fail_unless(errno == EBADF || errno == EINVAL,
+    "Expected EBADF (%d) or EINVAL (%d), got %s (%d)", EBADF, EINVAL,
+    strerror(errno), errno);
+
+  fd = STDERR_FILENO + 1;
+  res = pr_fs_get_usable_fd2(&fd);
+  fail_unless(res == 0, "Failed to handle fd: %s", strerror(errno));
+  fail_unless(fd == (STDERR_FILENO + 1), "Expected %d, got %d",
+    STDERR_FILENO + 1, fd);
+
+  fd = STDERR_FILENO - 1;
+  res = pr_fs_get_usable_fd2(&fd);
+  fail_unless(res == 0, "Failed to handle fd: %s", strerror(errno));
+  fail_unless(fd > STDERR_FILENO, "Expected >%d, got %d", STDERR_FILENO, fd);
+  (void) close(fd);
+}
+END_TEST
+
+START_TEST (fs_getsize_test) {
+  off_t res;
+  char *path;
+
+  res = pr_fs_getsize(NULL);
+  fail_unless(res == (off_t) -1, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_getsize(path);
+  fail_unless(res != (off_t) -1, "Failed to get fs size for '%s': %s", path,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_getsize2_test) {
+  int res;
+  char *path;
+  off_t sz = 0;
+
+  res = pr_fs_getsize2(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_fs_getsize2(path, NULL);
+  fail_unless(res < 0, "Failed to handle null size argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_getsize2(path, &sz);
+  fail_unless(res == 0, "Failed to get fs size for '%s': %s", path,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_fgetsize_test) {
+  int fd = -1, res;
+  off_t fs_sz = 0;
+
+  mark_point();
+  res = pr_fs_fgetsize(fd, &fs_sz);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  mark_point();
+  fd = 0;
+  fs_sz = 0;
+  res = pr_fs_fgetsize(fd, NULL);
+  fail_unless(res < 0, "Failed to handle null size argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  fd = 0;
+  fs_sz = 0;
+  res = pr_fs_fgetsize(fd, &fs_sz);
+  fail_unless(res == 0, "Failed to get fs size for fd %d: %s", fd,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_fadvise_test) {
+  int advice, fd = -1;
+  off_t off = 0, len = 0;
+
+  /* We make these function calls to exercise the code paths, even
+   * though there's no good way to verify the behavior changed.
+   */
+
+  advice = PR_FS_FADVISE_NORMAL;
+  pr_fs_fadvise(fd, off, len, advice);
+
+  advice = PR_FS_FADVISE_RANDOM;
+  pr_fs_fadvise(fd, off, len, advice);
+
+  advice = PR_FS_FADVISE_SEQUENTIAL;
+  pr_fs_fadvise(fd, off, len, advice);
+
+  advice = PR_FS_FADVISE_WILLNEED;
+  pr_fs_fadvise(fd, off, len, advice);
+
+  advice = PR_FS_FADVISE_DONTNEED;
+  pr_fs_fadvise(fd, off, len, advice);
+
+  advice = PR_FS_FADVISE_NOREUSE;
+  pr_fs_fadvise(fd, off, len, advice);
+}
+END_TEST
+
+START_TEST (fs_have_access_test) {
+  int res;
+  struct stat st;
+  uid_t uid;
+  gid_t gid;
+  array_header *suppl_gids;
+
+  mark_point();
+  res = pr_fs_have_access(NULL, R_OK, 0, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null stat");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(&st, 0, sizeof(struct stat));
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, 0, 0, NULL);
+  fail_unless(res == 0, "Failed to handle root access: %s", strerror(errno));
+
+  /* Use cases: no matching UID or GID; R_OK, W_OK, X_OK. */
+  memset(&st, 0, sizeof(struct stat));
+  uid = 1;
+  gid = 1;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing other R_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing other W_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing other X_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  st.st_mode = S_IFMT|S_IROTH|S_IWOTH|S_IXOTH;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle other R_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle other W_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle other X_OK access: %s",
+    strerror(errno));
+
+  /* Use cases: matching UID, not GID; R_OK, W_OK, X_OK. */
+  memset(&st, 0, sizeof(struct stat));
+
+  st.st_uid = uid;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing user R_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing user W_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing user X_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  st.st_mode = S_IFMT|S_IRUSR|S_IWUSR|S_IXUSR;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle user R_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle user W_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle user X_OK access: %s",
+    strerror(errno));
+
+  /* Use cases: matching GID, not UID; R_OK, W_OK, X_OK. */
+  memset(&st, 0, sizeof(struct stat));
+
+  st.st_gid = gid;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing group R_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing group W_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, gid, NULL);
+  fail_unless(res < 0, "Failed to handle missing group X_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  st.st_mode = S_IFMT|S_IRGRP|S_IWGRP|S_IXGRP;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle group R_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle group W_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, gid, NULL);
+  fail_unless(res == 0, "Failed to handle group X_OK access: %s",
+    strerror(errno));
+
+  /* Use cases: matching suppl GID, not UID; R_OK, W_OK, X_OK. */
+  memset(&st, 0, sizeof(struct stat));
+
+  suppl_gids = make_array(p, 1, sizeof(gid_t));
+  *((gid_t *) push_array(suppl_gids)) = 100;
+  *((gid_t *) push_array(suppl_gids)) = gid;
+  st.st_gid = gid;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, 0, suppl_gids);
+  fail_unless(res < 0, "Failed to handle missing group R_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, 0, suppl_gids);
+  fail_unless(res < 0, "Failed to handle missing group W_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, 0, suppl_gids);
+  fail_unless(res < 0, "Failed to handle missing group X_OK access");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
+
+  st.st_mode = S_IFMT|S_IRGRP|S_IWGRP|S_IXGRP;
+
+  mark_point();
+  res = pr_fs_have_access(&st, R_OK, uid, 0, suppl_gids);
+  fail_unless(res == 0, "Failed to handle group R_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, W_OK, uid, 0, suppl_gids);
+  fail_unless(res == 0, "Failed to handle group W_OK access: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_fs_have_access(&st, X_OK, uid, 0, suppl_gids);
+  fail_unless(res == 0, "Failed to handle group X_OK access: %s",
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (fs_is_nfs_test) {
+  int res;
+
+  res = pr_fs_is_nfs(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fs_is_nfs("/tmp");
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+}
+END_TEST
+
+START_TEST (fs_valid_path_test) {
+  int res;
+  const char *path;
+  pr_fs_t *fs;
+
+  res = pr_fs_valid_path(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+  res = pr_fs_valid_path(path);
+  fail_unless(res == 0, "'%s' is not a valid path: %s", path, strerror(errno));
+
+  path = ":tmp";
+  res = pr_fs_valid_path(path);
+  fail_unless(res < 0, "Failed to handle invalid path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  fs = pr_register_fs(p, "testsuite", "&");
+  fail_unless(fs != NULL, "Failed to register FS: %s", strerror(errno));
+
+  fs = pr_register_fs(p, "testsuite2", ":");
+  fail_unless(fs != NULL, "Failed to register FS: %s", strerror(errno));
+
+  res = pr_fs_valid_path(path);
+  fail_unless(res == 0, "Failed to handle valid path: %s", strerror(errno));
+
+  (void) pr_remove_fs("/testsuite2");
+  (void) pr_remove_fs("/testsuite");
+}
+END_TEST
+
+START_TEST (fsio_smkdir_test) {
+  int res;
+  const char *path;
+  mode_t mode = 0755;
+  uid_t uid = getuid();
+  gid_t gid = getgid();
+
+  res = pr_fsio_smkdir(NULL, NULL, mode, uid, gid);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_smkdir(p, NULL, mode, uid, gid);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = fsio_testdir_path;
+  res = pr_fsio_smkdir(p, path, mode, uid, gid);
+  fail_unless(res == 0, "Failed to securely create '%s': %s", fsio_testdir_path,
+    strerror(errno));
+  (void) pr_fsio_rmdir(fsio_testdir_path);
+
+  res = pr_fsio_set_use_mkdtemp(-1);
+  fail_unless(res < 0, "Failed to handle invalid setting");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+#ifdef HAVE_MKDTEMP
+  res = pr_fsio_set_use_mkdtemp(FALSE);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  res = pr_fsio_smkdir(p, path, mode, uid, gid);
+  fail_unless(res == 0, "Failed to securely create '%s': %s", fsio_testdir_path,
+    strerror(errno));
+  (void) pr_fsio_rmdir(fsio_testdir_path);
+
+  res = pr_fsio_set_use_mkdtemp(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+#else
+  res = pr_fsio_set_use_mkdtemp(TRUE);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  res = pr_fsio_set_use_mkdtemp(FALSE);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+#endif /* HAVE_MKDTEMP */
+
+  (void) pr_fsio_rmdir(fsio_testdir_path);
+}
+END_TEST
+
+START_TEST (fsio_getpipebuf_test) {
+  char *res;
+  int fd = -1;
+  long bufsz = 0;
+
+  res = pr_fsio_getpipebuf(NULL, fd, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_getpipebuf(p, fd, NULL);
+  fail_unless(res == NULL, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  fd = 0;
+  res = pr_fsio_getpipebuf(p, fd, NULL);
+  fail_unless(res != NULL, "Failed to get pipebuf for fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_fsio_getpipebuf(p, fd, &bufsz);
+  fail_unless(res != NULL, "Failed to get pipebuf for fd %d: %s", fd,
+    strerror(errno));
+  fail_unless(bufsz > 0, "Expected >0, got %ld", bufsz);
+}
+END_TEST
+
+START_TEST (fsio_gets_test) {
+  char buf[PR_TUNABLE_PATH_MAX], *res, *text;
+  pr_fh_t *fh;
+  int res2;
+
+  res = pr_fsio_gets(NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_gets(buf, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null file handle");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_RDWR);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_gets(buf, 0, fh);
+  fail_unless(res == NULL, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "Hello, World!\n";
+  res2 = pr_fsio_puts(text, fh);
+  fail_if(res2 < 0, "Error writing to '%s': %s", fsio_test_path,
+    strerror(errno));
+  pr_fsio_fsync(fh);
+  pr_fsio_lseek(fh, 0, SEEK_SET);
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_fsio_gets(buf, sizeof(buf)-1, fh);
+  fail_if(res == NULL, "Failed reading from '%s': %s", fsio_test_path,
+    strerror(errno));
+  fail_unless(strcmp(res, text) == 0, "Expected '%s', got '%s'", text, res);
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_getline_test) {
+  char buf[PR_TUNABLE_PATH_MAX], *res, *text;
+  pr_fh_t *fh;
+  unsigned int lineno = 0;
+  int res2;
+
+  res = pr_fsio_getline(NULL, 0, NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_getline(buf, 0, NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle file handle");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_RDWR);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_getline(buf, 0, fh, NULL);
+  fail_unless(res == NULL, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_fsio_getline(buf, sizeof(buf)-1, fh, &lineno);
+  fail_unless(res == NULL, "Failed to read empty '%s' file", fsio_test_path);
+
+  text = "Hello, World!\n";
+  res2 = pr_fsio_puts(text, fh);
+  fail_if(res2 < 0, "Error writing to '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  text = "How\\\n are you?\n";
+  res2 = pr_fsio_puts(text, fh);
+  fail_if(res2 < 0, "Error writing to '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  pr_fsio_fsync(fh);
+  pr_fsio_lseek(fh, 0, SEEK_SET);
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_fsio_getline(buf, sizeof(buf)-1, fh, &lineno);
+  fail_if(res == NULL, "Failed to read line from '%s': %s", fsio_test_path,
+    strerror(errno));
+  fail_unless(strcmp(res, "Hello, World!\n") == 0,
+    "Expected 'Hello, World!\n', got '%s'", res);
+  fail_unless(lineno == 1, "Expected 1, got %u", lineno);
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_fsio_getline(buf, sizeof(buf)-1, fh, &lineno);
+  fail_if(res == NULL, "Failed to read line from '%s': %s", fsio_test_path,
+    strerror(errno));
+  fail_unless(strcmp(res, "How are you?\n") == 0,
+    "Expected 'How are you?\n', got '%s'", res);
+  fail_unless(lineno == 3, "Expected 3, got %u", lineno);
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_puts_test) {
+  int res;
+  const char *text;
+  pr_fh_t *fh;
+
+  res = pr_fsio_puts(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "Hello, World!\n";
+  res = pr_fsio_puts(text, NULL);
+  fail_unless(res < 0, "Failed to handle null file handle");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  res = pr_fsio_puts(NULL, fh);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+START_TEST (fsio_blocking_test) {
+  int fd, res;
+  pr_fh_t *fh;
+
+  res = pr_fsio_set_block(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(fsio_test_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", fsio_test_path,
+    strerror(errno));
+
+  fd = fh->fh_fd;
+  fh->fh_fd = -1;
+
+  res = pr_fsio_set_block(fh);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  fh->fh_fd = fd;
+  res = pr_fsio_set_block(fh);
+  fail_unless(res == 0, "Failed to make '%s' blocking: %s", fsio_test_path,
+    strerror(errno));
+
+  (void) pr_fsio_close(fh);
+  (void) pr_fsio_unlink(fsio_test_path);
+}
+END_TEST
+
+Suite *tests_get_fsio_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("fsio");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  /* Main FSIO API tests */
+  tcase_add_test(testcase, fsio_sys_open_test);
+  tcase_add_test(testcase, fsio_sys_open_canon_test);
+  tcase_add_test(testcase, fsio_sys_open_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_close_test);
+  tcase_add_test(testcase, fsio_sys_unlink_test);
+  tcase_add_test(testcase, fsio_sys_unlink_canon_test);
+  tcase_add_test(testcase, fsio_sys_unlink_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_stat_test);
+  tcase_add_test(testcase, fsio_sys_stat_canon_test);
+  tcase_add_test(testcase, fsio_sys_fstat_test);
+  tcase_add_test(testcase, fsio_sys_read_test);
+  tcase_add_test(testcase, fsio_sys_write_test);
+  tcase_add_test(testcase, fsio_sys_lseek_test);
+  tcase_add_test(testcase, fsio_sys_link_test);
+  tcase_add_test(testcase, fsio_sys_link_canon_test);
+  tcase_add_test(testcase, fsio_sys_link_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_symlink_test);
+  tcase_add_test(testcase, fsio_sys_symlink_canon_test);
+  tcase_add_test(testcase, fsio_sys_symlink_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_readlink_test);
+  tcase_add_test(testcase, fsio_sys_readlink_canon_test);
+  tcase_add_test(testcase, fsio_sys_lstat_test);
+  tcase_add_test(testcase, fsio_sys_lstat_canon_test);
+  tcase_add_test(testcase, fsio_sys_access_dir_test);
+  tcase_add_test(testcase, fsio_sys_access_file_test);
+  tcase_add_test(testcase, fsio_sys_faccess_test);
+  tcase_add_test(testcase, fsio_sys_truncate_test);
+  tcase_add_test(testcase, fsio_sys_truncate_canon_test);
+  tcase_add_test(testcase, fsio_sys_truncate_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_ftruncate_test);
+  tcase_add_test(testcase, fsio_sys_chmod_test);
+  tcase_add_test(testcase, fsio_sys_chmod_canon_test);
+  tcase_add_test(testcase, fsio_sys_chmod_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_fchmod_test);
+  tcase_add_test(testcase, fsio_sys_chown_test);
+  tcase_add_test(testcase, fsio_sys_chown_canon_test);
+  tcase_add_test(testcase, fsio_sys_chown_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_fchown_test);
+  tcase_add_test(testcase, fsio_sys_lchown_test);
+  tcase_add_test(testcase, fsio_sys_lchown_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_rename_test);
+  tcase_add_test(testcase, fsio_sys_rename_canon_test);
+  tcase_add_test(testcase, fsio_sys_rename_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_utimes_test);
+  tcase_add_test(testcase, fsio_sys_utimes_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_futimes_test);
+  tcase_add_test(testcase, fsio_sys_fsync_test);
+
+  /* Extended attribute tests */
+  tcase_add_test(testcase, fsio_sys_getxattr_test);
+  tcase_add_test(testcase, fsio_sys_lgetxattr_test);
+  tcase_add_test(testcase, fsio_sys_fgetxattr_test);
+  tcase_add_test(testcase, fsio_sys_listxattr_test);
+  tcase_add_test(testcase, fsio_sys_llistxattr_test);
+  tcase_add_test(testcase, fsio_sys_flistxattr_test);
+  tcase_add_test(testcase, fsio_sys_removexattr_test);
+  tcase_add_test(testcase, fsio_sys_lremovexattr_test);
+  tcase_add_test(testcase, fsio_sys_fremovexattr_test);
+  tcase_add_test(testcase, fsio_sys_setxattr_test);
+  tcase_add_test(testcase, fsio_sys_lsetxattr_test);
+  tcase_add_test(testcase, fsio_sys_fsetxattr_test);
+
+  tcase_add_test(testcase, fsio_sys_mkdir_test);
+  tcase_add_test(testcase, fsio_sys_mkdir_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_rmdir_test);
+  tcase_add_test(testcase, fsio_sys_rmdir_chroot_guard_test);
+  tcase_add_test(testcase, fsio_sys_chdir_test);
+  tcase_add_test(testcase, fsio_sys_chdir_canon_test);
+  tcase_add_test(testcase, fsio_sys_chroot_test);
+  tcase_add_test(testcase, fsio_sys_opendir_test);
+  tcase_add_test(testcase, fsio_sys_readdir_test);
+  tcase_add_test(testcase, fsio_sys_closedir_test);
+
+  /* FSIO statcache tests */
+  tcase_add_test(testcase, fsio_statcache_clear_cache_test);
+  tcase_add_test(testcase, fsio_statcache_cache_hit_test);
+  tcase_add_test(testcase, fsio_statcache_negative_cache_test);
+  tcase_add_test(testcase, fsio_statcache_expired_test);
+  tcase_add_test(testcase, fsio_statcache_dump_test);
+
+  /* Custom FSIO management tests */
+  tcase_add_test(testcase, fs_create_fs_test);
+  tcase_add_test(testcase, fs_insert_fs_test);
+  tcase_add_test(testcase, fs_get_fs_test);
+  tcase_add_test(testcase, fs_unmount_fs_test);
+  tcase_add_test(testcase, fs_remove_fs_test);
+  tcase_add_test(testcase, fs_register_fs_test);
+  tcase_add_test(testcase, fs_unregister_fs_test);
+  tcase_add_test(testcase, fs_resolve_fs_map_test);
+#if defined(PR_USE_DEVEL)
+  tcase_add_test(testcase, fs_dump_fs_test);
+#endif /* PR_USE_DEVEL */
+
+  /* Custom FSIO tests */
+  tcase_add_test(testcase, fsio_custom_chroot_test);
+
+  /* Misc */
+  tcase_add_test(testcase, fs_clean_path_test);
+  tcase_add_test(testcase, fs_clean_path2_test);
 
-  tcase_add_test(testcase, fs_clean_path_test);
-  tcase_add_test(testcase, fs_clean_path2_test);
   tcase_add_test(testcase, fs_dircat_test);
   tcase_add_test(testcase, fs_setcwd_test);
+  tcase_add_test(testcase, fs_glob_test);
+  tcase_add_test(testcase, fs_copy_file_test);
+  tcase_add_test(testcase, fs_copy_file2_test);
+  tcase_add_test(testcase, fs_interpolate_test);
+  tcase_add_test(testcase, fs_resolve_partial_test);
+  tcase_add_test(testcase, fs_resolve_path_test);
+  tcase_add_test(testcase, fs_use_encoding_test);
+  tcase_add_test(testcase, fs_decode_path2_test);
+  tcase_add_test(testcase, fs_encode_path_test);
+  tcase_add_test(testcase, fs_split_path_test);
+  tcase_add_test(testcase, fs_join_path_test);
+  tcase_add_test(testcase, fs_virtual_path_test);
+  tcase_add_test(testcase, fs_get_usable_fd_test);
+  tcase_add_test(testcase, fs_get_usable_fd2_test);
+  tcase_add_test(testcase, fs_getsize_test);
+  tcase_add_test(testcase, fs_getsize2_test);
+  tcase_add_test(testcase, fs_fgetsize_test);
+  tcase_add_test(testcase, fs_fadvise_test);
+  tcase_add_test(testcase, fs_have_access_test);
+  tcase_add_test(testcase, fs_is_nfs_test);
+  tcase_add_test(testcase, fs_valid_path_test);
+  tcase_add_test(testcase, fsio_smkdir_test);
+  tcase_add_test(testcase, fsio_getpipebuf_test);
+  tcase_add_test(testcase, fsio_gets_test);
+  tcase_add_test(testcase, fsio_getline_test);
+  tcase_add_test(testcase, fsio_puts_test);
+  tcase_add_test(testcase, fsio_blocking_test);
 
   suite_add_tcase(suite, testcase);
   return suite;
diff --git a/tests/api/help.c b/tests/api/help.c
new file mode 100644
index 0000000..f330706
--- /dev/null
+++ b/tests/api/help.c
@@ -0,0 +1,178 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015-2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Help API tests. */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+  pr_response_set_pool(p);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_use_stderr(TRUE);
+    pr_trace_set_levels("response", 0, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_use_stderr(FALSE);
+  }
+
+  pr_response_set_pool(NULL);
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  }
+}
+
+START_TEST (help_add_test) {
+  const char *cmd, *syntax;
+
+  mark_point();
+  pr_help_add(NULL, NULL, 0);
+
+  cmd = "FOO";
+
+  mark_point();
+  pr_help_add(cmd, NULL, 0);
+
+  syntax = "<path>";
+
+  mark_point();
+  pr_help_add(cmd, syntax, FALSE);
+
+  mark_point();
+  pr_help_add(cmd, syntax, TRUE);
+
+  cmd = "BAR";
+
+  mark_point();
+  pr_help_add(cmd, syntax, FALSE);
+}
+END_TEST
+
+START_TEST (help_add_response_test) {
+  int res;
+  const char *resp_code = NULL, *resp_msg = NULL;
+  cmd_rec *cmd;
+
+  res = pr_help_add_response(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  mark_point();
+
+  cmd = pr_cmd_alloc(p, 2, C_HELP, "FOO");
+  res = pr_help_add_response(cmd, NULL);
+  fail_unless(res == -1, "Failed to handle empty help list");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %s (%d)",
+    strerror(errno), errno);
+
+  mark_point();
+
+  /* Now add one command to the help list, and try again. */
+  pr_help_add("FOO", "", TRUE);
+
+  mark_point();
+
+  res = pr_help_add_response(cmd, NULL);
+  fail_unless(res == 0, "Failed to add help response: %s", strerror(errno));
+
+  mark_point();
+
+  resp_code = resp_msg = NULL;
+  res = pr_response_get_last(p, &resp_code, &resp_msg);
+  fail_unless(res == 0, "Failed to get last response: %s", strerror(errno));
+  fail_unless(resp_code != NULL, "Expected non-null response code");
+  fail_unless(strcmp(resp_code, R_214) == 0,
+    "Expected response code %s, got %s", R_214, resp_code);
+  fail_unless(resp_msg != NULL, "Expected non-null response message");
+  fail_unless(strcmp(resp_msg, "Direct comments to ftp-admin") == 0,
+    "Expected response message '%s', got '%s'", "Direct comments to ftp-admin",
+    resp_msg);
+
+  mark_point();
+
+  res = pr_help_add_response(cmd, "FOO");
+  fail_unless(res == 0, "Failed to add help response: %s", strerror(errno));
+
+  mark_point();
+
+  resp_code = resp_msg = NULL;
+  res = pr_response_get_last(p, &resp_code, &resp_msg);
+  fail_unless(res == 0, "Failed to get last response: %s", strerror(errno));
+  fail_unless(resp_code != NULL, "Expected non-null response code");
+  fail_unless(strcmp(resp_code, R_214) == 0,
+    "Expected response code %s, got %s", R_214, resp_code);
+  fail_unless(resp_msg != NULL, "Expected non-null response message");
+  fail_unless(strcmp(resp_msg, "Syntax: FOO ") == 0,
+    "Expected response message '%s', got '%s'", "Syntax: FOO ", resp_msg);
+
+  /* Now add an unimplemented command, and test that one. */
+
+  mark_point();
+
+  pr_help_add("BAR", "<path>", FALSE);
+
+  res = pr_help_add_response(cmd, "BAR");
+  fail_unless(res == 0, "Failed to add help response: %s", strerror(errno));
+
+  mark_point();
+
+  resp_code = resp_msg = NULL;
+  res = pr_response_get_last(p, &resp_code, &resp_msg);
+  fail_unless(res == 0, "Failed to get last response: %s", strerror(errno));
+  fail_unless(resp_code != NULL, "Expected non-null response code");
+  fail_unless(strcmp(resp_code, R_214) == 0,
+    "Expected response code %s, got %s", R_214, resp_code);
+  fail_unless(resp_msg != NULL, "Expected non-null response message");
+  fail_unless(strcmp(resp_msg, "Syntax: BAR <path>") == 0,
+    "Expected response message '%s', got '%s'", "Syntax: BAR <path>", resp_msg);
+}
+END_TEST
+
+Suite *tests_get_help_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("help");
+
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, help_add_test);
+  tcase_add_test(testcase, help_add_response_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/inet.c b/tests/api/inet.c
new file mode 100644
index 0000000..b75c839
--- /dev/null
+++ b/tests/api/inet.c
@@ -0,0 +1,815 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014-2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Inet API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_netaddr();
+  init_netio();
+  init_inet();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("inet", 1, 20);
+  }
+
+  pr_inet_set_default_family(p, AF_INET);
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("inet", 0, 0);
+  }
+
+  pr_inet_set_default_family(p, 0);
+  pr_inet_clear();
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  } 
+}
+
+/* Tests */
+
+START_TEST (inet_family_test) {
+  int res;
+
+  pr_inet_set_default_family(p, 0);
+
+  res = pr_inet_set_default_family(p, AF_INET);
+  fail_unless(res == 0, "Expected previous family 0, got %d", res);
+
+  res = pr_inet_set_default_family(p, 0);
+  fail_unless(res == AF_INET, "Expected previous family %d, got %d", AF_INET,
+    res);
+
+  /* Restore the default family to AF_INET, for other tests. */
+  pr_inet_set_default_family(p, AF_INET);
+}
+END_TEST
+
+START_TEST (inet_create_conn_test) {
+  int sockfd = -2, port = INPORT_ANY;
+  conn_t *conn, *conn2;
+
+  conn = pr_inet_create_conn(NULL, sockfd, NULL, port, FALSE);
+  fail_unless(conn == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL,
+    "Failed to set errno to EINVAL (%d), got '%s' (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+  fail_unless(conn->listen_fd == sockfd, "Expected listen_fd %d, got %d",
+    sockfd, conn->listen_fd);
+  pr_inet_close(p, conn);
+
+  sockfd = -1;
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+  fail_unless(conn->listen_fd != sockfd,
+    "Expected listen_fd other than %d, got %d",
+    sockfd, conn->listen_fd);
+
+  /* Create another conn, with the same port, make sure it fails. */
+  conn2 = pr_inet_create_conn(p, sockfd, NULL, conn->local_port, FALSE);
+  if (conn2 == NULL) {
+    fail_unless(errno == EADDRINUSE, "Expected EADDRINUSE (%d), got %s (%d)",
+      EADDRINUSE, strerror(errno), errno);
+    pr_inet_close(p, conn2);
+  }
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_create_conn_portrange_test) {
+  conn_t *conn;
+
+  conn = pr_inet_create_conn_portrange(NULL, NULL, -1, -1);
+  fail_unless(conn == NULL, "Failed to handle negative ports");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn_portrange(NULL, NULL, 10, 1);
+  fail_unless(conn == NULL, "Failed to handle bad ports");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn_portrange(p, NULL, 49152, 65534);
+  fail_unless(conn != NULL, "Failed to create conn in portrange: %s",
+    strerror(errno));
+  pr_inet_lingering_close(p, conn, 0L);
+}
+END_TEST
+
+START_TEST (inet_copy_conn_test) {
+  int fd = -1, sockfd = -1, port = INPORT_ANY;
+  conn_t *conn, *conn2;
+  const char *name;
+
+  conn = pr_inet_copy_conn(NULL, NULL);
+  fail_unless(conn == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_copy_conn(p, NULL);
+  fail_unless(conn == NULL, "Failed to handle null conn argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  conn2 = pr_inet_copy_conn(p, conn);
+  fail_unless(conn2 != NULL, "Failed to copy conn: %s", strerror(errno));
+
+  pr_inet_close(p, conn);
+  pr_inet_close(p, conn2);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  name = "127.0.0.1";
+  conn->remote_addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(conn->remote_addr != NULL, "Failed to resolve '%s': %s",
+    name, strerror(errno));
+  conn->remote_name = pstrdup(p, name);
+  conn->instrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(conn->instrm != NULL, "Failed to open ctrl reading stream: %s",
+    strerror(errno));
+  conn->outstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_WR);
+  fail_unless(conn->instrm != NULL, "Failed to open ctrl writing stream: %s",
+    strerror(errno));
+
+  conn2 = pr_inet_copy_conn(p, conn);
+  fail_unless(conn2 != NULL, "Failed to copy conn: %s", strerror(errno));
+
+  mark_point();
+  pr_inet_lingering_close(NULL, NULL, 0L);
+
+  pr_inet_lingering_close(p, conn, 0L);
+  pr_inet_close(p, conn2);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  conn->instrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(conn->instrm != NULL, "Failed to open ctrl reading stream: %s",
+    strerror(errno));
+  conn->outstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_WR);
+  fail_unless(conn->instrm != NULL, "Failed to open ctrl writing stream: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_inet_lingering_abort(NULL, NULL, 0L);
+
+  pr_inet_lingering_abort(p, conn, 0L);
+}
+END_TEST
+
+START_TEST (inet_set_async_test) {
+  int fd, sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+
+  res = pr_inet_set_async(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno EINVAL (%d), got '%s' (%d)",
+    EINVAL, strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_set_async(p, conn);
+  fail_unless(res == 0, "Failed to set conn %p async: %s", conn,
+    strerror(errno));
+
+  fd = conn->rfd;
+  conn->rfd = 77;
+  res = pr_inet_set_async(p, conn);
+  fail_unless(res == 0, "Failed to set conn %p async: %s", conn,
+    strerror(errno));
+  conn->rfd = fd;
+
+  fd = conn->wfd;
+  conn->wfd = 78;
+  res = pr_inet_set_async(p, conn);
+  fail_unless(res == 0, "Failed to set conn %p async: %s", conn,
+    strerror(errno));
+  conn->wfd = fd;
+
+  fd = conn->listen_fd;
+  conn->listen_fd = 79;
+  res = pr_inet_set_async(p, conn);
+  fail_unless(res == 0, "Failed to set conn %p async: %s", conn,
+    strerror(errno));
+  conn->listen_fd = fd;
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_set_block_test) {
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn; 
+
+  res = pr_inet_set_block(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno EINVAL (%d), got '%s' (%d)",
+    EINVAL, strerror(errno), errno);
+
+  res = pr_inet_set_nonblock(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected errno EINVAL (%d), got '%s' (%d)",
+    EINVAL, strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_set_nonblock(p, conn);
+  fail_unless(res < 0, "Failed to handle bad socket");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  res = pr_inet_set_block(p, conn);
+  fail_unless(res < 0, "Failed to handle bad socket");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_set_proto_cork_test) {
+  int res, sockfd = -1;
+
+  res = pr_inet_set_proto_cork(sockfd, TRUE);
+  fail_unless(res < 0, "Failed to handle bad socket descriptor");
+  fail_unless(errno == EBADF,
+    "Failed to set errno to EBADF (%d), got '%s' (%d)", EBADF, strerror(errno),
+    errno);
+}
+END_TEST
+
+START_TEST (inet_set_proto_nodelay_test) {
+  int fd, sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+
+  res = pr_inet_set_proto_nodelay(NULL, NULL, 1);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_set_proto_nodelay(p, conn, 1);
+  fail_unless(res == 0, "Failed to enable nodelay: %s", strerror(errno));
+
+  res = pr_inet_set_proto_nodelay(p, conn, 0);
+  fail_unless(res == 0, "Failed to disable nodelay: %s", strerror(errno));
+
+  fd = conn->rfd;
+  conn->rfd = 8;
+  res = pr_inet_set_proto_nodelay(p, conn, 0);
+  fail_unless(res == 0, "Failed to disable nodelay: %s", strerror(errno));
+  conn->rfd = fd;
+
+  fd = conn->rfd;
+  conn->rfd = -2;
+  res = pr_inet_set_proto_nodelay(p, conn, 0);
+  fail_unless(res == 0, "Failed to disable nodelay: %s", strerror(errno));
+  conn->rfd = fd;
+
+  fd = conn->wfd;
+  conn->rfd = 9;
+  res = pr_inet_set_proto_nodelay(p, conn, 0);
+  fail_unless(res == 0, "Failed to disable nodelay: %s", strerror(errno));
+  conn->wfd = fd;
+
+  fd = conn->wfd;
+  conn->rfd = -3;
+  res = pr_inet_set_proto_nodelay(p, conn, 0);
+  fail_unless(res == 0, "Failed to disable nodelay: %s", strerror(errno));
+  conn->wfd = fd;
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_set_proto_opts_test) {
+  int fd, sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+
+  mark_point();
+  res = pr_inet_set_proto_opts(NULL, NULL, 1, 1, 1, 1);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  mark_point();
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+
+  mark_point();
+  fd = conn->rfd;
+  conn->rfd = 8;
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+  conn->rfd = fd;
+
+  mark_point();
+  fd = conn->wfd;
+  conn->wfd = 9;
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+  conn->wfd = fd;
+
+  mark_point();
+  fd = conn->listen_fd;
+  conn->listen_fd = 10;
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+  conn->listen_fd = fd;
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_set_proto_opts_ipv6_test) {
+#ifdef PR_USE_IPV6
+  int fd, sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+  unsigned char use_ipv6;
+
+  use_ipv6 = pr_netaddr_use_ipv6();
+
+  pr_netaddr_enable_ipv6();
+  pr_inet_set_default_family(p, AF_INET6);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  mark_point();
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+
+  mark_point();
+  fd = conn->rfd;
+  conn->rfd = 8;
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+  conn->rfd = fd;
+
+  mark_point();
+  fd = conn->wfd;
+  conn->wfd = 9;
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+  conn->wfd = fd;
+
+  mark_point();
+  fd = conn->listen_fd;
+  conn->listen_fd = 10;
+  res = pr_inet_set_proto_opts(p, conn, 1, 1, 1, 1);
+  fail_unless(res == 0, "Failed to set proto opts: %s", strerror(errno));
+  conn->listen_fd = fd;
+
+  pr_inet_close(p, conn);
+
+  pr_inet_set_default_family(p, AF_INET);
+  if (use_ipv6 == FALSE) {
+    pr_netaddr_disable_ipv6();
+  }
+#endif /* PR_USE_IPV6 */
+}
+END_TEST
+
+START_TEST (inet_set_socket_opts_test) {
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+  struct tcp_keepalive keepalive;
+
+  res = pr_inet_set_socket_opts(NULL, NULL, 1, 2, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_set_socket_opts(p, conn, 1, 2, NULL);
+  fail_unless(res == 0, "Failed to set socket opts: %s", strerror(errno));
+
+  res = pr_inet_set_socket_opts(p, conn, INT_MAX, INT_MAX, NULL);
+  fail_unless(res == 0, "Failed to set socket opts: %s", strerror(errno));
+
+  keepalive.keepalive_enabled = 1;
+  keepalive.keepalive_idle = 1;
+  keepalive.keepalive_count = 2;
+  keepalive.keepalive_intvl = 3;
+  res = pr_inet_set_socket_opts(p, conn, 1, 2, &keepalive);
+  fail_unless(res == 0, "Failed to set socket opts: %s", strerror(errno));
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_listen_test) {
+  int fd, mode, sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+
+  res = pr_inet_listen(NULL, NULL, 5, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_inet_resetlisten(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  fd = conn->listen_fd;
+  conn->listen_fd = 777;
+  res = pr_inet_listen(p, conn, 5, 0);
+  fail_unless(res < 0, "Succeeded in listening on conn unexpectedly");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  mode = conn->mode;
+  res = pr_inet_resetlisten(p, conn);
+  fail_unless(res < 0, "Succeeded in resetting listening on conn unexpectedly");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  conn->listen_fd = fd;
+  conn->mode = mode;
+
+  res = pr_inet_listen(p, conn, 5, 0);
+  fail_unless(res == 0, "Failed to listen on conn: %s", strerror(errno));
+
+  res = pr_inet_resetlisten(p, conn);
+  fail_unless(res == 0, "Failed to reset listen mode: %s", strerror(errno));
+
+  res = pr_inet_listen(p, conn, 5, 0);
+  fail_unless(res < 0, "Failed to handle already-listening socket");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_connect_ipv4_test) {
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+  const pr_netaddr_t *addr;
+
+  res = pr_inet_connect(NULL, NULL, NULL, port);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_connect(p, conn, NULL, 80);
+  fail_unless(res < 0, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '127.0.0.1': %s",
+    strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 80);
+  fail_unless(res < 0, "Connected to 127.0.0.1#80 unexpectedly");
+  fail_unless(errno == ECONNREFUSED, "Expected ECONNREFUSED (%d), got %s (%d)",
+    ECONNREFUSED, strerror(errno), errno);
+
+  /* Try connecting to Google's DNS server. */
+
+  addr = pr_netaddr_get_addr(p, "8.8.8.8", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '8.8.8.8': %s",
+    strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 53);
+  if (res < 0) {
+    /* Note: We get EINVAL here because the socket already tried (and failed)
+     * to connect to a different address.  Interestingly, trying to connect(2)
+     * using that same fd to a different address yields EINVAL.
+     */
+    fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+      strerror(errno), errno);
+  }
+  pr_inet_close(p, conn);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 53);
+  fail_if(res < 0, "Failed to connect to 8.8.8.8#53: %s", strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 53);
+  fail_unless(res < 0, "Failed to connect to 8.8.8.8#53: %s",
+    strerror(errno));
+  fail_unless(errno == EISCONN, "Expected EISCONN (%d), got %s (%d)",
+    EISCONN, strerror(errno), errno);
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_connect_ipv6_test) {
+#ifdef PR_USE_IPV6
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+  const pr_netaddr_t *addr;
+  unsigned char use_ipv6;
+
+  use_ipv6 = pr_netaddr_use_ipv6();
+
+  pr_netaddr_enable_ipv6();
+  pr_inet_set_default_family(p, AF_INET6);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  addr = pr_netaddr_get_addr(p, "::1", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '::1': %s",
+    strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 80);
+  fail_unless(res < 0, "Connected to ::1#80 unexpectedly");
+  fail_unless(errno == ECONNREFUSED || errno == ENETUNREACH || errno == EADDRNOTAVAIL,
+    "Expected ECONNREFUSED (%d), ENETUNREACH (%d), or EADDRNOTAVAIL (%d), got %s (%d)",
+    ECONNREFUSED, ENETUNREACH, EADDRNOTAVAIL, strerror(errno), errno);
+
+  /* Try connecting to Google's DNS server. */
+
+  addr = pr_netaddr_get_addr(p, "2001:4860:4860::8888", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '2001:4860:4860::8888': %s",
+    strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 53);
+  if (res < 0) {
+    /* Note: We get EINVAL here because the socket already tried (and failed)
+     * to connect to a different address.  Interestingly, trying to connect(2)
+     * using that same fd to a different address yields EINVAL.
+     */
+    fail_unless(errno == EINVAL || errno == ENETUNREACH || errno == EADDRNOTAVAIL,
+      "Expected EINVAL (%d), ENETUNREACH (%d) or EADDRNOTAVAIL (%d), got %s (%d)",
+      EINVAL, ENETUNREACH, EADDRNOTAVAIL, strerror(errno), errno);
+  }
+  pr_inet_close(p, conn);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_connect(p, conn, addr, 53);
+  if (res < 0) {
+    /* This could be expected, e.g. if there's no route. */
+    fail_unless(errno == EHOSTUNREACH || errno == ENETUNREACH || errno == EADDRNOTAVAIL,
+      "Expected EHOSTUNREACH (%d) or ENETUNREACH (%d) or EADDRNOTAVAIL (%d), got %s (%d)",
+      EHOSTUNREACH, ENETUNREACH, EADDRNOTAVAIL, strerror(errno), errno);
+  }
+
+  res = pr_inet_connect(p, conn, addr, 53);
+  fail_unless(res < 0, "Failed to connect to 2001:4860:4860::8888#53: %s",
+    strerror(errno));
+  fail_unless(errno == EISCONN || errno == EHOSTUNREACH || errno == ENETUNREACH || errno == EADDRNOTAVAIL,
+    "Expected EISCONN (%d) or EHOSTUNREACH (%d) or ENETUNREACH (%d) or EADDRNOTAVAIL (%d), got %s (%d)", EISCONN, EHOSTUNREACH, ENETUNREACH, EADDRNOTAVAIL, strerror(errno), errno);
+  pr_inet_close(p, conn);
+
+  pr_inet_set_default_family(p, AF_INET);
+
+  if (use_ipv6 == FALSE) {
+    pr_netaddr_disable_ipv6();
+  }
+#endif /* PR_USE_IPV6 */
+}
+END_TEST
+
+START_TEST (inet_connect_nowait_test) {
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+  const pr_netaddr_t *addr;
+
+  res = pr_inet_connect_nowait(NULL, NULL, NULL, port);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_connect_nowait(p, conn, NULL, 80);
+  fail_unless(res < 0, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '127.0.0.1': %s",
+    strerror(errno));
+
+  res = pr_inet_connect_nowait(p, conn, addr, 80);
+  fail_unless(res != -1, "Connected to 127.0.0.1#80 unexpectedly");
+
+  /* Try connecting to Google's DNS server. */
+
+  addr = pr_netaddr_get_addr(p, "8.8.8.8", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '8.8.8.8': %s",
+    strerror(errno));
+
+  res = pr_inet_connect_nowait(p, conn, addr, 53);
+  if (res < 0 &&
+      errno != ECONNREFUSED) {
+    fail_unless(res != -1, "Failed to connect to 8.8.8.8#53: %s",
+      strerror(errno));
+  }
+
+  pr_inet_close(p, conn);
+
+  /* Restore the default family to AF_INET, for other tests. */
+  pr_inet_set_default_family(p, AF_INET);
+}
+END_TEST
+
+START_TEST (inet_accept_test) {
+  conn_t *conn;
+
+  conn = pr_inet_accept(NULL, NULL, NULL, 0, 2, FALSE);
+  fail_unless(conn == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (inet_accept_nowait_test) {
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+
+  res = pr_inet_accept_nowait(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_accept_nowait(p, conn);
+  fail_unless(res < 0, "Accepted connection unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_conn_info_test) {
+  int sockfd = -1, port = INPORT_ANY, res;
+  conn_t *conn;
+
+  res = pr_inet_get_conn_info(NULL, -1);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_get_conn_info(conn, -1);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  res = pr_inet_get_conn_info(conn, 1);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == ENOTSOCK, "Expected ENOTSOCK (%d), got %s (%d)",
+    ENOTSOCK, strerror(errno), errno);
+
+  pr_inet_close(p, conn);
+}
+END_TEST
+
+START_TEST (inet_openrw_test) {
+  int sockfd = -1, port = INPORT_ANY;
+  conn_t *conn, *res;
+  const pr_netaddr_t *addr;
+
+  res = pr_inet_openrw(NULL, NULL, NULL, PR_NETIO_STRM_CTRL, -1, -1, -1, FALSE);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, port, FALSE);
+  fail_unless(conn != NULL, "Failed to create conn: %s", strerror(errno));
+
+  res = pr_inet_openrw(p, conn, NULL, PR_NETIO_STRM_CTRL, -1, -1, -1, FALSE);
+  fail_unless(res == NULL, "Opened rw conn unexpectedly");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  fail_unless(addr != NULL, "Failed to resolve 127.0.0.1: %s", strerror(errno));
+
+  res = pr_inet_openrw(p, conn, addr, PR_NETIO_STRM_CTRL, -1, -1, -1, FALSE);
+  fail_unless(res != NULL, "Failed to open rw conn: %s", strerror(errno));
+  (void) pr_inet_close(p, res);
+
+  res = pr_inet_openrw(p, conn, addr, PR_NETIO_STRM_CTRL, -1, -1, -1, TRUE);
+  fail_unless(res != NULL, "Failed to open rw conn: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (inet_generate_socket_event_test) {
+  int res;
+  const char *name;
+  server_rec *s;
+
+  res = pr_inet_generate_socket_event(NULL, NULL, NULL, -1);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "foo.bar";
+  res = pr_inet_generate_socket_event(name, NULL, NULL, -1);
+  fail_unless(res < 0, "Failed to handle null server_rec");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  s = pcalloc(p, sizeof(server_rec));
+  res = pr_inet_generate_socket_event(name, s, NULL, -1);
+  fail_unless(res < 0, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+Suite *tests_get_inet_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("inet");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, inet_family_test);
+  tcase_add_test(testcase, inet_create_conn_test);
+  tcase_add_test(testcase, inet_create_conn_portrange_test);
+  tcase_add_test(testcase, inet_copy_conn_test);
+  tcase_add_test(testcase, inet_set_async_test);
+  tcase_add_test(testcase, inet_set_block_test);
+  tcase_add_test(testcase, inet_set_proto_cork_test);
+  tcase_add_test(testcase, inet_set_proto_nodelay_test);
+  tcase_add_test(testcase, inet_set_proto_opts_test);
+  tcase_add_test(testcase, inet_set_proto_opts_ipv6_test);
+  tcase_add_test(testcase, inet_set_socket_opts_test);
+  tcase_add_test(testcase, inet_listen_test);
+  tcase_add_test(testcase, inet_connect_ipv4_test);
+  tcase_add_test(testcase, inet_connect_ipv6_test);
+  tcase_add_test(testcase, inet_connect_nowait_test);
+  tcase_add_test(testcase, inet_accept_test);
+  tcase_add_test(testcase, inet_accept_nowait_test);
+  tcase_add_test(testcase, inet_conn_info_test);
+  tcase_add_test(testcase, inet_openrw_test);
+  tcase_add_test(testcase, inet_generate_socket_event_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/json.c b/tests/api/json.c
new file mode 100644
index 0000000..3271f24
--- /dev/null
+++ b/tests/api/json.c
@@ -0,0 +1,1858 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* JSON API tests */
+
+#include <math.h>
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = make_sub_pool(NULL);
+  }
+
+  init_json();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("json", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("json", 0, 0);
+  }
+
+  finish_json();
+
+  if (p != NULL) {
+    destroy_pool(p);
+    p = NULL;
+  }
+}
+
+START_TEST (json_object_free_test) {
+  int res;
+
+  mark_point();
+  res = pr_json_object_free(NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (json_object_alloc_test) {
+  int res;
+  pr_json_object_t *json;
+
+  mark_point();
+  json = pr_json_object_alloc(NULL);
+  fail_unless(json == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  json = pr_json_object_alloc(p);
+  fail_unless(json != NULL, "Failed to allocate object: %s", strerror(errno));
+
+  mark_point();
+  res = pr_json_object_free(json);
+  fail_unless(res == 0, "Failed to free object: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (json_object_from_text_test) {
+  pr_json_object_t *json;
+  const char *text;
+
+  mark_point();
+  json = pr_json_object_from_text(NULL, NULL);
+  fail_unless(json == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  json = pr_json_object_from_text(p, NULL);
+  fail_unless(json == NULL, "Failed to handle null text");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "foo bar";
+
+  mark_point();
+  json = pr_json_object_from_text(p, text);
+  fail_unless(json == NULL, "Failed to handle invalid text '%s'", text);
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  text = "[\"foo\",\"bar\"]";
+
+  mark_point();
+  json = pr_json_object_from_text(p, text);
+  fail_unless(json == NULL, "Failed to handle non-object text '%s'", text);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  text = "{\"foo\":\"bar\"}";
+
+  mark_point();
+  json = pr_json_object_from_text(p, text);
+  fail_unless(json != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST (json_object_to_text_test) {
+  const char *text, *expected;
+  pr_json_object_t *json;
+
+  mark_point();
+  text = pr_json_object_to_text(NULL, NULL, NULL);
+  fail_unless(text == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  text = pr_json_object_to_text(p, NULL, NULL);
+  fail_unless(text == NULL, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  text = pr_json_object_to_text(p, json, NULL);
+  fail_unless(text == NULL, "Failed to handle null indent");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  expected = "{}";
+
+  mark_point();
+  text = pr_json_object_to_text(p, json, "");
+  fail_unless(text != NULL, "Failed to get text for object: %s",
+    strerror(errno));
+  fail_unless(strcmp(text, expected) == 0, "Expected '%s', got '%s'", expected,
+    text);
+
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST (json_object_count_test) {
+  int res;
+  pr_json_object_t *json;
+  const char *text;
+
+  mark_point();
+  res = pr_json_object_count(NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_count(json);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":true,\"bar\":false,\"baz\":1}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_count(json);
+  fail_unless(res == 3, "Expected 3, got %d", res);
+
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST (json_object_exists_test) {
+  int res;
+  pr_json_object_t *json;
+  const char *key, *text;
+
+  mark_point();
+  res = pr_json_object_exists(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_exists(json, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_exists(json, key);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":true,\"bar\":false,\"baz\":1}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_exists(json, key);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST (json_object_remove_test) {
+  int res;
+  pr_json_object_t *json;
+  const char *key, *text;
+
+  mark_point();
+  res = pr_json_object_remove(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_remove(json, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_remove(json, key);
+  fail_unless(res == 0, "Failed to remove nonexistent key '%s': %s", key,
+    strerror(errno));
+
+  res = pr_json_object_count(json);
+  fail_unless(res == 0, "Expected count 0, got %d", res);
+
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":true,\"bar\":false,\"baz\":1}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_remove(json, key);
+  fail_unless(res == 0, "Failed to remove existing key '%s': %s", key,
+    strerror(errno));
+  
+  res = pr_json_object_count(json);
+  fail_unless(res == 2, "Expected count 2, got %d", res);
+
+  mark_point();
+  res = pr_json_object_exists(json, key);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_get_bool_test) {
+  int res, val;
+  const char *key, *text;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_get_bool(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_bool(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_get_bool(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_bool(p, json, key, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_bool(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":1,\"bar\":true}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_get_bool(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle non-boolean key '%s'", key);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  key = "bar";
+ 
+  mark_point();
+  res = pr_json_object_get_bool(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(val == TRUE, "Expected TRUE, got %d", val);
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_set_bool_test) {
+  int res, val = TRUE;
+  const char *key;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_set_bool(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_set_bool(p, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_set_bool(p, json, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_set_bool(p, json, key, val);
+  fail_unless(res == 0, "Failed to set key '%s' to %d: %s", key, val,
+    strerror(errno));
+ 
+  val = FALSE;
+ 
+  mark_point();
+  res = pr_json_object_get_bool(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(val == TRUE, "Expected TRUE, got %d", val);
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_get_null_test) {
+  int res;
+  const char *key, *text;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_get_null(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_null(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_get_null(p, json, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_null(p, json, key);
+  fail_unless(res < 0, "Failed to handle nonexistent key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":1,\"bar\":null}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_get_null(p, json, key);
+  fail_unless(res < 0, "Failed to handle non-null key '%s'", key);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  key = "bar";
+ 
+  mark_point();
+  res = pr_json_object_get_null(p, json, key);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_set_null_test) {
+  int res;
+  const char *key;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_set_null(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_set_null(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_set_null(p, json, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_set_null(p, json, key);
+  fail_unless(res == 0, "Failed to set key '%s': %s", key, strerror(errno));
+ 
+  mark_point();
+  res = pr_json_object_get_null(p, json, key);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_get_number_test) {
+  int res;
+  double val;
+  const char *key, *text;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_get_number(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_number(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_get_number(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_number(p, json, key, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_number(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":false,\"bar\":7}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_get_number(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle non-number key '%s'", key);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  key = "bar";
+ 
+  mark_point();
+  res = pr_json_object_get_number(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(fabs(val) == fabs((double) 7.0), "Expected 7, got %e", val);
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_set_number_test) {
+  int res;
+  double val = 7;
+  const char *key;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_set_number(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_set_number(p, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_set_number(p, json, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_set_number(p, json, key, val);
+  fail_unless(res == 0, "Failed to set key '%s' to %d: %s", key, val,
+    strerror(errno));
+ 
+  val = 3;
+ 
+  mark_point();
+  res = pr_json_object_get_number(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(fabs(val) == fabs((double) 7.0), "Expected 7, got %e", val);
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_get_string_test) {
+  int res;
+  const char *key, *val, *text;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_get_string(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_string(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_get_string(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_string(p, json, key, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_string(p, json, key, (char **) &val);
+  fail_unless(res < 0, "Failed to handle nonexistent key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":false,\"bar\":\"baz\"}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_get_string(p, json, key, (char **) &val);
+  fail_unless(res < 0, "Failed to handle non-string key '%s'", key);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  key = "bar";
+ 
+  mark_point();
+  res = pr_json_object_get_string(p, json, key, (char **) &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(strcmp(val, "baz") == 0, "Expected 'baz', got '%s'", val);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":\"\"}";
+  json = pr_json_object_from_text(p, text);
+
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_string(p, json, key, (char **) &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(strcmp(val, "") == 0, "Expected '', got '%s'", val);
+
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_set_string_test) {
+  int res;
+  const char *key, *val;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_set_string(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_set_string(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_set_string(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+ 
+  mark_point();
+  res = pr_json_object_set_string(p, json, key, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "Hello, World!";
+
+  mark_point();
+  res = pr_json_object_set_string(p, json, key, val);
+  fail_unless(res == 0, "Failed to set key '%s' to '%s': %s", key, val,
+    strerror(errno));
+ 
+  val = "glarg";
+ 
+  mark_point();
+  res = pr_json_object_get_string(p, json, key, (char **) &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(strcmp(val, "Hello, World!") == 0,
+    "Expected 'Hello, World!', got '%s'", val);
+ 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_get_array_test) {
+  int res;
+  const char *key, *text;
+  pr_json_array_t *val = NULL;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_get_array(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_array(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_get_array(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_array(p, json, key, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_array(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":false,\"bar\":[\"baz\"]}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_get_array(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle non-array key '%s'", key);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  key = "bar";
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_object_get_array(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected array, got null");
+
+  mark_point();
+  (void) pr_json_array_free(val);
+
+  mark_point();
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_set_array_test) {
+  int res;
+  const char *key, *text;
+  pr_json_array_t *val = NULL;
+  pr_json_object_t *json;
+
+  mark_point();
+  res = pr_json_object_set_array(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_set_array(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_set_array(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_set_array(p, json, key, val);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "[1, 1, 2, 3, 5, 8]";
+  val = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_set_array(p, json, key, val);
+  fail_unless(res == 0, "Failed to set key '%s' to '%s': %s", key, val,
+    strerror(errno));
+
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_object_get_array(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected array, got null");
+
+  mark_point();
+  (void) pr_json_array_free(val); 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_get_object_test) {
+  int res;
+  const char *key, *text;
+  pr_json_object_t *json, *val = NULL;
+
+  mark_point();
+  res = pr_json_object_get_object(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_object(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_get_object(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_get_object(p, json, key, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_get_object(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_object_free(json);
+
+  text = "{\"foo\":false,\"bar\":{\"baz\":null}}";
+  json = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_get_object(p, json, key, &val);
+  fail_unless(res < 0, "Failed to handle non-object key '%s'", key);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  key = "bar";
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_object_get_object(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected object, got null");
+
+  mark_point();
+  (void) pr_json_object_free(val);
+
+  mark_point();
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_object_set_object_test) {
+  int res;
+  const char *key, *text;
+  pr_json_object_t *json, *val = NULL;
+
+  mark_point();
+  res = pr_json_object_set_object(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_object_set_object(p, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_object_alloc(p);
+
+  mark_point();
+  res = pr_json_object_set_object(p, json, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  key = "foo";
+
+  mark_point();
+  res = pr_json_object_set_object(p, json, key, val);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "{\"eeny\":1,\"meeny\":2,\"miny\":3,\"moe\":false}";
+  val = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_object_set_object(p, json, key, val);
+  fail_unless(res == 0, "Failed to set key '%s' to '%s': %s", key, val,
+    strerror(errno));
+
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_object_get_object(p, json, key, &val);
+  fail_unless(res == 0, "Failed to handle existing key '%s': %s", key,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected object, got null");
+
+  mark_point();
+  (void) pr_json_object_free(val); 
+  (void) pr_json_object_free(json);
+}
+END_TEST
+
+START_TEST(json_array_free_test) {
+  int res;
+
+  mark_point();
+  res = pr_json_array_free(NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST(json_array_alloc_test) {
+  int res;
+  pr_json_array_t *json;
+
+  mark_point();
+  json = pr_json_array_alloc(NULL);
+  fail_unless(json == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  json = pr_json_array_alloc(p);
+  fail_unless(json != NULL, "Failed to allocate array: %s", strerror(errno));
+
+  mark_point();
+  res = pr_json_array_free(json);
+  fail_unless(res == 0, "Failed to free array: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST(json_array_from_text_test) {
+  pr_json_array_t *json;
+  const char *text;
+
+  mark_point();
+  json = pr_json_array_from_text(NULL, NULL);
+  fail_unless(json == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  json = pr_json_array_from_text(p, NULL);
+  fail_unless(json == NULL, "Failed to handle null text");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "foo bar";
+
+  mark_point();
+  json = pr_json_array_from_text(p, text);
+  fail_unless(json == NULL, "Failed to handle invalid text '%s'", text);
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  text = "{\"foo\":null,\"bar\":false}";
+
+  mark_point();
+  json = pr_json_array_from_text(p, text);
+  fail_unless(json == NULL, "Failed to handle non-array text '%s'", text);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  text = "[\"foo\",\"bar\"]";
+
+  mark_point();
+  json = pr_json_array_from_text(p, text);
+  fail_unless(json != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_to_text_test) {
+  const char *text, *expected;
+  pr_json_array_t *json;
+
+  mark_point();
+  text = pr_json_array_to_text(NULL, NULL, NULL);
+  fail_unless(text == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  text = pr_json_array_to_text(p, NULL, NULL);
+  fail_unless(text == NULL, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  text = pr_json_array_to_text(p, json, NULL);
+  fail_unless(text == NULL, "Failed to handle null indent");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  expected = "[]";
+
+  mark_point();
+  text = pr_json_array_to_text(p, json, "");
+  fail_unless(text != NULL, "Failed to get text for array: %s",
+    strerror(errno));
+  fail_unless(strcmp(text, expected) == 0, "Expected '%s', got '%s'", expected,
+    text);
+
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_count_test) {
+  int res;
+  pr_json_array_t *json;
+  const char *text;
+
+  mark_point();
+  res = pr_json_array_count(NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_count(json);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",true,\"bar\",false,\"baz\",1]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_count(json);
+  fail_unless(res == 6, "Expected 6, got %d", res);
+
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_exists_test) {
+  int res;
+  pr_json_array_t *json;
+  unsigned int idx;
+  const char *text;
+
+  mark_point();
+  res = pr_json_array_exists(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_exists(json, 0);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",true,\"bar\",false,\"baz\",1]";
+  json = pr_json_array_from_text(p, text);
+
+  idx = 3;
+
+  mark_point();
+  res = pr_json_array_exists(json, idx);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_remove_test) {
+  int res;
+  pr_json_array_t *json;
+  unsigned int idx;
+  const char *text;
+
+  mark_point();
+  res = pr_json_array_remove(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  json = pr_json_array_alloc(p);
+
+  idx = 2;
+
+  mark_point();
+  res = pr_json_array_remove(json, idx);
+  fail_unless(res == 0, "Failed to remove nonexistent index %u: %s", idx,
+    strerror(errno));
+
+  res = pr_json_array_count(json);
+  fail_unless(res == 0, "Expected count 0, got %d", res);
+
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",true,\"bar\",false,\"baz\",1]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_remove(json, idx);
+  fail_unless(res == 0, "Failed to remove existing index %u: %s", idx,
+    strerror(errno));
+  
+  res = pr_json_array_count(json);
+  fail_unless(res == 5, "Expected count 5, got %d", res);
+
+  mark_point();
+  res = pr_json_array_exists(json, idx);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_get_bool_test) {
+  int res, val;
+  unsigned int idx;
+  const char *text;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_get_bool(NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_get_bool(p, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_get_bool(p, json, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  idx = 0;
+ 
+  mark_point();
+  res = pr_json_array_get_bool(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent index %u", idx);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",2,\"bar\",true]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_get_bool(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle non-boolean index %u", idx);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  idx = 3;
+
+  mark_point();
+  res = pr_json_array_get_bool(p, json, idx, &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(val == TRUE, "Expected TRUE, got %d", val);
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_append_bool_test) {
+  int res, val = TRUE;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_append_bool(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_append_bool(p, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_append_bool(p, json, val);
+  fail_unless(res == 0, "Failed to append val %d: %s", val, strerror(errno));
+ 
+  val = FALSE;
+ 
+  mark_point();
+  res = pr_json_array_get_bool(p, json, 0, &val);
+  fail_unless(res == 0, "Failed to handle existing index 0: %s",
+    strerror(errno));
+  fail_unless(val == TRUE, "Expected TRUE, got %d", val);
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_get_null_test) {
+  int res;
+  unsigned int idx;
+  const char *text;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_get_null(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_get_null(p, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  idx = 1;
+ 
+  mark_point();
+  res = pr_json_array_get_null(p, json, idx);
+  fail_unless(res < 0, "Failed to handle nonexistent index %u", idx);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",2,\"bar\",null]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_get_null(p, json, idx);
+  fail_unless(res < 0, "Failed to handle non-null index %u", idx);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  idx = 3;
+
+  mark_point();
+  res = pr_json_array_get_null(p, json, idx);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_append_null_test) {
+  int res;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_append_null(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_append_null(p, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_append_null(p, json);
+  fail_unless(res == 0, "Failed to append null vall: %s", strerror(errno));
+ 
+  mark_point();
+  res = pr_json_array_get_null(p, json, 0);
+  fail_unless(res == 0, "Failed to handle existing index 0: %s",
+    strerror(errno));
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_get_number_test) {
+  int res;
+  double val;
+  unsigned int idx;
+  const char *text;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_get_number(NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_get_number(p, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_get_number(p, json, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  idx = 3;
+ 
+  mark_point();
+  res = pr_json_array_get_number(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent index %u", idx);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",2,\"bar\",true]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_get_number(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle non-number index %u", idx);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  idx = 1;
+
+  mark_point();
+  res = pr_json_array_get_number(p, json, idx, &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(fabs(val) == fabs((double) 2.0), "Expected 2, got '%e'", val);
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_append_number_test) {
+  int res;
+  double val = 7;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_append_number(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_append_number(p, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_append_number(p, json, val);
+  fail_unless(res == 0, "Failed to append val %e: %s", val, strerror(errno));
+ 
+  val = 2;
+ 
+  mark_point();
+  res = pr_json_array_get_number(p, json, 0, &val);
+  fail_unless(res == 0, "Failed to handle existing index 0: %s",
+    strerror(errno));
+  fail_unless(fabs(val) == fabs((double) 7.0), "Expected 7, got %e", val);
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_get_string_test) {
+  int res;
+  unsigned int idx;
+  const char *text, *val;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_get_string(NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_get_string(p, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_get_string(p, json, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  idx = 3;
+ 
+  mark_point();
+  res = pr_json_array_get_string(p, json, idx, (char **) &val);
+  fail_unless(res < 0, "Failed to handle nonexistent index %u", idx);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",2,\"bar\",true]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_get_string(p, json, idx, (char **) &val);
+  fail_unless(res < 0, "Failed to handle non-string index %u", idx);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  idx = 0;
+
+  mark_point();
+  res = pr_json_array_get_string(p, json, idx, (char **) &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(strcmp(val, "foo") == 0, "Expected 'foo', got '%s'", val);
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_append_string_test) {
+  int res;
+  const char *val;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_append_string(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_append_string(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_append_string(p, json, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+ 
+  val = "foo!";
+ 
+  mark_point();
+  res = pr_json_array_append_string(p, json, val);
+  fail_unless(res == 0, "Failed to append val '%s': %s", val, strerror(errno));
+ 
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_array_get_string(p, json, 0, (char **) &val);
+  fail_unless(res == 0, "Failed to handle existing index 0: %s",
+    strerror(errno));
+  fail_unless(strcmp(val, "foo!") == 0, "Expected 'foo!', got '%s'", val);
+ 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_get_array_test) {
+  int res;
+  unsigned int idx;
+  const char *text;
+  pr_json_array_t *json, *val = NULL;
+
+  mark_point();
+  res = pr_json_array_get_array(NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_get_array(p, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_get_array(p, json, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  idx = 0;
+ 
+  mark_point();
+  res = pr_json_array_get_array(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent index %u", idx);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",false,\"bar\",[\"baz\"]]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_get_array(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle non-array index %u", idx);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  idx = 3;
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_array_get_array(p, json, idx, &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected array, got null");
+
+  mark_point();
+  (void) pr_json_array_free(val);
+
+  mark_point();
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_append_array_test) {
+  int res;
+  unsigned int idx;
+  const char *text;
+  pr_json_array_t *json, *val = NULL;
+
+  mark_point();
+  res = pr_json_array_append_array(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_append_array(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_append_array(p, json, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "[1, 1, 2, 3, 5, 8]";
+  val = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_append_array(p, json, val);
+  fail_unless(res == 0, "Failed to append array: %s", strerror(errno));
+
+  val = NULL;
+  idx = 0;
+ 
+  mark_point();
+  res = pr_json_array_get_array(p, json, idx, &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected array, got null");
+
+  mark_point();
+  (void) pr_json_array_free(val); 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_get_object_test) {
+  int res;
+  unsigned int idx;
+  const char *text;
+  pr_json_object_t *val = NULL;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_get_object(NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_get_object(p, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_get_object(p, json, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  idx = 0;
+ 
+  mark_point();
+  res = pr_json_array_get_object(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle nonexistent index %u", idx);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+ 
+  (void) pr_json_array_free(json);
+
+  text = "[\"foo\",false,\"bar\",{}]";
+  json = pr_json_array_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_get_object(p, json, idx, &val);
+  fail_unless(res < 0, "Failed to handle non-object index %u", idx);
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  idx = 3;
+  val = NULL;
+ 
+  mark_point();
+  res = pr_json_array_get_object(p, json, idx, &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected object, got null");
+
+  mark_point();
+  (void) pr_json_object_free(val);
+
+  mark_point();
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_array_append_object_test) {
+  int res;
+  unsigned int idx;
+  const char *text;
+  pr_json_object_t *val = NULL;
+  pr_json_array_t *json;
+
+  mark_point();
+  res = pr_json_array_append_object(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  mark_point();
+  res = pr_json_array_append_object(p, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null json");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  
+  json = pr_json_array_alloc(p);
+
+  mark_point();
+  res = pr_json_array_append_object(p, json, NULL);
+  fail_unless(res < 0, "Failed to handle null val");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "{\"foo\":1,\"bar\":2}";
+  val = pr_json_object_from_text(p, text);
+
+  mark_point();
+  res = pr_json_array_append_object(p, json, val);
+  fail_unless(res == 0, "Failed to append object: %s", strerror(errno));
+
+  val = NULL;
+  idx = 0;
+ 
+  mark_point();
+  res = pr_json_array_get_object(p, json, idx, &val);
+  fail_unless(res == 0, "Failed to handle existing index %u: %s", idx,
+    strerror(errno));
+  fail_unless(val != NULL, "Expected object, got null");
+
+  mark_point();
+  (void) pr_json_object_free(val); 
+  (void) pr_json_array_free(json);
+}
+END_TEST
+
+START_TEST(json_text_validate_test) {
+  int res;
+  const char *text;
+
+  mark_point();
+  res = pr_json_text_validate(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_json_text_validate(p, NULL);
+  fail_unless(res < 0, "Failed to handle null text");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "foo bar";
+
+  mark_point();
+  res = pr_json_text_validate(p, text);
+  fail_unless(res == FALSE, "Failed to handle invalid text '%s'", text);
+
+  text = "[{}]";
+
+  mark_point();
+  res = pr_json_text_validate(p, text);
+  fail_unless(res == TRUE, "Failed to handle valid text '%s'", text);
+}
+END_TEST
+
+Suite *tests_get_json_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("json");
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, json_object_free_test);
+  tcase_add_test(testcase, json_object_alloc_test);
+  tcase_add_test(testcase, json_object_from_text_test);
+  tcase_add_test(testcase, json_object_to_text_test);
+  tcase_add_test(testcase, json_object_count_test);
+  tcase_add_test(testcase, json_object_exists_test);
+  tcase_add_test(testcase, json_object_remove_test);
+  tcase_add_test(testcase, json_object_get_bool_test);
+  tcase_add_test(testcase, json_object_set_bool_test);
+  tcase_add_test(testcase, json_object_get_null_test);
+  tcase_add_test(testcase, json_object_set_null_test);
+  tcase_add_test(testcase, json_object_get_number_test);
+  tcase_add_test(testcase, json_object_set_number_test);
+  tcase_add_test(testcase, json_object_get_string_test);
+  tcase_add_test(testcase, json_object_set_string_test);
+  tcase_add_test(testcase, json_object_get_array_test);
+  tcase_add_test(testcase, json_object_set_array_test);
+  tcase_add_test(testcase, json_object_get_object_test);
+  tcase_add_test(testcase, json_object_set_object_test);
+
+  tcase_add_test(testcase, json_array_free_test);
+  tcase_add_test(testcase, json_array_alloc_test);
+  tcase_add_test(testcase, json_array_from_text_test);
+  tcase_add_test(testcase, json_array_to_text_test);
+  tcase_add_test(testcase, json_array_count_test);
+  tcase_add_test(testcase, json_array_exists_test);
+  tcase_add_test(testcase, json_array_remove_test);
+  tcase_add_test(testcase, json_array_get_bool_test);
+  tcase_add_test(testcase, json_array_append_bool_test);
+  tcase_add_test(testcase, json_array_get_null_test);
+  tcase_add_test(testcase, json_array_append_null_test);
+  tcase_add_test(testcase, json_array_get_number_test);
+  tcase_add_test(testcase, json_array_append_number_test);
+  tcase_add_test(testcase, json_array_get_string_test);
+  tcase_add_test(testcase, json_array_append_string_test);
+  tcase_add_test(testcase, json_array_get_array_test);
+  tcase_add_test(testcase, json_array_append_array_test);
+  tcase_add_test(testcase, json_array_get_object_test);
+  tcase_add_test(testcase, json_array_append_object_test);
+
+  tcase_add_test(testcase, json_text_validate_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/misc.c b/tests/api/misc.c
new file mode 100644
index 0000000..16d56cb
--- /dev/null
+++ b/tests/api/misc.c
@@ -0,0 +1,1193 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Miscellaneous tests
+ */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static unsigned int schedule_called = 0;
+static const char *misc_test_shutmsg = "/tmp/prt-shutmsg.dat";
+static const char *misc_test_readlink = "/tmp/prt-readlink.lnk";
+
+/* Fixtures */
+
+static void set_up(void) {
+  (void) unlink(misc_test_readlink);
+  (void) unlink(misc_test_shutmsg);
+
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_fs();
+  pr_fs_statcache_set_policy(PR_TUNABLE_FS_STATCACHE_SIZE,
+    PR_TUNABLE_FS_STATCACHE_MAX_AGE, 0);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("auth", 1, 20);
+    pr_trace_set_levels("fsio", 1, 20);
+    pr_trace_set_levels("fs.statcache", 1, 20);
+  }
+
+  schedule_called = 0;
+  session.user = NULL;
+}
+
+static void tear_down(void) {
+  (void) unlink(misc_test_readlink);
+  (void) unlink(misc_test_shutmsg);
+
+  pr_fs_statcache_set_policy(PR_TUNABLE_FS_STATCACHE_SIZE,
+    PR_TUNABLE_FS_STATCACHE_MAX_AGE, 0);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("auth", 0, 0);
+    pr_trace_set_levels("fsio", 0, 0);
+    pr_trace_set_levels("fs.statcache", 0, 0);
+  }
+
+  session.user = NULL;
+
+  if (p) {
+    destroy_pool(p);
+    p = session.pool = permanent_pool = NULL;
+  }
+}
+
+static void schedule_cb(void *arg1, void *arg2, void *arg3, void *arg4) {
+  schedule_called++;
+}
+
+/* Tests */
+
+START_TEST (schedule_test) {
+  mark_point();
+  schedule(NULL, 0, NULL, NULL, NULL, NULL);
+
+  mark_point();
+  schedule(schedule_cb, -1, NULL, NULL, NULL, NULL);
+
+  mark_point();
+  run_schedule();
+
+  mark_point();
+  schedule(schedule_cb, 0, NULL, NULL, NULL, NULL);
+
+  run_schedule();
+  fail_unless(schedule_called == 1, "Expected 1, got %u", schedule_called);
+
+  run_schedule();
+  fail_unless(schedule_called == 1, "Expected 1, got %u", schedule_called);
+
+  mark_point();
+  schedule(schedule_cb, 0, NULL, NULL, NULL, NULL);
+  schedule(schedule_cb, 0, NULL, NULL, NULL, NULL);
+
+  run_schedule();
+  fail_unless(schedule_called == 3, "Expected 3, got %u", schedule_called);
+
+  run_schedule();
+  fail_unless(schedule_called == 3, "Expected 3, got %u", schedule_called);
+
+  mark_point();
+
+  /* Schedule this callback to run after 2 "loops", i.e. calls to
+   * run_schedule().
+   */
+  schedule(schedule_cb, 2, NULL, NULL, NULL, NULL);
+
+  run_schedule();
+  fail_unless(schedule_called == 3, "Expected 3, got %u", schedule_called);
+
+  run_schedule();
+  fail_unless(schedule_called == 3, "Expected 3, got %u", schedule_called);
+
+  run_schedule();
+  fail_unless(schedule_called == 4, "Expected 4, got %u", schedule_called);
+}
+END_TEST
+
+START_TEST (get_name_max_test) {
+  long res;
+  char *path;
+  int fd;
+
+  res = get_name_max(NULL, -1);
+  fail_unless(res < 0, "Failed to handle invalid arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+  res = get_name_max(path, -1);
+  fail_if(res < 0, "Failed to handle path '%s': %s", path, strerror(errno));
+
+  fd = 1;
+  res = get_name_max(NULL, fd);
+
+  /* It seems that fpathconf(2) on some platforms will handle stdin as a
+   * valid file descriptor, and some will not.
+   */
+  if (res < 0) {
+    fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+START_TEST (dir_interpolate_test) {
+  char *res;
+  const char *path;
+
+  res = dir_interpolate(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_interpolate(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/foo";
+  res = dir_interpolate(p, path);
+  fail_unless(path != NULL, "Failed to interpolate '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected '%s', got '%s'", path, res);
+
+  mark_point();
+  path = "~foo.bar.bar.quxx.quzz/foo";
+  res = dir_interpolate(p, path);
+  fail_unless(path != NULL, "Failed to interpolate '%s': %s", path,
+    strerror(errno));
+  fail_unless(*path == '~', "Interpolated path with unknown user unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (dir_best_path_test) {
+  char *res;
+  const char *path;
+
+  res = dir_best_path(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_best_path(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/foo";
+  res = dir_best_path(p, path);
+  fail_unless(path != NULL, "Failed to get best path for '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected '%s', got '%s'", path, res);
+}
+END_TEST
+
+START_TEST (dir_canonical_path_test) {
+  char *res;
+  const char *path;
+
+  res = dir_canonical_path(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_canonical_path(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/foo";
+  res = dir_canonical_path(p, path);
+  fail_unless(path != NULL, "Failed to get canonical path for '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected '%s', got '%s'", path, res);
+}
+END_TEST
+
+START_TEST (dir_canonical_vpath_test) {
+  char *res;
+  const char *path;
+
+  res = dir_canonical_vpath(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_canonical_vpath(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/foo";
+  res = dir_canonical_vpath(p, path);
+  fail_unless(path != NULL, "Failed to get canonical vpath for '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected '%s', got '%s'", path, res);
+}
+END_TEST
+
+START_TEST (dir_readlink_test) {
+  int res, flags = 0;
+  const char *path;
+  char *buf, *dst_path, *expected_path;
+  size_t bufsz, dst_pathlen, expected_pathlen;
+
+  (void) unlink(misc_test_readlink);
+
+  /* Parameter validation */
+  res = dir_readlink(NULL, NULL, NULL, 0, flags);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_readlink(p, NULL, NULL, 0, flags);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = misc_test_readlink;
+  res = dir_readlink(p, path, NULL, 0, flags);
+  fail_unless(res < 0, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  bufsz = 1024;
+  buf = palloc(p, bufsz);
+  res = dir_readlink(p, path, buf, 0, flags);
+  fail_unless(res == 0, "Failed to handle zero buffer length");
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_unless(res < 0, "Failed to handle nonexistent file");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  dst_path = "";
+  res = symlink(dst_path, path);
+  if (res == 0) {
+    /* Some platforms will not allow creation of empty symlinks.  Nice of
+     * them.
+     */
+    res = dir_readlink(p, path, buf, bufsz, flags);
+    fail_unless(res == 0, "Failed to handle empty symlink");
+  }
+
+  /* Not chrooted, absolute dst path */
+  memset(buf, '\0', bufsz);
+  dst_path = "/home/user/file.dat";
+  dst_pathlen = strlen(dst_path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == dst_pathlen, "Expected length %lu, got %d",
+    (unsigned long) dst_pathlen, res);
+  fail_unless(strcmp(buf, dst_path) == 0, "Expected '%s', got '%s'",
+    dst_path, buf);
+
+  /* Not chrooted, relative dst path, flags to ignore rel path */
+  memset(buf, '\0', bufsz);
+  dst_path = "./file.dat";
+  dst_pathlen = strlen(dst_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == dst_pathlen, "Expected length %lu, got %d",
+    (unsigned long) dst_pathlen, res);
+  fail_unless(strcmp(buf, dst_path) == 0, "Expected '%s', got '%s'",
+    dst_path, buf);
+
+  /* Not chrooted, relative dst path, flags to HANDLE rel path */
+  memset(buf, '\0', bufsz);
+  dst_path = "./file.dat";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "/tmp/file.dat";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  flags = PR_DIR_READLINK_FL_HANDLE_REL_PATH;
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  /* Not chrooted, dst path longer than given buffer */
+  flags = 0;
+  memset(buf, '\0', bufsz);
+  res = dir_readlink(p, path, buf, 2, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless(res == 2, "Expected length 2, got %d", res);
+  fail_unless(strncmp(buf, dst_path, 2) == 0, "Expected '%*s', got '%*s'",
+    2, dst_path, 2, buf);
+
+  /* Chrooted to "/" */
+  session.chroot_path = "/";
+  memset(buf, '\0', bufsz);
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == dst_pathlen, "Expected length %lu, got %d",
+    (unsigned long) dst_pathlen, res);
+  fail_unless(strcmp(buf, dst_path) == 0, "Expected '%s', got '%s'",
+    dst_path, buf);
+
+  /* Chrooted, absolute destination path shorter than chroot path */
+  session.chroot_path = "/home/user";
+  memset(buf, '\0', bufsz);
+  dst_path = "/foo";
+  dst_pathlen = strlen(dst_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == dst_pathlen, "Expected length %lu, got %d",
+    (unsigned long) dst_pathlen, res);
+  fail_unless(strcmp(buf, dst_path) == 0, "Expected '%s', got '%s'",
+    dst_path, buf);
+
+  /* Chrooted, overlapping chroot to non-dir */
+  memset(buf, '\0', bufsz);
+  dst_path = "/home/user2";
+  dst_pathlen = strlen(dst_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == dst_pathlen, "Expected length %lu, got %d",
+    (unsigned long) dst_pathlen, res);
+  fail_unless(strcmp(buf, dst_path) == 0, "Expected '%s', got '%s'",
+    dst_path, buf);
+
+  /* Chrooted, absolute destination within chroot */
+  memset(buf, '\0', bufsz);
+  dst_path = "/home/user/file.txt";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "/file.txt";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  /* Chrooted, absolute destination outside of chroot */
+  memset(buf, '\0', bufsz);
+  dst_path = "/home/user/../file.txt";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "/home/file.txt";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  /* Chrooted, relative destination within chroot */
+  memset(buf, '\0', bufsz);
+  dst_path = "./file.txt";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "./file.txt";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  /* Chrooted, relative destination outside of chroot */
+  memset(buf, '\0', bufsz);
+  dst_path = "../file.txt";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "../file.txt";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  /* First, tell dir_readlink() to ignore relative destination paths. */
+  flags = 0;
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  /* Now do it again, telling dir_readlink() to handle relative destination
+   * paths.
+   */
+  memset(buf, '\0', bufsz);
+  dst_path = "../file.txt";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "/file.txt";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  flags = PR_DIR_READLINK_FL_HANDLE_REL_PATH;
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  /* One more time, this time changing the chroot path to align with the
+   * source path.
+   */
+  memset(buf, '\0', bufsz);
+  dst_path = "../file.txt";
+  dst_pathlen = strlen(dst_path);
+  expected_path = "/file.txt";
+  expected_pathlen = strlen(expected_path);
+
+  (void) unlink(path);
+  res = symlink(dst_path, path);
+  fail_unless(res == 0, "Failed to symlink '%s' to '%s': %s", path, dst_path,
+    strerror(errno));
+
+  session.chroot_path = "/tmp";
+  flags = PR_DIR_READLINK_FL_HANDLE_REL_PATH;
+  res = dir_readlink(p, path, buf, bufsz, flags);
+  fail_if(res < 0, "Failed to read '%s' symlink: %s", path, strerror(errno));
+  fail_unless((size_t) res == expected_pathlen, "Expected length %lu, got %d",
+    (unsigned long) expected_pathlen, res);
+  fail_unless(strcmp(buf, expected_path) == 0, "Expected '%s', got '%s'",
+    expected_path, buf);
+
+  (void) unlink(misc_test_readlink);
+}
+END_TEST
+
+START_TEST (dir_realpath_test) {
+  char *res;
+  const char *path;
+
+  res = dir_realpath(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_realpath(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/foo";
+  res = dir_realpath(p, path);
+  fail_unless(res == NULL, "Got real path for '%s' unexpectedly", path);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/";
+  res = dir_realpath(p, path);
+  fail_unless(res != NULL, "Failed to get real path for '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected '%s', got '%s'", path, res);
+}
+END_TEST
+
+START_TEST (dir_abs_path_test) {
+  char *res;
+  const char *path;
+
+  res = dir_abs_path(NULL, NULL, TRUE);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = dir_abs_path(p, NULL, TRUE);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  path = "/foo";
+  res = dir_abs_path(p, path, TRUE);
+  fail_unless(path != NULL, "Failed to get absolute path for '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, path) == 0, "Expected '%s', got '%s'", path, res);
+}
+END_TEST
+
+START_TEST (safe_token_test) {
+  char *res, *text, *expected;
+
+  mark_point();
+  expected = "";
+  res = safe_token(NULL);
+  fail_unless(res != NULL, "Failed to handle null arguments");
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  mark_point();
+  text = "";
+  expected = "";
+  res = safe_token(&text);
+  fail_unless(res != NULL, "Failed to handle null arguments");
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  mark_point();
+  text = "foo";
+  expected = text;
+  res = safe_token(&text);
+  fail_unless(res != NULL, "Failed to handle null arguments");
+  fail_unless(res == expected, "Expected '%s', got '%s'", expected, res);
+  fail_unless(strcmp(text, "") == 0, "Expected '', got '%s'", text);
+
+  mark_point();
+  text = "  foo";
+  expected = text + 2;
+  res = safe_token(&text);
+  fail_unless(res != NULL, "Failed to handle null arguments");
+  fail_unless(res == expected, "Expected '%s', got '%s'", expected, res);
+  fail_unless(strcmp(text, "") == 0, "Expected '', got '%s'", text);
+
+  mark_point();
+  text = "  \t";
+  expected = "";
+  res = safe_token(&text);
+  fail_unless(res != NULL, "Failed to handle null arguments");
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+}
+END_TEST
+
+static int write_shutmsg(const char *path, const char *line) {
+  FILE *fh;
+  int res;
+  size_t line_len;
+
+  fh = fopen(path, "w+");
+  if (fh == NULL) {
+    return -1;
+  }
+
+
+  line_len = strlen(line);
+  fwrite(line, line_len, 1, fh);
+
+  res = fclose(fh);
+  return res;
+}
+
+START_TEST (check_shutmsg_test) {
+  int res;
+  const char *path;
+  time_t when_shutdown = 0, when_deny = 0, when_disconnect = 0;
+  char shutdown_msg[PR_TUNABLE_BUFFER_SIZE];
+
+  res = check_shutmsg(NULL, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/foo/bar/baz/quxx/quzz";
+  res = check_shutmsg(path, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle nonexistent path");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  path = "/";
+  res = check_shutmsg(path, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle directory path");
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  /* XXX More testing needed */
+
+  path = misc_test_shutmsg;
+
+  (void) unlink(path);
+  res = write_shutmsg(path,
+    "1970 1 1 0 0 0 0000 0000\nGoodbye, cruel world!\n");
+  fail_unless(res == 0, "Failed to write '%s': %s", path, strerror(errno));
+
+  memset(shutdown_msg, '\0', sizeof(shutdown_msg));
+  pr_env_set(p, "TZ", "GMT");
+
+  mark_point();
+  res = check_shutmsg(path, &when_shutdown, &when_deny, &when_disconnect,
+    shutdown_msg, sizeof(shutdown_msg));
+  fail_unless(res == 1, "Expected 1, got %d", res);
+  fail_unless(when_shutdown == (time_t) 0, "Expected 0, got %lu",
+    (unsigned long) when_shutdown);
+  fail_unless(when_deny == (time_t) 0, "Expected 0, got %lu",
+    (unsigned long) when_deny);
+  fail_unless(when_disconnect == (time_t) 0, "Expected 0, got %lu",
+    (unsigned long) when_disconnect);
+  fail_unless(strcmp(shutdown_msg, "Goodbye, cruel world!") == 0,
+    "Expected 'Goodbye, cruel world!', got '%s'", shutdown_msg);
+
+  (void) unlink(path);
+  res = write_shutmsg(path,
+    "2340 1 1 0 0 0 0000 0000\nGoodbye, cruel world!\n");
+  fail_unless(res == 0, "Failed to write '%s': %s", path, strerror(errno));
+
+  mark_point();
+  res = check_shutmsg(path, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res == 1, "Expected 1, got %d", res);
+
+  (void) unlink(path);
+  res = write_shutmsg(path,
+    "0 0 0 0 0 0 0000 0000\nGoodbye, cruel world!\n");
+  fail_unless(res == 0, "Failed to write '%s': %s", path, strerror(errno));
+
+  mark_point();
+  res = check_shutmsg(path, NULL, NULL, NULL, NULL, 0);
+
+  (void) unlink(misc_test_shutmsg);
+}
+END_TEST
+
+START_TEST (memscrub_test) {
+  size_t len;
+  char *expected, *text;
+
+  mark_point();
+  pr_memscrub(NULL, 1);
+
+  expected = "Hello, World!";
+  text = pstrdup(p, expected);
+
+  mark_point();
+  pr_memscrub(text, 0);
+
+  len = strlen(text);
+
+  mark_point();
+  pr_memscrub(text, len);
+  fail_unless(strncmp(text, expected, len + 1) != 0,
+    "Expected other than '%s'", expected);
+}
+END_TEST
+
+START_TEST (getopt_reset_test) {
+  mark_point();
+  pr_getopt_reset();
+}
+END_TEST
+
+START_TEST (exists_test) {
+  int res;
+  const char *path;
+
+  res = exists(NULL);
+  fail_unless(res == FALSE, "Failed to handle null path");
+
+  path = "/";
+  res = exists(path);
+  fail_unless(res == TRUE, "Expected TRUE for path '%s', got FALSE", path);
+}
+END_TEST
+
+START_TEST (exists2_test) {
+  int res;
+  const char *path;
+
+  res = exists2(NULL, NULL);
+  fail_unless(res == FALSE, "Failed to handle null arguments");
+
+  res = exists2(p, NULL);
+  fail_unless(res == FALSE, "Failed to handle null path");
+
+  path = "/";
+  res = exists2(p, path);
+  fail_unless(res == TRUE, "Expected TRUE for path '%s', got FALSE", path);
+}
+END_TEST
+
+START_TEST (dir_exists_test) {
+  int res;
+  const char *path;
+
+  res = dir_exists(NULL);
+  fail_unless(res == FALSE, "Failed to handle null path");
+
+  path = "/";
+  res = dir_exists(path);
+  fail_unless(res == TRUE, "Expected TRUE for path '%s', got FALSE", path);
+
+  path = "./api-tests";
+  res = dir_exists(path);
+  fail_unless(res == FALSE, "Expected FALSE for path '%s', got TRUE", path);
+}
+END_TEST
+
+START_TEST (dir_exists2_test) {
+  int res;
+  const char *path;
+
+  res = dir_exists2(NULL, NULL);
+  fail_unless(res == FALSE, "Failed to handle null arguments");
+
+  res = dir_exists2(p, NULL);
+  fail_unless(res == FALSE, "Failed to handle null path");
+
+  path = "/";
+  res = dir_exists2(p, path);
+  fail_unless(res == TRUE, "Expected TRUE for path '%s', got FALSE", path);
+
+  path = "./api-tests";
+  res = dir_exists2(p, path);
+  fail_unless(res == FALSE, "Expected FALSE for path '%s', got TRUE", path);
+}
+END_TEST
+
+START_TEST (symlink_mode_test) {
+  mode_t res;
+  const char *path;
+
+  res = symlink_mode(NULL);
+  fail_unless(res == 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+  res = symlink_mode(path);
+  fail_unless(res == 0, "Found mode for non-symlink '%s'", path);
+}
+END_TEST
+
+START_TEST (symlink_mode2_test) {
+  mode_t res;
+  const char *path;
+
+  res = symlink_mode2(NULL, NULL);
+  fail_unless(res == 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = symlink_mode2(p, NULL);
+  fail_unless(res == 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+  res = symlink_mode2(p, path);
+  fail_unless(res == 0, "Found mode for non-symlink '%s'", path);
+}
+END_TEST
+
+START_TEST (file_mode_test) {
+  mode_t res;
+  const char *path;
+
+  res = file_mode(NULL);
+  fail_unless(res == 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+  res = file_mode(path);
+  fail_unless(res != 0, "Failed to find mode for '%s': %s", path,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (file_mode2_test) {
+  mode_t res;
+  const char *path;
+
+  res = file_mode2(NULL, NULL);
+  fail_unless(res == 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = file_mode2(p, NULL);
+  fail_unless(res == 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/";
+  res = file_mode2(p, path);
+  fail_unless(res != 0, "Failed to find mode for '%s': %s", path,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (file_exists_test) {
+  int res;
+  const char *path;
+
+  res = file_exists(NULL);
+  fail_unless(res == FALSE, "Failed to handle null path");
+
+  path = "/";
+  res = file_exists(path);
+  fail_unless(res == FALSE, "Expected FALSE for path '%s', got TRUE", path);
+
+  path = "./api-tests";
+  res = file_exists(path);
+  fail_unless(res == TRUE, "Expected TRUE for path '%s', got FALSE", path);
+}
+END_TEST
+
+START_TEST (file_exists2_test) {
+  int res;
+  const char *path;
+
+  res = file_exists2(NULL, NULL);
+  fail_unless(res == FALSE, "Failed to handle null arguments");
+
+  res = file_exists2(p, NULL);
+  fail_unless(res == FALSE, "Failed to handle null path");
+
+  path = "/";
+  res = file_exists2(p, path);
+  fail_unless(res == FALSE, "Expected FALSE for path '%s', got TRUE", path);
+
+  path = "./api-tests";
+  res = file_exists2(p, path);
+  fail_unless(res == TRUE, "Expected TRUE for path '%s', got FALSE", path);
+}
+END_TEST
+
+START_TEST (gmtime_test) {
+  struct tm *res;
+  time_t now;
+
+  mark_point();
+  res = pr_gmtime(NULL, NULL); 
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  time(&now);
+
+  mark_point();
+  res = pr_gmtime(NULL, &now);
+  fail_unless(res != NULL, "Failed to handle %lu: %s", (unsigned long) now,
+    strerror(errno));
+
+  mark_point();
+  res = pr_gmtime(p, &now);
+  fail_unless(res != NULL, "Failed to handle %lu: %s", (unsigned long) now,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (localtime_test) {
+  struct tm *res;
+  time_t now;
+
+  mark_point();
+  res = pr_localtime(NULL, NULL); 
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  time(&now);
+
+  mark_point();
+  res = pr_localtime(NULL, &now);
+  fail_unless(res != NULL, "Failed to handle %lu: %s", (unsigned long) now,
+    strerror(errno));
+
+  mark_point();
+  res = pr_localtime(p, &now);
+  fail_unless(res != NULL, "Failed to handle %lu: %s", (unsigned long) now,
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (strtime_test) {
+  const char *res;
+  time_t now;
+
+  mark_point();
+  now = 0;
+  res = pr_strtime(now);
+  fail_unless(res != NULL, "Failed to convert time %lu: %s",
+    (unsigned long) now, strerror(errno));
+}
+END_TEST
+
+START_TEST (strtime2_test) {
+  const char *res;
+  char *expected;
+  time_t now;
+
+  mark_point();
+  now = 0;
+  expected = "Thu Jan 01 00:00:00 1970";
+  res = pr_strtime2(now, TRUE);
+  fail_unless(res != NULL, "Failed to convert time %lu: %s",
+    (unsigned long) now, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+}
+END_TEST
+
+START_TEST (timeval2millis_test) {
+  int res;
+  struct timeval tv;
+  uint64_t ms;
+
+  res = pr_timeval2millis(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_timeval2millis(&tv, NULL);
+  fail_unless(res < 0, "Failed to handle null millis argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  tv.tv_sec = tv.tv_usec = 0;
+  res = pr_timeval2millis(&tv, &ms);
+  fail_unless(res == 0, "Failed to convert timeval to millis: %s",
+    strerror(errno));
+  fail_unless(ms == 0, "Expected 0 ms, got %lu", (unsigned long) ms);
+}
+END_TEST
+
+START_TEST (gettimeofday_millis_test) {
+  int res;
+  uint64_t ms;
+
+  res = pr_gettimeofday_millis(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  ms = 0;
+  res = pr_gettimeofday_millis(&ms);
+  fail_unless(res == 0, "Failed to get current time ms: %s", strerror(errno));
+  fail_unless(ms > 0, "Expected >0, got %lu", (unsigned long) ms);
+}
+END_TEST
+
+START_TEST (path_subst_uservar_test) {
+  const char *path = NULL, *res, *original, *expected;
+
+  res = path_subst_uservar(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = path_subst_uservar(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path pointer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = path_subst_uservar(p, &path);
+  fail_unless(res == NULL, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  original = expected = "somepathhere";
+  path = pstrdup(p, expected);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  session.user = "user";
+  original = "/home/%u";
+  expected = "/home/user";
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  session.user = "user";
+  original = "/home/%u[";
+  expected = "/home/user[";
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  session.user = "user";
+  original = "/home/%u[]";
+  expected = "/home/user[]";
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  session.user = "user";
+  original = "/home/users/%u[0]/%u[0]%u[1]/%u";
+  expected = "/home/users/u/us/user";
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  /* Attempt to use an invalid index */
+  session.user = "user";
+  original = "/home/users/%u[a]/%u[b]%u[c]/%u";
+  expected = original;
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  /* Attempt to use an out-of-bounds index */
+  session.user = "user";
+  original = "/home/users/%u[0]/%u[-1]%u[1]/%u";
+  expected = original;
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  /* Attempt to use an out-of-bounds index */
+  session.user = "user";
+  original = "/home/users/%u[0]/%u[0]%u[4]/%u";
+  expected = original;
+  path = pstrdup(p, original);
+  mark_point();
+  res = path_subst_uservar(p, &path);
+  fail_unless(res != NULL, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+}
+END_TEST
+
+Suite *tests_get_misc_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("misc");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, schedule_test);
+  tcase_add_test(testcase, get_name_max_test);
+  tcase_add_test(testcase, dir_interpolate_test);
+  tcase_add_test(testcase, dir_best_path_test);
+  tcase_add_test(testcase, dir_canonical_path_test);
+  tcase_add_test(testcase, dir_canonical_vpath_test);
+  tcase_add_test(testcase, dir_readlink_test);
+  tcase_add_test(testcase, dir_realpath_test);
+  tcase_add_test(testcase, dir_abs_path_test);
+  tcase_add_test(testcase, symlink_mode_test);
+  tcase_add_test(testcase, symlink_mode2_test);
+  tcase_add_test(testcase, file_mode_test);
+  tcase_add_test(testcase, file_mode2_test);
+  tcase_add_test(testcase, exists_test);
+  tcase_add_test(testcase, exists2_test);
+  tcase_add_test(testcase, dir_exists_test);
+  tcase_add_test(testcase, dir_exists2_test);
+  tcase_add_test(testcase, file_exists_test);
+  tcase_add_test(testcase, file_exists2_test);
+  tcase_add_test(testcase, safe_token_test);
+  tcase_add_test(testcase, check_shutmsg_test);
+  tcase_add_test(testcase, memscrub_test);
+  tcase_add_test(testcase, getopt_reset_test);
+  tcase_add_test(testcase, gmtime_test);
+  tcase_add_test(testcase, localtime_test);
+  tcase_add_test(testcase, strtime_test);
+  tcase_add_test(testcase, strtime2_test);
+  tcase_add_test(testcase, timeval2millis_test);
+  tcase_add_test(testcase, gettimeofday_millis_test);
+  tcase_add_test(testcase, path_subst_uservar_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/modules.c b/tests/api/modules.c
index b98f81f..a72cc91 100644
--- a/tests/api/modules.c
+++ b/tests/api/modules.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,12 +22,12 @@
  * OpenSSL in the source distribution.
  */
 
-/* Modules API tests
- * $Id: modules.c,v 1.3 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Modules API tests */
 
 #include "tests.h"
 
+extern module *loaded_modules;
+
 static pool *p = NULL;
 
 static void set_up(void) {
@@ -39,26 +39,77 @@ static void set_up(void) {
 }
 
 static void tear_down(void) {
+  loaded_modules = NULL;
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
+    p = permanent_pool = NULL;
   } 
 }
 
+/* Tests */
+
+static int sess_init_eperm = FALSE;
+
+static int module_sess_init_cb(void) {
+  if (sess_init_eperm) {
+    sess_init_eperm = FALSE;
+    errno = EPERM;
+    return -1;
+  }
+
+  return 0;
+}
+
+START_TEST (module_sess_init_test) {
+  int res;
+  module m;
+
+  res = modules_session_init();
+  fail_unless(res == 0, "Failed to initialize modules: %s", strerror(errno));
+
+  memset(&m, 0, sizeof(m));
+  m.name = "testsuite";
+
+  loaded_modules = &m;
+  res = modules_session_init();
+  fail_unless(res == 0, "Failed to initialize modules: %s", strerror(errno));
+
+  m.sess_init = module_sess_init_cb;
+  res = modules_session_init();
+  fail_unless(res == 0, "Failed to initialize modules: %s", strerror(errno));
+
+  sess_init_eperm = TRUE;
+  res = modules_session_init();
+  fail_unless(res < 0, "Initialized modules unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  loaded_modules = NULL;
+}
+END_TEST
+
+START_TEST (module_command_exists_test) {
+  int res;
+
+  res = command_exists(NULL);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+}
+END_TEST
+
 START_TEST (module_exists_test) {
   unsigned char res;
   module m;
 
   res = pr_module_exists(NULL);
   fail_unless(res == FALSE, "Failed to handle null argument");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_module_exists("mod_foo.c");
   fail_unless(res == FALSE, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
   memset(&m, 0, sizeof(m));
   m.name = "bar";
@@ -67,16 +118,18 @@ START_TEST (module_exists_test) {
 
   res = pr_module_exists("mod_foo.c");
   fail_unless(res == FALSE, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
   res = pr_module_exists("mod_bar.c");
   fail_unless(res == TRUE, "Failed to detect existing module");
 
   res = pr_module_exists("mod_BAR.c");
   fail_unless(res == FALSE, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  loaded_modules = NULL;
 }
 END_TEST
 
@@ -85,13 +138,13 @@ START_TEST (module_get_test) {
 
   res = pr_module_get(NULL);
   fail_unless(res == NULL, "Failed to handle null argument");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_module_get("mod_foo.c");
   fail_unless(res == NULL, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
   memset(&m, 0, sizeof(m));
   m.name = "bar";
@@ -100,8 +153,8 @@ START_TEST (module_get_test) {
 
   res = pr_module_get("mod_foo.c");
   fail_unless(res == NULL, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
   res = pr_module_get("mod_bar.c");
   fail_unless(res != NULL, "Failed to detect existing module");
@@ -109,8 +162,51 @@ START_TEST (module_get_test) {
 
   res = pr_module_get("mod_BAR.c");
   fail_unless(res == NULL, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  loaded_modules = NULL;
+}
+END_TEST
+
+static unsigned int listed = 0;
+static int module_listf(const char *fmt, ...) {
+  listed++;
+  return 0;
+}
+
+START_TEST (module_list_test) {
+  module m, m2;
+
+  mark_point();
+  listed = 0;
+  modules_list2(module_listf, 0);
+  fail_unless(listed > 0, "Expected >0, got %u", listed);
+
+  memset(&m, 0, sizeof(m));
+  m.name = "testsuite";
+  m.module_version = "a.b";
+
+  memset(&m2, 0, sizeof(m2));
+  m2.name = "testsuite2";
+
+  m.next = &m2;
+  loaded_modules = &m;
+
+  mark_point();
+  listed = 0;
+  modules_list2(module_listf, PR_MODULES_LIST_FL_SHOW_STATIC);
+  fail_unless(listed > 0, "Expected >0, got %u", listed);
+
+  mark_point();
+  listed = 0;
+  modules_list2(module_listf, PR_MODULES_LIST_FL_SHOW_VERSION);
+  fail_unless(listed > 0, "Expected >0, got %u", listed);
+
+  mark_point();
+  modules_list(PR_MODULES_LIST_FL_SHOW_STATIC);
+
+  loaded_modules = NULL;
 }
 END_TEST
 
@@ -124,31 +220,31 @@ START_TEST (module_load_test) {
   module m;
 
   res = pr_module_load(NULL);
-  fail_unless(res == -1, "Failed to handle null argument");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   memset(&m, 0, sizeof(m));
 
   res = pr_module_load(&m);
-  fail_unless(res == -1, "Failed to handle null name");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle null name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   m.name = "foo";
 
   res = pr_module_load(&m);
-  fail_unless(res == -1, "Failed to handle badly versioned module");
-  fail_unless(errno == EACCES, "Failed to set errno to EACCES (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle badly versioned module");
+  fail_unless(errno == EACCES, "Expected EACCES (%d), got %s (%d)", EACCES,
+    strerror(errno), errno);
 
   m.api_version = PR_MODULE_API_VERSION;
   m.init = init_cb;
 
   res = pr_module_load(&m);
-  fail_unless(res == -1, "Failed to handle bad module init callback");
-  fail_unless(errno == EPERM, "Failed to set errno to EPERM (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle bad module init callback");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
 
   m.init = NULL;
 
@@ -156,34 +252,47 @@ START_TEST (module_load_test) {
   fail_unless(res == 0, "Failed to load module: %s", strerror(errno));
 
   res = pr_module_load(&m);
-  fail_unless(res == -1, "Failed to handle duplicate module load");
-  fail_unless(errno == EEXIST, "Failed to set errno to EEXIST (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle duplicate module load");
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
 }
 END_TEST
 
 START_TEST (module_unload_test) {
   int res;
   module m;
+  authtable authtab[] = {
+    { 0, "setpwent", NULL },
+    { 0, NULL, NULL }
+  };
+  cmdtable cmdtab[] = {
+    { CMD, C_RETR, G_READ, NULL, TRUE, FALSE, CL_READ },
+    { HOOK, "foo", G_READ, NULL, FALSE, FALSE },
+    { 0, NULL }
+  };
+  conftable conftab[] = {
+    { "TestSuite", NULL, NULL },
+    { NULL }
+  };
 
   res = pr_module_unload(NULL);
-  fail_unless(res == -1, "Failed to handle null argument");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   memset(&m, 0, sizeof(m));
 
   res = pr_module_unload(&m);
-  fail_unless(res == -1, "Failed to handle null module name");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle null module name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   m.name = "bar";
 
   res = pr_module_unload(&m);
-  fail_unless(res == -1, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle nonexistent module");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
   loaded_modules = &m;
 
@@ -191,9 +300,129 @@ START_TEST (module_unload_test) {
   fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
 
   res = pr_module_unload(&m);
-  fail_unless(res == -1, "Failed to handle nonexistent module");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(res < 0, "Failed to handle nonexistent module");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  m.authtable = authtab;
+  m.cmdtable = cmdtab;
+  m.conftable = conftab;
+  loaded_modules = &m;
+
+  res = pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
+
+  loaded_modules = NULL;
+}
+END_TEST
+
+START_TEST (module_load_authtab_test) {
+  int res;
+  module m;
+  authtable authtab[] = {
+    { 0, "setpwent", NULL },
+    { 0, NULL, NULL }
+  };
+
+  res = pr_module_load_authtab(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(&m, 0, sizeof(m));
+
+  res = pr_module_load_authtab(&m);
+  fail_unless(res < 0, "Failed to handle null module name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  m.name = "testsuite";
+  res = pr_module_load_authtab(&m);
+  fail_unless(res == 0, "Failed to load module authtab: %s", strerror(errno));
+
+  pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
+
+  m.authtable = authtab;
+  res = pr_module_load_authtab(&m);
+  fail_unless(res == 0, "Failed to load module authtab: %s", strerror(errno));
+
+  pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (module_load_cmdtab_test) {
+  int res;
+  module m;
+  cmdtable cmdtab[] = {
+    { CMD, C_RETR, G_READ, NULL, TRUE, FALSE, CL_READ },
+    { HOOK, "foo", G_READ, NULL, FALSE, FALSE },
+    { 0, NULL }
+  };
+
+  res = pr_module_load_cmdtab(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(&m, 0, sizeof(m));
+
+  res = pr_module_load_cmdtab(&m);
+  fail_unless(res < 0, "Failed to handle null module name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  m.name = "testsuite";
+  res = pr_module_load_cmdtab(&m);
+  fail_unless(res == 0, "Failed to load module cmdtab: %s", strerror(errno));
+
+  pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
+
+  m.name = "testsuite";
+  m.cmdtable = cmdtab;
+  res = pr_module_load_cmdtab(&m);
+  fail_unless(res == 0, "Failed to load module cmdtab: %s", strerror(errno));
+
+  pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (module_load_conftab_test) {
+  int res;
+  module m;
+  conftable conftab[] = {
+    { "TestSuite", NULL, NULL },
+    { NULL }
+  };
+
+  res = pr_module_load_conftab(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(&m, 0, sizeof(m));
+
+  res = pr_module_load_conftab(&m);
+  fail_unless(res < 0, "Failed to handle null module name");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  m.name = "testsuite";
+  res = pr_module_load_conftab(&m);
+  fail_unless(res == 0, "Failed to load module conftab: %s", strerror(errno));
+
+  pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
+
+  m.conftable = conftab;
+  res = pr_module_load_conftab(&m);
+  fail_unless(res == 0, "Failed to load module conftab: %s", strerror(errno));
+
+  pr_module_unload(&m);
+  fail_unless(res == 0, "Failed to unload module: %s", strerror(errno));
 }
 END_TEST
 
@@ -252,6 +481,72 @@ START_TEST (module_call_test) {
 }
 END_TEST
 
+START_TEST (module_create_ret_test) {
+  cmd_rec *cmd;
+  modret_t *mr;
+  char *numeric, *msg;
+
+  mr = mod_create_ret(NULL, 0, NULL, NULL);
+  fail_unless(mr == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  cmd = pr_cmd_alloc(p, 1, "testsuite");
+  mr = mod_create_ret(cmd, 1, NULL, NULL);
+  fail_unless(mr != NULL, "Failed to create modret: %s", strerror(errno));
+  fail_unless(mr->mr_error == 1, "Expected 1, got %d", mr->mr_error);
+  fail_unless(mr->mr_numeric == NULL, "Expected null, got '%s'",
+    mr->mr_numeric);
+  fail_unless(mr->mr_message == NULL, "Expected null, got '%s'",
+    mr->mr_message);
+
+  numeric = "foo";
+  msg = "bar";
+  mr = mod_create_ret(cmd, 1, numeric, msg);
+  fail_unless(mr != NULL, "Failed to create modret: %s", strerror(errno));
+  fail_unless(mr->mr_error == 1, "Expected 1, got %d", mr->mr_error);
+  fail_unless(mr->mr_numeric != NULL, "Expected '%s', got null");
+  fail_unless(strcmp(mr->mr_numeric, numeric) == 0,
+    "Expected '%s', got '%s'", numeric, mr->mr_numeric);
+  fail_unless(mr->mr_message != NULL, "Expected '%s', got null");
+  fail_unless(strcmp(mr->mr_message, msg) == 0,
+    "Expected '%s', got '%s'", msg, mr->mr_message);
+}
+END_TEST
+
+START_TEST (module_create_error_test) {
+  cmd_rec *cmd;
+  modret_t *mr;
+
+  mr = mod_create_error(NULL, 0);
+  fail_unless(mr == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  cmd = pr_cmd_alloc(p, 1, "testsuite");
+  mr = mod_create_error(cmd, 1);
+  fail_unless(mr != NULL, "Failed to create modret: %s", strerror(errno));
+  fail_unless(mr->mr_error == 1, "Expected 1, got %d", mr->mr_error);
+}
+END_TEST
+
+START_TEST (module_create_data_test) {
+  cmd_rec *cmd;
+  modret_t *mr;
+  int data = 1;
+
+  mr = mod_create_data(NULL, NULL);
+  fail_unless(mr == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  cmd = pr_cmd_alloc(p, 1, "testsuite");
+  mr = mod_create_data(cmd, &data);
+  fail_unless(mr != NULL, "Failed to create modret: %s", strerror(errno));
+  fail_unless(mr->data == &data, "Expected %p, got %p", &data, mr->data);
+}
+END_TEST
+
 Suite *tests_get_modules_suite(void) {
   Suite *suite;
   TCase *testcase;
@@ -261,17 +556,22 @@ Suite *tests_get_modules_suite(void) {
   testcase = tcase_create("module");
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
+  tcase_add_test(testcase, module_sess_init_test);
+  tcase_add_test(testcase, module_command_exists_test);
   tcase_add_test(testcase, module_exists_test);
   tcase_add_test(testcase, module_get_test);
+  tcase_add_test(testcase, module_list_test);
   tcase_add_test(testcase, module_load_test);
   tcase_add_test(testcase, module_unload_test);
+  tcase_add_test(testcase, module_load_authtab_test);
+  tcase_add_test(testcase, module_load_cmdtab_test);
+  tcase_add_test(testcase, module_load_conftab_test);
   tcase_add_test(testcase, module_call_test);
 
-  suite_add_tcase(suite, testcase);
-
-  /* XXX At some point, unit tests for the mod_create_*() functions
-   * should be written.
-   */
+  tcase_add_test(testcase, module_create_ret_test);
+  tcase_add_test(testcase, module_create_error_test);
+  tcase_add_test(testcase, module_create_data_test);
 
+  suite_add_tcase(suite, testcase);
   return suite;
 }
diff --git a/tests/api/netacl.c b/tests/api/netacl.c
index ef3fb96..2a5dc6e 100644
--- a/tests/api/netacl.c
+++ b/tests/api/netacl.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2016 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* NetACL API tests
- * $Id: netacl.c,v 1.4 2011-05-23 20:50:31 castaglia Exp $
- */
+/* NetACL API tests */
 
 #include "tests.h"
 
@@ -38,13 +36,22 @@ static void set_up(void) {
   }
 
   init_netaddr();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("dns", 1, 20);
+    pr_trace_set_levels("netacl", 1, 20);
+  }
 }
 
 static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("dns", 0, 0);
+    pr_trace_set_levels("netacl", 0, 0);
+  }
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
+    p = permanent_pool = NULL;
   } 
 }
 
@@ -107,6 +114,11 @@ START_TEST (netacl_create_test) {
   fail_unless(res == NULL, "Failed to handle bad ACL string '%s': %s", acl_str,
     strerror(errno));
 
+  acl_str = pstrdup(p, "0.0.0.0/0");
+  res = pr_netacl_create(p, acl_str);
+  fail_unless(res != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
 #ifdef PR_USE_IPV6
   acl_str = pstrdup(p, "::1/36");
   res = pr_netacl_create(p, acl_str);
@@ -224,6 +236,33 @@ START_TEST (netacl_create_test) {
   acl_type = pr_netacl_get_type(res);
   fail_unless(acl_type == PR_NETACL_TYPE_DNSMATCH,
     "Failed to have DNSMATCH type for ACL string '%s'", acl_str);
+
+  acl_str = pstrdup(p, "foobar");
+  res = pr_netacl_create(p, acl_str);
+  fail_unless(res != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  acl_type = pr_netacl_get_type(res);
+  fail_unless(acl_type == PR_NETACL_TYPE_DNSMATCH,
+    "Failed to have DNSMATCH type for ACL string '%s'", acl_str);
+
+  acl_str = pstrdup(p, "!foobar");
+  res = pr_netacl_create(p, acl_str);
+  fail_unless(res != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  acl_type = pr_netacl_get_type(res);
+  fail_unless(acl_type == PR_NETACL_TYPE_DNSMATCH,
+    "Failed to have DNSMATCH type for ACL string '%s'", acl_str);
+
+  acl_str = pstrdup(p, "!fo?bar");
+  res = pr_netacl_create(p, acl_str);
+  fail_unless(res != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  acl_type = pr_netacl_get_type(res);
+  fail_unless(acl_type == PR_NETACL_TYPE_DNSGLOB,
+    "Failed to have DNSGLOB type for ACL string '%s'", acl_str);
 }
 END_TEST
 
@@ -333,6 +372,15 @@ START_TEST (netacl_get_str_test) {
   fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
   fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
 
+  acl_str = pstrdup(p, "0.0.0.0/0");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "0.0.0.0/0 <IP address mask, 0-bit mask>";
+  res = pr_netacl_get_str(p, acl);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
   acl_str = pstrdup(p, "!127.0.0.1/24");
   acl = pr_netacl_create(p, acl_str);
   fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
@@ -373,6 +421,162 @@ START_TEST (netacl_get_str_test) {
 }
 END_TEST
 
+START_TEST (netacl_get_str2_test) {
+  pr_netacl_t *acl;
+  char *acl_str, *ok;
+  const char *res;
+  int flags = PR_NETACL_FL_STR_NO_DESC;
+
+  res = pr_netacl_get_str2(NULL, NULL, 0);
+  fail_unless(res == NULL, "Failed to handle NULL arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  res = pr_netacl_get_str2(p, NULL, 0);
+  fail_unless(res == NULL, "Failed to handle NULL ACL");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  acl_str = "all";
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  res = pr_netacl_get_str2(NULL, acl, flags);
+  fail_unless(res == NULL, "Failed to handle NULL pool");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+
+  ok = "all";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = "AlL";
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = "None";
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "none";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "127.0.0.1");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "127.0.0.1";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "!127.0.0.1");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "!127.0.0.1";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "127.0.0.");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "127.0.0.*";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "localhost");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "localhost";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, ".castaglia.org");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "*.castaglia.org";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "127.0.0.1/24");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "127.0.0.1/24";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "127.0.0.1/0");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "127.0.0.1/0";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "0.0.0.0/0");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "0.0.0.0/0";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "!127.0.0.1/24");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "!127.0.0.1/24";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+#ifdef PR_USE_IPV6
+  acl_str = pstrdup(p, "::1/24");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "::1/24";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "::1/127");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "::1/127";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  acl_str = pstrdup(p, "::ffff:127.0.0.1/127");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to create ACL: %s", strerror(errno));
+
+  ok = "::ffff:127.0.0.1/127";
+  res = pr_netacl_get_str2(p, acl, flags);
+  fail_unless(res != NULL, "Failed to get ACL string: %s", strerror(errno));
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+#endif
+}
+END_TEST
+
 START_TEST (netacl_dup_test) {
   pr_netacl_t *acl, *res;
 
@@ -401,9 +605,9 @@ END_TEST
 
 START_TEST (netacl_match_test) {
   pr_netacl_t *acl;
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   char *acl_str;
-  int have_localdomain = FALSE, res;
+  int have_localdomain = FALSE, res, reverse_dns;
 
   res = pr_netacl_match(NULL, NULL);
   fail_unless(res == -2, "Failed to handle NULL arguments");
@@ -422,12 +626,14 @@ START_TEST (netacl_match_test) {
   fail_unless(addr != NULL, "Failed to get addr for '%s': %s", "localhost",
     strerror(errno));
 
-  /* It's possible that the DNS name for 'localhost' that is used will
-   * actually be 'localhost.localdomain', depending on the contents of
-   * the host's /etc/hosts file.
-   */
-  if (strcmp(pr_netaddr_get_dnsstr(addr), "localhost.localdomain") == 0) {
-    have_localdomain = TRUE;
+  if (getenv("TRAVIS") == NULL) {
+    /* It's possible that the DNS name for 'localhost' that is used will
+     * actually be 'localhost.localdomain', depending on the contents of
+     * the host's /etc/hosts file.
+     */
+    if (strcmp(pr_netaddr_get_dnsstr(addr), "localhost.localdomain") == 0) {
+      have_localdomain = TRUE;
+    }
   }
 
   res = pr_netacl_match(NULL, addr);
@@ -500,6 +706,15 @@ START_TEST (netacl_match_test) {
   fail_unless(res == -1, "Failed to negatively match ACL to addr: %s",
     strerror(errno));
 
+  acl_str = pstrdup(p, "!1.2.3.4/24");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  res = pr_netacl_match(acl, addr);
+  fail_unless(res == 1, "Failed to positively match ACL to addr: %s",
+    strerror(errno));
+
   acl_str = pstrdup(p, "127.0.0.");
   acl = pr_netacl_create(p, acl_str);
   fail_unless(acl != NULL, "Failed to handle ACL string '%s': %s", acl_str,
@@ -518,6 +733,15 @@ START_TEST (netacl_match_test) {
   fail_unless(res == -1, "Failed to negatively match ACL to addr: %s",
     strerror(errno));
 
+  acl_str = pstrdup(p, "!1.2.3.");
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  res = pr_netacl_match(acl, addr);
+  fail_unless(res == 1, "Failed to positively match ACL to addr: %s",
+    strerror(errno));
+
   if (!have_localdomain) {
     acl_str = pstrdup(p, "localhost");
 
@@ -530,7 +754,7 @@ START_TEST (netacl_match_test) {
     strerror(errno));
 
   res = pr_netacl_match(acl, addr);
-  if (getenv("TRAVIS_CI") == NULL) {
+  if (getenv("TRAVIS") == NULL) {
     fail_unless(res == 1, "Failed to positively match ACL to addr: %s",
       strerror(errno));
   }
@@ -547,11 +771,20 @@ START_TEST (netacl_match_test) {
     strerror(errno));
 
   res = pr_netacl_match(acl, addr);
-  if (getenv("TRAVIS_CI") == NULL) {
+  if (getenv("TRAVIS") == NULL) {
     fail_unless(res == -1, "Failed to negatively match ACL to addr: %s",
       strerror(errno));
   }
 
+  acl_str = "!www.google.com";
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  res = pr_netacl_match(acl, addr);
+  fail_unless(res == 1, "Failed to positively match ACL to addr: %s",
+    strerror(errno));
+
   if (!have_localdomain) {
     acl_str = pstrdup(p, "loc*st");
 
@@ -564,7 +797,7 @@ START_TEST (netacl_match_test) {
     strerror(errno));
 
   res = pr_netacl_match(acl, addr);
-  if (getenv("TRAVIS_CI") == NULL) {
+  if (getenv("TRAVIS") == NULL) {
     fail_unless(res == 1, "Failed to positively match ACL to addr: %s",
       strerror(errno));
   }
@@ -581,10 +814,27 @@ START_TEST (netacl_match_test) {
     strerror(errno));
 
   res = pr_netacl_match(acl, addr);
-  if (getenv("TRAVIS_CI") == NULL) {
+  if (getenv("TRAVIS") == NULL) {
     fail_unless(res == -1, "Failed to negatively match ACL to addr: %s",
       strerror(errno));
   }
+
+  acl_str = "!www.g*g.com";
+  acl = pr_netacl_create(p, acl_str);
+  fail_unless(acl != NULL, "Failed to handle ACL string '%s': %s", acl_str,
+    strerror(errno));
+
+  res = pr_netacl_match(acl, addr);
+  fail_unless(res == 1, "Failed to positively match ACL to addr: %s",
+    strerror(errno));
+
+  reverse_dns = ServerUseReverseDNS;
+  ServerUseReverseDNS = FALSE;
+
+  res = pr_netacl_match(acl, addr);
+  fail_unless(res == 0, "Matched DNS glob ACL to addr unexpectedly");
+
+  ServerUseReverseDNS = reverse_dns;
 }
 END_TEST
 
@@ -617,16 +867,15 @@ Suite *tests_get_netacl_suite(void) {
   suite = suite_create("netacl");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, netacl_create_test);
   tcase_add_test(testcase, netacl_get_str_test);
+  tcase_add_test(testcase, netacl_get_str2_test);
   tcase_add_test(testcase, netacl_dup_test);
   tcase_add_test(testcase, netacl_match_test);
   tcase_add_test(testcase, netacl_get_negated_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/netaddr.c b/tests/api/netaddr.c
index d3d5c78..80d3327 100644
--- a/tests/api/netaddr.c
+++ b/tests/api/netaddr.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2016 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* NetAddr API tests
- * $Id: netaddr.c,v 1.13 2014-01-27 18:31:35 castaglia Exp $
- */
+/* NetAddr API tests */
 
 #include "tests.h"
 
@@ -38,14 +36,21 @@ static void set_up(void) {
   }
 
   init_netaddr();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("dns", 1, 20);
+  }
 }
 
 static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("dns", 0, 0);
+  }
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
-  } 
+    p = permanent_pool = NULL;
+  }
 }
 
 /* Tests */
@@ -103,7 +108,7 @@ START_TEST (netaddr_clear_test) {
 END_TEST
 
 START_TEST (netaddr_get_addr_test) {
-  pr_netaddr_t *res;
+  const pr_netaddr_t *res;
   const char *name;
   array_header *addrs = NULL;
 
@@ -115,11 +120,19 @@ START_TEST (netaddr_get_addr_test) {
   fail_unless(res == NULL, "Failed to handle null name");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  name = "127.0.0.1";
+  name = "134.289.999.0";
 
   res = pr_netaddr_get_addr(NULL, name, NULL);
   fail_unless(res == NULL, "Failed to handle null pool");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(res == NULL, "Unexpected got address for '%s'", name);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  name = "localhost";
 
   res = pr_netaddr_get_addr(p, name, NULL);
   fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
@@ -127,7 +140,29 @@ START_TEST (netaddr_get_addr_test) {
   fail_unless(res->na_family == AF_INET, "Expected family %d, got %d",
     AF_INET, res->na_family);
 
-  name = "localhost";
+  res = pr_netaddr_get_addr(p, name, &addrs);
+  fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
+    strerror(errno));
+  fail_unless(res->na_family == AF_INET, "Expected family %d, got %d",
+    AF_INET, res->na_family);
+
+  /* Google: the Dial Tone of the Internet. */
+  name = "www.google.com";
+
+  res = pr_netaddr_get_addr(p, name, &addrs);
+  fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
+    strerror(errno));
+  fail_unless(res->na_family == AF_INET, "Expected family %d, got %d",
+    AF_INET, res->na_family);
+  fail_unless(addrs != NULL, "Expected additional addresses for '%s'", name);
+
+  res = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
+    strerror(errno));
+  fail_unless(res->na_family == AF_INET, "Expected family %d, got %d",
+    AF_INET, res->na_family);
+
+  name = "127.0.0.1";
 
   res = pr_netaddr_get_addr(p, name, NULL);
   fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
@@ -140,15 +175,47 @@ START_TEST (netaddr_get_addr_test) {
     strerror(errno));
   fail_unless(res->na_family == AF_INET, "Expected family %d, got %d",
     AF_INET, res->na_family);
+  fail_unless(addrs == NULL, "Expected no additional addresses for '%s'", name);
+
+  /* Deliberately test an unresolvable name (related to Bug#4104). */
+  name = "foo.bar.castaglia.example.com";
+
+  res = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(res == NULL, "Resolved '%s' unexpectedly", name);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+#if defined(PR_USE_IPV6)
+  name = "::1";
+
+  res = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
+    strerror(errno));
+  fail_unless(res->na_family == AF_INET6, "Expected family %d, got %d",
+    AF_INET6, res->na_family);
+
+  res = pr_netaddr_get_addr(p, name, &addrs);
+  fail_unless(res != NULL, "Failed to get addr for '%s': %s", name,
+    strerror(errno));
+  fail_unless(res->na_family == AF_INET6, "Expected family %d, got %d",
+    AF_INET6, res->na_family);
+  fail_unless(addrs == NULL, "Expected no additional addresses for '%s'", name);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_get_addr2_test) {
-  pr_netaddr_t *res;
+  const pr_netaddr_t *res;
   const char *name;
   int flags;
 
   flags = PR_NETADDR_GET_ADDR_FL_INCL_DEVICE;
+  name = "foobarbaz";
+  res = pr_netaddr_get_addr2(p, name, NULL, flags);
+  fail_unless(res == NULL, "Failed to handle unknown device '%s'", name);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
   name = "lo0";
   res = pr_netaddr_get_addr2(p, name, NULL, flags);
   if (res == NULL) {
@@ -170,7 +237,7 @@ START_TEST (netaddr_get_addr2_test) {
 END_TEST
 
 START_TEST (netaddr_get_family_test) {
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   int res;
 
   res = pr_netaddr_get_family(NULL);
@@ -195,7 +262,7 @@ START_TEST (netaddr_set_family_test) {
   fail_unless(res == -1, "Failed to handle null arguments");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, "127.0.0.1", NULL);
   fail_unless(addr != NULL, "Failed to get addr for '127.0.0.1': %s",
     strerror(errno));
 
@@ -213,38 +280,469 @@ START_TEST (netaddr_set_family_test) {
 END_TEST
 
 START_TEST (netaddr_cmp_test) {
+  const pr_netaddr_t *addr, *addr2;
+  int res;
+  const char *name;
+
+  res = pr_netaddr_cmp(NULL, NULL);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  name = "127.0.0.1";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_cmp(addr, NULL);
+  fail_unless(res == 1, "Expected 1, got %d", res);
+
+  res = pr_netaddr_cmp(NULL, addr);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  name = "::1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_cmp(addr, addr2);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  res = pr_netaddr_cmp(addr2, addr);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  name = "::ffff:127.0.0.1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_cmp(addr, addr2);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  res = pr_netaddr_cmp(addr2, addr);
+  fail_unless(res == 0, "Expected 0, got %d", res);
 }
 END_TEST
 
 START_TEST (netaddr_ncmp_test) {
+  const pr_netaddr_t *addr, *addr2;
+  int res;
+  unsigned int nbits = 0;
+  const char *name;
+
+  res = pr_netaddr_ncmp(NULL, NULL, nbits);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  name = "127.0.0.1";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_ncmp(addr, NULL, nbits);
+  fail_unless(res == 1, "Expected 1, got %d", res);
+
+  res = pr_netaddr_ncmp(NULL, addr, nbits);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  res = pr_netaddr_ncmp(NULL, addr, nbits);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  nbits = 48;
+  res = pr_netaddr_ncmp(addr, addr, nbits);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "::1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  nbits = 0;
+  res = pr_netaddr_ncmp(addr, addr2, nbits);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  res = pr_netaddr_ncmp(addr2, addr, nbits);
+  fail_unless(res == -1, "Expected -1, got %d", res);
+
+  name = "::ffff:127.0.0.1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_ncmp(addr, addr2, nbits);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  res = pr_netaddr_ncmp(addr2, addr, nbits);
+  fail_unless(res == 0, "Expected 0, got %d", res);
+
+  nbits = 24;
+  res = pr_netaddr_ncmp(addr2, addr, nbits);
+  fail_unless(res == 0, "Expected 0, got %d", res);
 }
 END_TEST
 
 START_TEST (netaddr_fnmatch_test) {
+  const pr_netaddr_t *addr;
+  int flags, res;
+  const char *name;
+
+  res = pr_netaddr_fnmatch(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "localhost";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_fnmatch(addr, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pattern");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  flags = PR_NETADDR_MATCH_DNS;
+  res = pr_netaddr_fnmatch(addr, "foo", flags);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  res = pr_netaddr_fnmatch(addr, "LOCAL*", flags);
+  if (getenv("TRAVIS") == NULL) {
+    /* This test is sensitive the environment. */
+    fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+  }
+
+  flags = PR_NETADDR_MATCH_IP;
+  res = pr_netaddr_fnmatch(addr, "foo", flags);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  res = pr_netaddr_fnmatch(addr, "127.0*", flags);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+#ifdef PR_USE_IPV6
+  name = "::ffff:127.0.0.1";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_fnmatch(addr, "foo", flags);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  res = pr_netaddr_fnmatch(addr, "127.0.*", flags);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_get_sockaddr_test) {
+  pr_netaddr_t *addr;
+  struct sockaddr *sockaddr;
+  const char *name;
+#ifdef PR_USE_IPV6
+  int family;
+#endif /* PR_USE_IPV6 */
+
+  sockaddr = pr_netaddr_get_sockaddr(NULL);
+  fail_unless(sockaddr == NULL, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  sockaddr = pr_netaddr_get_sockaddr(addr);
+  fail_unless(sockaddr != NULL, "Failed to get sock addr: %s", strerror(errno));
+
+#ifdef PR_USE_IPV6
+  name = "::1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  sockaddr = pr_netaddr_get_sockaddr(addr);
+  fail_unless(sockaddr != NULL, "Failed to get sock addr: %s", strerror(errno));
+
+  pr_netaddr_disable_ipv6();
+  sockaddr = pr_netaddr_get_sockaddr(addr);
+  fail_unless(sockaddr == NULL, "Got sock addr for IPv6 addr", strerror(errno));
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_netaddr_enable_ipv6();
+  family = addr->na_family;
+  addr->na_family = 777;
+  sockaddr = pr_netaddr_get_sockaddr(addr);
+  fail_unless(sockaddr == NULL, "Got sock addr for IPv6 addr", strerror(errno));
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+  addr->na_family = family;
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_get_sockaddr_len_test) {
+  pr_netaddr_t *addr;
+  size_t res;
+  const char *name;
+#ifdef PR_USE_IPV6
+  int family;
+#endif /* PR_USE_IPV6 */
+
+  res = pr_netaddr_get_sockaddr_len(NULL);
+  fail_unless(res == (size_t) -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_get_sockaddr_len(addr);
+  fail_unless(res > 0, "Failed to get sockaddr len: %s", strerror(errno));
+
+#ifdef PR_USE_IPV6
+  name = "::1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_get_sockaddr_len(addr);
+  fail_unless(res > 0, "Failed to get sockaddr len: %s", strerror(errno));
+
+  pr_netaddr_disable_ipv6();
+  res = pr_netaddr_get_sockaddr_len(addr);
+  fail_unless(res == (size_t) -1, "Got sockaddr len unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_netaddr_enable_ipv6();
+
+  family = addr->na_family;
+  addr->na_family = 777;
+  res = pr_netaddr_get_sockaddr_len(addr);
+  addr->na_family = family;
+
+  fail_unless(res == (size_t) -1, "Got sockaddr len unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_set_sockaddr_test) {
+  pr_netaddr_t *addr;
+  int res;
+  struct sockaddr sa;
+  const char *name;
+#ifdef PR_USE_IPV6
+  int family;
+# if defined(HAVE_GETADDRINFO)
+  struct addrinfo hints, *info = NULL;
+# endif /* HAVE_GETADDRINFO */
+#endif /* PR_USE_IPV6 */
+
+  res = pr_netaddr_set_sockaddr(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_set_sockaddr(addr, NULL);
+  fail_unless(res < 0, "Failed to handle null sockaddr");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  memset(&sa, 0, sizeof(sa));
+
+  res = pr_netaddr_set_sockaddr(addr, &sa);
+  fail_unless(res == 0, "Failed to set sockaddr: %s", strerror(errno));
+
+#ifdef PR_USE_IPV6
+  name = "::1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+# if defined(HAVE_GETADDRINFO)
+  memset(&hints, 0, sizeof(hints));
+  hints.ai_family = AF_INET6;
+  hints.ai_socktype = SOCK_STREAM;
+  hints.ai_flags = AI_NUMERICHOST;
+#  if defined(AI_V4MAPPED)
+  hints.ai_flags |= AI_V4MAPPED;
+#  endif /* AI_V4MAPPED */
+  res = getaddrinfo("::1", NULL, &hints, &info);
+  fail_unless(res == 0, "getaddrinfo('::1') failed: %s", gai_strerror(res));
+
+  res = pr_netaddr_set_sockaddr(addr, info->ai_addr);
+  fail_unless(res == 0, "Failed to set sockaddr: %s", strerror(errno));
+
+  pr_netaddr_disable_ipv6();
+  res = pr_netaddr_set_sockaddr(addr, info->ai_addr);
+  fail_unless(res < 0, "Set sockaddr unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  freeaddrinfo(info);
+  pr_netaddr_enable_ipv6();
+# endif /* HAVE_GETADDRINFO */
+
+  family = addr->na_family;
+  addr->na_family = 777;
+  res = pr_netaddr_set_sockaddr(addr, &sa);
+  addr->na_family = family;
+
+  fail_unless(res < 0, "Set sockaddr unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_set_sockaddr_any_test) {
+  pr_netaddr_t *addr;
+  int res;
+  const char *name;
+#ifdef PR_USE_IPV6
+  int family;
+#endif /* PR_USE_IPV6 */
+
+  res = pr_netaddr_set_sockaddr_any(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_set_sockaddr_any(addr);
+  fail_unless(res == 0, "Failed to set sockaddr any: %s", strerror(errno));
+
+#ifdef PR_USE_IPV6
+  name = "::1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_set_sockaddr_any(addr);
+  fail_unless(res == 0, "Failed to set sockaddr any: %s", strerror(errno));
+
+  pr_netaddr_disable_ipv6();
+  res = pr_netaddr_set_sockaddr_any(addr);
+  fail_unless(res < 0, "Set sockaddr any unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_netaddr_enable_ipv6();
+
+  family = addr->na_family;
+  addr->na_family = 777;
+  res = pr_netaddr_set_sockaddr_any(addr);
+  addr->na_family = family;
+
+  fail_unless(res < 0, "Set sockaddr any unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_get_inaddr_test) {
+  pr_netaddr_t *addr;
+  int family;
+  void *inaddr;
+  const char *name;
+
+  inaddr = pr_netaddr_get_inaddr(NULL);
+  fail_unless(inaddr == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  family = AF_INET;
+  name = "127.0.0.1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  inaddr = pr_netaddr_get_inaddr(addr);
+  fail_unless(inaddr != NULL, "Failed to get inaddr: %s", strerror(errno));
+
+#ifdef PR_USE_IPV6
+  family = AF_INET6;
+  name = "::1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  inaddr = pr_netaddr_get_inaddr(addr);
+  fail_unless(inaddr != NULL, "Failed to get inaddr: %s", strerror(errno));
+
+  pr_netaddr_disable_ipv6();
+  inaddr = pr_netaddr_get_inaddr(addr);
+  fail_unless(inaddr == NULL, "Got inaddr unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_netaddr_enable_ipv6();
+
+  family = addr->na_family;
+  addr->na_family = 777;
+  inaddr = pr_netaddr_get_inaddr(addr);
+  addr->na_family = family;
+
+  fail_unless(inaddr == NULL, "Got inaddr unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
 START_TEST (netaddr_get_inaddr_len_test) {
+  pr_netaddr_t *addr;
+  size_t res;
+  const char *name;
+#ifdef PR_USE_IPV6
+  int family;
+#endif /* PR_USE_IPV6 */
+
+  res = pr_netaddr_get_inaddr_len(NULL);
+  fail_unless(res == (size_t) -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_get_inaddr_len(addr);
+  fail_unless(res > 0, "Failed to get inaddr len: %s", strerror(errno));
+
+#ifdef PR_USE_IPV6
+  name = "::1";
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_get_inaddr_len(addr);
+  fail_unless(res > 0, "Failed to get inaddr len: %s", strerror(errno));
+
+  family = addr->na_family;
+  addr->na_family = 777;
+  res = pr_netaddr_get_inaddr_len(addr);
+  addr->na_family = family;
+
+  fail_unless(res == (size_t) -1, "Got inaddr len unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+#endif /* PR_USE_IPV6 */
 }
 END_TEST
 
@@ -256,7 +754,7 @@ START_TEST (netaddr_get_port_test) {
   fail_unless(res == 0, "Failed to handle null addr");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, "127.0.0.1", NULL);
   fail_unless(addr != NULL, "Failed to get addr for '127.0.0.1': %s",
     strerror(errno));
 
@@ -279,7 +777,7 @@ START_TEST (netaddr_set_port_test) {
   fail_unless(res == -1, "Failed to handle null addr");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  addr = (pr_netaddr_t *) pr_netaddr_get_addr(p, "127.0.0.1", NULL);
   fail_unless(addr != NULL, "Failed to get addr for '127.0.0.1': %s",
     strerror(errno));
 
@@ -309,7 +807,7 @@ START_TEST (netaddr_set_reverse_dns_test) {
 END_TEST
 
 START_TEST (netaddr_get_dnsstr_test) {
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   const char *ip, *res;
 
   ip = "127.0.0.1";
@@ -339,7 +837,7 @@ START_TEST (netaddr_get_dnsstr_test) {
     strerror(errno));
   fail_unless(strcmp(res, ip) == 0, "Expected '%s', got '%s'", ip, res);
 
-  pr_netaddr_clear(addr);
+  pr_netaddr_clear((pr_netaddr_t *) addr);
 
   /* Clearing the address doesn't work, since that removes even the address
    * info, in addition to the cached strings.
@@ -350,7 +848,7 @@ START_TEST (netaddr_get_dnsstr_test) {
   fail_unless(strcmp(res, "") == 0, "Expected '%s', got '%s'", "", res);
 
   /* We need to clear the netaddr internal cache as well. */
-  pr_netaddr_clear_cache();
+  pr_netaddr_clear_ipcache(ip);
   addr = pr_netaddr_get_addr(p, ip, NULL);
   fail_unless(addr != NULL, "Failed to get addr for '%s': %s", ip,
     strerror(errno));
@@ -369,7 +867,8 @@ START_TEST (netaddr_get_dnsstr_test) {
    * return either "localhost" or "localhost.localdomain".  Perhaps even
    * other variations, although these should be the most common.
    */
-  if (getenv("TRAVIS_CI") == NULL) {
+  if (getenv("TRAVIS") == NULL) {
+    /* This test is sensitive the environment. */
     fail_unless(strcmp(res, "localhost") == 0 ||
                 strcmp(res, "localhost.localdomain") == 0,
       "Expected '%s', got '%s'", "localhost or localhost.localdomain", res);
@@ -377,9 +876,59 @@ START_TEST (netaddr_get_dnsstr_test) {
 }
 END_TEST
 
+START_TEST (netaddr_get_dnsstr_list_test) {
+  array_header *res, *addrs = NULL;
+  const pr_netaddr_t *addr;
+  int reverse_dns;
+  const char *dnsstr;
+
+  res = pr_netaddr_get_dnsstr_list(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_netaddr_get_dnsstr_list(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  addr = pr_netaddr_get_addr(p, "localhost", NULL);
+  fail_unless(addr != NULL, "Failed to resolve 'localhost': %s",
+    strerror(errno));
+
+  res = pr_netaddr_get_dnsstr_list(p, addr);
+  fail_unless(res != NULL, "Failed to get DNS list: %s", strerror(errno));
+
+  reverse_dns = pr_netaddr_set_reverse_dns(TRUE);
+
+  pr_netaddr_clear_cache();
+
+  addr = pr_netaddr_get_addr(p, "www.google.com", &addrs);
+  fail_unless(addr != NULL, "Failed to resolve 'www.google.com': %s",
+    strerror(errno));
+
+  dnsstr = pr_netaddr_get_dnsstr(addr);
+  fail_unless(dnsstr != NULL, "Failed to get DNS string for '%s': %s",
+    pr_netaddr_get_ipstr(addr), strerror(errno));
+
+  /* We may get a DNS name, but there is no guarantee that the reverse
+   * DNS lookup will return the original "www.google.com" we requested.
+   */
+
+  res = pr_netaddr_get_dnsstr_list(p, addr);
+  fail_unless(res != NULL, "Failed to get DNS list: %s", strerror(errno));
+
+  /* Ideally we would check that res->nelts > 0, BUT this turns out to
+   * a fragile test condition, dependent on DNS vagaries.
+   */
+
+  pr_netaddr_set_reverse_dns(reverse_dns);
+}
+END_TEST
+
 #ifdef PR_USE_IPV6
 START_TEST (netaddr_get_dnsstr_ipv6_test) {
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   const char *ip, *res;
 
   ip = "::1";
@@ -409,7 +958,7 @@ START_TEST (netaddr_get_dnsstr_ipv6_test) {
     strerror(errno));
   fail_unless(strcmp(res, ip) == 0, "Expected '%s', got '%s'", ip, res);
 
-  pr_netaddr_clear(addr);
+  pr_netaddr_clear((pr_netaddr_t *) addr);
 
   /* Clearing the address doesn't work, since that removes even the address
    * info, in addition to the cached strings.
@@ -420,7 +969,7 @@ START_TEST (netaddr_get_dnsstr_ipv6_test) {
   fail_unless(strcmp(res, "") == 0, "Expected '%s', got '%s'", "", res);
 
   /* We need to clear the netaddr internal cache as well. */
-  pr_netaddr_clear_cache();
+  pr_netaddr_clear_ipcache(ip);
   addr = pr_netaddr_get_addr(p, ip, NULL);
   fail_unless(addr != NULL, "Failed to get addr for '%s': %s", ip,
     strerror(errno));
@@ -439,20 +988,22 @@ START_TEST (netaddr_get_dnsstr_ipv6_test) {
    * return either "localhost" or "localhost.localdomain".  Perhaps even
    * other variations, although these should be the most common.
    */
-  fail_unless(strcmp(res, "localhost") == 0 ||
-              strcmp(res, "localhost.localdomain") == 0 ||
-              strcmp(res, "localhost6") == 0 ||
-              strcmp(res, "localhost6.localdomain") == 0 ||
-              strcmp(res, "ip6-localhost") == 0 ||
-              strcmp(res, "ip6-loopback") == 0 ||
-              strcmp(res, ip) == 0,
-    "Expected '%s', got '%s'", "localhost, localhost.localdomain et al", res);
+  if (getenv("TRAVIS") == NULL) {
+    fail_unless(strcmp(res, "localhost") == 0 ||
+                strcmp(res, "localhost.localdomain") == 0 ||
+                strcmp(res, "localhost6") == 0 ||
+                strcmp(res, "localhost6.localdomain") == 0 ||
+                strcmp(res, "ip6-localhost") == 0 ||
+                strcmp(res, "ip6-loopback") == 0 ||
+                strcmp(res, ip) == 0,
+      "Expected '%s', got '%s'", "localhost, localhost.localdomain et al", res);
+  }
 }
 END_TEST
 #endif /* PR_USE_IPV6 */
 
 START_TEST (netaddr_get_ipstr_test) {
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
   const char *res;
 
   res = pr_netaddr_get_ipstr(NULL);
@@ -470,10 +1021,9 @@ START_TEST (netaddr_get_ipstr_test) {
     "127.0.0.1", res);
   fail_unless(addr->na_have_ipstr == 1, "addr should have cached IP str");
 
-  pr_netaddr_clear(addr);
+  pr_netaddr_clear((pr_netaddr_t *) addr);
   res = pr_netaddr_get_ipstr(addr);
   fail_unless(res == NULL, "Expected null, got '%s'", res);
-
 }
 END_TEST
 
@@ -522,6 +1072,52 @@ START_TEST (netaddr_get_localaddr_str_test) {
 }
 END_TEST
 
+START_TEST (netaddr_is_loopback_test) {
+  const pr_netaddr_t *addr;
+  int res;
+  const char *name;
+
+  res = pr_netaddr_is_loopback(NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "www.google.com";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_is_loopback(addr);
+  fail_unless(res == FALSE, "Expected FALSE, got %d", res);
+
+  name = "127.0.0.1";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_is_loopback(addr);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+#ifdef PR_USE_IPV6
+  name = "::1";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_is_loopback(addr);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+
+  name = "::ffff:127.0.0.1";
+  addr = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  res = pr_netaddr_is_loopback(addr);
+  fail_unless(res == TRUE, "Expected TRUE, got %d", res);
+#endif /* PR_USE_IPV6 */
+}
+END_TEST
+
 START_TEST (netaddr_is_v4_test) {
   int res;
   const char *name;
@@ -579,7 +1175,7 @@ END_TEST
 START_TEST (netaddr_is_v4mappedv6_test) {
   int res;
   const char *name;
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
 
   res = pr_netaddr_is_v4mappedv6(NULL);
   fail_unless(res == -1, "Failed to handle null arguments");
@@ -630,7 +1226,7 @@ END_TEST
 START_TEST (netaddr_is_rfc1918_test) {
   int res;
   const char *name;
-  pr_netaddr_t *addr;
+  const pr_netaddr_t *addr;
 
   res = pr_netaddr_is_rfc1918(NULL);
   fail_unless(res == -1, "Failed to handle null arguments");
@@ -681,6 +1277,98 @@ START_TEST (netaddr_is_rfc1918_test) {
 }
 END_TEST
 
+START_TEST (netaddr_v6tov4_test) {
+  const pr_netaddr_t *addr, *addr2;
+  const char *name, *ipstr;
+
+  addr = pr_netaddr_v6tov4(NULL, NULL);
+  fail_unless(addr == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  addr = pr_netaddr_v6tov4(p, NULL);
+  fail_unless(addr == NULL, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  addr = pr_netaddr_v6tov4(p, addr2);
+  fail_unless(addr == NULL, "Converted '%s' to IPv4 address unexpectedly",
+    name);
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  name = "::ffff:127.0.0.1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  addr = pr_netaddr_v6tov4(p, addr2);
+  fail_unless(addr != NULL, "Failed to convert '%s' to IPv4 addres: %s",
+    name, strerror(errno));
+  fail_unless(pr_netaddr_get_family(addr) == AF_INET,
+    "Expected %d, got %d", AF_INET, pr_netaddr_get_family(addr));
+
+  ipstr = pr_netaddr_get_ipstr(addr);
+  fail_unless(strcmp(ipstr, "127.0.0.1") == 0,
+    "Expected '127.0.0.1', got '%s'", ipstr);
+}
+END_TEST
+
+START_TEST (netaddr_v4tov6_test) {
+  const pr_netaddr_t *addr, *addr2;
+  const char *name, *ipstr;
+
+  addr = pr_netaddr_v4tov6(NULL, NULL);
+  fail_unless(addr == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  addr = pr_netaddr_v4tov6(p, NULL);
+  fail_unless(addr == NULL, "Failed to handle null address");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  name = "::ffff:127.0.0.1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  addr = pr_netaddr_v4tov6(p, addr2);
+  fail_unless(addr == NULL, "Converted '%s' to IPv6 address unexpectedly",
+    name);
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  name = "127.0.0.1";
+  addr2 = pr_netaddr_get_addr(p, name, NULL);
+  fail_unless(addr2 != NULL, "Failed to resolve '%s': %s", name,
+    strerror(errno));
+
+  addr = pr_netaddr_v4tov6(p, addr2);
+#ifdef PR_USE_IPV6
+  fail_unless(addr != NULL, "Failed to convert '%s' to IPv6 addres: %s",
+    name, strerror(errno));
+  fail_unless(pr_netaddr_get_family(addr) == AF_INET6,
+    "Expected %d, got %d", AF_INET6, pr_netaddr_get_family(addr));
+
+  ipstr = pr_netaddr_get_ipstr(addr);
+  fail_unless(strcmp(ipstr, "::ffff:127.0.0.1") == 0,
+    "Expected '::ffff:127.0.0.1', got '%s'", ipstr);
+
+#else
+  fail_unless(addr == NULL, "Converted '%s' to IPv6 address unexpectedly",
+    name);
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+#endif /* PR_USE_IPV6 */
+}
+END_TEST
+
 START_TEST (netaddr_disable_ipv6_test) {
   unsigned char use_ipv6;
 
@@ -720,7 +1408,6 @@ Suite *tests_get_netaddr_suite(void) {
   suite = suite_create("netaddr");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, netaddr_alloc_test);
@@ -743,20 +1430,23 @@ Suite *tests_get_netaddr_suite(void) {
   tcase_add_test(testcase, netaddr_set_port_test);
   tcase_add_test(testcase, netaddr_set_reverse_dns_test);
   tcase_add_test(testcase, netaddr_get_dnsstr_test);
+  tcase_add_test(testcase, netaddr_get_dnsstr_list_test);
 #ifdef PR_USE_IPV6
   tcase_add_test(testcase, netaddr_get_dnsstr_ipv6_test);
 #endif /* PR_USE_IPV6 */
   tcase_add_test(testcase, netaddr_get_ipstr_test);
   tcase_add_test(testcase, netaddr_validate_dns_str_test);
   tcase_add_test(testcase, netaddr_get_localaddr_str_test);
+  tcase_add_test(testcase, netaddr_is_loopback_test);
   tcase_add_test(testcase, netaddr_is_v4_test);
   tcase_add_test(testcase, netaddr_is_v6_test);
   tcase_add_test(testcase, netaddr_is_v4mappedv6_test);
   tcase_add_test(testcase, netaddr_is_rfc1918_test);
+  tcase_add_test(testcase, netaddr_v6tov4_test);
+  tcase_add_test(testcase, netaddr_v4tov6_test);
   tcase_add_test(testcase, netaddr_disable_ipv6_test);
   tcase_add_test(testcase, netaddr_enable_ipv6_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/netio.c b/tests/api/netio.c
index c5069da..22fae4a 100644
--- a/tests/api/netio.c
+++ b/tests/api/netio.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2014 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* NetIO API tests
- * $Id: netio.c,v 1.1 2014-01-06 06:58:23 castaglia Exp $
- */
+/* NetIO API tests. */
 
 #include "tests.h"
 
@@ -40,6 +38,7 @@
 #define TELNET_DM      242
 
 static pool *p = NULL;
+static int xfer_bufsz = -1;
 
 static int tmp_fd = -1;
 static const char *tmp_path = NULL;
@@ -52,6 +51,8 @@ static void test_cleanup(void) {
     (void) unlink(tmp_path);
     tmp_path = NULL;
   }
+
+  pr_unregister_netio(PR_NETIO_STRM_CTRL|PR_NETIO_STRM_DATA|PR_NETIO_STRM_OTHR);
 }
 
 static int open_tmpfile(void) {
@@ -75,22 +76,31 @@ static void set_up(void) {
   }
 
   init_netio();
+  xfer_bufsz = pr_config_get_server_xfer_bufsz(PR_NETIO_IO_RD);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("netio", 1, 20);
+  }
 }
 
 static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("netio", 0, 0);
+  }
+
+  test_cleanup();
+
   if (p) {
     destroy_pool(p);
     p = permanent_pool = NULL;
-  } 
-
-  test_cleanup();
+  }
 }
 
+/* Tests */
+
 START_TEST (netio_open_test) {
   pr_netio_stream_t *nstrm;
-  int fd;
-
-  fd = -1;
+  int fd = -1;
 
   nstrm = pr_netio_open(NULL, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
   fail_unless(nstrm == NULL, "Failed to handle null pool argument");
@@ -102,25 +112,77 @@ START_TEST (netio_open_test) {
   fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
     strerror(errno), errno);
 
+  /* open/close CTRL stream */
   nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
-  fail_unless(nstrm != NULL, "Failed to open stream on fd %d: %s", fd,
+  fail_unless(nstrm != NULL, "Failed to open ctrl stream on fd %d: %s", fd,
+    strerror(errno));
+
+  pr_netio_close(nstrm);
+
+  /* open/close DATA stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_WR);
+  fail_unless(nstrm != NULL, "Failed to open data stream on fd %d: %s", fd,
+    strerror(errno));
+
+  pr_netio_close(nstrm);
+
+  /* open/close OTHR stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_WR);
+  fail_unless(nstrm != NULL, "Failed to open othr stream on fd %d: %s", fd,
     strerror(errno));
 
   pr_netio_close(nstrm);
 }
 END_TEST
 
-START_TEST (netio_close_test) {
+START_TEST (netio_postopen_test) {
   pr_netio_stream_t *nstrm;
-  int res, fd;
+  int fd = -1, res;
+
+  res = pr_netio_postopen(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* open/postopen/close CTRL stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open stream on fd %d: %s", fd,
+    strerror(errno));
 
-  fd = -1;
+  res = pr_netio_postopen(nstrm);
+  fail_unless(res == 0, "Failed to post-open ctrl stream: %s", strerror(errno));
+  (void) pr_netio_close(nstrm);
+
+  /* open/postopen/close DATA stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_postopen(nstrm);
+  fail_unless(res == 0, "Failed to post-open data stream: %s", strerror(errno));
+  (void) pr_netio_close(nstrm);
+
+  /* open/postopen/close OTHR stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_postopen(nstrm);
+  fail_unless(res == 0, "Failed to post-open othr stream: %s", strerror(errno));
+  (void) pr_netio_close(nstrm);
+}
+END_TEST
+
+START_TEST (netio_close_test) {
+  pr_netio_stream_t *nstrm;
+  int res, fd = -1;
 
   res = pr_netio_close(NULL);
   fail_unless(res == -1, "Failed to handle null stream argument");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
     strerror(errno), errno);
 
+  /* Open/close CTRL stream */
   nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
   nstrm->strm_type = 7777;
 
@@ -134,6 +196,102 @@ START_TEST (netio_close_test) {
   fail_unless(res == -1, "Failed to handle bad file descriptor");
   fail_unless(errno == EBADF, "Failed to set errno to EBADF, got %s (%d)",
     strerror(errno), errno);
+
+  /* Open/close DATA stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_RD);
+  res = pr_netio_close(nstrm);
+  fail_unless(res == -1, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Failed to set errno to EBADF, got %s (%d)",
+    strerror(errno), errno);
+
+  /* Open/close OTHR stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_RD);
+  res = pr_netio_close(nstrm);
+  fail_unless(res == -1, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Failed to set errno to EBADF, got %s (%d)",
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (netio_lingering_close_test) {
+  pr_netio_stream_t *nstrm;
+  int res, fd = -1;
+  long linger = 0L;
+
+  res = pr_netio_lingering_close(NULL, linger);
+  fail_unless(res == -1, "Failed to handle null stream argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  /* Open/close CTRL stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  nstrm->strm_type = 7777;
+
+  res = pr_netio_lingering_close(nstrm, linger);
+  fail_unless(res < 0, "Failed to handle unknown stream type argument");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+    strerror(errno), errno);
+
+  nstrm->strm_type = PR_NETIO_STRM_CTRL;
+  res = pr_netio_lingering_close(nstrm, linger);
+  fail_unless(res == 0, "Failed to close stream: %s", strerror(errno));
+
+  /* Open/close DATA stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_RD);
+  res = pr_netio_lingering_close(nstrm, linger);
+  fail_unless(res == 0, "Failed to close stream: %s", strerror(errno));
+
+  /* Open/close OTHR stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_RD);
+  res = pr_netio_lingering_close(nstrm, linger);
+  fail_unless(res == 0, "Failed to close stream: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (netio_reopen_test) {
+  pr_netio_stream_t *nstrm, *nstrm2;
+  int res, fd = -1;
+
+  nstrm2 = pr_netio_reopen(NULL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm2 == NULL, "Failed to handle null stream argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  /* Open/reopen/close CTRL stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  nstrm->strm_type = 7777;
+
+  nstrm2 = pr_netio_reopen(nstrm, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm2 == NULL, "Failed to handle unknown stream type argument");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+    strerror(errno), errno);
+
+  nstrm->strm_type = PR_NETIO_STRM_CTRL;
+  nstrm2 = pr_netio_reopen(nstrm, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm2 != NULL, "Failed to reopen ctrl stream: %s",
+    strerror(errno));
+
+  /* Open/reopen/close DATA stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_RD);
+  nstrm2 = pr_netio_reopen(nstrm, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm2 != NULL, "Failed to reopen data stream: %s",
+    strerror(errno));
+
+  res = pr_netio_close(nstrm);
+  fail_unless(res == -1, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Failed to set errno to EBADF, got %s (%d)",
+    strerror(errno), errno);
+
+  /* Open/reopen/close OTHR stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_RD);
+  nstrm2 = pr_netio_reopen(nstrm, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm2 != NULL, "Failed to reopen othr stream: %s",
+    strerror(errno));
+
+  res = pr_netio_close(nstrm);
+  fail_unless(res == -1, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Failed to set errno to EBADF, got %s (%d)",
+    strerror(errno), errno);
 }
 END_TEST
 
@@ -218,11 +376,14 @@ START_TEST (netio_telnet_gets_single_line_test) {
     xerrno, strerror(xerrno));
   fail_unless(strcmp(buf, cmd) == 0, "Expected string '%s', got '%s'", cmd,
     buf);
+  fail_unless(pbuf->remaining == (size_t) xfer_bufsz,
+    "Expected %d remaining bytes, got %lu", xfer_bufsz,
+    (unsigned long) pbuf->remaining);
 }
 END_TEST
 
 START_TEST (netio_telnet_gets_multi_line_test) {
-  char buf[256], *cmd, *first_cmd, *res;
+  char buf[256], *cmd, *first_cmd, *second_cmd, *res;
   pr_netio_stream_t *in, *out;
   pr_buffer_t *pbuf;
   int len, xerrno;
@@ -230,8 +391,10 @@ START_TEST (netio_telnet_gets_multi_line_test) {
   in = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_RD);
   out = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_WR);
 
-  cmd = "Hello, World!\nHow are you?\n";
+  /* Note: the line terminator in Telnet is CRLF, not just a bare LF. */
+  cmd = "Hello, World!\r\nHow are you?\r\n";
   first_cmd = "Hello, World!\n";
+  second_cmd = "How are you?\n";
 
   pr_netio_buffer_alloc(in);
   pbuf = in->strm_buf;
@@ -244,13 +407,26 @@ START_TEST (netio_telnet_gets_multi_line_test) {
   res = pr_netio_telnet_gets(buf, sizeof(buf)-1, in, out);
   xerrno = errno;
 
-  pr_netio_close(in);
-  pr_netio_close(out);
-
   fail_unless(res != NULL, "Failed to get string from stream: (%d) %s",
     xerrno, strerror(xerrno));
   fail_unless(strcmp(buf, first_cmd) == 0, "Expected string '%s', got '%s'",
     first_cmd, buf);
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_netio_telnet_gets(buf, sizeof(buf)-1, in, out);
+  xerrno = errno;
+
+  fail_unless(res != NULL, "Failed to get string from stream: (%d) %s",
+    xerrno, strerror(xerrno));
+  fail_unless(strcmp(buf, second_cmd) == 0, "Expected string '%s', got '%s'",
+    second_cmd, buf);
+
+  pr_netio_close(in);
+  pr_netio_close(out);
+
+  fail_unless(pbuf->remaining == (size_t) xfer_bufsz,
+    "Expected %d remaining bytes, got %lu", xfer_bufsz,
+    (unsigned long) pbuf->remaining);
 }
 END_TEST
 
@@ -907,7 +1083,7 @@ START_TEST (netio_telnet_gets_telnet_single_iac_test) {
 END_TEST
 
 START_TEST (netio_telnet_gets_bug3521_test) {
-  char buf[10], *cmd, *res, telnet_opt;
+  char buf[10], *res, telnet_opt;
   pr_netio_stream_t *in, *out;
   pr_buffer_t *pbuf;
   int len, xerrno;
@@ -915,8 +1091,6 @@ START_TEST (netio_telnet_gets_bug3521_test) {
   in = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_RD);
   out = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_WR);
 
-  cmd = "Hello, World!\n";
-
   pr_netio_buffer_alloc(in);
   pbuf = in->strm_buf;
 
@@ -1016,6 +1190,835 @@ START_TEST (netio_telnet_gets_eof_test) {
 }
 END_TEST
 
+START_TEST (netio_telnet_gets2_single_line_test) {
+  int res;
+  char buf[256], *cmd;
+  size_t cmd_len;
+  pr_netio_stream_t *in, *out;
+  pr_buffer_t *pbuf;
+  int len, xerrno;
+
+  in = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_RD);
+  out = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_WR);
+
+  cmd = "Hello, World!\n";
+  cmd_len = strlen(cmd);
+
+  pr_netio_buffer_alloc(in);
+  pbuf = in->strm_buf;
+  len = snprintf(pbuf->buf, pbuf->buflen-1, "%s", cmd);
+  pbuf->remaining = pbuf->buflen - len;
+  pbuf->current = pbuf->buf;
+
+  buf[sizeof(buf)-1] = '\0';
+
+  res = pr_netio_telnet_gets2(buf, sizeof(buf)-1, in, out);
+  xerrno = errno;
+
+  pr_netio_close(in);
+  pr_netio_close(out);
+
+  fail_unless(res > 0, "Failed to get string from stream: (%d) %s",
+    xerrno, strerror(xerrno));
+  fail_unless(strcmp(buf, cmd) == 0, "Expected string '%s', got '%s'", cmd,
+    buf);
+
+  fail_unless((size_t) res == cmd_len, "Expected length %lu, got %d",
+    (unsigned long) cmd_len, res);
+  fail_unless(pbuf->remaining == (size_t) xfer_bufsz,
+    "Expected %d remaining bytes, got %lu", xfer_bufsz,
+    (unsigned long) pbuf->remaining);
+}
+END_TEST
+
+START_TEST (netio_telnet_gets2_single_line_crnul_test) {
+  int res;
+  char buf[256], *cmd;
+  size_t cmd_len;
+  pr_netio_stream_t *in, *out;
+  pr_buffer_t *pbuf;
+  int xerrno;
+
+  in = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_RD);
+  out = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_WR);
+
+  /* See Bug#4167.  We cannot use strlen(3) due to the embedded NUL. */
+  cmd = "Hello, \015\000World!\n";
+  cmd_len = 14;
+
+  pr_netio_buffer_alloc(in);
+  pbuf = in->strm_buf;
+  memcpy(pbuf->buf, cmd, cmd_len);
+  pbuf->remaining = pbuf->buflen - cmd_len;
+  pbuf->current = pbuf->buf;
+
+  buf[sizeof(buf)-1] = '\0';
+
+  res = pr_netio_telnet_gets2(buf, sizeof(buf)-1, in, out);
+  xerrno = errno;
+
+  pr_netio_close(in);
+  pr_netio_close(out);
+
+  fail_unless(res > 0, "Failed to get string from stream: (%d) %s",
+    xerrno, strerror(xerrno));
+  fail_unless(strcmp(buf, cmd) == 0, "Expected string '%s', got '%s'", cmd,
+    buf);
+
+  fail_unless((size_t) res == cmd_len, "Expected length %lu, got %d",
+    (unsigned long) cmd_len, res);
+  fail_unless(pbuf->remaining == (size_t) xfer_bufsz,
+    "Expected %d remaining bytes, got %lu", xfer_bufsz,
+    (unsigned long) pbuf->remaining);
+}
+END_TEST
+
+START_TEST (netio_telnet_gets2_single_line_lf_test) {
+  int res;
+  char buf[256], *cmd;
+  size_t cmd_len;
+  pr_netio_stream_t *in, *out;
+  pr_buffer_t *pbuf;
+  int xerrno;
+
+  in = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_RD);
+  out = pr_netio_open(p, PR_NETIO_STRM_CTRL, -1, PR_NETIO_IO_WR);
+
+  cmd = "Hello, \012World!\n";
+  cmd_len = strlen(cmd);
+
+  pr_netio_buffer_alloc(in);
+  pbuf = in->strm_buf;
+  memcpy(pbuf->buf, cmd, cmd_len);
+  pbuf->remaining = pbuf->buflen - cmd_len;
+  pbuf->current = pbuf->buf;
+
+  buf[sizeof(buf)-1] = '\0';
+
+  res = pr_netio_telnet_gets2(buf, sizeof(buf)-1, in, out);
+  xerrno = errno;
+
+  pr_netio_close(in);
+  pr_netio_close(out);
+
+  fail_unless(res > 0, "Failed to get string from stream: (%d) %s",
+    xerrno, strerror(xerrno));
+  fail_unless(strcmp(buf, cmd) == 0, "Expected string '%s', got '%s'", cmd,
+    buf);
+
+  fail_unless((size_t) res == cmd_len, "Expected length %lu, got %d",
+    (unsigned long) cmd_len, res);
+  fail_unless(pbuf->remaining == (size_t) xfer_bufsz,
+    "Expected %d remaining bytes, got %lu", xfer_bufsz,
+    (unsigned long) pbuf->remaining);
+}
+END_TEST
+
+static int netio_poll_cb(pr_netio_stream_t *nstrm) {
+  /* Always return >0, to indicate that we haven't timed out, AND that there
+   * is a writable fd available.
+   */
+  return 7;
+}
+
+static int netio_read_eof = FALSE;
+static int netio_read_epipe = FALSE;
+
+static int netio_read_cb(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
+  const char *text;
+  int res;
+
+  if (netio_read_eof) {
+    netio_read_eof = FALSE;
+    return 0;
+  }
+
+  if (netio_read_epipe) {
+    netio_read_epipe = FALSE;
+    errno = EPIPE;
+    return -1;
+  }
+
+  text = "Hello, World!\r\n";
+  sstrncpy(buf, text, buflen);
+
+  /* Make sure the next read returns EOF. */
+  netio_read_eof = TRUE;
+
+  res = strlen(text);
+  return res;
+}
+
+static int netio_write_epipe = FALSE;
+
+static int netio_write_cb(pr_netio_stream_t *nstrm, char *buf, size_t buflen) {
+  if (netio_write_epipe) {
+    netio_write_epipe = FALSE;
+    errno = EPIPE;
+    return -1;
+  }
+
+  return buflen;
+}
+
+static int netio_read_from_stream(int strm_type) {
+  int fd = 2, res;
+  char buf[1024], *expected_text;
+  size_t expected_sz;
+  pr_netio_stream_t *nstrm;
+
+  res = pr_netio_read(NULL, NULL, 0, 0);
+  if (res == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  nstrm = pr_netio_open(p, strm_type, fd, PR_NETIO_IO_RD);
+  if (nstrm == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg("netio", 1, "error opening custom netio stream: %s",
+      strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  res = pr_netio_read(nstrm, NULL, 0, 0);
+  if (res == 0) {
+    pr_netio_close(nstrm);
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = pr_netio_read(nstrm, buf, 0, 0);
+  if (res == 0) {
+    pr_netio_close(nstrm);
+    errno = EINVAL;
+    return -1;
+  }
+
+  expected_text = "Hello, World!\r\n";
+  expected_sz = strlen(expected_text);
+
+  memset(buf, '\0', sizeof(buf));
+  res = pr_netio_read(nstrm, buf, sizeof(buf)-1, 1);
+
+  if (res != (int) expected_sz) {
+    pr_trace_msg("netio", 1, "Expected %lu bytes, got %d",
+      (unsigned long) expected_sz, res);
+    pr_netio_close(nstrm);
+
+    if (res < 0) {
+      return -1;
+    }
+
+    errno = EIO;
+    return -1;
+  }
+
+  if (strcmp(buf, expected_text) != 0) {
+    pr_trace_msg("netio", 1, "Expected '%s', got '%s'", expected_text, buf);
+    pr_netio_close(nstrm);
+
+    errno = EIO;
+    return -1;
+  }
+
+  netio_read_eof = TRUE;
+  res = pr_netio_read(nstrm, buf, sizeof(buf)-1, 1);
+  if (res > 0) {
+    pr_trace_msg("netio", 1, "Expected EOF (0), got %d", res);
+    pr_netio_close(nstrm);
+
+    errno = EIO;
+    return -1;
+  }
+
+  netio_read_epipe = TRUE;
+  res = pr_netio_read(nstrm, buf, sizeof(buf)-1, sizeof(buf)-1);
+  if (res >= 0) {
+    pr_trace_msg("netio", 1, "Expected EPIPE (-1), got %d", res);
+    pr_netio_close(nstrm);
+
+    errno = EIO;
+    return -1;
+  }
+
+  mark_point();
+  pr_netio_close(nstrm);
+  return 0;
+}
+
+static int netio_write_to_stream(int strm_type, int use_async) {
+  int fd = 2, res;
+  char *buf;
+  size_t buflen;
+  pr_netio_stream_t *nstrm;
+
+  res = pr_netio_write(NULL, NULL, 0);
+  if (res == 0) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  nstrm = pr_netio_open(p, strm_type, fd, PR_NETIO_IO_WR);
+  if (nstrm == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg("netio", 1, "error opening custom netio stream: %s",
+      strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  res = pr_netio_write(nstrm, NULL, 0);
+  if (res == 0) {
+    pr_netio_close(nstrm);
+    errno = EINVAL;
+    return -1;
+  }
+
+  buf = "Hello, World!\n";
+  buflen = strlen(buf);
+
+  res = pr_netio_write(nstrm, buf, 0);
+  if (res == 0) {
+    pr_netio_close(nstrm);
+    errno = EINVAL;
+    return -1;
+  }
+
+  if (use_async) {
+    res = pr_netio_write_async(nstrm, buf, buflen);
+
+  } else {
+    res = pr_netio_write(nstrm, buf, buflen);
+  }
+
+  if ((size_t) res != buflen) {
+    pr_trace_msg("netio", 1, "wrote buffer (%lu bytes), got %d",
+      (unsigned long) buflen, res);
+    pr_netio_close(nstrm);
+
+    if (res < 0) {
+      return -1;
+    }
+
+    errno = EIO;
+    return -1;
+  }
+
+  netio_write_epipe = TRUE;
+  res = pr_netio_write(nstrm, buf, buflen);
+  if (res >= 0) {
+    pr_trace_msg("netio", 1, "Expected EPIPE (-1), got %d", res);
+    pr_netio_close(nstrm);
+    errno = EIO;
+    return -1;
+  }
+
+  mark_point();
+  pr_netio_close(nstrm);
+  return 0;
+}
+
+START_TEST (netio_read_test) {
+  int res;
+  pr_netio_t *netio, *netio2;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = netio_poll_cb;
+  netio->read = netio_read_cb;
+
+  /* Write to control stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  netio2 = pr_get_netio(PR_NETIO_STRM_CTRL);
+  fail_unless(netio2 != NULL, "Failed to get custom ctrl NetIO: %s",
+    strerror(errno));
+  fail_unless(netio2 == netio, "Expected custom ctrl NetIO %p, got %p",
+    netio, netio2);
+
+  res = netio_read_from_stream(PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to read from custom ctrl NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  /* Read from data stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to register custom data NetIO: %s",
+    strerror(errno));
+
+  netio2 = pr_get_netio(PR_NETIO_STRM_DATA);
+  fail_unless(netio2 != NULL, "Failed to get custom data NetIO: %s",
+    strerror(errno));
+  fail_unless(netio2 == netio, "Expected custom data NetIO %p, got %p",
+    netio, netio2);
+
+  res = netio_read_from_stream(PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to read from custom data NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_DATA);
+
+  /* Read from other stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_OTHR);
+  fail_unless(res == 0, "Failed to register custom other NetIO: %s",
+    strerror(errno));
+
+  netio2 = pr_get_netio(PR_NETIO_STRM_OTHR);
+  fail_unless(netio2 != NULL, "Failed to get custom othr NetIO: %s",
+    strerror(errno));
+  fail_unless(netio2 == netio, "Expected custom othr NetIO %p, got %p",
+    netio, netio2);
+
+  res = netio_read_from_stream(PR_NETIO_STRM_OTHR);
+  fail_unless(res == 0, "Failed to read from custom other NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_OTHR);
+}
+END_TEST
+
+START_TEST (netio_gets_test) {
+  int fd = 2, res;
+  char *buf, *expected, *text;
+  size_t buflen;
+  pr_netio_t *netio;
+  pr_netio_stream_t *nstrm;
+
+  text = pr_netio_gets(NULL, 0, NULL);
+  fail_unless(text == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = netio_poll_cb;
+  netio->read = netio_read_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open stream: %s", strerror(errno));
+
+  text = pr_netio_gets(NULL, 0, nstrm);
+  fail_unless(text == NULL, "Failed to handle null buffer");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  buflen = 1024;
+  buf = pcalloc(p, buflen);
+
+  text = pr_netio_gets(buf, 0, nstrm);
+  fail_unless(text == NULL, "Failed to handle zero buffer length");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  expected = "Hello, World!\r\n";
+  text = pr_netio_gets(buf, buflen-1, nstrm);
+  fail_unless(text != NULL, "Failed to get text: %s", strerror(errno));
+  fail_unless(strcmp(text, expected) == 0, "Expected '%s', got '%s'",
+    expected, text);
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+}
+END_TEST
+
+START_TEST (netio_write_test) {
+  int res;
+  pr_netio_t *netio, *netio2;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = netio_poll_cb;
+  netio->write = netio_write_cb;
+
+  /* Write to control stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  netio2 = pr_get_netio(PR_NETIO_STRM_CTRL);
+  fail_unless(netio2 != NULL, "Failed to get custom ctrl NetIO: %s",
+    strerror(errno));
+  fail_unless(netio2 == netio, "Expected custom ctrl NetIO %p, got %p",
+    netio, netio2);
+
+  res = netio_write_to_stream(PR_NETIO_STRM_CTRL, FALSE);
+  fail_unless(res == 0, "Failed to write to custom ctrl NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  /* Write to data stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to register custom data NetIO: %s",
+    strerror(errno));
+
+  netio2 = pr_get_netio(PR_NETIO_STRM_DATA);
+  fail_unless(netio2 != NULL, "Failed to get custom data NetIO: %s",
+    strerror(errno));
+  fail_unless(netio2 == netio, "Expected custom data NetIO %p, got %p",
+    netio, netio2);
+
+  res = netio_write_to_stream(PR_NETIO_STRM_DATA, FALSE);
+  fail_unless(res == 0, "Failed to write to custom data NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_DATA);
+
+  /* Write to other stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_OTHR);
+  fail_unless(res == 0, "Failed to register custom other NetIO: %s",
+    strerror(errno));
+
+  netio2 = pr_get_netio(PR_NETIO_STRM_OTHR);
+  fail_unless(netio2 != NULL, "Failed to get custom othr NetIO: %s",
+    strerror(errno));
+  fail_unless(netio2 == netio, "Expected custom othr NetIO %p, got %p",
+    netio, netio2);
+
+  res = netio_write_to_stream(PR_NETIO_STRM_OTHR, FALSE);
+  fail_unless(res == 0, "Failed to write to custom other NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_OTHR);
+}
+END_TEST
+
+START_TEST (netio_write_async_test) {
+  int res;
+  pr_netio_t *netio;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = netio_poll_cb;
+  netio->write = netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  res = netio_write_to_stream(PR_NETIO_STRM_CTRL, TRUE);
+  fail_unless(res == 0, "Failed to write to custom ctrl NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+}
+END_TEST
+
+static int netio_print_to_stream(int strm_type, int use_async) {
+  int fd = 2, res;
+  char *buf;
+  size_t buflen;
+  pr_netio_stream_t *nstrm;
+
+  nstrm = pr_netio_open(p, strm_type, fd, PR_NETIO_IO_WR);
+  if (nstrm == NULL) {
+    int xerrno = errno;
+
+    pr_trace_msg("netio", 1, "error opening custom netio stream: %s",
+      strerror(xerrno));
+    errno = xerrno;
+    return -1;
+  }
+
+  buf = "Hello, World!\n";
+  buflen = strlen(buf);
+
+  if (use_async) {
+    res = pr_netio_printf_async(nstrm, "%s", buf);
+
+  } else {
+    res = pr_netio_printf(nstrm, "%s", buf);
+  }
+
+  if ((size_t) res != buflen) {
+    pr_trace_msg("netio", 1, "printed buffer (%lu bytes), got %d",
+      (unsigned long) buflen, res);
+    pr_netio_close(nstrm);
+
+    if (res < 0) {
+      return -1;
+    }
+
+    errno = EIO;
+    return -1;
+  }
+
+  mark_point();
+  pr_netio_close(nstrm);
+  return 0;
+}
+
+START_TEST (netio_printf_test) {
+  int res;
+  pr_netio_t *netio;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = netio_poll_cb;
+  netio->write = netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  res = netio_print_to_stream(PR_NETIO_STRM_CTRL, FALSE);
+  fail_unless(res == 0, "Failed to print to custom ctrl NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+}
+END_TEST
+
+START_TEST (netio_printf_async_test) {
+  int res;
+  pr_netio_t *netio;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = netio_poll_cb;
+  netio->write = netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  res = netio_print_to_stream(PR_NETIO_STRM_CTRL, TRUE);
+  fail_unless(res == 0, "Failed to print to custom ctrl NetIO: %s",
+    strerror(errno));
+
+  mark_point();
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+}
+END_TEST
+
+START_TEST (netio_abort_test) {
+  pr_netio_stream_t *nstrm;
+  int fd = -1;
+
+  mark_point();
+  pr_netio_abort(NULL);
+
+  /* open/abort/close CTRL stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open ctrl stream on fd %d: %s", fd,
+    strerror(errno));
+
+  pr_netio_abort(nstrm);
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_ABORT,
+    "Failed to set PR_NETIO_SESS_ABORT flags on ctrl stream");
+
+  pr_netio_close(nstrm);
+
+  /* open/abort/close DATA stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_WR);
+  fail_unless(nstrm != NULL, "Failed to open data stream on fd %d: %s", fd,
+    strerror(errno));
+
+  pr_netio_abort(nstrm);
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_ABORT,
+    "Failed to set PR_NETIO_SESS_ABORT flags on data stream");
+
+  pr_netio_close(nstrm);
+
+  /* open/abort/close OTHR stream */
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_WR);
+  fail_unless(nstrm != NULL, "Failed to open othr stream on fd %d: %s", fd,
+    strerror(errno));
+
+  pr_netio_abort(nstrm);
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_ABORT,
+    "Failed to set PR_NETIO_SESS_ABORT flags on othr stream");
+
+  pr_netio_close(nstrm);
+}
+END_TEST
+
+static int netio_close_cb(pr_netio_stream_t *nstrm) {
+  return 0;
+}
+
+START_TEST (netio_lingering_abort_test) {
+  pr_netio_t *netio;
+  pr_netio_stream_t *nstrm;
+  int fd = 0, res;
+  long linger = 0L;
+
+  mark_point();
+  res = pr_netio_lingering_abort(NULL, linger);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->close = netio_close_cb;
+
+  /* open/abort/close CTRL stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open ctrl stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_lingering_abort(nstrm, linger);
+  fail_unless(res == 0, "Failed to set lingering abort on ctrl stream: %s",
+    strerror(errno));
+
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_ABORT,
+    "Failed to set PR_NETIO_SESS_ABORT flags on ctrl stream");
+
+  pr_netio_close(nstrm);
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  /* open/abort/close DATA stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to register custom data NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open data stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_lingering_abort(nstrm, linger);
+  fail_unless(res == 0, "Failed to set lingering abort on data stream: %s",
+    strerror(errno));
+
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_ABORT,
+    "Failed to set PR_NETIO_SESS_ABORT flags on data stream");
+
+  pr_netio_close(nstrm);
+  pr_unregister_netio(PR_NETIO_STRM_DATA);
+
+  /* open/abort/close OTHR stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_OTHR);
+  fail_unless(res == 0, "Failed to register custom othr NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open othr stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_lingering_abort(nstrm, linger);
+  fail_unless(res == 0, "Failed to set lingering abort on othr stream: %s",
+    strerror(errno));
+
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_ABORT,
+    "Failed to set PR_NETIO_SESS_ABORT flags on othr stream");
+
+  pr_netio_close(nstrm);
+  pr_unregister_netio(PR_NETIO_STRM_OTHR);
+}
+END_TEST
+
+START_TEST (netio_poll_interval_test) {
+  pr_netio_stream_t *nstrm;
+  int fd = -1;
+  unsigned int interval = 3;
+
+  mark_point();
+  pr_netio_set_poll_interval(NULL, 0);
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open ctrl stream on fd %d: %s", fd,
+    strerror(errno));
+
+  pr_netio_set_poll_interval(nstrm, interval); 
+  fail_unless(nstrm->strm_interval == interval,
+    "Expected stream interval %u, got %u", interval, nstrm->strm_interval);
+  fail_unless(nstrm->strm_flags & PR_NETIO_SESS_INTR,
+    "Failed to set PR_NETIO_SESS_INTR stream flag");
+
+  mark_point();
+  pr_netio_reset_poll_interval(NULL);
+
+  pr_netio_reset_poll_interval(nstrm);
+  fail_if(nstrm->strm_flags & PR_NETIO_SESS_INTR,
+    "Failed to clear PR_NETIO_SESS_INTR stream flag");
+
+  (void) pr_netio_close(nstrm);
+}
+END_TEST
+
+static int netio_shutdown_cb(pr_netio_stream_t *nstrm, int how) {
+  return 0;
+}
+
+START_TEST (netio_shutdown_test) {
+  pr_netio_t *netio;
+  pr_netio_stream_t *nstrm;
+  int fd = 0, how = SHUT_RD, res;
+
+  mark_point();
+  res = pr_netio_shutdown(NULL, how);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->close = netio_close_cb;
+  netio->shutdown = netio_shutdown_cb;
+
+  /* open/shutdown/close CTRL stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_CTRL, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open ctrl stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_shutdown(nstrm, how);
+  fail_unless(res == 0, "Failed to shutdown ctrl stream: %s", strerror(errno));
+
+  pr_netio_close(nstrm);
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  /* open/shutdown/close DATA stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_DATA);
+  fail_unless(res == 0, "Failed to register custom data NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_DATA, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open data stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_shutdown(nstrm, how);
+  fail_unless(res == 0, "Failed to shutdown ctrl stream: %s", strerror(errno));
+
+  pr_netio_close(nstrm);
+  pr_unregister_netio(PR_NETIO_STRM_DATA);
+
+  /* open/shutdown/close OTHR stream */
+  res = pr_register_netio(netio, PR_NETIO_STRM_OTHR);
+  fail_unless(res == 0, "Failed to register custom othr NetIO: %s",
+    strerror(errno));
+
+  nstrm = pr_netio_open(p, PR_NETIO_STRM_OTHR, fd, PR_NETIO_IO_RD);
+  fail_unless(nstrm != NULL, "Failed to open othr stream on fd %d: %s", fd,
+    strerror(errno));
+
+  res = pr_netio_shutdown(nstrm, how);
+  fail_unless(res == 0, "Failed to shutdown ctrl stream: %s", strerror(errno));
+
+  pr_netio_close(nstrm);
+  pr_unregister_netio(PR_NETIO_STRM_OTHR);
+}
+END_TEST
+
 Suite *tests_get_netio_suite(void) {
   Suite *suite;
   TCase *testcase;
@@ -1023,12 +2026,15 @@ Suite *tests_get_netio_suite(void) {
   suite = suite_create("netio");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, netio_open_test);
+  tcase_add_test(testcase, netio_postopen_test);
   tcase_add_test(testcase, netio_close_test);
+  tcase_add_test(testcase, netio_lingering_close_test);
+  tcase_add_test(testcase, netio_reopen_test);
   tcase_add_test(testcase, netio_buffer_alloc_test);
+
   tcase_add_test(testcase, netio_telnet_gets_args_test);
   tcase_add_test(testcase, netio_telnet_gets_single_line_test);
   tcase_add_test(testcase, netio_telnet_gets_multi_line_test);
@@ -1051,7 +2057,21 @@ Suite *tests_get_netio_suite(void) {
   tcase_add_test(testcase, netio_telnet_gets_bug3697_test);
   tcase_add_test(testcase, netio_telnet_gets_eof_test);
 
-  suite_add_tcase(suite, testcase);
+  tcase_add_test(testcase, netio_telnet_gets2_single_line_test);
+  tcase_add_test(testcase, netio_telnet_gets2_single_line_crnul_test);
+  tcase_add_test(testcase, netio_telnet_gets2_single_line_lf_test);
+
+  tcase_add_test(testcase, netio_read_test);
+  tcase_add_test(testcase, netio_gets_test);
+  tcase_add_test(testcase, netio_write_test);
+  tcase_add_test(testcase, netio_write_async_test);
+  tcase_add_test(testcase, netio_printf_test);
+  tcase_add_test(testcase, netio_printf_async_test);
+  tcase_add_test(testcase, netio_abort_test);
+  tcase_add_test(testcase, netio_lingering_abort_test);
+  tcase_add_test(testcase, netio_poll_interval_test);
+  tcase_add_test(testcase, netio_shutdown_test);
 
+  suite_add_tcase(suite, testcase);
   return suite;
 }
diff --git a/tests/api/parser.c b/tests/api/parser.c
new file mode 100644
index 0000000..fe037eb
--- /dev/null
+++ b/tests/api/parser.c
@@ -0,0 +1,656 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Parser API tests. */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static const char *config_path = "/tmp/prt-parser.conf";
+static const char *config_path2 = "/tmp/prt-parser2.conf";
+static const char *config_tmp_path = "/tmp/prt-parser.conf~";
+
+static void set_up(void) {
+  (void) unlink(config_path);
+  (void) unlink(config_path2);
+  (void) unlink(config_tmp_path);
+
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_fs();
+  pr_fs_statcache_set_policy(PR_TUNABLE_FS_STATCACHE_SIZE,
+    PR_TUNABLE_FS_STATCACHE_MAX_AGE, 0);
+  modules_init();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("config", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  (void) pr_parser_cleanup();
+
+  (void) unlink(config_path);
+  (void) unlink(config_path2);
+  (void) unlink(config_tmp_path);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("config", 0, 0);
+  }
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  } 
+}
+
+/* Tests */
+
+START_TEST (parser_prepare_test) {
+  int res;
+  xaset_t *parsed_servers = NULL;
+
+  res = pr_parser_prepare(NULL, NULL);
+  fail_unless(res == 0, "Failed to handle null arguments: %s", strerror(errno));
+
+  res = pr_parser_prepare(p, NULL);
+  fail_unless(res == 0, "Failed to handle null parsed_servers: %s",
+    strerror(errno));
+
+  res = pr_parser_prepare(NULL, &parsed_servers);
+  fail_unless(res == 0, "Failed to handle null pool: %s", strerror(errno));
+
+  (void) pr_parser_cleanup();
+}
+END_TEST
+
+START_TEST (parser_server_ctxt_test) {
+  server_rec *ctx, *res;
+
+  ctx = pr_parser_server_ctxt_get();
+  fail_unless(ctx == NULL, "Found server context unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pr_parser_prepare(p, NULL);
+
+  mark_point();
+  res = pr_parser_server_ctxt_close();
+  fail_unless(res == NULL, "Closed server context unexpectedly");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno));
+
+  mark_point();
+  res = pr_parser_server_ctxt_open("127.0.0.1");
+  fail_unless(res != NULL, "Failed to open server context: %s",
+    strerror(errno));
+
+  mark_point();
+  ctx = pr_parser_server_ctxt_get();
+  fail_unless(ctx != NULL, "Failed to get current server context: %s",
+    strerror(errno));
+  fail_unless(ctx == res, "Expected server context %p, got %p", res, ctx);
+
+  mark_point();
+  (void) pr_parser_server_ctxt_close();
+  (void) pr_parser_cleanup();
+}
+END_TEST
+
+START_TEST (parser_server_ctxt_push_test) {
+  int res;
+  server_rec *ctx, *ctx2;
+
+  res = pr_parser_server_ctxt_push(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  ctx = pcalloc(p, sizeof(server_rec));
+
+  mark_point();
+  res = pr_parser_server_ctxt_push(ctx);
+  fail_unless(res < 0, "Failed to handle unprepared parser");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_parser_prepare(p, NULL);
+
+  mark_point();
+  res = pr_parser_server_ctxt_push(ctx);
+  fail_unless(res == 0, "Failed to push server rec: %s", strerror(errno));
+
+  mark_point();
+  ctx2 = pr_parser_server_ctxt_get();
+  fail_unless(ctx2 != NULL, "Failed to get current server context: %s",
+    strerror(errno));
+  fail_unless(ctx2 == ctx, "Expected server context %p, got %p", ctx, ctx2);
+
+  (void) pr_parser_server_ctxt_close();
+  (void) pr_parser_cleanup();
+}
+END_TEST
+
+START_TEST (parser_config_ctxt_test) {
+  int is_empty = FALSE;
+  config_rec *ctx, *res;
+
+  ctx = pr_parser_config_ctxt_get();
+  fail_unless(ctx == NULL, "Found config context unexpectedly");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pr_parser_prepare(p, NULL);
+  pr_parser_server_ctxt_open("127.0.0.1");
+
+  res = pr_parser_config_ctxt_open(NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_parser_config_ctxt_open("<TestSuite>");
+  fail_unless(res != NULL, "Failed to open config context: %s",
+    strerror(errno));
+
+  mark_point();
+  ctx = pr_parser_config_ctxt_get();
+  fail_unless(ctx != NULL, "Failed to get current config context: %s",
+    strerror(errno));
+  fail_unless(ctx == res, "Expected config context %p, got %p", res, ctx);
+
+  mark_point();
+  (void) pr_parser_config_ctxt_close(&is_empty);
+  fail_unless(is_empty == TRUE, "Expected config context to be empty");
+
+  (void) pr_parser_server_ctxt_close();
+  (void) pr_parser_cleanup();
+}
+END_TEST
+
+START_TEST (parser_config_ctxt_push_test) {
+  int res;
+  config_rec *ctx, *ctx2;
+
+  res = pr_parser_config_ctxt_push(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  ctx = pcalloc(p, sizeof(config_rec));
+
+  mark_point();
+  res = pr_parser_config_ctxt_push(ctx);
+  fail_unless(res < 0, "Failed to handle unprepared parser");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_parser_prepare(p, NULL);
+
+  mark_point();
+  res = pr_parser_config_ctxt_push(ctx);
+  fail_unless(res == 0, "Failed to push config rec: %s", strerror(errno));
+
+  mark_point();
+  ctx2 = pr_parser_config_ctxt_get();
+  fail_unless(ctx2 != NULL, "Failed to get current config context: %s",
+    strerror(errno));
+  fail_unless(ctx2 == ctx, "Expected config context %p, got %p", ctx, ctx2);
+
+  (void) pr_parser_config_ctxt_close(NULL);
+  (void) pr_parser_cleanup();
+}
+END_TEST
+
+START_TEST (parser_get_lineno_test) {
+  unsigned int res;
+
+  res = pr_parser_get_lineno();
+  fail_unless(res == 0, "Expected 0, got %u", res);
+
+  res = pr_parser_get_lineno();
+  fail_unless(res == 0, "Expected 0, got %u", res);
+}
+END_TEST
+
+START_TEST (parser_read_line_test) {
+  char *res;
+
+  res = pr_parser_read_line(NULL, 0);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (parser_parse_line_test) {
+  cmd_rec *cmd;
+  char *text;
+  unsigned int lineno;
+
+  mark_point();
+  cmd = pr_parser_parse_line(NULL, NULL, 0);
+  fail_unless(cmd == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  cmd = pr_parser_parse_line(p, NULL, 0);
+  fail_unless(cmd == NULL, "Failed to handle null input");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  cmd = pr_parser_parse_line(p, "", 0);
+  fail_unless(cmd == NULL, "Failed to handle empty input");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pr_parser_prepare(p, NULL);
+  pr_parser_server_ctxt_open("127.0.0.1");
+
+  text = pstrdup(p, "FooBar");
+  cmd = pr_parser_parse_line(p, text, 0);
+  fail_unless(cmd != NULL, "Failed to parse text '%s': %s", text,
+    strerror(errno));
+  fail_unless(cmd->argc == 1, "Expected 1, got %d", cmd->argc);
+  fail_unless(strcmp(cmd->argv[0], text) == 0,
+    "Expected '%s', got '%s'", text, (char *) cmd->argv[0]);
+  lineno = pr_parser_get_lineno();
+  fail_unless(lineno != 1, "Expected lineno 1, got %u", lineno);
+
+  text = pstrdup(p, "FooBar baz quxx");
+  cmd = pr_parser_parse_line(p, text, 0);
+  fail_unless(cmd != NULL, "Failed to parse text '%s': %s", text,
+    strerror(errno));
+  fail_unless(cmd->argc == 3, "Expected 3, got %d", cmd->argc);
+  fail_unless(strcmp(cmd->argv[0], "FooBar") == 0,
+    "Expected 'FooBar', got '%s'", (char *) cmd->argv[0]);
+  fail_unless(strcmp(cmd->arg, "baz quxx") == 0,
+    "Expected 'baz quxx', got '%s'", cmd->arg);
+  lineno = pr_parser_get_lineno();
+  fail_unless(lineno != 2, "Expected lineno 2, got %u", lineno);
+
+  pr_env_set(p, "FOO_TEST", "BAR");
+  text = pstrdup(p, "BarBaz %{env:FOO_TEST}");
+  cmd = pr_parser_parse_line(p, text, 0);
+  fail_unless(cmd != NULL, "Failed to parse text '%s': %s", text,
+    strerror(errno));
+  fail_unless(cmd->argc == 2, "Expected 2, got %d", cmd->argc);
+  fail_unless(strcmp(cmd->argv[0], "BarBaz") == 0,
+    "Expected 'BarBaz', got '%s'", (char *) cmd->argv[0]);
+  fail_unless(strcmp(cmd->arg, "BAR") == 0,
+    "Expected 'BAR', got '%s'", cmd->arg);
+  lineno = pr_parser_get_lineno();
+  fail_unless(lineno != 3, "Expected lineno 3, got %u", lineno);
+
+  /* This time, without the requested environment variable present. */
+  pr_env_unset(p, "FOO_TEST");
+  cmd = pr_parser_parse_line(p, text, 0);
+  fail_unless(cmd != NULL, "Failed to parse text '%s': %s", text,
+    strerror(errno));
+  fail_unless(cmd->argc == 1, "Expected 1, got %d", cmd->argc);
+  fail_unless(strcmp(cmd->argv[0], "BarBaz") == 0,
+    "Expected 'BarBaz', got '%s'", (char *) cmd->argv[0]);
+  fail_unless(strcmp(cmd->arg, "") == 0, "Expected '', got '%s'", cmd->arg);
+  lineno = pr_parser_get_lineno();
+  fail_unless(lineno != 3, "Expected lineno 3, got %u", lineno);
+
+  text = pstrdup(p, "<FooBar baz>");
+  cmd = pr_parser_parse_line(p, text, 0);
+  fail_unless(cmd != NULL, "Failed to parse text '%s': %s", text,
+    strerror(errno));
+  fail_unless(cmd->argc == 2, "Expected 2, got %d", cmd->argc);
+  fail_unless(strcmp(cmd->argv[0], "<FooBar>") == 0,
+    "Expected '<FooBar>', got '%s'", (char *) cmd->argv[0]);
+  lineno = pr_parser_get_lineno();
+  fail_unless(lineno != 5, "Expected lineno 5, got %u", lineno);
+
+  mark_point();
+  (void) pr_parser_server_ctxt_close();
+  (void) pr_parser_cleanup();
+}
+END_TEST
+
+MODRET parser_set_testsuite_enabled(cmd_rec *cmd) {
+  return PR_HANDLED(cmd);
+}
+
+MODRET parser_set_testsuite_engine(cmd_rec *cmd) {
+  return PR_HANDLED(cmd);
+}
+
+static module parser_module;
+
+static conftable parser_conftab[] = {
+  { "TestSuiteEnabled",	parser_set_testsuite_enabled, NULL },
+  { "TestSuiteEngine",	parser_set_testsuite_engine, NULL },
+  { NULL },
+};
+
+static int load_parser_module(void) {
+  /* Load the module's config handlers. */
+  memset(&parser_module, 0, sizeof(parser_module));
+  parser_module.name = "parser";
+  parser_module.conftable = parser_conftab;
+
+  return pr_module_load_conftab(&parser_module);
+}
+
+START_TEST (parse_config_path_test) {
+  int res;
+  char *text;
+  const char *path;
+  struct stat st;
+  unsigned long include_opts;
+  pr_fh_t *fh;
+
+  (void) pr_parser_cleanup();
+
+  res = parse_config_path(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = parse_config_path2(p, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "foo";
+  fail_unless(res < 0, "Failed to handle relative path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = parse_config_path2(p, path, 1024);
+  fail_unless(res < 0, "Failed to handle excessive depth");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = parse_config_path2(p, path, 0);
+  fail_unless(res < 0, "Failed to handle invalid path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = lstat(path, &st);
+  fail_unless(res == 0, "Failed lstat(2) on '/tmp': %s", strerror(errno));
+
+  res = parse_config_path2(p, path, 0);
+  if (S_ISLNK(st.st_mode)) {
+    fail_unless(res < 0, "Failed to handle uninitialized parser");
+    fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+      strerror(errno), errno);
+
+  } else if (S_ISDIR(st.st_mode)) {
+    fail_unless(res == 0, "Failed to handle empty directory");
+  }
+
+  pr_parser_prepare(p, NULL);
+  pr_parser_server_ctxt_open("127.0.0.1");
+
+  res = parse_config_path2(p, path, 0);
+  if (S_ISLNK(st.st_mode)) {
+    fail_unless(res < 0, "Failed to handle directory-only path");
+    fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+      strerror(errno), errno);
+
+  } else if (S_ISDIR(st.st_mode)) {
+    fail_unless(res == 0, "Failed to handle empty directory");
+  }
+
+  mark_point();
+  path = config_path;
+  res = parse_config_path2(p, path, 0);
+  fail_unless(res < 0, "Failed to handle nonexistent file");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  include_opts = pr_parser_set_include_opts(PR_PARSER_INCLUDE_OPT_IGNORE_WILDCARDS);
+  mark_point();
+  path = "/tmp*/foo.conf";
+  res = parse_config_path2(p, path, 0);
+  fail_unless(res < 0, "Failed to handle directory-only path");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+  (void) pr_parser_set_include_opts(include_opts);
+
+  /* On Mac, `/tmp` is a symlink.  And currently, parse_config_path() does
+   * not allow following of symlinked directories.  So this MIGHT fail, if
+   * we're on a Mac.
+   */
+  res = lstat("/tmp", &st);
+  fail_unless(res == 0, "Failed lstat(2) on '/tmp': %s", strerror(errno));
+
+  mark_point();
+  path = "/tmp/prt*foo*bar*.conf";
+  res = parse_config_path2(p, path, 0);
+
+  if (S_ISLNK(st.st_mode)) {
+    fail_unless(res < 0, "Failed to handle nonexistent file");
+    fail_unless(errno == ENOTDIR, "Expected ENOTDIR (%d), got %s (%d)", ENOTDIR,
+      strerror(errno), errno);
+
+    include_opts = pr_parser_set_include_opts(PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS);
+
+    /* By default, we ignore the case where there are no matching files. */
+    res = parse_config_path2(p, path, 0);
+    fail_unless(res == 0, "Failed to handle nonexistent file: %s",
+      strerror(errno));
+
+    (void) pr_parser_set_include_opts(include_opts);
+
+  } else {
+    /* By default, we ignore the case where there are no matching files. */
+    fail_unless(res == 0, "Failed to handle nonexistent file: %s",
+      strerror(errno));
+  }
+
+  /* Load the module's config handlers. */
+  res = load_parser_module();
+  fail_unless(res == 0, "Failed to load module conftab: %s", strerror(errno));
+
+  include_opts = pr_parser_set_include_opts(PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS);
+
+  /* Parse single file. */
+  path = config_path;
+  fh = pr_fsio_open(path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", path, strerror(errno));
+
+  text = "TestSuiteEngine on\r\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to write '%s': %s", path, strerror(errno));
+
+  mark_point();
+  res = parse_config_path2(p, path, 0);
+  fail_if(res < 0, "Failed to parse '%s': %s", path, strerror(errno));
+
+  path = "/tmp/prt*.conf";
+  res = parse_config_path2(p, path, 0);
+  fail_if(res < 0, "Failed to parse '%s': %s", path, strerror(errno));
+
+  (void) pr_parser_set_include_opts(PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS|PR_PARSER_INCLUDE_OPT_IGNORE_TMP_FILES|PR_PARSER_INCLUDE_OPT_IGNORE_WILDCARDS);
+
+  path = config_tmp_path;
+  fh = pr_fsio_open(path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", path, strerror(errno));
+
+  text = "TestSuiteEnabled off\r\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to write '%s': %s", path, strerror(errno));
+
+  mark_point();
+  path = "/tmp/prt*.conf*";
+  res = parse_config_path2(p, path, 0);
+  fail_if(res < 0, "Failed to parse '%s': %s", path, strerror(errno));
+
+  mark_point();
+  path = "/t*p/prt*.conf*";
+  res = parse_config_path2(p, path, 0);
+  fail_unless(res < 0, "Failed to handle wildcard path '%s'", path);
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) pr_parser_set_include_opts(PR_PARSER_INCLUDE_OPT_ALLOW_SYMLINKS|PR_PARSER_INCLUDE_OPT_IGNORE_TMP_FILES);
+
+  mark_point();
+  path = "/t*p/prt*.conf*";
+  res = parse_config_path2(p, path, 0);
+  fail_if(res < 0, "Failed to parse '%s': %s", path, strerror(errno));
+
+  (void) pr_parser_server_ctxt_close();
+  (void) pr_parser_cleanup();
+  (void) pr_module_unload(&parser_module);
+  (void) pr_parser_set_include_opts(include_opts);
+}
+END_TEST
+
+START_TEST (parser_parse_file_test) {
+  int res;
+  pr_fh_t *fh;
+  char *text;
+
+  (void) unlink(config_path);
+
+  mark_point();
+  res = pr_parser_parse_file(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_parser_parse_file(p, config_path, NULL, 0);
+  fail_unless(res < 0, "Failed to handle invalid file");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  pr_parser_prepare(p, NULL);
+  pr_parser_server_ctxt_open("127.0.0.1");
+
+  mark_point();
+  res = pr_parser_parse_file(p, config_path, NULL, 0);
+  fail_unless(res < 0, "Failed to handle invalid file");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_parser_parse_file(p, "/tmp", NULL, 0);
+  fail_unless(res < 0, "Failed to handle directory");
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  fh = pr_fsio_open(config_path, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", config_path,
+    strerror(errno));
+
+  text = "TestSuiteEngine on\r\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  text = "TestSuiteEnabled on\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  text = "Include ";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  text = (char *) config_path2;
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  text = "\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to write '%s': %s", config_path,
+    strerror(errno));
+
+  fh = pr_fsio_open(config_path2, O_CREAT|O_EXCL|O_WRONLY);
+  fail_unless(fh != NULL, "Failed to open '%s': %s", config_path2,
+    strerror(errno));
+
+  text = "TestSuiteOptions Bebugging\n";
+  res = pr_fsio_write(fh, text, strlen(text));
+  fail_if(res < 0, "Failed to write '%s': %s", text, strerror(errno));
+
+  res = pr_fsio_close(fh);
+  fail_unless(res == 0, "Failed to write '%s': %s", config_path2,
+    strerror(errno));
+
+  mark_point();
+
+  /* Load the module's config handlers. */
+  res = load_parser_module();
+  fail_unless(res == 0, "Failed to load module conftab: %s", strerror(errno));
+
+  res = pr_parser_parse_file(p, config_path, NULL, PR_PARSER_FL_DYNAMIC_CONFIG);
+  fail_unless(res == 0, "Failed to parse '%s': %s", config_path,
+    strerror(errno));
+
+  res = pr_parser_parse_file(p, config_path, NULL, 0);
+  fail_unless(res < 0, "Parsed '%s' unexpectedly", config_path);
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
+  (void) pr_parser_server_ctxt_close();
+  (void) pr_parser_cleanup();
+  (void) pr_module_unload(&parser_module);
+  (void) pr_fsio_unlink(config_path);
+  (void) pr_fsio_unlink(config_path2);
+}
+END_TEST
+
+Suite *tests_get_parser_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("parser");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, parser_prepare_test);
+  tcase_add_test(testcase, parser_server_ctxt_test);
+  tcase_add_test(testcase, parser_server_ctxt_push_test);
+  tcase_add_test(testcase, parser_config_ctxt_test);
+  tcase_add_test(testcase, parser_config_ctxt_push_test);
+  tcase_add_test(testcase, parser_get_lineno_test);
+  tcase_add_test(testcase, parser_read_line_test);
+  tcase_add_test(testcase, parser_parse_line_test);
+  tcase_add_test(testcase, parse_config_path_test);
+  tcase_add_test(testcase, parser_parse_file_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/pidfile.c b/tests/api/pidfile.c
new file mode 100644
index 0000000..c0c7e09
--- /dev/null
+++ b/tests/api/pidfile.c
@@ -0,0 +1,126 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Pidfile API tests. */
+
+#include "tests.h"
+
+static pool *p = NULL;
+static const char *pidfile_path = "/tmp/prt-pidfile";
+
+/* Fixtures */
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+}
+
+static void tear_down(void) {
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  }
+
+  (void) unlink(pidfile_path);
+}
+
+/* Tests */
+
+START_TEST (pidfile_set_test) {
+  int res;
+  const char *path;
+
+  res = pr_pidfile_set(NULL);
+  fail_unless(res < 0, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  path = "foo";
+  res = pr_pidfile_set(path);
+  fail_unless(res < 0, "Failed to handle relative path");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  path = "/foo";
+  res = pr_pidfile_set(path);
+  fail_unless(res == 0, "Failed to handle path '%s': %s", path,
+    strerror(errno));
+
+  path = pr_pidfile_get();
+  fail_unless(strcmp(path, "/foo") == 0, "Expected path '/foo', got '%s'",
+    path);
+}
+END_TEST
+
+START_TEST (pidfile_remove_test) {
+  int res;
+
+  res = pr_pidfile_remove();
+  fail_unless(res < 0, "Removed nonexistent file unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+}
+END_TEST
+
+START_TEST (pidfile_write_test) {
+  int res;
+
+  res = pr_pidfile_set(pidfile_path);
+  fail_unless(res == 0, "Failed to set path '%s': %s", pidfile_path,
+    strerror(errno));
+
+  res = pr_pidfile_write();
+  fail_unless(res == 0, "Failed to write to path '%s': %s", pidfile_path,
+    strerror(errno));
+
+  res = pr_pidfile_remove();
+  fail_unless(res == 0, "Failed to remove path '%s': %s", pidfile_path,
+    strerror(errno));
+
+  res = pr_pidfile_remove();
+  fail_unless(res < 0, "Removed nonexistent file unexpectedly");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+}
+END_TEST
+
+Suite *tests_get_pidfile_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("pidfile");
+
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, pidfile_set_test);
+  tcase_add_test(testcase, pidfile_remove_test);
+  tcase_add_test(testcase, pidfile_write_test);
+
+  suite_add_tcase(suite, testcase);
+
+  return suite;
+}
diff --git a/tests/api/pool.c b/tests/api/pool.c
index f70dca0..8008f1c 100644
--- a/tests/api/pool.c
+++ b/tests/api/pool.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,27 +22,68 @@
  * OpenSSL in the source distribution.
  */
 
-/* Pool API tests
- * $Id: pool.c,v 1.5 2013-02-19 15:49:16 castaglia Exp $
- */
+/* Pool API tests */
 
 #include "tests.h"
 
-START_TEST (parent_pool_test) {
-  pool *p;
+static void set_up(void) {
+  init_pools();
 
-  p = make_sub_pool(NULL);
-  fail_if(p == NULL, "Failed to allocate parent pool");
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("pool", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("pool", 0, 0);
+  }
 
+  free_pools();
+}
+
+START_TEST (pool_destroy_pool_test) {
+  pool *p, *sub_pool;
+
+  mark_point();
+  destroy_pool(NULL);
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  destroy_pool(p);
+
+#if !defined(PR_USE_DEVEL)
+  /* What happens if we destroy an already-destroyed pool?  Answer: IFF
+   * --enable-devel was used, THEN destroying an already-destroyed pool
+   * will result in an exit(2) call from within pool.c, via the
+   * chk_on_blk_list() function.  How impolite.
+   */
+  mark_point();
+  destroy_pool(p);
+#endif
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  sub_pool = make_sub_pool(p);
+  destroy_pool(p);
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  sub_pool = make_sub_pool(p);
+  destroy_pool(sub_pool);
   destroy_pool(p);
 }
 END_TEST
 
-START_TEST (parent_sub_pool_test) {
+START_TEST (pool_make_sub_pool_test) {
   pool *p, *sub_pool;
 
   p = make_sub_pool(NULL);
   fail_if(p == NULL, "Failed to allocate parent pool");
+  destroy_pool(p);
+
+  p = make_sub_pool(NULL);
+  fail_if(p == NULL, "Failed to allocate parent pool");
 
   sub_pool = make_sub_pool(p);
   fail_if(sub_pool == NULL, "Failed to allocate sub pool");
@@ -134,7 +175,7 @@ START_TEST (pool_create_sz_with_alloc_test) {
 }
 END_TEST
 
-START_TEST (palloc_test) {
+START_TEST (pool_palloc_test) {
   pool *p;
   char *v;
   size_t sz;
@@ -158,7 +199,31 @@ START_TEST (palloc_test) {
 }
 END_TEST
 
-START_TEST (pcalloc_test) {
+START_TEST (pool_pallocsz_test) {
+  pool *p;
+  char *v;
+  size_t sz;
+
+  p = make_sub_pool(NULL);
+  fail_if(p == NULL, "Failed to allocate parent pool");
+
+  sz = 0;
+  v = pallocsz(p, sz);
+  fail_unless(v == NULL, "Allocated %u-len memory", sz);
+
+  sz = 1;
+  v = pallocsz(p, sz);
+  fail_if(v == NULL, "Failed to allocate %u-len memory", sz);
+
+  sz = 16382;
+  v = pallocsz(p, sz);
+  fail_if(v == NULL, "Failed to allocate %u-len memory", sz);
+
+  destroy_pool(p);
+}
+END_TEST
+
+START_TEST (pool_pcalloc_test) {
   register unsigned int i;
   pool *p;
   char *v;
@@ -189,6 +254,142 @@ START_TEST (pcalloc_test) {
 }
 END_TEST
 
+START_TEST (pool_pcallocsz_test) {
+  pool *p;
+  char *v;
+  size_t sz;
+
+  p = make_sub_pool(NULL);
+  fail_if(p == NULL, "Failed to allocate parent pool");
+
+  sz = 0;
+  v = pcallocsz(p, sz);
+  fail_unless(v == NULL, "Allocated %u-len memory", sz);
+
+  sz = 1;
+  v = pcallocsz(p, sz);
+  fail_if(v == NULL, "Failed to allocate %u-len memory", sz);
+
+  sz = 16382;
+  v = pcallocsz(p, sz);
+  fail_if(v == NULL, "Failed to allocate %u-len memory", sz);
+
+  destroy_pool(p);
+}
+END_TEST
+
+START_TEST (pool_tag_test) {
+  pool *p;
+
+  p = make_sub_pool(permanent_pool);
+
+  mark_point();
+  pr_pool_tag(NULL, NULL);
+
+  mark_point();
+  pr_pool_tag(p, NULL);
+
+  mark_point();
+  pr_pool_tag(p, "foo");
+
+  destroy_pool(p);
+}
+END_TEST
+
+#if defined(PR_USE_DEVEL)
+START_TEST (pool_debug_memory_test) {
+  pool *p, *sub_pool;
+
+  mark_point();
+  pr_pool_debug_memory(NULL);
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  pr_pool_debug_memory(NULL);
+
+  mark_point();
+  destroy_pool(p);
+  pr_pool_debug_memory(NULL);
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  sub_pool = make_sub_pool(p);
+  pr_pool_debug_memory(NULL);
+
+  destroy_pool(sub_pool);
+  pr_pool_debug_memory(NULL);
+
+  destroy_pool(p);
+  pr_pool_debug_memory(NULL);
+}
+END_TEST
+
+START_TEST (pool_debug_flags_test) {
+  int res;
+
+  res = pr_pool_debug_set_flags(-1);
+  fail_unless(res < 0, "Failed to handle invalid flags");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_pool_debug_set_flags(0);
+  fail_if(res < 0, "Failed to set flags: %s", strerror(errno));
+}
+END_TEST
+#endif /* PR_USE_DEVEL */
+
+static unsigned int pool_cleanup_count = 0;
+
+static void cleanup_cb(void *data) {
+  pool_cleanup_count++;
+}
+
+START_TEST (pool_register_cleanup_test) {
+  pool *p;
+
+  pool_cleanup_count = 0;
+
+  mark_point();
+  register_cleanup(NULL, NULL, NULL, NULL);
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  register_cleanup(p, NULL, NULL, NULL);
+
+  register_cleanup(p, NULL, cleanup_cb, cleanup_cb);
+  destroy_pool(p);
+  fail_unless(pool_cleanup_count > 0, "Expected cleanup count >0, got %u",
+    pool_cleanup_count);
+}
+END_TEST
+
+START_TEST (pool_unregister_cleanup_test) {
+  pool *p;
+
+  pool_cleanup_count = 0;
+
+  mark_point();
+  unregister_cleanup(NULL, NULL, NULL);
+
+  mark_point();
+  p = make_sub_pool(permanent_pool);
+  register_cleanup(p, NULL, cleanup_cb, cleanup_cb);
+  unregister_cleanup(p, NULL, NULL);
+  fail_unless(pool_cleanup_count == 0, "Expected cleanup count 0, got %u",
+    pool_cleanup_count);
+
+  pool_cleanup_count = 0;
+  register_cleanup(p, NULL, cleanup_cb, cleanup_cb);
+  unregister_cleanup(p, NULL, cleanup_cb);
+  fail_unless(pool_cleanup_count == 0, "Expected cleanup count >0, got %u",
+    pool_cleanup_count);
+
+  destroy_pool(p);
+  fail_unless(pool_cleanup_count == 0, "Expected cleanup count >0, got %u",
+    pool_cleanup_count);
+}
+END_TEST
+
 Suite *tests_get_pool_suite(void) {
   Suite *suite;
   TCase *testcase;
@@ -196,8 +397,10 @@ Suite *tests_get_pool_suite(void) {
   suite = suite_create("pool");
 
   testcase = tcase_create("base");
-  tcase_add_test(testcase, parent_pool_test);
-  tcase_add_test(testcase, parent_sub_pool_test);
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, pool_destroy_pool_test);
+  tcase_add_test(testcase, pool_make_sub_pool_test);
   tcase_add_test(testcase, pool_create_sz_test);
 
   /* Seems this particular testcase reveals a bug in the pool code.  On the
@@ -216,10 +419,18 @@ Suite *tests_get_pool_suite(void) {
 #if 0
   tcase_add_test(testcase, pool_create_sz_with_alloc_test);
 #endif
-  tcase_add_test(testcase, palloc_test);
-  tcase_add_test(testcase, pcalloc_test);
+  tcase_add_test(testcase, pool_palloc_test);
+  tcase_add_test(testcase, pool_pallocsz_test);
+  tcase_add_test(testcase, pool_pcalloc_test);
+  tcase_add_test(testcase, pool_pcallocsz_test);
+  tcase_add_test(testcase, pool_tag_test);
+#if defined(PR_USE_DEVEL)
+  tcase_add_test(testcase, pool_debug_memory_test);
+  tcase_add_test(testcase, pool_debug_flags_test);
+#endif /* PR_USE_DEVEL */
+  tcase_add_test(testcase, pool_register_cleanup_test);
+  tcase_add_test(testcase, pool_unregister_cleanup_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/privs.c b/tests/api/privs.c
new file mode 100644
index 0000000..5cc5a1c
--- /dev/null
+++ b/tests/api/privs.c
@@ -0,0 +1,185 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015-2016 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Privs API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static uid_t privs_uid = (uid_t) -1;
+static gid_t privs_gid = (gid_t) -1;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_privs();
+  privs_uid = getuid();
+  privs_gid = getgid();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("privs", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("privs", 0, 0);
+  }
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  }
+}
+
+START_TEST (privs_set_nonroot_daemon_test) {
+  int nonroot, res;
+
+  res = set_nonroot_daemon(-1);
+  fail_unless(res < 0, "Failed to handle non-Boolean parameter");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  nonroot = set_nonroot_daemon(TRUE);
+  fail_if(nonroot != FALSE && nonroot != TRUE,  "Expected true/false, got %d",
+    nonroot);
+  set_nonroot_daemon(nonroot);
+}
+END_TEST
+
+START_TEST (privs_setup_test) {
+  int nonroot, res;
+
+  if (privs_uid != 0) {
+    res = pr_privs_setup(privs_uid, privs_gid, __FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to setup privs: %s", strerror(errno));
+    fail_unless(session.uid == privs_uid, "Expected %lu, got %lu",
+      (unsigned long) privs_uid, (unsigned long) session.uid);
+    fail_unless(session.gid == privs_gid, "Expected %lu, got %lu",
+      (unsigned long) privs_gid, (unsigned long) session.gid);
+
+    nonroot = set_nonroot_daemon(FALSE);
+
+    res = pr_privs_setup(privs_uid, privs_gid, __FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to setup privs: %s", strerror(errno));
+    fail_unless(session.uid == privs_uid, "Expected %lu, got %lu",
+      (unsigned long) privs_uid, (unsigned long) session.uid);
+    fail_unless(session.gid == privs_gid, "Expected %lu, got %lu",
+      (unsigned long) privs_gid, (unsigned long) session.gid);
+
+    set_nonroot_daemon(nonroot);
+  }
+}
+END_TEST
+
+START_TEST (privs_root_test) {
+  int nonroot, res;
+
+  if (privs_uid != 0) {
+    res = pr_privs_root(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to set root privs: %s", strerror(errno));
+
+    nonroot = set_nonroot_daemon(FALSE);
+
+    res = pr_privs_root(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to set root privs: %s", strerror(errno));
+
+    set_nonroot_daemon(nonroot);
+  }
+}
+END_TEST
+
+START_TEST (privs_user_test) {
+  int nonroot, res;
+
+  if (privs_uid != 0) {
+    res = pr_privs_user(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to set user privs: %s", strerror(errno));
+
+    nonroot = set_nonroot_daemon(FALSE);
+
+    res = pr_privs_user(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to set user privs: %s", strerror(errno));
+
+    set_nonroot_daemon(nonroot);
+  }
+}
+END_TEST
+
+START_TEST (privs_relinquish_test) {
+  int nonroot, res;
+
+  if (privs_uid != 0) {
+    res = pr_privs_relinquish(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to relinquish privs: %s", strerror(errno));
+
+    nonroot = set_nonroot_daemon(FALSE);
+
+    res = pr_privs_relinquish(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to relinquish privs: %s", strerror(errno));
+
+    set_nonroot_daemon(nonroot);
+  }
+}
+END_TEST
+
+START_TEST (privs_revoke_test) {
+  int nonroot, res;
+
+  if (privs_uid != 0) {
+    res = pr_privs_revoke(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to revoke privs: %s", strerror(errno));
+
+    nonroot = set_nonroot_daemon(FALSE);
+
+    res = pr_privs_revoke(__FILE__, __LINE__);
+    fail_unless(res == 0, "Failed to revoke privs: %s", strerror(errno));
+
+    set_nonroot_daemon(nonroot);
+  }
+}
+END_TEST
+
+Suite *tests_get_privs_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("privs");
+
+  testcase = tcase_create("base");
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, privs_set_nonroot_daemon_test);
+  tcase_add_test(testcase, privs_setup_test);
+  tcase_add_test(testcase, privs_root_test);
+  tcase_add_test(testcase, privs_user_test);
+  tcase_add_test(testcase, privs_relinquish_test);
+  tcase_add_test(testcase, privs_revoke_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/redis.c b/tests/api/redis.c
new file mode 100644
index 0000000..232d2a1
--- /dev/null
+++ b/tests/api/redis.c
@@ -0,0 +1,4475 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2017 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Redis API tests. */
+
+#include "tests.h"
+
+#ifdef PR_USE_REDIS
+
+static pool *p = NULL;
+static const char *redis_server = "127.0.0.1";
+static int redis_port = 6379;
+
+/* Fixtures */
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  redis_init();
+  redis_set_server(redis_server, redis_port, NULL);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("redis", 1, 20);
+  }
+}
+
+static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("redis", 0, 0);
+  }
+
+  redis_clear();
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  }
+}
+
+/* Tests */
+
+START_TEST (redis_conn_destroy_test) {
+  int res;
+
+  mark_point();
+  res = pr_redis_conn_destroy(NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (redis_conn_close_test) {
+  int res;
+
+  mark_point();
+  res = pr_redis_conn_close(NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (redis_conn_new_test) {
+  int res;
+  pr_redis_t *redis;
+
+  mark_point();
+  redis = pr_redis_conn_new(NULL, NULL, 0);
+  fail_unless(redis == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+
+  if (getenv("TRAVIS") == NULL) {
+    /* Now deliberately set the wrong server and port. */
+    redis_set_server("127.1.2.3", redis_port, NULL);
+
+    mark_point();
+    redis = pr_redis_conn_new(p, NULL, 0);
+    fail_unless(redis == NULL, "Failed to handle invalid address");
+    fail_unless(errno == EIO, "Expected EIO (%d), got %s (%d)", EIO,
+      strerror(errno), errno);
+  }
+
+  redis_set_server(redis_server, 1020, NULL);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis == NULL, "Failed to handle invalid port");
+  fail_unless(errno == EIO, "Expected EIO (%d), got %s (%d)", EIO,
+    strerror(errno), errno);
+
+  /* Restore our testing server/port. */
+  redis_set_server(redis_server, redis_port, NULL);
+}
+END_TEST
+
+START_TEST (redis_conn_get_test) {
+  int res;
+  pr_redis_t *redis, *redis2;
+
+  mark_point();
+  redis = pr_redis_conn_get(NULL);
+  fail_unless(redis == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_get(p);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+
+  mark_point();
+  redis = pr_redis_conn_get(p);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  redis2 = pr_redis_conn_get(p);
+  fail_unless(redis2 != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+  fail_unless(redis == redis2, "Expected %p, got %p", redis, redis2);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == FALSE, "Expected FALSE, got TRUE");
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_conn_set_namespace_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *prefix;
+  size_t prefixsz;
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, NULL, 0);
+  fail_unless(res == 0, "Failed to set null namespace prefix: %s",
+    strerror(errno));
+
+  prefix = "test.";
+  prefixsz = strlen(prefix);
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, prefix, 0);
+  fail_unless(res < 0, "Failed to handle empty namespace prefix");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, prefix, prefixsz);
+  fail_unless(res == 0, "Failed to set namespace prefix '%s': %s", prefix,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, NULL, 0);
+  fail_unless(res == 0, "Failed to set null namespace prefix: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_conn_auth_test) {
+  int res;
+  pr_redis_t *redis;
+  const char *text;
+  array_header *args;
+
+  mark_point();
+  res = pr_redis_auth(NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_auth(redis, NULL);
+  fail_unless(res < 0, "Failed to handle null password");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "password";
+
+  /* What happens if we try to AUTH to a non-password-protected Redis?
+   * Answer: Redis returns an error indicating that no password is required.
+   */
+  mark_point();
+  res = pr_redis_auth(redis, text);
+  fail_unless(res < 0, "Failed to handle lack of need for authentication");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Use CONFIG SET to require a password. */
+  args = make_array(p, 0, sizeof(char *));
+  *((char **) push_array(args)) = pstrdup(p, "CONFIG");
+  *((char **) push_array(args)) = pstrdup(p, "SET");
+  *((char **) push_array(args)) = pstrdup(p, "requirepass");
+  *((char **) push_array(args)) = pstrdup(p, text);
+
+  mark_point();
+  res = pr_redis_command(redis, args, PR_REDIS_REPLY_TYPE_STATUS);
+  fail_unless(res == 0, "Failed to enable authentication: %s", strerror(errno));
+
+  args = make_array(p, 0, sizeof(char *));
+  *((char **) push_array(args)) = pstrdup(p, "TIME");
+
+  mark_point();
+  res = pr_redis_command(redis, args, PR_REDIS_REPLY_TYPE_ARRAY);
+  fail_unless(res < 0, "Failed to handle required authentication");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_auth(redis, text);
+  fail_unless(res == 0, "Failed to authenticate client: %s", strerror(errno));
+
+  /* Don't forget to remove the password. */
+  args = make_array(p, 0, sizeof(char *));
+  *((char **) push_array(args)) = pstrdup(p, "CONFIG");
+  *((char **) push_array(args)) = pstrdup(p, "SET");
+  *((char **) push_array(args)) = pstrdup(p, "requirepass");
+  *((char **) push_array(args)) = pstrdup(p, "");
+
+  mark_point();
+  res = pr_redis_command(redis, args, PR_REDIS_REPLY_TYPE_STATUS);
+  fail_unless(res == 0, "Failed to remove password authentication: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_command_test) {
+  int res;
+  pr_redis_t *redis;
+  array_header *args;
+
+  mark_point();
+  res = pr_redis_command(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_command(redis, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null args");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  args = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_command(redis, args, 0);
+  fail_unless(res < 0, "Failed to handle empty args");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((char **) push_array(args)) = pstrdup(p, "FOO");
+
+  mark_point();
+  res = pr_redis_command(redis, args, -1);
+  fail_unless(res < 0, "Failed to handle invalid reply type");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_command(redis, args, PR_REDIS_REPLY_TYPE_ERROR);
+  fail_unless(res == 0, "Failed to handle invalid command with error: %s",
+    strerror(errno));
+
+  args = make_array(p, 0, sizeof(char *));
+  *((char **) push_array(args)) = pstrdup(p, "COMMAND");
+  *((char **) push_array(args)) = pstrdup(p, "COUNT");
+
+  mark_point();
+  res = pr_redis_command(redis, args, PR_REDIS_REPLY_TYPE_INTEGER);
+  fail_unless(res == 0, "Failed to handle valid command with integer: %s",
+    strerror(errno));
+
+  args = make_array(p, 0, sizeof(char *));
+  *((char **) push_array(args)) = pstrdup(p, "INFO");
+
+  mark_point();
+  res = pr_redis_command(redis, args, PR_REDIS_REPLY_TYPE_STRING);
+  fail_unless(res == 0, "Failed to handle valid command with array: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_remove_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+
+  mark_point();
+  res = pr_redis_remove(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res < 0, "Unexpectedly removed key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_add_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  time_t expires;
+
+  mark_point();
+  res = pr_redis_add(NULL, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_add(redis, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_add(redis, &m, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_add(redis, &m, key, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_add(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to add key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  expires = 3;
+
+  mark_point();
+  res = pr_redis_add(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to add key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_add_with_namespace_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *prefix, *key;
+  char *val;
+  size_t prefixsz, valsz;
+  time_t expires;
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  prefix = "test.";
+  prefixsz = strlen(prefix);
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, prefix, prefixsz);
+  fail_unless(res == 0, "Failed to set namespace prefix '%s': %s", prefix,
+    strerror(errno));
+
+  key = "key";
+  val = "val";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_add(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to add key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, NULL, 0);
+  fail_unless(res == 0, "Failed to set null namespace prefix: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_get_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  time_t expires;
+  void *data;
+
+  mark_point();
+  data = pr_redis_get(NULL, NULL, NULL, NULL, NULL);
+  fail_unless(data == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  data = pr_redis_get(p, NULL, NULL, NULL, NULL);
+  fail_unless(data == NULL, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  data = pr_redis_get(p, redis, NULL, NULL, NULL);
+  fail_unless(data == NULL, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  data = pr_redis_get(p, redis, &m, NULL, NULL);
+  fail_unless(data == NULL, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  data = pr_redis_get(p, redis, &m, key, NULL);
+  fail_unless(data == NULL, "Failed to handle null valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  data = pr_redis_get(p, redis, &m, key, &valsz);
+  fail_unless(data == NULL, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "Hello, World!";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  valsz = 0;
+
+  mark_point();
+  data = pr_redis_get(p, redis, &m, key, &valsz);
+  fail_unless(data != NULL, "Failed to get data for key '%s': %s", key,
+    strerror(errno));
+  fail_unless(valsz == strlen(val), "Expected %lu, got %lu",
+    (unsigned long) strlen(val), (unsigned long) valsz);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  data = pr_redis_get(p, redis, &m, key, &valsz);
+  fail_unless(data == NULL, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_get_with_namespace_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *prefix, *key;
+  char *val;
+  size_t prefixsz, valsz;
+  time_t expires;
+  void *data;
+
+  /* set a value, set the namespace, get it. */
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  prefix = "prefix.";
+  prefixsz = strlen(prefix);
+
+  key = "prefix.testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  val = "Hello, World!";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_set_namespace(redis, &m, prefix, prefixsz);
+  fail_unless(res == 0, "Failed to set namespace prefix '%s': %s", prefix,
+    strerror(errno));
+
+  key = "testkey";
+  valsz = 0;
+
+  mark_point();
+  data = pr_redis_get(p, redis, &m, key, &valsz);
+  fail_unless(data != NULL, "Failed to get data for key '%s': %s", key,
+    strerror(errno));
+  fail_unless(valsz == strlen(val), "Expected %lu, got %lu",
+    (unsigned long) strlen(val), (unsigned long) valsz);
+  fail_unless(strncmp(data, val, valsz) == 0, "Expected '%s', got '%.*s'",
+    val, (int) valsz, data);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_get_str_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  size_t valsz;
+  time_t expires;
+  char *val, *str;
+
+  mark_point();
+  str = pr_redis_get_str(NULL, NULL, NULL, NULL);
+  fail_unless(str == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  str = pr_redis_get_str(p, NULL, NULL, NULL);
+  fail_unless(str == NULL, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  str = pr_redis_get_str(p, redis, NULL, NULL);
+  fail_unless(str == NULL, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  str = pr_redis_get_str(p, redis, &m, NULL);
+  fail_unless(str == NULL, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "test_string";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  str = pr_redis_get_str(p, redis, &m, key);
+  fail_unless(str == NULL, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "Hello, World!";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  str = pr_redis_get_str(p, redis, &m, key);
+  fail_unless(str != NULL, "Failed to get string for key '%s': %s", key,
+    strerror(errno));
+  fail_unless(strlen(str) == strlen(val), "Expected %lu, got %lu",
+    (unsigned long) strlen(val), (unsigned long) strlen(str));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  str = pr_redis_get_str(p, redis, &m, key);
+  fail_unless(str == NULL, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_incr_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *value;
+  uint32_t incr;
+  uint64_t val = 0;
+  size_t valsz;
+  time_t expires;
+
+  mark_point();
+  res = pr_redis_incr(NULL, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_incr(redis, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_incr(redis, &m, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testval";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_incr(redis, &m, key, 0, NULL);
+  fail_unless(res < 0, "Failed to handle zero incr");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  incr = 2;
+
+  mark_point();
+  res = pr_redis_incr(redis, &m, key, incr, NULL);
+  fail_unless(res < 0, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Note: Yes, Redis wants a string, NOT the actual bytes.  Makes sense,
+   * I guess, given its text-based protocol.
+   */
+  value = "31";
+  valsz = strlen(value);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, value, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_incr(redis, &m, key, incr, NULL);
+  fail_unless(res == 0, "Failed to increment key '%s' by %lu: %s", key,
+    (unsigned long) incr, strerror(errno));
+
+  val = 0;
+
+  mark_point();
+  res = pr_redis_incr(redis, &m, key, incr, &val);
+  fail_unless(res == 0, "Failed to increment key '%s' by %lu: %s", key,
+    (unsigned long) incr, strerror(errno));
+  fail_unless(val == 35, "Expected %lu, got %lu", 35, (unsigned long) val);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  /* Now, let's try incrementing a non-numeric value. */
+  value = "Hello, World!";
+  valsz = strlen(value);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, value, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_incr(redis, &m, key, incr, &val);
+  fail_unless(res < 0, "Failed to handle non-numeric key value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_decr_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *value;
+  uint32_t decr;
+  uint64_t val = 0;
+  size_t valsz;
+  time_t expires;
+
+  mark_point();
+  res = pr_redis_decr(NULL, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_decr(redis, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_decr(redis, &m, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testval";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_decr(redis, &m, key, 0, NULL);
+  fail_unless(res < 0, "Failed to handle zero decr");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  decr = 5;
+
+  mark_point();
+  res = pr_redis_decr(redis, &m, key, decr, NULL);
+  fail_unless(res < 0, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Note: Yes, Redis wants a string, NOT the actual bytes.  Makes sense,
+   * I guess, given its text-based protocol.
+   */
+  value = "31";
+  valsz = strlen(value);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, value, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_decr(redis, &m, key, decr, NULL);
+  fail_unless(res == 0, "Failed to decrement key '%s' by %lu: %s", key,
+    (unsigned long) decr, strerror(errno));
+
+  val = 0;
+
+  mark_point();
+  res = pr_redis_decr(redis, &m, key, decr, &val);
+  fail_unless(res == 0, "Failed to decrement key '%s' by %lu: %s", key,
+    (unsigned long) decr, strerror(errno));
+  fail_unless(val == 21, "Expected %lu, got %lu", 21, (unsigned long) val);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  /* Now, let's try decrementing a non-numeric value. */
+  value = "Hello, World!";
+  valsz = strlen(value);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, value, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_decr(redis, &m, key, decr, &val);
+  fail_unless(res < 0, "Failed to handle non-numeric key value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_rename_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *from, *to;
+  char *val;
+  size_t valsz;
+  time_t expires;
+
+  mark_point();
+  res = pr_redis_rename(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_rename(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_rename(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null from");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  from = "fromkey";
+
+  mark_point();
+  res = pr_redis_rename(redis, &m, from, NULL);
+  fail_unless(res < 0, "Failed to handle null to");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  to = "tokey";
+
+  mark_point();
+  res = pr_redis_rename(redis, &m, from, to);
+  fail_unless(res < 0, "Failed to handle nonexistent from key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, from, val, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", from, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_rename(redis, &m, from, to);
+  fail_unless(res == 0, "Failed to rename '%s' to '%s': %s", from, to,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, to);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", to, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  time_t expires;
+
+  mark_point();
+  res = pr_redis_set(NULL, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set(redis, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set(redis, &m, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = strlen(val);
+  expires = 0;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  expires = 3;
+
+  mark_point();
+  res = pr_redis_set(redis, &m, key, val, valsz, expires);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_remove_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+
+  mark_point();
+  res = pr_redis_hash_remove(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_remove(redis, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_remove(redis, &m, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_hash_remove(redis, &m, key);
+  fail_unless(res < 0, "Unexpectedly removed key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_get_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_hash_get(NULL, NULL, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_get(p, NULL, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_get(p, redis, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_get(p, redis, &m, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_get(p, redis, &m, key, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null field");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  field = "hashfield";
+
+  mark_point();
+  res = pr_redis_hash_get(p, redis, &m, key, field, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_get(p, redis, &m, key, field, (void **) &val, &valsz);
+  fail_unless(res < 0, "Failed to handle nonexistent item");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_set_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_hash_set(NULL, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_set(redis, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null field");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  field = "hashfield";
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "hashval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, 0);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_hash_get(p, redis, &m, key, field, (void **) &val, &valsz);
+  fail_unless(res == 0, "Failed to get item: %s", strerror(errno));
+  fail_unless(valsz == 7, "Expected item length 7, got %lu",
+    (unsigned long) valsz);
+  fail_unless(val != NULL, "Failed to get value from hash");
+  fail_unless(strncmp(val, "hashval", valsz) == 0,
+    "Expected 'hashval', got '%.*s'", (int) valsz, val);
+
+  mark_point();
+  res = pr_redis_hash_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove hash: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_delete_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_hash_delete(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_delete(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_delete(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_delete(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null field");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  field = "hashfield";
+
+  mark_point();
+  res = pr_redis_hash_delete(redis, &m, key, field);
+  fail_unless(res < 0, "Failed to handle nonexistent field");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "hashval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_delete(redis, &m, key, field);
+  fail_unless(res == 0, "Failed to delete field: %s", strerror(errno));
+
+  /* Note that we add this item back, just so that the hash is NOT empty when
+   * we go to remove it entirely.
+   */
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_count_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  uint64_t count = 0;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_hash_count(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_count(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_count(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_count(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to get count using key '%s': %s", key,
+    strerror(errno));
+
+  field = "hashfield";
+  val = "hashval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to get count: %s", strerror(errno));
+  fail_unless(count == 1, "Expected 1, got %lu", (unsigned long) count);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_exists_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_hash_exists(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_exists(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_exists(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_exists(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null field");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  field = "hashfield";
+
+  mark_point();
+  res = pr_redis_hash_exists(redis, &m, key, field);
+  fail_unless(res == FALSE, "Failed to handle nonexistent field");
+
+  val = "hashval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_exists(redis, &m, key, field);
+  fail_unless(res == TRUE, "Failed to handle existing field");
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_incr_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  int64_t num;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_hash_incr(NULL, NULL, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, NULL, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, &m, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, &m, key, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null field");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  field = "hashfield";
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, &m, key, field, 0, NULL);
+  fail_unless(res < 0, "Failed to handle nonexistent field");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "1";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, &m, key, field, 0, NULL);
+  fail_unless(res == 0, "Failed to handle existing field: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, &m, key, field, 1, &num);
+  fail_unless(res == 0, "Failed to handle existing field: %s", strerror(errno));
+  fail_unless(num == 2, "Expected 2, got %lu", (unsigned long) num);
+
+  mark_point();
+  res = pr_redis_hash_incr(redis, &m, key, field, -3, &num);
+  fail_unless(res == 0, "Failed to handle existing field: %s", strerror(errno));
+  fail_unless(num == -1, "Expected -1, got %lu", (unsigned long) num);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_keys_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+  array_header *fields = NULL;
+
+  mark_point();
+  res = pr_redis_hash_keys(NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_keys(p, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_keys(p, redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_keys(p, redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_keys(p, redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null fields");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_keys(p, redis, &m, key, &fields);
+  fail_unless(res < 0, "Failed to handle nonexistent fields");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Add some fields */
+
+  field = "foo";
+  val = "1";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  field = "bar";
+  val = "baz quxx";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  fields = NULL;
+
+  mark_point();
+  res = pr_redis_hash_keys(p, redis, &m, key, &fields);
+  fail_unless(res == 0, "Failed to handle existing fields: %s", strerror(errno));
+  fail_unless(fields != NULL);
+  fail_unless(fields->nelts == 2, "Expected 2, got %u", fields->nelts);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_values_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+  array_header *values = NULL;
+
+  mark_point();
+  res = pr_redis_hash_values(NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_values(p, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_values(p, redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_values(p, redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_values(p, redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_values(p, redis, &m, key, &values);
+  fail_unless(res < 0, "Failed to handle nonexistent values");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Add some fields */
+
+  field = "foo";
+  val = "1";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  field = "bar";
+  val = "baz quxx";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  values = NULL;
+
+  mark_point();
+  res = pr_redis_hash_values(p, redis, &m, key, &values);
+  fail_unless(res == 0, "Failed to handle existing values: %s", strerror(errno));
+  fail_unless(values != NULL);
+  fail_unless(values->nelts == 2, "Expected 2, got %u", values->nelts);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_getall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+  pr_table_t *hash = NULL;
+
+  mark_point();
+  res = pr_redis_hash_getall(NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_getall(p, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_getall(p, redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_getall(p, redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_getall(p, redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null hash");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_getall(p, redis, &m, key, &hash);
+  fail_unless(res < 0, "Failed to handle nonexistent hash");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  /* Add some fields */
+
+  field = "foo";
+  val = "1";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  field = "bar";
+  val = "baz quxx";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_hash_set(redis, &m, key, field, val, valsz);
+  fail_unless(res == 0, "Failed to set item: %s", strerror(errno));
+
+  hash = NULL;
+
+  mark_point();
+  res = pr_redis_hash_getall(p, redis, &m, key, &hash);
+  fail_unless(res == 0, "Failed to handle existing fields: %s", strerror(errno));
+  fail_unless(hash != NULL);
+  res = pr_table_count(hash);
+  fail_unless(res == 2, "Expected 2, got %d", res);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_hash_setall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key, *field;
+  char *val;
+  size_t valsz;
+  pr_table_t *hash = NULL;
+  uint64_t count = 0;
+
+  mark_point();
+  res = pr_redis_hash_setall(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_setall(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_hash_setall(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testhashkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_hash_setall(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null hash");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  hash = pr_table_alloc(p, 0);
+
+  mark_point();
+  res = pr_redis_hash_setall(redis, &m, key, hash);
+  fail_unless(res < 0, "Failed to handle empty hash");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Add some fields */
+  field = "foo";
+  val = "1";
+  valsz = strlen(val);
+  (void) pr_table_add_dup(hash, pstrdup(p, field), val, valsz);
+
+  field = "bar";
+  val = "baz quxx";
+  valsz = strlen(val);
+  (void) pr_table_add_dup(hash, pstrdup(p, field), val, valsz);
+
+  mark_point();
+  res = pr_redis_hash_setall(redis, &m, key, hash);
+  fail_unless(res == 0, "Failed to set hash: %s", strerror(errno));
+
+  mark_point();
+  res = pr_redis_hash_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to count hash: %s", strerror(errno));
+  fail_unless(count == 2, "Expected 2, got %lu", (unsigned long) count);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_remove_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+
+  mark_point();
+  res = pr_redis_list_remove(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res < 0, "Failed to handle nonexistent list");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_append_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_append(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_append(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "Some JSON here";
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, 0);
+  fail_unless(res < 0, "Failed to handle empty value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  (void) pr_redis_list_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append to list '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_count_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  uint64_t count = 0;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_count(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_count(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_count(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_count(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to get list count: %s", strerror(errno));
+  fail_unless(count == 0, "Expected 0, got %lu", (unsigned long) count);
+
+  val = "Some JSON here";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append to list '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to get list count: %s", strerror(errno));
+  fail_unless(count == 1, "Expected 1, got %lu", (unsigned long) count);
+
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_delete_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_delete(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_delete(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_delete(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_delete(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "Some JSON here";
+
+  mark_point();
+  res = pr_redis_list_delete(redis, &m, key, val, 0);
+  fail_unless(res < 0, "Failed to handle empty value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  (void) pr_redis_list_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_delete(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle nonexistent items");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append to list '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_delete(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to handle existing items");
+
+  /* Note that we add this item back, just so that the list is NOT empty when
+   * we go to remove it entirely.
+   */
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append to list '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_exists_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_exists(NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_exists(redis, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_exists(redis, &m, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_exists(redis, &m, key, 0);
+  fail_unless(res == FALSE, "Failed to handle nonexistent item");
+
+  val = "testval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_exists(redis, &m, key, 0);
+  fail_unless(res == TRUE, "Failed to handle existing item");
+
+  mark_point();
+  res = pr_redis_list_exists(redis, &m, key, 3);
+  fail_unless(res < 0, "Failed to handle invalid index");
+  fail_unless(errno == ERANGE, "Expected ERANGE (%d), got %s (%d)", ERANGE,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_get_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_get(NULL, NULL, NULL, NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_get(p, NULL, NULL, NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_get(p, redis, NULL, NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_get(p, redis, &m, NULL, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_get(p, redis, &m, key, 0, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_get(p, redis, &m, key, 0, (void **) &val, NULL);
+  fail_unless(res < 0, "Failed to handle null valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item to key '%s': %s", key,
+    strerror(errno));
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_get(p, redis, &m, key, 3, (void **) &val, &valsz);
+  fail_unless(res < 0, "Failed to handle invalid index");
+  fail_unless(errno == ERANGE, "Expected ERANGE (%d), got %s (%d)", ERANGE,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_get(p, redis, &m, key, 0, (void **) &val, &valsz);
+  fail_unless(res == 0, "Failed to get item in list: %s", strerror(errno));
+  fail_unless(val != NULL, "Expected value, got null");
+  fail_unless(valsz == 3, "Expected 3, got %lu", (unsigned long) valsz);
+  fail_unless(strncmp(val, "foo", 3) == 0, "Expected 'foo', got '%.*s'",
+    (int) valsz, val);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_getall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  array_header *values = NULL, *valueszs = NULL;
+
+  mark_point();
+  res = pr_redis_list_getall(NULL, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_getall(p, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_getall(p, redis, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_getall(p, redis, &m, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_getall(p, redis, &m, key, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_getall(p, redis, &m, key, &values, NULL);
+  fail_unless(res < 0, "Failed to handle null valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item to key '%s': %s", key,
+    strerror(errno));
+
+  val = "bar";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item to key '%s': %s", key,
+    strerror(errno));
+
+  val = "baz";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_getall(p, redis, &m, key, &values, &valueszs);
+  fail_unless(res == 0, "Failed to get items in list: %s", strerror(errno));
+  fail_unless(values != NULL, "Expected values, got null");
+  fail_unless(valueszs != NULL, "Expected valueszs, got null");
+  fail_unless(values->nelts == 3, "Expected 3, got %u", values->nelts);
+  fail_unless(valueszs->nelts == 3, "Expected 3, got %u", valueszs->nelts);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_pop_params_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_pop(NULL, NULL, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_pop(p, NULL, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, (void **) &val, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, (void **) &val, &valsz, 0);
+  fail_unless(res < 0, "Failed to handle invalid flags");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_pop_left_test) {
+  int res, flags = PR_REDIS_LIST_FL_LEFT;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, (void **) &val, &valsz, flags);
+  fail_unless(res < 0, "Failed to handle nonexistent list");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item to key '%s': %s", key,
+    strerror(errno));
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, (void **) &val, &valsz, flags);
+  fail_unless(res == 0, "Failed to get item in list: %s", strerror(errno));
+  fail_unless(val != NULL, "Expected value, got null");
+  fail_unless(valsz == 3, "Expected 3, got %lu", (unsigned long) valsz);
+  fail_unless(strncmp(val, "foo", 3) == 0, "Expected 'foo', got '%.*s'",
+    (int) valsz, val);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_pop_right_test) {
+  int res, flags = PR_REDIS_LIST_FL_RIGHT;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, (void **) &val, &valsz, flags);
+  fail_unless(res < 0, "Failed to handle nonexistent list");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item to key '%s': %s", key,
+    strerror(errno));
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_pop(p, redis, &m, key, (void **) &val, &valsz, flags);
+  fail_unless(res == 0, "Failed to get item in list: %s", strerror(errno));
+  fail_unless(val != NULL, "Expected value, got null");
+  fail_unless(valsz == 3, "Expected 3, got %lu", (unsigned long) valsz);
+  fail_unless(strncmp(val, "foo", 3) == 0, "Expected 'foo', got '%.*s'",
+    (int) valsz, val);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_push_params_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_push(NULL, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_push(redis, NULL, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_push(redis, &m, NULL, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_list_push(redis, &m, key, NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_push(redis, &m, key, val, 0, 0);
+  fail_unless(res < 0, "Failed to handle empty valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_push(redis, &m, key, val, valsz, 0);
+  fail_unless(res < 0, "Failed to handle invalid flags");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_push_left_test) {
+  int res, flags = PR_REDIS_LIST_FL_LEFT;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  val = "Some JSON here";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_push(redis, &m, key, val, valsz, flags);
+  fail_unless(res == 0, "Failed to append to list '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_push_right_test) {
+  int res, flags = PR_REDIS_LIST_FL_RIGHT;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  val = "Some JSON here";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_push(redis, &m, key, val, valsz, flags);
+  fail_unless(res == 0, "Failed to append to list '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_rotate_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val = NULL;
+  size_t valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_rotate(NULL, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_rotate(p, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, key, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, key, (void **) &val, NULL);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, key, (void **) &val, &valsz);
+  fail_unless(res < 0, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item using key '%s': %s", key,
+    strerror(errno));
+
+  val = "bar";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item using key '%s': %s", key,
+    strerror(errno));
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, key, (void **) &val, &valsz);
+  fail_unless(res == 0, "Failed to rotate list '%s': %s", key, strerror(errno));
+  fail_unless(val != NULL, "Expected value, got NULL");
+  fail_unless(valsz == 3, "Expected 3, got %lu", (unsigned long) valsz);
+  fail_unless(strncmp(val, "bar", valsz) == 0, "Expected 'bar', got '%.*s'",
+    (int) valsz, val);
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, key, (void **) &val, &valsz);
+  fail_unless(res == 0, "Failed to rotate list '%s': %s", key, strerror(errno));
+  fail_unless(val != NULL, "Expected value, got NULL");
+  fail_unless(valsz == 3, "Expected 3, got %lu", (unsigned long) valsz);
+  fail_unless(strncmp(val, "foo", valsz) == 0, "Expected 'foo', got '%.*s'",
+    (int) valsz, val);
+
+  val = NULL;
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_list_rotate(p, redis, &m, key, (void **) &val, &valsz);
+  fail_unless(res == 0, "Failed to rotate list '%s': %s", key, strerror(errno));
+  fail_unless(val != NULL, "Expected value, got NULL");
+  fail_unless(valsz == 3, "Expected 3, got %lu", (unsigned long) valsz);
+  fail_unless(strncmp(val, "bar", valsz) == 0, "Expected 'bar', got '%.*s'",
+    (int) valsz, val);
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_set_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_list_set(NULL, NULL, NULL, 0, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_set(redis, NULL, NULL, 0, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_set(redis, &m, NULL, 0, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_set(redis, &m, key, 0, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "Some JSON here";
+
+  mark_point();
+  res = pr_redis_list_set(redis, &m, key, 0, val, 0);
+  fail_unless(res < 0, "Failed to handle empty value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  (void) pr_redis_list_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_set(redis, &m, key, 3, val, valsz);
+  fail_unless(res < 0, "Failed to handle invalid index");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_set(redis, &m, key, 0, val, valsz);
+  fail_unless(res < 0, "Failed to handle invalid index");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Append the item first, then set it. */
+
+  mark_point();
+  res = pr_redis_list_append(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to append item using key '%s': %s", key,
+    strerror(errno));
+
+  val = "listval2";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_list_set(redis, &m, key, 0, val, valsz);
+  fail_unless(res == 0, "Failed to set item at index 0 using key '%s': %s",
+    key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_list_setall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  array_header *vals, *valszs;
+
+  mark_point();
+  res = pr_redis_list_setall(NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_setall(redis, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testlistkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, key, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  vals = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, key, vals, NULL);
+  fail_unless(res < 0, "Failed to handle empty values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((char **) push_array(vals)) = pstrdup(p, "Some JSON here");
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, key, vals, NULL);
+  fail_unless(res < 0, "Failed to handle null valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valszs = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, key, vals, valszs);
+  fail_unless(res < 0, "Failed to handle empty valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((size_t *) push_array(valszs)) = strlen("Some JSON here");
+  *((char **) push_array(vals)) = pstrdup(p, "bar");
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, key, vals, valszs);
+  fail_unless(res < 0, "Failed to handle mismatched values/valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((size_t *) push_array(valszs)) = strlen("bar");
+
+  mark_point();
+  (void) pr_redis_list_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_list_setall(redis, &m, key, vals, valszs);
+  fail_unless(res == 0, "Failed to set items using key '%s': %s",
+    key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_list_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove list '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_remove_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+
+  mark_point();
+  res = pr_redis_set_remove(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_remove(redis, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_remove(redis, &m, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_set_remove(redis, &m, key);
+  fail_unless(res < 0, "Unexpectedly removed key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_exists_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_set_exists(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_exists(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_exists(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_exists(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_set_exists(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_exists(redis, &m, key, val, valsz);
+  fail_unless(res == FALSE, "Failed to handle nonexistent item");
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_exists(redis, &m, key, val, valsz);
+  fail_unless(res == TRUE, "Failed to handle existing item");
+
+  mark_point();
+  res = pr_redis_set_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_add_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_set_add(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_add(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_set_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, 0);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle duplicates");
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_count_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  uint64_t count;
+  void *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_set_count(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_count(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_count(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_count(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to handle get set count: %s", strerror(errno));
+  fail_unless(count == 0, "Expected 0, got %lu", (unsigned long) count);
+
+  val = "testval";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to handle get set count: %s", strerror(errno));
+  fail_unless(count == 1, "Expected 1, got %lu", (unsigned long) count);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_delete_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_set_delete(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_delete(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_delete(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_delete(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_set_delete(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_delete(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle nonexistent item");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_delete(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to delete item from set: %s", strerror(errno));
+
+  /* Note that we add this item back, just so that the set is NOT empty when
+   * we go to remove it entirely.
+   */
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_getall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  array_header *values = NULL, *valueszs = NULL;
+
+  mark_point();
+  res = pr_redis_set_getall(NULL, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_getall(p, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_getall(p, redis, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_getall(p, redis, &m, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_getall(p, redis, &m, key, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_getall(p, redis, &m, key, &values, NULL);
+  fail_unless(res < 0, "Failed to handle null valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  val = "bar";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  val = "baz";
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_set_add(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_getall(p, redis, &m, key, &values, &valueszs);
+  fail_unless(res == 0, "Failed to get items in set: %s", strerror(errno));
+  fail_unless(values != NULL, "Expected values, got null");
+  fail_unless(valueszs != NULL, "Expected valueszs, got null");
+  fail_unless(values->nelts == 3, "Expected 3, got %u", values->nelts);
+  fail_unless(valueszs->nelts == 3, "Expected 3, got %u", valueszs->nelts);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_set_setall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  array_header *vals, *valszs;
+
+  mark_point();
+  res = pr_redis_set_setall(NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_setall(redis, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testsetkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, key, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  vals = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, key, vals, NULL);
+  fail_unless(res < 0, "Failed to handle empty values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((char **) push_array(vals)) = pstrdup(p, "Some JSON here");
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, key, vals, NULL);
+  fail_unless(res < 0, "Failed to handle null valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valszs = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, key, vals, valszs);
+  fail_unless(res < 0, "Failed to handle empty valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((size_t *) push_array(valszs)) = strlen("Some JSON here");
+  *((char **) push_array(vals)) = pstrdup(p, "bar");
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, key, vals, valszs);
+  fail_unless(res < 0, "Failed to handle mismatched values/valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((size_t *) push_array(valszs)) = strlen("bar");
+
+  mark_point();
+  (void) pr_redis_set_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_set_setall(redis, &m, key, vals, valszs);
+  fail_unless(res == 0, "Failed to set items using key '%s': %s",
+    key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove set '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_remove_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+
+  mark_point();
+  res = pr_redis_sorted_set_remove(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_remove(redis, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_remove(redis, &m, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+
+  mark_point();
+  res = pr_redis_sorted_set_remove(redis, &m, key);
+  fail_unless(res < 0, "Unexpectedly removed key '%s'", key);
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_exists_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(redis, &m, key, val, valsz);
+  fail_unless(res == FALSE, "Failed to handle nonexistent item");
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, 1.0);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_exists(redis, &m, key, val, valsz);
+  fail_unless(res == TRUE, "Failed to handle existing item");
+
+  mark_point();
+  res = pr_redis_sorted_set_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_add_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  float score;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(NULL, NULL, NULL, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, NULL, NULL, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, NULL, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+  score = 75.32;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res < 0, "Failed to handle duplicates");
+  fail_unless(errno == EEXIST, "Expected EEXIST (%d), got %s (%d)", EEXIST,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_count_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  uint64_t count;
+  void *val;
+  size_t valsz;
+  float score;
+
+  mark_point();
+  res = pr_redis_sorted_set_count(NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_count(redis, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_count(redis, &m, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_count(redis, &m, key, NULL);
+  fail_unless(res < 0, "Failed to handle null count");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to handle get sorted set count: %s",
+    strerror(errno));
+  fail_unless(count == 0, "Expected 0, got %lu", (unsigned long) count);
+
+  val = "testval";
+  valsz = strlen(val);
+  score = 23.45;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_count(redis, &m, key, &count);
+  fail_unless(res == 0, "Failed to handle get set count: %s", strerror(errno));
+  fail_unless(count == 1, "Expected 1, got %lu", (unsigned long) count);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_delete_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  float score;
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(NULL, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(redis, NULL, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(redis, &m, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(redis, &m, key, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(redis, &m, key, val, valsz);
+  fail_unless(res < 0, "Failed to handle nonexistent item");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  score = 1.23;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_delete(redis, &m, key, val, valsz);
+  fail_unless(res == 0, "Failed to delete item from set: %s", strerror(errno));
+
+  /* Note that we add this item back, just so that the set is NOT empty when
+   * we go to remove it entirely.
+   */
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_getn_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  float score;
+  array_header *values = NULL, *valueszs = NULL;
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(NULL, NULL, NULL, NULL, 0, 0, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, NULL, NULL, NULL, 0, 0, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, NULL, NULL, 0, 0, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, NULL, 0, 0, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, key, 0, 0, NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, key, 0, 0, &values, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, key, 0, 0, &values, &valueszs,
+    0);
+  fail_unless(res < 0, "Failed to handle invalid flags value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "foo";
+  valsz = strlen(val);
+  score = 0.123;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  val = "bar";
+  valsz = strlen(val);
+  score = -1.56;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  val = "baz";
+  valsz = strlen(val);
+  score = 234235.1;
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add item to key '%s': %s", key,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, key, 0, 3, &values, &valueszs,
+    PR_REDIS_SORTED_SET_FL_DESC);
+  fail_unless(res == 0, "Failed to get items in sorted set: %s",
+    strerror(errno));
+  fail_unless(values != NULL, "Expected values, got null");
+  fail_unless(valueszs != NULL, "Expected valueszs, got null");
+  fail_unless(values->nelts == 3, "Expected 3, got %u", values->nelts);
+  fail_unless(valueszs->nelts == 3, "Expected 3, got %u", valueszs->nelts);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, key, 1, 2, &values, &valueszs,
+    PR_REDIS_SORTED_SET_FL_ASC);
+  fail_unless(res == 0, "Failed to get items in sorted set: %s",
+    strerror(errno));
+  fail_unless(values != NULL, "Expected values, got null");
+  fail_unless(valueszs != NULL, "Expected valueszs, got null");
+  fail_unless(values->nelts == 2, "Expected 2, got %u", values->nelts);
+  fail_unless(valueszs->nelts == 2, "Expected 2, got %u", valueszs->nelts);
+
+  mark_point();
+  res = pr_redis_sorted_set_getn(p, redis, &m, key, 1, 10, &values, &valueszs,
+    PR_REDIS_SORTED_SET_FL_ASC);
+  fail_unless(res == 0, "Failed to get items in sorted set: %s",
+    strerror(errno));
+  fail_unless(values != NULL, "Expected values, got null");
+  fail_unless(valueszs != NULL, "Expected valueszs, got null");
+  fail_unless(values->nelts == 2, "Expected 2, got %u", values->nelts);
+  fail_unless(valueszs->nelts == 2, "Expected 2, got %u", valueszs->nelts);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_incr_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  float incr, curr;
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(NULL, NULL, NULL, NULL, 0, 0.0, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, NULL, NULL, NULL, 0, 0.0, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, &m, NULL, NULL, 0, 0.0, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testval";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, &m, key, NULL, 0, 0.0, NULL);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "foo";
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, &m, key, val, 0, 0.0, NULL);
+  fail_unless(res < 0, "Failed to handle empty value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+  incr = 2.0;
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, &m, key, val, valsz, incr, NULL);
+  fail_unless(res < 0, "Failed to handle null current value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, &m, key, val, valsz, incr, &curr);
+  fail_unless(res < 0, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, incr);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_incr(redis, &m, key, val, valsz, -incr, &curr);
+  fail_unless(res == 0, "Failed to increment key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_score_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  float score;
+
+  mark_point();
+  res = pr_redis_sorted_set_score(NULL, NULL, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, NULL, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, &m, NULL, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testval";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, &m, key, NULL, 0, NULL);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "foo";
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, &m, key, val, 0, NULL);
+  fail_unless(res < 0, "Failed to handle empty value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, &m, key, val, valsz, NULL);
+  fail_unless(res < 0, "Failed to handle null score");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, &m, key, val, valsz, &score);
+  fail_unless(res < 0, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, 1.0);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_score(redis, &m, key, val, valsz, &score);
+  fail_unless(res == 0, "Failed to score key '%s', val '%s': %s", key, val,
+    strerror(errno));
+  fail_unless(score > 0.0, "Expected > 0.0, got %0.3f", score);
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_set_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  char *val;
+  size_t valsz;
+  float score;
+
+  mark_point();
+  res = pr_redis_sorted_set_set(NULL, NULL, NULL, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_set(redis, NULL, NULL, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_set(redis, &m, NULL, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_set(redis, &m, key, NULL, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle null value");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  val = "testval";
+  valsz = 0;
+
+  mark_point();
+  res = pr_redis_sorted_set_set(redis, &m, key, val, 0, 0.0);
+  fail_unless(res < 0, "Failed to handle zero valuesz");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valsz = strlen(val);
+  score = 75.32;
+
+  mark_point();
+  res = pr_redis_sorted_set_set(redis, &m, key, val, valsz, score);
+  fail_unless(res < 0, "Failed to handle nonexistent key");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_add(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to add key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  score = 23.11;
+
+  mark_point();
+  res = pr_redis_sorted_set_set(redis, &m, key, val, valsz, score);
+  fail_unless(res == 0, "Failed to set key '%s', val '%s': %s", key, val,
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove key '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (redis_sorted_set_setall_test) {
+  int res;
+  pr_redis_t *redis;
+  module m;
+  const char *key;
+  array_header *vals, *valszs, *scores;
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(NULL, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null redis");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  redis = pr_redis_conn_new(p, NULL, 0);
+  fail_unless(redis != NULL, "Failed to open connection to Redis: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, NULL, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null module");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, NULL, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null key");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  key = "testsetkey";
+  (void) pr_redis_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  vals = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle empty values");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((char **) push_array(vals)) = pstrdup(p, "Some JSON here");
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  valszs = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, valszs, NULL);
+  fail_unless(res < 0, "Failed to handle empty valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((size_t *) push_array(valszs)) = strlen("Some JSON here");
+  *((char **) push_array(vals)) = pstrdup(p, "bar");
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, valszs, NULL);
+  fail_unless(res < 0, "Failed to handle mismatched values/valueszs");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((size_t *) push_array(valszs)) = strlen("bar");
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, valszs, NULL);
+  fail_unless(res < 0, "Failed to handle null scores");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  scores = make_array(p, 0, sizeof(char *));
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, valszs, scores);
+  fail_unless(res < 0, "Failed to handle empty scores");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((float *) push_array(scores)) = 1.0;
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, valszs, scores);
+  fail_unless(res < 0, "Failed to handle mismatched values/scores");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  *((float *) push_array(scores)) = 2.0;
+
+  mark_point();
+  (void) pr_redis_set_remove(redis, &m, key);
+
+  mark_point();
+  res = pr_redis_sorted_set_setall(redis, &m, key, vals, valszs, scores);
+  fail_unless(res == 0, "Failed to set items using key '%s': %s",
+    key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_set_remove(redis, &m, key);
+  fail_unless(res == 0, "Failed to remove set '%s': %s", key, strerror(errno));
+
+  mark_point();
+  res = pr_redis_conn_destroy(redis);
+  fail_unless(res == TRUE, "Failed to close redis: %s", strerror(errno));
+}
+END_TEST
+
+#endif /* PR_USE_REDIS */
+
+Suite *tests_get_redis_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("redis");
+  testcase = tcase_create("base");
+
+#ifdef PR_USE_REDIS
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, redis_conn_destroy_test);
+  tcase_add_test(testcase, redis_conn_close_test);
+  tcase_add_test(testcase, redis_conn_new_test);
+  tcase_add_test(testcase, redis_conn_get_test);
+  tcase_add_test(testcase, redis_conn_set_namespace_test);
+  tcase_add_test(testcase, redis_conn_auth_test);
+  tcase_add_test(testcase, redis_command_test);
+
+  tcase_add_test(testcase, redis_remove_test);
+  tcase_add_test(testcase, redis_add_test);
+  tcase_add_test(testcase, redis_add_with_namespace_test);
+  tcase_add_test(testcase, redis_get_test);
+  tcase_add_test(testcase, redis_get_with_namespace_test);
+  tcase_add_test(testcase, redis_get_str_test);
+  tcase_add_test(testcase, redis_incr_test);
+  tcase_add_test(testcase, redis_decr_test);
+  tcase_add_test(testcase, redis_rename_test);
+  tcase_add_test(testcase, redis_set_test);
+
+  tcase_add_test(testcase, redis_hash_remove_test);
+  tcase_add_test(testcase, redis_hash_get_test);
+  tcase_add_test(testcase, redis_hash_set_test);
+  tcase_add_test(testcase, redis_hash_delete_test);
+  tcase_add_test(testcase, redis_hash_count_test);
+  tcase_add_test(testcase, redis_hash_exists_test);
+  tcase_add_test(testcase, redis_hash_incr_test);
+  tcase_add_test(testcase, redis_hash_keys_test);
+  tcase_add_test(testcase, redis_hash_values_test);
+  tcase_add_test(testcase, redis_hash_getall_test);
+  tcase_add_test(testcase, redis_hash_setall_test);
+
+  tcase_add_test(testcase, redis_list_remove_test);
+  tcase_add_test(testcase, redis_list_append_test);
+  tcase_add_test(testcase, redis_list_count_test);
+  tcase_add_test(testcase, redis_list_delete_test);
+  tcase_add_test(testcase, redis_list_exists_test);
+  tcase_add_test(testcase, redis_list_get_test);
+  tcase_add_test(testcase, redis_list_getall_test);
+  tcase_add_test(testcase, redis_list_pop_params_test);
+  tcase_add_test(testcase, redis_list_pop_left_test);
+  tcase_add_test(testcase, redis_list_pop_right_test);
+  tcase_add_test(testcase, redis_list_push_params_test);
+  tcase_add_test(testcase, redis_list_push_left_test);
+  tcase_add_test(testcase, redis_list_push_right_test);
+  tcase_add_test(testcase, redis_list_rotate_test);
+  tcase_add_test(testcase, redis_list_set_test);
+  tcase_add_test(testcase, redis_list_setall_test);
+
+  tcase_add_test(testcase, redis_set_remove_test);
+  tcase_add_test(testcase, redis_set_exists_test);
+  tcase_add_test(testcase, redis_set_add_test);
+  tcase_add_test(testcase, redis_set_count_test);
+  tcase_add_test(testcase, redis_set_delete_test);
+  tcase_add_test(testcase, redis_set_getall_test);
+  tcase_add_test(testcase, redis_set_setall_test);
+
+  tcase_add_test(testcase, redis_sorted_set_remove_test);
+  tcase_add_test(testcase, redis_sorted_set_exists_test);
+  tcase_add_test(testcase, redis_sorted_set_add_test);
+  tcase_add_test(testcase, redis_sorted_set_count_test);
+  tcase_add_test(testcase, redis_sorted_set_delete_test);
+  tcase_add_test(testcase, redis_sorted_set_getn_test);
+  tcase_add_test(testcase, redis_sorted_set_incr_test);
+  tcase_add_test(testcase, redis_sorted_set_score_test);
+  tcase_add_test(testcase, redis_sorted_set_set_test);
+  tcase_add_test(testcase, redis_sorted_set_setall_test);
+
+  suite_add_tcase(suite, testcase);
+#endif /* PR_USE_REDIS */
+
+  return suite;
+}
diff --git a/tests/api/regexp.c b/tests/api/regexp.c
index f86f3b7..39e251b 100644
--- a/tests/api/regexp.c
+++ b/tests/api/regexp.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2011 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Regexp API tests
- * $Id: regexp.c,v 1.4 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Regexp API tests */
 
 #include "tests.h"
 
@@ -36,69 +34,219 @@ static void set_up(void) {
   }
 
   init_regexp();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("regexp", 1, 20);
+  }
 }
 
 static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("regexp", 0, 0);
+  }
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
-  } 
+    p = permanent_pool = NULL;
+  }
 }
 
 START_TEST (regexp_alloc_test) {
   pr_regex_t *res;
 
   res = pr_regexp_alloc(NULL);
-  fail_unless(res != NULL, "Failed to allocate regex");
+  fail_unless(res != NULL, "Failed to allocate regex: %s", strerror(errno));
+  pr_regexp_free(NULL, res);
 }
 END_TEST
 
 START_TEST (regexp_free_test) {
-  pr_regex_t *res = NULL;
+  mark_point();
+  pr_regexp_free(NULL, NULL);
+}
+END_TEST
 
-  pr_regexp_free(NULL, res);
+START_TEST (regexp_error_test) {
+  size_t bufsz, res;
+  const pr_regex_t *pre;
+  char *buf;
 
-  res = pr_regexp_alloc(NULL);
-  fail_unless(res != NULL, "Failed to allocate regex");
+  mark_point();
+  res = pr_regexp_error(0, NULL, NULL, 0);
+  fail_unless(res == 0, "Failed to handle null regexp");
 
-  pr_regexp_free(NULL, res);
+  pre = (const pr_regex_t *) 3;
+
+  mark_point();
+  res = pr_regexp_error(0, pre, NULL, 0);
+  fail_unless(res == 0, "Failed to handle null buf");
+
+  bufsz = 256;
+  buf = pcalloc(p, bufsz);
+
+  mark_point();
+  res = pr_regexp_error(0, pre, buf, 0);
+  fail_unless(res == 0, "Failed to handle zero bufsz");
 }
 END_TEST
 
-START_TEST (regexp_compile) {
+START_TEST (regexp_compile_test) {
   pr_regex_t *pre = NULL;
   int res;
   char errstr[256], *pattern;
   size_t errstrlen;
 
+  res = pr_regexp_compile(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
   pre = pr_regexp_alloc(NULL);
 
+  res = pr_regexp_compile(pre, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pattern");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
   pattern = "[=foo";
   res = pr_regexp_compile(pre, pattern, 0); 
   fail_unless(res != 0, "Successfully compiled pattern unexpectedly"); 
 
+  errstrlen = pr_regexp_error(1, NULL, NULL, 0);
+  fail_unless(errstrlen == 0, "Failed to handle null arguments");
+
+  errstrlen = pr_regexp_error(1, pre, NULL, 0);
+  fail_unless(errstrlen == 0, "Failed to handle null buffer");
+
+  errstrlen = pr_regexp_error(1, pre, errstr, 0);
+  fail_unless(errstrlen == 0, "Failed to handle zero buffer length");
+
   errstrlen = pr_regexp_error(res, pre, errstr, sizeof(errstr));
   fail_unless(errstrlen > 0, "Failed to get regex compilation error string");
 
   pattern = "foo";
   res = pr_regexp_compile(pre, pattern, 0);
-  fail_unless(res == 0, "Failed to compile regex pattern");
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  pattern = "foo";
+  res = pr_regexp_compile(pre, pattern, REG_ICASE);
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  pr_regexp_free(NULL, pre);
+}
+END_TEST
+
+START_TEST (regexp_compile_posix_test) {
+  pr_regex_t *pre = NULL;
+  int res;
+  char errstr[256], *pattern;
+  size_t errstrlen;
+
+  res = pr_regexp_compile_posix(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  pre = pr_regexp_alloc(NULL);
+
+  res = pr_regexp_compile_posix(pre, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null pattern");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  pattern = "[=foo";
+  res = pr_regexp_compile_posix(pre, pattern, 0);
+  fail_unless(res != 0, "Successfully compiled pattern unexpectedly");
+
+  errstrlen = pr_regexp_error(res, pre, errstr, sizeof(errstr));
+  fail_unless(errstrlen > 0, "Failed to get regex compilation error string");
+
+  pattern = "foo";
+  res = pr_regexp_compile_posix(pre, pattern, 0);
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  pattern = "foo";
+  res = pr_regexp_compile_posix(pre, pattern, REG_ICASE);
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  pr_regexp_free(NULL, pre);
+}
+END_TEST
+
+START_TEST (regexp_get_pattern_test) {
+  pr_regex_t *pre = NULL;
+  int res;
+  const char *str;
+  char *pattern;
+
+  str = pr_regexp_get_pattern(NULL);
+  fail_unless(str == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  pre = pr_regexp_alloc(NULL);
+
+  str = pr_regexp_get_pattern(pre);
+  fail_unless(str == NULL, "Failed to handle null pattern");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  pattern = "^foo";
+  res = pr_regexp_compile(pre, pattern, 0);
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  str = pr_regexp_get_pattern(pre);
+  fail_unless(str != NULL, "Failed to get regex pattern: %s", strerror(errno));
+  fail_unless(strcmp(str, pattern) == 0, "Expected '%s', got '%s'", pattern,
+    str);
 
   pr_regexp_free(NULL, pre);
 }
 END_TEST
 
-START_TEST (regexp_exec) {
+START_TEST (regexp_set_limits_test) {
+  int res;
+  pr_regex_t *pre = NULL;
+  const char *pattern, *str;
+
+  res = pr_regexp_set_limits(0, 0);
+  fail_unless(res == 0, "Failed to set limits: %s", strerror(errno));
+
+  /* Set the limits, and compile/execute a regex. */
+  res = pr_regexp_set_limits(1, 1);
+  fail_unless(res == 0, "Failed to set limits: %s", strerror(errno));
+
+  pre = pr_regexp_alloc(NULL);
+
+  pattern = "^foo";
+  res = pr_regexp_compile(pre, pattern, REG_ICASE);
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  str = "fooBAR";
+  (void) pr_regexp_exec(pre, str, 0, NULL, 0, 0, 0);
+
+  pr_regexp_free(NULL, pre);
+}
+END_TEST
+
+START_TEST (regexp_exec_test) {
   pr_regex_t *pre = NULL;
   int res;
   char *pattern, *str;
 
+  res = pr_regexp_exec(NULL, NULL, 0, NULL, 0, 0, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
   pre = pr_regexp_alloc(NULL);
 
   pattern = "^foo";
   res = pr_regexp_compile(pre, pattern, 0);
-  fail_unless(res == 0, "Failed to compile regex pattern");
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  res = pr_regexp_exec(pre, NULL, 0, NULL, 0, 0, 0);
+  fail_unless(res != 0, "Failed to handle null string");
 
   str = "bar";
   res = pr_regexp_exec(pre, str, 0, NULL, 0, 0, 0);
@@ -109,6 +257,60 @@ START_TEST (regexp_exec) {
   fail_unless(res == 0, "Failed to match string");
 
   pr_regexp_free(NULL, pre);
+
+  pre = pr_regexp_alloc(NULL);
+
+  pattern = "^foo";
+  res = pr_regexp_compile_posix(pre, pattern, REG_ICASE);
+  fail_unless(res == 0, "Failed to compile regex pattern '%s'", pattern);
+
+  res = pr_regexp_exec(pre, NULL, 0, NULL, 0, 0, 0);
+  fail_unless(res != 0, "Failed to handle null string");
+
+  str = "BAR";
+  res = pr_regexp_exec(pre, str, 0, NULL, 0, 0, 0);
+  fail_unless(res != 0, "Matched string unexpectedly");
+
+  str = "FOOBAR";
+  res = pr_regexp_exec(pre, str, 0, NULL, 0, 0, 0);
+  fail_unless(res == 0, "Failed to match string");
+
+  pr_regexp_free(NULL, pre);
+}
+END_TEST
+
+START_TEST (regexp_cleanup_test) {
+  pr_regex_t *pre, *pre2, *pre3;
+  int res;
+  char *pattern;
+
+  pattern = "^foo";
+
+  pre = pr_regexp_alloc(NULL);
+  res = pr_regexp_compile(pre, pattern, 0);
+  fail_unless(res == 0, "Failed to compile regexp pattern '%s'", pattern);
+
+  pattern = "bar$";
+  pre2 = pr_regexp_alloc(NULL);
+  res = pr_regexp_compile(pre2, pattern, 0);
+  fail_unless(res == 0, "Failed to compile regexp pattern '%s'", pattern);
+
+  pattern = "&baz$";
+  pre3 = pr_regexp_alloc(NULL);
+  res = pr_regexp_compile_posix(pre3, pattern, 0);
+  fail_unless(res == 0, "Failed to compile POSIX regexp pattern '%s'", pattern);
+
+  mark_point();
+  pr_event_generate("core.restart", NULL);
+
+  mark_point();
+  pr_event_generate("core.exit", NULL);
+
+  mark_point();
+  pr_regexp_free(NULL, pre);
+
+  mark_point();
+  pr_regexp_free(NULL, pre2);
 }
 END_TEST
 
@@ -119,15 +321,18 @@ Suite *tests_get_regexp_suite(void) {
   suite = suite_create("regexp");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, regexp_alloc_test);
   tcase_add_test(testcase, regexp_free_test);
-  tcase_add_test(testcase, regexp_compile);
-  tcase_add_test(testcase, regexp_exec);
+  tcase_add_test(testcase, regexp_error_test);
+  tcase_add_test(testcase, regexp_compile_test);
+  tcase_add_test(testcase, regexp_compile_posix_test);
+  tcase_add_test(testcase, regexp_exec_test);
+  tcase_add_test(testcase, regexp_get_pattern_test);
+  tcase_add_test(testcase, regexp_set_limits_test);
+  tcase_add_test(testcase, regexp_cleanup_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/response.c b/tests/api/response.c
index d07c377..0d95069 100644
--- a/tests/api/response.c
+++ b/tests/api/response.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2011-2012 The ProFTPD Project team
+ * Copyright (c) 2011-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,25 +22,47 @@
  * OpenSSL in the source distribution.
  */
 
-/* Response API tests
- * $Id: response.c,v 1.5 2012-07-26 22:40:37 castaglia Exp $
- */
+/* Response API tests */
 
 #include "tests.h"
 
+extern pr_response_t *resp_list, *resp_err_list;
+
 static pool *p = NULL;
 
 static void set_up(void) {
   if (p == NULL) {
-    p = make_sub_pool(NULL);
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_netio();
+  init_inet();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("netio", 1, 20);
+    pr_trace_set_levels("response", 1, 20);
   }
 }
 
 static void tear_down(void) {
+  pr_response_register_handler(NULL);
+
+  if (session.c != NULL) {
+    pr_inet_close(p, session.c);
+    session.c = NULL;
+  }
+
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("netio", 0, 0);
+    pr_trace_set_levels("response", 0, 0);
+  }
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-  } 
+    p = permanent_pool = NULL;
+  }
 }
 
 START_TEST (response_pool_get_test) {
@@ -62,11 +84,20 @@ END_TEST
 
 START_TEST (response_add_test) {
   int res;
-  char *last_resp_code = NULL, *last_resp_msg = NULL;
+  const char *last_resp_code = NULL, *last_resp_msg = NULL;
   char *resp_code = R_200, *resp_msg = "OK";
 
   pr_response_set_pool(p);
+
+  mark_point();
+  pr_response_add(NULL, NULL);
+
+  mark_point();
+  pr_response_add(NULL, "%s", resp_msg);
+
+  mark_point();
   pr_response_add(resp_code, "%s", resp_msg);
+  pr_response_add(NULL, "%s", resp_msg);
 
   res = pr_response_get_last(p, &last_resp_code, &last_resp_msg);
   fail_unless(res == 0, "Failed to get last values: %d (%s)", errno,
@@ -84,10 +115,15 @@ END_TEST
 
 START_TEST (response_add_err_test) {
   int res;
-  char *last_resp_code = NULL, *last_resp_msg = NULL;
+  const char *last_resp_code = NULL, *last_resp_msg = NULL;
   char *resp_code = R_450, *resp_msg = "Busy";
 
   pr_response_set_pool(p);
+
+  mark_point();
+  pr_response_add_err(NULL, NULL);
+
+  mark_point();
   pr_response_add_err(resp_code, "%s", resp_msg);
 
   res = pr_response_get_last(p, &last_resp_code, &last_resp_msg);
@@ -106,7 +142,7 @@ END_TEST
 
 START_TEST (response_get_last_test) {
   int res;
-  char *resp_code = NULL, *resp_msg = NULL;
+  const char *resp_code = NULL, *resp_msg = NULL;
 
   res = pr_response_get_last(NULL, NULL, NULL);
   fail_unless(res == -1, "Failed to handle null pool");
@@ -129,6 +165,208 @@ START_TEST (response_get_last_test) {
 }
 END_TEST
 
+START_TEST (response_block_test) {
+  int res;
+
+  res = pr_response_block(-1);
+  fail_unless(res == -1, "Failed to handle invalid argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)",
+    EINVAL, strerror(errno), errno);
+
+  res = pr_response_block(TRUE);
+  fail_unless(res == 0, "Failed to block responses: %s", strerror(errno));
+
+  res = pr_response_block(FALSE);
+  fail_unless(res == 0, "Failed to unblock responses: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (response_clear_test) {
+  mark_point();
+  pr_response_clear(NULL);
+
+  pr_response_set_pool(p);
+  pr_response_add(R_200, "%s", "OK");
+  pr_response_clear(&resp_list);
+}
+END_TEST
+
+static int response_netio_poll_cb(pr_netio_stream_t *nstrm) {
+  /* Always return >0, to indicate that we haven't timed out, AND that there
+   * is a writable fd available.
+   */
+  return 7;
+}
+
+static int response_netio_write_cb(pr_netio_stream_t *nstrm, char *buf,
+    size_t buflen) {
+  return buflen;
+}
+
+static unsigned int resp_nlines = 0;
+static char *resp_line = NULL;
+
+static char *response_handler_cb(pool *cb_pool, const char *fmt, ...) {
+  char buf[PR_RESPONSE_BUFFER_SIZE] = {'\0'};
+  va_list msg;
+
+  va_start(msg, fmt);
+  vsnprintf(buf, sizeof(buf), fmt, msg);
+  va_end(msg);
+
+  buf[sizeof(buf)-1] = '\0';
+
+  resp_nlines++;
+  resp_line = pstrdup(cb_pool, buf);
+  return resp_line;
+}
+
+START_TEST (response_flush_test) {
+  int res, sockfd = -2;
+  conn_t *conn;
+  pr_netio_t *netio;
+
+  mark_point();
+  pr_response_flush(NULL);
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = response_netio_poll_cb;
+  netio->write = response_netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, INPORT_ANY, FALSE);
+  session.c = conn;
+
+  pr_response_register_handler(response_handler_cb);
+
+  resp_nlines = 0;
+  resp_line = NULL;
+  pr_response_set_pool(p);
+
+  pr_response_add(R_200, "%s", "OK");
+  pr_response_add(R_DUP, "%s", "Still OK");
+  pr_response_add(R_DUP, "%s", "OK already!");
+  pr_response_flush(&resp_list);
+
+  pr_response_register_handler(NULL);
+  pr_inet_close(p, session.c);
+  session.c = NULL;
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  fail_unless(resp_nlines == 3, "Expected 3 response lines flushed, got %u",
+    resp_nlines);
+}
+END_TEST
+
+START_TEST (response_send_test) {
+  int res, sockfd = -2;
+  conn_t *conn;
+  pr_netio_t *netio;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = response_netio_poll_cb;
+  netio->write = response_netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, INPORT_ANY, FALSE);
+  session.c = conn;
+
+  pr_response_register_handler(response_handler_cb);
+
+  resp_nlines = 0;
+  resp_line = NULL;
+  pr_response_set_pool(p);
+
+  pr_response_send(R_200, "%s", "OK");
+
+  pr_response_register_handler(NULL);
+  pr_inet_close(p, session.c);
+  session.c = NULL;
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  fail_unless(resp_nlines == 1, "Expected 1 response line flushed, got %u",
+    resp_nlines);
+}
+END_TEST
+
+START_TEST (response_send_async_test) {
+  int res, sockfd = -2;
+  conn_t *conn;
+  pr_netio_t *netio;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = response_netio_poll_cb;
+  netio->write = response_netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, INPORT_ANY, FALSE);
+  session.c = conn;
+
+  pr_response_register_handler(response_handler_cb);
+
+  resp_nlines = 0;
+  resp_line = NULL;
+  pr_response_set_pool(p);
+
+  pr_response_send_async(R_200, "%s", "OK");
+
+  pr_response_register_handler(NULL);
+  pr_inet_close(p, session.c);
+  session.c = NULL;
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  fail_unless(resp_nlines == 1, "Expected 1 response line flushed, got %u",
+    resp_nlines);
+  fail_unless(resp_line != NULL, "Expected response line");
+  fail_unless(strcmp(resp_line, "200 OK\r\n") == 0,
+    "Expected '200 OK', got '%s'", resp_line);
+}
+END_TEST
+
+START_TEST (response_send_raw_test) {
+  int res, sockfd = -2;
+  conn_t *conn;
+  pr_netio_t *netio;
+
+  netio = pr_alloc_netio2(p, NULL, "testsuite");
+  netio->poll = response_netio_poll_cb;
+  netio->write = response_netio_write_cb;
+
+  res = pr_register_netio(netio, PR_NETIO_STRM_CTRL);
+  fail_unless(res == 0, "Failed to register custom ctrl NetIO: %s",
+    strerror(errno));
+
+  conn = pr_inet_create_conn(p, sockfd, NULL, INPORT_ANY, FALSE);
+  session.c = conn;
+
+  pr_response_register_handler(response_handler_cb);
+
+  resp_nlines = 0;
+  resp_line = NULL;
+  pr_response_set_pool(p);
+
+  pr_response_send_raw("%s", "OK");
+
+  pr_response_register_handler(NULL);
+  pr_inet_close(p, session.c);
+  session.c = NULL;
+  pr_unregister_netio(PR_NETIO_STRM_CTRL);
+
+  fail_unless(resp_nlines == 1, "Expected 1 response line flushed, got %u",
+    resp_nlines);
+}
+END_TEST
+
+#if defined(TEST_BUG3711)
 START_TEST (response_pool_bug3711_test) {
   cmd_rec *cmd;
   pool *resp_pool, *cmd_pool;
@@ -171,17 +409,18 @@ START_TEST (response_pool_bug3711_test) {
   pr_response_add_err(err_code, "%s", err_msg);
 }
 END_TEST
-
+#endif /* TEST_BUG3711 */
 
 Suite *tests_get_response_suite(void) {
   Suite *suite;
   TCase *testcase;
+#if defined(TEST_BUG3711)
   int bug3711_signo = 0;
+#endif /* TEST_BUG3711 */
 
   suite = suite_create("response");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, response_pool_get_test);
@@ -190,12 +429,20 @@ Suite *tests_get_response_suite(void) {
   tcase_add_test(testcase, response_add_err_test);
   tcase_add_test(testcase, response_get_last_test);
 
+  tcase_add_test(testcase, response_block_test);
+  tcase_add_test(testcase, response_clear_test);
+  tcase_add_test(testcase, response_flush_test);
+  tcase_add_test(testcase, response_send_test);
+  tcase_add_test(testcase, response_send_async_test);
+  tcase_add_test(testcase, response_send_raw_test);
+
+#if defined(TEST_BUG3711)
   /* We expect this test to fail due to a segfault; see Bug#3711.
    *
    * Note that on some platforms (e.g. Darwin), the test case should fail
    * with a SIGBUS rather than SIGSEGV, hence the conditional here.
    */
-#if defined(DARWIN9)
+#if defined(DARWIN9) || defined(DARWIN10) || defined(DARWIN11)
   bug3711_signo = SIGBUS;
 #else
   bug3711_signo = SIGSEGV;
@@ -205,12 +452,10 @@ Suite *tests_get_response_suite(void) {
    * a regression test, and only generates core files which can litter
    * the filesystems of build/test machines needlessly.
    */
-#if 0
   tcase_add_test_raise_signal(testcase, response_pool_bug3711_test,
     bug3711_signo);
-#endif
+#endif /* TEST_BUG3711 */
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/rlimit.c b/tests/api/rlimit.c
new file mode 100644
index 0000000..c683081
--- /dev/null
+++ b/tests/api/rlimit.c
@@ -0,0 +1,191 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2015 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* RLimit API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = make_sub_pool(NULL);
+  }
+}
+
+static void tear_down(void) {
+  if (p) {
+    destroy_pool(p);
+    p = NULL;
+  }
+}
+
+START_TEST (rlimit_core_test) {
+  int res;
+  rlim_t curr_rlim = 0, max_rlim = 0 ;
+
+  res = pr_rlimit_get_core(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  res = pr_rlimit_get_core(&curr_rlim, &max_rlim);
+  fail_unless(res == 0, "Failed to get core resource limits: %s",
+    strerror(errno));
+
+  curr_rlim = max_rlim = -1;
+  res = pr_rlimit_set_core(curr_rlim, max_rlim);
+
+  /* Note that some platforms will NOT fail a setrlimit(2) command if the
+   * arguments are negative.  Hence this conditional check.
+   */
+  if (res < 0) {
+    fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+START_TEST (rlimit_cpu_test) {
+  int res;
+  rlim_t curr_rlim = 0, max_rlim = 0 ;
+
+  res = pr_rlimit_get_cpu(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  res = pr_rlimit_get_cpu(&curr_rlim, &max_rlim);
+  fail_unless(res == 0, "Failed to get CPU resource limits: %s",
+    strerror(errno));
+
+  curr_rlim = max_rlim = -1;
+  res = pr_rlimit_set_cpu(curr_rlim, max_rlim);
+
+  /* Note that some platforms will NOT fail a setrlimit(2) command if the
+   * arguments are negative.  Hence this conditional check.
+   */
+  if (res < 0) {
+    fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+START_TEST (rlimit_files_test) {
+  int res;
+  rlim_t curr_rlim = 0, max_rlim = 0 ;
+
+  res = pr_rlimit_get_files(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  res = pr_rlimit_get_files(&curr_rlim, &max_rlim);
+  fail_unless(res == 0, "Failed to get file resource limits: %s",
+    strerror(errno));
+
+  curr_rlim = max_rlim = -1;
+  res = pr_rlimit_set_files(curr_rlim, max_rlim);
+
+  /* Note that some platforms will NOT fail a setrlimit(2) command if the
+   * arguments are negative.  Hence this conditional check.
+   */
+  if (res < 0) {
+    fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+START_TEST (rlimit_memory_test) {
+  int res;
+  rlim_t curr_rlim = 0, max_rlim = 0 ;
+
+  res = pr_rlimit_get_memory(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  res = pr_rlimit_get_memory(&curr_rlim, &max_rlim);
+  fail_unless(res == 0, "Failed to get memory resource limits: %s",
+    strerror(errno));
+
+  curr_rlim = max_rlim = -1;
+  res = pr_rlimit_set_memory(curr_rlim, max_rlim);
+
+  /* Note that some platforms will NOT fail a setrlimit(2) command if the
+   * arguments are negative.  Hence this conditional check.
+   */
+  if (res < 0) {
+    fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+START_TEST (rlimit_nproc_test) {
+  int res;
+  rlim_t curr_rlim = 0, max_rlim = 0 ;
+
+  res = pr_rlimit_get_nproc(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %s (%d)",
+    strerror(errno), errno);
+
+  res = pr_rlimit_get_nproc(&curr_rlim, &max_rlim);
+  fail_unless(res == 0, "Failed to get nproc resource limits: %s",
+    strerror(errno));
+
+  curr_rlim = max_rlim = -1;
+  res = pr_rlimit_set_nproc(curr_rlim, max_rlim);
+
+  /* Note that some platforms will NOT fail a setrlimit(2) command if the
+   * arguments are negative.  Hence this conditional check.
+   */
+  if (res < 0) {
+    fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %s (%d)",
+      strerror(errno), errno);
+  }
+}
+END_TEST
+
+Suite *tests_get_rlimit_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("rlimit");
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+  tcase_add_test(testcase, rlimit_core_test);
+  tcase_add_test(testcase, rlimit_cpu_test);
+  tcase_add_test(testcase, rlimit_files_test);
+  tcase_add_test(testcase, rlimit_memory_test);
+  tcase_add_test(testcase, rlimit_nproc_test);
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/scoreboard.c b/tests/api/scoreboard.c
index 6fb6503..4f8dff9 100644
--- a/tests/api/scoreboard.c
+++ b/tests/api/scoreboard.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,27 +22,50 @@
  * OpenSSL in the source distribution.
  */
 
-/* Scoreboard API tests
- * $Id: scoreboard.c,v 1.10 2013-01-05 03:36:39 castaglia Exp $
- */
+/* Scoreboard API tests */
 
 #include "tests.h"
 
 static pool *p = NULL;
 
+static const char *test_dir = "/tmp/prt-scoreboard/";
+static const char *test_file = "/tmp/prt-scoreboard/test.dat";
+static const char *test_mutex = "/tmp/prt-scoreboard/test.dat.lck";
+static const char *test_file2 = "/tmp/prt-scoreboard-mutex.dat";
+
 static void set_up(void) {
+  (void) unlink(test_file);
+  (void) unlink(test_file2);
+  (void) unlink(test_mutex);
+  (void) rmdir(test_dir);
+
   if (p == NULL) {
     p = permanent_pool = make_sub_pool(NULL);
   }
 
   ServerType = SERVER_STANDALONE;
+  init_netaddr();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("lock", 1, 20);
+    pr_trace_set_levels("scoreboard", 1, 20);
+  }
 }
 
 static void tear_down(void) {
+  (void) unlink(test_file);
+  (void) unlink(test_file2);
+  (void) unlink(test_mutex);
+  (void) rmdir(test_dir);
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_set_levels("lock", 0, 0);
+    pr_trace_set_levels("scoreboard", 0, 0);
+  }
+
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
+    p = permanent_pool = NULL;
   } 
 }
 
@@ -100,16 +123,14 @@ START_TEST (scoreboard_set_test) {
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
     errno);
 
-  res = mkdir("/tmp/prt-scoreboard", 0777);
+  res = mkdir(test_dir, 0777);
   fail_unless(res == 0,
-    "Failed to create tmp directory '/tmp/prt-scoreboard': %s",
-    strerror(errno));
-  res = chmod("/tmp/prt-scoreboard/", 0777);
+    "Failed to create tmp directory '%s': %s", test_dir, strerror(errno));
+  res = chmod(test_dir, 0777);
   fail_unless(res == 0,
-    "Failed to create set 0777 perms on '/tmp/prt-scoreboard': %s",
-    strerror(errno));
+    "Failed to create set 0777 perms on '%s': %s", test_dir, strerror(errno));
 
-  res = pr_set_scoreboard("/tmp/prt-scoreboard/");
+  res = pr_set_scoreboard(test_dir);
   fail_unless(res == -1, "Failed to handle nonexistent file argument");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
@@ -117,64 +138,64 @@ START_TEST (scoreboard_set_test) {
   fail_unless(res == -1, "Failed to handle world-writable path argument");
   fail_unless(errno == EPERM, "Failed to set errno to EPERM");
 
-  res = chmod("/tmp/prt-scoreboard/", 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir("/tmp/prt-scoreboard");
-    fail("Failed to set 0775 perms on '/tmp/prt-scoreboard/': %s",
-      strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set 0775 perms on '%s': %s", test_dir,
+    strerror(errno));
 
   res = pr_set_scoreboard("/tmp/prt-scoreboard/bar");
   fail_unless(res == 0, "Failed to set scoreboard: %s", strerror(errno));
-  (void) rmdir("/tmp/prt-scoreboard/");
+  (void) rmdir(test_dir);
 
   path = pr_get_scoreboard();
   fail_unless(path != NULL, "Failed to get scoreboard path: %s",
     strerror(errno));  
   fail_unless(strcmp("/tmp/prt-scoreboard/bar", path) == 0,
     "Expected '%s', got '%s'", "/tmp/prt-scoreboard/bar", path);
+
+  (void) rmdir(test_dir);
 }
 END_TEST
 
-START_TEST (scoreboard_open_close_test) {
+START_TEST (scoreboard_set_mutex_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck",
-    *symlink_path = "/tmp/prt-scoreboard/symlink";
+  const char *path;
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
-    strerror(errno));
+  res = pr_set_scoreboard_mutex(NULL);
+  fail_unless(res == -1, "Failed to handle null argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
+  res = pr_set_scoreboard_mutex("/tmp");
+  fail_unless(res == 0, "Failed to set scoreboard mutex: %s", strerror(errno));
 
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
+  path = pr_get_scoreboard_mutex();
+  fail_unless(path != NULL, "Failed to get scoreboard mutex path: %s",
+    strerror(errno));  
+  fail_unless(strcmp("/tmp", path) == 0,
+    "Expected '%s', got '%s'", "/tmp", path);
+}
+END_TEST
 
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
+START_TEST (scoreboard_open_close_test) {
+  int res;
+  const char *symlink_path = "/tmp/prt-scoreboard/symlink";
 
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s': %s", test_dir,
+    strerror(errno));
 
-  if (symlink(symlink_path, path) == 0) {
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
+
+  if (symlink(symlink_path, test_file) == 0) {
 
     res = pr_open_scoreboard(O_RDWR);
     if (res == 0) {
-      (void) unlink(path);
-      (void) unlink(mutex_path);
       (void) unlink(symlink_path);
-      (void) rmdir(dir);
 
       fail("Unexpectedly opened symlink scoreboard");
     }
@@ -183,463 +204,277 @@ START_TEST (scoreboard_open_close_test) {
       int xerrno = errno;
 
       (void) unlink(symlink_path);
-      (void) unlink(mutex_path);
-      (void) unlink(path);
-      (void) rmdir(dir);
 
       fail("Failed to set errno to EPERM (got %d)", xerrno);
     }
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
     (void) unlink(symlink_path);
+
+    res = pr_set_scoreboard(test_file);
+    fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+      strerror(errno));
   }
 
   res = pr_open_scoreboard(O_RDONLY);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly opened scoreboard using O_RDONLY");
-  }
+  fail_unless(res < 0, "Unexpectedly opened scoreboard using O_RDONLY");
 
   if (errno != EINVAL) {
     int xerrno = errno;
 
     (void) unlink(symlink_path);
-    (void) unlink(mutex_path);
-    (void) unlink(path);
-    (void) rmdir(dir);
 
     fail("Failed to set errno to EINVAL (got %d)", xerrno);
   }
 
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
-    (void) unlink(mutex_path);
-    (void) unlink(path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  /* Try opening the scoreboard again; it should be OK, since we are the
+   * opener.
+   */
+  res = pr_open_scoreboard(O_RDWR);
+  fail_unless(res == 0, "Failed to open scoreboard again: %s", strerror(errno));
 
   /* Now that we have a scoreboard, try opening it again using O_RDONLY. */
   pr_close_scoreboard(FALSE);
 
   res = pr_open_scoreboard(O_RDONLY);
-  if (res == 0) {
-    (void) unlink(mutex_path);
-    (void) unlink(path);
-    (void) rmdir(dir);
+  fail_unless(res < 0, "Unexpectedly opened scoreboard using O_RDONLY");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-    fail("Unexpectedly opened scoreboard using O_RDONLY");
-  }
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
+}
+END_TEST
 
-  if (errno != EINVAL) {
-    int xerrno = errno;
+START_TEST (scoreboard_lock_test) {
+  int fd = -1, lock_type = -1, res;
 
-    (void) unlink(mutex_path);
-    (void) unlink(path);
-    (void) rmdir(dir);
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res < 0, "Failed to handle bad lock type");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  lock_type = F_RDLCK;
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
+
+  fd = open(test_file2, O_CREAT|O_EXCL|O_RDWR, S_IRUSR|S_IWUSR);
+  fail_unless(fd >= 0, "Failed to open '%s': %s", test_file2, strerror(errno));
+
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_WRLCK;
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_UNLCK;
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_WRLCK;
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  /* Note: apparently attempt to lock (again) a file on which a lock
+   * (of the same type) is already held will succeed.  Huh.
+   */
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
 
-  (void) unlink(mutex_path);
-  (void) unlink(path);
-  (void) rmdir(dir);
+  lock_type = F_RDLCK;
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_UNLCK;
+  res = pr_lock_scoreboard(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  (void) unlink(test_file2);
 }
 END_TEST
 
 START_TEST (scoreboard_delete_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
   struct stat st;
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(mutex_path);
-    (void) unlink(path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = stat(pr_get_scoreboard(), &st);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(mutex_path);
-    (void) unlink(path);
-    (void) rmdir(dir);
-
-    fail("Failed to stat scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to stat scoreboard: %s", strerror(errno));
 
   pr_delete_scoreboard();
 
   res = stat(pr_get_scoreboard(), &st);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly found deleted scoreboard");
-  }
+  fail_unless(res < 0, "Unexpectedly found deleted scoreboard");
 
   res = stat(pr_get_scoreboard_mutex(), &st);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  fail_unless(res < 0, "Unexpectedly found deleted scoreboard mutex");
 
-    fail("Unexpectedly found deleted scoreboard mutex");
-  }
-
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_restore_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_restore_scoreboard();
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly restored scoreboard");
-  }
-
-  if (errno != EINVAL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  fail_unless(res < 0, "Unexpectedly restored scoreboard");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = pr_restore_scoreboard();
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("restoring scoreboard before rewind succeeded unexpectedly");
-  }
+  fail_unless(res < 0,
+    "Restoring scoreboard before rewind succeeded unexpectedly");
 
   res = pr_rewind_scoreboard();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to rewind scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to rewind scoreboard: %s", strerror(errno));
 
   res = pr_restore_scoreboard();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  fail_unless(res == 0, "Failed to restore scoreboard: %s", strerror(errno));
 
-    fail("Failed to restore scoreboard: %s", strerror(xerrno));
-  }
-
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_rewind_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_rewind_scoreboard();
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly rewound scoreboard");
-  }
-
-  if (errno != EINVAL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  fail_unless(res < 0, "Unexpectedly rewound scoreboard");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = pr_rewind_scoreboard();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to rewind scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to rewind scoreboard: %s", strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_scrub_test) {
   uid_t euid;
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_scoreboard_scrub();
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly scrubbed scoreboard");
-  }
+  fail_unless(res < 0, "Unexpectedly scrubbed scoreboard");
 
   euid = geteuid();
   if (euid != 0) {
     if (errno != EPERM &&
         errno != ENOENT) {
-      int xerrno = errno;
-
-      (void) unlink(path);
-      (void) unlink(mutex_path);
-      (void) rmdir(dir);
-
       fail("Failed to set errno to EPERM/ENOENT, got %d [%s] (euid = %lu)",
-        xerrno, strerror(xerrno), (unsigned long) euid);
+        errno, strerror(errno), (unsigned long) euid);
     }
 
   } else {
     if (errno != ENOENT) {
-      int xerrno = errno;
-
-      (void) unlink(path);
-      (void) unlink(mutex_path);
-      (void) rmdir(dir);
-
-      fail("Failed to set errno to ENOENT, got %d [%s] (euid = %lu)", xerrno,
-        strerror(xerrno), (unsigned long) euid);
+      fail("Failed to set errno to ENOENT, got %d [%s] (euid = %lu)", errno,
+        strerror(errno), (unsigned long) euid);
     }
   }
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = pr_scoreboard_scrub();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to scrub scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to scrub scoreboard: %s", strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_get_daemon_pid_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
   pid_t daemon_pid;
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   daemon_pid = pr_scoreboard_get_daemon_pid();
   if (daemon_pid != getpid()) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
     fail("Expected %lu, got %lu", (unsigned long) getpid(),
       (unsigned long) daemon_pid);
   }
@@ -649,80 +484,43 @@ START_TEST (scoreboard_get_daemon_pid_test) {
   ServerType = SERVER_INETD;
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   daemon_pid = pr_scoreboard_get_daemon_pid();
   if (daemon_pid != 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
     fail("Expected %lu, got %lu", (unsigned long) 0,
       (unsigned long) daemon_pid);
   }
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_get_daemon_uptime_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
   time_t daemon_uptime, now;
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   daemon_uptime = pr_scoreboard_get_daemon_uptime();
   now = time(NULL);
 
   if (daemon_uptime > now) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
     fail("Expected %lu, got %lu", (unsigned long) now,
       (unsigned long) daemon_uptime);
   }
@@ -732,622 +530,418 @@ START_TEST (scoreboard_get_daemon_uptime_test) {
   ServerType = SERVER_INETD;
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   daemon_uptime = pr_scoreboard_get_daemon_uptime();
   if (daemon_uptime != 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
     fail("Expected %lu, got %lu", (unsigned long) 0,
       (unsigned long) daemon_uptime);
   }
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_entry_add_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_scoreboard_entry_add();
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly added entry to scoreboard");
-  }
-
-  if (errno != EINVAL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  fail_unless(res < 0, "Unexpectedly added entry to scoreboard");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = pr_scoreboard_entry_add();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to add entry to scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to add entry to scoreboard: %s",
+    strerror(errno));
 
   res = pr_scoreboard_entry_add();
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly added entry to scoreboard");
-  }
-
-  if (errno != EPERM) {
-    int xerrno = errno;
+  fail_unless(res < 0, "Unexpectedly added entry to scoreboard");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EPERM (got %d)", xerrno);
-  }
-
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_entry_del_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   res = pr_scoreboard_entry_del(FALSE);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly deleted entry from scoreboard");
-  }
-
-  if (errno != EINVAL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  fail_unless(res < 0, "Unexpectedly deleted entry from scoreboard");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = pr_scoreboard_entry_del(FALSE);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly deleted entry from scoreboard");
-  }
-
-  if (errno != ENOENT) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to ENOENT (got %d)", xerrno);
-  }
+  fail_unless(res < 0, "Unexpectedly deleted entry from scoreboard");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
   res = pr_scoreboard_entry_add();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to add entry to scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to add entry to scoreboard: %s",
+    strerror(errno));
 
   res = pr_scoreboard_entry_del(FALSE);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to delete entry from scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to delete entry from scoreboard: %s",
+    strerror(errno));
 
   res = pr_scoreboard_entry_del(FALSE);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly deleted entry from scoreboard");
-  }
-
-  if (errno != ENOENT) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  fail_unless(res < 0, "Unexpectedly deleted entry from scoreboard");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
-    fail("Failed to set errno to ENOENT (got %d)", xerrno);
-  }
-
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_entry_read_test) {
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
   pr_scoreboard_entry_t *score;
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   score = pr_scoreboard_entry_read();
-  if (score != NULL) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly read scoreboard entry");
-  }
-
-  if (errno != EINVAL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  fail_unless(score == NULL, "Unexpectedly read scoreboard entry");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   /* We expect NULL here because the scoreboard file should be empty. */
   score = pr_scoreboard_entry_read();
-  if (score != NULL) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly read scoreboard entry");
-  }
+  fail_unless(score == NULL, "Unexpectedly read scoreboard entry");
 
   res = pr_scoreboard_entry_add();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to add entry to scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to add entry to scoreboard: %s",
+    strerror(errno));
 
   score = pr_scoreboard_entry_read();
-  if (score == NULL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to read scoreboard entry: %s", strerror(xerrno));
-  }
+  fail_unless(score != NULL, "Failed to read scoreboard entry: %s",
+    strerror(errno));
 
   if (score->sce_pid != getpid()) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
     fail("Failed to read expected scoreboard entry (expected PID %lu, got %lu)",
       (unsigned long) getpid(), (unsigned long) score->sce_pid);
   }
 
   score = pr_scoreboard_entry_read();
-  if (score != NULL) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  fail_unless(score == NULL, "Unexpectedly read scoreboard entry");
 
-    fail("Unexpectedly read scoreboard entry");
-  }
-
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_entry_get_test) {
+  register unsigned int i;
   int res;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
   const char *val;
+  int scoreboard_fields[] = {
+    PR_SCORE_USER,
+    PR_SCORE_CLIENT_ADDR,
+    PR_SCORE_CLIENT_NAME,
+    PR_SCORE_CLASS,
+    PR_SCORE_CWD,
+    PR_SCORE_CMD,
+    PR_SCORE_CMD_ARG,
+    PR_SCORE_PROTOCOL,
+    -1
+  };
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
-
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
   val = pr_scoreboard_entry_get(-1);
-  if (val != NULL) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly read value from scoreboard entry");
-  }
-
-  if (errno != EINVAL) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  fail_unless(val == NULL, "Unexpectedly read value from scoreboard entry");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
   res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   val = pr_scoreboard_entry_get(-1);
-  if (val != NULL) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Unexpectedly read value from scoreboard entry");
-  }
-
-  if (errno != EPERM) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to EPERM (got %d)", xerrno);
-  }
+  fail_unless(val == NULL, "Unexpectedly read value from scoreboard entry");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
 
   res = pr_scoreboard_entry_add();
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to add entry to scoreboard: %s", strerror(xerrno));
-  }
+  fail_unless(res == 0, "Failed to add entry to scoreboard: %s",
+    strerror(errno));
 
   val = pr_scoreboard_entry_get(-1);
-  if (val != NULL) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  fail_unless(val == NULL, "Unexpectedly read value from scoreboard entry");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
-    fail("Unexpectedly read value from scoreboard entry");
+  for (i = 0; scoreboard_fields[i] != -1; i++) {
+    val = pr_scoreboard_entry_get(scoreboard_fields[i]);
+    fail_unless(val != NULL, "Failed to read scoreboard field %d: %s",
+      scoreboard_fields[i], strerror(errno));
   }
 
-  if (errno != ENOENT) {
-    int xerrno = errno;
-
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
-
-    fail("Failed to set errno to ENOENT (got %d)", xerrno);
-  }
-
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
 }
 END_TEST
 
 START_TEST (scoreboard_entry_update_test) {
-  int res;
+  int num, res;
   const char *val;
-  const char *dir = "/tmp/prt-scoreboard/", *path = "/tmp/prt-scoreboard/test",
-    *mutex_path = "/tmp/prt-scoreboard/test.lck";
   pid_t pid = getpid();
+  const pr_netaddr_t *addr;
+  time_t now;
+  off_t len;
+  unsigned long elapsed;
 
-  res = mkdir(dir, 0775);
-  fail_unless(res == 0, "Failed to create directory '%s': %s", dir,
+  res = mkdir(test_dir, 0775);
+  fail_unless(res == 0, "Failed to create directory '%s': %s", test_dir,
     strerror(errno));
 
-  res = chmod(dir, 0775);
-  if (res < 0) {
-    int xerrno = errno;
-
-    (void) rmdir(dir);
-    fail("Failed to set perms on '%s' to 0775': %s", dir, strerror(xerrno));
-  }
+  res = chmod(test_dir, 0775);
+  fail_unless(res == 0, "Failed to set perms on '%s' to 0775': %s", test_dir,
+    strerror(errno));
 
-  res = pr_set_scoreboard(path);
-  if (res < 0) {
-    int xerrno = errno;
+  res = pr_set_scoreboard(test_file);
+  fail_unless(res == 0, "Failed to set scoreboard to '%s': %s", test_file,
+    strerror(errno));
 
-    (void) rmdir(dir);
-    fail("Failed to set scoreboard to '%s': %s", path, strerror(xerrno));
-  }
+  res = pr_scoreboard_entry_update(pid, 0);
+  fail_unless(res < 0, "Unexpectedly updated scoreboard entry");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
+  res = pr_open_scoreboard(O_RDWR);
+  fail_unless(res == 0, "Failed to open scoreboard: %s", strerror(errno));
 
   res = pr_scoreboard_entry_update(pid, 0);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  fail_unless(res < 0, "Unexpectedly updated scoreboard entry");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
 
-    fail("Unexpectedly updated scoreboard entry");
-  }
+  res = pr_scoreboard_entry_add();
+  fail_unless(res == 0, "Failed to add entry to scoreboard: %s",
+    strerror(errno));
 
-  if (errno != EINVAL) {
-    int xerrno = errno;
+  res = pr_scoreboard_entry_update(pid, -1);
+  fail_unless(res < 0, "Unexpectedly updated scoreboard entry");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  val = "cwd";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_CWD, val, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_CWD: %s", strerror(errno));
+ 
+  val = pr_scoreboard_entry_get(PR_SCORE_CWD); 
+  fail_unless(val != NULL, "Failed to get entry PR_SCORE_CWD: %s",
+    strerror(errno));
+  fail_unless(strcmp(val, "cwd") == 0, "Expected 'cwd', got '%s'", val);
 
-    fail("Failed to set errno to EINVAL (got %d)", xerrno);
-  }
+  val = "user";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_USER, val, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_USER: %s", strerror(errno));
 
-  res = pr_open_scoreboard(O_RDWR);
-  if (res < 0) {
-    int xerrno = errno;
+  addr = pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+  fail_unless(addr != NULL, "Failed to resolve '127.0.0.1': %s",
+    strerror(errno));
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_CLIENT_ADDR, addr, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_CLIENT_ADDR: %s",
+    strerror(errno));
 
-    fail("Failed to open scoreboard: %s", strerror(xerrno));
-  }
+  val = "remote_name";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_CLIENT_NAME, val, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_CLIENT_NAME: %s",
+    strerror(errno));
 
-  res = pr_scoreboard_entry_update(pid, 0);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  val = "session_class";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_CLASS, val, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_CLASS: %s", strerror(errno));
 
-    fail("Unexpectedly updated scoreboard entry");
-  }
+  val = "USER";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_CMD, "%s", val, NULL, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_CMD: %s", strerror(errno));
 
-  if (errno != EPERM) {
-    int xerrno = errno;
+  val = "foo bar";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_CMD_ARG, "%s", val, NULL,
+    NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_CMD_ARG: %s",
+    strerror(errno));
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  num = 77;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_SERVER_PORT, num, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_SERVER_PORT: %s",
+    strerror(errno));
 
-    fail("Failed to set errno to EPERM (got %d)", xerrno);
-  }
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_SERVER_ADDR, addr, num, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_SERVER_ADDR: %s",
+    strerror(errno));
 
-  res = pr_scoreboard_entry_add();
-  if (res < 0) {
-    int xerrno = errno;
+  val = "label";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_SERVER_LABEL, val, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_SERVER_LABEL: %s",
+    strerror(errno));
+ 
+  now = 1;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_BEGIN_IDLE, now, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_BEGIN_IDLE: %s",
+    strerror(errno));
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  now = 2;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_BEGIN_SESSION, now, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_BEGIN_SESSION: %s",
+    strerror(errno));
 
-    fail("Failed to add entry to scoreboard: %s", strerror(xerrno));
-  }
+  len = 7;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_XFER_DONE, len, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_XFER_DONE: %s",
+    strerror(errno));
 
-  res = pr_scoreboard_entry_update(pid, -1);
-  if (res == 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  len = 8;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_XFER_SIZE, len, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_XFER_SIZE: %s",
+    strerror(errno));
 
-    fail("Unexpectedly updated scoreboard entry");
-  }
+  len = 9;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_XFER_LEN, len, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_XFER_LEN: %s",
+    strerror(errno));
 
-  if (errno != ENOENT) {
-    int xerrno = errno;
+  elapsed = 1;
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_XFER_ELAPSED, elapsed, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_XFER_ELAPSED: %s",
+    strerror(errno));
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  val = "protocol";
+  res = pr_scoreboard_entry_update(pid, PR_SCORE_PROTOCOL, val, NULL);
+  fail_unless(res == 0, "Failed to update PR_SCORE_PROTOCOL: %s",
+    strerror(errno));
 
-    fail("Failed to set errno to ENOENT (got %d)", xerrno);
-  }
+  (void) unlink(test_mutex);
+  (void) unlink(test_file);
+  (void) rmdir(test_dir);
+}
+END_TEST
 
-  val = "cwd";
-  res = pr_scoreboard_entry_update(pid, PR_SCORE_CWD, val, NULL);
-  if (res < 0) {
-    int xerrno = errno;
+START_TEST (scoreboard_entry_kill_test) {
+  int res;
+  pr_scoreboard_entry_t sce;
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  res = pr_scoreboard_entry_kill(NULL, 0);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-    fail("Failed to update PR_SCORE_CWD: %s", strerror(xerrno));
-  }
- 
-  val = pr_scoreboard_entry_get(PR_SCORE_CWD); 
-  if (val == NULL) {
-    int xerrno = errno;
+  sce.sce_pid = getpid();
+  res = pr_scoreboard_entry_kill(&sce, 0);
+  fail_unless(res == 0, "Failed to send signal 0 to PID %lu: %s",
+    (unsigned long) sce.sce_pid, strerror(errno));
+}
+END_TEST
 
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+START_TEST (scoreboard_entry_lock_test) {
+  int fd = -1, lock_type = -1, res;
 
-    fail("Failed to get entry PR_SCORE_CWD: %s", strerror(xerrno));
-  }
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res < 0, "Failed to handle bad lock type");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 
-  if (strcmp(val, "cwd") != 0) {
-    (void) unlink(path);
-    (void) unlink(mutex_path);
-    (void) rmdir(dir);
+  lock_type = F_RDLCK;
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res < 0, "Failed to handle bad file descriptor");
+  fail_unless(errno == EBADF, "Expected EBADF (%d), got %s (%d)", EBADF,
+    strerror(errno), errno);
 
-    fail("Expected '%s', got '%s'", "cwd", val);
-  }
+  fd = open(test_file2, O_CREAT|O_EXCL|O_RDWR, S_IRUSR|S_IWUSR);
+  fail_unless(fd >= 0, "Failed to open '%s': %s", test_file2, strerror(errno));
+
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_WRLCK;
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_UNLCK;
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
 
-  (void) unlink(path);
-  (void) unlink(mutex_path);
-  (void) rmdir(dir);
+  lock_type = F_WRLCK;
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  /* Note: apparently attempt to lock (again) a file on which a lock
+   * (of the same type) is already held will succeed.  Huh.
+   */
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_RDLCK;
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  lock_type = F_UNLCK;
+  res = pr_scoreboard_entry_lock(fd, lock_type);
+  fail_unless(res == 0, "Failed to lock fd %d: %s", fd, strerror(errno));
+
+  (void) unlink(test_file2);
 }
 END_TEST
 
@@ -1434,12 +1028,13 @@ Suite *tests_get_scoreboard_suite(void) {
   suite = suite_create("scoreboard");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, scoreboard_get_test);
   tcase_add_test(testcase, scoreboard_set_test);
+  tcase_add_test(testcase, scoreboard_set_mutex_test);
   tcase_add_test(testcase, scoreboard_open_close_test);
+  tcase_add_test(testcase, scoreboard_lock_test);
   tcase_add_test(testcase, scoreboard_delete_test);
   tcase_add_test(testcase, scoreboard_restore_test);
   tcase_add_test(testcase, scoreboard_rewind_test);
@@ -1451,9 +1046,10 @@ Suite *tests_get_scoreboard_suite(void) {
   tcase_add_test(testcase, scoreboard_entry_read_test);
   tcase_add_test(testcase, scoreboard_entry_get_test);
   tcase_add_test(testcase, scoreboard_entry_update_test);
+  tcase_add_test(testcase, scoreboard_entry_kill_test);
+  tcase_add_test(testcase, scoreboard_entry_lock_test);
   tcase_add_test(testcase, scoreboard_disabled_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/sets.c b/tests/api/sets.c
index eea5c6a..9f3deaf 100644
--- a/tests/api/sets.c
+++ b/tests/api/sets.c
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Sets API tests
- * $Id: sets.c,v 1.2 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Sets API tests */
 
 #include "tests.h"
 
@@ -449,7 +447,6 @@ Suite *tests_get_sets_suite(void) {
   TCase *testcase;
 
   suite = suite_create("sets");
-
   testcase = tcase_create("base");
 
   tcase_add_checked_fixture(testcase, set_up, tear_down);
@@ -462,6 +459,5 @@ Suite *tests_get_sets_suite(void) {
   tcase_add_test(testcase, set_copy_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/stash.c b/tests/api/stash.c
index 0afd17f..0fd1c94 100644
--- a/tests/api/stash.c
+++ b/tests/api/stash.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2010-2011 The ProFTPD Project team
+ * Copyright (c) 2010-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Stash API tests
- * $Id: stash.c,v 1.2 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Stash API tests */
 
 #include "tests.h"
 
@@ -41,8 +39,7 @@ static void set_up(void) {
 static void tear_down(void) {
   if (p) {
     destroy_pool(p);
-    p = NULL;
-    permanent_pool = NULL;
+    p = permanent_pool = NULL;
   } 
 }
 
@@ -54,37 +51,44 @@ START_TEST (stash_add_symbol_test) {
   
   res = pr_stash_add_symbol(0, NULL);
   fail_unless(res == -1, "Failed to handle null arguments");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
 
   res = pr_stash_add_symbol(0, "Foo");
   fail_unless(res == -1, "Failed to handle bad type");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&conftab, 0, sizeof(conftab));
   res = pr_stash_add_symbol(PR_SYM_CONF, &conftab);
   fail_unless(res == -1, "Failed to handle null conf name");
-  fail_unless(errno == EPERM, "Failed to set errno to EPERM (got %d)",
-    errno);
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&cmdtab, 0, sizeof(cmdtab));
   res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
   fail_unless(res == -1, "Failed to handle null cmd name");
-  fail_unless(errno == EPERM, "Failed to set errno to EPERM (got %d)",
-    errno);
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&authtab, 0, sizeof(authtab));
   res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
   fail_unless(res == -1, "Failed to handle null auth name");
-  fail_unless(errno == EPERM, "Failed to set errno to EPERM (got %d)",
-    errno);
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&hooktab, 0, sizeof(hooktab));
   res = pr_stash_add_symbol(PR_SYM_HOOK, &hooktab);
   fail_unless(res == -1, "Failed to handle null hook name");
-  fail_unless(errno == EPERM, "Failed to set errno to EPERM (got %d)",
-    errno);
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
+
+  memset(&conftab, 0, sizeof(conftab));
+  conftab.directive = pstrdup(p, "");
+  res = pr_stash_add_symbol(PR_SYM_CONF, &conftab);
+  fail_unless(res == -1, "Failed to handle empty conf name");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&conftab, 0, sizeof(conftab));
   conftab.directive = pstrdup(p, "Foo");
@@ -117,33 +121,33 @@ START_TEST (stash_get_symbol_test) {
 
   sym = pr_stash_get_symbol(0, NULL, NULL, NULL);
   fail_unless(sym == NULL, "Failed to handle null arguments");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
 
   sym = pr_stash_get_symbol(0, "foo", NULL, NULL);
   fail_unless(sym == NULL, "Failed to handle bad type argument");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
 
   sym = pr_stash_get_symbol(PR_SYM_CONF, "foo", NULL, NULL);
   fail_unless(sym == NULL, "Failed to handle nonexistent CONF symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   sym = pr_stash_get_symbol(PR_SYM_CMD, "foo", NULL, NULL);
   fail_unless(sym == NULL, "Failed to handle nonexistent CMD symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   sym = pr_stash_get_symbol(PR_SYM_AUTH, "foo", NULL, NULL);
   fail_unless(sym == NULL, "Failed to handle nonexistent AUTH symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   sym = pr_stash_get_symbol(PR_SYM_HOOK, "foo", NULL, NULL);
   fail_unless(sym == NULL, "Failed to handle nonexistent HOOK symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&conftab, 0, sizeof(conftab));
   conftab.directive = pstrdup(p, "foo");
@@ -156,8 +160,8 @@ START_TEST (stash_get_symbol_test) {
 
   sym = pr_stash_get_symbol(PR_SYM_CONF, "foo", sym, NULL);
   fail_unless(sym == NULL, "Unexpectedly found CONF symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&cmdtab, 0, sizeof(cmdtab));
   cmdtab.command = pstrdup(p, "foo");
@@ -170,8 +174,8 @@ START_TEST (stash_get_symbol_test) {
 
   sym = pr_stash_get_symbol(PR_SYM_CMD, "foo", sym, NULL);
   fail_unless(sym == NULL, "Unexpectedly found CMD symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&authtab, 0, sizeof(authtab));
   authtab.name = pstrdup(p, "foo");
@@ -184,8 +188,8 @@ START_TEST (stash_get_symbol_test) {
 
   sym = pr_stash_get_symbol(PR_SYM_AUTH, "foo", sym, NULL);
   fail_unless(sym == NULL, "Unexpectedly found AUTH symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 
   memset(&hooktab, 0, sizeof(hooktab));
   hooktab.command = pstrdup(p, "foo");
@@ -198,11 +202,164 @@ START_TEST (stash_get_symbol_test) {
 
   sym = pr_stash_get_symbol(PR_SYM_HOOK, "foo", sym, NULL);
   fail_unless(sym == NULL, "Unexpectedly found HOOK symbol");
-  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT (got %d)",
-    errno);
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
 }
 END_TEST
 
+START_TEST (stash_get_symbol2_test) {
+  int res;
+  void *sym;
+  conftable conftab;
+  cmdtable cmdtab, hooktab;
+  authtable authtab;
+
+  sym = pr_stash_get_symbol2(0, NULL, NULL, NULL, NULL);
+  fail_unless(sym == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  sym = pr_stash_get_symbol2(0, "foo", NULL, NULL, NULL);
+  fail_unless(sym == NULL, "Failed to handle bad type argument");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_CONF, "foo", NULL, NULL, NULL);
+  fail_unless(sym == NULL, "Failed to handle nonexistent CONF symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_CMD, "foo", NULL, NULL, NULL);
+  fail_unless(sym == NULL, "Failed to handle nonexistent CMD symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_AUTH, "foo", NULL, NULL, NULL);
+  fail_unless(sym == NULL, "Failed to handle nonexistent AUTH symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_HOOK, "foo", NULL, NULL, NULL);
+  fail_unless(sym == NULL, "Failed to handle nonexistent HOOK symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  memset(&conftab, 0, sizeof(conftab));
+  conftab.directive = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_CONF, &conftab);
+  fail_unless(res == 0, "Failed to add CONF symbol: %s", strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_CONF, "foo", NULL, NULL, NULL);
+  fail_unless(sym != NULL, "Failed to get CONF symbol: %s", strerror(errno));
+  fail_unless(sym == &conftab, "Expected %p, got %p", &conftab, sym);
+
+  sym = pr_stash_get_symbol2(PR_SYM_CONF, "foo", sym, NULL, NULL);
+  fail_unless(sym == NULL, "Unexpectedly found CONF symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  memset(&cmdtab, 0, sizeof(cmdtab));
+  cmdtab.command = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_CMD, "foo", NULL, NULL, NULL);
+  fail_unless(sym != NULL, "Failed to get CMD symbol: %s", strerror(errno));
+  fail_unless(sym == &cmdtab, "Expected %p, got %p", &cmdtab, sym);
+
+  sym = pr_stash_get_symbol2(PR_SYM_CMD, "foo", sym, NULL, NULL);
+  fail_unless(sym == NULL, "Unexpectedly found CMD symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add AUTH symbol: %s", strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_AUTH, "foo", NULL, NULL, NULL);
+  fail_unless(sym != NULL, "Failed to get AUTH symbol: %s", strerror(errno));
+  fail_unless(sym == &authtab, "Expected %p, got %p", &authtab, sym);
+
+  sym = pr_stash_get_symbol2(PR_SYM_AUTH, "foo", sym, NULL, NULL);
+  fail_unless(sym == NULL, "Unexpectedly found AUTH symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  memset(&hooktab, 0, sizeof(hooktab));
+  hooktab.command = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_HOOK, &hooktab);
+  fail_unless(res == 0, "Failed to add HOOK symbol: %s", strerror(errno));
+
+  sym = pr_stash_get_symbol2(PR_SYM_HOOK, "foo", NULL, NULL, NULL);
+  fail_unless(sym != NULL, "Failed to get HOOK symbol: %s", strerror(errno));
+  fail_unless(sym == &hooktab, "Expected %p, got %p", &hooktab, sym);
+
+  sym = pr_stash_get_symbol2(PR_SYM_HOOK, "foo", sym, NULL, NULL);
+  fail_unless(sym == NULL, "Unexpectedly found HOOK symbol");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+}
+END_TEST
+
+#ifdef PR_USE_DEVEL
+static void stash_dump(const char *fmt, ...) {
+}
+
+START_TEST (stash_dump_test) {
+  int res;
+  conftable conftab;
+  cmdtable cmdtab, hooktab;
+  authtable authtab;
+  module m;
+
+  mark_point();
+  pr_stash_dump(stash_dump);
+
+  memset(&conftab, 0, sizeof(conftab));
+  conftab.directive = pstrdup(p, "Conf");
+  res = pr_stash_add_symbol(PR_SYM_CONF, &conftab);
+  fail_unless(res == 0, "Failed to add CONF symbol: %s", strerror(errno));
+
+  memset(&cmdtab, 0, sizeof(cmdtab));
+  cmdtab.command = pstrdup(p, "Cmd");
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = pstrdup(p, "Auth");
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add AUTH symbol: %s", strerror(errno));
+
+  memset(&m, 0, sizeof(m));
+  m.name = "testsuite";
+  memset(&hooktab, 0, sizeof(hooktab));
+  hooktab.command = pstrdup(p, "Hook");
+  hooktab.m = &m;
+  res = pr_stash_add_symbol(PR_SYM_HOOK, &hooktab);
+  fail_unless(res == 0, "Failed to add HOOK symbol: %s", strerror(errno));
+
+  mark_point();
+  pr_stash_dump(stash_dump);
+
+  res = pr_stash_remove_symbol(PR_SYM_CONF, "Conf", NULL);
+  fail_unless(res > 0, "Failed to remove CONF symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_symbol(PR_SYM_CMD, "Cmd", NULL);
+  fail_unless(res > 0, "Failed to remove CMD symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_symbol(PR_SYM_AUTH, "Auth", NULL);
+  fail_unless(res > 0, "Failed to remove AUTH symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_symbol(PR_SYM_HOOK, "Hook", NULL);
+  fail_unless(res > 0, "Failed to remove HOOK symbol: %s", strerror(errno));
+
+  mark_point();
+  pr_stash_dump(stash_dump);
+}
+END_TEST
+#endif /* PR_USE_DEVEL */
+
 START_TEST (stash_remove_symbol_test) {
   int res;
   conftable conftab;
@@ -211,13 +368,13 @@ START_TEST (stash_remove_symbol_test) {
 
   res = pr_stash_remove_symbol(0, NULL, NULL);
   fail_unless(res == -1, "Failed to handle null arguments");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
 
   res = pr_stash_remove_symbol(0, "foo", NULL);
   fail_unless(res == -1, "Failed to handle bad symbol type");
-  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL (got %d)",
-    errno);
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
 
   res = pr_stash_remove_symbol(PR_SYM_CONF, "foo", NULL);
   fail_unless(res == 0, "Expected %d, got %d", 0, res);
@@ -268,20 +425,186 @@ START_TEST (stash_remove_symbol_test) {
 }
 END_TEST
 
+START_TEST (stash_remove_conf_test) {
+  int res;
+  conftable conftab;
+
+  res = pr_stash_remove_conf(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_stash_remove_conf("foo", NULL);
+  fail_unless(res == 0, "Expected %d, got %d", 0, res);
+
+  memset(&conftab, 0, sizeof(conftab));
+  conftab.directive = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_CONF, &conftab);
+  fail_unless(res == 0, "Failed to add CONF symbol: %s", strerror(errno));
+
+  res = pr_stash_add_symbol(PR_SYM_CONF, &conftab);
+  fail_unless(res == 0, "Failed to add CONF symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_conf("foo", NULL);
+  fail_unless(res == 2, "Expected %d, got %d", 2, res);
+}
+END_TEST
+
+START_TEST (stash_remove_cmd_test) {
+  int res;
+  cmdtable cmdtab, cmdtab2;
+
+  res = pr_stash_remove_cmd(NULL, NULL, 0, NULL, -1);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_stash_remove_cmd("foo", NULL, 0, NULL, -1);
+  fail_unless(res == 0, "Expected %d, got %d", 0, res);
+
+  memset(&cmdtab, 0, sizeof(cmdtab));
+  cmdtab.command = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  memset(&cmdtab2, 0, sizeof(cmdtab2));
+  cmdtab2.command = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab2);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_cmd("foo", NULL, 0, NULL, -1);
+  fail_unless(res == 2, "Expected %d, got %d", 2, res);
+
+  /* Remove only the PRE_CMD cmd handlers */
+  mark_point();
+  memset(&cmdtab, 0, sizeof(cmdtab));
+  cmdtab.command = pstrdup(p, "foo");
+  cmdtab.cmd_type = PRE_CMD;
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  memset(&cmdtab2, 0, sizeof(cmdtab2));
+  cmdtab2.command = pstrdup(p, "foo");
+  cmdtab2.cmd_type = CMD;
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab2);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  mark_point();
+  res = pr_stash_remove_cmd("foo", NULL, PRE_CMD, NULL, -1);
+  fail_unless(res == 1, "Expected %d, got %d", 1, res);
+  (void) pr_stash_remove_symbol(PR_SYM_CMD, "foo", NULL);
+
+  /* Remove only the G_WRITE cmd handlers */
+  mark_point();
+  memset(&cmdtab, 0, sizeof(cmdtab));
+  cmdtab.command = pstrdup(p, "foo");
+  cmdtab.group = G_WRITE;
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  memset(&cmdtab2, 0, sizeof(cmdtab2));
+  cmdtab2.command = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab2);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  mark_point();
+  res = pr_stash_remove_cmd("foo", NULL, 0, G_WRITE, -1);
+  fail_unless(res == 1, "Expected %d, got %d", 1, res);
+  (void) pr_stash_remove_symbol(PR_SYM_CMD, "foo", NULL);
+
+  /* Remove only the CL_SFTP cmd handlers */
+  mark_point();
+  memset(&cmdtab, 0, sizeof(cmdtab));
+  cmdtab.command = pstrdup(p, "foo");
+  cmdtab.cmd_class = CL_SFTP;
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  memset(&cmdtab2, 0, sizeof(cmdtab2));
+  cmdtab2.command = pstrdup(p, "foo");
+  cmdtab2.cmd_class = CL_MISC;
+  res = pr_stash_add_symbol(PR_SYM_CMD, &cmdtab2);
+  fail_unless(res == 0, "Failed to add CMD symbol: %s", strerror(errno));
+
+  mark_point();
+  res = pr_stash_remove_cmd("foo", NULL, 0, NULL, CL_SFTP);
+  fail_unless(res == 1, "Expected %d, got %d", 1, res);
+  (void) pr_stash_remove_symbol(PR_SYM_CMD, "foo", NULL);
+}
+END_TEST
+
+START_TEST (stash_remove_auth_test) {
+  int res;
+  authtable authtab;
+
+  res = pr_stash_remove_auth(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_stash_remove_auth("foo", NULL);
+  fail_unless(res == 0, "Expected %d, got %d", 0, res);
+
+  memset(&authtab, 0, sizeof(authtab));
+  authtab.name = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add AUTH symbol: %s", strerror(errno));
+
+  res = pr_stash_add_symbol(PR_SYM_AUTH, &authtab);
+  fail_unless(res == 0, "Failed to add AUTH symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_auth("foo", NULL);
+  fail_unless(res == 2, "Expected %d, got %d", 2, res);
+}
+END_TEST
+
+START_TEST (stash_remove_hook_test) {
+  int res;
+  cmdtable hooktab;
+
+  res = pr_stash_remove_hook(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_stash_remove_hook("foo", NULL);
+  fail_unless(res == 0, "Expected %d, got %d", 0, res);
+
+  memset(&hooktab, 0, sizeof(hooktab));
+  hooktab.command = pstrdup(p, "foo");
+  res = pr_stash_add_symbol(PR_SYM_HOOK, &hooktab);
+  fail_unless(res == 0, "Failed to add HOOK symbol: %s", strerror(errno));
+
+  res = pr_stash_add_symbol(PR_SYM_HOOK, &hooktab);
+  fail_unless(res == 0, "Failed to add HOOK symbol: %s", strerror(errno));
+
+  res = pr_stash_remove_hook("foo", NULL);
+  fail_unless(res == 2, "Expected %d, got %d", 2, res);
+}
+END_TEST
+
 Suite *tests_get_stash_suite(void) {
   Suite *suite;
   TCase *testcase;
 
   suite = suite_create("stash");
-
   testcase = tcase_create("stash");
+
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, stash_add_symbol_test);
   tcase_add_test(testcase, stash_get_symbol_test);
+  tcase_add_test(testcase, stash_get_symbol2_test);
   tcase_add_test(testcase, stash_remove_symbol_test);
+#ifdef PR_USE_DEVEL
+  tcase_add_test(testcase, stash_dump_test);
+#endif /* PR_USE_DEVEL */
 
-  suite_add_tcase(suite, testcase);
+  tcase_add_test(testcase, stash_remove_conf_test);
+  tcase_add_test(testcase, stash_remove_cmd_test);
+  tcase_add_test(testcase, stash_remove_auth_test);
+  tcase_add_test(testcase, stash_remove_hook_test);
 
+  suite_add_tcase(suite, testcase);
   return suite;
 }
diff --git a/tests/api/str.c b/tests/api/str.c
index a05de2c..7c6e110 100644
--- a/tests/api/str.c
+++ b/tests/api/str.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2015 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* String API tests
- * $Id: str.c,v 1.12 2014-01-27 18:34:44 castaglia Exp $
- */
+/* String API tests. */
 
 #include "tests.h"
 
@@ -74,7 +72,7 @@ START_TEST (sstrncpy_test) {
   len = 1;
 
   res = sstrncpy(dst, ok, len);
-  fail_unless(res <= len, "Expected result %d, got %d", len, res);
+  fail_unless((size_t) res <= len, "Expected result %d, got %d", len, res);
   fail_unless(strlen(dst) == (len - 1), "Expected len %u, got len %u", len - 1,
     strlen(dst));
   fail_unless(dst[len-1] == '\0', "Expected NUL, got '%c'", dst[len-1]);
@@ -83,7 +81,7 @@ START_TEST (sstrncpy_test) {
   len = 7;
 
   res = sstrncpy(dst, ok, len);
-  fail_unless(res <= len, "Expected result %d, got %d", len, res);
+  fail_unless((size_t) res <= len, "Expected result %d, got %d", len, res);
   fail_unless(strlen(dst) == (len - 1), "Expected len %u, got len %u", len - 1,
     strlen(dst));
   fail_unless(dst[len-1] == '\0', "Expected NUL, got '%c'", dst[len-1]);
@@ -92,7 +90,7 @@ START_TEST (sstrncpy_test) {
   len = sz;
 
   res = sstrncpy(dst, ok, len);
-  fail_unless(res <= len, "Expected result %d, got %d", len, res);
+  fail_unless((size_t) res <= len, "Expected result %d, got %d", len, res);
   fail_unless(strlen(dst) == (len - 1), "Expected len %u, got len %u", len - 1,
     strlen(dst));
   fail_unless(dst[len-1] == '\0', "Expected NUL, got '%c'", dst[len-1]);
@@ -101,7 +99,7 @@ START_TEST (sstrncpy_test) {
   len = sz;
 
   res = sstrncpy(dst, "", len);
-  fail_unless(res <= len, "Expected result %d, got %d", len, res);
+  fail_unless((size_t) res <= len, "Expected result %d, got %d", len, res);
   fail_unless(strlen(dst) == 0, "Expected len %u, got len %u", 0, strlen(dst));
   fail_unless(*dst == '\0', "Expected NUL, got '%c'", *dst);
 }
@@ -142,6 +140,7 @@ START_TEST (sstrcat_test) {
 
   fail_unless(dst[1] == 0, "Failed to terminate destination buffer");
 
+  mark_point();
   src[0] = 'f';
   src[1] = '\0';
   dst[0] = 'e';
@@ -149,28 +148,45 @@ START_TEST (sstrcat_test) {
   res = sstrcat(dst, src, 3);
   fail_unless(res == dst, "Returned wrong destination buffer");
 
+  mark_point();
   fail_unless(dst[0] == 'e',
     "Failed to preserve destination buffer (expected '%c' at index 0, "
     "got '%c')", 'e', dst[0]);
 
+  mark_point();
   fail_unless(dst[1] == 'f',
     "Failed to copy source buffer (expected '%c' at index 1, got '%c')",
     'f', dst[1]);
 
+  mark_point();
   fail_unless(dst[2] == 0, "Failed to terminate destination buffer");
 
-  memset(src, c, sizeof(src));
+  mark_point();
+  memset(src, c, sizeof(src)-1);
 
+  /* Note: we need to NUL-terminate the source buffer, for e.g. strlcat(3)
+   * implementations.  Failure to do so can yield SIGABRT/SIGSEGV problems
+   * during e.g. unit tests.
+   */
+  src[sizeof(src)-1] = '\0';
   dst[0] = '\0';
+
+  mark_point();
   res = sstrcat(dst, src, sizeof(dst));
+
+  mark_point();
   fail_unless(res == dst, "Returned wrong destination buffer");
+
+  mark_point();
   fail_unless(dst[sizeof(dst)-1] == 0,
     "Failed to terminate destination buffer");
 
+  mark_point();
   fail_unless(strlen(dst) == (sizeof(dst)-1),
     "Failed to copy all the data (expected len %u, got len %u)",
     sizeof(dst)-1, strlen(dst));
 
+  mark_point();
   for (i = 0; i < sizeof(dst)-1; i++) {
     fail_unless(dst[i] == c, "Copied wrong value (expected '%c', got '%c')",
       c, dst[i]);
@@ -179,7 +195,8 @@ START_TEST (sstrcat_test) {
 END_TEST
 
 START_TEST (sreplace_test) {
-  char *fmt = NULL, *res, *ok;
+  const char *res;
+  char *fmt = NULL, *ok;
 
   res = sreplace(NULL, NULL, 0);
   fail_unless(res == NULL, "Failed to handle invalid arguments");
@@ -227,7 +244,8 @@ START_TEST (sreplace_test) {
 END_TEST
 
 START_TEST (sreplace_enospc_test) {
-  char *fmt = NULL, *res;
+  const char *res;
+  char *fmt = NULL;
   size_t bufsz = 8192;
 
   fmt = palloc(p, bufsz + 1);
@@ -243,7 +261,8 @@ START_TEST (sreplace_enospc_test) {
 END_TEST
 
 START_TEST (sreplace_bug3614_test) {
-  char *fmt = NULL, *res, *ok;
+  const char *res;
+  char *fmt = NULL, *ok;
 
   fmt = "%a %b %c %d %e %f %g %h %i %j %k %l %m "
         "%n %o %p %q %r %s %t %u %v %w %x %y %z "
@@ -303,7 +322,8 @@ START_TEST (sreplace_bug3614_test) {
 END_TEST
 
 START_TEST (str_replace_test) {
-  char *fmt = NULL, *res, *ok;
+  const char *res;
+  char *fmt = NULL, *ok;
   int max_replace = PR_STR_MAX_REPLACEMENTS;
 
   res = pr_str_replace(NULL, max_replace, NULL, 0);
@@ -484,7 +504,7 @@ START_TEST (pstrndup_test) {
 END_TEST
 
 START_TEST (strip_test) {
-  char *ok, *res, *str;
+  const char *ok, *res, *str;
 
   res = pr_str_strip(NULL, NULL);
   fail_unless(res == NULL, "Failed to handle null arguments");
@@ -721,7 +741,8 @@ START_TEST (get_word_test) {
   ok = "foo";
   fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
 
-  str = pstrdup(p, "foo \"bar\" baz");
+  /* Test multiple embedded quotes. */
+  str = pstrdup(p, "foo \"bar baz\" qux \"quz norf\"");
   res = pr_str_get_word(&str, 0);
   fail_unless(res != NULL, "Failed to handle quoted argument: %s",
     strerror(errno));
@@ -733,18 +754,62 @@ START_TEST (get_word_test) {
   fail_unless(res != NULL, "Failed to handle quoted argument: %s",
     strerror(errno));
 
-  ok = "bar";
+  ok = "bar baz";
   fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
 
   res = pr_str_get_word(&str, 0);
   fail_unless(res != NULL, "Failed to handle quoted argument: %s",
     strerror(errno));
 
-  ok = "baz";
+  ok = "qux";
+  fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
+
+  res = pr_str_get_word(&str, 0);
+  fail_unless(res != NULL, "Failed to handle quoted argument: %s",
+    strerror(errno));
+
+  ok = "quz norf";
   fail_unless(strcmp(res, ok) == 0, "Expected '%s', got '%s'", ok, res);
 }
 END_TEST
 
+START_TEST (get_word_utf8_test) {
+  const char *path;
+  FILE *fh;
+
+  /* Test UT8 spaces. Note that in order to do this, I had to use
+   * some other tool (Perl) to emit the desired UTF8 characters to
+   * a file; we then read in the bytes to parse from that file.  Some
+   * compilers (e.g. gcc), in conjunction with the terminal/editor I'm
+   * using, don't like using the '\uNNNN' syntax for encoding UTF8 in C
+   * source code.
+   */
+
+  path = "api/etc/str/utf8-space.txt";
+  fh = fopen(path, "r"); 
+  if (fh != NULL) {
+    char *ok, *res, *str;
+    size_t nread = 0, sz;
+
+    sz = 256;
+    str = pcalloc(p, sz);
+
+    nread = fread(str, sizeof(char), sz-1, fh);
+    fail_if(ferror(fh), "Error reading '%s': %s", path, strerror(errno));
+    fail_unless(nread > 0, "Expected >0 bytes read, got 0");
+
+    res = pr_str_get_word(&str, 0);
+      fail_unless(res != NULL, "Failed to handle UTF8 argument: %s",
+      strerror(errno));
+
+    ok = "foo";
+    fail_if(strcmp(res, ok) == 0, "Did NOT expect '%s'", ok);
+
+    fclose(fh);
+  }
+}
+END_TEST
+
 START_TEST (is_boolean_test) {
   int res;
 
@@ -788,6 +853,9 @@ START_TEST (is_fnmatch_test) {
   int res;
   char *str;
 
+  res = pr_str_is_fnmatch(NULL);
+  fail_unless(res == FALSE, "Expected false for NULL");
+
   str = "foo";
   res = pr_str_is_fnmatch(str);
   fail_if(res != FALSE, "Expected false for string '%s'", str);
@@ -1177,6 +1245,487 @@ START_TEST (strnrstr_test) {
 }
 END_TEST
 
+START_TEST (bin2hex_test) {
+  char *expected, *res;
+  const unsigned char *str;
+
+  res = pr_str_bin2hex(NULL, NULL, 0, 0);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_str_bin2hex(p, NULL, 0, 0);
+  fail_unless(res == NULL, "Failed to handle null data argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Empty string. */
+  str = (const unsigned char *) "foobar";
+  expected = "";
+  res = pr_str_bin2hex(p, (const unsigned char *) str, 0, 0);
+  fail_unless(res != NULL, "Failed to hexify '%s': %s", str, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'",
+    expected, res);
+
+  /* default (lowercase) */
+  expected = "666f6f626172";
+  res = pr_str_bin2hex(p, str, strlen((char *) str), 0);
+  fail_unless(res != NULL, "Failed to hexify '%s': %s", str, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'",
+    expected, res);
+
+  /* lowercase */
+  expected = "666f6f626172";
+  res = pr_str_bin2hex(p, str, strlen((char *) str), 0);
+  fail_unless(res != NULL, "Failed to hexify '%s': %s", str, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'",
+    expected, res);
+
+  /* uppercase */
+  expected = "666F6F626172";
+  res = pr_str_bin2hex(p, str, strlen((char *) str), PR_STR_FL_HEX_USE_UC);
+  fail_unless(res != NULL, "Failed to hexify '%s': %s", str, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'",
+    expected, res);
+}
+END_TEST
+
+START_TEST (hex2bin_test) {
+  unsigned char *expected, *res;
+  const unsigned char *hex;
+  size_t expected_len, hex_len, len;
+
+  res = pr_str_hex2bin(NULL, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_str_hex2bin(p, NULL, 0, 0);
+  fail_unless(res == NULL, "Failed to handle null data argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  /* Empty string. */
+  hex = (const unsigned char *) "";
+  hex_len = strlen((char *) hex);
+  expected = (unsigned char *) "";
+  res = pr_str_hex2bin(p, hex, hex_len, &len);
+  fail_unless(res != NULL, "Failed to unhexify '%s': %s", hex, strerror(errno));
+  fail_unless(strcmp((const char *) res, (const char *) expected) == 0,
+    "Expected '%s', got '%s'", expected, res);
+
+  hex = (const unsigned char *) "112233";
+  hex_len = strlen((char *) hex);
+  expected_len = 3;
+  expected = palloc(p, expected_len);
+  expected[0] = 17;
+  expected[1] = 34;
+  expected[2] = 51;
+
+  res = pr_str_hex2bin(p, (const unsigned char *) hex, hex_len, &len);
+  fail_unless(res != NULL, "Failed to unhexify '%s': %s", hex, strerror(errno));
+  fail_unless(len == expected_len, "Expected len %lu, got %lu",
+    (unsigned long) expected_len, len);
+  fail_unless(memcmp(res, expected, len) == 0,
+    "Did not receive expected unhexified data");
+
+  /* lowercase */
+  hex = (const unsigned char *) "666f6f626172";
+  hex_len = strlen((char *) hex);
+  expected_len = 6;
+  expected = palloc(p, expected_len);
+  expected[0] = 'f';
+  expected[1] = 'o';
+  expected[2] = 'o';
+  expected[3] = 'b';
+  expected[4] = 'a';
+  expected[5] = 'r';
+
+  res = pr_str_hex2bin(p, (const unsigned char *) hex, hex_len, &len);
+  fail_unless(res != NULL, "Failed to unhexify '%s': %s", hex, strerror(errno));
+  fail_unless(len == expected_len, "Expected len %lu, got %lu",
+    (unsigned long) expected_len, len);
+  fail_unless(memcmp(res, expected, len) == 0,
+    "Did not receive expected unhexified data");
+
+  /* uppercase */
+  hex = (const unsigned char *) "666F6F626172";
+  hex_len = strlen((char *) hex);
+
+  res = pr_str_hex2bin(p, (const unsigned char *) hex, hex_len, &len);
+  fail_unless(res != NULL, "Failed to unhexify '%s': %s", hex, strerror(errno));
+  fail_unless(len == expected_len, "Expected len %lu, got %lu",
+    (unsigned long) expected_len, len);
+  fail_unless(memcmp(res, expected, len) == 0,
+    "Did not receive expected unhexified data");
+
+  /* Handle known not-hex data properly. */
+  hex = (const unsigned char *) "Hello, World!\n";
+  hex_len = strlen((char *) hex);
+  res = pr_str_hex2bin(p, hex, hex_len, &len);
+  fail_unless(res == NULL, "Successfully unhexified '%s' unexpectedly", hex);
+  fail_unless(errno == ERANGE, "Expected ERANGE (%d), got %s (%d)", ERANGE,
+    strerror(errno), errno);
+}
+END_TEST
+
+START_TEST (levenshtein_test) {
+  int res, expected, flags = 0;
+  const char *a, *b;
+
+  mark_point();
+  res = pr_str_levenshtein(NULL, NULL, NULL, 0, 0, 0, 0, flags);
+  fail_unless(res < 0, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_str_levenshtein(p, NULL, NULL, 0, 0, 0, 0, flags);
+  fail_unless(res < 0, "Failed to handle null a string");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  a = "foo";
+
+  mark_point();
+  res = pr_str_levenshtein(p, a, NULL, 0, 0, 0, 0, flags);
+  fail_unless(res < 0, "Failed to handle null b string");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  expected = 0;
+  b = "Foo";
+
+  mark_point();
+  res = pr_str_levenshtein(p, a, b, 0, 0, 0, 0, flags);
+  fail_if(res < 0,
+    "Failed to compute Levenshtein distance from '%s' to '%s': %s", a, b,
+    strerror(errno));
+  fail_unless(expected == res, "Expected distance %d, got %d", expected, res);
+
+  expected = 3;
+  b = "Foo";
+  res = pr_str_levenshtein(p, a, b, 0, 1, 1, 1, flags);
+  fail_if(res < 0,
+    "Failed to compute Levenshtein distance from '%s' to '%s': %s", a, b,
+    strerror(errno));
+  fail_unless(expected == res, "Expected distance %d, got %d", expected, res);
+
+  flags = PR_STR_FL_IGNORE_CASE;
+  expected = 2;
+  b = "Foo";
+  res = pr_str_levenshtein(p, a, b, 0, 1, 1, 1, flags);
+  fail_if(res < 0,
+    "Failed to compute Levenshtein distance from '%s' to '%s': %s", a, b,
+    strerror(errno));
+  fail_unless(expected == res, "Expected distance %d, got %d", expected, res);
+}
+END_TEST
+
+START_TEST (similars_test) {
+  array_header *res, *candidates;
+  const char *s, **similars, *expected;
+
+  mark_point();
+  res = pr_str_get_similars(NULL, NULL, NULL, 0, 0);
+  fail_unless(res == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_str_get_similars(p, NULL, NULL, 0, 0);
+  fail_unless(res == NULL, "Failed to handle null string");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  s = "foo";
+
+  mark_point();
+  res = pr_str_get_similars(p, s, NULL, 0, 0);
+  fail_unless(res == NULL, "Failed to handle null candidates");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  candidates = make_array(p, 5, sizeof(const char *));
+
+  mark_point();
+  res = pr_str_get_similars(p, s, candidates, 0, 0);
+  fail_unless(res == NULL, "Failed to handle empty candidates");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
+  *((const char **) push_array(candidates)) = pstrdup(p, "fools");
+  *((const char **) push_array(candidates)) = pstrdup(p, "odd");
+  *((const char **) push_array(candidates)) = pstrdup(p, "bar");
+  *((const char **) push_array(candidates)) = pstrdup(p, "FOO");
+
+  mark_point();
+  res = pr_str_get_similars(p, s, candidates, 0, 0);
+  fail_unless(res != NULL, "Failed to find similar strings to '%s': %s", s,
+    strerror(errno));
+  fail_unless(res->nelts > 0, "Expected >0 similar strings, got %u",
+    res->nelts);
+
+  mark_point();
+  similars = (const char **) res->elts;
+
+  /* Note: We see different results here due to (I think) different
+   * qsort(3) implementations.
+   */
+
+  expected = "FOO";
+  if (strcmp(similars[0], expected) != 0) {
+    expected = "fools";
+  }
+
+  fail_unless(strcmp(similars[0], expected) == 0,
+    "Expected similar '%s', got '%s'", expected, similars[0]);
+
+  expected = "fools";
+  if (strcmp(similars[1], expected) != 0) {
+    expected = "FOO";
+  }
+
+  fail_unless(strcmp(similars[1], expected) == 0,
+    "Expected similar '%s', got '%s'", expected, similars[1]);
+
+  mark_point();
+  res = pr_str_get_similars(p, s, candidates, 0, PR_STR_FL_IGNORE_CASE);
+  fail_unless(res != NULL, "Failed to find similar strings to '%s': %s", s,
+    strerror(errno));
+  fail_unless(res->nelts > 0, "Expected >0 similar strings, got %u",
+    res->nelts);
+
+  mark_point();
+  similars = (const char **) res->elts;
+
+  expected = "FOO";
+  if (strcmp(similars[0], expected) != 0) {
+    expected = "fools";
+  }
+
+  fail_unless(strcmp(similars[0], expected) == 0,
+    "Expected similar '%s', got '%s'", expected, similars[0]);
+
+  expected = "fools";
+  if (strcmp(similars[1], expected) != 0) {
+    expected = "FOO";
+  }
+
+  fail_unless(strcmp(similars[1], expected) == 0,
+    "Expected similar '%s', got '%s'", expected, similars[1]);
+}
+END_TEST
+
+START_TEST (str2uid_test) {
+  int res;
+
+  res = pr_str2uid(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+}
+END_TEST
+
+START_TEST (str2gid_test) {
+  int res;
+
+  res = pr_str2gid(NULL, NULL);
+  fail_unless(res == -1, "Failed to handle null arguments");
+}
+END_TEST
+
+START_TEST (uid2str_test) {
+  const char *res;
+
+  res = pr_uid2str(NULL, (uid_t) 1);
+  fail_unless(strcmp(res, "1") == 0);
+
+  res = pr_uid2str(NULL, (uid_t) -1);
+  fail_unless(strcmp(res, "-1") == 0);
+}
+END_TEST
+
+START_TEST (gid2str_test) {
+  const char *res;
+
+  res = pr_gid2str(NULL, (gid_t) 1);
+  fail_unless(strcmp(res, "1") == 0);
+
+  res = pr_gid2str(NULL, (gid_t) -1);
+  fail_unless(strcmp(res, "-1") == 0);
+}
+END_TEST
+
+START_TEST (str_quote_test) {
+  const char *res;
+  char *expected, *path;
+
+  res = pr_str_quote(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_str_quote(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp/";
+  expected = path;
+  res = pr_str_quote(p, path);
+  fail_unless(res != NULL, "Failed to quote '%s': %s", path, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  path = "/\"tmp\"/";
+  expected = "/\"\"tmp\"\"/";
+  res = pr_str_quote(p, path);
+  fail_unless(res != NULL, "Failed to quote '%s': %s", path, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+}
+END_TEST
+
+START_TEST (quote_dir_test) {
+  const char *res;
+  char *expected, *path;
+
+  res = quote_dir(NULL, NULL);
+  fail_unless(res == NULL, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = quote_dir(p, NULL);
+  fail_unless(res == NULL, "Failed to handle null path argument");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  path = "/tmp/";
+  expected = path;
+  res = quote_dir(p, path);
+  fail_unless(res != NULL, "Failed to quote '%s': %s", path, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+
+  path = "/\"tmp\"/";
+  expected = "/\"\"tmp\"\"/";
+  res = quote_dir(p, path);
+  fail_unless(res != NULL, "Failed to quote '%s': %s", path, strerror(errno));
+  fail_unless(strcmp(res, expected) == 0, "Expected '%s', got '%s'", expected,
+    res);
+}
+END_TEST
+
+START_TEST (text_to_array_test) {
+  register unsigned int i;
+  array_header *res;
+  const char *text;
+
+  mark_point();
+  res = pr_str_text_to_array(NULL, NULL, ',');
+  fail_unless(res == NULL, "Failed to handle null pool");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_str_text_to_array(p, NULL, ',');
+  fail_unless(res == NULL, "Failed to handle null text");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  text = "";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, ',');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 0, "Expected 0 items, got %u", res->nelts);
+
+  text = ",";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, ',');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 0, "Expected 0 items, got %u", res->nelts);
+
+  text = ",,,";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, ',');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 0, "Expected 0 items, got %u", res->nelts);
+
+  text = "foo";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, ',');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 1, "Expected 1 item, got %u", res->nelts);
+
+  text = "foo,foo,foo";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, ',');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 3, "Expected 3 items, got %u", res->nelts);
+  for (i = 0; i < res->nelts; i++) {
+    char *item, *expected;
+
+    item = ((char **) res->elts)[i];
+    fail_unless(item != NULL, "Expected item at index %u, got null", i);
+
+    expected = "foo";
+    fail_unless(strcmp(item, expected) == 0,
+      "Expected '%s' at index %u, got '%s'", expected, i, item);
+  }
+
+  text = "foo,foo,foo,";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, ',');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 3, "Expected 3 items, got %u", res->nelts);
+  for (i = 0; i < res->nelts; i++) {
+    char *item, *expected;
+
+    item = ((char **) res->elts)[i];
+    fail_unless(item != NULL, "Expected item at index %u, got null", i);
+
+    if (i == 3) {
+      expected = "";
+
+    } else {
+      expected = "foo";
+    }
+
+    fail_unless(strcmp(item, expected) == 0,
+      "Expected '%s' at index %u, got '%s'", expected, i, item);
+  }
+
+  text = "foo|foo|foo";
+
+  mark_point();
+  res = pr_str_text_to_array(p, text, '|');
+  fail_unless(res != NULL, "Failed to handle text '%s': %s", text,
+    strerror(errno));
+  fail_unless(res->nelts == 3, "Expected 3 items, got %u", res->nelts);
+  for (i = 0; i < res->nelts; i++) {
+    char *item, *expected;
+
+    item = ((char **) res->elts)[i];
+    fail_unless(item != NULL, "Expected item at index %u, got null", i);
+
+    expected = "foo";
+    fail_unless(strcmp(item, expected) == 0,
+      "Expected '%s' at index %u, got '%s'", expected, i, item);
+  }
+}
+END_TEST
+
 Suite *tests_get_str_suite(void) {
   Suite *suite;
   TCase *testcase;
@@ -1184,7 +1733,6 @@ Suite *tests_get_str_suite(void) {
   suite = suite_create("str");
 
   testcase = tcase_create("base");
-
   tcase_add_checked_fixture(testcase, set_up, tear_down);
 
   tcase_add_test(testcase, sstrncpy_test);
@@ -1202,13 +1750,24 @@ Suite *tests_get_str_suite(void) {
   tcase_add_test(testcase, get_token_test);
   tcase_add_test(testcase, get_token2_test);
   tcase_add_test(testcase, get_word_test);
+  tcase_add_test(testcase, get_word_utf8_test);
   tcase_add_test(testcase, is_boolean_test);
   tcase_add_test(testcase, is_fnmatch_test);
   tcase_add_test(testcase, get_nbytes_test);
   tcase_add_test(testcase, get_duration_test);
+  tcase_add_test(testcase, bin2hex_test);
+  tcase_add_test(testcase, hex2bin_test);
+  tcase_add_test(testcase, levenshtein_test);
+  tcase_add_test(testcase, similars_test);
   tcase_add_test(testcase, strnrstr_test);
+  tcase_add_test(testcase, str2uid_test);
+  tcase_add_test(testcase, str2gid_test);
+  tcase_add_test(testcase, uid2str_test);
+  tcase_add_test(testcase, gid2str_test);
+  tcase_add_test(testcase, str_quote_test);
+  tcase_add_test(testcase, quote_dir_test);
+  tcase_add_test(testcase, text_to_array_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/stubs.c b/tests/api/stubs.c
index a2ff7c1..27e80dd 100644
--- a/tests/api/stubs.c
+++ b/tests/api/stubs.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server API testsuite
- * Copyright (c) 2008-2014 The ProFTPD Project team
+ * Copyright (c) 2008-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -34,19 +34,62 @@ server_rec *main_server = NULL;
 pid_t mpid = 1;
 module *static_modules[] = { NULL };
 module *loaded_modules = NULL;
+xaset_t *server_list = NULL;
 
-char *dir_realpath(pool *p, const char *path) {
-  return NULL;
+static cmd_rec *next_cmd = NULL;
+
+int tests_stubs_set_next_cmd(cmd_rec *cmd) {
+  next_cmd = cmd;
+  return 0;
+}
+
+int tests_stubs_set_main_server(server_rec *s) {
+  main_server = s;
+  return 0;
+}
+
+void init_dirtree(void) {
+  pool *main_pool;
+  xaset_t *servers;
+
+  main_pool = make_sub_pool(permanent_pool);
+  pr_pool_tag(main_pool, "testsuite#main_server pool");
+
+  servers = xaset_create(main_pool, NULL);
+
+  main_server = (server_rec *) pcalloc(main_pool, sizeof(server_rec));
+  xaset_insert(servers, (xasetmember_t *) main_server);
+
+  main_server->pool = main_pool;
+  main_server->set = servers;
+  main_server->sid = 1;
+  main_server->notes = pr_table_nalloc(main_pool, 0, 8);
+
+  /* TCP KeepAlive is enabled by default, with the system defaults. */
+  main_server->tcp_keepalive = palloc(main_server->pool,
+    sizeof(struct tcp_keepalive));
+  main_server->tcp_keepalive->keepalive_enabled = TRUE;
+  main_server->tcp_keepalive->keepalive_idle = -1;
+  main_server->tcp_keepalive->keepalive_count = -1;
+  main_server->tcp_keepalive->keepalive_intvl = -1;
+
+  main_server->ServerPort = 21;
 }
 
-void *get_param_ptr(xaset_t *set, const char *name, int recurse) {
-  errno = ENOENT;
-  return NULL;
+int pr_cmd_dispatch(cmd_rec *cmd) {
+  return 0;
 }
 
-struct passwd *pr_auth_getpwnam(pool *p, const char *name) {
-  errno = ENOENT;
-  return NULL;
+int pr_cmd_read(cmd_rec **cmd) {
+  if (next_cmd != NULL) {
+    *cmd = next_cmd;
+    next_cmd = NULL;
+
+  } else {
+    *cmd = NULL;
+  }
+
+  return 0;
 }
 
 int pr_config_get_server_xfer_bufsz(int direction) {
@@ -73,11 +116,11 @@ int pr_ctrls_unregister(module *m, const char *action) {
   return 0;
 }
 
-void pr_log_debug(int level, const char *fmt, ...) {
+void pr_log_auth(int level, const char *fmt, ...) {
   if (getenv("TEST_VERBOSE") != NULL) {
     va_list msg;
 
-    fprintf(stderr, "DEBUG%d: ", level);
+    fprintf(stderr, "AUTH%d: ", level);
 
     va_start(msg, fmt);
     vfprintf(stderr, fmt, msg);
@@ -87,11 +130,11 @@ void pr_log_debug(int level, const char *fmt, ...) {
   }
 }
 
-void pr_log_pri(int prio, const char *fmt, ...) {
+void pr_log_debug(int level, const char *fmt, ...) {
   if (getenv("TEST_VERBOSE") != NULL) {
     va_list msg;
 
-    fprintf(stderr, "PRI%d: ", prio);
+    fprintf(stderr, "DEBUG%d: ", level);
 
     va_start(msg, fmt);
     vfprintf(stderr, fmt, msg);
@@ -101,25 +144,21 @@ void pr_log_pri(int prio, const char *fmt, ...) {
   }
 }
 
-void pr_signals_handle(void) {
-}
-
-void pr_signals_block(void) {
-}
-
-void pr_signals_unblock(void) {
+int pr_log_event_generate(unsigned int log_type, int log_fd, int log_level,
+    const char *log_msg, size_t log_msglen) {
+  errno = ENOSYS;
+  return -1;
 }
 
-int pr_trace_get_level(const char *channel) {
-  return 0;
+int pr_log_event_listening(unsigned int log_type) {
+  return FALSE;
 }
 
-int pr_trace_msg(const char *channel, int level, const char *fmt, ...) {
-
+void pr_log_pri(int prio, const char *fmt, ...) {
   if (getenv("TEST_VERBOSE") != NULL) {
     va_list msg;
 
-    fprintf(stderr, "TRACE: <%s:%d>: ", channel, level);
+    fprintf(stderr, "PRI%d: ", prio);
 
     va_start(msg, fmt);
     vfprintf(stderr, fmt, msg);
@@ -127,9 +166,55 @@ int pr_trace_msg(const char *channel, int level, const char *fmt, ...) {
 
     fprintf(stderr, "\n");
   }
+}
+
+int pr_log_openfile(const char *log_file, int *log_fd, mode_t log_mode) {
+  int res;
+  struct stat st;
+
+  if (log_file == NULL ||
+      log_fd == NULL) {
+    errno = EINVAL;
+    return -1;
+  }
+
+  res = stat(log_file, &st);
+  if (res < 0) {
+    if (errno != ENOENT) {
+      return -1;
+    }
+
+  } else {
+    if (S_ISDIR(st.st_mode)) {
+      errno = EISDIR;
+      return -1;
+    }
+  }
 
+  *log_fd = STDERR_FILENO;
   return 0;
 }
 
-void run_schedule(void) {
+void pr_log_stacktrace(int fd, const char *name) {
+}
+
+int pr_proctitle_get(char *buf, size_t buflen) {
+  errno = ENOSYS;
+  return -1;
+}
+
+void pr_proctitle_set(const char *fmt, ...) {
+}
+
+void pr_proctitle_set_str(const char *str) {
+}
+
+void pr_session_disconnect(module *m, int reason_code, const char *details) {
+}
+
+int pr_session_set_idle(void) {
+  return 0;
+}
+
+void pr_signals_handle(void) {
 }
diff --git a/tests/api/table.c b/tests/api/table.c
index 822901c..178247b 100644
--- a/tests/api/table.c
+++ b/tests/api/table.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2012 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Table API tests
- * $Id: table.c,v 1.6 2012-01-26 17:55:07 castaglia Exp $
- */
+/* Table API tests */
 
 #include "tests.h"
 
@@ -49,16 +47,30 @@ static void tear_down(void) {
 
 static unsigned int b_val_count = 0;
 
-static int table_cb(const void *key, size_t keysz, void *value,
+static int do_cb(const void *key, size_t keysz, const void *value,
     size_t valuesz, void *user_data) {
 
-  if (*((char *) value) == 'b') {
+  if (*((const char *) value) == 'b') {
     b_val_count++;
   }
 
   return -1;
 }
 
+static int do_with_remove_cb(const void *key, size_t keysz, const void *value,
+    size_t valuesz, void *user_data) {
+  pr_table_t *tab;
+ 
+  tab = user_data;
+
+  if (*((const char *) value) == 'b') {
+    b_val_count++;
+  }
+
+  pr_table_kremove(tab, key, keysz, NULL);
+  return 0;
+}
+
 static void table_dump(const char *fmt, ...) {
 }
 
@@ -145,6 +157,10 @@ START_TEST (table_add_dup_test) {
   res = pr_table_add_dup(tab, "", NULL, 0);
   fail_unless(res == -1, "Failed to handle duplicate (empty) key");
   fail_unless(errno == EEXIST, "Failed to set errno to EEXIST");
+
+  mark_point();
+  res = pr_table_add_dup(tab, "foo", "bar", 0);
+  fail_unless(res == 0, "Failed to add 'foo': %s", strerror(errno));
 }
 END_TEST
 
@@ -215,6 +231,18 @@ START_TEST (table_exists_test) {
   ok = 2;
   res = pr_table_exists(tab, "foo");
   fail_unless(res == ok, "Expected value count %d, got %d", ok, res);
+
+  mark_point();
+  res = pr_table_kexists(NULL, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null table");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_table_kexists(tab, NULL, 0);
+  fail_unless(res < 0, "Failed to handle null key_data");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 }
 END_TEST
 
@@ -270,7 +298,7 @@ END_TEST
 
 START_TEST (table_get_test) {
   int ok, xerrno;
-  void *res;
+  const void *res;
   pr_table_t *tab;
   char *str;
   size_t sz;
@@ -310,6 +338,12 @@ START_TEST (table_get_test) {
 
   fail_unless(strcmp(str, "baz") == 0,
     "Expected value '%s', got '%s'", "baz", str);
+
+  mark_point();
+  res = pr_table_kget(NULL, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null table");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 }
 END_TEST
 
@@ -319,7 +353,7 @@ static unsigned int cache_key_hash(const void *key, size_t keysz) {
 
 START_TEST (table_get_use_cache_test) {
   int ok, xerrno;
-  void *res;
+  const void *res;
   pr_table_t *tab;
   const char *key = "bar";
   char *str;
@@ -370,7 +404,8 @@ END_TEST
 
 START_TEST (table_next_test) {
   int ok;
-  char *res;
+  const char *res;
+  size_t sz = 0;
   pr_table_t *tab;
 
   res = pr_table_next(NULL);
@@ -393,12 +428,24 @@ START_TEST (table_next_test) {
 
   res = pr_table_next(tab);
   fail_unless(res == NULL, "Expected no more keys, got '%s'", res);
+
+  pr_table_rewind(tab);
+
+  res = pr_table_knext(tab, &sz);
+  fail_unless(res != NULL, "Failed to get next key: %s", strerror(errno));
+  fail_unless(sz == 4, "Expected 4, got %lu", (unsigned long) sz);
+  fail_unless(strcmp(res, "foo") == 0,
+    "Expected key '%s', got '%s'", "foo", res);
+
+  sz = 0;
+  res = pr_table_knext(tab, &sz);
+  fail_unless(res == NULL, "Expected no more keys, got '%s'", res);
 }
 END_TEST
 
 START_TEST (table_rewind_test) {
   int res;
-  char *key;
+  const char *key;
   pr_table_t *tab;
 
   res = pr_table_rewind(NULL);
@@ -436,7 +483,8 @@ END_TEST
 
 START_TEST (table_remove_test) {
   int ok;
-  char *res, *str;
+  const char *res;
+  char *str;
   pr_table_t *tab;
   size_t sz;
 
@@ -473,13 +521,25 @@ START_TEST (table_remove_test) {
   res = pr_table_remove(tab, "foo", &sz);
   fail_unless(res == NULL, "Failed to handle absent value");
   fail_unless(errno == ENOENT, "Failed to set errno to ENOENT");
+
+  mark_point();
+  res = pr_table_kremove(NULL, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null table");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  mark_point();
+  res = pr_table_kremove(tab, NULL, 0, NULL);
+  fail_unless(res == NULL, "Failed to handle null key data");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
 }
 END_TEST
 
 START_TEST (table_set_test) {
   int res;
   pr_table_t *tab;
-  void *v;
+  const void *v;
   char *str;
   size_t sz;
 
@@ -497,6 +557,12 @@ START_TEST (table_set_test) {
   fail_unless(res == -1, "Failed to handle null value (len 1)");
   fail_unless(errno == EINVAL, "Failed to handle null value (len 1)");
 
+  mark_point();
+  res = pr_table_set(tab, "foo", "bar", 1);
+  fail_unless(res < 0, "Failed to handle empty table");
+  fail_unless(errno == ENOENT, "Expected ENOENT (%d), got %s (%d)", ENOENT,
+    strerror(errno), errno);
+
   res = pr_table_add(tab, "foo", "bar", 0);
   fail_unless(res == 0, "Failed to add 'foo' to table: %s", strerror(errno));
 
@@ -530,7 +596,7 @@ START_TEST (table_do_test) {
   fail_unless(res == -1, "Failed to handle null arguments");
   fail_unless(errno == EINVAL, "Failed to set errno to EINVAL");
 
-  res = pr_table_do(tab, table_cb, NULL, 0);
+  res = pr_table_do(tab, do_cb, NULL, 0);
   fail_unless(res == 0, "Failed to handle empty table");
 
   res = pr_table_add(tab, "foo", "bar", 0);
@@ -539,13 +605,32 @@ START_TEST (table_do_test) {
   res = pr_table_add(tab, "bar", "baz", 0);
   fail_unless(res == 0, "Failed to add 'bar' to table: %s", strerror(errno));
 
-  res = pr_table_do(tab, table_cb, NULL, 0);
+  res = pr_table_do(tab, do_cb, NULL, 0);
   fail_unless(res == -1, "Expected res %d, got %d", -1, res);
   fail_unless(errno == EPERM, "Failed to set errno to EPERM");
   fail_unless(b_val_count == 1, "Expected count %u, got %u", 1, b_val_count);
 
   b_val_count = 0;
-  res = pr_table_do(tab, table_cb, NULL, PR_TABLE_DO_FL_ALL);
+  res = pr_table_do(tab, do_cb, NULL, PR_TABLE_DO_FL_ALL);
+  fail_unless(res == 0, "Failed to do table: %s", strerror(errno));
+  fail_unless(b_val_count == 2, "Expected count %u, got %u", 2, b_val_count);
+}
+END_TEST
+
+START_TEST (table_do_with_remove_test) {
+  int res;
+  pr_table_t *tab;
+
+  tab = pr_table_alloc(p, 0);
+
+  res = pr_table_add(tab, "foo", "bar", 0);
+  fail_unless(res == 0, "Failed to add 'foo' to table: %s", strerror(errno));
+
+  res = pr_table_add(tab, "bar", "baz", 0);
+  fail_unless(res == 0, "Failed to add 'bar' to table: %s", strerror(errno));
+
+  b_val_count = 0;
+  res = pr_table_do(tab, do_with_remove_cb, tab, PR_TABLE_DO_FL_ALL);
   fail_unless(res == 0, "Failed to do table: %s", strerror(errno));
   fail_unless(b_val_count == 2, "Expected count %u, got %u", 2, b_val_count);
 }
@@ -563,9 +648,25 @@ START_TEST (table_ctl_test) {
 
   tab = pr_table_alloc(p, 0);
  
+  mark_point();
+  res = pr_table_ctl(tab, PR_TABLE_CTL_SET_ENT_INSERT, NULL);
+  fail_unless(res == 0, "Failed to set entry insert callback: %s",
+    strerror(errno));
+
+  mark_point();
+  res = pr_table_ctl(tab, PR_TABLE_CTL_SET_ENT_REMOVE, NULL);
+  fail_unless(res == 0, "Failed to set entry removal callback: %s",
+    strerror(errno));
+
   res = pr_table_add(tab, "foo", "bar", 0);
   fail_unless(res == 0, "Failed to add 'foo' to table: %s", strerror(errno));
 
+  mark_point();
+  res = pr_table_ctl(tab, PR_TABLE_CTL_SET_MAX_ENTS, 0);
+  fail_unless(res < 0, "Failed to handle SET_MAX_ENTS smaller than table");
+  fail_unless(errno == EPERM, "Expected EPERM (%d), got %s (%d)", EPERM,
+    strerror(errno), errno);
+
   res = pr_table_ctl(tab, 0, NULL);
   fail_unless(res == -1, "Failed to handle non-empty table");
   fail_unless(errno == EPERM, "Failed to set errno to EPERM");
@@ -691,7 +792,6 @@ Suite *tests_get_table_suite(void) {
   TCase *testcase;
 
   suite = suite_create("table");
-
   testcase = tcase_create("base");
 
   tcase_add_checked_fixture(testcase, set_up, tear_down);
@@ -711,12 +811,12 @@ Suite *tests_get_table_suite(void) {
   tcase_add_test(testcase, table_remove_test);
   tcase_add_test(testcase, table_set_test);
   tcase_add_test(testcase, table_do_test);
+  tcase_add_test(testcase, table_do_with_remove_test);
   tcase_add_test(testcase, table_ctl_test);
   tcase_add_test(testcase, table_load_test);
   tcase_add_test(testcase, table_dump_test);
   tcase_add_test(testcase, table_pcalloc_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/tests.c b/tests/api/tests.c
index 65eef77..e3e1778 100644
--- a/tests/api/tests.c
+++ b/tests/api/tests.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server API testsuite
- * Copyright (c) 2008-2014 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -53,79 +53,34 @@ static struct testsuite_info suites[] = {
   { "response",		tests_get_response_suite },
   { "fsio",		tests_get_fsio_suite },
   { "netio",		tests_get_netio_suite },
+  { "trace",		tests_get_trace_suite },
+  { "parser",		tests_get_parser_suite },
+  { "pidfile",		tests_get_pidfile_suite },
+  { "config",		tests_get_config_suite },
+  { "auth",		tests_get_auth_suite },
+  { "filter",		tests_get_filter_suite },
+  { "inet",		tests_get_inet_suite },
+  { "data",		tests_get_data_suite },
+  { "ascii",		tests_get_ascii_suite },
+  { "help",		tests_get_help_suite },
+  { "rlimit",		tests_get_rlimit_suite },
+  { "encode",		tests_get_encode_suite },
+  { "privs",		tests_get_privs_suite },
+  { "display",		tests_get_display_suite },
+  { "misc",		tests_get_misc_suite },
+  { "json",		tests_get_json_suite },
+  { "redis",		tests_get_redis_suite },
 
   { NULL, NULL }
 };
 
 static Suite *tests_get_suite(const char *suite) { 
-  if (strcmp(suite, "pool") == 0) { 
-    return tests_get_pool_suite();
- 
-  } else if (strcmp(suite, "array") == 0) {
-    return tests_get_array_suite(); 
+  register unsigned int i;
 
-  } else if (strcmp(suite, "str") == 0) {
-    return tests_get_str_suite(); 
-
-  } else if (strcmp(suite, "sets") == 0) {
-    return tests_get_sets_suite(); 
-
-  } else if (strcmp(suite, "timers") == 0) {
-    return tests_get_timers_suite(); 
-
-  } else if (strcmp(suite, "table") == 0) {
-    return tests_get_table_suite(); 
-
-  } else if (strcmp(suite, "var") == 0) {
-    return tests_get_var_suite(); 
-
-  } else if (strcmp(suite, "event") == 0) {
-    return tests_get_event_suite(); 
-
-  } else if (strcmp(suite, "version") == 0) {
-    return tests_get_version_suite(); 
-
-  } else if (strcmp(suite, "env") == 0) {
-    return tests_get_env_suite(); 
-
-  } else if (strcmp(suite, "feat") == 0) {
-    return tests_get_feat_suite(); 
-
-  } else if (strcmp(suite, "netaddr") == 0) {
-    return tests_get_netaddr_suite(); 
-
-  } else if (strcmp(suite, "netacl") == 0) {
-    return tests_get_netacl_suite();
-
-  } else if (strcmp(suite, "class") == 0) {
-    return tests_get_class_suite();
-
-  } else if (strcmp(suite, "regexp") == 0) {
-    return tests_get_regexp_suite();
-
-  } else if (strcmp(suite, "expr") == 0) {
-    return tests_get_expr_suite();
-
-  } else if (strcmp(suite, "scoreboard") == 0) {
-    return tests_get_scoreboard_suite();
-
-  } else if (strcmp(suite, "stash") == 0) {
-    return tests_get_stash_suite();
-
-  } else if (strcmp(suite, "modules") == 0) {
-    return tests_get_modules_suite();
-
-  } else if (strcmp(suite, "cmd") == 0) {
-    return tests_get_cmd_suite();
-
-  } else if (strcmp(suite, "response") == 0) {
-    return tests_get_response_suite();
-
-  } else if (strcmp(suite, "fsio") == 0) {
-    return tests_get_fsio_suite();
-
-  } else if (strcmp(suite, "netio") == 0) {
-    return tests_get_netio_suite();
+  for (i = 0; suites[i].name != NULL; i++) {
+    if (strcmp(suite, suites[i].name) == 0) {
+      return (*suites[i].get_suite)();
+    }
   }
 
   return NULL;
@@ -171,9 +126,17 @@ int main(int argc, char *argv[]) {
     }
   }
 
+  /* Configure the Trace API to write to stderr. */
+  pr_trace_use_stderr(TRUE);
+
   requested = getenv("PR_TEST_NOFORK");
   if (requested) {
     srunner_set_fork_status(runner, CK_NOFORK);
+  } else {
+    requested = getenv("CK_DEFAULT_TIMEOUT");
+    if (requested == NULL) {
+      setenv("CK_DEFAULT_TIMEOUT", "60", 1);
+    }
   }
 
   srunner_run_all(runner, CK_NORMAL);
diff --git a/tests/api/tests.h b/tests/api/tests.h
index 06845e4..3e27dea 100644
--- a/tests/api/tests.h
+++ b/tests/api/tests.h
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server API testsuite
- * Copyright (c) 2008-2014 The ProFTPD Project team
+ * Copyright (c) 2008-2017 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Testsuite management
- * $Id: tests.h,v 1.7 2014-01-06 06:58:23 castaglia Exp $
- */
+/* Testsuite management */
 
 #ifndef PR_TESTS_H
 #define PR_TESTS_H
@@ -38,6 +36,9 @@
 # error "Missing Check installation; necessary for ProFTPD testsuite"
 #endif
 
+int tests_stubs_set_main_server(server_rec *);
+int tests_stubs_set_next_cmd(cmd_rec *);
+
 Suite *tests_get_pool_suite(void);
 Suite *tests_get_array_suite(void);
 Suite *tests_get_str_suite(void);
@@ -61,6 +62,23 @@ Suite *tests_get_cmd_suite(void);
 Suite *tests_get_response_suite(void);
 Suite *tests_get_fsio_suite(void);
 Suite *tests_get_netio_suite(void);
+Suite *tests_get_trace_suite(void);
+Suite *tests_get_parser_suite(void);
+Suite *tests_get_pidfile_suite(void);
+Suite *tests_get_config_suite(void);
+Suite *tests_get_auth_suite(void);
+Suite *tests_get_filter_suite(void);
+Suite *tests_get_inet_suite(void);
+Suite *tests_get_data_suite(void);
+Suite *tests_get_ascii_suite(void);
+Suite *tests_get_help_suite(void);
+Suite *tests_get_rlimit_suite(void);
+Suite *tests_get_encode_suite(void);
+Suite *tests_get_privs_suite(void);
+Suite *tests_get_display_suite(void);
+Suite *tests_get_misc_suite(void);
+Suite *tests_get_json_suite(void);
+Suite *tests_get_redis_suite(void);
 
 /* Temporary hack/placement for this variable, until we get to testing
  * the Signals API.
diff --git a/tests/api/timers.c b/tests/api/timers.c
index 8a347f7..c5c77fd 100644
--- a/tests/api/timers.c
+++ b/tests/api/timers.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server testsuite
- * Copyright (c) 2008-2013 The ProFTPD Project team
+ * Copyright (c) 2008-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Timers API tests
- * $Id: timers.c,v 1.7 2013-09-24 01:21:16 castaglia Exp $
- */
+/* Timers API tests */
 
 #include "tests.h"
 
@@ -37,15 +35,26 @@ static unsigned int timer_triggered_count = 0;
 
 static void set_up(void) {
   if (p == NULL) {
-    p = make_sub_pool(NULL);
+    p = permanent_pool = make_sub_pool(NULL);
   }
-  permanent_pool = p;
 
   repeat_cb = FALSE;
   timer_triggered_count = 0;
+
+  timers_init();
+
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_use_stderr(TRUE);
+    pr_trace_set_levels("timers", 1, 20);
+  }
 }
 
 static void tear_down(void) {
+  if (getenv("TEST_VERBOSE") != NULL) {
+    pr_trace_use_stderr(FALSE);
+    pr_trace_set_levels("timers", 0, 0);
+  }
+
   if (p) {
     destroy_pool(p);
     p = permanent_pool = NULL;
@@ -270,7 +279,6 @@ Suite *tests_get_timers_suite(void) {
   TCase *testcase;
 
   suite = suite_create("timers");
-
   testcase = tcase_create("base");
 
   tcase_add_checked_fixture(testcase, set_up, tear_down);
@@ -288,6 +296,5 @@ Suite *tests_get_timers_suite(void) {
   tcase_set_timeout(testcase, 5);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/trace.c b/tests/api/trace.c
new file mode 100644
index 0000000..8429ae4
--- /dev/null
+++ b/tests/api/trace.c
@@ -0,0 +1,420 @@
+/*
+ * ProFTPD - FTP server testsuite
+ * Copyright (c) 2014-2015 The ProFTPD Project team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
+ *
+ * As a special exemption, The ProFTPD Project team and other respective
+ * copyright holders give permission to link this program with OpenSSL, and
+ * distribute the resulting executable, without including the source code for
+ * OpenSSL in the source distribution.
+ */
+
+/* Trace API tests */
+
+#include "tests.h"
+
+static pool *p = NULL;
+
+static const char *trace_path = "/tmp/prt-trace.log";
+
+static void set_up(void) {
+  if (p == NULL) {
+    p = permanent_pool = make_sub_pool(NULL);
+  }
+
+  init_inet();
+}
+
+static void tear_down(void) {
+  (void) unlink(trace_path);
+
+  pr_inet_clear();
+  pr_trace_set_options(PR_TRACE_OPT_DEFAULT);
+
+  if (p) {
+    destroy_pool(p);
+    p = permanent_pool = NULL;
+  } 
+}
+
+#ifdef PR_USE_TRACE
+
+START_TEST (trace_set_levels_test) {
+  int min_level, max_level, res;
+  const char *channel;
+
+  res = pr_trace_set_levels(NULL, 0, 0);
+  fail_unless(res < 0, "Failed to handle null channel, no table");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  channel = "foo";
+  min_level = 3;
+  max_level = 1;
+  res = pr_trace_set_levels(channel, min_level, max_level);
+  fail_unless(res < 0, "Failed to handle min level > max level");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  min_level = 1;
+  max_level = 2; 
+  res = pr_trace_set_levels(channel, min_level, max_level);
+  fail_unless(res == 0, "Failed to handle valid channel and levels: %s",
+    strerror(errno));
+
+  res = pr_trace_set_levels(PR_TRACE_DEFAULT_CHANNEL, 1, 5);
+  fail_unless(res == 0, "Failed to set default channels: %s", strerror(errno));
+
+  res = pr_trace_set_levels(PR_TRACE_DEFAULT_CHANNEL, 0, 0);
+  fail_unless(res == 0, "Failed to set default channels: %s", strerror(errno));
+}
+END_TEST
+
+START_TEST (trace_get_table_test) {
+  pr_table_t *res;
+
+  res = pr_trace_get_table();
+  fail_unless(res == NULL, "Failed to handle uninitialized Trace API");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  pr_trace_set_levels("foo", 1, 2);
+
+  res = pr_trace_get_table();
+  fail_unless(res != NULL, "Did not get Trace API table as expected: %s",
+    strerror(errno));
+}
+END_TEST
+
+START_TEST (trace_get_max_level_test) {
+  int min_level, max_level, res;
+  const char *channel;
+
+  channel = "foo";
+  min_level = 1;
+  max_level = 2;
+
+  res = pr_trace_get_max_level(NULL);
+  fail_unless(res < 0, "Failed to handle null channel");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_get_max_level("bar");
+  fail_unless(res < 0, "Failed to handle unset channels/levels");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_set_levels(channel, min_level, max_level);
+  fail_unless(res == 0, "Failed to set '%s:%d-%d': %s", channel, min_level,
+    max_level, strerror(errno));
+
+  res = pr_trace_get_max_level("bar");
+  fail_unless(res < 0, "Failed to handle unknown channel");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_get_max_level(channel);
+  fail_unless(res == max_level, "Failed to get level %d for channel '%s': %s",
+    max_level, channel, strerror(errno));
+}
+END_TEST
+
+START_TEST (trace_get_min_level_test) {
+  int min_level, max_level, res;
+  const char *channel;
+
+  channel = "foo";
+  min_level = 1;
+  max_level = 2;
+
+  res = pr_trace_get_min_level(NULL);
+  fail_unless(res < 0, "Failed to handle null channel");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_get_min_level("bar");
+  fail_unless(res < 0, "Failed to handle unset channels/levels");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_set_levels(channel, min_level, max_level);
+  fail_unless(res == 0, "Failed to set '%s:%d-%d': %s", channel, min_level,
+    max_level, strerror(errno));
+
+  res = pr_trace_get_min_level("bar");
+  fail_unless(res < 0, "Failed to handle unknown channel");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_get_min_level(channel);
+  fail_unless(res == min_level, "Failed to get level %d for channel '%s': %s",
+    min_level, channel, strerror(errno));
+}
+END_TEST
+
+START_TEST (trace_get_level_test) {
+  int min_level, max_level, res;
+  const char *channel;
+
+  channel = "foo";
+  min_level = 1;
+  max_level = 2;
+
+  res = pr_trace_get_level(NULL);
+  fail_unless(res < 0, "Failed to handle null channel");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_get_level("bar");
+  fail_unless(res < 0, "Failed to handle unset channels/levels");
+  fail_unless(errno == EPERM, "Failed to set errno to EPERM, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_set_levels(channel, min_level, max_level);
+  fail_unless(res == 0, "Failed to set '%s:%d-%d': %s", channel, min_level,
+    max_level, strerror(errno));
+
+  res = pr_trace_get_level("bar");
+  fail_unless(res < 0, "Failed to handle unknown channel");
+  fail_unless(errno == ENOENT, "Failed to set errno to ENOENT, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_get_level(channel);
+  fail_unless(res == max_level, "Failed to get level %d for channel '%s': %s",
+    max_level, channel, strerror(errno));
+}
+END_TEST
+
+START_TEST (trace_parse_levels_test) {
+  int min_level, max_level, res;
+  char *level_str;
+
+  res = pr_trace_parse_levels(NULL, NULL, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  res = pr_trace_parse_levels("", &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle empty string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = "foo";
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle invalid levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "-7");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle negative levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  /* Overflow the int data type, in order to get a negative number without
+   * using the dash.
+   */
+  level_str = pstrdup(p, "2147483653");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle negative levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "0");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res == 0, "Failed to handle single level zero string");
+  fail_unless(min_level == 0, "Expected min level 0, got %d", max_level);
+  fail_unless(max_level == 0, "Expected max level 0, got %d", max_level);
+
+  level_str = pstrdup(p, "7");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res == 0, "Failed to handle single level string");
+  fail_unless(min_level == 1, "Expected min level 1, got %d", max_level);
+  fail_unless(max_level == 7, "Expected max level 7, got %d", max_level);
+
+  level_str = pstrdup(p, "abc-def");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle invalid levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "1-def");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle invalid levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  /* Overflow the int data type, in order to get a negative number without
+   * using the dash.
+   */
+  level_str = pstrdup(p, "2147483653-10");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle negative levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "10-2147483653");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle negative levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "-7-5");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle negative levels string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "0--1");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle single level zero string");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "8-7");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res < 0, "Failed to handle max level < min level");
+  fail_unless(errno == EINVAL, "Failed to set errno to EINVAL, got %d (%s)",
+    errno, strerror(errno));
+
+  level_str = pstrdup(p, "1-7");
+  res = pr_trace_parse_levels(level_str, &min_level, &max_level);
+  fail_unless(res == 0, "Failed to handle levels string");
+  fail_unless(min_level == 1, "Expected min level 1, got %d", max_level);
+  fail_unless(max_level == 7, "Expected max level 7, got %d", max_level);
+}
+END_TEST
+
+START_TEST (trace_msg_test) {
+  int res;
+  char *channel, msg[16384];
+
+  res = pr_trace_msg(NULL, -1, NULL);
+  fail_unless(res < 0, "Failed to handle null arguments");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  channel = "testsuite";
+
+  res = pr_trace_msg(channel, -1, NULL);
+  fail_unless(res < 0, "Failed to handle bad level");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  res = pr_trace_msg(channel, 1, NULL);
+  fail_unless(res < 0, "Failed to handle null message");
+  fail_unless(errno == EINVAL, "Expected EINVAL (%d), got %s (%d)", EINVAL,
+    strerror(errno), errno);
+
+  pr_trace_set_levels(channel, 1, 10);
+
+  memset(msg, 'A', sizeof(msg)-1);
+  msg[sizeof(msg)-1] = '\0';
+  pr_trace_msg(channel, 5, "%s", msg);
+
+  session.c = pr_inet_create_conn(p, -1, NULL, INPORT_ANY, FALSE);
+  fail_unless(session.c != NULL, "Failed to create conn: %s", strerror(errno));
+  session.c->local_addr = session.c->remote_addr =
+    pr_netaddr_get_addr(p, "127.0.0.1", NULL);
+
+  res = pr_trace_set_options(PR_TRACE_OPT_LOG_CONN_IPS|PR_TRACE_OPT_USE_TIMESTAMP_MILLIS);
+  fail_unless(res == 0, "Failed to set options: %s", strerror(errno));
+  pr_trace_msg(channel, 5, "%s", "alef bet vet?");
+
+  res = pr_trace_set_options(0);
+  fail_unless(res == 0, "Failed to set options: %s", strerror(errno));
+  pr_trace_msg(channel, 5, "%s", "alef bet vet?");
+
+  pr_inet_close(p, session.c);
+  session.c = NULL;
+
+  pr_trace_set_levels(channel, 0, 0);
+}
+END_TEST
+
+START_TEST (trace_set_file_test) {
+  int res;
+  const char *path;
+
+  path = "/";
+  res = pr_trace_set_file(path);
+  fail_unless(res < 0, "Failed to handle path '%s'", path);
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  path = "/tmp";
+  res = pr_trace_set_file(path);
+  fail_unless(res < 0, "Failed to handle path '%s'", path);
+  fail_unless(errno == EISDIR, "Expected EISDIR (%d), got %s (%d)", EISDIR,
+    strerror(errno), errno);
+
+  path = trace_path;
+  res = pr_trace_set_file(path);
+  fail_unless(res == 0, "Failed to set trace file '%s': %s", path,
+    strerror(errno));
+  pr_trace_set_levels("foo", 1, 20);
+
+  pr_trace_msg("foo", 1, "bar?");
+  pr_trace_msg("foo", 1, "baz!");
+
+  res = pr_trace_set_options(PR_TRACE_OPT_LOG_CONN_IPS|PR_TRACE_OPT_USE_TIMESTAMP_MILLIS);
+  fail_unless(res == 0, "Failed to set options: %s", strerror(errno));
+
+  pr_trace_msg("foo", 1, "quxx?");
+  pr_trace_msg("foo", 1, "QUZZ!");
+
+  res = pr_trace_set_options(PR_TRACE_OPT_DEFAULT);
+  fail_unless(res == 0, "Failed to set default options: %s", strerror(errno));
+
+  pr_trace_set_levels("foo", 0, 0);
+  res = pr_trace_set_file(NULL);
+  fail_unless(res == 0, "Failed to reset trace file: %s", strerror(errno));
+
+  (void) unlink(trace_path);
+}
+END_TEST
+
+START_TEST (trace_restart_test) {
+  pr_trace_set_levels("testsuite", 1, 10);
+  pr_event_generate("core.restart", NULL);
+}
+END_TEST
+#endif /* PR_USE_TRACE */
+
+Suite *tests_get_trace_suite(void) {
+  Suite *suite;
+  TCase *testcase;
+
+  suite = suite_create("trace");
+  testcase = tcase_create("base");
+
+  tcase_add_checked_fixture(testcase, set_up, tear_down);
+
+#ifdef PR_USE_TRACE
+  tcase_add_test(testcase, trace_set_levels_test);
+  tcase_add_test(testcase, trace_get_table_test);
+  tcase_add_test(testcase, trace_get_max_level_test);
+  tcase_add_test(testcase, trace_get_min_level_test);
+  tcase_add_test(testcase, trace_get_level_test);
+  tcase_add_test(testcase, trace_parse_levels_test);
+  tcase_add_test(testcase, trace_msg_test);
+  tcase_add_test(testcase, trace_set_file_test);
+  tcase_add_test(testcase, trace_restart_test);
+#endif /* PR_USE_TRACE */
+
+  suite_add_tcase(suite, testcase);
+  return suite;
+}
diff --git a/tests/api/var.c b/tests/api/var.c
index ab86b95..d66b0dc 100644
--- a/tests/api/var.c
+++ b/tests/api/var.c
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Var API tests
- * $Id: var.c,v 1.2 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Var API tests */
 
 #include "tests.h"
 
@@ -296,7 +294,6 @@ Suite *tests_get_var_suite(void) {
   TCase *testcase;
 
   suite = suite_create("var");
-
   testcase = tcase_create("base");
 
   tcase_add_checked_fixture(testcase, set_up, tear_down);
@@ -309,6 +306,5 @@ Suite *tests_get_var_suite(void) {
   tcase_add_test(testcase, var_rewind_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/api/version.c b/tests/api/version.c
index 2bf1932..14e61d1 100644
--- a/tests/api/version.c
+++ b/tests/api/version.c
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* Version API tests
- * $Id: version.c,v 1.2 2011-05-23 20:50:31 castaglia Exp $
- */
+/* Version API tests */
 
 #include "tests.h"
 
@@ -63,7 +61,6 @@ Suite *tests_get_version_suite(void) {
   TCase *testcase;
 
   suite = suite_create("version");
-
   testcase = tcase_create("base");
 
   tcase_add_test(testcase, version_get_module_api_number_test);
@@ -71,6 +68,5 @@ Suite *tests_get_version_suite(void) {
   tcase_add_test(testcase, version_get_str_test);
 
   suite_add_tcase(suite, testcase);
-
   return suite;
 }
diff --git a/tests/t/commands/clnt.t b/tests/t/commands/clnt.t
new file mode 100644
index 0000000..08ad578
--- /dev/null
+++ b/tests/t/commands/clnt.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::Commands::CLNT");
diff --git a/tests/t/config/anonallowrobots.t b/tests/t/config/anonallowrobots.t
new file mode 100644
index 0000000..f1ddb59
--- /dev/null
+++ b/tests/t/config/anonallowrobots.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::Config::AnonAllowRobots");
diff --git a/tests/t/config/loginpasswordprompt.t b/tests/t/config/loginpasswordprompt.t
new file mode 100644
index 0000000..bbea85e
--- /dev/null
+++ b/tests/t/config/loginpasswordprompt.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::Config::LoginPasswordPrompt");
diff --git a/tests/t/config/maxpasswordsize.t b/tests/t/config/maxpasswordsize.t
new file mode 100644
index 0000000..a28f5a6
--- /dev/null
+++ b/tests/t/config/maxpasswordsize.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::Config::MaxPasswordSize");
diff --git a/tests/t/config/maxtransfersperhost.t b/tests/t/config/maxtransfersperhost.t
new file mode 100644
index 0000000..472df3a
--- /dev/null
+++ b/tests/t/config/maxtransfersperhost.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::Config::MaxTransfersPerHost");
diff --git a/tests/t/config/maxtransfersperuser.t b/tests/t/config/maxtransfersperuser.t
new file mode 100644
index 0000000..db09591
--- /dev/null
+++ b/tests/t/config/maxtransfersperuser.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::Config::MaxTransfersPerUser");
diff --git a/tests/t/config/transferoptions.t b/tests/t/config/transferoptions.t
new file mode 100644
index 0000000..32dc660
--- /dev/null
+++ b/tests/t/config/transferoptions.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::Config::TransferOptions");
diff --git a/tests/t/config/virtualhost.t b/tests/t/config/virtualhost.t
new file mode 100644
index 0000000..c52130a
--- /dev/null
+++ b/tests/t/config/virtualhost.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::Config::VirtualHost");
diff --git a/tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key b/tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key
new file mode 100644
index 0000000..6fa2dbb
--- /dev/null
+++ b/tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key
@@ -0,0 +1,12 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQDTXVbdcVV/ZEIwq5/X5Tma/zK63KptRBgz6knZzLyyrK/pvFoj
+qF4OGn+wdf9FUSnouMjWtzBtIie54vyvPqW0u58Ft0xS/w94q2d3tCwi/fZSJ58e
+9jdarjoYXZLFJQbGb/Xc8E+1D8zVP0hwPUZfwtpygiral6/Sto+Wcwy3LwIVAPFU
+AsieBhPZpI+7j/hzOR7v7npPAoGAMOX2vvoY7wF7XZ9JESKjfzsA+Fxpz5C/TVHX
+Hx74d9SpRGCG/XkD0tomIB2R77KqcMD1Vzvi4WLXX+VeFf4CLC4oUI1ibXzqAyvx
+su1aboDxEdcBjXYQ7djTi/YE4IebFkfLx0h3saxtuqU4goba3iGfw3z20Bo01vgR
+ynn0ANsCgYAO/yccxY5b2WRWLfTxcyjr73ZXIKfOAlSVxgCjABKv+89T0LtTw2TO
+ahpCmOoQXhXg7GGMZgQCtlGxpMM7IwOLNi7rVVzi9nojSAQkDi8yy4fk6WgDtnz2
+qxlf1RGBu3c3tJSiCIwjr54o1Pl2OT+1V2KN94qKkQw3yMOz0jxCUAIVAM1NTo+i
+0943pn1stNRFzc6fC2OA
+-----END DSA PRIVATE KEY-----
diff --git a/tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key.pub b/tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key.pub
new file mode 100644
index 0000000..48628ce
--- /dev/null
+++ b/tests/t/etc/modules/mod_auth_otp/ssh_host_dsa_key.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBANNdVt1xVX9kQjCrn9flOZr/Mrrcqm1EGDPqSdnMvLKsr+m8WiOoXg4af7B1/0VRKei4yNa3MG0iJ7ni/K8+pbS7nwW3TFL/D3irZ3e0LCL99lInnx72N1quOhhdksUlBsZv9dzwT7UPzNU/SHA9Rl/C2nKCKtqXr9K2j5ZzDLcvAAAAFQDxVALIngYT2aSPu4/4czke7+56TwAAAIAw5fa++hjvAXtdn0kRIqN/OwD4XGnPkL9NUdcfHvh31KlEYIb9eQPS2iYgHZHvsqpwwPVXO+LhYtdf5V4V/gIsLihQjWJtfOoDK/Gy7VpugPER1wGNdhDt2NOL9gTgh5sWR8vHSHexrG26pTiChtreIZ/DfPbQGjTW+BHKefQA2wAAAIAO/yccxY5b2WRWLfTxcyjr73ZXIKfOAlSVxgCjABKv+89T0LtTw2TOahpCmOoQXhXg7GGMZgQCtlGx [...]
diff --git a/tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key b/tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key
new file mode 100644
index 0000000..1616460
--- /dev/null
+++ b/tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAyzvasQY4M70dkgZejKTpWMWZzh6QESXoNMkJ1o+Ui+flJ74b
+zNtVt2AnK71qcJUSbRMQfwKksy60Nm6R+2Qc0uGX+/CEzXSJjQJdp5xOc0caVnQS
+gUpxyrmkFUgu4RDgDKCbs5jJAlxL8AvcFfjC50X+0Oz3IkSajUIzsuo9Mgf87i+c
+f1JCEFOoqeocLZnoUiRSeUn5u0lNM6nsU1VWBUhLhHBBiVwap66h0RzYi9C+57kC
+8/QcR8JqKHllyo48jja71YyxNv6WstC1PN/BUx81aXGS1b/R7bidKZIOY3qmRGD5
+rWNm0zqAPI06ObtNxYFHEG/xIM88mYrvO5fq5QIBIwKCAQBuU7E7hwiRHYUUwaEK
+hWi7KXDAWbvsDUOKXoGu/XyGdpJXZzOpuOVji/gBzTnIFmkWo/MDHrEuEgoO65ET
+cNxyencTvRTkj7hifaCOMEfYLem55z0+30UWR4T1nDa0sWOvFVvWhiP595bwBnAp
+L0U7uEiAC5wZ6rpMrujdd9gT13sNRJF9vue/NbmasocxJtp8RjYUh1RqCJHh/Shr
+aRYJuPI+tNPFwC2tteVFIPiniORO4//fxfyDrubH5eh0GH/eFEpgx/APCnaoHEpu
+NiDablfLmOR6dlSYvPp61aY41jHmVC4alsD6eMIxmQ9200+x2sq23EIeYAS4Pj2d
+cDrXAoGBAPVpHY6Dk6A6PyZx2PfkJm/Jb63NeXZ7mYp/MTXK2pPM8lxuLa/JNCMI
+asqJI0Y9x8W8rcNVUB0aMWSGDGVzEwi7SkqVEcCQFrFTVre6VdxoJsK67Ay0AuhP
+/EQrORGGsU6O6PvPHlYDbah2B0T6Yi0cG1LI+UKNlwwFWxLSsXerAoGBANQA1xzr
+r86DUznKQYmQl7AqhCYaNYlu41eoYwBJWtz+AT5R5/4rdRy9lnRPXoTjhhE7Zz/a
+AWPqJmn8TDkwx1XAkSubrFFixyW+AoHf8EoVOqiIUO65aawYlh/LOzZ0FloapwdB
+gDQP4OtQNhwhFz0DdJnOMcj88xppNnhh1FevAoGBAOdjG94kSVyOsJH2UDou4mlm
+J3f8P1J0iXP0RFdKO80u8yPy2p5tP8lCc1E/eQe2l8kQ+5rqCbUKEVAmnflAnOr5
+uxMe1jk3ZdpkhPZfOwMDHTtChsomEV6x0JgLfvNEe0oRtxlOQSyG47wXgzJoazHY
+rAw51RLdRUXZKfvN98iZAoGBAKOLnqFIGehz777d2sk1B01ioHUqKUzDQaoFjjNr
+ycBsLNhN0DkaNcW20yaGXtuK8mUQkXpmWNgMZsbJ8ah2Fh2UjT7lzhLlzNP4+p60
+ESM08ryGalHCO5NjbH7tPE6UET4x0U69gCgpgZ+VphWzII4nPLEqCSYE9g0PVefW
+cJtpAoGBAMwtrOsnbT6TPKLJW8GeKeC8MK/0kfzX/u1+9MyXK0fbGFzjPDlNWSys
+JlgVRr4qCM99lEOcdWxcvtE7/o9GQpil+kgTwS2WQMaOA2p/HCtBGov0eXRUmGBp
+hCD/EYLj24Eu7Njo4VfEpLB4lK8AVagVQH97iFijBbp66FJ8RyGS
+-----END RSA PRIVATE KEY-----
diff --git a/tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key.pub b/tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key.pub
new file mode 100644
index 0000000..ef2d87b
--- /dev/null
+++ b/tests/t/etc/modules/mod_auth_otp/ssh_host_rsa_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAyzvasQY4M70dkgZejKTpWMWZzh6QESXoNMkJ1o+Ui+flJ74bzNtVt2AnK71qcJUSbRMQfwKksy60Nm6R+2Qc0uGX+/CEzXSJjQJdp5xOc0caVnQSgUpxyrmkFUgu4RDgDKCbs5jJAlxL8AvcFfjC50X+0Oz3IkSajUIzsuo9Mgf87i+cf1JCEFOoqeocLZnoUiRSeUn5u0lNM6nsU1VWBUhLhHBBiVwap66h0RzYi9C+57kC8/QcR8JqKHllyo48jja71YyxNv6WstC1PN/BUx81aXGS1b/R7bidKZIOY3qmRGD5rWNm0zqAPI06ObtNxYFHEG/xIM88mYrvO5fq5Q== tj at Imp.local
diff --git a/tests/t/etc/modules/mod_geoip/GeoIP.dat b/tests/t/etc/modules/mod_geoip/GeoIP.dat
index 73fbe58..387b967 100644
Binary files a/tests/t/etc/modules/mod_geoip/GeoIP.dat and b/tests/t/etc/modules/mod_geoip/GeoIP.dat differ
diff --git a/tests/t/etc/modules/mod_geoip/GeoIPASNum.dat b/tests/t/etc/modules/mod_geoip/GeoIPASNum.dat
index cbafac0..c442793 100644
Binary files a/tests/t/etc/modules/mod_geoip/GeoIPASNum.dat and b/tests/t/etc/modules/mod_geoip/GeoIPASNum.dat differ
diff --git a/tests/t/etc/modules/mod_geoip/GeoIPv6.dat b/tests/t/etc/modules/mod_geoip/GeoIPv6.dat
index 3dfd3d1..935afff 100644
Binary files a/tests/t/etc/modules/mod_geoip/GeoIPv6.dat and b/tests/t/etc/modules/mod_geoip/GeoIPv6.dat differ
diff --git a/tests/t/etc/modules/mod_geoip/GeoLiteCity.dat b/tests/t/etc/modules/mod_geoip/GeoLiteCity.dat
index 9136f70..dbb2674 100644
Binary files a/tests/t/etc/modules/mod_geoip/GeoLiteCity.dat and b/tests/t/etc/modules/mod_geoip/GeoLiteCity.dat differ
diff --git a/tests/t/etc/modules/mod_sftp/authorized_rsa_keys2 b/tests/t/etc/modules/mod_sftp/authorized_rsa_keys2
new file mode 100644
index 0000000..a409ee3
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/authorized_rsa_keys2
@@ -0,0 +1,23 @@
+---- BEGIN SSH2 PUBLIC KEY ----
+Comment: "4096-bit RSA, converted from OpenSSH by tj at familiar"
+AAAAB3NzaC1yc2EAAAABIwAAAgEAxtoQsovDmh+Ud4sW6NTM6Bp8npZIB0irgKVLimbh8g
+OdxN2KR04Hm/X2OhGBfrY4v6bS7MM6zfXrNoZI+iLvZKL3K/zFZPJHTIsRjsCZAcn6zA8M
+bd0BLg2dEjYn0tVJv6c2h6SAMTYeLsKLsGihggRnXadyGDWzNdBfolJ131QyinfyP8/5zX
+bKzuaY25zrzw9O/xbS2dH/uAr1KoXrPKxt2kC/OglWPSeZZdIMVPRqE/X2V6NKbXhq89BF
+h87yRPQjir1yQ2OBOfhjG7RA9Thfu4OfxgliBaXV0MPUQMsFFT4f3mF/I3kQUmhXQxdIrS
+TXQctBXs1elhMhjmGYFbyb8y76yQO3kywFUrBSbgpqIrS20UhszfuEXS04zGMbR539OxKT
+7L2unQasFUNjMrtbS84I6gytiKUqgpPYG6hpTkxqQao/owPsa2IC3Ikrh+YLLReELaoUOo
+/SIRZ3mRNeZTpj+VxsGt2+S4L7I3XYpHfThOxmrh5Ls93+Wubhew12GIwJsYuNUz9+yO82
+YOs8lNwGbW/4JkMGS/c/nM3bKmE/CGTwXR4c9aZlKwbbQcucKNJ8/hvsKgvWtZu385Mnn3
+Kbvilzsjc8hqSbiD+QRXpoVcgV6X369H+H8vHusjhYtJ8mH5J/YpsIUa1v3RKQG38QHuQq
+1e1YNo9hYjE=
+---- END SSH2 PUBLIC KEY ----
+---- BEGIN SSH2 PUBLIC KEY ----
+Comment: "2048-bit RSA, converted from OpenSSH by tj at Imp.local"
+AAAAB3NzaC1yc2EAAAABIwAAAQEAzJ1CLwnVP9mUa8uyM+XBzxLxsRvGz4cS59aPTgdw7j
+Gx1jCvC9ya400x7ej5Q4ubwlAAPblXzG5GYv2ROmYQ1DIjrhmR/61tDKUvAAZIgtvLZ00y
+dqqpq5lG4ubVJ4gW6sxbPfq/X12kV1gxGsFLUJCgoYInZGyIONrnvmQjFIfIx+mQXaK84u
+O6w0CT6KhRWgonajMrlO6P8O7qr80rFmOZsBNIMooyYrGTaMyxVsQK2SY+VKbXWFC+2HMm
+ef62n+02ohAOBKtOsSOn8HE2wi7yMA0g8jRTd8kZcWBIkAhizPvl8pqG1F0DCmLn00rhPk
+Byq2pv4VBo953gK7f1AQ==
+---- END SSH2 PUBLIC KEY ----
diff --git a/tests/t/etc/modules/mod_sftp/authorized_rsa_keys_no_nl b/tests/t/etc/modules/mod_sftp/authorized_rsa_keys_no_nl
new file mode 100644
index 0000000..a0c5f7d
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/authorized_rsa_keys_no_nl
@@ -0,0 +1,9 @@
+---- BEGIN SSH2 PUBLIC KEY ----
+Comment: "2048-bit RSA, converted from OpenSSH by tj at Imp.local"
+AAAAB3NzaC1yc2EAAAABIwAAAQEAzJ1CLwnVP9mUa8uyM+XBzxLxsRvGz4cS59aPTgdw7j
+Gx1jCvC9ya400x7ej5Q4ubwlAAPblXzG5GYv2ROmYQ1DIjrhmR/61tDKUvAAZIgtvLZ00y
+dqqpq5lG4ubVJ4gW6sxbPfq/X12kV1gxGsFLUJCgoYInZGyIONrnvmQjFIfIx+mQXaK84u
+O6w0CT6KhRWgonajMrlO6P8O7qr80rFmOZsBNIMooyYrGTaMyxVsQK2SY+VKbXWFC+2HMm
+ef62n+02ohAOBKtOsSOn8HE2wi7yMA0g8jRTd8kZcWBIkAhizPvl8pqG1F0DCmLn00rhPk
+Byq2pv4VBo953gK7f1AQ==
+---- END SSH2 PUBLIC KEY ----
\ No newline at end of file
diff --git a/tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key b/tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key
new file mode 100644
index 0000000..e680411
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDnY3SRAd+cBX98qagrW1RvvPsWxR1nqtkcDLJlSlAaNtjtpRub
+TkgTGkw2JA3xPDN1lfNN74o6ndCkrUcNbBj+RTx4tJwKN/UKmQDQeYRFBXa7yEOW
+Jon9g8erBR/IMw2BnNMBj099AS2UaokAffoGtAWb/wAjkQ1ijCEWAAI9uwIDAQAB
+AoGBAMuoowman4kA4dQEs8rtiOUMbef9uMLtAkkI99/lR5m7GrYYc7L/dLqzk863
+bzob5aMiR1MaegzbqK+4/+fVdCx0iq8xQKBUXxcKp+QE4ppfoFr6xLTlq7ilIJpr
+/isFsvP899Vrg3hwBcI6dLa7hP6U2FjV9+uRyp+ex4wKl6sZAkEA+Z8vXrNTvIUS
+0XhW6F7tV28yq6NwDSmYD8JAAFKp5hIzSDbFi107fpij8Qgm+4kbvl4UdhKm5A38
+jX9y2Jc/bQJBAO1NANqINZQ8ProqfwW3pEMC8spEAe1mz/Qo7Kftkb5sOYMQa7x4
+1eTiOzcA+/Kgl0R/JidfHvII+5czsJwJsMcCQEZ3xsKgM0Jj+sUBiN8+dRgavx4v
+HGHK3S+Nsc2liGr3tlxrgebu4e3CH33axE58DUX3fyU57L0yqZo0YXJ9eB0CQC4P
+oVfJ0qSYYRCfyRIiCEddniT2uG0NZNYez3j1GzIcLbmsCU2HIvWPmDDgBwecdmA8
+UfqYcxhF+BfsV56iHucCQQD1Y4wQzt/9GOSWNdNY7BSiVsThvmkGcB4XFM0Ez91q
+BkNGF3FMsUe+WxvQo59JVWFmgUtkyTcUPe+vbPwyrScd
+-----END RSA PRIVATE KEY-----
diff --git a/tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key.pub b/tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key.pub
new file mode 100644
index 0000000..f459a1d
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/ssh_host_rsa1024_key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDnY3SRAd+cBX98qagrW1RvvPsWxR1nqtkcDLJlSlAaNtjtpRubTkgTGkw2JA3xPDN1lfNN74o6ndCkrUcNbBj+RTx4tJwKN/UKmQDQeYRFBXa7yEOWJon9g8erBR/IMw2BnNMBj099AS2UaokAffoGtAWb/wAjkQ1ijCEWAAI9uw== tj at Ts-MacBook-Pro.local
diff --git a/tests/t/etc/modules/mod_sftp/test_rsa2048_key2 b/tests/t/etc/modules/mod_sftp/test_rsa2048_key2
new file mode 100644
index 0000000..0b3e76f
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/test_rsa2048_key2
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEoQIBAAKCAQEA1MDOdQ8ddQGd0hNPbO14zFAD1/c0Ontkw3egGGuVDm48VTnD
+NWGWbH5CirShUhjfLzxZkStyepdKFsYXZOeyBaHdqMfEhXhWZ+M7z9B9rUBM6R7W
+G34v7pzd8bOYDbff25PCITNYk9m/2ZGrFgAK5EChZ9jtxmaqhYWl6xKLilXYkmhI
+d66TTqMgPUrM1sH9QeFV2axQmK1SVEkSzYTaY8WePds5D5cmZLmABAT3UYPQCcrO
+ahISyKazJ9E+YjoMl9GoniSEiHTPK+XfyND83zIihJO16VxUVUMStR5yHBd133SV
+ar4yKo8fv9wfgOxDcfGLxgWrkgXv/3qR/8zNaQIBIwKCAQBhQjJ+p9LzqPewfdsq
+ey/oXxezLy3Rl36FPgALKd29dDjWgM5ESd5de4wiJrAlh7Z7/lTUpiW0YmsDGLo8
+wbB3mnP1YqL7L7J4omR8Qg2RB3OdxPQp00kcn3tnLYdWunT2qfJYUfybXDpyFrSc
+V8p2+PlUC2ViAwwfyBFVhNITWZwZWEjj/qrJnSX/zFl3HcZ6+qC/U9vr18VPFOGt
+LtuOURf4ityJOgt7G2QrNR7D50ZOzdK1c/nxAxDb8xeYjd3jx+VXl0H9i7pyZYyk
+D55Bj8KZueJCd0CGJOLYRD4TI6WVkxc5Vl5FUNYLuL7ca/M4sEHeZeNUm/yrnGgX
+/jlLAoGBAPKcQGMNB3EesllfDXR8N5R/Dkvx6+N1xlPphJfTOGN8XPUSPeqEctBX
+Rgu0bKmWVs/RWNqF6HYP0K3zajqPR+6J/NQNBgR9tnEbFw03E5G/mNX2mQ5qEqWH
+HUeErGAok9Zx/opmA1h8ucQRfEhaW6B+JsA7AiJtO/b/w3Lm7m/vAoGBAOB+tsX3
+6gyW6QO+rlSl+UaVzspFOPwO/v7Nxs1PdfagF0S77Ywe9VzwJ4kxFBpuoxZsD7b3
+O+n5n1BTQ0A1O8irj47hg5+wHLY2kK7q20DwLtLdtAiN5NGotRr3XMoppaU2REg3
+Tet2NId0u7WkZH7nyTt0hKfbtMBIUPtkYkAnAoGAfMVi8RykvdVFx4/4SokGpCQV
++yv1qC3w9/RwE5Ey8VXmqewf17HUpak592QaoFvyIbwfEUwuaJprUiVp1Pk677St
+6WXIlJhsdK7Yp7Xszp0Mxc9HZn+x0Xiv+OUlc0gRhDqgKetSLYH5IwGtoY2ONUgx
+L6lRjgxKuYrZi4y1I58CgYATPg+qkZe3762Sm1DFbU/heo4RVmP4WQ6K3nAgOgLL
+/xfv8tnYz3Qd6LLugIyxzvgJPHZgI9Hve8vTr12JKSJhqE4iMJZA4zWpN+CEBYCB
+6LOOasZJ4EbQJGdLdEnIL3SZIeiYecTZqwvRGKJgBsbJDI2XYcI6RggtvQ5BbwEb
+cQKBgQDxwh/gvG5v+ZtdAl5iugdGACBM+lDxMcDLAqBNJZGSiF9yeURZXzsMr5BE
+iSPFhjt1E2FcJkt8BIoKNPuVRSD0wpHnpAufZKd2/ZnqmNLi1Lqat2112pEsT6ny
+8RYd6/skhkJ39lwxvel93jE9uPN3kPcTougz4VuUIaNRwtvVSA==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/t/etc/modules/mod_sftp/test_rsa2048_key2.pub b/tests/t/etc/modules/mod_sftp/test_rsa2048_key2.pub
new file mode 100644
index 0000000..198bb3e
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/test_rsa2048_key2.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA1MDOdQ8ddQGd0hNPbO14zFAD1/c0Ontkw3egGGuVDm48VTnDNWGWbH5CirShUhjfLzxZkStyepdKFsYXZOeyBaHdqMfEhXhWZ+M7z9B9rUBM6R7WG34v7pzd8bOYDbff25PCITNYk9m/2ZGrFgAK5EChZ9jtxmaqhYWl6xKLilXYkmhId66TTqMgPUrM1sH9QeFV2axQmK1SVEkSzYTaY8WePds5D5cmZLmABAT3UYPQCcrOahISyKazJ9E+YjoMl9GoniSEiHTPK+XfyND83zIihJO16VxUVUMStR5yHBd133SVar4yKo8fv9wfgOxDcfGLxgWrkgXv/3qR/8zNaQ== tj at familiar
diff --git a/tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155 b/tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155
new file mode 100644
index 0000000..a7888d5
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA13H33uYHCPKX+any43mlzsjxrZuFpgdACmCuPa90KhXe6hIg
+6rx5nNLMOuKHfpMEshCQnj9zmtjSGyLZ9ufJv6Wg3SSHTIKQW2HtR9MLM8zzVXDE
+pcsWQUbwAs7mBdYKlOJxFP3J4PMVAiJe+GnQ889nXkdixB4SRU6LCfrPwg5c1Ho5
+FOPYyseAxMNjgsR1n8NUDg5COxlktnR+Tunlu/S/7TgcLi+ugvIIEB5vlhaHEZoP
+Ipz2fl15l9FYueYvzU73ESvUgdNQE16RmKpdmr6WwN9g5mG+tQCMrhWCkk4IAo5g
+Ulx/Go1Osgp2r0ouj1MSJJkwubawXDDPj/RUjwIBIwKCAQEAyyJQG03pYDUwNIpL
+aMMnTfCpWotSIDK4uVPXe/0zAxvvbvsmWapkCOQCY2fe1m1cMtxrGNy9dL3Nagrq
+xDmg9oY4lf3epzlGR4g3fjwDP3gYoQPsnFHEhrCZJzgatQujk7DuRv3qOn7Kqj2q
+AClBWugfogjK1ir7SKkkAhhdfHI1tsCffVTftdQgbVWsbCgo5iwYP7Urv8j3DsNi
+KKQDVkHRr8YKgzb0BAwwmW3++sdCf9QSLNJpi9+KOyDoN59pyvLVtyWl659QvqrT
+Il93fV9YkVP/17m7LtqG+o/nHFUMS8Q4vbCLF+/chYynvPd/EX8uAFQpaLObhs6H
+28X5UwKBgQD25ALcWi6edjiX0fs74RSbX0PCuR+JBv756AC9d9sQCshbhseRjASI
+ozYckkKHsVs0us6cVAopWb/3bI+yGANjDXleebxPqEnroIzsYOxO0QBCM8kBwnUF
+/nGoVZ33ThW1hKRIY7ZKr/e0rVil0Mj5U9rOyFYh/RdKQYl4ye3B3QKBgQDfZPDi
+HoH/fG/d1Cd4hE70sTmHIOD/KqJdBJzFVp7xLuaVhTmmRYwECfyRKLTRs/eALE9f
+h8Mm5Zkl3Qd6Cc0ee/rU9DgS7yY9WwUx5L2nVEmwzCnjL5wY+TqDu1NIksbY2DkC
+ik/PcDocvN6TVCVg4xgp2AjTTZNUi61CzWrnWwKBgQCpS+S0WxinLH6T/bOWxjoE
+JBE1EToyE20DIr6tzoeV+MPm/VWlodc5H3WP70OQPxn4RZT9e3Suo/FZNH/KlB+U
+YQLEcLukViQPSYU0X7f7iAAtZVaiHvEornnSg9oIqpKLGSAxo3Wvjps6EHdNIXPe
+Kt8utUJgcwin8mzlHMBKawKBgBMl6MLsrA6VozBFYngLVzmLiJaNyiR//0nNMgJJ
+QNLYImSWZAbwBK/jmU5FUVPNmN8ZvakaQ+1kIxHfvs/yNicvMsHLykrECpeLdXlO
+HuJ08F+ceJ5xy40crT59xU9dCbrQtG3umSBotIYe0T/UAzQwuO2kzY50XRXgFipM
+HxslAoGBAJqJdxHYjdYrSrQfrAc65TPE5N2mgzJxTsWL2y814YD83GLcYVawiPGZ
+vgq6VdhQV/9L3WDZvNEgnUzZyirckTXe7anjLOyeHLgU53uM2729XnhUGeEXaalX
+/FZ2EaHxtDZhEFSNJTkg84cjrAdFx9K6tgncskr5Xyiroy1g6QTE
+-----END RSA PRIVATE KEY-----
diff --git a/tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155.pub b/tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155.pub
new file mode 100644
index 0000000..4402416
--- /dev/null
+++ b/tests/t/etc/modules/mod_sftp/test_rsa_key_bug4155.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA13H33uYHCPKX+any43mlzsjxrZuFpgdACmCuPa90KhXe6hIg6rx5nNLMOuKHfpMEshCQnj9zmtjSGyLZ9ufJv6Wg3SSHTIKQW2HtR9MLM8zzVXDEpcsWQUbwAs7mBdYKlOJxFP3J4PMVAiJe+GnQ889nXkdixB4SRU6LCfrPwg5c1Ho5FOPYyseAxMNjgsR1n8NUDg5COxlktnR+Tunlu/S/7TgcLi+ugvIIEB5vlhaHEZoPIpz2fl15l9FYueYvzU73ESvUgdNQE16RmKpdmr6WwN9g5mG+tQCMrhWCkk4IAo5gUlx/Go1Osgp2r0ouj1MSJJkwubawXDDPj/RUjw== jbaird at fc-qaftp01.corp.follett.com
diff --git a/tests/t/etc/modules/mod_sql_odbc/odbc.ini b/tests/t/etc/modules/mod_sql_odbc/odbc.ini
new file mode 100644
index 0000000..ae82ca9
--- /dev/null
+++ b/tests/t/etc/modules/mod_sql_odbc/odbc.ini
@@ -0,0 +1,15 @@
+[ODBC Data Sources]
+mysql = MySQL ODBC 5.3.4 Driver
+
+[ODBC]
+Trace = No
+TraceFile = /tmp/odbc-trace.log
+
+[mysql]
+Description = MySQL
+Driver = /usr/local/lib/libmyodbc5a.so
+Server = 127.0.0.1
+Port = 3308
+Socket = /Users/tj/mysql/mysql.sock
+Option = 3
+Database = ftp
diff --git a/tests/t/etc/modules/mod_sql_odbc/odbcinst.ini b/tests/t/etc/modules/mod_sql_odbc/odbcinst.ini
new file mode 100644
index 0000000..c3456b4
--- /dev/null
+++ b/tests/t/etc/modules/mod_sql_odbc/odbcinst.ini
@@ -0,0 +1,11 @@
+[ODBC]
+Trace = No
+TraceFile = /tmp/odbc.log
+
+[MySQL]
+Name = myodbc
+Description = MySQL ODBC driver
+Driver = /usr/local/lib/libmyodbc5a.so
+FileUsage = 1
+UsageCount = 2
+Debug = 5
diff --git a/tests/t/etc/modules/mod_tls/tls-get-passphrase-once.pl b/tests/t/etc/modules/mod_tls/tls-get-passphrase-once.pl
new file mode 100755
index 0000000..e0fa589
--- /dev/null
+++ b/tests/t/etc/modules/mod_tls/tls-get-passphrase-once.pl
@@ -0,0 +1,22 @@
+#!/usr/bin/env perl
+
+use strict;
+
+my $lock_file = "/tmp/tls-passphrase.lock";
+if (-f $lock_file) {
+  print STDERR "Passphrase already obtained (see lock file $lock_file); exiting\n";
+  exit 1;
+}
+
+if (open(my $fh, "> $lock_file")) {
+  close($fh);
+
+  my $passphrase = "password";
+  print STDOUT "$passphrase\n";
+  exit 0;
+
+} else {
+  print STDERR "Error opening lock file $lock_file: $!\n";
+}
+
+exit 1;
diff --git a/tests/t/lib/ProFTPD/TestSuite/FTP.pm b/tests/t/lib/ProFTPD/TestSuite/FTP.pm
index 049b8ea..cb0792e 100644
--- a/tests/t/lib/ProFTPD/TestSuite/FTP.pm
+++ b/tests/t/lib/ProFTPD/TestSuite/FTP.pm
@@ -12,7 +12,7 @@ sub new {
   my $class = shift;
   my ($addr, $port, $use_port, $conn_timeout, $cmd_timeout) = @_;
   $use_port = 0 unless defined($use_port);
-  $conn_timeout = 10 unless defined($conn_timeout);
+  $conn_timeout = 2 unless defined($conn_timeout);
  
   my $ftp;
 
@@ -1630,7 +1630,7 @@ sub stat {
   }
 }
 
-# From the FTP HOST command RFCXXXX (currently in Draft form)
+# From the FTP HOST command RFC 7151
 sub host {
   my $self = shift;
   my $host = shift;
@@ -1657,6 +1657,32 @@ sub host {
   }
 }
 
+sub clnt {
+  my $self = shift;
+  my $info = shift;
+  $info = 'ProFTPD::TestSuite::FTP' unless defined($info);
+  my $code;
+
+  $code = $self->{ftp}->quot('CLNT', $info);
+  unless ($code) {
+    croak("CLNT command failed: " .  $self->{ftp}->code . ' ' .
+      $self->response_msg());
+  }
+
+  if ($code == 4 || $code == 5) {
+    croak("CLNT command failed: " .  $self->{ftp}->code . ' ' .
+      $self->response_msg());
+  }
+
+  my $msg = $self->response_msg();
+  if (wantarray()) {
+    return ($self->{ftp}->code, $msg);
+
+  } else {
+    return $msg;
+  }
+}
+
 sub abort {
   my $self = shift;
 
diff --git a/tests/t/lib/ProFTPD/TestSuite/Utils.pm b/tests/t/lib/ProFTPD/TestSuite/Utils.pm
index c994582..8872231 100644
--- a/tests/t/lib/ProFTPD/TestSuite/Utils.pm
+++ b/tests/t/lib/ProFTPD/TestSuite/Utils.pm
@@ -32,6 +32,7 @@ our @FEATURES = qw(
 
 our @RUNNING = qw(
   server_restart
+  server_signal
   server_start
   server_stop
   server_wait
@@ -333,6 +334,28 @@ sub config_write_subsection {
 
       print $fh "$indent</Limit>\n";
     }
+
+  } elsif ($type eq 'Class') {
+    my $sections = $config;
+
+    foreach my $class (keys(%$sections)) {
+      print $fh "<Class $class>\n";
+
+      my $section = $sections->{$class};
+
+      if (ref($section) eq 'HASH') {
+        while (my ($class_k, $class_v) = each(%$section)) {
+          print $fh "  $class_k $class_v\n";
+        }
+
+      } elsif (ref($section) eq 'ARRAY') {
+        foreach my $line (@$section) {
+          print $fh "  $line\n";
+        }
+      }
+
+      print $fh "</Class>\n";
+    }
   }
 }
 
@@ -359,7 +382,9 @@ sub config_write {
     $port = $config->{Port};
 
     unless (defined($config->{User})) {
-      $config->{User} = $user_name;
+      # Handle User names that may contain embedded backslashes and spaces
+      $user_name =~ s/\\/\\\\/g;
+      $config->{User} = "\"$user_name\"";
 
       if ($< == 0) {
         $config->{User} = 'root';
@@ -367,7 +392,9 @@ sub config_write {
     }
 
     unless (defined($config->{Group})) {
-      $config->{Group} = $group_name;
+      # Handle Group names that may contain embedded backslashes and spaces
+      $group_name =~ s/\\/\\\\/g;
+      $config->{Group} = "\"$group_name\"";
     }
 
     unless ($opts->{NoAllowOverride}) {
@@ -519,7 +546,14 @@ sub config_write {
 
             if (ref($section) eq 'HASH') {
               while (my ($mod_k, $mod_v) = each(%$section)) {
-                print $fh "  $mod_k $mod_v\n";
+
+                if (ref($mod_v) eq 'HASH' ||
+                    ref($mod_v) eq 'ARRAY') {
+                  config_write_subsection($fh, $mod_k, $mod_v, "  ");
+
+                } else {
+                  print $fh "  $mod_k $mod_v\n";
+                }
               }
 
             } elsif (ref($section) eq 'ARRAY') {
@@ -832,11 +866,13 @@ sub feature_have_module_loaded {
       my $alt_module = $module;
       $alt_module =~ s/\.c$/\//g;
 
+      my $res = 0;
       if (grep { /^($module$|$alt_module)/ } @$mod_list) {
-        return 1;
+        $res = 1;
       }
 
-      return 0;
+      close($cmdh);
+      return $res;
     }
 
     close($cmdh);
@@ -846,9 +882,11 @@ sub feature_have_module_loaded {
   }
 }
 
-sub server_restart {
+sub server_signal {
   my $pid_file = shift;
   croak("Missing PID file argument") unless $pid_file;
+  my $signal_name = shift;
+  $signal_name = 'HUP' unless $signal_name;
 
   my $pid;
   if (open(my $fh, "< $pid_file")) {
@@ -857,16 +895,33 @@ sub server_restart {
     close($fh);
 
   } else {
-    die("Can't read $pid_file: $!");
+    croak("Can't read $pid_file: $!");
   }
 
-  my $cmd = "kill -HUP $pid";
+  my $cmd = "kill -$signal_name $pid";
 
   if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Restarting server: $cmd\n";
+    my $label = 'Signalling';
+    if ($signal_name eq 'HUP') {
+      $label = 'Restarting';
+    }
+
+    print STDERR "$label server: $cmd\n";
   }
 
   my @output = `$cmd`;
+  if ($? != 0) {
+    return undef;
+  }
+
+  return 1;
+}
+
+sub server_restart {
+  my $pid_file = shift;
+  croak("Missing PID file argument") unless $pid_file;
+
+  return server_signal($pid_file, 'HUP');
 }
 
 sub server_start {
@@ -892,7 +947,12 @@ sub server_start {
     }
   }
 
-  $cmd .= "$proftpd_bin -q -c $abs_config_file";
+  my $quiet = '-q';
+  if ($ENV{TEST_VERBOSE}) {
+    $quiet = '';
+  }
+
+  $cmd .= "$proftpd_bin $quiet -c $abs_config_file";
 
   if (defined($server_opts->{define})) {
     my $defines = $server_opts->{define};
@@ -973,6 +1033,9 @@ sub server_stop {
   }
 
   my @output = `$cmd`;
+  unless ($? == 0) {
+    return undef;
+  }
 
   unless ($nowait) {
     # Wait until the PidFile has been deleted by the shutting-down server.
@@ -986,6 +1049,8 @@ sub server_stop {
       select(undef, undef, undef, 0.5);
     }
   }
+
+  return 1;
 }
 
 my $server_wait_timeout = 0;
@@ -1047,11 +1112,7 @@ sub test_append_logfile {
   my $out_file = File::Spec->rel2abs('tests.log');
 
   unless (open($outfh, ">> $out_file")) {
-    die("Can't append to $out_file: $!");
-  }
-
-  unless (open($infh, "< $log_file")) {
-    die("Can't read $log_file: $!");
+    croak("Can't append to $out_file: $!");
   }
 
   my ($pkg, $filename, $lineno, $func) = (caller(1))[0, 1, 2, 3];
@@ -1061,8 +1122,12 @@ sub test_append_logfile {
 
   print $outfh "-----BEGIN $func-----\n";
 
-  while (my $line = <$infh>) {
-    print $outfh $line;
+  if (open($infh, "+< $log_file")) {
+    while (my $line = <$infh>) {
+      print $outfh $line;
+    }
+
+    close($infh);
   }
 
   # If an exception was provided, write that out to the log file, too.
@@ -1072,10 +1137,8 @@ sub test_append_logfile {
 
   print $outfh "-----END $func-----\n";
 
-  close($infh);
-
   unless (close($outfh)) {
-    die("Can't write $out_file: $!");
+    croak("Can't write $out_file: $!");
   }
 }
 
@@ -1135,6 +1198,7 @@ sub test_setup {
   $uid = 500 unless defined($uid);
   my $gid = shift;
   $gid = 500 unless defined($gid);
+  my $home_dir = shift;
 
   my $config_file = "$tmpdir/$name.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/$name.pid");
@@ -1143,17 +1207,21 @@ sub test_setup {
   my $auth_user_file = File::Spec->rel2abs("$tmpdir/$name.passwd");
   my $auth_group_file = File::Spec->rel2abs("$tmpdir/$name.group");
 
-  my $home_dir = File::Spec->rel2abs($tmpdir);
+  # If the caller provides the home directory, it is ASSUMED that they will
+  # have created it.
+  unless (defined($home_dir)) {
+    $home_dir = File::Spec->rel2abs($tmpdir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      croak("Can't set perms on $home_dir to 0755: $!");
-    }
+    # Make sure that, if we're running as root, that the home directory has
+    # permissions/privs set for the account we create
+    if ($< == 0) {
+      unless (chmod(0755, $home_dir)) {
+        croak("Can't set perms on $home_dir to 0755: $!");
+      }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      croak("Can't set owner of $home_dir to $uid/$gid: $!");
+      unless (chown($uid, $gid, $home_dir)) {
+        croak("Can't set owner of $home_dir to $uid/$gid: $!");
+      }
     }
   }
 
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/APPE.pm b/tests/t/lib/ProFTPD/Tests/Commands/APPE.pm
index 4a6ad9b..23ce1c4 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/APPE.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/APPE.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -41,11 +42,71 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  appe_fails_abs_symlink_new => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_fails_abs_symlink_new_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  appe_fails_rel_symlink_new => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_fails_rel_symlink_new_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  appe_ok_rel_symlink_existing => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_ok_rel_symlink_existing_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  appe_ok_abs_symlink_existing => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_ok_abs_symlink_existing_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   appe_fails_not_reg => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  appe_fails_rel_symlink_not_reg => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_fails_rel_symlink_not_reg_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_fails_abs_symlink_not_reg => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  appe_fails_abs_symlink_not_reg_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   appe_fails_login_required => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -61,7 +122,7 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  appe_hiddenstores_bug3598 => {
+  appe_hiddenstores_bug4144 => {
     order => ++$order,
     test_class => [qw(bug forking)],
   },
@@ -79,45 +140,15 @@ sub list_tests {
 sub appe_ok_raw_active {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -126,7 +157,8 @@ sub appe_ok_raw_active {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -143,9 +175,8 @@ sub appe_ok_raw_active {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->appe_raw('bar');
       unless ($conn) {
@@ -163,7 +194,6 @@ sub appe_ok_raw_active {
 
       $client->quit();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -172,7 +202,7 @@ sub appe_ok_raw_active {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -182,62 +212,24 @@ sub appe_ok_raw_active {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub appe_ok_raw_passive {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-  
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -246,7 +238,8 @@ sub appe_ok_raw_passive {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -263,9 +256,8 @@ sub appe_ok_raw_passive {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-
-      $client->login($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->appe_raw('bar');
       unless ($conn) {
@@ -283,7 +275,6 @@ sub appe_ok_raw_passive {
 
       $client->quit();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -292,7 +283,7 @@ sub appe_ok_raw_passive {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -302,65 +293,26 @@ sub appe_ok_raw_passive {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub appe_ok_file_new {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-  
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/bar");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     AllowOverwrite => 'on',
     AllowStoreRestart => 'on',
@@ -372,7 +324,8 @@ sub appe_ok_file_new {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -389,8 +342,8 @@ sub appe_ok_file_new {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->appe_raw('bar');
       unless ($conn) {
@@ -411,7 +364,6 @@ sub appe_ok_file_new {
       $self->assert($expected == $size,
         test_msg("Expected $expected, got $size"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -420,7 +372,7 @@ sub appe_ok_file_new {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -430,55 +382,16 @@ sub appe_ok_file_new {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub appe_ok_file_existing {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-  
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/bar");
   if (open(my $fh, "> $test_file")) {
@@ -493,13 +406,23 @@ sub appe_ok_file_existing {
 
   my $test_sz = -s $test_file;
 
+  if ($< == 0) {
+    unless (chmod(0755, $test_file)) {
+      die("Can't set perms on $test_file to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     AllowOverwrite => 'on',
     AllowStoreRestart => 'on',
@@ -511,7 +434,8 @@ sub appe_ok_file_existing {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -528,8 +452,8 @@ sub appe_ok_file_existing {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->appe_raw('bar');
       unless ($conn) {
@@ -550,7 +474,6 @@ sub appe_ok_file_existing {
       $self->assert($expected == $size,
         test_msg("Expected $expected, got $size"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -559,7 +482,7 @@ sub appe_ok_file_existing {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -569,66 +492,27 @@ sub appe_ok_file_existing {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub appe_ok_files_new_and_existing_bug3612 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-  
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file1 = File::Spec->rel2abs("$tmpdir/foo");
   my $test_file2 = File::Spec->rel2abs("$tmpdir/bar");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     AllowOverwrite => 'on',
     AllowStoreRestart => 'on',
@@ -640,7 +524,8 @@ sub appe_ok_files_new_and_existing_bug3612 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -657,8 +542,8 @@ sub appe_ok_files_new_and_existing_bug3612 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       # 1.  STOR foo
       my $conn = $client->stor_raw('foo');
@@ -736,13 +621,13 @@ sub appe_ok_files_new_and_existing_bug3612 {
       $resp_code = $client->response_code();
       $resp_msg = $client->response_msg();
       $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
 
       $expected = $test_sz2;
       $test_sz = -s $test_file2;
       $self->assert($expected == $test_sz,
         test_msg("Expected $expected bytes, got $test_sz bytes"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -751,7 +636,7 @@ sub appe_ok_files_new_and_existing_bug3612 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -761,63 +646,47 @@ sub appe_ok_files_new_and_existing_bug3612 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub appe_fails_not_reg {
+sub appe_fails_abs_symlink_new {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
-  
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    AllowOverwrite => 'on',
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -826,7 +695,8 @@ sub appe_fails_not_reg {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -843,32 +713,31 @@ sub appe_fails_not_reg {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-
-      my $conn = $client->appe_raw($home_dir);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
+      my $conn = $client->appe_raw('test.d/test.lnk');
       if ($conn) {
-        die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
+        die("APPE succeeded unexpectedly");
       }
 
-      my $expected;
+      # In this case, we SHOULD expect failure, since we have a dangling
+      # symlink.  We COULD automatically create a file, but we don't know
+      # whether that symlink is meant to point to a file, or a directory,
+      # or anything else.  Thus we bail.
 
-      $expected = 550;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$home_dir: Not a regular file";
+      $expected = 'test.d/test.lnk: Not a regular file';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -877,7 +746,7 @@ sub appe_fails_not_reg {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -887,34 +756,50 @@ sub appe_fails_not_reg {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub appe_fails_login_required {
+sub appe_fails_abs_symlink_new_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
 
-  my $log_file = test_get_logfile();
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:20 fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -923,7 +808,8 @@ sub appe_fails_login_required {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -940,29 +826,31 @@ sub appe_fails_login_required {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->appe($config_file, '/dev/null') };
-      unless ($@) {
-        die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
       }
 
-      my $expected;
+      # In this case, we SHOULD expect failure, since we have a dangling
+      # symlink.  We COULD automatically create a file, but we don't know
+      # whether that symlink is meant to point to a file, or a directory,
+      # or anything else.  Thus we bail.
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 530;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "Please login with USER and PASS";
+      $expected = 'test.d/test.lnk: Not a regular file';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -971,7 +859,7 @@ sub appe_fails_login_required {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -981,62 +869,58 @@ sub appe_fails_login_required {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub appe_fails_no_path {
+sub appe_fails_rel_symlink_new {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $log_file = test_get_logfile();
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
-  
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -1045,7 +929,8 @@ sub appe_fails_no_path {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1062,40 +947,40 @@ sub appe_fails_no_path {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      eval { ($resp_code, $resp_msg) = $client->appe($config_file, '') };
-      unless ($@) {
-        die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
       }
 
-      my $expected;
+      # In this case, we SHOULD expect failure, since we have a dangling
+      # symlink.  We COULD automatically create a file, but we don't know
+      # whether that symlink is meant to point to a file, or a directory,
+      # or anything else.  Thus we bail.
 
-      $expected = 500;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "'APPE' not understood";
+      $expected = 'test.d/test.lnk: Not a regular file';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
 
-    print $wfh "done\n";
+    $wfh->print("done\n");
+    $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1105,75 +990,59 @@ sub appe_fails_no_path {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub appe_fails_eperm {
+sub appe_fails_rel_symlink_new_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
 
-  } else {
-    die("Can't open $src_file: $!");
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
   }
 
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo/bar.txt");
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
-  
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -1182,7 +1051,8 @@ sub appe_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1199,16 +1069,1127 @@ sub appe_fails_eperm {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
+      }
 
-      $client->login($user, $passwd);
-      $client->port();
+      # In this case, we SHOULD expect failure, since we have a dangling
+      # symlink.  We COULD automatically create a file, but we don't know
+      # whether that symlink is meant to point to a file, or a directory,
+      # or anything else.  Thus we bail.
 
-      chmod(0660, $sub_dir);
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->appe($src_file, $dst_file) };
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'test.d/test.lnk: Not a regular file';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message $expected, got $resp_msg"));
+    };
+    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 appe_ok_abs_symlink_existing {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$test_file': $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      unless ($conn) {
+        die("APPE failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected $expected, got $size"));
+    };
+    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 appe_ok_abs_symlink_existing_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:20 fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      unless ($conn) {
+        die("APPE failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected $expected, got $size"));
+    };
+    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 appe_ok_rel_symlink_existing {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      unless ($conn) {
+        die("APPE failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected $expected, got $size"));
+    };
+    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 appe_ok_rel_symlink_existing_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:20 fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/test.lnk');
+      unless ($conn) {
+        die("APPE failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected $expected, got $size"));
+    };
+    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 appe_fails_not_reg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw($setup->{home_dir});
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      if ($conn) {
+        die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
+      }
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$setup->{home_dir}: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 appe_fails_abs_symlink_not_reg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d/foo.d");
+  mkpath($test_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/foo.d');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.d/foo.d: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 appe_fails_abs_symlink_not_reg_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d/foo.d");
+  mkpath($test_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  unless (symlink($test_dir, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$test_dir': $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/foo.d');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.d/foo.d: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 appe_fails_rel_symlink_not_reg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d/foo.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./foo.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './foo.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/foo.d');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.d/foo.d: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 appe_fails_rel_symlink_not_reg_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d/foo.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./foo.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './foo.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->appe_raw('test.d/foo.d');
+      if ($conn) {
+        die("APPE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.d/foo.d: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 appe_fails_login_required {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    IfModules => {
+      '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, 0, 1);
+      my ($resp_code, $resp_msg);
+
+      my $path = $setup->{config_file};
+      eval { ($resp_code, $resp_msg) = $client->appe($path, '/dev/null') };
       unless ($@) {
         die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
 
@@ -1217,17 +2198,16 @@ sub appe_fails_eperm {
         $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$dst_file: Permission denied";
+      $expected = "Please login with USER and PASS";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1236,7 +2216,7 @@ sub appe_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1246,65 +2226,224 @@ sub appe_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub appe_fails_no_path {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg);
+
+      my $path = $setup->{config_file};
+      eval { ($resp_code, $resp_msg) = $client->appe($path, '') };
+      unless ($@) {
+        die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
+
+      } else {
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
+      }
+
+      $client->quit();
+
+      my $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "'APPE' not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
 
-    die($ex);
+    exit 0;
   }
 
-  unlink($log_file);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub appe_hiddenstores_bug3598 {
+sub appe_fails_eperm {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  } else {
+    die("Can't open $src_file: $!");
+  }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/bar");
+  my $dst_file = File::Spec->rel2abs("$tmpdir/foo/bar.txt");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
   
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->port();
+
+      chmod(0660, $sub_dir);
+
+      my ($resp_code, $resp_msg);
+      eval { ($resp_code, $resp_msg) = $client->appe($src_file, $dst_file) };
+      unless ($@) {
+        die("APPE succeeded unexpectedly ($resp_code $resp_msg)");
+
+      } else {
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
+      }
+
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$dst_file: Permission denied";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub appe_hiddenstores_bug4144 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/bar");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     HiddenStores => 'on',
 
@@ -1315,7 +2454,8 @@ sub appe_hiddenstores_bug3598 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1332,29 +2472,32 @@ sub appe_hiddenstores_bug3598 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->appe_raw('bar');
-      if ($conn) {
-        die("APPE succeeded unexpectedly");
+      unless ($conn) {
+        die("APPE failed: " . $client->response_code() . " " .
+          $client->response_msg());
       }
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
 
-      my $expected;
-
-      $expected = 550;
+      my $expected = 150;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "APPE not compatible with server configuration";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
 
+      $client->quit();
+    };
     if ($@) {
       $ex = $@;
     }
@@ -1363,7 +2506,7 @@ sub appe_hiddenstores_bug3598 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1373,18 +2516,10 @@ sub appe_hiddenstores_bug3598 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/CLNT.pm b/tests/t/lib/ProFTPD/Tests/Commands/CLNT.pm
new file mode 100644
index 0000000..3eb1981
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Commands/CLNT.pm
@@ -0,0 +1,106 @@
+package ProFTPD::Tests::Commands::CLNT;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  clnt_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub clnt_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    IfModules => {
+      '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, 0, 1);
+      my ($resp_code, $resp_msg) = $client->clnt('FOOBAR');
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "OK";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/CWD.pm b/tests/t/lib/ProFTPD/Tests/Commands/CWD.pm
index 1f29519..0635597 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/CWD.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/CWD.pm
@@ -22,6 +22,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  cwd_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  cwd_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  cwd_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  cwd_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   cwd_fails_enoent => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -32,6 +52,31 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  cwd_fails_enotdir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  cwd_fails_abs_symlink_enotdir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  cwd_fails_abs_symlink_enotdir_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  cwd_fails_rel_symlink_enotdir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  cwd_fails_rel_symlink_enotdir_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   xcwd_ok => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -72,61 +117,939 @@ my $TESTS = {
     test_class => [qw(bug forking rootprivs)],
   },
 
-};
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub cwd_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->cwd($sub_dir);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "CWD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  mkpath($sub_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/sub.d/test.lnk");
+
+  unless (symlink($sub_dir, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$sub_dir': $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->cwd($path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "CWD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  mkpath($sub_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/sub.d/test.lnk");
+
+  my $dst_path = $sub_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->cwd($path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "CWD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->cwd($path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "CWD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->cwd($path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "CWD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_fails_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->cwd($sub_dir) };
+      unless ($@) {
+        die("CWD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$sub_dir: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_fails_eperm {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # Make it such that perms on the dir do not allow chdirs
+      unless (chmod(0440, $sub_dir)) {
+        die("Failed to change perms on $sub_dir: $!");
+      }
+
+      eval { $client->cwd($sub_dir) };
+      unless ($@) {
+        die("CWD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$sub_dir: (No such file or directory|Permission denied)";
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_fails_enotdir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo.d");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'foo.d';
+      eval { $client->cwd($path) };
+      unless ($@) {
+        die("CWD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: Not a directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 cwd_fails_abs_symlink_enotdir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.d");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$test_file': $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.lnk';
+      eval { $client->cwd($path) };
+      unless ($@) {
+        die("CWD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: Not a directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
 
-sub new {
-  return shift()->SUPER::new(@_);
-}
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
 
-sub list_tests {
-  return testsuite_get_runnable_tests($TESTS);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub cwd_ok {
+sub cwd_fails_abs_symlink_enotdir_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.d");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
-    }
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -135,7 +1058,8 @@ sub cwd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -152,24 +1076,27 @@ sub cwd_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->cwd($sub_dir);
+      my $path = 'sub.d/test.lnk';
+      eval { $client->cwd($path) };
+      unless ($@) {
+        die("CWD succeeded unexpectedly");
+      }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "CWD command successful";
+      $expected = "$path: Not a directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -178,7 +1105,7 @@ sub cwd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -188,64 +1115,51 @@ sub cwd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub cwd_fails_enoent {
+sub cwd_fails_rel_symlink_enotdir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.d");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
   }
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -254,7 +1168,8 @@ sub cwd_fails_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -271,31 +1186,27 @@ sub cwd_fails_enoent {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->cwd($sub_dir) };
+      my $path = 'sub.d/test.lnk';
+      eval { $client->cwd($path) };
       unless ($@) {
         die("CWD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$sub_dir: No such file or directory";
+      $expected = "$path: Not a directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -304,7 +1215,7 @@ sub cwd_fails_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -314,65 +1225,53 @@ sub cwd_fails_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub cwd_fails_eperm {
+sub cwd_fails_rel_symlink_enotdir_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.d");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
-    }
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
-    }
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -381,7 +1280,8 @@ sub cwd_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -398,36 +1298,27 @@ sub cwd_fails_eperm {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      # Make it such that perms on the dir do not allow chdirs
-      unless (chmod(0440, $sub_dir)) {
-        die("Failed to change perms on $sub_dir: $!");
-      }
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->cwd($sub_dir) };
+      my $path = 'sub.d/test.lnk';
+      eval { $client->cwd($path) };
       unless ($@) {
         die("CWD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$sub_dir: No such file or directory";
+      $expected = "$path: Not a directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -436,7 +1327,7 @@ sub cwd_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -446,65 +1337,37 @@ sub cwd_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub xcwd_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
   mkpath($sub_dir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -513,7 +1376,8 @@ sub xcwd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -530,24 +1394,20 @@ sub xcwd_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->xcwd($sub_dir);
-
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->xcwd($sub_dir);
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "XCWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -556,7 +1416,7 @@ sub xcwd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -566,44 +1426,21 @@ sub xcwd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub cwd_symlinks_traversing_bug3297 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir1 = File::Spec->rel2abs("$tmpdir/xte");
   my $sub_dir2 = File::Spec->rel2abs("$tmpdir/xte/data");
   my $sub_dir3 = File::Spec->rel2abs("$tmpdir/xte/data/.newarchive/XTE_IDX1");
   my $sub_dir4 = File::Spec->rel2abs("$tmpdir/xte/data/.newarchive/XTE3/ASMProducts");
-
   mkpath([$sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4]);
 
   my $cwd = getcwd();
@@ -612,7 +1449,7 @@ sub cwd_symlinks_traversing_bug3297 {
   }
 
   my $chrooted_dir3 = $sub_dir3;
-  $chrooted_dir3 =~ s/$home_dir//g;
+  $chrooted_dir3 =~ s/$setup->{home_dir}//g;
 
   # Bug#3297 occurred because proftpd wasn't handling this trailing slash
   # in the symlink destination path.  If this trailing slash is NOT present
@@ -651,31 +1488,25 @@ sub cwd_symlinks_traversing_bug3297 {
     die("Can't open $sub_dir4/test.txt: $!");
   }
  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
+      die("Can't set perms on $sub_dir1 to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
+      die("Can't set owner of $sub_dir1 to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'fsio:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     ShowSymlinks => 'on',
     DefaultRoot => '~',
@@ -687,7 +1518,8 @@ sub cwd_symlinks_traversing_bug3297 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -704,65 +1536,62 @@ sub cwd_symlinks_traversing_bug3297 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->pwd();
 
-      my ($resp_code, $resp_msg);
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->cwd('xte');
 
-      ($resp_code, $resp_msg) = $client->cwd('xte');
-
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->cwd('data');
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->cwd('archive');
     
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
     
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->cwd('ASMProducts');
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->pwd();
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "\"/xte/data/.newarchive/XTE3/ASMProducts\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $client->quit();
+    };
     if ($@) {
       $ex = $@;
     }
@@ -771,7 +1600,7 @@ sub cwd_symlinks_traversing_bug3297 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -781,44 +1610,21 @@ sub cwd_symlinks_traversing_bug3297 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{config_file}, $ex);
 }
 
 sub cwd_symlinks_oneshot_bug3297 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir1 = File::Spec->rel2abs("$tmpdir/xte");
   my $sub_dir2 = File::Spec->rel2abs("$tmpdir/xte/data");
   my $sub_dir3 = File::Spec->rel2abs("$tmpdir/xte/data/.newarchive/XTE_IDX1");
   my $sub_dir4 = File::Spec->rel2abs("$tmpdir/xte/data/.newarchive/XTE3/ASMProducts");
-
   mkpath([$sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4]);
 
   my $cwd = getcwd();
@@ -827,7 +1633,7 @@ sub cwd_symlinks_oneshot_bug3297 {
   }
 
   my $chrooted_dir3 = $sub_dir3;
-  $chrooted_dir3 =~ s/$home_dir//g;
+  $chrooted_dir3 =~ s/$setup->{home_dir}//g;
 
   # Bug#3297 occurred because proftpd wasn't handling this trailing slash
   # in the symlink destination path.  If this trailing slash is NOT present
@@ -866,31 +1672,25 @@ sub cwd_symlinks_oneshot_bug3297 {
     die("Can't open $sub_dir4/test.txt: $!");
   }
  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
+      die("Can't set perms on $sub_dir1 to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir1, $sub_dir2, $sub_dir3, $sub_dir4)) {
+      die("Can't set owner of $sub_dir1 to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'fsio:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     ShowSymlinks => 'on',
     DefaultRoot => '~',
@@ -902,7 +1702,8 @@ sub cwd_symlinks_oneshot_bug3297 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -919,38 +1720,35 @@ sub cwd_symlinks_oneshot_bug3297 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->syst();
       $client->pwd();
       $client->type('binary');
 
-      my ($resp_code, $resp_msg);
-      my $expected;
-
       # Now try to go to the ASMProducts directory in a single CWD
-      ($resp_code, $resp_msg) = $client->cwd('/xte/data/archive/ASMProducts');
+      my ($resp_code, $resp_msg) = $client->cwd('/xte/data/archive/ASMProducts');
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->pwd();
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "\"/xte/data/.newarchive/XTE3/ASMProducts\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $client->quit();
+    };
     if ($@) {
       $ex = $@;
     }
@@ -959,7 +1757,7 @@ sub cwd_symlinks_oneshot_bug3297 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -969,55 +1767,16 @@ sub cwd_symlinks_oneshot_bug3297 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub cwd_long_path_bug3730 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $long_path = "../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.. [...]
 
@@ -1028,12 +1787,12 @@ sub cwd_long_path_bug3730 {
   my $timeout_idle = 3;
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     CommandBufferSize => $cmd_bufsz,
     DefaultRoot => '~',
@@ -1046,7 +1805,8 @@ sub cwd_long_path_bug3730 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1063,21 +1823,14 @@ sub cwd_long_path_bug3730 {
   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($user, $passwd);
-      my ($resp_code, $resp_msg) = $client->cwd($long_path);
-
-      my $expected;
-
-      $expected = 250;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      $expected = "CWD command successful";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      eval { $client->cwd($long_path) };
+      eval { $client->quit() };
+      unless ($@) {
+        die("QUIT succeeded unexpectedly");
+      }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1086,7 +1839,7 @@ sub cwd_long_path_bug3730 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1096,65 +1849,37 @@ sub cwd_long_path_bug3730 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub cwd_tilde_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/0");
   mkpath($sub_dir);
   
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1163,7 +1888,8 @@ sub cwd_tilde_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1180,23 +1906,20 @@ sub cwd_tilde_ok {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->cwd('~/0');
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->cwd('~/0');
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1205,7 +1928,7 @@ sub cwd_tilde_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1215,32 +1938,16 @@ sub cwd_tilde_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub cwd_tilde_user_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $user = 'proftpd';
   my $passwd = 'test';
@@ -1258,34 +1965,29 @@ sub cwd_tilde_user_ok {
   my $sub_dir = File::Spec->rel2abs("$tmpdir/0");
   mkpath($sub_dir);
   
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $uid/$gid: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_user_write($auth_user_file, $user2, $passwd2, $uid2, $gid2, $home_dir2,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-  auth_group_write($auth_group_file, $group, $gid2, $user2);
+  auth_user_write($setup->{auth_user_file}, $user2, $passwd2, $uid2, $gid2,
+    $home_dir2, '/bin/bash');
+  auth_group_write($setup->{auth_group_file}, $group, $gid2, $user2);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'auth:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1294,7 +1996,8 @@ sub cwd_tilde_user_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1311,8 +2014,8 @@ sub cwd_tilde_user_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $dir = "~$user2/0";
       eval { $client->cwd($dir) };
@@ -1323,19 +2026,16 @@ sub cwd_tilde_user_ok {
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
 
-      my $expected;
-
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "$dir: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1344,7 +2044,7 @@ sub cwd_tilde_user_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1354,65 +2054,37 @@ sub cwd_tilde_user_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub cwd_tilde_chrooted_bug3785 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/0");
   mkpath($sub_dir);
   
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
     DefaultRoot => '~',
 
     IfModules => {
@@ -1422,7 +2094,8 @@ sub cwd_tilde_chrooted_bug3785 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1439,35 +2112,31 @@ sub cwd_tilde_chrooted_bug3785 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->cwd('~/0');
+      my ($resp_code, $resp_msg) = $client->cwd('~/0');
 
-      my $expected;
-
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->pwd();
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "\"/0\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1476,7 +2145,7 @@ sub cwd_tilde_chrooted_bug3785 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1486,32 +2155,16 @@ sub cwd_tilde_chrooted_bug3785 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub cwd_tilde_user_chrooted_bug3785 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $user = 'proftpd';
   my $passwd = 'test';
@@ -1529,34 +2182,29 @@ sub cwd_tilde_user_chrooted_bug3785 {
   my $sub_dir = File::Spec->rel2abs("$tmpdir/0");
   mkpath($sub_dir);
   
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $uid/$gid: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_user_write($auth_user_file, $user2, $passwd2, $uid2, $gid2, $home_dir2,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-  auth_group_write($auth_group_file, $group, $gid2, $user2);
+  auth_user_write($setup->{auth_user_file}, $user2, $passwd2, $uid2, $gid2,
+    $home_dir2, '/bin/bash');
+  auth_group_write($setup->{auth_group_file}, $group, $gid2, $user2);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'auth:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
     DefaultRoot => '~',
 
     IfModules => {
@@ -1566,7 +2214,8 @@ sub cwd_tilde_user_chrooted_bug3785 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1583,8 +2232,8 @@ sub cwd_tilde_user_chrooted_bug3785 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $dir = "~$user2/0";
       eval { $client->cwd($dir) };
@@ -1595,19 +2244,16 @@ sub cwd_tilde_user_chrooted_bug3785 {
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
 
-      my $expected;
-
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "$dir: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1616,7 +2262,7 @@ sub cwd_tilde_user_chrooted_bug3785 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1626,18 +2272,10 @@ sub cwd_tilde_user_chrooted_bug3785 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/DELE.pm b/tests/t/lib/ProFTPD/Tests/Commands/DELE.pm
index cf956c0..44b95ac 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/DELE.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/DELE.pm
@@ -577,22 +577,7 @@ sub dele_fails_eisdir {
 sub dele_symlink_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/foo");
   if (open(my $fh, "> $test_file")) {
@@ -604,8 +589,8 @@ sub dele_symlink_ok {
 
   # Create a symlink to the test file
   my $cwd = getcwd();
-  unless (chdir($home_dir)) {
-    die("Can't chdir to $home_dir: $!");
+  unless (chdir($setup->{home_dir})) {
+    die("Can't chdir to $setup->{home_dir}: $!");
   }
 
   unless (symlink('./foo', 'bar')) {
@@ -618,29 +603,13 @@ sub dele_symlink_ok {
 
   my $test_symlink = File::Spec->rel2abs("$tmpdir/bar");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -649,7 +618,8 @@ sub dele_symlink_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -666,23 +636,19 @@ sub dele_symlink_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->dele('bar');
-
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->dele('bar');
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "DELE command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      $client->quit();
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       unless (-f $test_file) {
         die("File $test_file does not exist as expected");
@@ -691,9 +657,7 @@ sub dele_symlink_ok {
       if (-l $test_symlink) {
         die("Symlink $test_symlink exists unexpectedly");
       }
-
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -702,7 +666,7 @@ sub dele_symlink_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -712,39 +676,16 @@ sub dele_symlink_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub dele_symlink_bug3754 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/foo");
   if (open(my $fh, "> $test_file")) {
@@ -782,26 +723,22 @@ sub dele_symlink_bug3754 {
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $home_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -810,9 +747,10 @@ sub dele_symlink_bug3754 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  if (open(my $fh, ">> $config_file")) {
+  if (open(my $fh, ">> $setup->{config_file}")) {
     print $fh <<EOC;
 <Directory ~>
   <Limit WRITE>
@@ -827,11 +765,11 @@ sub dele_symlink_bug3754 {
 </Directory>
 EOC
     unless (close($fh)) {
-      die("Can't write $config_file: $!");
+      die("Can't write $setup->{config_file}: $!");
     }
 
   } else {
-    die("Can't open $config_file: $!");
+    die("Can't open $setup->{config_file}: $!");
   }
 
   # Open pipes, for use between the parent and child processes.  Specifically,
@@ -849,24 +787,20 @@ EOC
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->cwd('sub.d');
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->dele('bar');
-
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->dele('bar');
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "DELE command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      $client->quit();
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       unless (-f $test_file) {
         die("File $test_file does not exist as expected");
@@ -875,9 +809,7 @@ EOC
       if (-l $test_symlink) {
         die("Symlink $test_symlink exists unexpectedly");
       }
-
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -886,7 +818,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -896,18 +828,10 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/FEAT.pm b/tests/t/lib/ProFTPD/Tests/Commands/FEAT.pm
index 3b0f030..af15f72 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/FEAT.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/FEAT.pm
@@ -33,45 +33,15 @@ sub list_tests {
 sub feat_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -80,8 +50,8 @@ sub feat_ok {
     },
   };
 
-  # By default, we expect to see 9 lines in the FEAT response
-  my $expected_nfeat = 9;
+  # By default, we expect to see 12 lines in the FEAT response
+  my $expected_nfeat = 12;
 
   my $have_nls = feature_have_feature_enabled('nls');
   if ($have_nls) {
@@ -101,7 +71,8 @@ sub feat_ok {
     $expected_nfeat += 1;
   }
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -124,11 +95,9 @@ sub feat_ok {
       my $resp_code = $client->response_code();
       my $resp_msgs = $client->response_msgs();
 
-      my $expected;
-
-      $expected = 211;
+      my $expected = 211;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       my $nfeat = scalar(@$resp_msgs);
       $self->assert($expected_nfeat == $nfeat,
@@ -136,6 +105,9 @@ sub feat_ok {
 
       my $feats = { 
         'Features:' => 1,
+        ' EPRT' => 1,
+        ' EPSV' => 1,
+        ' HOST' => 1,
         ' MDTM' => 1,
         ' MFMT' => 1,
         ' TVFS' => 1,
@@ -184,7 +156,7 @@ sub feat_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -194,18 +166,10 @@ sub feat_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/HOST.pm b/tests/t/lib/ProFTPD/Tests/Commands/HOST.pm
index 4c00a45..e9d9fd2 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/HOST.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/HOST.pm
@@ -21,6 +21,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  host_login_succeeds => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  # TODO:
+  #  host_default_server_login_succeeds
+  #    where we do NOT send HOST, and mark an aliased name-based host with
+  #    DefaultServer
+
   host_literal_ipv6_fails_useipv6_off => {
     order => ++$order,
     test_class => [qw(feature_ipv6 forking)],
@@ -71,8 +81,24 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  # HOST before/after FEAT
+  host_before_feat_ok => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  host_after_feat_ok => {
+    order => ++$order,
+    test_class => [qw(forking mod_tls)],
+  },
+
+  # Various config situations
+
   # 2-vhost config, 2 vhost (same addr, different DNS) config,
+
+  host_config_limit_denied => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
 };
 
 sub new {
@@ -83,49 +109,126 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
-sub host_after_login_fails {
+sub host_login_succeeds {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:20',
+
+    AuthOrder => 'mod_auth_pam.c mod_auth_unix.c',
+    DefaultServer => 'on',
+
+    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;
+<VirtualHost 127.0.0.1>
+  ServerAlias localhost
+  Port $port
+
+  AuthUserFile $setup->{auth_user_file}
+  AuthGroupFile $setup->{auth_group_file}
+  AuthOrder mod_auth_file.c
+
+  <IfModule mod_delay.c>
+    DelayEngine off
+  </IfModule>
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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('localhost');
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
-  my $log_file = test_get_logfile();
+      my $expected;
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub host_after_login_fails {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -134,7 +237,8 @@ sub host_after_login_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -152,7 +256,7 @@ sub host_after_login_fails {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       eval { $client->host('localhost') };
       unless ($@) {
@@ -181,7 +285,7 @@ sub host_after_login_fails {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -191,63 +295,25 @@ sub host_after_login_fails {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_literal_ipv6_fails_useipv6_off {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
     UseIPv6 => 'off',
 
     IfModules => {
@@ -257,7 +323,8 @@ sub host_literal_ipv6_fails_useipv6_off {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -304,7 +371,7 @@ sub host_literal_ipv6_fails_useipv6_off {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -314,63 +381,25 @@ sub host_literal_ipv6_fails_useipv6_off {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_literal_ipv6_with_port_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
     UseIPv6 => 'on',
 
     IfModules => {
@@ -380,7 +409,8 @@ sub host_literal_ipv6_with_port_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -427,7 +457,7 @@ sub host_literal_ipv6_with_port_fails {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -437,63 +467,25 @@ sub host_literal_ipv6_with_port_fails {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_invalid_ipv4_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -502,7 +494,8 @@ sub host_invalid_ipv4_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -549,7 +542,7 @@ sub host_invalid_ipv4_fails {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -559,63 +552,25 @@ sub host_invalid_ipv4_fails {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_ipv4_with_port_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -624,7 +579,8 @@ sub host_ipv4_with_port_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -671,7 +627,7 @@ sub host_ipv4_with_port_fails {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -681,63 +637,25 @@ sub host_ipv4_with_port_fails {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_unknown_host_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -746,7 +664,8 @@ sub host_unknown_host_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -793,7 +712,7 @@ sub host_unknown_host_fails {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -803,63 +722,25 @@ sub host_unknown_host_fails {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_known_ipv4_same_host_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -868,7 +749,8 @@ sub host_known_ipv4_same_host_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -911,7 +793,7 @@ sub host_known_ipv4_same_host_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -921,63 +803,25 @@ sub host_known_ipv4_same_host_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_known_ipv6_same_host_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
     UseIPv6 => 'on',
     DefaultAddress => '::1',
 
@@ -988,7 +832,8 @@ sub host_known_ipv6_same_host_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1032,7 +877,7 @@ sub host_known_ipv6_same_host_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1042,55 +887,17 @@ sub host_known_ipv6_same_host_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_known_ipv4_different_host_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $ipv4_addr = Sys::HostAddr->new();
   my $v4_addrs = $ipv4_addr->addresses();
@@ -1109,12 +916,12 @@ sub host_known_ipv4_different_host_fails {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1123,9 +930,10 @@ sub host_known_ipv4_different_host_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  if (open(my $fh, ">> $config_file")) {
+  if (open(my $fh, ">> $setup->{config_file}")) {
     print $fh <<EOC;
 <VirtualHost $vhost_addr>
   Port $port
@@ -1133,11 +941,11 @@ sub host_known_ipv4_different_host_fails {
 </VirtualHost>
 EOC
     unless (close($fh)) {
-      die("Can't write $config_file: $!");
+      die("Can't write $setup->{config_file}: $!");
     }
 
   } else {
-    die("Can't open $config_file: $!");
+    die("Can't open $setup->{config_file}: $!");
   }
 
   # Open pipes, for use between the parent and child processes.  Specifically,
@@ -1185,7 +993,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1195,55 +1003,17 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub host_known_ipv6_different_host_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $ipv6_addr = Sys::HostAddr->new(ipv => 6);
   my $v6_addrs = $ipv6_addr->addresses();
@@ -1266,12 +1036,12 @@ sub host_known_ipv6_different_host_fails {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
     UseIPv6 => 'on',
     DefaultAddress => '::1',
 
@@ -1282,9 +1052,10 @@ sub host_known_ipv6_different_host_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  if (open(my $fh, ">> $config_file")) {
+  if (open(my $fh, ">> $setup->{config_file}")) {
     print $fh <<EOC;
 <VirtualHost $vhost_addr>
   Port $port
@@ -1292,11 +1063,11 @@ sub host_known_ipv6_different_host_fails {
 </VirtualHost>
 EOC
     unless (close($fh)) {
-      die("Can't write $config_file: $!");
+      die("Can't write $setup->{config_file}: $!");
     }
 
   } else {
-    die("Can't open $config_file: $!");
+    die("Can't open $setup->{config_file}: $!");
   }
 
   # Open pipes, for use between the parent and child processes.  Specifically,
@@ -1344,7 +1115,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1354,65 +1125,231 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub host_known_dns_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'binding:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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}")) {
+    my $dns_name = 'localhost';
+
+    print $fh <<EOC;
+<VirtualHost $dns_name>
+  Port $port
+  ServerName $dns_name
+  ServerIdent off
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
 
-    die($ex);
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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);
+      my $host = "localhost";
+
+      $client->host($host);
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 220;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = '127.0.0.1 FTP server ready';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
   }
 
-  unlink($log_file);
+  # Stop server
+  server_stop($setup->{pid_file});
+
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub host_known_dns_ok {
+sub host_config_limit_denied {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $log_file = test_get_logfile();
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+<Limit HOST>
+  DenyAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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);
+
+      my $host = "127.0.0.1";
+      eval { $client->host($host) };
+      unless ($@) {
+        die("HOST after login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 504;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$host: Permission denied";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub host_before_feat_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $server_alias = 'ftp.nospam.org';
+  my $server_name = 'Other Virtual Host';
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'binding:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1421,24 +1358,28 @@ sub host_known_dns_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  if (open(my $fh, ">> $config_file")) {
-    my $dns_name = 'localhost';
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
+  if (open(my $fh, ">> $setup->{config_file}")) {
     print $fh <<EOC;
-<VirtualHost $dns_name>
+<VirtualHost 127.0.0.1>
   Port $port
-  ServerName $dns_name
-  ServerIdent off
+  ServerName "$server_name"
+  ServerAlias $server_alias
+
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
 </VirtualHost>
 EOC
     unless (close($fh)) {
-      die("Can't write $config_file: $!");
+      die("Can't write $setup->{config_file}: $!");
     }
 
   } else {
-    die("Can't open $config_file: $!");
+    die("Can't open $setup->{config_file}: $!");
   }
 
   # Open pipes, for use between the parent and child processes.  Specifically,
@@ -1457,9 +1398,7 @@ EOC
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      my $host = "localhost";
-
-      $client->host($host);
+      $client->host($server_alias);
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
 
@@ -1469,9 +1408,30 @@ EOC
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = '127.0.0.1 FTP server ready';
-      $self->assert($expected eq $resp_msg,
+      $expected = $server_name;
+      $self->assert(qr/$expected/, $resp_msg,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->feat();
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $saw_auth_tls = 0;
+      foreach my $feat (@$resp_msgs) {
+        if ($feat =~ /AUTH TLS/) {
+          $saw_auth_tls = 1;
+          last;
+        }
+      }
+
+      $self->assert($saw_auth_tls,
+        test_msg("Did not see expected 'AUTH TLS' feature listed via FEAT"));
+
+      $client->quit();
     };
 
     if ($@) {
@@ -1482,7 +1442,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1492,18 +1452,139 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub host_after_feat_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
 
-    die($ex);
+  my $server_alias = 'ftp.nospam.org';
+  my $server_name = 'Other Virtual Host';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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;
+<VirtualHost 127.0.0.1>
+  Port $port
+  ServerName "$server_name"
+  ServerAlias $server_alias
+
+  TLSEngine on
+  TLSLog $setup->{log_file}
+  TLSRSACertificateFile $cert_file
+  TLSCACertificateFile $ca_file
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
   }
 
-  unlink($log_file);
+  # 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->feat();
+
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      my $expected;
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $saw_auth_tls = 0;
+      foreach my $feat (@$resp_msgs) {
+        if ($feat =~ /AUTH TLS/) {
+          $saw_auth_tls = 1;
+          last;
+        }
+      }
+
+      $self->assert(!$saw_auth_tls,
+        test_msg("Saw 'AUTH TLS' feature listed via FEAT unexpectedly"));
+
+      $client->host($server_alias);
+      $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $expected = 220;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $server_name;
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/LIST.pm b/tests/t/lib/ProFTPD/Tests/Commands/LIST.pm
index 90f0337..5528a25 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/LIST.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/LIST.pm
@@ -8,6 +8,7 @@ use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
+use Time::HiRes qw(gettimeofday tv_interval);
 
 use ProFTPD::TestSuite::FTP;
 use ProFTPD::TestSuite::Utils qw(:auth :config :features :running :test :testsuite);
@@ -32,11 +33,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  list_file_rel_paths_bug4259 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  list_file_after_upload => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   list_ok_dir => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  list_dir_twice => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   list_ok_no_path => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -72,6 +88,11 @@ my $TESTS = {
     test_class => [qw(bug forking slow)],
   },
 
+  list_unsorted_buffering_bug4060 => {
+    order => ++$order,
+    test_class => [qw(bug forking slow)],
+  },
+
   list_opt_C => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -107,11 +128,26 @@ my $TESTS = {
     test_class => [qw(bug forking rootprivs)],
   },
 
+  list_showsymlinks_off => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  list_showsymlinks_off_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   list_showsymlinks_on => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  list_showsymlinks_on_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   list_glob_bug2367 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -177,6 +213,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  list_option_parsing => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   # XXX Plenty of other tests needed: params, maxfiles, maxdirs, depth, etc
 };
 
@@ -191,45 +232,15 @@ sub list_tests {
 sub list_ok_raw_active {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -238,7 +249,8 @@ sub list_ok_raw_active {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -255,9 +267,8 @@ sub list_ok_raw_active {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->list_raw();
       unless ($conn) {
@@ -269,6 +280,12 @@ sub list_ok_raw_active {
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
@@ -302,7 +319,6 @@ sub list_ok_raw_active {
         die("Unexpected name '$mismatch' appeared in LIST data")
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -311,7 +327,7 @@ sub list_ok_raw_active {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -321,62 +337,24 @@ sub list_ok_raw_active {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub list_ok_raw_passive {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -385,7 +363,8 @@ sub list_ok_raw_passive {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -402,9 +381,8 @@ sub list_ok_raw_passive {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-
-      $client->login($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->list_raw();
       unless ($conn) {
@@ -416,10 +394,16 @@ sub list_ok_raw_passive {
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/(\r)?\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
           $res->{$1} = 1;
@@ -449,7 +433,6 @@ sub list_ok_raw_passive {
         die("Unexpected name '$mismatch' appeared in LIST data")
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -458,7 +441,7 @@ sub list_ok_raw_passive {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -468,64 +451,26 @@ sub list_ok_raw_passive {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub list_ok_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
-  my $test_file = File::Spec->rel2abs($config_file);
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -534,7 +479,8 @@ sub list_ok_file {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -551,9 +497,8 @@ sub list_ok_file {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->list_raw($test_file);
       unless ($conn) {
@@ -565,6 +510,12 @@ sub list_ok_file {
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       my $res = {};
       my $lines = [split(/\n/, $buf)];
       foreach my $line (@$lines) {
@@ -582,7 +533,6 @@ sub list_ok_file {
         die("LIST failed to return $test_file");
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -591,7 +541,7 @@ sub list_ok_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -601,62 +551,36 @@ sub list_ok_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub list_ok_dir {
+# See: https://forums.proftpd.org/smf/index.php/topic,12000.0.html
+sub list_file_rel_paths_bug4259 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -665,7 +589,8 @@ sub list_ok_dir {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -682,11 +607,11 @@ sub list_ok_dir {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->list_raw($home_dir);
+      my $rel_path = 'test.dat';
+      my $conn = $client->list_raw($rel_path);
       unless ($conn) {
         die("LIST failed: " . $client->response_code() . " " .
           $client->response_msg());
@@ -696,6 +621,14 @@ sub list_ok_dir {
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# response: $buf\n";
+      }
+
       my $res = {};
       my $lines = [split(/\n/, $buf)];
       foreach my $line (@$lines) {
@@ -705,16 +638,53 @@ sub list_ok_dir {
       }
 
       my $count = scalar(keys(%$res));
-      my $expected = 6;
-      unless ($count == $expected) {
-        if ($ENV{TEST_VERBOSE}) {
-          print STDERR "Received:\n$buf\n";
+      unless ($count == 1) {
+        die("LIST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      unless (defined($res->{$rel_path})) {
+        die("LIST failed to return $rel_path");
+      }
+
+      # Now list using a different relative path syntax
+      $rel_path = './test.dat';
+      $conn = $client->list_raw($rel_path);
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# response: $buf\n";
+      }
+
+      $res = {};
+      $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$1} = 1;
         }
+      }
 
-        die("LIST returned wrong number of entries (expected $expected, got $count)");
+      $count = scalar(keys(%$res));
+      unless ($count == 1) {
+        die("LIST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      unless (defined($res->{$rel_path})) {
+        die("LIST failed to return $rel_path");
       }
-    };
 
+      $client->quit();
+    };
     if ($@) {
       $ex = $@;
     }
@@ -723,7 +693,7 @@ sub list_ok_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -733,21 +703,13 @@ sub list_ok_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub list_ok_no_path {
+sub list_file_after_upload {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -762,6 +724,7 @@ sub list_ok_no_path {
 
   my $user = 'proftpd';
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
@@ -780,12 +743,16 @@ sub list_ok_no_path {
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 fs.statcache:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -815,31 +782,73 @@ sub list_ok_no_path {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
-      my $conn = $client->list_raw();
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Logged in, sending STOR\n";
+      }
+ 
+      my $conn = $client->stor_raw('test.dat');
       unless ($conn) {
-        die("LIST failed: " . $client->response_code() . " " .
+        die("STOR test.dat failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
-      my $buf;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Data connection opened, writing data\n";
+      }
+ 
+      my $start = [gettimeofday()];
+      my $buf = "ABCD" x 10240;
+      $conn->write($buf, length($buf), 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $elapsed = tv_interval($start);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# elapsed upload duration: $elapsed\n";
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# File uploaded, sending LIST\n";
+      }
+ 
+      $conn = $client->list_raw($test_file);
+      unless ($conn) {
+        die("LIST $test_file failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r?\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
-          $res->{$1} = 1;
+          my $path = $1;
+          $res->{$path} = 1;
         }
       }
 
       my $count = scalar(keys(%$res));
-      my $expected = 6;
-      unless ($count == $expected) {
-        die("LIST returned wrong number of entries (expected $expected, got $count)");
+      unless ($count == 1) {
+        die("LIST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      unless (defined($res->{$test_file})) {
+        die("LIST failed to return $test_file");
       }
     };
 
@@ -847,7 +856,8 @@ sub list_ok_no_path {
       $ex = $@;
     }
 
-    print $wfh "done\n";
+    $wfh->print("done\n");
+    $wfh->flush();
 
   } else {
     eval { server_wait($config_file, $rfh) };
@@ -874,7 +884,7 @@ sub list_ok_no_path {
   unlink($log_file);
 }
 
-sub list_ok_glob {
+sub list_ok_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -945,7 +955,7 @@ sub list_ok_glob {
 
       $client->login($user, $passwd);
 
-      my $conn = $client->list_raw("*.conf");
+      my $conn = $client->list_raw($home_dir);
       unless ($conn) {
         die("LIST failed: " . $client->response_code() . " " .
           $client->response_msg());
@@ -964,13 +974,13 @@ sub list_ok_glob {
       }
 
       my $count = scalar(keys(%$res));
-      unless ($count == 1) {
-        die("LIST returned wrong number of entries (expected 1, got $count)");
-      }
+      my $expected = 6;
+      unless ($count == $expected) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Received:\n$buf\n";
+        }
 
-      my $test_file = 'cmds.conf';
-      unless (defined($res->{$test_file})) {
-        die("LIST failed to return $test_file");
+        die("LIST returned wrong number of entries (expected $expected, got $count)");
       }
     };
 
@@ -1006,7 +1016,7 @@ sub list_ok_glob {
   unlink($log_file);
 }
 
-sub list_fails_login_required {
+sub list_dir_twice {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1016,11 +1026,40 @@ sub list_fails_login_required {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -1046,26 +1085,64 @@ sub list_fails_login_required {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->list() };
-      unless ($@) {
-        die("LIST succeeded unexpectedly ($resp_code $resp_msg)");
+      my $conn = $client->list_raw($home_dir);
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $res = {};
+      my $lines = [split(/\r?\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$1} = 1;
+        }
       }
 
-      my $expected;
+      my $count = scalar(keys(%$res));
+      my $expected = 6;
+      unless ($count == $expected) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Received:\n$buf\n";
+        }
 
-      $expected = 530;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        die("LIST returned wrong number of entries (expected $expected, got $count)");
+      }
 
-      $expected = "Please login with USER and PASS";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $conn = $client->list_raw($home_dir);
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $res2 = {};
+      $lines = [split(/\r?\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res2->{$1} = 1;
+        }
+      }
+
+      my $count2 = scalar(keys(%$res2));
+      unless ($count2 == $count) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Received:\n$buf\n";
+        }
+
+        die("Second LIST returned wrong number of entries (expected $count, got $count2)");
+      }
+
+      $client->quit();
     };
 
     if ($@) {
@@ -1100,7 +1177,7 @@ sub list_fails_login_required {
   unlink($log_file);
 }
 
-sub list_fails_enoent {
+sub list_ok_no_path {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1133,7 +1210,7 @@ sub list_fails_enoent {
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $uid, $user);
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
 
   my $config = {
     PidFile => $pid_file,
@@ -1171,37 +1248,36 @@ sub list_fails_enoent {
 
       $client->login($user, $passwd);
 
-      my ($resp_code, $resp_msg);
-      $client->port();
-
-      my $test_file = 'foo/bar/baz';
-
-      eval { ($resp_code, $resp_msg) = $client->list($test_file) };
-      unless ($@) {
-        die("LIST succeeded unexpectedly ($resp_code $resp_msg)");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $conn = $client->list_raw();
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
       }
 
-      my $expected;
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
 
-      $expected = 450;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$1} = 1;
+        }
+      }
 
-      $expected = "$test_file: No such file or directory";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      my $count = scalar(keys(%$res));
+      my $expected = 6;
+      unless ($count == $expected) {
+        die("LIST returned wrong number of entries (expected $expected, got $count)");
+      }
     };
 
     if ($@) {
       $ex = $@;
     }
 
-    $wfh->print("done\n");
-    $wfh->flush();
+    print $wfh "done\n";
 
   } else {
     eval { server_wait($config_file, $rfh) };
@@ -1228,7 +1304,7 @@ sub list_fails_enoent {
   unlink($log_file);
 }
 
-sub list_fails_enoent_glob {
+sub list_ok_glob {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1298,20 +1374,37 @@ sub list_fails_enoent_glob {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
-      $client->port();
-
-      my $test_glob = '*foo';
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->list($test_glob);
-
-      # Unlike NLST, for LIST proftpd returns 226, and simply sends an
-      # empty list.
+      my $conn = $client->list_raw("*.conf");
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
 
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-    };
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
 
-    if ($@) {
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$1} = 1;
+        }
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 1) {
+        die("LIST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      my $test_file = 'cmds.conf';
+      unless (defined($res->{$test_file})) {
+        die("LIST failed to return $test_file");
+      }
+    };
+
+    if ($@) {
       $ex = $@;
     }
 
@@ -1343,7 +1436,7 @@ sub list_fails_enoent_glob {
   unlink($log_file);
 }
 
-sub list_fails_eperm {
+sub list_fails_login_required {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1353,50 +1446,11 @@ sub list_fails_eperm {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -1423,13 +1477,8 @@ sub list_fails_eperm {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      $client->login($user, $passwd);
-      $client->port();
-
-      chmod(0660, $sub_dir);
-
       my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->list($test_file) };
+      eval { ($resp_code, $resp_msg) = $client->list() };
       unless ($@) {
         die("LIST succeeded unexpectedly ($resp_code $resp_msg)");
 
@@ -1440,11 +1489,11 @@ sub list_fails_eperm {
 
       my $expected;
 
-      $expected = 450;
+      $expected = 530;
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "$test_file: Permission denied";
+      $expected = "Please login with USER and PASS";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
     };
@@ -1481,10 +1530,10 @@ sub list_fails_eperm {
   unlink($log_file);
 }
 
-sub list_bug2821 {
+sub list_fails_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-  
+
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
@@ -1512,47 +1561,18 @@ sub list_bug2821 {
     }
   }
 
-  # For this test, we need to create MANY (i.e. 110K) files in the
-  # home directory.
-  my $test_file_prefix = File::Spec->rel2abs("$tmpdir/test_");
-
-  my $count = 110000;
-  print STDOUT "# Creating $count files in $tmpdir\n";
-  for (my $i = 1; $i <= $count; $i++) {
-    my $test_file = 'test_' . sprintf("%07s", $i);
-    my $test_path = "$home_dir/$test_file";
-
-    if (open(my $fh, "> $test_path")) {
-      close($fh);
-
-    } else {
-      die("Can't open $test_path: $!");
-    }
-
-    if ($i % 10000 == 0) {
-      print STDOUT "# Created file $test_file\n";
-    }
-  }
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
-  my $timeout = 900;
+  auth_group_write($auth_group_file, 'ftpd', $uid, $user);
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    TimeoutIdle => $timeout + 15,
-    TimeoutNoTransfer => $timeout + 15,
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -1581,47 +1601,29 @@ sub list_bug2821 {
 
       $client->login($user, $passwd);
 
-      # One of the symptoms of Bug#2821 is that a LIST (or NLST) glob
-      # which matches more than 99999 files will not receive the full
-      # list.
-      #
-      # The issue turned out to be the MAX_RESULTS (now
-      # PR_TUNABLE_GLOBBING_MAX_MATCHES) limit in lib/glibc-glob.c.
-      # So now, we deliberate make sure that the number of files returned
-      # is less than the number of potential matches.
-
-      my $conn = $client->list_raw("-C test_*");
-      unless ($conn) {
-        die("LIST failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf;
-      my $tmp;
-      while ($conn->read($tmp, 32768, $timeout)) {
-        $buf .= $tmp;
-      }
-      eval { $conn->close() };
-      $client->quit();
+      my ($resp_code, $resp_msg);
+      $client->port();
 
-      my $res = {};
+      my $test_file = 'foo/bar/baz';
 
-      # Preallocate memory for the expected hash size, to help speed up the
-      # evaluation of the results.
-      keys(%$res) = $count;
+      eval { ($resp_code, $resp_msg) = $client->list($test_file) };
+      unless ($@) {
+        die("LIST succeeded unexpectedly ($resp_code $resp_msg)");
 
-      my $lines = [split(/\n/, $buf)];
-      foreach my $line (@$lines) {
-        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
-          $res->{$line} = 1;
-        }
+      } else {
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
       }
 
-      my $list_count = scalar(keys(%$res));
-      if ($list_count >= $count) {
-        die("LIST returned wrong number of entries (expected less than $count, got $list_count); check the PR_TUNABLE_GLOBBING_MAX_MATCHES value");
-      }
+      my $expected;
 
+      $expected = 450;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1632,7 +1634,7 @@ sub list_bug2821 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout * 2) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1656,7 +1658,7 @@ sub list_bug2821 {
   unlink($log_file);
 }
 
-sub list_opt_C {
+sub list_fails_enoent_glob {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1687,23 +1689,6 @@ sub list_opt_C {
     }
   }
 
-  # For this test, we need to create about 100 files in home directory.
-  my $test_file_prefix = File::Spec->rel2abs($tmpdir);
-
-  my $count = 100;
-  print STDOUT "# Creating $count files in $tmpdir\n";
-  for (my $i = 1; $i <= $count; $i++) {
-    my $test_file = sprintf("%04s", $i);
-    my $test_path = "$home_dir/$test_file";
-
-    if (open(my $fh, "> $test_path")) {
-      close($fh);
-
-    } else {
-      die("Can't open $test_path: $!");
-    }
-  }
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, 'ftpd', $gid, $user);
@@ -1743,47 +1728,17 @@ sub list_opt_C {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->port();
 
-      my $conn = $client->list_raw("-C 0*");
-      unless ($conn) {
-        die("LIST failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf;
-      $conn->read($buf, 32768, 30);
-      eval { $conn->close() };
+      my $test_glob = '*foo';
 
-      my $res = {};
-      my $lines = [split(/\n/, $buf)];
-      foreach my $line (@$lines) {
-        if ($line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/) {
-          $res->{$1} = 1;
-          $res->{$2} = 1;
-          $res->{$3} = 1;
-          $res->{$4} = 1;
-          $res->{$5} = 1;
-          $res->{$6} = 1;
-          $res->{$7} = 1;
-          $res->{$8} = 1;
-          $res->{$9} = 1;
+      my ($resp_code, $resp_msg);
+      ($resp_code, $resp_msg) = $client->list($test_glob);
 
-        } elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/) {
-          $res->{$1} = 1;
-          $res->{$2} = 1;
-          $res->{$3} = 1;
-          $res->{$4} = 1;
-          $res->{$5} = 1;
-          $res->{$6} = 1;
-          $res->{$7} = 1;
-          $res->{$8} = 1;
-        }
-      }
+      # Unlike NLST, for LIST proftpd returns 226, and simply sends an
+      # empty list.
 
-      my $list_count = scalar(keys(%$res));
-      unless ($list_count == $count) {
-        die("LIST returned wrong number of entries (expected $count, got $list_count)");
-      }
+      $self->assert_transfer_ok($resp_code, $resp_msg);
     };
 
     if ($@) {
@@ -1818,15 +1773,10 @@ sub list_opt_C {
   unlink($log_file);
 }
 
-sub list_nonascii_chars_bug3032 {
+sub list_fails_eperm {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
-  if (feature_have_feature_enabled('nls')) {
-    # This test is only valid if NLS support is not enabled.
-    return;
-  }
-
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
@@ -1842,19 +1792,10 @@ sub list_nonascii_chars_bug3032 {
   my $uid = 500;
   my $gid = 500;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/" . sprintf("test\b"));
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
@@ -1862,11 +1803,23 @@ sub list_nonascii_chars_bug3032 {
     die("Can't open $test_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    }
 
-  my $config = {
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
@@ -1901,42 +1854,29 @@ sub list_nonascii_chars_bug3032 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->port();
 
-      my $conn = $client->list_raw("-B test*");
-      unless ($conn) {
-        die("LIST failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
+      chmod(0660, $sub_dir);
 
-      my $buf;
-      $conn->read($buf, 32768, 30);
-      eval { $conn->close() };
+      my ($resp_code, $resp_msg);
+      eval { ($resp_code, $resp_msg) = $client->list($test_file) };
+      unless ($@) {
+        die("LIST succeeded unexpectedly ($resp_code $resp_msg)");
 
-      my $res = {};
-      my $lines = [split(/\n/, $buf)];
-      foreach my $line (@$lines) {
-        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
-          $res->{$1} = 1;
-        }
+      } else {
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
       }
 
-      my $expected = {
-        'test\010' => 1,
-      };
+      my $expected;
 
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
+      $expected = 450;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in LIST data")
-      }
+      $expected = "$test_file: Permission denied";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1971,10 +1911,10 @@ sub list_nonascii_chars_bug3032 {
   unlink($log_file);
 }
 
-sub list_leading_whitespace_bug3268 {
+sub list_bug2821 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
+  
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
@@ -2002,26 +1942,47 @@ sub list_leading_whitespace_bug3268 {
     }
   }
 
+  # For this test, we need to create MANY (i.e. 110K) files in the
+  # home directory.
+  my $test_file_prefix = File::Spec->rel2abs("$tmpdir/test_");
+
+  my $count = 110000;
+  print STDOUT "# Creating $count files in $tmpdir\n";
+  for (my $i = 1; $i <= $count; $i++) {
+    my $test_file = 'test_' . sprintf("%07s", $i);
+    my $test_path = "$home_dir/$test_file";
+
+    if (open(my $fh, "> $test_path")) {
+      close($fh);
+
+    } else {
+      die("Can't open $test_path: $!");
+    }
+
+    if ($i % 10000 == 0) {
+      print STDOUT "# Created file $test_file\n";
+    }
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, 'ftpd', $gid, $user);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open '$test_file': $!");
-  }
+  my $timeout = 900;
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TimeoutIdle => $timeout + 15,
+    TimeoutNoTransfer => $timeout + 15,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -2047,49 +2008,50 @@ sub list_leading_whitespace_bug3268 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
       $client->login($user, $passwd);
 
-      my $conn = $client->list_raw(' test.txt');
+      # One of the symptoms of Bug#2821 is that a LIST (or NLST) glob
+      # which matches more than 99999 files will not receive the full
+      # list.
+      #
+      # The issue turned out to be the MAX_RESULTS (now
+      # PR_TUNABLE_GLOBBING_MAX_MATCHES) limit in lib/glibc-glob.c.
+      # So now, we deliberate make sure that the number of files returned
+      # is less than the number of potential matches.
+
+      my $conn = $client->list_raw("-C test_*");
       unless ($conn) {
-        die("Failed to LIST: " . $client->response_code() . " " .
+        die("LIST failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
       my $buf;
-      $conn->read($buf, 8192, 30);
+      my $tmp;
+      while ($conn->read($tmp, 32768, $timeout)) {
+        $buf .= $tmp;
+      }
       eval { $conn->close() };
+      $client->quit();
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-
-      # We have to be careful of the fact that readdir returns directory
-      # entries in an unordered fashion.
       my $res = {};
+
+      # Preallocate memory for the expected hash size, to help speed up the
+      # evaluation of the results.
+      keys(%$res) = $count;
+
       my $lines = [split(/\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+ (.*?)$/) {
-          $res->{$1} = 1;
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$line} = 1;
         }
       }
 
-      my $expected = {
-        ' test.txt' => 1,
-      };
-
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+      my $list_count = scalar(keys(%$res));
+      if ($list_count >= $count) {
+        die("LIST returned wrong number of entries (expected less than $count, got $list_count); check the PR_TUNABLE_GLOBBING_MAX_MATCHES value");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in LIST data")
-      }
     };
 
     if ($@) {
@@ -2100,7 +2062,7 @@ sub list_leading_whitespace_bug3268 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout * 2) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2124,10 +2086,10 @@ sub list_leading_whitespace_bug3268 {
   unlink($log_file);
 }
 
-sub list_leading_whitespace_with_opts_bug3268 {
+sub list_unsorted_buffering_bug4060 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
+  
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
@@ -2139,6 +2101,7 @@ sub list_leading_whitespace_with_opts_bug3268 {
 
   my $user = 'proftpd';
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
@@ -2155,26 +2118,47 @@ sub list_leading_whitespace_with_opts_bug3268 {
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  # For this test, we need to create MANY (i.e. 110K) files in a subdirectory.
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
+  my $count = 100000;
+  print STDOUT "# Creating $count files in $test_dir\n";
+  for (my $i = 1; $i <= $count; $i++) {
+    my $test_file = 'test_' . sprintf("%07s", $i);
+    my $test_path = "$test_dir/$test_file";
 
-  } else {
-    die("Can't open '$test_file': $!");
+    if (open(my $fh, "> $test_path")) {
+      close($fh);
+
+    } else {
+      die("Can't open $test_path: $!");
+    }
+
+    if ($i % 10000 == 0) {
+      print STDOUT "# Created file $test_file\n";
+    }
   }
 
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $timeout = 900;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:0 data:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TimeoutIdle => $timeout + 15,
+    TimeoutNoTransfer => $timeout + 15,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -2202,46 +2186,44 @@ sub list_leading_whitespace_with_opts_bug3268 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $conn = $client->list_raw('-a -b  test.txt');
+      my $conn = $client->list_raw('-U test.d');
       unless ($conn) {
-        die("Failed to LIST: " . $client->response_code() . " " .
+        die("LIST failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
+      my $list_start = [gettimeofday()];
+
       my $buf;
-      $conn->read($buf, 8192, 30);
+      my $tmp;
+      while ($conn->read($tmp, 32768, $timeout)) {
+        $buf .= $tmp;
+      }
       eval { $conn->close() };
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
+      my $list_elapsed = tv_interval($list_start);
+
+      $client->quit();
 
-      # We have to be careful of the fact that readdir returns directory
-      # entries in an unordered fashion.
       my $res = {};
+
+      # Preallocate memory for the expected hash size, to help speed up the
+      # evaluation of the results.
+      keys(%$res) = $count;
+
       my $lines = [split(/\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+ (.*?)$/) {
-          $res->{$1} = 1;
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$line} = 1;
         }
       }
 
-      my $expected = {
-        ' test.txt' => 1,
-      };
-
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
+      my $list_count = scalar(keys(%$res));
+      $self->assert($list_count == $count,
+        test_msg("LIST returned wrong number of entries (expected $count, got $list_count)"));
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in LIST data")
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# LIST elapsed time: $list_elapsed\n";
       }
     };
 
@@ -2253,7 +2235,7 @@ sub list_leading_whitespace_with_opts_bug3268 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout * 2) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2277,7 +2259,7 @@ sub list_leading_whitespace_with_opts_bug3268 {
   unlink($log_file);
 }
 
-sub list_leading_whitespace_with_strict_opts_bug3268 {
+sub list_opt_C {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2308,18 +2290,27 @@ sub list_leading_whitespace_with_strict_opts_bug3268 {
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  # For this test, we need to create about 100 files in home directory.
+  my $test_file_prefix = File::Spec->rel2abs($tmpdir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
+  my $count = 100;
+  print STDOUT "# Creating $count files in $tmpdir\n";
+  for (my $i = 1; $i <= $count; $i++) {
+    my $test_file = sprintf("%04s", $i);
+    my $test_path = "$home_dir/$test_file";
 
-  } else {
-    die("Can't open '$test_file': $!");
+    if (open(my $fh, "> $test_path")) {
+      close($fh);
+
+    } else {
+      die("Can't open $test_path: $!");
+    }
   }
 
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -2328,8 +2319,6 @@ sub list_leading_whitespace_with_strict_opts_bug3268 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    ListOptions => '"-a +R" strict',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -2355,9 +2344,1006 @@ sub list_leading_whitespace_with_strict_opts_bug3268 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      $client->login($user, $passwd);
+
+      my $conn = $client->list_raw("-C 0*");
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 32768, 30);
+      eval { $conn->close() };
+
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/) {
+          $res->{$1} = 1;
+          $res->{$2} = 1;
+          $res->{$3} = 1;
+          $res->{$4} = 1;
+          $res->{$5} = 1;
+          $res->{$6} = 1;
+          $res->{$7} = 1;
+          $res->{$8} = 1;
+          $res->{$9} = 1;
+
+        } elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/) {
+          $res->{$1} = 1;
+          $res->{$2} = 1;
+          $res->{$3} = 1;
+          $res->{$4} = 1;
+          $res->{$5} = 1;
+          $res->{$6} = 1;
+          $res->{$7} = 1;
+          $res->{$8} = 1;
+        }
+      }
+
+      my $list_count = scalar(keys(%$res));
+      unless ($list_count == $count) {
+        die("LIST returned wrong number of entries (expected $count, got $list_count)");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub list_nonascii_chars_bug3032 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  if (feature_have_feature_enabled('nls')) {
+    # This test is only valid if NLS support is not enabled.
+    return;
+  }
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/" . sprintf("test\b"));
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->list_raw("-B test*");
+      unless ($conn) {
+        die("LIST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 32768, 30);
+      eval { $conn->close() };
+
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
+          $res->{$1} = 1;
+        }
+      }
+
+      my $expected = {
+        'test\010' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in LIST data")
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub list_leading_whitespace_bug3268 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open '$test_file': $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->list_raw(' test.txt');
+      unless ($conn) {
+        die("Failed to LIST: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+ (.*?)$/) {
+          $res->{$1} = 1;
+        }
+      }
+
+      my $expected = {
+        ' test.txt' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in LIST data")
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub list_leading_whitespace_with_opts_bug3268 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open '$test_file': $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->list_raw('-a -b  test.txt');
+      unless ($conn) {
+        die("Failed to LIST: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+ (.*?)$/) {
+          $res->{$1} = 1;
+        }
+      }
+
+      my $expected = {
+        ' test.txt' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in LIST data")
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub list_leading_whitespace_with_strict_opts_bug3268 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open '$test_file': $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    ListOptions => '"-a +R" strict',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->list_raw('-a  -b  test.txt');
+      unless ($conn) {
+        die("Failed to LIST: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+ (.*?)$/) {
+          $res->{$1} = 1;
+        }
+      }
+
+      my $expected = {
+        ' test.txt' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in LIST data")
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub list_symlink_bug3254 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
+  my $baz_dir = File::Spec->rel2abs("$tmpdir/bar/baz");
+  my $quxx_dir = File::Spec->rel2abs("$tmpdir/bar/quxx");
+  mkpath([$foo_dir, $bar_dir, $baz_dir, $quxx_dir]);
+
+  # Change to the 'foo' directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir("$foo_dir")) {
+    die("Can't chdir to $foo_dir: $!");
+  }
+
+  unless (symlink('../bar/baz', 'baz')) {
+    die("Can't symlink '../bar/baz' to 'baz': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $ftpaccess_file = File::Spec->rel2abs("$foo_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser $user
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  $ftpaccess_file = File::Spec->rel2abs("$bar_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser bar
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  $ftpaccess_file = File::Spec->rel2abs("$baz_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser bar,$user
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  $ftpaccess_file = File::Spec->rel2abs("$quxx_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser bar,$user,
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
+      die("Can't set perms on dirs to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
+      die("Can't set owner of dirs to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowOverride => 'on',
+    DefaultRoot => '~',
+    ShowSymlinks => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      $client->cwd("foo/baz");
+      my $conn = $client->list_raw();
+      unless ($conn) {
+        die("Failed to LIST: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quote('CWD', '..');
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub list_symlink_rel_with_double_slash_bug3719 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
+  my $baz_dir = File::Spec->rel2abs("$tmpdir/bar/baz");
+  mkpath([$foo_dir, $bar_dir, $baz_dir]);
+
+  # Change to the 'foo' directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir("$foo_dir")) {
+    die("Can't chdir to $foo_dir: $!");
+  }
+
+  unless (symlink('../bar//baz', 'baz')) {
+    die("Can't symlink '../bar//baz' to 'baz': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set perms on dirs to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set owner of dirs to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
 
-      my $conn = $client->list_raw('-a  -b  test.txt');
+      $client->cwd("foo");
+      my $conn = $client->list_raw();
       unless ($conn) {
         die("Failed to LIST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -2371,18 +3357,20 @@ sub list_leading_whitespace_with_strict_opts_bug3268 {
       my $resp_msg = $client->response_msg();
       $self->assert_transfer_ok($resp_code, $resp_msg);
 
+      $client->quit();
+
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
       my $lines = [split(/\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+ (.*?)$/) {
+        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
           $res->{$1} = 1;
         }
       }
 
       my $expected = {
-        ' test.txt' => 1,
+        '../bar//baz' => 1,
       };
 
       my $ok = 1;
@@ -2432,36 +3420,21 @@ sub list_leading_whitespace_with_strict_opts_bug3268 {
   unlink($log_file);
 }
 
-sub list_symlink_bug3254 {
+sub list_showsymlinks_off {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
   my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
   my $baz_dir = File::Spec->rel2abs("$tmpdir/bar/baz");
-  my $quxx_dir = File::Spec->rel2abs("$tmpdir/bar/quxx");
-  mkpath([$foo_dir, $bar_dir, $baz_dir, $quxx_dir]);
+  mkpath([$foo_dir, $bar_dir, $baz_dir]);
 
   # Change to the 'foo' directory in order to create a relative path in the
   # symlink we need
 
   my $cwd = getcwd();
-  unless (chdir("$foo_dir")) {
+  unless (chdir($foo_dir)) {
     die("Can't chdir to $foo_dir: $!");
   }
 
@@ -2473,95 +3446,160 @@ sub list_symlink_bug3254 {
     die("Can't chdir to $cwd: $!");
   }
 
-  my $ftpaccess_file = File::Spec->rel2abs("$foo_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser $user
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
+  # Make sure that, if we're running as root, that the directories have
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set perms on dirs to 0755: $!");
     }
 
-  } else {
-    die("Can't open $ftpaccess_file: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set owner of dirs to $setup->{uid}/$setup->{gid}: $!");
+    }
   }
 
-  $ftpaccess_file = File::Spec->rel2abs("$bar_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser bar
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  } else {
-    die("Can't open $ftpaccess_file: $!");
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    ShowSymlinks => 'off',
+
+    IfModules => {
+      '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: $!");
   }
 
-  $ftpaccess_file = File::Spec->rel2abs("$baz_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser bar,$user
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      $client->cwd("foo");
+      my $conn = $client->list_raw('-l');
+      unless ($conn) {
+        die("Failed to LIST: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      chomp($buf);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $expected = '^d\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+\s+(.*?)$';
+      $self->assert(qr/$expected/, $buf,
+        test_msg("Expected '$expected', got '$buf'"));
+
+      $buf =~ /$expected/;
+      my $finfo = $1;
+
+      $expected = 'baz';
+      $self->assert($expected eq $finfo,
+        test_msg("Expected '$expected', got '$finfo'"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
+    $wfh->print("done\n");
+    $wfh->flush();
+
   } else {
-    die("Can't open $ftpaccess_file: $!");
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
   }
 
-  $ftpaccess_file = File::Spec->rel2abs("$quxx_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser bar,$user,
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
 
-  } else {
-    die("Can't open $ftpaccess_file: $!");
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub list_showsymlinks_off_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
+  my $baz_dir = File::Spec->rel2abs("$tmpdir/bar/baz");
+  mkpath([$foo_dir, $bar_dir, $baz_dir]);
+
+  # Change to the 'foo' directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($foo_dir)) {
+    die("Can't chdir to $foo_dir: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
+  unless (symlink('../bar/baz', 'baz')) {
+    die("Can't symlink '../bar/baz' to 'baz': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  # Make sure that, if we're running as root, that the directories have
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
+    unless (chmod(0755, $foo_dir, $bar_dir, $baz_dir)) {
       die("Can't set perms on dirs to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
-      die("Can't set owner of dirs to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set owner of dirs to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AllowOverride => 'on',
     DefaultRoot => '~',
     ShowSymlinks => 'off',
 
@@ -2572,7 +3610,8 @@ EOL
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -2589,12 +3628,11 @@ EOL
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      $client->login($user, $passwd);
-
-      $client->cwd("foo/baz");
-      my $conn = $client->list_raw();
+      $client->cwd("foo");
+      my $conn = $client->list_raw('-l');
       unless ($conn) {
         die("Failed to LIST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -2606,12 +3644,27 @@ EOL
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
+      $client->quit();
+
       $self->assert_transfer_ok($resp_code, $resp_msg);
 
-      $client->quote('CWD', '..');
-      $client->quit();
-    };
+      chomp($buf);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $expected = '^d\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+\s+(.*?)$';
+      $self->assert(qr/$expected/, $buf,
+        test_msg("Expected '$expected', got '$buf'"));
+
+      $buf =~ /$expected/;
+      my $finfo = $1;
 
+      $expected = 'baz';
+      $self->assert($expected eq $finfo,
+        test_msg("Expected '$expected', got '$finfo'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -2620,7 +3673,7 @@ EOL
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2629,40 +3682,17 @@ EOL
     exit 0;
   }
 
-  # Stop server
-  server_stop($pid_file);
-
-  $self->assert_child_ok($pid);
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
-}
-
-sub list_symlink_rel_with_double_slash_bug3719 {
-  my $self = shift;
-  my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+sub list_showsymlinks_on {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
   my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
@@ -2673,43 +3703,39 @@ sub list_symlink_rel_with_double_slash_bug3719 {
   # symlink we need
 
   my $cwd = getcwd();
-  unless (chdir("$foo_dir")) {
+  unless (chdir($foo_dir)) {
     die("Can't chdir to $foo_dir: $!");
   }
 
-  unless (symlink('../bar//baz', 'baz')) {
-    die("Can't symlink '../bar//baz' to 'baz': $!");
+  unless (symlink('../bar/baz', 'baz')) {
+    die("Can't symlink '../bar/baz' to 'baz': $!");
   }
 
   unless (chdir($cwd)) {
     die("Can't chdir to $cwd: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
+  # Make sure that, if we're running as root, that the directories have
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir)) {
+    unless (chmod(0755, $foo_dir, $bar_dir, $baz_dir)) {
       die("Can't set perms on dirs to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir)) {
-      die("Can't set owner of dirs to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set owner of dirs to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    DefaultRoot => '~',
+    ShowSymlinks => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -2718,7 +3744,8 @@ sub list_symlink_rel_with_double_slash_bug3719 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -2735,12 +3762,11 @@ sub list_symlink_rel_with_double_slash_bug3719 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       $client->cwd("foo");
-      my $conn = $client->list_raw();
+      my $conn = $client->list_raw('-l');
       unless ($conn) {
         die("Failed to LIST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -2752,39 +3778,27 @@ sub list_symlink_rel_with_double_slash_bug3719 {
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
+      $client->quit();
+
       $self->assert_transfer_ok($resp_code, $resp_msg);
 
-      $client->quit();
+      chomp($buf);
 
-      # We have to be careful of the fact that readdir returns directory
-      # entries in an unordered fashion.
-      my $res = {};
-      my $lines = [split(/\n/, $buf)];
-      foreach my $line (@$lines) {
-        if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
-          $res->{$1} = 1;
-        }
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
       }
 
-      my $expected = {
-        '../bar//baz' => 1,
-      };
+      my $expected = '^l\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+\s+(.*?)$';
+      $self->assert(qr/$expected/, $buf,
+        test_msg("Expected '$expected', got '$buf'"));
 
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
+      $buf =~ /$expected/;
+      my $finfo = $1;
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in LIST data")
-      }
+      $expected = 'baz -> ../bar/baz';
+      $self->assert($expected eq $finfo,
+        test_msg("Expected '$expected', got '$finfo'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -2793,7 +3807,7 @@ sub list_symlink_rel_with_double_slash_bug3719 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2803,38 +3817,16 @@ sub list_symlink_rel_with_double_slash_bug3719 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub list_showsymlinks_on {
+sub list_showsymlinks_on_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
   my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
@@ -2845,7 +3837,7 @@ sub list_showsymlinks_on {
   # symlink we need
 
   my $cwd = getcwd();
-  unless (chdir("$foo_dir")) {
+  unless (chdir($foo_dir)) {
     die("Can't chdir to $foo_dir: $!");
   }
 
@@ -2857,29 +3849,27 @@ sub list_showsymlinks_on {
     die("Can't chdir to $cwd: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
+  # Make sure that, if we're running as root, that the directories have
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir)) {
+    unless (chmod(0755, $foo_dir, $bar_dir, $baz_dir)) {
       die("Can't set perms on dirs to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir)) {
-      die("Can't set owner of dirs to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir, $bar_dir, $baz_dir)) {
+      die("Can't set owner of dirs to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
     ShowSymlinks => 'on',
 
     IfModules => {
@@ -2889,7 +3879,8 @@ sub list_showsymlinks_on {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -2906,9 +3897,8 @@ sub list_showsymlinks_on {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       $client->cwd("foo");
       my $conn = $client->list_raw('-l');
@@ -2923,10 +3913,16 @@ sub list_showsymlinks_on {
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
+      $client->quit();
+
       $self->assert_transfer_ok($resp_code, $resp_msg);
 
       chomp($buf);
 
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
       my $expected = '^l\S+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+\S+\s+(.*?)$';
       $self->assert(qr/$expected/, $buf,
         test_msg("Expected '$expected', got '$buf'"));
@@ -2934,13 +3930,10 @@ sub list_showsymlinks_on {
       $buf =~ /$expected/;
       my $finfo = $1;
 
-      $expected = 'baz -> ../bar/baz';
+      $expected = 'baz -> /bar/baz';
       $self->assert($expected eq $finfo,
         test_msg("Expected '$expected', got '$finfo'"));
-
-      $client->quit();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -2949,7 +3942,7 @@ sub list_showsymlinks_on {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2959,18 +3952,10 @@ sub list_showsymlinks_on {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub list_glob_bug2367 {
@@ -5089,4 +6074,135 @@ sub list_opt_R_rel_symlinked_file_bug3719 {
   unlink($log_file);
 }
 
+sub list_option_parsing {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      for (my $i = 0; $i < 5; $i++) {
+        my $conn = $client->list_raw("-a ");
+        unless ($conn) {
+          die("LIST failed: " . $client->response_code() . " " .
+            $client->response_msg());
+        }
+
+        my $buf;
+        my $tmp;
+
+        my $res = $conn->read($tmp, 32768, 25);
+        while ($res) {
+          $buf .= $tmp;
+          $tmp = undef;
+
+          $res = $conn->read($tmp, 32768, 25);
+        }
+
+        eval { $conn->close() };
+
+        my $resp_code = $client->response_code();
+        my $resp_msg = $client->response_msg();
+        $self->assert_transfer_ok($resp_code, $resp_msg);
+      }
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm b/tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm
index 4cb47d1..a36dcfb 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/MDTM.pm
@@ -4,6 +4,8 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
+use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
 
@@ -20,11 +22,51 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  mdtm_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mdtm_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  mdtm_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mdtm_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   mdtm_fails_enoent => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  mdtm_fails_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mdtm_fails_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  mdtm_fails_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mdtm_fails_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   mdtm_fails_eperm => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -43,23 +85,95 @@ sub list_tests {
 sub mdtm_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  } else {
+    die("Can't create $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $log_file = test_get_logfile();
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  # 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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mdtm($test_file);
+      $client->quit();
+
+      my $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $self->assert(qr/\d+/, $resp_msg);
+    };
+
+    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 mdtm_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
@@ -67,29 +181,25 @@ sub mdtm_ok {
     die("Can't create $test_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $dst_path = $test_symlink;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (symlink($test_file, $dst_path)) {
+    die("Can't symlink '$dst_path' to '$test_file': $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -98,7 +208,8 @@ sub mdtm_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -115,17 +226,15 @@ sub mdtm_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mdtm($test_file);
+      my ($resp_code, $resp_msg) = $client->mdtm('test.d/test.lnk');
+      $client->quit();
 
-      my $expected;
-
-      $expected = 213;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $self->assert(qr/\d+/, $resp_msg);
     };
@@ -138,7 +247,7 @@ sub mdtm_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -148,64 +257,50 @@ sub mdtm_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mdtm_fails_enoent {
+sub mdtm_abs_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  } else {
+    die("Can't create $test_file: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -214,7 +309,8 @@ sub mdtm_fails_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -231,28 +327,17 @@ sub mdtm_fails_enoent {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mdtm($test_file) };
-      unless ($@) {
-        die("MDTM succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
-      }
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->mdtm('test.d/test.lnk');
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$test_file: No such file or directory";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $self->assert(qr/\d+/, $resp_msg);
     };
 
     if ($@) {
@@ -263,7 +348,7 @@ sub mdtm_fails_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -273,40 +358,21 @@ sub mdtm_fails_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mdtm_fails_eperm {
+sub mdtm_rel_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
@@ -314,29 +380,29 @@ sub mdtm_fails_eperm {
     die("Can't create $test_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -345,7 +411,8 @@ sub mdtm_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -362,36 +429,121 @@ sub mdtm_fails_eperm {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      # Make it such that perms on the home do to not allow reads
-      my $perms = (stat($home_dir))[2];
-      unless (chmod(0220, $home_dir)) {
-        die("Failed to change perms on $home_dir: $!");
-      }
+      my ($resp_code, $resp_msg) = $client->mdtm('test.d/test.lnk');
+      $client->quit();
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mdtm($test_file) };
-      unless ($@) {
-        die("MDTM succeeded unexpectedly");
+      my $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
-      }
+      $self->assert(qr/\d+/, $resp_msg);
+    };
+
+    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 mdtm_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't create $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-      chmod($perms, $home_dir);
+  # 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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->mdtm('test.d/test.lnk');
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$test_file: Permission denied";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $self->assert(qr/\d+/, $resp_msg);
     };
 
     if ($@) {
@@ -402,7 +554,7 @@ sub mdtm_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -412,18 +564,600 @@ sub mdtm_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub mdtm_fails_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-    die($ex);
+  # 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: $!");
   }
 
-  unlink($log_file);
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->mdtm($test_file) };
+      unless ($@) {
+        die("MDTM succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 mdtm_fails_abs_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_symlink;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($test_file, $dst_path)) {
+    die("Can't symlink '$dst_path' to '$test_file': $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->mdtm('test.d/test.lnk') };
+      unless ($@) {
+        die("MDTM succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'test.d/test.lnk: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 mdtm_fails_abs_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->mdtm('test.d/test.lnk') };
+      unless ($@) {
+        die("MDTM succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'test.d/test.lnk: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 mdtm_fails_rel_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->mdtm('test.d/test.lnk') };
+      unless ($@) {
+        die("MDTM succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'test.d/test.lnk: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mdtm_fails_rel_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->mdtm('test.d/test.lnk') };
+      unless ($@) {
+        die("MDTM succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'test.d/test.lnk: No such file or directory';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mdtm_fails_eperm {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't create $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # Make it such that perms on the home do to not allow reads
+      my $perms = (stat($setup->{home_dir}))[2];
+      unless (chmod(0220, $setup->{home_dir})) {
+        die("Failed to change perms on $setup->{home_dir}: $!");
+      }
+
+      eval { $client->mdtm($test_file) };
+      unless ($@) {
+        die("MDTM succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      chmod($perms, $setup->{home_dir});
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: Permission denied";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/MKD.pm b/tests/t/lib/ProFTPD/Tests/Commands/MKD.pm
index 868dc6f..ed3232a 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/MKD.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/MKD.pm
@@ -4,10 +4,12 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use Encode;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
+use IO::Socket::INET;
 
 use ProFTPD::TestSuite::FTP;
 use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
@@ -22,6 +24,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  mkd_fails_abs_symlink_new => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mkd_fails_abs_symlink_new_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  mkd_fails_rel_symlink_new => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mkd_fails_rel_symlink_new_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   mkd_umask_one_param_ok => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -72,6 +94,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  mkd_fails_eexists => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   xmkd_ok => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -82,6 +109,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  mkd_embedded_cr_bug4167 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  mkd_embedded_lf_bug4167 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -95,48 +132,17 @@ sub list_tests {
 sub mkd_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -145,7 +151,8 @@ sub mkd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -162,21 +169,19 @@ sub mkd_ok {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "\"$sub_dir\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $self->assert(-d $sub_dir,
         test_msg("$sub_dir directory does not exist as expected"));
@@ -186,7 +191,6 @@ sub mkd_ok {
       $self->assert($expected == $perms,
         test_msg("Expected perms $expected, got $perms"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -195,7 +199,7 @@ sub mkd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -205,67 +209,39 @@ sub mkd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_umask_one_param_ok {
+sub mkd_fails_abs_symlink_new {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo.d");
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-related hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
   }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
-    Umask => '077',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -274,7 +250,8 @@ sub mkd_umask_one_param_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -291,31 +268,34 @@ sub mkd_umask_one_param_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      eval { $client->mkd('test.lnk') };
+      unless ($@) {
+        die("MKD succeeded unexpectedly");
+      }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"$sub_dir\" - Directory successfully created";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_symlink = '/private' . $test_symlink;
+      }
 
-      $self->assert(-d $sub_dir,
-        test_msg("$sub_dir directory does not exist as expected"));
+      $expected = "test.lnk: File exists";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      my $perms = ((stat($sub_dir))[2] & 07777);
-      $expected = 0700;
-      $self->assert($expected == $perms,
-        test_msg("Expected perms $expected, got $perms"));
+      $self->assert(!-d $test_dir,
+        test_msg("$test_dir directory does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -324,7 +304,7 @@ sub mkd_umask_one_param_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -334,67 +314,41 @@ sub mkd_umask_one_param_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_umask_two_params_ok {
+sub mkd_fails_abs_symlink_new_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo.d");
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-related hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
   }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    Umask => '022 077',
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -403,7 +357,8 @@ sub mkd_umask_two_params_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -420,31 +375,34 @@ sub mkd_umask_two_params_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      eval { $client->mkd('test.lnk') };
+      unless ($@) {
+        die("MKD succeeded unexpectedly");
+      }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"$sub_dir\" - Directory successfully created";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_symlink = '/private' . $test_symlink;
+      }
 
-      $self->assert(-d $sub_dir,
-        test_msg("$sub_dir directory does not exist as expected"));
+      $expected = "test.lnk: File exists";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      my $perms = ((stat($sub_dir))[2] & 07777);
-      $expected = 0700;
-      $self->assert($expected == $perms,
-        test_msg("Expected perms $expected, got $perms"));
+      $self->assert(!-d $test_dir,
+        test_msg("$test_dir directory does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -453,7 +411,7 @@ sub mkd_umask_two_params_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -463,65 +421,45 @@ sub mkd_umask_two_params_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_with_spaces_ok {
+sub mkd_fails_rel_symlink_new {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo bar");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -530,7 +468,8 @@ sub mkd_with_spaces_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -547,26 +486,26 @@ sub mkd_with_spaces_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      eval { $client->mkd('test.d/test.lnk') };
+      unless ($@) {
+        die("MKD succeeded unexpectedly");
+      }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"$sub_dir\" - Directory successfully created";
+      $expected = "test.d\/test.lnk: File exists";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      $self->assert(-d $sub_dir,
-        test_msg("$sub_dir directory does not exist as expected"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -575,7 +514,7 @@ sub mkd_with_spaces_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -585,84 +524,57 @@ sub mkd_with_spaces_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_utf8_with_spaces_ok {
+sub mkd_fails_rel_symlink_new_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $sub_dir = 'olá çim poi...!#$%#%$>= ü';
-  my $utf8_dir = encode_utf8($sub_dir);
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
 
-  my $sub_path = File::Spec->rel2abs("$tmpdir/$sub_dir");
-  my $utf8_path = encode_utf8($sub_path);
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'encode:10',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
-
-      'mod_lang.c' => {
-        UseEncoding => 'on',
-      },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -679,26 +591,26 @@ sub mkd_utf8_with_spaces_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($utf8_dir);
+      eval { $client->mkd('test.d/test.lnk') };
+      unless ($@) {
+        die("MKD succeeded unexpectedly");
+      }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"$utf8_path\" - Directory successfully created";
+      $expected = "test.d\/test.lnk: File exists";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      $self->assert(-d $utf8_path,
-        test_msg("$utf8_path directory does not exist as expected"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -707,7 +619,7 @@ sub mkd_utf8_with_spaces_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -717,66 +629,28 @@ sub mkd_utf8_with_spaces_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_chrooted_ok {
+sub mkd_umask_one_param_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    Umask => '077',
 
     IfModules => {
       'mod_delay.c' => {
@@ -785,7 +659,8 @@ sub mkd_chrooted_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -802,30 +677,28 @@ sub mkd_chrooted_ok {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg) = $client->mkd('foo');
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"/foo\" - Directory successfully created";
+      $expected = "\"$sub_dir\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $self->assert(-d $sub_dir,
         test_msg("$sub_dir directory does not exist as expected"));
 
       my $perms = ((stat($sub_dir))[2] & 07777);
-      $expected = 0755;
+      $expected = 0700;
       $self->assert($expected == $perms,
         test_msg("Expected perms $expected, got $perms"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -834,7 +707,7 @@ sub mkd_chrooted_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -844,69 +717,28 @@ sub mkd_chrooted_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_chrooted_with_cwd_ok {
+sub mkd_umask_two_params_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
-  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
  
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    Umask => '022 077',
 
     IfModules => {
       'mod_delay.c' => {
@@ -915,7 +747,8 @@ sub mkd_chrooted_with_cwd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -932,31 +765,28 @@ sub mkd_chrooted_with_cwd_ok {
   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($user, $passwd);
-      $client->cwd('foo');
-
-      my ($resp_code, $resp_msg) = $client->xmkd('bar');
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"/foo/bar\" - Directory successfully created";
+      $expected = "\"$sub_dir\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      $self->assert(-d $test_dir,
-        test_msg("$test_dir directory does not exist as expected"));
+      $self->assert(-d $sub_dir,
+        test_msg("$sub_dir directory does not exist as expected"));
 
       my $perms = ((stat($sub_dir))[2] & 07777);
-      $expected = 0755;
+      $expected = 0700;
       $self->assert($expected == $perms,
         test_msg("Expected perms $expected, got $perms"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -965,7 +795,7 @@ sub mkd_chrooted_with_cwd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -975,76 +805,26 @@ sub mkd_chrooted_with_cwd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_sgid_umask_one_param_ok {
+sub mkd_with_spaces_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
-  mkpath($sub_dir);
-
-  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo bar");
  
-  # Make sure that our sub directory has the SGID bit set
-  my $mode = oct(2755);
-  unless (chmod($mode, $sub_dir)) {
-    die("Can't set perms on $sub_dir: $!"); 
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
-    Umask => '077',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1053,7 +833,8 @@ sub mkd_sgid_umask_one_param_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1070,31 +851,603 @@ sub mkd_sgid_umask_one_param_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($test_dir);
+      my ($resp_code, $resp_msg) = $client->mkd($sub_dir);
+      $client->quit();
 
-      my $expected;
-
-      $expected = 257;
+      my $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\"$test_dir\" - Directory successfully created";
+      $expected = "\"$sub_dir\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      $self->assert(-d $test_dir,
-        test_msg("$test_dir directory does not exist as expected"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $sub_dir,
+        test_msg("$sub_dir directory does not exist as expected"));
+    };
+    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 mkd_utf8_with_spaces_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = 'olá çim poi...!#$%#%$>= ü';
+  my $utf8_dir = encode_utf8($sub_dir);
+
+  my $sub_path = File::Spec->rel2abs("$tmpdir/$sub_dir");
+  my $utf8_path = encode_utf8($sub_path);
+ 
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'encode:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_lang.c' => {
+        UseEncoding => 'on',
+      },
+    },
+  };
+
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mkd($utf8_dir);
+      $client->quit();
+
+      my $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\"$utf8_path\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $utf8_path,
+        test_msg("$utf8_path directory does not exist as expected"));
+    };
+    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 mkd_chrooted_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+ 
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mkd('foo');
+      $client->quit();
+
+      my $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\"/foo\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $sub_dir,
+        test_msg("$sub_dir directory does not exist as expected"));
+
+      my $perms = ((stat($sub_dir))[2] & 07777);
+      $expected = 0755;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms $expected, got $perms"));
+    };
+    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 mkd_chrooted_with_cwd_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar");
+ 
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+ 
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->cwd('foo');
+
+      my ($resp_code, $resp_msg) = $client->xmkd('bar');
+      $client->quit();
+
+      my $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\"/foo/bar\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $test_dir,
+        test_msg("$test_dir directory does not exist as expected"));
+
+      my $perms = ((stat($sub_dir))[2] & 07777);
+      $expected = 0755;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms $expected, got $perms"));
+    };
+    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 mkd_sgid_umask_one_param_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+ 
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+ 
+  # Make sure that our sub directory has the SGID bit set
+  my $mode = oct(2755);
+  unless (chmod($mode, $sub_dir)) {
+    die("Can't set perms on $sub_dir: $!"); 
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    Umask => '077',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mkd($test_dir);
+      $client->quit();
+
+      my $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\"$test_dir\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $test_dir,
+        test_msg("$test_dir directory does not exist as expected"));
 
       my $perms = ((stat($test_dir))[2] & 07777);
       $expected = oct(2700);
       $self->assert($expected == $perms,
         test_msg("Expected perms $expected, got $perms"));
     };
+    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 mkd_sgid_umask_two_params_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+ 
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+ 
+  # Make sure that our sub directory has the SGID bit set
+  my $mode = oct(2755);
+  unless (chmod($mode, $sub_dir)) {
+    die("Can't set perms on $sub_dir: $!"); 
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    Umask => '022 077',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mkd($test_dir);
+      $client->quit();
+
+      my $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\"$test_dir\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $test_dir,
+        test_msg("$test_dir directory does not exist as expected"));
+
+      my $perms = ((stat($test_dir))[2] & 07777);
+      $expected = oct(2700);
+      $self->assert($expected == $perms,
+        test_msg("Expected perms $expected, got $perms"));
+    };
+    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 mkd_fails_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->mkd($sub_dir) };
+      unless ($@) {
+        die("MKD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
+      $expected = "$sub_dir: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(!-d $sub_dir,
+        test_msg("$sub_dir directory exists unexpectedly"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -1103,7 +1456,7 @@ sub mkd_sgid_umask_one_param_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1113,76 +1466,26 @@ sub mkd_sgid_umask_one_param_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_sgid_umask_two_params_ok {
+sub mkd_fails_eperm {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
-  mkpath($sub_dir);
-
-  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
- 
-  # Make sure that our sub directory has the SGID bit set
-  my $mode = oct(2755);
-  unless (chmod($mode, $sub_dir)) {
-    die("Can't set perms on $sub_dir: $!"); 
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
-    Umask => '022 077',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1191,7 +1494,8 @@ sub mkd_sgid_umask_two_params_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1208,31 +1512,38 @@ sub mkd_sgid_umask_two_params_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($test_dir);
+      # Make it such that perms on the parent dir do not allow writes
+      my $perms = (stat($setup->{home_dir}))[2];
+      unless (chmod(0550, $setup->{home_dir})) {
+        die("Failed to change perms on $setup->{home_dir}: $!");
+      }
 
-      my $expected;
+      eval { $client->mkd($sub_dir) };
+      unless ($@) {
+        die("MKD succeeded unexpectedly");
+      }
 
-      $expected = 257;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = "\"$test_dir\" - Directory successfully created";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      # Restore the perms
+      chmod($perms, $setup->{home_dir});
 
-      $self->assert(-d $test_dir,
-        test_msg("$test_dir directory does not exist as expected"));
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      my $perms = ((stat($test_dir))[2] & 07777);
-      $expected = oct(2700);
-      $self->assert($expected == $perms,
-        test_msg("Expected perms $expected, got $perms"));
-    };
+      $expected = "\/foo: (Operation not permitted|Permission denied)";
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $self->assert(!-d $sub_dir,
+        test_msg("$sub_dir directory exists unexpectedly"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -1241,7 +1552,7 @@ sub mkd_sgid_umask_two_params_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1251,65 +1562,27 @@ sub mkd_sgid_umask_two_params_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_fails_enoent {
+sub mkd_fails_eexists {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar");
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1318,7 +1591,8 @@ sub mkd_fails_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1335,33 +1609,26 @@ sub mkd_fails_enoent {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mkd($sub_dir) };
+      eval { $client->mkd($sub_dir) };
       unless ($@) {
         die("MKD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      $expected = "$sub_dir: No such file or directory";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $self->assert(!-d $sub_dir,
-        test_msg("$sub_dir directory exists unexpectedly"));
+      $expected = "\/foo: File exists";
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1370,7 +1637,7 @@ sub mkd_fails_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1380,65 +1647,26 @@ sub mkd_fails_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_fails_eperm {
+sub xmkd_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1447,7 +1675,8 @@ sub mkd_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1464,42 +1693,23 @@ sub mkd_fails_eperm {
   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($user, $passwd);
-
-      # Make it such that perms on the parent dir do not allow writes
-      my $perms = (stat($home_dir))[2];
-      unless (chmod(0550, $home_dir)) {
-        die("Failed to change perms on $home_dir: $!");
-      }
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mkd($sub_dir) };
-      unless ($@) {
-        die("MKD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
-      }
-
-      # Restore the perms
-      chmod($perms, $home_dir);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->xmkd($sub_dir);;
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\/foo: (Operation not permitted|Permission denied)";
-      $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $expected = "\"$sub_dir\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      $self->assert(!-d $sub_dir,
-        test_msg("$sub_dir directory exists unexpectedly"));
+      $self->assert(-d $sub_dir,
+        test_msg("$sub_dir directory does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1508,7 +1718,7 @@ sub mkd_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1518,65 +1728,114 @@ sub mkd_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub xmkd_ok {
+sub mkd_digits_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/001");
+ 
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $log_file = test_get_logfile();
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  # 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: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $dir = "001";
+
+      my ($resp_code, $resp_msg) = $client->mkd($dir);
+      $client->quit();
+
+      my $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # MacOSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
+
+      $expected = "\"$sub_dir\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(-d $sub_dir,
+        test_msg("$sub_dir directory does not exist as expected"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub mkd_embedded_cr_bug4167 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/ab\015cd");
+ 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1585,7 +1844,8 @@ sub xmkd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1602,26 +1862,30 @@ sub xmkd_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->xmkd($sub_dir);;
+      my $dir = "ab\015\00cd";
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->mkd($dir);
+      $client->quit();
 
-      $expected = 257;
+      my $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # MacOSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $self->assert(-d $sub_dir,
         test_msg("$sub_dir directory does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1630,7 +1894,7 @@ sub xmkd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1640,65 +1904,28 @@ sub xmkd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub mkd_digits_ok {
+sub mkd_embedded_lf_bug4167 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/001");
- 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/ab\012cd");
  
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1707,7 +1934,8 @@ sub mkd_digits_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1724,33 +1952,109 @@ sub mkd_digits_ok {
   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($user, $passwd);
+      # Note: the Net::Cmd module's command() method automatically translates
+      # all LF to spaces!  Crap!  That is why we can't Net::FTP (it's based on
+      # Net::Cmd), and instead have to do the socket IO manually.  Sigh.
+
+      sleep(2);
+
+      my $client = IO::Socket::INET->new(
+        PeerAddr => '127.0.0.1',
+        PeerPort => $port,
+        Proto => 'tcp',
+        Timeout => 5
+      );
+      unless ($client) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
 
-      my $dir = "001";
+      # Read the banner
+      my $banner = <$client>;
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mkd($dir);
+      # Send the USER command
+      my $cmd = "USER $setup->{user}\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sending: $cmd";
+      }
+      $client->print($cmd);
+      $client->flush();
 
-      my $expected;
+      # Read USER response
+      my $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Received: $resp";
+      }
 
-      $expected = 257;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      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}\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sending: $cmd";
+      }
+      $client->print($cmd);
+      $client->flush();
+
+      # Read PASS response
+      $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Received: $resp";
+      }
+
+      $expected = "230 User $setup->{user} logged in\r\n";
+      $self->assert($expected eq $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
+
+      my $dir = "ab\012cd";
+
+      # Send the MKD command
+      $cmd = "MKD $dir\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sending: $cmd";
+      }
+
+      $client->print($cmd);
+      $client->flush();
+
+      # Read MKD response.  Note that perl's <> operator reads until
+      # end of line -- and that means just LF, not necessarily CRLF.  Thus
+      # we deliberate do TWO reads here to get the entire response.
+      $resp = <$client>;
+      $resp .= <$client>;
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Received: $resp";
+      }
 
       if ($^O eq 'darwin') {
         # MacOSX hack
         $sub_dir = '/private' . $sub_dir;
       }
 
-      $expected = "\"$sub_dir\" - Directory successfully created";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $expected = "257 \"$sub_dir\" - Directory successfully created\r\n";
+      $self->assert($expected eq $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
+      
+      $cmd = "QUIT\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sending: $cmd";
+      }
+
+      $client->print($cmd);
+      $client->flush();
+
+      $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Received: $resp";
+      }
+
+      $client->close();
 
       $self->assert(-d $sub_dir,
         test_msg("$sub_dir directory does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1759,7 +2063,7 @@ sub mkd_digits_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1769,18 +2073,10 @@ sub mkd_digits_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm b/tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm
index ccb2af1..8c343bb 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm
@@ -32,11 +32,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  mlsd_ok_dir => {
+  mlsd_ok_cwd_dir => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  mlsd_ok_other_dir_bug4198 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   mlsd_ok_chrooted_dir => {
     order => ++$order,
     test_class => [qw(forking rootprivs)],
@@ -97,6 +102,16 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  mlsd_symlink_showsymlinks_on_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  mlsd_symlink_showsymlinks_on_use_slink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   mlsd_symlinked_dir_bug3859 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -107,6 +122,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  mlsd_wide_dir => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   # XXX Plenty of other tests needed: params, maxfiles, maxdirs, depth, etc
 };
 
@@ -121,45 +141,15 @@ sub list_tests {
 sub mlsd_ok_raw_active {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -168,7 +158,7 @@ sub mlsd_ok_raw_active {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -185,9 +175,9 @@ sub mlsd_ok_raw_active {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw();
       unless ($conn) {
@@ -199,12 +189,19 @@ sub mlsd_ok_raw_active {
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
+      $self->assert(scalar(@$lines) > 1,
+        test_msg("Expected several MLSD lines, got " . scalar(@$lines)));
+
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$1} = 1;
         }
       }
@@ -243,7 +240,7 @@ sub mlsd_ok_raw_active {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -253,18 +250,10 @@ sub mlsd_ok_raw_active {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub mlsd_ok_raw_passive {
@@ -335,7 +324,6 @@ sub mlsd_ok_raw_passive {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-
       $client->login($user, $passwd);
 
       my $conn = $client->mlsd_raw();
@@ -351,9 +339,9 @@ sub mlsd_ok_raw_passive {
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$1} = 1;
         }
       }
@@ -486,7 +474,6 @@ sub mlsd_fails_file {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
 
       my $conn = $client->mlsd_raw($test_file);
@@ -501,11 +488,11 @@ sub mlsd_fails_file {
 
       $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "'$test_file' is not a directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -540,7 +527,7 @@ sub mlsd_fails_file {
   unlink($log_file);
 }
 
-sub mlsd_ok_dir {
+sub mlsd_ok_cwd_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -608,8 +595,8 @@ sub mlsd_ok_dir {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($home_dir);
       unless ($conn) {
@@ -622,10 +609,10 @@ sub mlsd_ok_dir {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
-          $res->{$1} = 1;
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
+          $res->{$2} = $1;
         }
       }
 
@@ -634,6 +621,13 @@ sub mlsd_ok_dir {
       unless ($count == $expected) {
         die("MLSD returned wrong number of entries (expected $expected, got $count)");
       }
+
+      # Make sure that the 'type' fact for the current directory is
+      # "cdir" (Bug#4198).
+      my $type = $res->{'.'};
+      my $expected = 'cdir';
+      $self->assert($expected eq $type,
+        test_msg("Expected type '$expected', got '$type'"));
     };
 
     if ($@) {
@@ -668,7 +662,7 @@ sub mlsd_ok_dir {
   unlink($log_file);
 }
 
-sub mlsd_ok_chrooted_dir {
+sub mlsd_ok_other_dir_bug4198 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -703,6 +697,10 @@ sub mlsd_ok_chrooted_dir {
     '/bin/bash');
   auth_group_write($auth_group_file, 'ftpd', $gid, $user);
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  my $sub_dir = File::Spec->rel2abs("$test_dir/sub.d");
+  mkpath($sub_dir);
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -711,8 +709,6 @@ sub mlsd_ok_chrooted_dir {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    DefaultRoot => '~',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -738,10 +734,13 @@ sub mlsd_ok_chrooted_dir {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
-      my $conn = $client->mlsd_raw('/');
+      # First, change to some other directory
+      $client->cwd($sub_dir);
+
+      my $conn = $client->mlsd_raw($test_dir);
       unless ($conn) {
         die("MLSD failed: " . $client->response_code() . " " .
           $client->response_msg());
@@ -752,17 +751,32 @@ sub mlsd_ok_chrooted_dir {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
-          $res->{$1} = 1;
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
+          $res->{$2} = $1;
         }
       }
 
       my $count = scalar(keys(%$res));
-      unless ($count == 7) {
-        die("MLSD returned wrong number of entries (expected 7, got $count)");
+      my $expected = 3;
+      unless ($count == $expected) {
+        die("MLSD returned wrong number of entries (expected $expected, got $count)");
       }
+
+      # Make sure that the 'type' fact for the current directory is
+      # "cdir" (Bug#4198).
+      my $type = $res->{'.'};
+      my $expected = 'cdir';
+      $self->assert($expected eq $type,
+        test_msg("Expected type '$expected', got '$type'"));
+
+      # Similarly, make sure that the 'type' fact for parent directory
+      # (by name) is NOT "cdir", but is just "dir" (Bug#4198).
+      my $type = $res->{'sub.d'};
+      my $expected = 'dir';
+      $self->assert($expected eq $type,
+        test_msg("Expected type '$expected', got '$type'"));
     };
 
     if ($@) {
@@ -797,6 +811,101 @@ sub mlsd_ok_chrooted_dir {
   unlink($log_file);
 }
 
+sub mlsd_ok_chrooted_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->mlsd_raw('/');
+      unless ($conn) {
+        die("MLSD failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $res = {};
+      my $lines = [split(/\r\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
+          $res->{$1} = 1;
+        }
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 8) {
+        die("MLSD returned wrong number of entries (expected 7, got $count)");
+      }
+    };
+    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 mlsd_ok_empty_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -868,8 +977,8 @@ sub mlsd_ok_empty_dir {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($test_dir);
       unless ($conn) {
@@ -882,9 +991,9 @@ sub mlsd_ok_empty_dir {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$2} = $1;
         }
       }
@@ -900,12 +1009,12 @@ sub mlsd_ok_empty_dir {
       my $type = $res->{'.'};
       my $expected = 'cdir';
       $self->assert($expected eq $type,
-        test_msg("Expected '$expected', got '$type'"));
+        test_msg("Expected type '$expected', got '$type'"));
 
       $type = $res->{'..'};
       $expected = 'pdir';
       $self->assert($expected eq $type,
-        test_msg("Expected '$expected', got '$type'"));
+        test_msg("Expected type '$expected', got '$type'"));
     };
 
     if ($@) {
@@ -1008,8 +1117,8 @@ sub mlsd_ok_no_path {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw();
       unless ($conn) {
@@ -1022,9 +1131,9 @@ sub mlsd_ok_no_path {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$1} = 1;
         }
       }
@@ -1138,8 +1247,8 @@ sub mlsd_ok_glob {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd('?foo*');
       unless ($conn) {
@@ -1154,11 +1263,11 @@ sub mlsd_ok_glob {
 
       $expected = 226;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Transfer complete";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1234,25 +1343,23 @@ sub mlsd_fails_login_required {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mlsd() };
+      eval { $client->mlsd() };
       unless ($@) {
         die("MLSD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Please login with USER and PASS";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1355,32 +1462,30 @@ sub mlsd_fails_enoent {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
+      $client->type('binary');
       $client->port();
 
       my $test_file = 'foo/bar/baz';
 
-      eval { ($resp_code, $resp_msg) = $client->mlsd($test_file) };
+      eval { $client->mlsd($test_file) };
       unless ($@) {
-        die("MLSD succeeded unexpectedly ($resp_code $resp_msg)");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+        die("MLSD succeeded unexpectedly: " . $client->response_code() . " " .
+          $client->response_msg());
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "$test_file: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1494,31 +1599,30 @@ sub mlsd_fails_eperm {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
       $client->port();
 
       chmod(0660, $sub_dir);
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mlsd($test_file) };
+      eval { $client->mlsd($test_file) };
       unless ($@) {
-        die("MLSD succeeded unexpectedly ($resp_code $resp_msg)");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+        die("MLSD succeeded unexpectedly: " . $client->response_code() . " " .
+          $client->response_msg());
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "$test_file: Permission denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1635,8 +1739,8 @@ sub mlsd_ok_hidden_file {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($home_dir);
       unless ($conn) {
@@ -1649,9 +1753,9 @@ sub mlsd_ok_hidden_file {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$1} = 1;
         }
       }
@@ -1769,8 +1873,8 @@ sub mlsd_ok_path_with_spaces {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('test foo');
       unless ($conn) {
@@ -1783,9 +1887,9 @@ sub mlsd_ok_path_with_spaces {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$1} = 1;
         }
       }
@@ -1899,8 +2003,8 @@ sub mlsd_nonascii_chars_bug3032 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw("test\b");
       unless ($conn) {
@@ -1913,9 +2017,9 @@ sub mlsd_nonascii_chars_bug3032 {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$1} = 1;
         }
       }
@@ -1961,26 +2065,12 @@ sub mlsd_nonascii_chars_bug3032 {
 sub mlsd_symlink_showsymlinks_off_bug3318 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $foo_dir = File::Spec->rel2abs("$setup->{home_dir}/foo");
   mkpath($foo_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo/test.txt");
+  my $test_file = File::Spec->rel2abs("$foo_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     print $fh "Hello, World!\n";
     unless (close($fh)) {
@@ -1995,41 +2085,38 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
   # symlink we need
 
   my $cwd = getcwd();
-  unless (chdir("$foo_dir")) {
+  unless (chdir($foo_dir)) {
     die("Can't chdir to $foo_dir: $!");
   }
 
-  unless (symlink('test.txt', 'test.lnk')) {
-    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
   }
 
   unless (chdir($cwd)) {
     die("Can't chdir to $cwd: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $foo_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $foo_dir)) {
+      die("Can't set perms on $foo_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $foo_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir)) {
+      die("Can't set owner of $foo_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20 fs.statcache:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
     ShowSymlinks => 'off',
 
     IfModules => {
@@ -2039,7 +2126,8 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -2056,9 +2144,9 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2068,11 +2156,16 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
 
       my $buf;
       $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=(\S+);UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$2} = $1;
         }
       }
@@ -2082,7 +2175,7 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
         die("MLSD returned wrong number of entries (expected 4, got $count)");
       }
 
-      # test.lnk is a symlink to test.txt.  According to RFC3659, the unique
+      # test.lnk is a symlink to test.txt.  According to RFC 3659, the unique
       # fact for both of these should thus be the same, since they are the
       # same underlying object.
 
@@ -2091,7 +2184,6 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
       $self->assert($expected eq $got,
         test_msg("Expected '$expected', got '$got'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -2100,7 +2192,7 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2110,43 +2202,173 @@ sub mlsd_symlink_showsymlinks_off_bug3318 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub mlsd_symlink_showsymlinks_on_bug3318 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $foo_dir = File::Spec->rel2abs("$setup->{home_dir}/foo");
+  mkpath($foo_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$foo_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the 'foo' directory in order to create a relative path in the
+  # symlink we need
 
-  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $cwd = getcwd();
+  unless (chdir("$foo_dir")) {
+    die("Can't chdir to $foo_dir: $!");
+  }
+
+  unless (symlink('test.txt', 'test.lnk')) {
+    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $foo_dir)) {
+      die("Can't set perms on $foo_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir)) {
+      die("Can't set owner of $foo_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->mlsd_raw('foo');
+      unless ($conn) {
+        die("MLSD failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $res = {};
+      my $lines = [split(/\r\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
+          $res->{$3} = { type => $1, unique => $2 };
+        }
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 4) {
+        die("MLSD returned wrong number of entries (expected 4, got $count)");
+      }
+
+      # test.lnk is a symlink to test.txt.  According to RFC3659, the unique
+      # fact for both of these should thus be the same, since they are the
+      # same underlying object.
+
+      my $expected = $res->{'test.txt'}->{unique};
+      my $got = $res->{'test.lnk'}->{unique};
+      $self->assert($expected eq $got,
+        test_msg("Expected '$expected', got '$got'"));
+
+      # Since ShowSymlinks is on, the type for test.lnk should indicate that
+      # it's a symlink
+      $expected = 'OS.unix=symlink';
+      $got = $res->{'test.lnk'}->{type};
+      $self->assert(qr/$expected/i, $got,
+        test_msg("Expected '$expected', got '$got'"));
+    };
+    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 mlsd_symlink_showsymlinks_on_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $foo_dir = File::Spec->rel2abs("$setup->{home_dir}/foo");
   mkpath($foo_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo/test.txt");
+  my $test_file = File::Spec->rel2abs("$foo_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     print $fh "Hello, World!\n";
     unless (close($fh)) {
@@ -2176,26 +2398,26 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $foo_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $foo_dir)) {
+      die("Can't set perms on $foo_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $foo_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir)) {
+      die("Can't set owner of $foo_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
     ShowSymlinks => 'on',
 
     IfModules => {
@@ -2205,7 +2427,8 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -2222,9 +2445,9 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2236,10 +2459,14 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
       $conn->read($buf, 8192, 30);
       eval { $conn->close() };
 
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$3} = { type => $1, unique => $2 };
         }
       }
@@ -2265,7 +2492,6 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
       $self->assert(qr/$expected/i, $got,
         test_msg("Expected '$expected', got '$got'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -2274,7 +2500,7 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -2284,18 +2510,167 @@ sub mlsd_symlink_showsymlinks_on_bug3318 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
+sub mlsd_symlink_showsymlinks_on_use_slink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $foo_dir = File::Spec->rel2abs("$setup->{home_dir}/foo");
+  mkpath($foo_dir);
+
+  my $test_file = File::Spec->rel2abs("$foo_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  unlink($log_file);
+  # Change to the 'foo' directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir("$foo_dir")) {
+    die("Can't chdir to $foo_dir: $!");
+  }
+
+  unless (symlink('test.txt', 'test.lnk')) {
+    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $foo_dir)) {
+      die("Can't set perms on $foo_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $foo_dir)) {
+      die("Can't set owner of $foo_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+    FactsOptions => 'UseSlink',
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->mlsd_raw('foo');
+      unless ($conn) {
+        die("MLSD failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $res = {};
+      my $lines = [split(/\r\n/, $buf)];
+      foreach my $line (@$lines) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
+          $res->{$3} = { type => $1, unique => $2 };
+        }
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 4) {
+        die("MLSD returned wrong number of entries (expected 4, got $count)");
+      }
+
+      # test.lnk is a symlink to test.txt.  According to RFC3659, the unique
+      # fact for both of these should thus be the same, since they are the
+      # same underlying object.
+
+      my $expected = $res->{'test.txt'}->{unique};
+      my $got = $res->{'test.lnk'}->{unique};
+      $self->assert($expected eq $got,
+        test_msg("Expected '$expected', got '$got'"));
+
+      # Since ShowSymlinks is on, the type for test.lnk should indicate that
+      # it's a symlink
+      $expected = 'OS.unix=slink:/foo/test.txt';
+      $got = $res->{'test.lnk'}->{type};
+      $self->assert(qr/$expected/i, $got,
+        test_msg("Expected '$expected', got '$got'"));
+    };
+    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);
 }
 
 # See also:
@@ -2394,6 +2769,7 @@ sub mlsd_symlinked_dir_bug3859 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2406,9 +2782,9 @@ sub mlsd_symlinked_dir_bug3859 {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$3} = { type => $1, unique => $2 };
         }
       }
@@ -2429,7 +2805,7 @@ sub mlsd_symlinked_dir_bug3859 {
 
       # Since ShowSymlinks is on by default, the type for test.lnk should
       # indicate that it's a symlink
-      $expected = 'OS.unix=symlink'; 
+      $expected = 'OS.unix=symlink';
       $got = $res->{'test.lnk'}->{type};
       $self->assert(qr/$expected/i, $got,
         test_msg("Expected type fact '$expected', got '$got'"));
@@ -2561,6 +2937,7 @@ sub mlsd_symlinked_dir_showsymlinks_off_bug3859 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2573,9 +2950,9 @@ sub mlsd_symlinked_dir_showsymlinks_off_bug3859 {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
-        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
+        if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
           $res->{$3} = { type => $1, unique => $2 };
         }
       }
@@ -2634,4 +3011,143 @@ sub mlsd_symlinked_dir_showsymlinks_off_bug3859 {
   unlink($log_file);
 }
 
+sub mlsd_wide_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $expected = {
+    '.' => 1,
+    '..' => 1,
+  };
+
+  my $max_nfiles = 500;
+  for (my $i = 0; $i < $max_nfiles; $i++) {
+    my $test_filename = 'SomeReallyLongAndObnoxiousTestFileNameTemplate' . $i;
+
+    # The expected hash is used later for verifying the results of the READDIR
+    $expected->{$test_filename} = 1;
+
+    my $test_path = File::Spec->rel2abs("$test_dir/$test_filename");
+
+    if (open(my $fh, "> $test_path")) {
+      close($fh);
+
+    } else {
+      die("Can't open $test_path: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'pool:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      for (my $i = 0; $i < 10; $i++) {
+        my $conn = $client->mlsd_raw($test_dir);
+        unless ($conn) {
+          die("Failed to MLSD: " . $client->response_code() . " " .
+            $client->response_msg());
+        }
+
+        my $buf;
+        my $tmp;
+        while ($conn->read($tmp, 16382, 25)) {
+          $buf .= $tmp;
+          $tmp = '';
+        }
+        eval { $conn->close() };
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "buf:\n$buf\n";
+        }
+
+        # We have to be careful of the fact that readdir returns directory
+        # entries in an unordered fashion.
+        my $res = {};
+        my $lines = [split(/\r\n/, $buf)];
+        foreach my $line (@$lines) {
+          if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.groupname=\S+;UNIX\.mode=\d+;UNIX\.owner=\d+;UNIX\.ownername=\S+; (.*?)$/) {
+            $res->{$1} = 1;
+          }
+        }
+
+        my $ok = 1;
+        my $mismatch;
+        foreach my $name (keys(%$res)) {
+          unless (defined($expected->{$name})) {
+            $mismatch = $name;
+            $ok = 0;
+            last;
+          }
+        }
+
+        unless ($ok) {
+          die("Unexpected name '$mismatch' appeared in MLSD data")
+        }
+      }
+
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/MLST.pm b/tests/t/lib/ProFTPD/Tests/Commands/MLST.pm
index 331fb19..3a355ac 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/MLST.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/MLST.pm
@@ -31,6 +31,36 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  mlst_dir_cwd_bug4198 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  mlst_symlink_showsymlinks_off_bug3318 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  mlst_symlink_showsymlinks_on_bug3318 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  mlst_symlink_showsymlinks_on_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  mlst_symlink_showsymlinks_on_use_slink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  mlst_symlink_showsymlinks_on_use_slink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   mlst_no_path_ok => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -79,6 +109,165 @@ sub list_tests {
 sub mlst_file_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $full_path = File::Spec->rel2abs($setup->{config_file});
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mlst('cmds.conf');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/cmds\.conf$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mlst_file_chrooted_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mlst('cmds.conf');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; /cmds.conf$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mlst_dir_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
@@ -96,15 +285,18 @@ sub mlst_file_ok {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+  
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
     }
   }
 
@@ -112,8 +304,6 @@ sub mlst_file_ok {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $full_path = File::Spec->rel2abs($config_file);
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -147,21 +337,20 @@ sub mlst_file_ok {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mlst('cmds.conf');
+      my ($resp_code, $resp_msg) = $client->mlst($sub_dir);
 
       my $expected;
-
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/cmds\.conf$';
+      $expected = ('modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/foo$');
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
     };
 
     if ($@) {
@@ -196,7 +385,7 @@ sub mlst_file_ok {
   unlink($log_file);
 }
 
-sub mlst_file_chrooted_ok {
+sub mlst_dir_cwd_bug4198 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -239,7 +428,6 @@ sub mlst_file_chrooted_ok {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -266,21 +454,22 @@ sub mlst_file_chrooted_ok {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mlst('cmds.conf');
+      my ($resp_code, $resp_msg) = $client->mlst('.');
 
       my $expected;
-
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; /cmds.conf$';
+      # Note that, per Bug#4198, we do NOT expect to see a type fact of
+      # "cdir" here, just "dir".
+      $expected = ('modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*$');
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
     };
 
     if ($@) {
@@ -315,52 +504,36 @@ sub mlst_file_chrooted_ok {
   unlink($log_file);
 }
 
-sub mlst_dir_ok {
+sub mlst_symlink_showsymlinks_off_bug3318 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
-    }
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    ShowSymlinks => 'off',
 
     IfModules => {
       'mod_delay.c' => {
@@ -369,7 +542,8 @@ sub mlst_dir_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -386,22 +560,20 @@ sub mlst_dir_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->mlst($sub_dir);
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
 
       my $expected;
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = ('modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/foo$');
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -412,7 +584,7 @@ sub mlst_dir_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -422,18 +594,430 @@ sub mlst_dir_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
+sub mlst_symlink_showsymlinks_on_bug3318 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  unlink($log_file);
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=OS.unix=symlink;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mlst_symlink_showsymlinks_on_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=(a)?dfr(w)?;size=\d+;type=OS.unix=symlink;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mlst_symlink_showsymlinks_on_use_slink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    FactsOptions => 'UseSlink',
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = ' modify=\d+;perm=adfr(w)?;size=\d+;type=OS.unix=slink:' . $dst_path . ';unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mlst_symlink_showsymlinks_on_use_slink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$setup->{home_dir}/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$setup->{home_dir}/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0644, $test_file, $test_symlink)) {
+      die("Can't set perms 0644 on $test_file, $test_symlink: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+    FactsOptions => 'UseSlink',
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = ' modify=\d+;perm=(a)?dfr(w)?;size=\d+;type=OS.unix=slink:\/test\.txt;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 mlst_no_path_ok {
@@ -520,7 +1104,7 @@ sub mlst_no_path_ok {
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = ('modify=\d+;perm=flcdmpe;type=cdir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/(.*?)$');
+      $expected = ('modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/(.*?)$');
       $self->assert(qr/$expected/, $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
     };
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/NLST.pm b/tests/t/lib/ProFTPD/Tests/Commands/NLST.pm
index 2da2d80..7fa5c84 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/NLST.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/NLST.pm
@@ -37,16 +37,36 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  nlst_ok_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  nlst_ok_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   nlst_ok_no_path => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
-  nlst_ok_glob => {
+  nlst_ok_glob_file => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  nlst_ok_glob_dir_bug4084 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  nlst_ok_glob_dir_with_recursion_bug4084 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   nlst_fails_login_required => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -127,6 +147,16 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  nlst_opt_a_root_dir_bug4069 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  nlst_opt_1_with_chroot => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
 };
 
 sub new {
@@ -140,46 +170,15 @@ sub list_tests {
 sub nlst_ok_raw_active {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -188,7 +187,8 @@ sub nlst_ok_raw_active {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -205,9 +205,8 @@ sub nlst_ok_raw_active {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->nlst_raw();
       unless ($conn) {
@@ -219,6 +218,12 @@ sub nlst_ok_raw_active {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
@@ -259,7 +264,7 @@ sub nlst_ok_raw_active {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -269,63 +274,24 @@ sub nlst_ok_raw_active {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub nlst_ok_raw_passive {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -334,7 +300,8 @@ sub nlst_ok_raw_passive {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -351,9 +318,8 @@ sub nlst_ok_raw_passive {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-
-      $client->login($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->nlst_raw();
       unless ($conn) {
@@ -369,6 +335,12 @@ sub nlst_ok_raw_passive {
       }
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
@@ -400,7 +372,6 @@ sub nlst_ok_raw_passive {
         die("Unexpected name '$mismatch' appeared in NLST data")
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -409,7 +380,7 @@ sub nlst_ok_raw_passive {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -419,65 +390,26 @@ sub nlst_ok_raw_passive {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub nlst_ok_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $test_file = File::Spec->rel2abs($config_file);
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -486,7 +418,8 @@ sub nlst_ok_file {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -503,9 +436,8 @@ sub nlst_ok_file {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->nlst_raw($test_file);
       unless ($conn) {
@@ -517,6 +449,12 @@ sub nlst_ok_file {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       my $res = {};
       my $names = [split(/\n/, $buf)];
       foreach my $name (@$names) {
@@ -532,7 +470,6 @@ sub nlst_ok_file {
         die("NLST failed to return $test_file");
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -541,7 +478,7 @@ sub nlst_ok_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -551,63 +488,24 @@ sub nlst_ok_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub nlst_ok_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -616,7 +514,8 @@ sub nlst_ok_dir {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -633,11 +532,10 @@ sub nlst_ok_dir {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->nlst_raw($home_dir);
+      my $conn = $client->nlst_raw($setup->{home_dir});
       unless ($conn) {
         die("NLST failed: " . $client->response_code() . " " .
           $client->response_msg());
@@ -647,6 +545,12 @@ sub nlst_ok_dir {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       my $res = {};
       my $names = [split(/\n/, $buf)];
       foreach my $name (@$names) {
@@ -659,7 +563,6 @@ sub nlst_ok_dir {
         die("NLST returned wrong number of entries (expected $expected, got $count)");
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -668,7 +571,7 @@ sub nlst_ok_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -678,18 +581,291 @@ sub nlst_ok_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-  unlink($log_file);
+sub nlst_ok_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->nlst_raw($path);
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $res = {};
+      my $names = [split(/(\r)?\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 1) {
+        die("NLST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      unless (defined($res->{$path})) {
+        die("NLST failed to return $path");
+      }
+    };
+    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 nlst_ok_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->nlst_raw($path);
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Response:\n$buf\n";
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $res = {};
+      my $names = [split(/(\r)?\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 1) {
+        die("NLST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      unless (defined($res->{$path})) {
+        die("NLST failed to return $path");
+      }
+    };
+    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 nlst_ok_no_path {
@@ -791,7 +967,431 @@ sub nlst_ok_no_path {
       $ex = $@;
     }
 
-    print $wfh "done\n";
+    print $wfh "done\n";
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub nlst_ok_glob_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->nlst_raw("*.conf");
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      my $res = {};
+      my $names = [split(/\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
+      }
+
+      my $count = scalar(keys(%$res));
+      unless ($count == 1) {
+        die("NLST returned wrong number of entries (expected 1, got $count)");
+      }
+
+      my $test_file = 'cmds.conf';
+      unless (defined($res->{$test_file})) {
+        die("NLST failed to return $test_file");
+      }
+    };
+
+    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 nlst_ok_glob_dir_bug4084 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $sub_file = File::Spec->rel2abs("$sub_dir/test.dat");
+  if (open(my $fh, "> $sub_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $sub_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->nlst_raw("*");
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      $client->quit();
+
+      my $res = {};
+      my $names = [split(/\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
+      }
+
+      my $expected = {
+        'cmds.pid' => 1,
+        'cmds.scoreboard' => 1,
+        'cmds.scoreboard.lck' => 1,
+        'cmds.group' => 1,
+        'cmds.passwd' => 1,
+        'cmds.conf' => 1,
+        'test.d' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in NLST data")
+      }
+    };
+
+    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 nlst_ok_glob_dir_with_recursion_bug4084 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $sub_file = File::Spec->rel2abs("$sub_dir/test.dat");
+  if (open(my $fh, "> $sub_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $sub_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->nlst_raw("-R *");
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      $client->quit();
+
+      my $res = {};
+      my $names = [split(/\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
+      }
+
+      my $expected = {
+        'cmds.pid' => 1,
+        'cmds.scoreboard' => 1,
+        'cmds.scoreboard.lck' => 1,
+        'cmds.group' => 1,
+        'cmds.passwd' => 1,
+        'cmds.conf' => 1,
+        'test.d/test.dat' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in NLST data")
+      }
+    };
+
+    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 nlst_fails_login_required {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/cmds.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      my ($resp_code, $resp_msg);
+      eval { ($resp_code, $resp_msg) = $client->nlst() };
+      unless ($@) {
+        die("NLST succeeded unexpectedly");
+
+      } else {
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
+      }
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "Please login with USER and PASS";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
 
   } else {
     eval { server_wait($config_file, $rfh) };
@@ -818,7 +1418,7 @@ sub nlst_ok_no_path {
   unlink($log_file);
 }
 
-sub nlst_ok_glob {
+sub nlst_fails_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -890,31 +1490,29 @@ sub nlst_ok_glob {
 
       $client->login($user, $passwd);
 
-      my $conn = $client->nlst_raw("*.conf");
-      unless ($conn) {
-        die("NLST failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
+      my ($resp_code, $resp_msg);
+      $client->port();
 
-      my $buf;
-      $conn->read($buf, 8192, 25);
-      eval { $conn->close() };
+      my $test_file = 'foo/bar/baz';
 
-      my $res = {};
-      my $names = [split(/\n/, $buf)];
-      foreach my $name (@$names) {
-        $res->{$name} = 1;
-      }
+      eval { ($resp_code, $resp_msg) = $client->nlst($test_file) };
+      unless ($@) {
+        die("NLST succeeded unexpectedly");
 
-      my $count = scalar(keys(%$res));
-      unless ($count == 1) {
-        die("NLST returned wrong number of entries (expected 1, got $count)");
+      } else {
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
       }
 
-      my $test_file = 'cmds.conf';
-      unless (defined($res->{$test_file})) {
-        die("NLST failed to return $test_file");
-      }
+      my $expected;
+
+      $expected = 450;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -949,7 +1547,7 @@ sub nlst_ok_glob {
   unlink($log_file);
 }
 
-sub nlst_fails_login_required {
+sub nlst_fails_enoent_glob {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -959,11 +1557,40 @@ sub nlst_fails_login_required {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -990,8 +1617,13 @@ sub nlst_fails_login_required {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
+      $client->login($user, $passwd);
+      $client->port();
+
+      my $test_glob = '*foo';
+
       my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->nlst() };
+      eval { ($resp_code, $resp_msg) = $client->nlst($test_glob) };
       unless ($@) {
         die("NLST succeeded unexpectedly");
 
@@ -1002,11 +1634,11 @@ sub nlst_fails_login_required {
 
       my $expected;
 
-      $expected = 530;
+      $expected = 450;
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "Please login with USER and PASS";
+      $expected = "No files found";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
     };
@@ -1043,7 +1675,7 @@ sub nlst_fails_login_required {
   unlink($log_file);
 }
 
-sub nlst_fails_enoent {
+sub nlst_fails_eperm {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1063,6 +1695,17 @@ sub nlst_fails_enoent {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -1114,12 +1757,11 @@ sub nlst_fails_enoent {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
       $client->port();
 
-      my $test_file = 'foo/bar/baz';
+      chmod(0660, $sub_dir);
 
+      my ($resp_code, $resp_msg);
       eval { ($resp_code, $resp_msg) = $client->nlst($test_file) };
       unless ($@) {
         die("NLST succeeded unexpectedly");
@@ -1135,7 +1777,7 @@ sub nlst_fails_enoent {
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "$test_file: No such file or directory";
+      $expected = "$test_file: Permission denied";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
     };
@@ -1172,7 +1814,7 @@ sub nlst_fails_enoent {
   unlink($log_file);
 }
 
-sub nlst_fails_enoent_glob {
+sub nlst_bug2821 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1204,18 +1846,47 @@ sub nlst_fails_enoent_glob {
     }
   }
 
+  # For this test, we need to create MANY (i.e. 150K) files in the
+  # home directory.
+  my $test_file_prefix = File::Spec->rel2abs("$tmpdir/test_");
+
+  my $count = 150000;
+  print STDOUT "# Creating $count files in $tmpdir\n";
+  for (my $i = 1; $i <= $count; $i++) {
+    my $test_file = 'test_' . sprintf("%07s", $i);
+    my $test_path = "$home_dir/$test_file";
+
+    if (open(my $fh, "> $test_path")) {
+      close($fh);
+
+    } else {
+      die("Can't open $test_path: $!");
+    }
+
+    if ($i % 10000 == 0) {
+      print STDOUT "# Created file $test_file\n";
+    }
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
+  my $timeout = 600;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TimeoutIdle => $timeout + 15,
+    TimeoutNoTransfer => $timeout + 15,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -1243,29 +1914,46 @@ sub nlst_fails_enoent_glob {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
-      $client->port();
 
-      my $test_glob = '*foo';
+      # One of the symptoms of Bug#2821 is that a LIST (or NLST) glob
+      # which matches more than 99999 files will not receive the full
+      # list.
+      #
+      # The issue turned out to be the MAX_RESULTS (now
+      # PR_TUNABLE_GLOBBING_MAX_MATCHES) limit in lib/glibc-glob.c.
+      # So now, we deliberate make sure that the number of files returned
+      # is less than the number of potential matches.
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->nlst($test_glob) };
-      unless ($@) {
-        die("NLST succeeded unexpectedly");
+      my $conn = $client->nlst_raw("test_*");
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $buf;
+      my $tmp;
+      while ($conn->read($tmp, 32768, 25)) {
+        $buf .= $tmp;
       }
+      eval { $conn->close() };
+      $client->quit();
 
-      my $expected;
+      my $res = {};
 
-      $expected = 450;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      # Preallocate memory for the expected hash size, to help speed up the
+      # evaluation of the results.
+      keys(%$res) = $count;
+
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        $res->{$line} = 1;
+      }
+
+      my $list_count = scalar(keys(%$res));
+      if ($list_count >= $count) {
+        die("NLST returned wrong number of entries (expected less than $count, got $list_count); check the PR_TUNABLE_GLOBBING_MAX_MATCHES value");
+      }
 
-      $expected = "No files found";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1276,7 +1964,7 @@ sub nlst_fails_enoent_glob {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1300,10 +1988,15 @@ sub nlst_fails_enoent_glob {
   unlink($log_file);
 }
 
-sub nlst_fails_eperm {
+sub nlst_nonascii_chars_bug3032 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
+  if (feature_have_feature_enabled('nls')) {
+    # This test is only valid if NLS support is not enabled.
+    return;
+  }
+
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
@@ -1320,17 +2013,6 @@ sub nlst_fails_eperm {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -1343,6 +2025,14 @@ sub nlst_fails_eperm {
     }
   }
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test\b");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -1351,6 +2041,8 @@ sub nlst_fails_eperm {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -1382,29 +2074,44 @@ sub nlst_fails_eperm {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
-      $client->port();
 
-      chmod(0660, $sub_dir);
+      my $conn = $client->nlst_raw("-B test*");
+      unless ($conn) {
+        die("NLST failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->nlst($test_file) };
-      unless ($@) {
-        die("NLST succeeded unexpectedly");
+      my $buf;
+      my $tmp;
+      while ($conn->read($tmp, 32768, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+      $client->quit();
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $res = {};
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        $res->{$line} = 1;
       }
 
-      my $expected;
+      my $expected = {
+        'test\010' => 1,
+      };
 
-      $expected = 450;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
 
-      $expected = "$test_file: Permission denied";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in NLST data")
+      }
     };
 
     if ($@) {
@@ -1439,7 +2146,7 @@ sub nlst_fails_eperm {
   unlink($log_file);
 }
 
-sub nlst_bug2821 {
+sub nlst_leading_whitespace_bug3268 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1471,47 +2178,26 @@ sub nlst_bug2821 {
     }
   }
 
-  # For this test, we need to create MANY (i.e. 150K) files in the
-  # home directory.
-  my $test_file_prefix = File::Spec->rel2abs("$tmpdir/test_");
-
-  my $count = 150000;
-  print STDOUT "# Creating $count files in $tmpdir\n";
-  for (my $i = 1; $i <= $count; $i++) {
-    my $test_file = 'test_' . sprintf("%07s", $i);
-    my $test_path = "$home_dir/$test_file";
-
-    if (open(my $fh, "> $test_path")) {
-      close($fh);
-
-    } else {
-      die("Can't open $test_path: $!");
-    }
-
-    if ($i % 10000 == 0) {
-      print STDOUT "# Created file $test_file\n";
-    }
-  }
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $timeout = 600;
+  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open '$test_file': $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    TimeoutIdle => $timeout + 15,
-    TimeoutNoTransfer => $timeout + 15,
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -1537,48 +2223,56 @@ sub nlst_bug2821 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
 
-      # One of the symptoms of Bug#2821 is that a LIST (or NLST) glob
-      # which matches more than 99999 files will not receive the full
-      # list.
-      #
-      # The issue turned out to be the MAX_RESULTS (now
-      # PR_TUNABLE_GLOBBING_MAX_MATCHES) limit in lib/glibc-glob.c.
-      # So now, we deliberate make sure that the number of files returned
-      # is less than the number of potential matches.
-
-      my $conn = $client->nlst_raw("test_*");
+      my $conn = $client->nlst_raw(' test.txt');
       unless ($conn) {
-        die("NLST failed: " . $client->response_code() . " " .
+        die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
       my $buf;
-      my $tmp;
-      while ($conn->read($tmp, 32768, 25)) {
-        $buf .= $tmp;
-      }
+      $conn->read($buf, 8192, 25);
       eval { $conn->close() };
-      $client->quit();
 
-      my $res = {};
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
-      # Preallocate memory for the expected hash size, to help speed up the
-      # evaluation of the results.
-      keys(%$res) = $count;
+      my $expected;
 
-      my $lines = [split(/\n/, $buf)];
-      foreach my $line (@$lines) {
-        $res->{$line} = 1;
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
+      my $res = {};
+      my $names = [split(/\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
       }
 
-      my $list_count = scalar(keys(%$res));
-      if ($list_count >= $count) {
-        die("NLST returned wrong number of entries (expected less than $count, got $list_count); check the PR_TUNABLE_GLOBBING_MAX_MATCHES value");
+      $expected = {
+        ' test.txt' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
       }
 
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in NLST data")
+      }
     };
 
     if ($@) {
@@ -1589,7 +2283,7 @@ sub nlst_bug2821 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1613,15 +2307,10 @@ sub nlst_bug2821 {
   unlink($log_file);
 }
 
-sub nlst_nonascii_chars_bug3032 {
+sub nlst_leading_whitespace_with_opts_bug3268 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
-  if (feature_have_feature_enabled('nls')) {
-    # This test is only valid if NLS support is not enabled.
-    return;
-  }
-
   my $config_file = "$tmpdir/cmds.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
@@ -1650,24 +2339,22 @@ sub nlst_nonascii_chars_bug3032 {
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test\b");
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
   } else {
-    die("Can't open $test_file: $!");
+    die("Can't open '$test_file': $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 fsio:0 lock:0',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -1697,31 +2384,41 @@ sub nlst_nonascii_chars_bug3032 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
 
-      my $conn = $client->nlst_raw("-B test*");
+      my $conn = $client->nlst_raw('-a  test.txt');
       unless ($conn) {
-        die("NLST failed: " . $client->response_code() . " " .
+        die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
       my $buf;
-      my $tmp;
-      while ($conn->read($tmp, 32768, 25)) {
-        $buf .= $tmp;
-      }
+      $conn->read($buf, 8192, 25);
       eval { $conn->close() };
-      $client->quit();
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
-      foreach my $line (@$lines) {
-        $res->{$line} = 1;
+      my $names = [split(/\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
       }
 
-      my $expected = {
-        'test\010' => 1,
+      $expected = {
+        ' test.txt' => 1,
       };
 
       my $ok = 1;
@@ -1771,7 +2468,7 @@ sub nlst_nonascii_chars_bug3032 {
   unlink($log_file);
 }
 
-sub nlst_leading_whitespace_bug3268 {
+sub nlst_leading_whitespace_with_strict_opts_bug3268 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1823,6 +2520,8 @@ sub nlst_leading_whitespace_bug3268 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    ListOptions => '"-a +R" strict',
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -1850,7 +2549,7 @@ sub nlst_leading_whitespace_bug3268 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $conn = $client->nlst_raw(' test.txt');
+      my $conn = $client->nlst_raw(' -l  test.txt');
       unless ($conn) {
         die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -1932,7 +2631,7 @@ sub nlst_leading_whitespace_bug3268 {
   unlink($log_file);
 }
 
-sub nlst_leading_whitespace_with_opts_bug3268 {
+sub nlst_symlink_bug3254 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1940,27 +2639,113 @@ sub nlst_leading_whitespace_with_opts_bug3268 {
   my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
 
-  my $log_file = test_get_logfile();
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
+  my $baz_dir = File::Spec->rel2abs("$tmpdir/bar/baz");
+  my $quxx_dir = File::Spec->rel2abs("$tmpdir/bar/quxx");
+  mkpath([$foo_dir, $bar_dir, $baz_dir, $quxx_dir]);
+
+  # Change to the 'foo' directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir("$foo_dir")) {
+    die("Can't chdir to $foo_dir: $!");
+  }
+
+  unless (symlink("../bar/baz", "baz")) {
+    die("Can't symlink '../bar/baz' to 'baz': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $ftpaccess_file = File::Spec->rel2abs("$foo_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser $user
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  $ftpaccess_file = File::Spec->rel2abs("$bar_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser bar
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  $ftpaccess_file = File::Spec->rel2abs("$baz_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser bar,$user
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  $ftpaccess_file = File::Spec->rel2abs("$quxx_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOL;
+<Limit ALL>
+  AllowUser bar,$user,
+  DenyAll
+</Limit>
+EOL
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
+      die("Can't set perms on dirs to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
+      die("Can't set owner of dirs to $uid/$gid: $!");
     }
   }
 
@@ -1968,14 +2753,6 @@ sub nlst_leading_whitespace_with_opts_bug3268 {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open '$test_file': $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -1984,6 +2761,10 @@ sub nlst_leading_whitespace_with_opts_bug3268 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    AllowOverride => 'on',
+    DefaultRoot => '~',
+    ShowSymlinks => 'off',
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -2009,9 +2790,11 @@ sub nlst_leading_whitespace_with_opts_bug3268 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
       $client->login($user, $passwd);
 
-      my $conn = $client->nlst_raw('-a  test.txt');
+      $client->cwd("foo/baz");
+      my $conn = $client->nlst_raw();
       unless ($conn) {
         die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -2034,31 +2817,9 @@ sub nlst_leading_whitespace_with_opts_bug3268 {
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
-      # We have to be careful of the fact that readdir returns directory
-      # entries in an unordered fashion.
-      my $res = {};
-      my $names = [split(/\n/, $buf)];
-      foreach my $name (@$names) {
-        $res->{$name} = 1;
-      }
-
-      $expected = {
-        ' test.txt' => 1,
-      };
-
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
+      $client->quote('CWD', '..');
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in NLST data")
-      }
+      $client->quit();
     };
 
     if ($@) {
@@ -2093,7 +2854,7 @@ sub nlst_leading_whitespace_with_opts_bug3268 {
   unlink($log_file);
 }
 
-sub nlst_leading_whitespace_with_strict_opts_bug3268 {
+sub nlst_dash_filename_bug3476 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2129,12 +2890,12 @@ sub nlst_leading_whitespace_with_strict_opts_bug3268 {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
+  my $test_file = File::Spec->rel2abs("$tmpdir/-test.txt");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
   } else {
-    die("Can't open '$test_file': $!");
+    die("Can't open $test_file: $!");
   }
 
   my $config = {
@@ -2145,8 +2906,6 @@ sub nlst_leading_whitespace_with_strict_opts_bug3268 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    ListOptions => '"-a +R" strict',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -2172,11 +2931,12 @@ sub nlst_leading_whitespace_with_strict_opts_bug3268 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
       $client->login($user, $passwd);
 
-      my $conn = $client->nlst_raw(' -l  test.txt');
+      my $conn = $client->nlst_raw('-test.txt');
       unless ($conn) {
-        die("Failed to NLST: " . $client->response_code() . " " .
+        die("NLST failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
@@ -2184,43 +2944,20 @@ sub nlst_leading_whitespace_with_strict_opts_bug3268 {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
-      my $expected;
-
-      $expected = 226;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      $expected = 'Transfer complete';
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      # We have to be careful of the fact that readdir returns directory
-      # entries in an unordered fashion.
       my $res = {};
       my $names = [split(/\n/, $buf)];
       foreach my $name (@$names) {
         $res->{$name} = 1;
       }
 
-      $expected = {
-        ' test.txt' => 1,
-      };
-
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+      my $count = scalar(keys(%$res));
+      unless ($count == 1) {
+        die("NLST returned wrong number of entries (expected 1, got $count)");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in NLST data")
+      my $expected = '-test.txt';
+      unless (defined($res->{$expected})) {
+        die("NLST failed to return $expected");
       }
     };
 
@@ -2256,7 +2993,7 @@ sub nlst_leading_whitespace_with_strict_opts_bug3268 {
   unlink($log_file);
 }
 
-sub nlst_symlink_bug3254 {
+sub nlst_opt_noerrorifabsent_bug3506 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2276,101 +3013,15 @@ sub nlst_symlink_bug3254 {
   my $uid = 500;
   my $gid = 500;
 
-  my $foo_dir = File::Spec->rel2abs("$tmpdir/foo");
-  my $bar_dir = File::Spec->rel2abs("$tmpdir/bar");
-  my $baz_dir = File::Spec->rel2abs("$tmpdir/bar/baz");
-  my $quxx_dir = File::Spec->rel2abs("$tmpdir/bar/quxx");
-  mkpath([$foo_dir, $bar_dir, $baz_dir, $quxx_dir]);
-
-  # Change to the 'foo' directory in order to create a relative path in the
-  # symlink we need
-
-  my $cwd = getcwd();
-  unless (chdir("$foo_dir")) {
-    die("Can't chdir to $foo_dir: $!");
-  }
-
-  unless (symlink("../bar/baz", "baz")) {
-    die("Can't symlink '../bar/baz' to 'baz': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
-  my $ftpaccess_file = File::Spec->rel2abs("$foo_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser $user
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
-  $ftpaccess_file = File::Spec->rel2abs("$bar_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser bar
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
-  $ftpaccess_file = File::Spec->rel2abs("$baz_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser bar,$user
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
-  $ftpaccess_file = File::Spec->rel2abs("$quxx_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOL;
-<Limit ALL>
-  AllowUser bar,$user,
-  DenyAll
-</Limit>
-EOL
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
-      die("Can't set perms on dirs to 0755: $!");
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $foo_dir, $bar_dir, $baz_dir, $quxx_dir)) {
-      die("Can't set owner of dirs to $uid/$gid: $!");
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
 
@@ -2378,6 +3029,8 @@ EOL
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
+  my $test_file = File::Spec->rel2abs('foo-bar.txt');
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -2386,9 +3039,7 @@ EOL
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    AllowOverride => 'on',
-    DefaultRoot => '~',
-    ShowSymlinks => 'off',
+    ListOptions => '"" NoErrorIfAbsent',
 
     IfModules => {
       'mod_delay.c' => {
@@ -2418,10 +3069,9 @@ EOL
 
       $client->login($user, $passwd);
 
-      $client->cwd("foo/baz");
-      my $conn = $client->nlst_raw();
+      my $conn = $client->nlst_raw($test_file);
       unless ($conn) {
-        die("Failed to NLST: " . $client->response_code() . " " .
+        die("NLST failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
@@ -2442,9 +3092,16 @@ EOL
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
-      $client->quote('CWD', '..');
+      my $res = {};
+      my $names = [split(/\n/, $buf)];
+      foreach my $name (@$names) {
+        $res->{$name} = 1;
+      }
 
-      $client->quit();
+      my $count = scalar(keys(%$res));
+      unless ($count == 0) {
+        die("NLST returned wrong number of entries (expected 0, got $count)");
+      }
     };
 
     if ($@) {
@@ -2479,7 +3136,7 @@ EOL
   unlink($log_file);
 }
 
-sub nlst_dash_filename_bug3476 {
+sub nlst_trailing_slashes_bug2457 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2499,14 +3156,28 @@ sub nlst_dash_filename_bug3476 {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/.test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "File\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -2515,14 +3186,6 @@ sub nlst_dash_filename_bug3476 {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/-test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -2556,12 +3219,11 @@ sub nlst_dash_filename_bug3476 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
 
-      my $conn = $client->nlst_raw('-test.txt');
+      my $conn = $client->nlst_raw('test.d///.test.dat');
       unless ($conn) {
-        die("NLST failed: " . $client->response_code() . " " .
+        die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
@@ -2569,20 +3231,34 @@ sub nlst_dash_filename_bug3476 {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
       my $res = {};
       my $names = [split(/\n/, $buf)];
       foreach my $name (@$names) {
         $res->{$name} = 1;
       }
 
-      my $count = scalar(keys(%$res));
-      unless ($count == 1) {
-        die("NLST returned wrong number of entries (expected 1, got $count)");
+      my $expected = {
+        'test.d/.test.dat' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
       }
 
-      my $expected = '-test.txt';
-      unless (defined($res->{$expected})) {
-        die("NLST failed to return $expected");
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in NLST data")
       }
     };
 
@@ -2618,7 +3294,7 @@ sub nlst_dash_filename_bug3476 {
   unlink($log_file);
 }
 
-sub nlst_opt_noerrorifabsent_bug3506 {
+sub nlst_rel_path_bug2496 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2638,14 +3314,28 @@ sub nlst_opt_noerrorifabsent_bug3506 {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/.test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "File\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -2654,8 +3344,6 @@ sub nlst_opt_noerrorifabsent_bug3506 {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $test_file = File::Spec->rel2abs('foo-bar.txt');
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -2664,8 +3352,6 @@ sub nlst_opt_noerrorifabsent_bug3506 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    ListOptions => '"" NoErrorIfAbsent',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -2691,12 +3377,12 @@ sub nlst_opt_noerrorifabsent_bug3506 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->cwd("test.d");
 
-      my $conn = $client->nlst_raw($test_file);
+      my $conn = $client->nlst_raw('.test.dat');
       unless ($conn) {
-        die("NLST failed: " . $client->response_code() . " " .
+        die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
@@ -2706,26 +3392,32 @@ sub nlst_opt_noerrorifabsent_bug3506 {
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
 
-      my $expected;
-
-      $expected = 226;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      $expected = 'Transfer complete';
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
+      # We have to be careful of the fact that readdir returns directory
+      # entries in an unordered fashion.
       my $res = {};
       my $names = [split(/\n/, $buf)];
       foreach my $name (@$names) {
         $res->{$name} = 1;
       }
 
-      my $count = scalar(keys(%$res));
-      unless ($count == 0) {
-        die("NLST returned wrong number of entries (expected 0, got $count)");
+      my $expected = {
+        '.test.dat' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+      foreach my $name (keys(%$res)) {
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in NLST data")
       }
     };
 
@@ -2761,7 +3453,7 @@ sub nlst_opt_noerrorifabsent_bug3506 {
   unlink($log_file);
 }
 
-sub nlst_trailing_slashes_bug2457 {
+sub nlst_rel_path_chrooted_bug2496 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2818,6 +3510,7 @@ sub nlst_trailing_slashes_bug2457 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -2845,8 +3538,9 @@ sub nlst_trailing_slashes_bug2457 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->cwd("test.d");
 
-      my $conn = $client->nlst_raw('test.d///.test.dat');
+      my $conn = $client->nlst_raw('.test.dat');
       unless ($conn) {
         die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -2869,7 +3563,7 @@ sub nlst_trailing_slashes_bug2457 {
       }
 
       my $expected = {
-        'test.d/.test.dat' => 1,
+        '.test.dat' => 1,
       };
 
       my $ok = 1;
@@ -2919,7 +3613,7 @@ sub nlst_trailing_slashes_bug2457 {
   unlink($log_file);
 }
 
-sub nlst_rel_path_bug2496 {
+sub nlst_parent_dir_bug4011 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2939,28 +3633,40 @@ sub nlst_rel_path_bug2496 {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($sub_dir);
+  my $sub_dir1 = File::Spec->rel2abs("$tmpdir/dir1");
+  my $sub_dir2 = File::Spec->rel2abs("$tmpdir/dir1/dir2");
+  mkpath($sub_dir2);
 
-  my $test_file = File::Spec->rel2abs("$sub_dir/.test.dat");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "File\n";
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/dir1/file1");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "File1\n";
     unless (close($fh)) {
-      die("Can't write $test_file: $!");
+      die("Can't write $test_file1: $!");
     }
 
   } else {
-    die("Can't open $test_file: $!");
+    die("Can't open $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/dir1/dir2/file2");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "File2\n";
+    unless (close($fh)) {
+      die("Can't write $test_file2: $!");
+    }
+
+  } else {
+    die("Can't open $test_file2: $!");
   }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
+    unless (chmod(0755, $home_dir, $sub_dir1)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+    unless (chown($uid, $gid, $home_dir, $sub_dir1)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -3003,9 +3709,10 @@ sub nlst_rel_path_bug2496 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->cwd("test.d");
+      $client->cwd("dir1");
+      $client->cwd("dir2");
 
-      my $conn = $client->nlst_raw('.test.dat');
+      my $conn = $client->nlst_raw('..////');
       unless ($conn) {
         die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -3015,10 +3722,6 @@ sub nlst_rel_path_bug2496 {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
@@ -3028,7 +3731,8 @@ sub nlst_rel_path_bug2496 {
       }
 
       my $expected = {
-        '.test.dat' => 1,
+        '../file1' => 1,
+        '../dir2' => 1,
       };
 
       my $ok = 1;
@@ -3078,7 +3782,7 @@ sub nlst_rel_path_bug2496 {
   unlink($log_file);
 }
 
-sub nlst_rel_path_chrooted_bug2496 {
+sub nlst_opt_a_root_dir_bug4069 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -3098,28 +3802,39 @@ sub nlst_rel_path_chrooted_bug2496 {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($sub_dir);
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $test_file = File::Spec->rel2abs("$sub_dir/.test.dat");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "File\n";
+  my $test_file1 = File::Spec->rel2abs("$test_dir/.file1");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "File1\n";
     unless (close($fh)) {
-      die("Can't write $test_file: $!");
+      die("Can't write $test_file1: $!");
     }
 
   } else {
-    die("Can't open $test_file: $!");
+    die("Can't open $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$test_dir/.file2");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "File2\n";
+    unless (close($fh)) {
+      die("Can't write $test_file2: $!");
+    }
+
+  } else {
+    die("Can't open $test_file2: $!");
   }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
+    unless (chmod(0755, $home_dir, $test_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+    unless (chown($uid, $gid, $home_dir, $test_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -3135,7 +3850,6 @@ sub nlst_rel_path_chrooted_bug2496 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -3163,9 +3877,9 @@ sub nlst_rel_path_chrooted_bug2496 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->cwd("test.d");
+      $client->cwd('test.d');
 
-      my $conn = $client->nlst_raw('.test.dat');
+      my $conn = $client->nlst_raw('-a');
       unless ($conn) {
         die("Failed to NLST: " . $client->response_code() . " " .
           $client->response_msg());
@@ -3175,10 +3889,6 @@ sub nlst_rel_path_chrooted_bug2496 {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
@@ -3188,7 +3898,10 @@ sub nlst_rel_path_chrooted_bug2496 {
       }
 
       my $expected = {
-        '.test.dat' => 1,
+        '.' => 1,
+        '..' => 1,
+        '.file1' => 1,
+        '.file2' => 1,
       };
 
       my $ok = 1;
@@ -3238,7 +3951,7 @@ sub nlst_rel_path_chrooted_bug2496 {
   unlink($log_file);
 }
 
-sub nlst_parent_dir_bug4011 {
+sub nlst_opt_1_with_chroot {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -3258,40 +3971,14 @@ sub nlst_parent_dir_bug4011 {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir1 = File::Spec->rel2abs("$tmpdir/dir1");
-  my $sub_dir2 = File::Spec->rel2abs("$tmpdir/dir1/dir2");
-  mkpath($sub_dir2);
-
-  my $test_file1 = File::Spec->rel2abs("$tmpdir/dir1/file1");
-  if (open(my $fh, "> $test_file1")) {
-    print $fh "File1\n";
-    unless (close($fh)) {
-      die("Can't write $test_file1: $!");
-    }
-
-  } else {
-    die("Can't open $test_file1: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/dir1/dir2/file2");
-  if (open(my $fh, "> $test_file2")) {
-    print $fh "File2\n";
-    unless (close($fh)) {
-      die("Can't write $test_file2: $!");
-    }
-
-  } else {
-    die("Can't open $test_file2: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir1)) {
+    unless (chmod(0755, $home_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir1)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -3307,6 +3994,7 @@ sub nlst_parent_dir_bug4011 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -3334,12 +4022,10 @@ sub nlst_parent_dir_bug4011 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->cwd("dir1");
-      $client->cwd("dir2");
 
-      my $conn = $client->nlst_raw('..////');
+      my $conn = $client->nlst_raw("-a1 /");
       unless ($conn) {
-        die("Failed to NLST: " . $client->response_code() . " " .
+        die("NLST failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
@@ -3347,31 +4033,18 @@ sub nlst_parent_dir_bug4011 {
       $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
-      # We have to be careful of the fact that readdir returns directory
-      # entries in an unordered fashion.
       my $res = {};
       my $names = [split(/\n/, $buf)];
       foreach my $name (@$names) {
-        $res->{$name} = 1;
-      }
-
-      my $expected = {
-        '../file1' => 1,
-        '../dir2' => 1,
-      };
-
-      my $ok = 1;
-      my $mismatch;
-      foreach my $name (keys(%$res)) {
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
+        if ($name !~ /^\//) {
+          $res->{$name} = 1;
         }
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in NLST data")
+      my $count = scalar(keys(%$res));
+      my $expected = 8;
+      unless ($count == $expected) {
+        die("NLST returned wrong number of entries (expected $expected, got $count)");
       }
     };
 
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/OPTS.pm b/tests/t/lib/ProFTPD/Tests/Commands/OPTS.pm
index f953db5..18a7e5c 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/OPTS.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/OPTS.pm
@@ -70,6 +70,8 @@ sub opts_bug3870 {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -101,7 +103,7 @@ sub opts_bug3870 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $opts_args = ('a ' x 2000);
+      my $opts_args = ('a ' x 100);
 
       eval { $client->opts($opts_args) };
       unless ($@) {
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/RETR.pm b/tests/t/lib/ProFTPD/Tests/Commands/RETR.pm
index c9e77da..8a747bf 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/RETR.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/RETR.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -31,6 +32,36 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  retr_ok_ascii_file_bug4237 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  retr_ok_ascii_file_bug4277 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  retr_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  retr_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  retr_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  retr_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   retr_fails_not_reg => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -56,6 +87,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  retr_fails_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  retr_fails_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  retr_fails_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  retr_fails_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
   retr_fails_eperm => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -76,36 +127,1242 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
-  retr_2nd_transfer_terminates_1st_transfer_bug4010 => {
-    order => ++$order,
-    test_class => [qw(bug forking)],
-  },
+  retr_2nd_transfer_terminates_1st_transfer_bug4010 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub retr_ok_raw_active {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my ($resp_code, $resp_msg);
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+    };
+
+    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 retr_ok_raw_passive {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 {
+      # When run too quickly with the other tests, this test can fail.  So
+      # pause a little here.
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my ($resp_code, $resp_msg);
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+    };
+
+    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 retr_ok_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Foo!\n";
+
+    unless (close($fh)) {
+      die("Unable to write $test_file: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      my $size = $conn->bytes_read();
+      eval { $conn->close() };
+
+      my $expected = 6;
+      $self->assert($expected == $size,
+        test_msg("Expected $expected, got $size"));
+    };
+
+    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 retr_ok_ascii_file_bug4237 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "\r" x 32768;
+
+    unless (close($fh)) {
+      die("Unable to write $test_file: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.dat");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "\n" x 32768;
+
+    unless (close($fh)) {
+      die("Unable to write $test_file2: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file2: $!");
+  }
+
+  my $test_file3 = File::Spec->rel2abs("$tmpdir/test3.dat");
+  if (open(my $fh, "> $test_file3")) {
+    print $fh "\r\n" x 32768;
+
+    unless (close($fh)) {
+      die("Unable to write $test_file3: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file3: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('ascii');
+
+      # Download a file of all CRs
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = '';
+      my $tmp;
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      my $size = length($buf);
+      my $expected = -s $test_file;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      # Download a file of all LFs
+      my $conn = $client->retr_raw($test_file2);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = '';
+      my $tmp;
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      $size = length($buf);
+      $expected = -s $test_file2;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      # Download a file of all CRLF pairs
+      $conn = $client->retr_raw($test_file3);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      $size = length($buf);
+
+      # Note: the expected size here is half the length of the file.  Why?
+      # The test3.dat file is comprised of 32K "\r\n" pairs.  As such, the
+      # will SEND that data as is.  And on the client end, since we are a Unix
+      # machine, they will be converted to just "\n" (32K of them) -- thus
+      # half the original file size.
+      $expected = (-s $test_file3) / 2;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      $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 retr_ok_ascii_file_bug4277 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  # Use Digest::MD5 to verify the absence of corruption.
+  use Digest::MD5;
+
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "\r" x 32768;
+
+    unless (close($fh)) {
+      die("Unable to write $test_file1: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file1: $!");
+  }
+
+  my $test_file1_digest;
+  if (open(my $fh, "< $test_file1")) {
+    my $ctx = Digest::MD5->new();
+    $ctx->addfile($fh);
+    $test_file1_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't open $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.dat");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "\n" x 32768;
+
+    unless (close($fh)) {
+      die("Unable to write $test_file2: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file2: $!");
+  }
+
+  my $test_file2_digest;
+  if (open(my $fh, "< $test_file2")) {
+    my $ctx = Digest::MD5->new();
+    $ctx->addfile($fh);
+    $test_file2_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't open $test_file2: $!");
+  }
+
+  my $test_file3 = File::Spec->rel2abs("$tmpdir/test3.dat");
+  if (open(my $fh, "> $test_file3")) {
+    print $fh "\r\n" x 32768;
+
+    unless (close($fh)) {
+      die("Unable to write $test_file3: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file3: $!");
+  }
+
+  # Since UNIX uses LF, not CRLF, the EXPECTED digest for $test_file3 will
+  # be the same as for $test_file2.
+  my $test_file3_digest = $test_file2_digest;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+      $client->type('ascii');
+
+      # Download a file of all CRs
+      my $conn = $client->retr_raw($test_file1);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = '';
+      my $tmp;
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      my $size = length($buf);
+      my $expected = -s $test_file1;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      my $ctx = Digest::MD5->new();
+      $ctx->add($buf);
+      my $digest = lc($ctx->hexdigest);
+      $self->assert($digest eq $test_file1_digest,
+        test_msg("Expected MD5 $test_file1_digest, got $digest"));
+
+      # Download a file of all LFs
+      $conn = $client->retr_raw($test_file2);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
+      $tmp = '';
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      $size = length($buf);
+      $expected = -s $test_file2;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($buf);
+      $digest = lc($ctx->hexdigest);
+      $self->assert($digest eq $test_file2_digest,
+        test_msg("Expected MD5 $test_file2_digest, got $digest"));
+
+      # Download a file of all CRLF pairs
+      $conn = $client->retr_raw($test_file3);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
+      $tmp = '';
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      $size = length($buf);
+
+      # Note: the expected size here is half the length of the file.  Why?
+      # The test3.dat file is comprised of 32K "\r\n" pairs.  As such, the
+      # will SEND that data as is.  And on the client end, since we are a Unix
+      # machine, they will be converted to just "\n" (32K of them) -- thus
+      # half the original file size.
+      $expected = (-s $test_file3) / 2;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      $ctx = Digest::MD5->new();
+      $ctx->add($buf);
+      $digest = lc($ctx->hexdigest);
+      $self->assert($digest eq $test_file3_digest,
+        test_msg("Expected MD5 $test_file3_digest, got $digest"));
+
+      $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 retr_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/foo");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Foo!\n";
+
+    unless (close($fh)) {
+      die("Unable to write $test_file: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      my $size = $conn->bytes_read();
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      my $expected = 5;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+
+    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 retr_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/foo");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Foo!\n";
+
+    unless (close($fh)) {
+      die("Unable to write $test_file: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      my $size = $conn->bytes_read();
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      my $expected = 5;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+
+    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 retr_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Foo!\n";
+
+    unless (close($fh)) {
+      die("Unable to write $test_file: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      my $size = $conn->bytes_read();
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      my $expected = 5;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+
+    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 retr_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Foo!\n";
+
+    unless (close($fh)) {
+      die("Unable to write $test_file: $!");
+    }
+
+  } else {
+    die("Unable to open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      my $size = $conn->bytes_read();
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      my $expected = 5;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+
+    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 retr_fails_not_reg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw($setup->{home_dir});
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      if ($conn) {
+        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
+      }
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$setup->{home_dir}: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
 
-};
+    exit 0;
+  }
 
-sub new {
-  return shift()->SUPER::new(@_);
-}
+  # Stop server
+  server_stop($setup->{pid_file});
 
-sub list_tests {
-  return testsuite_get_runnable_tests($TESTS);
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_ok_raw_active {
+sub retr_fails_login_required {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
-  my $test_file = File::Spec->rel2abs($setup->{config_file});
-
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
     SystemLog => $setup->{log_file},
 
-    AuthUserFile => $setup->{auth_user_file},
-    AuthGroupFile => $setup->{auth_group_file},
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -131,24 +1388,26 @@ sub retr_ok_raw_active {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-      $client->login($setup->{user}, $setup->{passwd});
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
 
-      my $conn = $client->retr_raw($test_file);
-      unless ($conn) {
-        die("Failed to RETR: " . $client->response_code() . " " .
-          $client->response_msg());
+      my ($resp_code, $resp_msg);
+      eval { ($resp_code, $resp_msg) = $client->retr() };
+      unless ($@) {
+        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
       }
 
-      my $buf;
-      $conn->read($buf, 8192, 30);
-      eval { $conn->close() };
-
-      my ($resp_code, $resp_msg);
       $resp_code = $client->response_code();
       $resp_msg = $client->response_msg();
 
-      $self->assert_transfer_ok($resp_code, $resp_msg);
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Please login with USER and PASS";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -176,13 +1435,11 @@ sub retr_ok_raw_active {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_ok_raw_passive {
+sub retr_fails_no_path {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
-  my $test_file = File::Spec->rel2abs($setup->{config_file});
-
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
@@ -216,36 +1473,34 @@ sub retr_ok_raw_passive {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      # When run too quickly with the other tests, this test can fail.  So
-      # pause a little here.
-      sleep(1);
-
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->retr_raw($test_file);
-      unless ($conn) {
-        die("Failed to RETR: " . $client->response_code() . " " .
-          $client->response_msg());
+      my $conn = $client->retr_raw();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      if ($conn) {
+        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
       }
 
-      my $buf;
-      $conn->read($buf, 8192, 30);
-      eval { $conn->close() };
+      my $expected;
 
-      my ($resp_code, $resp_msg);
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $expected = "'RETR' not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
       $ex = $@;
     }
 
-    $wfh->print("done\n");
-    $wfh->flush();
+    print $wfh "done\n";
 
   } else {
     eval { server_wait($setup->{config_file}, $rfh) };
@@ -265,23 +1520,11 @@ sub retr_ok_raw_passive {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_ok_file {
+sub retr_fails_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Foo!\n";
-
-    unless (close($fh)) {
-      die("Unable to write $test_file: $!");
-    }
-
-  } else {
-    die("Unable to open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
@@ -315,23 +1558,31 @@ sub retr_ok_file {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->retr_raw($test_file);
-      unless ($conn) {
-        die("RETR failed: " . $client->response_code() . " " .
-          $client->response_msg());
+      $client->port();
+
+      my $test_file = 'foo/bar/baz';
+
+      my ($resp_code, $resp_msg);
+      eval { ($resp_code, $resp_msg) = $client->retr($test_file) };
+      unless ($@) {
+        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
       }
 
-      my $buf;
-      $conn->read($buf, 8192, 30);
-      my $size = $conn->bytes_read();
-      eval { $conn->close() };
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
 
-      my $expected = 6;
-      $self->assert($expected == $size,
-        test_msg("Expected $expected, got $size"));
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -359,7 +1610,7 @@ sub retr_ok_file {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_fails_not_reg {
+sub retr_fails_enoent_glob {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
@@ -397,24 +1648,28 @@ sub retr_fails_not_reg {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
+      $client->port();
 
-      my $conn = $client->retr_raw($setup->{home_dir});
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my $test_glob = '*foo';
 
-      if ($conn) {
+      my ($resp_code, $resp_msg);
+      eval { ($resp_code, $resp_msg) = $client->retr($test_glob) };
+      unless ($@) {
         die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
       }
 
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$setup->{home_dir}: Not a regular file";
+      $expected = "*foo: No such file or directory";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
@@ -444,16 +1699,29 @@ sub retr_fails_not_reg {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_fails_login_required {
+sub retr_fails_abs_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$test_file': $!");
+  }
+
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
     SystemLog => $setup->{log_file},
 
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -479,24 +1747,24 @@ sub retr_fails_login_required {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->retr() };
-      unless ($@) {
-        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      if ($conn) {
+        die("RETR succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
-
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 530;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "Please login with USER and PASS";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
@@ -526,11 +1794,27 @@ sub retr_fails_login_required {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_fails_no_path {
+sub retr_fails_abs_symlink_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$dst_path': $!");
+  }
+
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
@@ -539,6 +1823,8 @@ sub retr_fails_no_path {
     AuthUserFile => $setup->{auth_user_file},
     AuthGroupFile => $setup->{auth_group_file},
 
+    DefaultRoot => '~',
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -564,25 +1850,24 @@ sub retr_fails_no_path {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->retr_raw();
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
       if ($conn) {
-        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
+        die("RETR succeeded unexpectedly");
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 500;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "'RETR' not understood";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
@@ -591,7 +1876,8 @@ sub retr_fails_no_path {
       $ex = $@;
     }
 
-    print $wfh "done\n";
+    $wfh->print("done\n");
+    $wfh->flush();
 
   } else {
     eval { server_wait($setup->{config_file}, $rfh) };
@@ -611,11 +1897,30 @@ sub retr_fails_no_path {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_fails_enoent {
+sub retr_fails_rel_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
@@ -649,29 +1954,24 @@ sub retr_fails_enoent {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
-      $client->port();
-
-      my $test_file = 'foo/bar/baz';
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->retr($test_file) };
-      unless ($@) {
-        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      if ($conn) {
+        die("RETR succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
-
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$test_file: No such file or directory";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
@@ -701,11 +2001,30 @@ sub retr_fails_enoent {
   test_cleanup($setup->{log_file}, $ex);
 }
 
-sub retr_fails_enoent_glob {
+sub retr_fails_rel_symlink_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
   my $setup = test_setup($tmpdir, 'cmds');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   my $config = {
     PidFile => $setup->{pid_file},
     ScoreboardFile => $setup->{scoreboard_file},
@@ -714,6 +2033,8 @@ sub retr_fails_enoent_glob {
     AuthUserFile => $setup->{auth_user_file},
     AuthGroupFile => $setup->{auth_group_file},
 
+    DefaultRoot => '~',
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -739,28 +2060,24 @@ sub retr_fails_enoent_glob {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
-      $client->port();
-
-      my $test_glob = '*foo';
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->retr($test_glob) };
-      unless ($@) {
-        die("RETR succeeded unexpectedly ($resp_code $resp_msg)");
+      my $path = 'test.d/test.lnk';
+      my $conn = $client->retr_raw($path);
+      if ($conn) {
+        die("RETR succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
-
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "*foo: No such file or directory";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
@@ -839,7 +2156,7 @@ sub retr_fails_eperm {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
       $client->login($setup->{user}, $setup->{passwd});
       $client->port();
 
@@ -949,7 +2266,7 @@ sub retr_ok_dir_with_spaces {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->retr_raw('sub dir/test.txt');
@@ -1045,7 +2362,7 @@ sub retr_leading_whitespace {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->retr_raw(' test.txt');
@@ -1156,7 +2473,7 @@ sub retr_bug3496 {
       # pause a little here.
       sleep(1);
 
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
       $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->retr_raw($test_file);
@@ -1168,7 +2485,7 @@ sub retr_bug3496 {
       # Close the _control_ connection immediately
       $client->{ftp}->close();
 
-      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
       $client2->login($setup->{user}, $setup->{passwd});
     };
 
@@ -1237,7 +2554,7 @@ sub retr_2nd_transfer_terminates_1st_transfer_bug4010 {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client1->login($setup->{user}, $setup->{passwd});
 
       my $conn1 = $client1->retr_raw($test_file);
@@ -1252,7 +2569,7 @@ sub retr_2nd_transfer_terminates_1st_transfer_bug4010 {
 
       # Now, log in a second time with same user/passwd, do a directory
       # listing, then close the client.
-      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       $client2->login($setup->{user}, $setup->{passwd});
 
       my $conn2 = $client2->list_raw('foo.bar.baz');
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/RMD.pm b/tests/t/lib/ProFTPD/Tests/Commands/RMD.pm
index ad7796a..eaa8901 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/RMD.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/RMD.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -21,6 +22,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  rmd_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  rmd_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   rmd_fails_enoent => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -31,11 +52,51 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  rmd_fails_enotdir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_fails_enotempty => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_fails_eloop => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_fails_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_fails_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  rmd_fails_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rmd_fails_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   xrmd_ok => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  rmd_with_spaces => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
 };
 
 sub new {
@@ -49,49 +110,122 @@ sub list_tests {
 sub rmd_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->rmd($test_dir);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "RMD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
 
-  my $log_file = test_get_logfile();
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+sub rmd_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
   mkpath($sub_dir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  mkpath($test_dir);
+
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -100,7 +234,8 @@ sub rmd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -117,23 +252,24 @@ sub rmd_ok {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->rmd($sub_dir);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my $path = 'sub.d/test.d';
+      my ($resp_code, $resp_msg) = $client->rmd($path);
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "RMD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -142,7 +278,7 @@ sub rmd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -152,65 +288,54 @@ sub rmd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub rmd_fails_enoent {
+sub rmd_abs_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  mkpath($test_dir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -219,7 +344,8 @@ sub rmd_fails_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -236,30 +362,24 @@ sub rmd_fails_enoent {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->rmd($sub_dir) };
-      unless ($@) {
-        die("RMD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
-      }
+      my $path = 'sub.d/test.d';
+      my ($resp_code, $resp_msg) = $client->rmd($path);
+      $client->quit();
 
-      my $expected;
-
-      $expected = 550;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$sub_dir: No such file or directory";
+      $expected = "RMD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -268,7 +388,7 @@ sub rmd_fails_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -278,66 +398,56 @@ sub rmd_fails_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub rmd_fails_eperm {
+sub rmd_rel_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
   mkpath($sub_dir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  mkpath($test_dir);
+
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -346,7 +456,8 @@ sub rmd_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -363,39 +474,24 @@ sub rmd_fails_eperm {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      # Make it such that perms on the parent dir do not allow writes
-      my $perms = (stat($home_dir))[2];
-      unless (chmod(0550, $home_dir)) {
-        die("Failed to change perms on $home_dir: $!");
-      }
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->rmd($sub_dir) };
-      unless ($@) {
-        die("RMD succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
-      }
-
-      # Restore the perms
-      chmod($perms, $home_dir);
-
-      my $expected;
+      my $path = 'sub.d/test.d';
+      my ($resp_code, $resp_msg) = $client->rmd($path);
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "\/foo: (Operation not permitted|Permission denied)";
-      $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+      $expected = "RMD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -404,7 +500,7 @@ sub rmd_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -414,66 +510,140 @@ sub rmd_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub xrmd_ok {
+sub rmd_rel_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  mkpath($test_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir, $test_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
 
-  my $log_file = test_get_logfile();
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $test_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.d';
+      my ($resp_code, $resp_msg) = $client->rmd($path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "RMD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub rmd_fails_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -482,7 +652,8 @@ sub xrmd_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -499,23 +670,26 @@ sub xrmd_ok {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->xrmd($sub_dir);
+      eval { $client->rmd($test_dir) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "XRMD command successful";
+      $expected = "$test_dir: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -524,7 +698,7 @@ sub xrmd_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -534,18 +708,994 @@ sub xrmd_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
-  }
+sub rmd_fails_eperm {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # Make it such that perms on the parent dir do not allow writes
+      my $perms = (stat($setup->{home_dir}))[2];
+      unless (chmod(0550, $setup->{home_dir})) {
+        die("Failed to change perms on $setup->{home_dir}: $!");
+      }
+
+      eval { $client->rmd($test_dir) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      # Restore the perms
+      chmod($perms, $setup->{home_dir});
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\/test\.d: (Operation not permitted|Permission denied)";
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_enotdir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  if (open(my $fh, "> $test_dir")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_dir: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->rmd($test_dir) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "\/test\.d: Not a directory";
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_enotempty {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->rmd($test_dir) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_dir: Directory not empty";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_eloop {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+
+  unless (symlink($test_dir, $test_symlink)) {
+    die("Can't symlink '$test_symlink' to '$test_dir': $!");
+  }
+
+  unless (symlink($test_symlink, $test_dir)) {
+    die("Can't symlink '$test_dir' to '$test_symlink': $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->rmd($test_dir) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_dir: Not a directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_abs_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.d';
+      eval { $client->rmd($path) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_abs_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.d';
+      eval { $client->rmd($path) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_rel_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.d';
+      eval { $client->rmd($path) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_fails_rel_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d/test.d");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'sub.d/test.d';
+      eval { $client->rmd($path) };
+      unless ($@) {
+        die("RMD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 xrmd_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->xrmd($test_dir);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "XRMD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rmd_with_spaces {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo bar baz");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $dir = 'foo bar baz';
+      my ($resp_code, $resp_msg) = $client->rmd($dir);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "RMD command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir unexpectedly present"));
+    };
+    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);
 
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm b/tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm
index 6b5e68a..64d1838 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/RNFR.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -21,92 +22,828 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  rnfr_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnfr_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  rnfr_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnfr_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  rnfr_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnfr_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnfr_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnfr_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   rnfr_fails_login_required => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
-  rnfr_fails_enoent => {
-    order => ++$order,
-    test_class => [qw(forking)],
-  },
+  rnfr_fails_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnfr_during_xfer_bug3492 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  rnfr_limit_bug3698 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  rnfr_list_bad_cmd_sequence_bug3829 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub rnfr_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->rnfr($test_file);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_abs_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_abs_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  rnfr_during_xfer_bug3492 => {
-    order => ++$order,
-    test_class => [qw(bug forking)],
-  },
+  # 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: $!");
+  }
 
-  rnfr_limit_bug3698 => {
-    order => ++$order,
-    test_class => [qw(bug forking)],
-  },
+  my $ex;
 
-  rnfr_list_bad_cmd_sequence_bug3829 => {
-    order => ++$order,
-    test_class => [qw(bug forking)],
-  },
+  # 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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-};
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
 
-sub new {
-  return shift()->SUPER::new(@_);
-}
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-sub list_tests {
-  return testsuite_get_runnable_tests($TESTS);
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_ok {
+sub rnfr_rel_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
 
-  } else {
-    die("Can't open $test_file: $!");
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -115,7 +852,8 @@ sub rnfr_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -132,24 +870,21 @@ sub rnfr_ok {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->rnfr($test_file);
-
-      my $expected;
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
 
-      $expected = 350;
+      my $expected = 350;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "File or directory exists, ready for destination name";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -158,7 +893,7 @@ sub rnfr_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -168,18 +903,120 @@ sub rnfr_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
+sub rnfr_rel_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
   }
 
-  unlink($log_file);
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->rnfr($path);
+      $client->quit();
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 rnfr_fails_login_required {
@@ -316,48 +1153,17 @@ sub rnfr_fails_login_required {
 sub rnfr_fails_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -366,7 +1172,8 @@ sub rnfr_fails_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -383,31 +1190,26 @@ sub rnfr_fails_enoent {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->rnfr($test_file) };
+      eval { $client->rnfr($test_file) };
       unless ($@) {
         die("RNFR succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "$test_file: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -416,7 +1218,7 @@ sub rnfr_fails_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -426,18 +1228,10 @@ sub rnfr_fails_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub rnfr_during_xfer_bug3492 {
@@ -710,7 +1504,7 @@ sub rnfr_limit_bug3698 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      eval { $client->rnfr($test_file) };
+      eval { $client->rnfr('sub.d/foo') };
       unless ($@) {
         die("RNFR succeeded unexpectedly");
       }
@@ -722,11 +1516,11 @@ sub rnfr_limit_bug3698 {
 
       $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$test_file: Operation not permitted";
+      $expected = "sub.d/foo: Operation not permitted";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/RNTO.pm b/tests/t/lib/ProFTPD/Tests/Commands/RNTO.pm
index 9942701..4017076 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/RNTO.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/RNTO.pm
@@ -22,6 +22,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  rnto_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  rnto_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   rnto_fails_login_required => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -80,24 +90,12 @@ sub list_tests {
 sub rnto_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $src_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $src_file = File::Spec->rel2abs("$test_dir/test.txt");
   if (open(my $fh, "> $src_file")) {
     close($fh);
 
@@ -105,43 +103,140 @@ sub rnto_ok {
     die("Can't open $src_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
+  my $dst_file = File::Spec->rel2abs("$test_dir/bar.txt");
+
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  my $dst_file = File::Spec->rel2abs("$tmpdir/bar");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->rnfr($src_file);
+      ($resp_code, $resp_msg) = $client->rnto($dst_file);
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Rename successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(!-f $src_file,
+        test_msg("File $src_file exists unexpectedly"));
+      $self->assert(-f $dst_file,
+        test_msg("File $dst_file does not exist as expected"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub rnto_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $src_file = File::Spec->rel2abs("$test_dir/foo.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$test_dir/bar.txt");
+  my $dst_symlink = File::Spec->rel2abs("$test_dir/bar.lnk");
+
+  my $dst_path = $dst_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $dst_symlink)) {
+    die("Can't symlink $dst_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -150,7 +245,8 @@ sub rnto_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -167,25 +263,39 @@ sub rnto_ok {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      $client->login($user, $passwd);
+      my $src_path = 'test.d/foo.txt';
+      $client->rnfr($src_path);
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->rnfr($src_file);
-      ($resp_code, $resp_msg) = $client->rnto($dst_file);
-
-      my $expected;
+      $dst_path = 'test.d/bar.lnk';
+      my ($resp_code, $resp_msg) = $client->rnto($dst_path);
+      $client->quit();
 
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Rename successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $self->assert(!-f $src_file,
+        test_msg("File $src_file exists unexpectedly"));
+
+      # rename(2) works by DELETING the destination file, if it exists.
+      # This means that it does NOT follow a symlink link.
+
+      $self->assert(!-f $dst_file,
+        test_msg("File $dst_file exists unexpectedly"));
+
+      $self->assert(!-l $dst_symlink,
+        test_msg("Symlink $dst_symlink exists unexpectedly"));
+
+      $self->assert(-f $dst_symlink,
+        test_msg("File $dst_symlink does not exist as expected"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -194,7 +304,7 @@ sub rnto_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -204,18 +314,145 @@ sub rnto_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
+sub rnto_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $src_file = File::Spec->rel2abs("$test_dir/foo.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $src_file: $!");
   }
 
-  unlink($log_file);
+  my $dst_file = File::Spec->rel2abs("$test_dir/bar.txt");
+  my $dst_symlink = File::Spec->rel2abs("$test_dir/bar.lnk");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./bar.txt', './bar.lnk')) {
+    die("Can't symlink 'bar.lnk' to './bar.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $src_path = 'test.d/foo.txt';
+      $client->rnfr($src_path);
+
+      my $dst_path = 'test.d/bar.lnk';
+      my ($resp_code, $resp_msg) = $client->rnto($dst_path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Rename successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert(!-f $src_file,
+        test_msg("File $src_file exists unexpectedly"));
+
+      # rename(2) works by DELETING the destination file, if it exists.
+      # This means that it does NOT follow a symlink link.
+
+      $self->assert(!-f $dst_file,
+        test_msg("File $dst_file exists unexpectedly"));
+
+      $self->assert(!-l $dst_symlink,
+        test_msg("Symlink $dst_symlink exists unexpectedly"));
+
+      $self->assert(-f $dst_symlink,
+        test_msg("File $dst_symlink does not exist as expected"));
+    };
+    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 rnto_fails_login_required {
@@ -473,24 +710,12 @@ sub rnto_fails_no_rnfr {
 sub rnto_fails_enoent_no_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $src_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $src_file = File::Spec->rel2abs("$test_dir/src.txt");
   if (open(my $fh, "> $src_file")) {
     close($fh);
 
@@ -498,31 +723,25 @@ sub rnto_fails_enoent_no_file {
     die("Can't open $src_file: $!");
   }
 
-  my $dst_file = File::Spec->rel2abs("$tmpdir/bar");
+  my $dst_file = File::Spec->rel2abs("$test_dir/dst.txt");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -531,7 +750,8 @@ sub rnto_fails_enoent_no_file {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -548,35 +768,29 @@ sub rnto_fails_enoent_no_file {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->rnfr($src_file);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
+      my ($resp_code, $resp_msg) = $client->rnfr($src_file);
       unlink($src_file);
 
-      eval { ($resp_code, $resp_msg) = $client->rnto($dst_file) };
+      eval { $client->rnto($dst_file) };
       unless ($@) {
         die("RNTO succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Rename $dst_file: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -585,7 +799,7 @@ sub rnto_fails_enoent_no_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -595,41 +809,21 @@ sub rnto_fails_enoent_no_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub rnto_fails_enoent_no_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $src_file = File::Spec->rel2abs("$test_dir/src.txt");
   if (open(my $fh, "> $src_file")) {
     close($fh);
 
@@ -637,31 +831,25 @@ sub rnto_fails_enoent_no_dir {
     die("Can't open $src_file: $!");
   }
 
-  my $dst_file = File::Spec->rel2abs("$tmpdir/bar/baz");
+  my $dst_file = File::Spec->rel2abs("$test_dir/foo.d/dst.txt");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -670,7 +858,8 @@ sub rnto_fails_enoent_no_dir {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -687,33 +876,27 @@ sub rnto_fails_enoent_no_dir {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->rnfr($src_file);
-
-      eval { ($resp_code, $resp_msg) = $client->rnto($dst_file) };
+      $client->rnfr($src_file);
+      eval { $client->rnto($dst_file) };
       unless ($@) {
         die("RNTO succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Rename $dst_file: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -722,7 +905,7 @@ sub rnto_fails_enoent_no_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -732,18 +915,10 @@ sub rnto_fails_enoent_no_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub rnto_fails_eperm {
@@ -1178,24 +1353,9 @@ sub rnto_fails_device_full_bug3354 {
 sub rnto_symlink_bug3754 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
@@ -1218,8 +1378,8 @@ sub rnto_symlink_bug3754 {
     die("Can't chdir to $sub_dir: $!");
   }
 
-  unless (symlink('../foo', 'bar')) {
-    die("Can't symlink '../foo' to 'bar': $!");
+  unless (symlink('../test.txt', 'bar')) {
+    die("Can't symlink 'bar' to '../test.txt': $!");
   }
 
   unless (chdir($cwd)) {
@@ -1227,44 +1387,25 @@ sub rnto_symlink_bug3754 {
   }
 
   my $src_file = File::Spec->rel2abs("$sub_dir/bar");
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
   my $dst_file = File::Spec->rel2abs("$sub_dir/baz");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1273,9 +1414,10 @@ sub rnto_symlink_bug3754 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  if (open(my $fh, ">> $config_file")) {
+  if (open(my $fh, ">> $setup->{config_file}")) {
     print $fh <<EOC;
 <Directory ~>
   <Limit WRITE>
@@ -1290,11 +1432,11 @@ sub rnto_symlink_bug3754 {
 </Directory>
 EOC
     unless (close($fh)) {
-      die("Can't write $config_file: $!");
+      die("Can't write $setup->{config_file}: $!");
     }
 
   } else {
-    die("Can't open $config_file: $!");
+    die("Can't open $setup->{config_file}: $!");
   }
 
   # Open pipes, for use between the parent and child processes.  Specifically,
@@ -1312,23 +1454,21 @@ EOC
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->cwd('sub.d');
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->rnfr('bar');
+      my ($resp_code, $resp_msg) = $client->rnfr('bar');
       ($resp_code, $resp_msg) = $client->rnto('baz');
+      $client->quit();
 
-      my $expected;
-
-      $expected = 250;
+      my $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Rename successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $self->assert(-f $test_file,
         test_msg("File '$test_file' does not exist as expected"));
@@ -1339,7 +1479,6 @@ EOC
       $self->assert(-l $dst_file,
         test_msg("Symlink '$dst_file' does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1348,7 +1487,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1358,18 +1497,10 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/SIZE.pm b/tests/t/lib/ProFTPD/Tests/Commands/SIZE.pm
index a3eeeb5..9774d11 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/SIZE.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/SIZE.pm
@@ -22,16 +22,46 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  size_symlink_file => {
+  size_abs_symlink_file => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
-  size_symlink_dir => {
+  size_abs_symlink_file_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  size_rel_symlink_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  size_rel_symlink_file_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  size_abs_symlink_dir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  size_abs_symlink_dir_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  size_rel_symlink_dir => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  size_rel_symlink_dir_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   size_fails_ascii => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -60,23 +90,9 @@ sub list_tests {
 sub size_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
     print $fh "Hello, World!\n";
     close($fh);
@@ -85,29 +101,13 @@ sub size_ok {
     die("Can't create $test_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -116,7 +116,8 @@ sub size_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -133,25 +134,21 @@ sub size_ok {
   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($user, $passwd);
-
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->type('binary');
-    
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->size($test_file);
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->size($test_file);
+      $client->quit();
 
-      $expected = 213;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 14;
       $self->assert($expected == $resp_msg,
-        test_msg("Expected $expected, got $resp_msg"));
+        test_msg("Expected response message $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -160,7 +157,7 @@ sub size_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -170,41 +167,21 @@ sub size_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub size_symlink_file {
+sub size_abs_symlink_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     print $fh "Hello, World!\n";
     close($fh);
@@ -213,42 +190,128 @@ sub size_symlink_file {
     die("Can't create $test_file: $!");
   }
 
-  my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  unless (symlink('./test.txt', 'test.lnk')) {
-    die("Can't symlink './test.txt' to 'test.lnk': $!");
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
   }
 
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my ($resp_code, $resp_msg) = $client->size('sub.d/test.lnk');
+      $client->quit();
+
+      my $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = -s $test_file;
+      $self->assert($expected == $resp_msg,
+        test_msg("Expected response messsage $expected, got $resp_msg"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $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 size_abs_symlink_file_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    close($fh);
+
+  } else {
+    die("Can't create $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -257,7 +320,8 @@ sub size_symlink_file {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -274,15 +338,14 @@ sub size_symlink_file {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->type('binary');
-    
-      my ($resp_code, $resp_msg) = $client->size('test.lnk');
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->size('sub.d/test.lnk');
+      $client->quit();
 
-      $expected = 213;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
@@ -290,7 +353,6 @@ sub size_symlink_file {
       $self->assert($expected == $resp_msg,
         test_msg("Expected response messsage $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -299,7 +361,7 @@ sub size_symlink_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -309,79 +371,49 @@ sub size_symlink_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub size_symlink_dir {
+sub size_rel_symlink_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    close($fh);
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($test_dir);
+  } else {
+    die("Can't create $test_file: $!");
+  }
 
   my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
+  unless (chdir($sub_dir)) {
     die("Can't chdir to $tmpdir: $!");
   }
 
-  unless (symlink('./test.d', 'test.lnk')) {
-    die("Can't symlink './test.d' to 'test.lnk': $!");
+  unless (symlink('./test.txt', 'test.lnk')) {
+    die("Can't symlink './test.txt' to 'test.lnk': $!");
   }
 
   unless (chdir($cwd)) {
     die("Can't chdir to $cwd: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -390,7 +422,8 @@ sub size_symlink_dir {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -407,29 +440,21 @@ sub size_symlink_dir {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->type('binary');
-  
-      eval { $client->size('test.lnk') };
-      unless ($@) {
-        die("SIZE test.lnk succeeded unexpectedly");
-      }
- 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->size('sub.d/test.lnk');
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "test.lnk: not a regular file";
+      $expected = -s $test_file;
       $self->assert($expected == $resp_msg,
         test_msg("Expected response messsage $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -438,7 +463,7 @@ sub size_symlink_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -448,64 +473,51 @@ sub size_symlink_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub size_fails_ascii {
+sub size_rel_symlink_file_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    close($fh);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  } else {
+    die("Can't create $test_file: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  unless (symlink('./test.txt', 'test.lnk')) {
+    die("Can't symlink './test.txt' to 'test.lnk': $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -514,7 +526,8 @@ sub size_fails_ascii {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -531,32 +544,21 @@ sub size_fails_ascii {
   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($user, $passwd);
-
-      $client->type('ascii');
-    
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->size($test_file) };
-      unless ($@) {
-        die("SIZE succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
-      }
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
-      my $expected;
+      my ($resp_code, $resp_msg) = $client->size('sub.d/test.lnk');
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "SIZE not allowed in ASCII mode";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $expected = -s $test_file;
+      $self->assert($expected == $resp_msg,
+        test_msg("Expected response messsage $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -565,7 +567,7 @@ sub size_fails_ascii {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -575,64 +577,41 @@ sub size_fails_ascii {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub size_fails_enoent {
+sub size_abs_symlink_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -641,7 +620,8 @@ sub size_fails_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -658,31 +638,28 @@ sub size_fails_enoent {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->type('binary');
-    
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->size($test_file) };
-      unless ($@) {
-        die("SIZE succeeded unexpectedly");
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $path = 'sub.d/test.lnk';
+      eval { $client->size($path) };
+      unless ($@) {
+        die("SIZE test.lnk succeeded unexpectedly");
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$test_file: No such file or directory";
+      $expected = "$path: not a regular file";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response messsage $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -691,7 +668,7 @@ sub size_fails_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -701,70 +678,44 @@ sub size_fails_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub size_fails_eperm {
+sub size_abs_symlink_dir_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
 
-  } else {
-    die("Can't create $test_file: $!");
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -773,7 +724,8 @@ sub size_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -790,39 +742,28 @@ sub size_fails_eperm {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->type('binary');
-    
-      # Make it such that perms on the home dir do not allow reads
-      my $perms = (stat($home_dir))[2];
-      unless (chmod(0220, $home_dir)) {
-        die("Failed to change perms on $home_dir: $!");
-      }
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->mdtm($test_file) };
+      my $path = 'sub.d/test.lnk';
+      eval { $client->size($path) };
       unless ($@) {
-        die("MDTM succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+        die("SIZE test.lnk succeeded unexpectedly");
       }
 
-      chmod($perms, $home_dir);
-
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$test_file: Permission denied";
+      $expected = "$path: not a regular file";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response messsage $expected, got $resp_msg"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -831,7 +772,7 @@ sub size_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -841,18 +782,493 @@ sub size_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub size_rel_symlink_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', 'test.lnk')) {
+    die("Can't symlink './test.d' to 'test.lnk': $!");
+  }
 
-    die($ex);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
 
-  unlink($log_file);
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'sub.d/test.lnk';
+      eval { $client->size($path) };
+      unless ($@) {
+        die("SIZE test.lnk succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response messsage $expected, got $resp_msg"));
+    };
+    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 size_rel_symlink_dir_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', 'test.lnk')) {
+    die("Can't symlink './test.d' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'sub.d/test.lnk';
+      eval { $client->size($path) };
+      unless ($@) {
+        die("SIZE test.lnk succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$path: not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response messsage $expected, got $resp_msg"));
+    };
+    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 size_fails_ascii {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('ascii');
+
+      eval { $client->size($test_file) };
+      unless ($@) {
+        die("SIZE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "SIZE not allowed in ASCII mode";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 size_fails_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      eval { $client->size($test_file) };
+      unless ($@) {
+        die("SIZE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 size_fails_eperm {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't create $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      # Make it such that perms on the home dir do not allow reads
+      my $perms = (stat($setup->{home_dir}))[2];
+      unless (chmod(0220, $setup->{home_dir})) {
+        die("Failed to change perms on $setup->{home_dir}: $!");
+      }
+
+      eval { $client->size($test_file) };
+      unless ($@) {
+        die("SIZE succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      chmod($perms, $setup->{home_dir});
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$test_file: Permission denied";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/STAT.pm b/tests/t/lib/ProFTPD/Tests/Commands/STAT.pm
index 6b3dabe..b4370e0 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/STAT.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/STAT.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -26,6 +27,31 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  stat_dir_with_files_bug3990 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  stat_symlink_showsymlinks_off => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stat_symlink_showsymlinks_on => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stat_symlink_showsymlinks_off_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  stat_symlink_showsymlinks_on_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
   stat_system => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -54,47 +80,109 @@ sub list_tests {
 sub stat_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
 
-  my $log_file = test_get_logfile();
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $test_file = File::Spec->rel2abs($config_file);
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  # 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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat('cmds.conf');
+
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+      my $resp_msg = $resp_msgs->[1];
+      $client->quit();
+
+      my $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = '^(\s+)?\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub stat_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -103,7 +191,8 @@ sub stat_file {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -120,25 +209,23 @@ sub stat_file {
   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($user, $passwd);
-      $client->stat('cmds.conf');
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat('test.d');
 
       my $resp_code = $client->response_code();
       my $resp_msgs = $client->response_msgs();
       my $resp_msg = $resp_msgs->[1];
+      $client->quit();
 
-      my $expected;
-
-      $expected = 211;
+      my $expected = 212;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = '^(\s+)?\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -147,7 +234,7 @@ sub stat_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -157,65 +244,59 @@ sub stat_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stat_dir {
+sub stat_dir_with_files_bug3990 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  for (my $i = 0; $i < 5; $i++) {
+    my $test_file = File::Spec->rel2abs("$test_dir/file$i.dat");
+    if (open(my $fh, "> $test_file")) {
+      close($fh);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+    } else {
+      die("Can't open $test_file: $!");
+    }
+  }
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($test_dir);
+  for (my $i = 0; $i < 5; $i++) {
+    my $test_file = File::Spec->rel2abs("$test_dir/.hidden$i.dat");
+    if (open(my $fh, "> $test_file")) {
+      close($fh);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+    } else {
+      die("Can't open $test_file: $!");
+    }
+  }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -224,7 +305,8 @@ sub stat_dir {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -241,25 +323,28 @@ sub stat_dir {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
       $client->stat('test.d');
 
       my $resp_code = $client->response_code();
       my $resp_msgs = $client->response_msgs();
+      my $resp_nmsgs = scalar(@$resp_msgs);
       my $resp_msg = $resp_msgs->[1];
+      $client->quit();
 
-      my $expected;
-
-      $expected = 211;
+      my $expected = 212;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = '^(\s+)?\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $expected = 14;
+      $self->assert($expected == $resp_nmsgs,
+        test_msg("Expected $expected response lines, got $resp_nmsgs"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -268,7 +353,7 @@ sub stat_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -278,62 +363,65 @@ sub stat_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stat_system {
+sub stat_symlink_showsymlinks_off {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    ShowSymlinks => 'off',
 
     IfModules => {
       'mod_delay.c' => {
@@ -342,7 +430,8 @@ sub stat_system {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -359,20 +448,28 @@ sub stat_system {
   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($user, $passwd);
-      $client->stat();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat('test.d/test.lnk');
 
       my $resp_code = $client->response_code();
       my $resp_msgs = $client->response_msgs();
+      my $resp_msg = $resp_msgs->[1];
+      $client->quit();
 
-      my $expected;
+      if ($ENV{TEST_VERBOSE}) {
+        use Data::Dumper;
+        print STDERR "# Response:\n", Dumper($resp_msgs), "\n";
+      }
 
-      $expected = 211;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-    };
+        test_msg("Expected response code $expected, got $resp_code"));
 
+      $expected = '^(\s+)?\-\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -381,7 +478,7 @@ sub stat_system {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -391,62 +488,195 @@ sub stat_system {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub stat_symlink_showsymlinks_on {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
 
-    die($ex);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
 
-  unlink($log_file);
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat('test.d/test.lnk');
+
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+      my $resp_msg = $resp_msgs->[1];
+      $client->quit();
+
+      if ($ENV{TEST_VERBOSE}) {
+        use Data::Dumper;
+        print STDERR "# Response:\n", Dumper($resp_msgs), "\n";
+      }
+
+      my $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = '^(\s+)?l\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $expected = '.*?test\.d\/test\.lnk \-> test\.d\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 stat_enoent {
+sub stat_symlink_showsymlinks_off_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+    ShowSymlinks => 'off',
 
     IfModules => {
       'mod_delay.c' => {
@@ -455,7 +685,8 @@ sub stat_enoent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -472,30 +703,158 @@ sub stat_enoent {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat('test.d/test.lnk');
 
-      my $enoent_file = 'foo.bar.baz.d';
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+      my $resp_msg = $resp_msgs->[1];
+      $client->quit();
 
-      eval { $client->stat($enoent_file) };
-      unless ($@) {
-        die("STAT succeeded unexpectedly");
+      if ($ENV{TEST_VERBOSE}) {
+        use Data::Dumper;
+        print STDERR "# Response:\n", Dumper($resp_msgs), "\n";
       }
 
+      my $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = '^(\s+)?\-\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 stat_symlink_showsymlinks_on_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+    ShowSymlinks => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat('test.d/test.lnk');
+
       my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my $resp_msgs = $client->response_msgs();
+      my $resp_msg = $resp_msgs->[1];
+      $client->quit();
 
-      my $expected;
+      if ($ENV{TEST_VERBOSE}) {
+        use Data::Dumper;
+        print STDERR "# Response:\n", Dumper($resp_msgs), "\n";
+      }
 
-      $expected = 450;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$enoent_file: No such file or directory";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-    };
+      $expected = '^(\s+)?l\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      $expected = '.*?test\.d\/test\.lnk \-> \/test\.d\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -504,7 +863,7 @@ sub stat_enoent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -514,64 +873,182 @@ sub stat_enoent {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub stat_system {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->stat();
+
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+      $client->quit();
+
+      my $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
 
-    die($ex);
+    exit 0;
   }
 
-  unlink($log_file);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stat_listoptions_bug3295 {
+sub stat_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $log_file = test_get_logfile();
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
 
-  my $test_file = File::Spec->rel2abs($config_file);
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # 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: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $enoent_file = 'foo.bar.baz.d';
+
+      eval { $client->stat($enoent_file) };
+      unless ($@) {
+        die("STAT succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 450;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$enoent_file: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub stat_listoptions_bug3295 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     ListOptions => '-1',
 
@@ -582,7 +1059,8 @@ sub stat_listoptions_bug3295 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -599,22 +1077,20 @@ sub stat_listoptions_bug3295 {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my ($resp_code, $resp_msg) = $client->stat('cmds.conf');
+      $client->quit();
 
-      my $expected;
-
-      $expected = 211;
+      my $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = '^(\s+)?\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -623,7 +1099,7 @@ sub stat_listoptions_bug3295 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -633,18 +1109,10 @@ sub stat_listoptions_bug3295 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/STOR.pm b/tests/t/lib/ProFTPD/Tests/Commands/STOR.pm
index e91f1f1..ef9fac9 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/STOR.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/STOR.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -26,11 +27,46 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  stor_ok_file => {
+  stor_ok_binary_file => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  stor_ok_ascii_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stor_ok_ascii_file_bug4237 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  stor_ok_ascii_file_bug4277 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  stor_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stor_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  stor_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stor_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   stor_fails_not_reg => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -51,6 +87,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  stor_fails_abs_symlink_dir_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stor_fails_abs_symlink_dir_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  stor_fails_rel_symlink_dir_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  stor_fails_rel_symlink_dir_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   stor_leading_whitespace => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -74,45 +130,1204 @@ sub list_tests {
 sub stor_ok_raw_active {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+    };
+    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 stor_ok_raw_passive {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+    };
+    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 stor_ok_binary_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\r\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+    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 stor_ok_ascii_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
 
-  my $log_file = test_get_logfile();
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  # 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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('ascii');
+
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\r\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+
+      # Take back one byte for whose CR this ASCII file is.
+      my $size = length($buf)-1;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+    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 stor_ok_ascii_file_bug4237 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('ascii');
+
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = ("\r\r" x 32768) . "\r";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      unlink($test_file);
+
+      $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = ("\r\n" x 32768);
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      $expected = -s $test_file;
+
+      # Take back one byte for per CR
+      my $size = length($buf) / 2;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      $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 stor_ok_ascii_file_bug4277 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  use Digest::MD5;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('ascii');
+
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = ("\r\r" x 32768) . "\r";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      my $digest;
+      if (open(my $fh, "< $test_file")) {
+        my $ctx = Digest::MD5->new();
+        $ctx->addfile($fh);
+        $digest = lc($ctx->hexdigest);
+
+      } else {
+        die("Can't read $test_file: $!");
+      }
+
+      $expected = '8ed41c85d5ae54141ac1010b34213ba6';
+      $self->assert($expected eq $digest,
+        test_msg("Expected MD5 $expected, got $digest"));
+
+      unlink($test_file);
+
+      $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = ("\r\n" x 32768);
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      $expected = -s $test_file;
+
+      # Take back one byte for per CR
+      $size = length($buf) / 2;
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      $digest = '';
+      if (open(my $fh, "< $test_file")) {
+        my $ctx = Digest::MD5->new();
+        $ctx->addfile($fh);
+        $digest = lc($ctx->hexdigest);
+
+      } else {
+        die("Can't read $test_file: $!");
+      }
+
+      $expected = '07f9f47f9ce9f8e6de19c6518dba04ca';
+      $self->assert($expected eq $digest,
+        test_msg("Expected MD5 $expected, got $digest"));
+
+      $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 stor_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.dat");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\r\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+    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 stor_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.dat");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\r\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+    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 stor_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.dat");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.dat', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.dat': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\r\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+    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 stor_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.dat");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.dat', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.dat': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\r\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+    };
+    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 stor_fails_not_reg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw($setup->{home_dir});
+      if ($conn) {
+        die("STOR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$setup->{home_dir}: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+    $wfh->print("done\n");
+    $wfh->flush();
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
 
-  my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub stor_fails_login_required {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -121,7 +1336,8 @@ sub stor_ok_raw_active {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -138,26 +1354,25 @@ sub stor_ok_raw_active {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-      $client->login($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
 
-      my $conn = $client->stor_raw('bar');
-      unless ($conn) {
-        die("Failed to STOR: " . $client->response_code() . " " .
-          $client->response_msg());
+      eval { $client->stor($setup->{config_file}, '/dev/null') };
+      unless ($@) {
+        die("STOR succeeded unexpectedly");
       }
 
-      my $buf = "Foo!\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
-
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-
       $client->quit();
-    };
 
+      my $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Please login with USER and PASS";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -166,7 +1381,7 @@ sub stor_ok_raw_active {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -176,62 +1391,24 @@ sub stor_ok_raw_active {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stor_ok_raw_passive {
+sub stor_fails_no_path {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -240,7 +1417,8 @@ sub stor_ok_raw_passive {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -257,36 +1435,34 @@ sub stor_ok_raw_passive {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-
-      $client->login($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->stor_raw('bar');
-      unless ($conn) {
-        die("Failed to STOR: " . $client->response_code() . " " .
-          $client->response_msg());
+      eval { $client->stor($setup->{config_file}, '') };
+      unless ($@) {
+        die("STOR succeeded unexpectedly");
       }
 
-      my $buf = "Foo!\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
-
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-
       $client->quit();
-    };
 
+      my $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "'STOR' not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
     if ($@) {
       $ex = $@;
     }
 
-    $wfh->print("done\n");
-    $wfh->flush();
+    print $wfh "done\n";
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -296,64 +1472,37 @@ sub stor_ok_raw_passive {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stor_ok_file {
+sub stor_fails_eperm {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  } else {
+    die("Can't open $src_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/bar");
+  my $dst_file = File::Spec->rel2abs("$sub_dir/bar.txt");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -362,7 +1511,8 @@ sub stor_ok_file {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -379,32 +1529,29 @@ sub stor_ok_file {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->port();
 
-      $client->login($user, $passwd);
+      chmod(0660, $sub_dir);
 
-      my $conn = $client->stor_raw('bar');
-      unless ($conn) {
-        die("STOR failed: " . $client->response_code() . " " .
-          $client->response_msg());
+      eval { $client->stor($src_file, $dst_file) };
+      unless ($@) {
+        die("STOR succeeded unexpectedly");
       }
 
-      my $buf = "Foo!\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
-
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-
       $client->quit();
 
-      my $expected = -s $test_file;
-      my $size = length($buf);
-      $self->assert($expected == $size,
-        test_msg("Expected $expected, got $size"));
-    };
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
+      $expected = "$dst_file: Permission denied";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -413,7 +1560,7 @@ sub stor_ok_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -423,63 +1570,50 @@ sub stor_ok_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stor_fails_not_reg {
+sub stor_fails_abs_symlink_dir_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.d/test.dat");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    AllowOverwrite => 'on',
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -488,7 +1622,8 @@ sub stor_fails_not_reg {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -505,32 +1640,28 @@ sub stor_fails_not_reg {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-
-      my $conn = $client->stor_raw($home_dir);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
       if ($conn) {
-        die("STOR succeeded unexpectedly ($resp_code $resp_msg)");
+        die("STOR succeeded unexpectedly");
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$home_dir: Not a regular file";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -539,7 +1670,7 @@ sub stor_fails_not_reg {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -549,34 +1680,52 @@ sub stor_fails_not_reg {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stor_fails_login_required {
+sub stor_fails_abs_symlink_dir_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.d/test.dat");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
 
-  my $log_file = test_get_logfile();
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -585,7 +1734,8 @@ sub stor_fails_login_required {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -602,29 +1752,28 @@ sub stor_fails_login_required {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->stor($config_file, '/dev/null') };
-      unless ($@) {
-        die("STOR succeeded unexpectedly ($resp_code $resp_msg)");
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      if ($conn) {
+        die("STOR succeeded unexpectedly");
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 530;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "Please login with USER and PASS";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -633,7 +1782,7 @@ sub stor_fails_login_required {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -643,62 +1792,65 @@ sub stor_fails_login_required {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stor_fails_no_path {
+sub stor_fails_rel_symlink_dir_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.d/test.dat");
 
-  my $log_file = test_get_logfile();
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.d/test.dat', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d/test.dat': $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -707,7 +1859,8 @@ sub stor_fails_no_path {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -724,40 +1877,37 @@ sub stor_fails_no_path {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-
-      eval { ($resp_code, $resp_msg) = $client->stor($config_file, '') };
-      unless ($@) {
-        die("STOR succeeded unexpectedly ($resp_code $resp_msg)");
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      if ($conn) {
+        die("STOR succeeded unexpectedly");
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 500;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "'STOR' not understood";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
 
-    print $wfh "done\n";
+    $wfh->print("done\n");
+    $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -767,75 +1917,67 @@ sub stor_fails_no_path {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub stor_fails_eperm {
+sub stor_fails_rel_symlink_dir_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.d/test.dat");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
-  my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
 
-  } else {
-    die("Can't open $src_file: $!");
+  unless (symlink('./test.d/test.dat', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d/test.dat': $!");
   }
 
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo/bar.txt");
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -844,7 +1986,8 @@ sub stor_fails_eperm {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -861,34 +2004,28 @@ sub stor_fails_eperm {
   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($user, $passwd);
-      $client->port();
-
-      chmod(0660, $sub_dir);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
 
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->stor($src_file, $dst_file) };
-      unless ($@) {
-        die("STOR succeeded unexpectedly ($resp_code $resp_msg)");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
+      my $path = 'sub.d/test.lnk';
+      my $conn = $client->stor_raw($path);
+      if ($conn) {
+        die("STOR succeeded unexpectedly");
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      $expected = 550;
+      my $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$dst_file: Permission denied";
+      $expected = "$path: No such file or directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -897,7 +2034,7 @@ sub stor_fails_eperm {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -907,64 +2044,26 @@ sub stor_fails_eperm {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub stor_leading_whitespace {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/ test.txt");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -973,7 +2072,8 @@ sub stor_leading_whitespace {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -990,9 +2090,8 @@ sub stor_leading_whitespace {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->stor_raw(' test.txt');
       unless ($conn) {
@@ -1007,14 +2106,12 @@ sub stor_leading_whitespace {
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
       $self->assert_transfer_ok($resp_code, $resp_msg);
-
       $client->quit();
 
       unless (-f $test_file) {
         die("File $test_file does not exist as expected");
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1023,7 +2120,7 @@ sub stor_leading_whitespace {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1033,64 +2130,26 @@ sub stor_leading_whitespace {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub stor_multiple_periods {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/multi...dot...file.txt");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1099,7 +2158,8 @@ sub stor_multiple_periods {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1116,9 +2176,8 @@ sub stor_multiple_periods {
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->stor_raw('multi...dot...file.txt');
       unless ($conn) {
@@ -1140,7 +2199,6 @@ sub stor_multiple_periods {
         die("File $test_file does not exist as expected");
       }
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -1149,7 +2207,7 @@ sub stor_multiple_periods {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1159,18 +2217,10 @@ sub stor_multiple_periods {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/STOU.pm b/tests/t/lib/ProFTPD/Tests/Commands/STOU.pm
index 5bafe8a..62010bd 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/STOU.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/STOU.pm
@@ -31,6 +31,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  stou_file_umask_bug4223 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  stou_file_umask_chrooted_bug4223 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   stou_fails_login_required => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -118,7 +128,7 @@ sub stou_ok_raw_active {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
       $client->login($user, $passwd);
 
       my $conn = $client->stou_raw('bar');
@@ -246,7 +256,7 @@ sub stou_ok_raw_passive {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
 
       $client->login($user, $passwd);
 
@@ -446,6 +456,199 @@ sub stou_ok_file {
   unlink($log_file);
 }
 
+sub stou_file_umask_bug4223 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stou_raw('bar');
+      unless ($conn) {
+        die("STOU failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $uniq_file = $client->response_uniq();
+      $self->assert($uniq_file, test_msg("Expected non-null unique file"));
+
+      my $test_file = File::Spec->rel2abs("$setup->{home_dir}/$uniq_file");
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      my $perms = sprintf("%04o", ((stat($test_file))[2] & 07777));
+      $expected = '0644';
+      $self->assert($expected eq $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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 stou_file_umask_chrooted_bug4223 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stou_raw('/bar');
+      unless ($conn) {
+        die("STOU failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo!\n";
+      $conn->write($buf, length($buf), 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      my $uniq_file = $client->response_uniq();
+      $self->assert($uniq_file, test_msg("Expected non-null unique file"));
+
+      my $test_file = File::Spec->rel2abs("$setup->{home_dir}/$uniq_file");
+
+      my $expected = -s $test_file;
+      my $size = length($buf);
+      $self->assert($expected == $size,
+        test_msg("Expected size $expected, got $size"));
+
+      my $perms = sprintf("%04o", ((stat($test_file))[2] & 07777));
+      $expected = '0644';
+      $self->assert($expected eq $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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 stou_fails_login_required {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
diff --git a/tests/t/lib/ProFTPD/Tests/Commands/TYPE.pm b/tests/t/lib/ProFTPD/Tests/Commands/TYPE.pm
index cefc626..da47f7e 100644
--- a/tests/t/lib/ProFTPD/Tests/Commands/TYPE.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/TYPE.pm
@@ -43,17 +43,12 @@ sub list_tests {
 sub type_ascii_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -62,7 +57,8 @@ sub type_ascii_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -80,21 +76,17 @@ sub type_ascii_ok {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my ($resp_code, $resp_msg) = $client->type('ascii');
+      $client->quit();
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->type('ascii');
-
-      my $expected;
-
-      $expected = 200;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Type set to A";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -103,7 +95,7 @@ sub type_ascii_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -113,34 +105,21 @@ sub type_ascii_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub type_binary_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -149,7 +128,8 @@ sub type_binary_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -167,21 +147,17 @@ sub type_binary_ok {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my ($resp_code, $resp_msg) = $client->type('binary');
+      $client->quit();
 
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->type('binary');
-
-      my $expected;
-
-      $expected = 200;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Type set to I";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -190,7 +166,7 @@ sub type_binary_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -200,34 +176,21 @@ sub type_binary_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub type_other_fails {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'cmds');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -236,7 +199,8 @@ sub type_other_fails {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -254,26 +218,21 @@ sub type_other_fails {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-      eval { ($resp_code, $resp_msg) = $client->type('other') };
+      eval { $client->type('other') };
       unless ($@) {
         die("TYPE succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
-      my $expected;
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
-      $expected = 504;
+      my $expected = 504;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "TYPE not implemented for 'other' parameter";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -284,7 +243,7 @@ sub type_other_fails {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -294,18 +253,10 @@ sub type_other_fails {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/AllowForeignAddress.pm b/tests/t/lib/ProFTPD/Tests/Config/AllowForeignAddress.pm
index 46fd8b6..6786fe9 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/AllowForeignAddress.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/AllowForeignAddress.pm
@@ -26,6 +26,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  fxp_allowed_2gb => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   # tls_fxp_allowed
 };
 
@@ -40,38 +45,7 @@ sub list_tests {
 sub fxp_denied {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'config');
 
   my $src_file = File::Spec->rel2abs("$tmpdir/src.txt");
   if (open(my $fh, "> $src_file")) {
@@ -88,12 +62,12 @@ sub fxp_denied {
   my $dst_file = File::Spec->rel2abs("$tmpdir/dst.txt");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     AllowForeignAddress => 'off',
 
@@ -104,7 +78,8 @@ sub fxp_denied {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -121,11 +96,11 @@ sub fxp_denied {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-      $client1->login($user, $passwd);
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client1->login($setup->{user}, $setup->{passwd});
 
-      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client2->login($user, $passwd);
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client2->login($setup->{user}, $setup->{passwd});
 
       # Get the PASV address from the first connection, and give it
       # to the second connection as a PORT command.
@@ -164,7 +139,6 @@ sub fxp_denied {
       $self->assert(!-f $dst_file,
         test_msg("File $dst_file exists unexpectedly"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -173,7 +147,7 @@ sub fxp_denied {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -183,12 +157,11 @@ sub fxp_denied {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
   eval {
-    if (open(my $fh, "< $log_file")) {
+    if (open(my $fh, "< $setup->{log_file}")) {
       my $ok = 0;
 
       while (my $line = <$fh>) {
@@ -205,62 +178,162 @@ sub fxp_denied {
       $self->assert($ok, test_msg("Did not see expected log messages"));
 
     } else {
-      die("Can't read $log_file: $!");
+      die("Can't read $setup->{log_file}: $!");
     }
   };
   if ($@) {
     $ex = $@;
   }
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub fxp_allowed {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $src_file = File::Spec->rel2abs("$tmpdir/src.txt");
+  if (open(my $fh, "> $src_file")) {
+    print $fh "Hello, FXP World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
 
-  my $config_file = "$tmpdir/cmds.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/cmds.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/cmds.scoreboard");
+  } else {
+    die("Can't open $src_file: $!");
+  }
 
-  my $log_file = test_get_logfile();
+  my $dst_file = File::Spec->rel2abs("$tmpdir/dst.txt");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    AllowForeignAddress => 'on',
+
+    IfModules => {
+      '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client1->login($setup->{user}, $setup->{passwd});
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client2->login($setup->{user}, $setup->{passwd});
+
+      # Get the PASV address from the first connection, and give it
+      # to the second connection as a PORT command.
+      my ($resp_code, $resp_msg) = $client1->pasv();
+
+      my $expected = 227;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = '^Entering Passive Mode \(\d+,\d+,\d+,\d+,\d+,\d+\)';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # This will actually work, since both our connections are
+      # from 127.0.0.1, which means we shouldn't run afoul of the
+      # AllowForeignAddress limit.
+      $resp_msg =~ /'^Entering Passive Mode \((\d+,\d+,\d+,\d+,\d+,\d+\))/;
+      my $port_addr = $1;
+
+      ($resp_code, $resp_msg) = $client2->port($port_addr);
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'PORT command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $tmpfile = 'tmpfile.bin';
+      ($resp_code, $resp_msg) = $client1->stor($src_file, $tmpfile);
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client2->retr($tmpfile, $dst_file);
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client1->quit();
+      $client2->quit();
+
+      $self->assert(-f $dst_file,
+        test_msg("File $dst_file does not exist as expected"));
+
+      my $dst_size = -s $dst_file;
+      my $expected = -s $src_file;
+      $self->assert($expected == $dst_size,
+        test_msg("Expected file size $expected, got $dst_size"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub fxp_allowed_2gb {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  # See:
+  #  http://serverfault.com/questions/660372/proftpd-1-34-and-files-size-2-gb
 
   my $src_file = File::Spec->rel2abs("$tmpdir/src.txt");
   if (open(my $fh, "> $src_file")) {
-    print $fh "Hello, FXP World!\n";
+    # Seek to the 2GB limit, then fill the rest with 'A'
+    unless (seek($fh, (2 ** 31), 0)) {
+       die("Can't seek to 2GB length: $!");
+    }
+
+    print $fh "A" x 24;
 
     unless (close($fh)) {
       die("Can't write $src_file: $!");
@@ -273,12 +346,12 @@ sub fxp_allowed {
   my $dst_file = File::Spec->rel2abs("$tmpdir/dst.txt");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     AllowForeignAddress => 'on',
 
@@ -289,7 +362,8 @@ sub fxp_allowed {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -306,11 +380,11 @@ sub fxp_allowed {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-      $client1->login($user, $passwd);
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1, 1);
+      $client1->login($setup->{user}, $setup->{passwd});
 
-      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client2->login($user, $passwd);
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client2->login($setup->{user}, $setup->{passwd});
 
       # Get the PASV address from the first connection, and give it
       # to the second connection as a PORT command.
@@ -352,8 +426,12 @@ sub fxp_allowed {
 
       $self->assert(-f $dst_file,
         test_msg("File $dst_file does not exist as expected"));
-    };
 
+      my $dst_size = -s $dst_file;
+      my $expected = -s $src_file;
+      $self->assert($expected == $dst_size,
+        test_msg("Expected file size $expected, got $dst_size"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -362,7 +440,7 @@ sub fxp_allowed {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh, 600) };
     if ($@) {
       warn($@);
       exit 1;
@@ -372,18 +450,10 @@ sub fxp_allowed {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/AnonAllowRobots.pm b/tests/t/lib/ProFTPD/Tests/Config/AnonAllowRobots.pm
new file mode 100644
index 0000000..90612ed
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/AnonAllowRobots.pm
@@ -0,0 +1,583 @@
+package ProFTPD::Tests::Config::AnonAllowRobots;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  anon_allow_robots_on_enoent => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  anon_allow_robots_on => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  anon_allow_robots_off => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  anon_allow_robots_off_existing_file => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  anon_allow_robots_off_non_anon_login => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub anon_allow_robots_on_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my ($user, $group) = config_get_identity();
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => $user,
+        Group => $group,
+        UserAlias => "anonymous $user",
+        RequireValidShell => 'off',
+
+        AnonAllowRobots => 'on',
+      },
+    },
+  };
+
+  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, 0, 1);
+      $client->login($user, 'ftp at nospam.org');
+
+      my $conn = $client->retr_raw('/robots.txt');
+      if ($conn) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "/robots.txt: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 anon_allow_robots_on {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my ($user, $group) = config_get_identity();
+
+  my $robots_data = "User-agent: *\nAllow:\n";
+  my $robots_file = File::Spec->rel2abs("$setup->{home_dir}/robots.txt");
+  if (open(my $fh, "> $robots_file")) {
+    print $fh $robots_data;
+    unless (close($fh)) {
+      die("Can't write $robots_file: $!");
+    }
+
+  } else {
+    die("Can't open $robots_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => $user,
+        Group => $group,
+        UserAlias => "anonymous $user",
+        RequireValidShell => 'off',
+
+        AnonAllowRobots => 'on',
+      },
+    },
+  };
+
+  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, 0, 1);
+      $client->login($user, 'ftp at nospam.org');
+      $client->type('binary');
+
+      my $conn = $client->retr_raw('/robots.txt');
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 15);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $self->assert($buf eq $robots_data,
+        test_msg("Did not receive expected robots.txt data"));
+    };
+    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 anon_allow_robots_off {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my ($user, $group) = config_get_identity();
+
+  my $robots_data = "User-agent: *\nDisallow: /\n";
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => $user,
+        Group => $group,
+        UserAlias => "anonymous $user",
+        RequireValidShell => 'off',
+
+        AnonAllowRobots => '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, 0, 1);
+      $client->login($user, 'ftp at nospam.org');
+      $client->type('binary');
+
+      my $conn = $client->retr_raw('/robots.txt');
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 15);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      eval { $client->dele('robots.txt') };
+      unless ($@) {
+        die("DELE succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "robots.txt: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $self->assert($buf eq $robots_data,
+        test_msg("Did not receive expected robots.txt data"));
+    };
+    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 anon_allow_robots_off_existing_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my ($user, $group) = config_get_identity();
+
+  my $robots_data = "User-agent: *\nAllow:\n";
+  my $robots_file = File::Spec->rel2abs("$setup->{home_dir}/robots.txt");
+  if (open(my $fh, "> $robots_file")) {
+    print $fh $robots_data;
+    unless (close($fh)) {
+      die("Can't write $robots_file: $!");
+    }
+
+  } else {
+    die("Can't open $robots_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => $user,
+        Group => $group,
+        UserAlias => "anonymous $user",
+        RequireValidShell => '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, 0, 1);
+      $client->login($user, 'ftp at nospam.org');
+      $client->type('binary');
+
+      my $conn = $client->retr_raw('/robots.txt');
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 15);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $self->assert($buf eq $robots_data,
+        test_msg("Did not receive expected robots.txt data"));
+    };
+    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 anon_allow_robots_off_non_anon_login {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $robots_data = "User-agent: *\nDisallow: /\n";
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => 'ftp',
+        Group => $setup->{group},
+        UserAlias => "anonymous ftp",
+        RequireValidShell => 'off',
+
+        AnonAllowRobots => '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, 0, 1);
+
+      # Deliberately log in as a non-anonymous user
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw('/robots.txt');
+      if ($conn) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "/robots.txt: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/AnonRejectPasswords.pm b/tests/t/lib/ProFTPD/Tests/Config/AnonRejectPasswords.pm
index 2bbb8e0..0ceefb4 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/AnonRejectPasswords.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/AnonRejectPasswords.pm
@@ -17,7 +17,17 @@ my $order = 0;
 my $TESTS = {
   anon_reject_passwords_ok => {
     order => ++$order,
-    test_class => [qw(forking rootprivs)],
+    test_class => [qw(forking)],
+  },
+
+  anon_reject_passwords_flags_nocase => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  anon_reject_passwords_not_and_flags_nocase => {
+    order => ++$order,
+    test_class => [qw(forking)],
   },
 
 };
@@ -112,38 +122,293 @@ sub anon_reject_passwords_ok {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
       # First, try to login using an email address as a password
       eval { $client->login($user, 'ftp at nospam.org') };
       unless ($@) {
         die("Login succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'Login incorrect.';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
    
-      # Now try again, using the password
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub anon_reject_passwords_flags_nocase {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
+
+  my ($user, $group) = config_get_identity();
+
+  my $anon_dir = File::Spec->rel2abs($tmpdir);
+  my $passwd = 'foobar';
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $anon_dir)) {
+      die("Can't set perms on $anon_dir to 0755: $!");
+    }
 
-      $expected = 230;
+    unless (chown($uid, $gid, $anon_dir)) {
+      die("Can't set owner of $anon_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid,
+    '/tmp', '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 privs:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $anon_dir => {
+        User => $user,
+        Group => $group,
+        UserAlias => "anonymous $user",
+        AnonRejectPasswords => 'SPAM [NC]',
+        RequireValidShell => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      # First, try to login using an email address as a password
+      eval { $client->login($user, 'ftp at nospam.org') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'Anonymous access granted, restrictions apply';
+      $expected = 'Login incorrect.';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+   
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub anon_reject_passwords_not_and_flags_nocase {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
+
+  my ($user, $group) = config_get_identity();
+
+  my $anon_dir = File::Spec->rel2abs($tmpdir);
+  my $passwd = 'foobar';
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $anon_dir)) {
+      die("Can't set perms on $anon_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $anon_dir)) {
+      die("Can't set owner of $anon_dir to $uid/$gid: $!");
+    }
+  }
 
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid,
+    '/tmp', '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 privs:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $anon_dir => {
+        User => $user,
+        Group => $group,
+        UserAlias => "anonymous $user",
+
+        # See: http://www.regular-expressions.info/email.html
+        AnonRejectPasswords => '!^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$ [NC]',
+        RequireValidShell => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      # First, try to login using an email address as a password
+      eval { $client->login($user, 'ftp at nospam') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+   
       $client->quit();
     };
 
diff --git a/tests/t/lib/ProFTPD/Tests/Config/AuthAliasOnly.pm b/tests/t/lib/ProFTPD/Tests/Config/AuthAliasOnly.pm
index 631233f..92a4d7c 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/AuthAliasOnly.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/AuthAliasOnly.pm
@@ -20,7 +20,7 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  authaliasonly_off_bug2070 => {
+  authaliasonly_off_anon_bug2070 => {
     order => ++$order,
     test_class => [qw(bug forking rootprivs)],
   },
@@ -35,6 +35,11 @@ my $TESTS = {
     test_class => [qw(bug forking rootprivs)],
   },
 
+  authaliasonly_on_anon_bug4255 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
 };
 
 sub new {
@@ -48,49 +53,19 @@ sub list_tests {
 sub authaliasonly_on {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
-  
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
+  my $setup = test_setup($tmpdir, 'config');
 
   my $alias = 'ftp';
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash'); 
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    UserAlias => "$alias $user",
+    UserAlias => "$alias $setup->{user}",
     AuthAliasOnly => 'on',
 
     IfModules => {
@@ -100,7 +75,8 @@ sub authaliasonly_on {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -117,14 +93,14 @@ sub authaliasonly_on {
   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($alias, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($alias, $setup->{passwd});
       $client->quit();
 
-      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->login($user, $passwd) };
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
       unless ($@) {
-        die("Login using user '$user' succeeded unexpectedly");
+        die("Login using user '$setup->{user}' succeeded unexpectedly");
       }
     };
 
@@ -136,7 +112,7 @@ sub authaliasonly_on {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -146,18 +122,13 @@ sub authaliasonly_on {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub authaliasonly_off_bug2070 {
+sub authaliasonly_off_anon_bug2070 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -188,7 +159,7 @@ sub authaliasonly_off_bug2070 {
     }
   }
 
-  my $alias = 'ftp';
+  my $alias = 'ftptest';
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash'); 
@@ -200,6 +171,8 @@ sub authaliasonly_off_bug2070 {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'auth:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -251,7 +224,18 @@ EOC
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($alias, $passwd);
+
+      # Make sure that we are indeed logging in anonymously
+      my ($resp_code, $resp_msg) = $client->login($alias, 'ftp at nospam.org');
+
+      my $expected = 230;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Anonymous access granted, restrictions apply';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
       $client->quit();
     };
 
@@ -368,20 +352,29 @@ sub authaliasonly_on_anon_bug3501 {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
       my ($resp_code, $resp_msg) = $client->user($config_user);
 
-      my $expected;
-
-      $expected = 331;
+      my $expected = 331;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
+      # Since AuthAliasOnly is true, it means that that anonymous login can
+      # ONLY happen via the alias, NOT via the configured User.
       $expected = "Password required for $config_user";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # What about using the alias, though?
+      ($resp_code, $resp_msg) = $client->user('anonymous');
+
+      $expected = 331;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
+      $expected = "Anonymous login ok, send your complete email address as your password";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
       $client->quit();
     };
 
@@ -506,18 +499,15 @@ sub authaliasonly_on_system_bug3501 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       my ($resp_code, $resp_msg) = $client->user($user);
 
-      my $expected;
-
-      $expected = 331;
+      my $expected = 331;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Password required for $user";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -550,4 +540,111 @@ sub authaliasonly_on_system_bug3501 {
   unlink($log_file);
 }
 
+sub authaliasonly_on_anon_bug4255 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my ($config_user, $config_group) = config_get_identity();
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:20',
+
+    User => $config_user,
+    Group => $config_group,
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthAliasOnly => 'on',
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => $config_user,
+        Group => $config_group,
+        RequireValidShell => 'off',
+        UserAlias => "anonymous $config_user",
+      },
+    },
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my $port;
+  ($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);
+
+      # First, try logging in as user 'anonymous', i.e. the alias.
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      my ($resp_code, $resp_msg) = $client->user("anonymous");
+
+      my $expected = 331;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Anonymous login ok, send your complete email address as your password';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->pass('ftp at nospam.org');
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Anonymous access granted, restrictions apply';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/DefaultChdir.pm b/tests/t/lib/ProFTPD/Tests/Config/DefaultChdir.pm
index c8be93e..28c493e 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/DefaultChdir.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/DefaultChdir.pm
@@ -146,21 +146,23 @@ sub defaultchdir_ok {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -267,21 +269,23 @@ sub defaultchdir_var_u {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -389,21 +393,23 @@ sub defaultchdir_with_defaultroot {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"/subdir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -511,21 +517,23 @@ sub defaultchdir_with_defaultroot2 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"/subdir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -634,21 +642,23 @@ sub defaultchdir_one_env_var_bug3502 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -758,21 +768,23 @@ sub defaultchdir_multi_env_var_bug3502 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -879,21 +891,23 @@ sub defaultchdir_empty_env_var_bug3502 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1003,21 +1017,23 @@ sub defaultchdir_user_mux_one_level {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1127,21 +1143,23 @@ sub defaultchdir_user_mux_three_levels {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->pwd();
-
+      my ($resp_code, $resp_msg) = $client->pwd();
       $client->quit();
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $sub_dir = '/private' . $sub_dir;
+      }
 
       $expected = "\"$sub_dir\" is the current directory";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
diff --git a/tests/t/lib/ProFTPD/Tests/Config/DefaultRoot.pm b/tests/t/lib/ProFTPD/Tests/Config/DefaultRoot.pm
index 3863c14..88486c3 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/DefaultRoot.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/DefaultRoot.pm
@@ -621,65 +621,39 @@ sub defaultroot_allowchrootsymlinks_bug3852 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
-
   my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs("$tmpdir/home.d/symlinks/$user");
   my $uid = 500;
   my $gid = 500;
 
-  my $intermed_dir = File::Spec->rel2abs("$tmpdir/home.d/symlinks");
-  mkpath($intermed_dir);
-
-  my $symlink_dst = File::Spec->rel2abs("$tmpdir/real/$user");
-  mkpath($symlink_dst);
+  my $symlink_dst = File::Spec->rel2abs("$tmpdir/real");
 
   my $cwd = getcwd();
 
-  unless (chdir($intermed_dir)) {
-    die("Can't chdir to $intermed_dir: $!");
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
   }
 
-  unless (symlink("../../real/$user", "./$user")) {
-    die("Can't symlink '../../real/$user' to './$user': $!");
+  unless (symlink("./real", "./home.d")) {
+    die("Can't symlink './real' to './home.d': $!");
   }
 
   unless (chdir($cwd)) {
     die("Can't chdir to $cwd: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $symlink_dst)) {
-      die("Can't set perms on $symlink_dst to 0755: $!");
-    }
+  mkpath(File::Spec->rel2abs("$tmpdir/real/symlinks/$user"));
 
-    unless (chown($uid, $gid, $symlink_dst)) {
-      die("Can't set owner of $symlink_dst to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'config', $user, undef, undef, $uid, $gid,
+    $home_dir);
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     AllowChrootSymlinks => 'off',
     DefaultRoot => '~',
@@ -691,7 +665,8 @@ sub defaultroot_allowchrootsymlinks_bug3852 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -709,7 +684,7 @@ sub defaultroot_allowchrootsymlinks_bug3852 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->login($user, $passwd) };
+      eval { $client->login($user, $setup->{passwd}) };
       unless ($@) {
         die("Login succeeded unexpectedly");
       }
@@ -721,13 +696,12 @@ sub defaultroot_allowchrootsymlinks_bug3852 {
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected response code $expected, got $resp_code"));
+        "Expected response code $expected, got $resp_code");
 
       $expected = "Login incorrect.";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected response message '$expected', got '$resp_msg'"));
+        "Expected response message '$expected', got '$resp_msg'");
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -736,7 +710,7 @@ sub defaultroot_allowchrootsymlinks_bug3852 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -746,18 +720,10 @@ sub defaultroot_allowchrootsymlinks_bug3852 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/Directory/Limits.pm b/tests/t/lib/ProFTPD/Tests/Config/Directory/Limits.pm
index becfdb1..b8c8119 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/Directory/Limits.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/Directory/Limits.pm
@@ -94,11 +94,6 @@ my $TESTS = {
     test_class => [qw(bug forking rootprivs)],
   },
 
-  limits_retr_bug3915_chrooted => {
-    order => ++$order,
-    test_class => [qw(bug forking rootprivs)],
-  },
-
   limits_stor_with_multiple_groups_chrooted => {
     order => ++$order,
     test_class => [qw(bug forking rootprivs)],
@@ -2588,162 +2583,6 @@ EOC
   unlink($log_file);
 }
 
-sub limits_retr_bug3915_chrooted {
-  my $self = shift;
-  my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/dir.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/dir.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/dir.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/dir.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/dir.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs("$tmpdir/home/users/$user");
-  my $uid = 500;
-  my $gid = 500;
-
-  mkpath($home_dir);
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
- 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 fsio:0 directory:10 lock:0',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
-    DefaultRoot => '~',
-
-    IfModules => {
-      'mod_delay.c' => {
-        DelayEngine => 'off',
-      },
-    },
-  };
-
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Directory ~$user>
-  <Limit RETR>
-    DenyAll
-  </Limit>
-</Directory>
-EOC
-    close($fh);
-
-  } else {
-    die("Can't read $config_file: $!");
-  }
-
-  # 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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-
-      my $conn = $client->retr_raw('test.txt');
-      if ($conn) {
-        die("RETR test.txt succeeded unexpectedly");
-      }
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
-
-      my $expected;
-
-      $expected = 550;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected response code $expected, got $resp_code"));
-
-      $expected = "test.txt: Operation not permitted";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected response message '$expected', got '$resp_msg'"));
-    };
-
-    if ($@) {
-      $ex = $@;
-    }
-
-    $wfh->print("done\n");
-    $wfh->flush();
-
-  } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
-    }
-
-    exit 0;
-  }
-
-  # Stop server
-  server_stop($pid_file);
-
-  $self->assert_child_ok($pid);
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
-}
-
 sub limits_stor_with_multiple_groups_chrooted {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
diff --git a/tests/t/lib/ProFTPD/Tests/Config/DisplayFileTransfer.pm b/tests/t/lib/ProFTPD/Tests/Config/DisplayFileTransfer.pm
index 84fb6ab..93e95fe 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/DisplayFileTransfer.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/DisplayFileTransfer.pm
@@ -36,6 +36,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  displayfilexfer_non_path => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   # XXX Add other tests with the various Display variables
 };
 
@@ -137,11 +142,11 @@ sub displayfilexfer_abs_path {
 
       $expected = 226;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Hello user!";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -264,11 +269,11 @@ sub displayfilexfer_rel_path {
 
       $expected = 226;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Hello user!";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -414,11 +419,11 @@ sub displayfilexfer_chrooted {
 
       $expected = 226;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Hello user!";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -540,11 +545,11 @@ sub displayfilexfer_multiline {
 
       $expected = 226;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "Hello user!";
+      $expected = " Hello user!";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -576,4 +581,93 @@ sub displayfilexfer_multiline {
   unlink($log_file);
 }
 
+sub displayfilexfer_non_path {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $xfer_file = '"SUCCESS: Transfer file count:%t | Bytes transferred:%K"';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'response:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DisplayFileTransfer => $xfer_file,
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw('config.conf');
+      unless ($conn) {
+        die("RETR config.conf failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 32768, 5);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/DisplayQuit.pm b/tests/t/lib/ProFTPD/Tests/Config/DisplayQuit.pm
index 9c5de9b..e3b7bbe 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/DisplayQuit.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/DisplayQuit.pm
@@ -30,6 +30,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  displayquit_non_path => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   # XXX Add other tests with the various Display variables
 };
 
@@ -393,4 +398,85 @@ sub displayquit_multiline {
   unlink($log_file);
 }
 
+sub displayquit_non_path {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $quit_file = '"GOODBYE: Ya\'ll come son now! Ya hear?"';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DisplayQuit => $quit_file,
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quit();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg(0);
+
+      my $expected = 221;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Goodbye.";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/FactsOptions.pm b/tests/t/lib/ProFTPD/Tests/Config/FactsOptions.pm
index 956683f..57af6f6 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/FactsOptions.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/FactsOptions.pm
@@ -519,7 +519,7 @@ sub factsoptions_use_slink_mlst_rel_symlinked_file {
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       if ($^O eq 'darwin') {
         # MacOSX-specific hack, due to how it handles tmp files
@@ -528,7 +528,7 @@ sub factsoptions_use_slink_mlst_rel_symlinked_file {
 
       $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=OS\.unix=slink:(\S+);unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; ' . $test_symlink . '$';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
diff --git a/tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm b/tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm
index 53c058a..bbf19b6 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/HiddenStores.pm
@@ -15,7 +15,7 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
-  hiddenstores_ok => {
+  hiddenstores_on => {
     order => ++$order,
     test_class => [qw(forking)],
   },
@@ -40,6 +40,31 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  hiddenstores_pid_var_bug4062 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  hiddenstores_timeout_idle_bug4035 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  hiddenstores_timeout_notransfer_bug4035 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  hiddenstores_timeout_stalled_bug4035 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  hiddenstores_timeout_session_bug4035 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -50,7 +75,7 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
-sub hiddenstores_ok {
+sub hiddenstores_on {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -766,4 +791,624 @@ sub hiddenstores_suffix_bug3872 {
   unlink($log_file);
 }
 
+sub find_hiddenstore_files {
+  my $path = shift;
+  my $prefix = shift;
+
+  my $dirh;
+  unless (opendir($dirh, $path)) {
+    die("Can't open directory '$path': $!");
+  }
+
+  my $files  = [grep { /^$prefix/ && -f "$path/$_" } readdir($dirh)]; 
+  closedir($dirh); 
+
+  return $files;
+}
+
+sub hiddenstores_pid_var_bug4062 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultChdir => '~',
+
+    HiddenStores => '.in. .%P',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 25);
+
+      my $hidden_files = find_hiddenstore_files($home_dir, '.in.');
+      unless (defined($hidden_files) && scalar(@$hidden_files) == 1) {
+        die("HiddenStores file does not exist as expected");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+
+      $hidden_files = find_hiddenstore_files($home_dir, '.in.');
+      if (defined($hidden_files) && scalar(@$hidden_files) > 0) {
+        die("HiddenStores file exists unexpectedly");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub hiddenstores_timeout_idle_bug4035 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $timeout_idle = 4;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultChdir => '~',
+
+    HiddenStores => 'on',
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # Sleep for longer than the TimeoutIdle
+      sleep($timeout_idle + 1);
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 25);
+
+      unless (-f $hidden_file) {
+        die("File $hidden_file does not exist as expected");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      if ($^O eq 'darwin') {
+        # MacOSX hack
+        $test_file = ('/private' . $test_file);
+        $hidden_file = ('/private' . $hidden_file);
+      }
+
+      if (-f $hidden_file) {
+        die("File $hidden_file exists unexpectedly");
+      }
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+    };
+
+    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 hiddenstores_timeout_notransfer_bug4035 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $timeout_noxfer = 2;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'timer:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultChdir => '~',
+
+    HiddenStores => 'on',
+    TimeoutNoTransfer => $timeout_noxfer,
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # Sleep for longer than the TimeoutNoTransfer
+      sleep($timeout_noxfer + 1);
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 25);
+
+      unless (-f $hidden_file) {
+        die("File $hidden_file does not exist as expected");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      if ($^O eq 'darwin') {
+        # MacOSX hack
+        $test_file = ('/private' . $test_file);
+        $hidden_file = ('/private' . $hidden_file);
+      }
+
+      if (-f $hidden_file) {
+        die("File $hidden_file exists unexpectedly");
+      }
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+    };
+
+    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 hiddenstores_timeout_stalled_bug4035 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $timeout_stalled = 2;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultChdir => '~',
+
+    HiddenStores => 'on',
+    TimeoutStalled => $timeout_stalled,
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # Sleep for longer than the TimeoutStalled
+      sleep($timeout_stalled + 1);
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 25);
+
+      if (-f $hidden_file) {
+        die("File $hidden_file exists unexpectedly");
+      }
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      eval { $client->quit() };
+      unless ($@) {
+        die("QUIT succeeded unexpectedly");
+      }
+
+      if ($^O eq 'darwin') {
+        # MacOSX hack
+        $test_file = ('/private' . $test_file);
+        $hidden_file = ('/private' . $hidden_file);
+      }
+
+      if (-f $hidden_file) {
+        die("File $hidden_file exists unexpectedly");
+      }
+
+      if (-f $test_file) {
+        die("File $test_file exists unexpectedly");
+      }
+    };
+
+    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 hiddenstores_timeout_session_bug4035 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $timeout_session = 2;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultChdir => '~',
+
+    HiddenStores => 'on',
+    TimeoutSession => $timeout_session,
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      # Sleep for longer than the TimeoutSession
+      sleep($timeout_session + 2);
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 25);
+
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 421;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      eval { $client->quit() };
+      unless ($@) {
+        die("QUIT succeeded unexpectedly");
+      }
+
+      if ($^O eq 'darwin') {
+        # MacOSX hack
+        $test_file = ('/private' . $test_file);
+        $hidden_file = ('/private' . $hidden_file);
+      }
+
+      if (-f $hidden_file) {
+        die("File $hidden_file exists unexpectedly");
+      }
+
+      if (-f $test_file) {
+        die("File $test_file exists unexpectedly");
+      }
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/HideFiles.pm b/tests/t/lib/ProFTPD/Tests/Config/HideFiles.pm
index 5f81525..cf971ac 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/HideFiles.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/HideFiles.pm
@@ -127,6 +127,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  hidefiles_multi_dirs => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -3535,7 +3540,7 @@ sub hidefiles_stat {
 
       my $expected;
 
-      $expected = 211;
+      $expected = 213;
       $self->assert($expected == $resp_code,
         test_msg("Expected response code $expected, got $resp_code"));
 
@@ -4055,4 +4060,179 @@ sub hidefiles_mlsd_symlink_bug3924 {
   unlink($log_file);
 }
 
+# See:
+#   https://forums.proftpd.org/smf/index.php/topic,12152.0.html
+sub hidefiles_multi_dirs {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $a_dir = File::Spec->rel2abs("$tmpdir/a");
+  mkpath($a_dir);
+
+  my $b_dir = File::Spec->rel2abs("$tmpdir/a/b");
+  mkpath($b_dir);
+
+  my $test_file = File::Spec->rel2abs("$b_dir/b.sh");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $c_dir = File::Spec->rel2abs("$tmpdir/a/b/c");
+  mkpath($c_dir);
+
+  $test_file = File::Spec->rel2abs("$c_dir/c.sh");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $a_dir, $b_dir)) {
+      die("Can't set perms on $a_dir, $b_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $a_dir, $b_dir)) {
+      die("Can't set owner of $a_dir, $b_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'directory:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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}")) {
+    if ($^O eq 'darwin') {
+      # Mac OSX hack
+      $b_dir = '/private' . $b_dir;
+    }
+
+    print $fh <<EOC;
+<Directory $b_dir>
+  HideFiles \(.sh\)\$
+</Directory>
+
+<Directory $b_dir/\*>
+  HideFiles none
+</Directory>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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});
+
+      my $conn = $client->mlsd_raw($b_dir);
+      unless ($conn) {
+        die("Failed to MLSD $b_dir: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      my $tmp;
+      while ($conn->read($tmp, 8192, 30)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Output:\n$buf\n";
+      }
+
+      $self->assert($buf !~ /b\.sh/, "Expected no .sh, saw 'b.sh'");
+
+      $conn = $client->mlsd_raw($c_dir);
+      unless ($conn) {
+        die("Failed to MLSD $c_dir: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = '';
+      $tmp = '';
+      while ($conn->read($tmp, 8192, 30)) {
+        $buf .= $tmp;
+      }
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Output:\n$buf\n";
+      }
+
+      $self->assert($buf =~ /c\.sh/, "Expected c.sh, saw none");
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/Include.pm b/tests/t/lib/ProFTPD/Tests/Config/Include.pm
index 0665625..ddbdc1d 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/Include.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/Include.pm
@@ -36,6 +36,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  include_limit => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
 };
 
 sub new {
@@ -835,4 +840,115 @@ EOC
   unlink($log_file);
 }
 
+sub include_limit {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $include_config = File::Spec->rel2abs("$tmpdir/include.conf");
+  if (open(my $fh, "> $include_config")) {
+    print $fh "Allow from 127.0.0.1\n";
+    unless (close($fh)) {
+      die("Can't write $include_config: $!");
+    }
+
+  } else {
+    die("Can't open $include_config: $!");
+  }
+
+  my $ftpaccess_file = File::Spec->rel2abs("$setup->{home_dir}/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOC;
+<Limit LOGIN>
+  Include $include_config
+  DenyAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AllowOverride => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultChdir => '~',
+
+    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;
+<Limit LOGIN>
+  Include $include_config
+  DenyAll
+</Limit>
+EOC
+
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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, 0);
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/Limit/RMD.pm b/tests/t/lib/ProFTPD/Tests/Config/Limit/RMD.pm
index 58a7619..1678c21 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/Limit/RMD.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/Limit/RMD.pm
@@ -52,7 +52,7 @@ sub rmd_unremovable_subdir {
 
   my $sub_dir = File::Spec->rel2abs("$tmpdir/domain.com/web/special");
   mkpath($sub_dir);
- 
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -69,6 +69,12 @@ sub rmd_unremovable_subdir {
     '/bin/bash');
   auth_group_write($auth_group_file, 'ftpd', $gid, $user);
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $home_dir = '/private' . $home_dir;
+    $sub_dir = '/private' . $sub_dir;
+  }
+ 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -144,53 +150,51 @@ EOC
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my ($resp_code, $resp_msg);
-
       # We should be able to create and delete a directory under web/.
       # We should be able to create and delete a directory under web/special/.
       # We should NOT be able to delete the web/special/ directory.
 
-      ($resp_code, $resp_msg) = $client->mkd('domain.com/web/foo');
+      my ($resp_code, $resp_msg) = $client->mkd('domain.com/web/foo');
 
       my $expected;
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "\"$home_dir/domain.com/web/foo\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->rmd('domain.com/web/foo');
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "RMD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->mkd('domain.com/web/special/foo');
 
       $expected = 257;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "\"$home_dir/domain.com/web/special/foo\" - Directory successfully created";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->rmd('domain.com/web/special/foo');
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "RMD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       eval { $client->rmd('domain.com/web/special') };
       unless ($@) {
@@ -202,11 +206,11 @@ EOC
 
       $expected = 550;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "domain.com/web/special: Permission denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
diff --git a/tests/t/lib/ProFTPD/Tests/Config/ListOptions.pm b/tests/t/lib/ProFTPD/Tests/Config/ListOptions.pm
index 2f7738d..b4ac964 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/ListOptions.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/ListOptions.pm
@@ -51,6 +51,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  listoptions_sortednlst_bug4267 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   listoptions_maxfiles => {
     order => ++$order,
     test_class => [qw(forking slow)],
@@ -180,8 +185,11 @@ sub listoptions_opt_t {
           $client->response_msg());
       }
 
-      my $buf;
-      $conn->read($buf, 16384, 25);
+      my $buf = '';
+      my $tmp;
+      while ($conn->read($tmp, 8192, 25)) {
+        $buf .= $tmp;
+      }
       eval { $conn->close() };
 
       my $resp_code = $client->response_code();
@@ -1398,6 +1406,154 @@ sub listoptions_nlstonly {
   unlink($log_file);
 }
 
+sub listoptions_sortednlst_bug4267 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $test_files = [];
+  my $nfiles = 1000;
+  for (my $i = 0; $i < $nfiles; $i++) {
+    my $fileno = sprintf("%04d", $i);
+    push(@$test_files, "$fileno.dat");
+  }
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "# Writing out files..."
+  }
+
+  my $count = scalar(@$test_files);
+  foreach my $test_file (@$test_files) {
+    my $path = File::Spec->rel2abs("$test_dir/$test_file");
+    if (open(my $fh, "> $path")) {
+      print $fh "Hello, World!\n";
+      unless (close($fh)) {
+        die("Can't write $path: $!");
+      }
+
+    } else {
+      die("Can't open $path: $!");
+    }
+  }
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "done\n";
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutLinger => 1,
+
+    ListOptions => '"" SortedNLST',
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->cwd('test.d');
+
+      my $conn = $client->nlst_raw();
+      unless ($conn) {
+        die("Failed to NLST: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my ($buf, $tmp);
+      my $res = $conn->read($tmp, 8192, 5);
+      while ($res) {
+        $buf .= $tmp;
+        $tmp = '';
+        $res = $conn->read($tmp, 8192, 5);
+      }
+      eval { $conn->close() };
+      sleep(2);
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "buf:\n$buf\n";
+      }
+
+      # Do NOT sort the results; we expect them to match the expected list.
+      my $res = [];
+      my $lines = [split(/\n/, $buf)];
+      foreach my $line (@$lines) {
+        push(@$res, $line);
+      }
+
+      my $expected = [@$test_files];
+      my $nexpected = scalar(@$expected);
+      my $nres = scalar(@$res);
+
+      $self->assert($nexpected == $nres,
+        test_msg("Expected $nexpected items, got $nres"));
+      for (my $i = 0; $i < $nexpected; $i++) {
+        $self->assert($expected->[$i] eq $res->[$i],
+          test_msg("Expected '$expected->[$i]' at index $i, got '$res->[$i]'"));
+      }
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 180) };
+    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 listoptions_maxfiles {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
diff --git a/tests/t/lib/ProFTPD/Tests/Config/LoginPasswordPrompt.pm b/tests/t/lib/ProFTPD/Tests/Config/LoginPasswordPrompt.pm
new file mode 100644
index 0000000..7218f1a
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/LoginPasswordPrompt.pm
@@ -0,0 +1,215 @@
+package ProFTPD::Tests::Config::LoginPasswordPrompt;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  loginpasswordprompt_on => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  loginpasswordprompt_off => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub loginpasswordprompt_on {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LoginPasswordPrompt => 'on',
+
+    IfModules => {
+      '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, 0, 1);
+      my ($resp_code, $resp_msg) = $client->user($setup->{user});
+
+      my $expected = 331;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Password required for $setup->{user}";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Now try again, this time with a known INVALID user
+      my $bad_user = 'foobarbaz';
+      ($resp_code, $resp_msg) = $client->user($bad_user);
+
+      $expected = 331;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Password required for $bad_user";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 loginpasswordprompt_off {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    # Turns out that the LoginPasswordPrompt directive is only effective
+    # in conjunction with AuthAliasOnly.
+    AuthAliasOnly => 'on',
+    LoginPasswordPrompt => 'off',
+
+    IfModules => {
+      '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, 0, 1);
+      my $bad_user = 'foobarbaz';
+
+      eval { $client->user($bad_user) };
+      unless ($@) {
+        die("USER $bad_user succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/MasqueradeAddress.pm b/tests/t/lib/ProFTPD/Tests/Config/MasqueradeAddress.pm
index 3ea5efd..08df10f 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/MasqueradeAddress.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/MasqueradeAddress.pm
@@ -20,6 +20,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  masqaddr_pasv_delayed_resolving_bug4104 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  masqaddr_empty_addr => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   # MasqueradeAddress not support for EPSV currently; see comments
   # in mod_core.c:core_epsv() on this topic.
 
@@ -161,4 +171,173 @@ sub masqaddr_pasv {
   unlink($log_file);
 }
 
+sub masqaddr_pasv_delayed_resolving_bug4104 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/cmds.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/cmds.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  # We deliberately choose a name which cannot be resolved, to test that
+  # the server still starts up (Bug#4104).
+  my $masq_addr = 'foo.bar.castaglia.example.com';
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    MasqueradeAddress => $masq_addr,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);;
+      my ($resp_code, $resp_msg) = $client->pasv();
+
+      my $expected;
+
+      $expected = 227;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = '^Entering Passive Mode \(\d+,\d+,\d+,\d+,\d+,\d+\)';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      $resp_msg =~ /\((.*)?,\d+,\d+\)/;
+      my $resp_addr = $1;
+
+      $expected = $masq_addr;
+      $expected =~ s/\./,/g;
+
+      $self->assert($expected ne $resp_addr,
+        test_msg("Expected PASV address '$expected', got '$resp_addr'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub masqaddr_empty_addr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  # We can't configure an empty address directly, but we CAN configure
+  # an environment variable that doesn't exist, which will tickle this.
+  my $masq_addr = '%{env:TEST_NON_EXISTENT_ADDR}';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    MasqueradeAddress => $masq_addr,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $ex;
+  eval { server_start($setup->{config_file}, $setup->{pid_file}) };
+  unless ($@) {
+    server_stop($setup->{pid_file});
+    $ex = "server started unexpectedly";
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/MaxClients.pm b/tests/t/lib/ProFTPD/Tests/Config/MaxClients.pm
index d91ccb9..128004e 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/MaxClients.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/MaxClients.pm
@@ -30,6 +30,11 @@ my $TESTS = {
     test_class => [qw(forking mod_ifsession mod_sql mod_sql_sqlite)],
   },
 
+  maxclients_one_anon_bug4068 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
 };
 
 sub new {
@@ -417,4 +422,116 @@ EOC
   unlink($log_file);
 }
 
+sub maxclients_one_anon_bug4068 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
+
+  my ($config_user, $config_group) = config_get_identity();
+
+  my $anon_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  auth_user_write($auth_user_file, $config_user, 'foo', $uid, $gid,
+    '/tmp', '/bin/bash');
+  auth_group_write($auth_group_file, $config_group, $gid, $config_user);
+
+  my $max_clients = 1;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    Anonymous => {
+      $anon_dir => {
+        User => $config_user,
+        Group => $config_group,
+        UserAlias => "anonymous $config_user",
+        RequireValidShell => 'off',
+        MaxClients => $max_clients,
+      },
+    },
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my $port;
+  ($port, $config_user, $config_group) = config_write($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 {
+      # First client should be able to connect and log in...
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($config_user, 'ftp at nospam.org');
+
+      # ...but the second client should be able to connect, but not login.
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      eval { $client2->login($config_user, 'ftp at nospam.org') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      $client1->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/MaxPasswordSize.pm b/tests/t/lib/ProFTPD/Tests/Config/MaxPasswordSize.pm
new file mode 100644
index 0000000..743e6ab
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/MaxPasswordSize.pm
@@ -0,0 +1,184 @@
+package ProFTPD::Tests::Config::MaxPasswordSize;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  maxpasswordsize_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  maxpasswordsize_failed_too_long => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub maxpasswordsize_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $max_passwd_size = 32;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    MaxPasswordSize => $max_passwd_size,
+
+    IfModules => {
+      '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, 0, 1);
+      $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 maxpasswordsize_failed_too_long {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $max_passwd_size = 2;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    MaxPasswordSize => $max_passwd_size,
+
+    IfModules => {
+      '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, 0, 1);
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login 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);
+}
+
+1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerHost.pm b/tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerHost.pm
new file mode 100644
index 0000000..f393baa
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerHost.pm
@@ -0,0 +1,236 @@
+package ProFTPD::Tests::Config::MaxTransfersPerHost;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  maxtransfersperhost_retr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  maxtransfersperhost_retr_custom_message => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub maxtransfersperhost_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+  my $max_transfers = 1;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file}, 
+    MaxTransfersPerHost => "RETR $max_transfers",
+
+    IfModules => {
+      '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 {
+      # First client should be able to connect and log in...
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($setup->{user}, $setup->{passwd});
+      my $conn1 = $client1->retr_raw($test_file);
+      unless ($conn1) {
+        die("RETR failed: " . $client1->response_code() . " " .
+          $client1->response_msg());
+      }
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client2->login($setup->{user}, $setup->{passwd});
+      my $conn2 = $client2->retr_raw($test_file);
+      if ($conn2) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client2->response_code();
+      my $resp_msg = $client2->response_msg();
+
+      my $expected;
+
+      $expected = 451;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Sorry, the maximum number of data transfers ($max_transfers) from your host are currently being used.";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client1->quit();
+      $client2->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 maxtransfersperhost_retr_custom_message {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+  my $max_transfers = 1;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file}, 
+    MaxTransfersPerHost => "RETR $max_transfers \"Too many transfers from your host\"",
+
+    IfModules => {
+      '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 {
+      # First client should be able to connect and log in...
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($setup->{user}, $setup->{passwd});
+      my $conn1 = $client1->retr_raw($test_file);
+      unless ($conn1) {
+        die("RETR failed: " . $client1->response_code() . " " .
+          $client1->response_msg());
+      }
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client2->login($setup->{user}, $setup->{passwd});
+      my $conn2 = $client2->retr_raw($test_file);
+      if ($conn2) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client2->response_code();
+      my $resp_msg = $client2->response_msg();
+
+      my $expected;
+
+      $expected = 451;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Too many transfers from your host';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client1->quit();
+      $client2->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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerUser.pm b/tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerUser.pm
new file mode 100644
index 0000000..7b138e2
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/MaxTransfersPerUser.pm
@@ -0,0 +1,236 @@
+package ProFTPD::Tests::Config::MaxTransfersPerUser;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  maxtransfersperuser_retr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  maxtransfersperuser_retr_custom_message => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub maxtransfersperuser_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+  my $max_transfers = 1;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file}, 
+    MaxTransfersPerUser => "RETR $max_transfers",
+
+    IfModules => {
+      '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 {
+      # First client should be able to connect and log in...
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($setup->{user}, $setup->{passwd});
+      my $conn1 = $client1->retr_raw($test_file);
+      unless ($conn1) {
+        die("RETR failed: " . $client1->response_code() . " " .
+          $client1->response_msg());
+      }
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client2->login($setup->{user}, $setup->{passwd});
+      my $conn2 = $client2->retr_raw($test_file);
+      if ($conn2) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client2->response_code();
+      my $resp_msg = $client2->response_msg();
+
+      my $expected;
+
+      $expected = 451;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Sorry, the maximum number of data transfers ($max_transfers) from this user are currently being used.";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client1->quit();
+      $client2->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 maxtransfersperuser_retr_custom_message {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+  my $max_transfers = 1;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file}, 
+    MaxTransfersPerUser => "RETR $max_transfers \"Too many transfers from your user\"",
+
+    IfModules => {
+      '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 {
+      # First client should be able to connect and log in...
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($setup->{user}, $setup->{passwd});
+      my $conn1 = $client1->retr_raw($test_file);
+      unless ($conn1) {
+        die("RETR failed: " . $client1->response_code() . " " .
+          $client1->response_msg());
+      }
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client2->login($setup->{user}, $setup->{passwd});
+      my $conn2 = $client2->retr_raw($test_file);
+      if ($conn2) {
+        die("RETR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client2->response_code();
+      my $resp_msg = $client2->response_msg();
+
+      my $expected;
+
+      $expected = 451;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Too many transfers from your user';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client1->quit();
+      $client2->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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/Order.pm b/tests/t/lib/ProFTPD/Tests/Config/Order.pm
index 9d108ad..de139f3 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/Order.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/Order.pm
@@ -15,11 +15,25 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
+  # The single "allow,deny" parameter form
+  order_allowdeny_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  # The double "allow, deny" parameter form with a space
   order_allow_deny_ok => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  # The single "deny,allow" parameter form
+  order_denyallow_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  # The double "deny, allow" parameter form with a space
   order_deny_allow_ok => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -35,7 +49,7 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
-sub order_allow_deny_ok {
+sub order_allowdeny_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -74,6 +88,8 @@ sub order_allow_deny_ok {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'config:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -169,7 +185,141 @@ sub order_allow_deny_ok {
   unlink($log_file);
 }
 
-sub order_deny_allow_ok {
+sub order_allow_deny_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultChdir => '~',
+
+    Limit => {
+      ALL => {
+        Order => 'allow, deny',
+        AllowUser => $user,
+        DenyAll => '',
+      },
+    },
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf));
+      $conn->close();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "Transfer complete";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub order_denyallow_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -298,4 +448,133 @@ sub order_deny_allow_ok {
   unlink($log_file);
 }
 
+sub order_deny_allow_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/config.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/config.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/config.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultChdir => '~',
+
+    Limit => {
+      ALL => {
+        Order => 'deny, allow',
+        AllowUser => $user,
+        DenyAll => '',
+      },
+    },
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->stor_raw('test.txt');
+      if ($conn) {
+        die("STOR test.txt succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "test.txt: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/ServerIdent.pm b/tests/t/lib/ProFTPD/Tests/Config/ServerIdent.pm
index 23bc04b..5ce378d 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/ServerIdent.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/ServerIdent.pm
@@ -55,6 +55,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  serverident_on_with_var_version => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
 };
 
 sub new {
@@ -68,17 +73,12 @@ sub list_tests {
 sub serverident_absent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -87,7 +87,8 @@ sub serverident_absent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -106,20 +107,18 @@ sub serverident_absent {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'ProFTPD \S+ Server \(ProFTPD\) \[\S+\]';
+      $expected = 'ProFTPD Server \(ProFTPD\) \[\S+\]';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -130,7 +129,7 @@ sub serverident_absent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -140,34 +139,22 @@ sub serverident_absent {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_off {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => 'off',
 
@@ -178,7 +165,8 @@ sub serverident_off {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -197,20 +185,18 @@ sub serverident_off {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = '\S+ FTP server ready';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -221,7 +207,7 @@ sub serverident_off {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -231,34 +217,22 @@ sub serverident_off {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_on {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => 'on',
 
@@ -269,7 +243,8 @@ sub serverident_on {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -288,20 +263,18 @@ sub serverident_on {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'ProFTPD \S+ Server \(ProFTPD\) \[\S+\]';
+      $expected = 'ProFTPD Server \(ProFTPD\) \[\S+\]';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -312,7 +285,7 @@ sub serverident_on {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -322,36 +295,24 @@ sub serverident_on {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_on_with_servername {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $server_name = '"Meat Loaf"';
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => 'on',
     ServerName => $server_name,
@@ -363,7 +324,8 @@ sub serverident_on_with_servername {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -382,21 +344,19 @@ sub serverident_on_with_servername {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $server_name =~ s/\"//g;
-      $expected = 'ProFTPD \S+ Server \(' . $server_name . '\) \[\S+\]';
+      $expected = 'ProFTPD Server \(' . $server_name . '\) \[\S+\]';
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -407,7 +367,7 @@ sub serverident_on_with_servername {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -417,37 +377,25 @@ sub serverident_on_with_servername {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_on_with_ident {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $server_name = '"Meat Loaf"';
   my $ident_str = "Yarr!";
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => "on $ident_str",
     ServerName => $server_name,
@@ -459,7 +407,8 @@ sub serverident_on_with_ident {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -478,20 +427,18 @@ sub serverident_on_with_ident {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = $ident_str;
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -502,7 +449,7 @@ sub serverident_on_with_ident {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -512,36 +459,24 @@ sub serverident_on_with_ident {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_on_with_var_L {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $ident_str = '"Server (%L)"';
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => "on $ident_str",
 
@@ -552,7 +487,8 @@ sub serverident_on_with_var_L {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -571,20 +507,18 @@ sub serverident_on_with_var_L {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'Server (127.0.0.1)';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -595,7 +529,7 @@ sub serverident_on_with_var_L {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -605,36 +539,24 @@ sub serverident_on_with_var_L {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_on_with_var_V {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $ident_str = '"Server (%V)"';
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => "on $ident_str",
 
@@ -645,7 +567,8 @@ sub serverident_on_with_var_V {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -664,20 +587,18 @@ sub serverident_on_with_var_V {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'Server (127.0.0.1)';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -688,7 +609,7 @@ sub serverident_on_with_var_V {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -698,37 +619,25 @@ sub serverident_on_with_var_V {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub serverident_on_with_var_v {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/config.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/config.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/config.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'config');
 
   my $ident_str = '"Server (%v)"';
   my $server_name = 'Peeves';
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     ServerIdent => "on $ident_str",
     ServerName => $server_name,
@@ -740,7 +649,8 @@ sub serverident_on_with_var_v {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -759,20 +669,18 @@ sub serverident_on_with_var_v {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 220;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Server ($server_name)";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -783,7 +691,7 @@ sub serverident_on_with_var_v {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -793,18 +701,91 @@ sub serverident_on_with_var_v {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub serverident_on_with_var_version {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $ident_str = '"Server (%{version})"';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    ServerIdent => "on $ident_str",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-    die($ex);
+  # 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: $!");
   }
 
-  unlink($log_file);
+  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);
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 220;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Server \(\d+\.\d+\.\d+(.*)?\)';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/ShowSymlinks.pm b/tests/t/lib/ProFTPD/Tests/Config/ShowSymlinks.pm
index ba7820e..7af4f9d 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/ShowSymlinks.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/ShowSymlinks.pm
@@ -2967,14 +2967,14 @@ sub showsymlinks_off_mlst_rel_symlinked_dir {
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       if ($^O eq 'darwin') {
         # MacOSX-specific hack, to compensate for how it handles tmp files
         $test_symlink = ('/private' . $test_symlink);
       }
 
-      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; ' . $test_symlink . '$';
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; ' . $test_symlink . '$';
       $self->assert(qr/$expected/, $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
     };
@@ -3556,9 +3556,9 @@ sub showsymlinks_off_stat_rel_symlinked_file {
 
       my $expected;
 
-      $expected = 211;
+      $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       unless ($resp_msg =~ /^\s+(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
         die("Response '$resp_msg' does not match expected pattern");
@@ -3718,9 +3718,9 @@ sub showsymlinks_on_stat_rel_symlinked_file {
 
       my $expected;
 
-      $expected = 211;
+      $expected = 213;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       unless ($resp_msg =~ /^\s+(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+\d{2}:\d{2}\s+(.*)?$/) {
         die("Response '$resp_msg' does not match expected pattern");
@@ -3885,9 +3885,9 @@ sub showsymlinks_off_stat_rel_symlinked_dir {
 
       my $expected;
 
-      $expected = 211;
+      $expected = 212;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       unless ($resp_msg =~ /^\s+(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) {
         die("Response '$resp_msg' does not match expected pattern");
@@ -4038,9 +4038,9 @@ sub showsymlinks_on_stat_rel_symlinked_dir {
 
       my $expected;
 
-      $expected = 211;
+      $expected = 212;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       # XXX Possible bug; this should return info on the symlink, rather
       # than trying to list the contents of the symlink target directory.
diff --git a/tests/t/lib/ProFTPD/Tests/Config/TransferOptions.pm b/tests/t/lib/ProFTPD/Tests/Config/TransferOptions.pm
new file mode 100644
index 0000000..33bbc8b
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/TransferOptions.pm
@@ -0,0 +1,271 @@
+package ProFTPD::Tests::Config::TransferOptions;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  transferoptions_ignore_ascii_download_bug4159 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  transferoptions_ignore_ascii_upload_bug4159 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub transferoptions_ignore_ascii_download_bug4159 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_data = "Hello, World!\r\n";
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh $test_data;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultTransferMode => 'binary',
+    TransferOptions => 'IgnoreASCII',
+
+    IfModules => {
+      '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 {
+      # When run too quickly with the other tests, this test can fail.  So
+      # pause a little here.
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # Tell the server we'll be doing an ASCII transfer, BUT configure
+      # the Net::FTP internals to expect binary data, so that we can properly
+      # compare the data we transfer.
+      $client->type('A');
+      my $ftp = $client->{ftp};
+      ${*$ftp}{net_ftp_type} = 'I';
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my ($resp_code, $resp_msg);
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      # Make sure that proftpd did NOT perform ASCII translation, per our
+      # configured TransferOption.
+      my $buflen = length($buf);
+      my $test_datalen = length($test_data);
+      $self->assert($buf eq $test_data,
+        test_msg("Downloaded data '$buf' ($buflen) did not match expected data '$test_data' ($test_datalen)"));
+    };
+
+    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 transferoptions_ignore_ascii_upload_bug4159 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'cmds');
+
+  my $test_data = "Hello, World!\r\n";
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultTransferMode => 'binary',
+    TransferOptions => 'IgnoreASCII',
+
+    IfModules => {
+      '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 {
+      # When run too quickly with the other tests, this test can fail.  So
+      # pause a little here.
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # Tell the server we'll be doing an ASCII transfer, BUT configure
+      # the Net::FTP internals to expect binary data, so that we can properly
+      # compare the data we transfer.
+      $client->type('A');
+      my $ftp = $client->{ftp};
+      ${*$ftp}{net_ftp_type} = 'I';
+
+      my $conn = $client->stor_raw($test_file);
+      unless ($conn) {
+        die("Failed to STOR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $conn->write($test_data, length($test_data), 30);
+      eval { $conn->close() };
+
+      my ($resp_code, $resp_msg);
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      # Make sure that proftpd did NOT perform ASCII translation, per our
+      # configured TransferOption.
+      if (open(my $fh, "< $test_file")) {
+        local $/;
+        my $buf = <$fh>;
+        close($fh);
+
+        my $buflen = length($buf);
+        my $test_datalen = length($test_data);
+        $self->assert($buf eq $test_data,
+          test_msg("Downloaded data '$buf' ($buflen) did not match expected data '$test_data' ($test_datalen)"));
+
+      } else {
+        die("Can't read $test_file: $!");
+      }
+
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Config/Umask.pm b/tests/t/lib/ProFTPD/Tests/Config/Umask.pm
index 6c32f83..d23f04a 100644
--- a/tests/t/lib/ProFTPD/Tests/Config/Umask.pm
+++ b/tests/t/lib/ProFTPD/Tests/Config/Umask.pm
@@ -38,7 +38,10 @@ sub new {
 }
 
 sub list_tests {
-  return testsuite_get_runnable_tests($TESTS);
+#  return testsuite_get_runnable_tests($TESTS);
+  return qw(
+    umask_new_dir_mode_subdir
+  );
 }
 
 sub umask_new_dir_mode {
@@ -126,6 +129,11 @@ sub umask_new_dir_mode {
       $self->assert($resp_code == $expected,
         test_msg("Expected response code $expected, got $resp_code"));
 
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $test_dir = '/private' . $test_dir;
+      }
+
       $expected = "\"$test_dir\" - Directory successfully created";
       $self->assert($resp_msg eq $expected,
         test_msg("Expected response message '$expected', got '$resp_msg'"));
@@ -207,6 +215,11 @@ sub umask_new_dir_mode_subdir {
   my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
   mkpath($sub_dir);
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $sub_dir = '/private' . $sub_dir;
+  }
+
   my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
 
   my $config = {
diff --git a/tests/t/lib/ProFTPD/Tests/Config/VirtualHost.pm b/tests/t/lib/ProFTPD/Tests/Config/VirtualHost.pm
new file mode 100644
index 0000000..1d161b3
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Config/VirtualHost.pm
@@ -0,0 +1,139 @@
+package ProFTPD::Tests::Config::VirtualHost;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+use IO::Socket::INET;
+use Sys::Hostname;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  vhost_namebased_different_ports => {
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub vhost_namebased_different_ports {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'config');
+
+  my $host = hostname();
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    ServerName => 'Real Server',
+    SocketBindTight => 'off',
+    DefaultServer => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $vhost_port = $port + 3;
+  my $vhost_name = 'Virtual Server';
+
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+<VirtualHost $host>
+  Port $vhost_port
+  ServerName "$vhost_name"
+
+  WtmpLog off
+  TransferLog none
+
+  AuthUserFile $setup->{auth_user_file}
+  AuthGroupFile $setup->{auth_group_file}
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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($host, $vhost_port, 0);
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quit();
+
+      $self->assert(qr/$vhost_name/, $resp_msg,
+        "Expected '$vhost_name', got '$resp_msg'");
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/HTTP.pm b/tests/t/lib/ProFTPD/Tests/HTTP.pm
index 42867ac..ae1ed76 100644
--- a/tests/t/lib/ProFTPD/Tests/HTTP.pm
+++ b/tests/t/lib/ProFTPD/Tests/HTTP.pm
@@ -162,7 +162,7 @@ sub test_http_req {
     eval {
       sleep(1);
 
-      # To reproduce Bug#XXXX, we only need to connect to the server,
+      # To reproduce Bug#4143, we only need to connect to the server,
       # then issue an HTTP request.
 
       my $client = LWP::UserAgent->new(
@@ -174,9 +174,6 @@ sub test_http_req {
       my $req = HTTP::Request->new($req => "http://127.0.0.1:$port/path/to/some/file");
       my $resp = $client->request($req);
 
-use Data::Dumper;
-print STDERR "resp: ", Dumper($resp), "\n";
-
       if ($ENV{TEST_VERBOSE}) {
         print STDERR "# response: ", $resp->as_string, "\n";
       }
diff --git a/tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm b/tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm
index 4f711f2..9d0c9fd 100644
--- a/tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm
+++ b/tests/t/lib/ProFTPD/Tests/Logging/ExtendedLog.pm
@@ -21,6 +21,11 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
+  extlog_retr_default => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   extlog_retr_bug3137 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -56,6 +61,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  extlog_remote_port => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   extlog_protocol_version_quoted_bug3383 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -366,6 +376,86 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  extlog_exclusion_bug4067 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  extlog_sftp_ssh_sftp_bug4067 => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_sftp)],
+  },
+
+  extlog_sftp_ssh_sftp_exclusion_bug4067 => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_sftp)],
+  },
+
+  extlog_sftp_read_write_bug4067 => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_sftp)],
+  },
+
+  extlog_sftp_xfer_status_filtered => {
+    order => ++$order,
+    test_class => [qw(forking mod_sftp)],
+  },
+
+  extlog_var_file_offset => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  extlog_var_file_size_retr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  extlog_sftp_var_file_size_retr => {
+    order => ++$order,
+    test_class => [qw(forking mod_sftp)],
+  },
+
+  extlog_var_file_size_stor => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  extlog_sftp_var_file_size_stor => {
+    order => ++$order,
+    test_class => [qw(forking mod_sftp)],
+  },
+
+  extlog_var_transfer_type_retr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  extlog_var_transfer_type_stor => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  extlog_file_transfer_secs => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  extlog_file_transfer_millisecs_bug4218 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  extlog_response_millisecs_bug4218 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  extlog_stor_var_f_xfer_timed_out => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   # XXX Need unit tests for all LogFormat variables
 };
 
@@ -392,6 +482,214 @@ sub set_up {
   }
 }
 
+sub extlog_retr_default {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    ExtendedLog => "$ext_log ALL",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if (open(my $fh, "< $ext_log")) {
+    while (my $line = <$fh>) {
+      chomp($line);
+
+      if ($ENV{TEST_VERBOSE}) { 
+        print STDERR "$line\n";
+      }
+
+      if ($line !~ /^127\.0\.0\.1 UNKNOWN/) {
+        die("Unexpected ExtendedLog line: $line");
+      }
+
+      if ($line =~ /\"USER (\S+)\" (\d+) /) {
+        my $logged_user = $1;
+        my $resp_code = $2;
+
+        my $expected = $user;
+        $self->assert($expected eq $logged_user,
+          test_msg("Expected user '$expected', got '$logged_user'"));
+
+        $expected = '331';
+        $self->assert($expected eq $resp_code,
+          test_msg("Expected response code '$expected', got '$resp_code'"));
+
+      } elsif ($line =~ /\"PASS \(hidden\)\" (\d+) /) {
+        my $resp_code = $1;
+
+        my $expected = '230';
+        $self->assert($expected eq $resp_code,
+          test_msg("Expected response code '$expected', got '$resp_code'"));
+
+      } elsif ($line =~ /\"PASV\" (\d+) /) {
+        my $resp_code = $1;
+
+        my $expected = '227';
+        $self->assert($expected eq $resp_code,
+          test_msg("Expected response code '$expected', got '$resp_code'"));
+
+      } elsif ($line =~ /\"RETR (\S+)\" (\d+) (\d+)/) {
+        my $logged_path = $1;
+        my $resp_code = $2;
+        my $xfer_len = $3;
+
+        my $expected = $test_file;
+        $self->assert($expected eq $logged_path,
+          test_msg("Expected transferred path '$expected', got '$logged_path'"));
+
+        $expected = '226';
+        $self->assert($expected eq $resp_code,
+          test_msg("Expected response code '$expected', got '$resp_code'"));
+
+        $expected = 14;
+        $self->assert($expected == $xfer_len,
+          test_msg("Expected tranferred bytes $expected, got $xfer_len"));
+
+      } elsif ($line =~ /\"QUIT\" (\d+) /) {
+        my $resp_code = $1;
+
+        my $expected = '221';
+        $self->assert($expected eq $resp_code,
+          test_msg("Expected response code '$expected', got '$resp_code'"));
+
+      } else {
+        die("Unexpected ExtendedLog line: $line");
+      }
+    }
+
+    close($fh);
+
+  } else {
+    die("Can't read $ext_log: $!");
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 sub extlog_retr_bug3137 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -517,6 +815,11 @@ sub extlog_retr_bug3137 {
   if (open(my $fh, "< $ext_log")) {
     my $line = <$fh>;
     chomp($line);
+
+    if ($ENV{TEST_VERBOSE}) { 
+      print STDERR "$line\n";
+    }
+
     close($fh);
 
     if ($^O eq 'darwin') {
@@ -1737,25 +2040,127 @@ sub extlog_protocol_version_quoted_bug3383 {
   unlink($log_file);
 }
 
-sub extlog_rename_from {
+sub extlog_remote_port {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
 
-  my $config_file = "$tmpdir/extlog.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
 
-  my $log_file = test_get_logfile();
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $test_file = File::Spec->rel2abs($config_file);
+    LogFormat => 'custom "%m: %{remote-port}"',
+    ExtendedLog => "$ext_log ALL custom",
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
+    IfModules => {
+      '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, 0, 1);
+      $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);
+
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+      my $line;
+
+      while ($line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# line: $line\n";
+        }
+
+        if ($line =~ /^USER: \d+$/) {
+          $ok = 1;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok,
+        test_msg("Did not see expected remote port in ExtendedLog"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_rename_from {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $test_file = File::Spec->rel2abs($config_file);
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
@@ -2348,6 +2753,12 @@ sub extlog_ext_sftp_posix_rename_bug3949 {
       my $line = <$fh>;
       chomp($line);
 
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $src_file = '/private' . $src_file;
+        $dst_file = '/private' . $dst_file;
+      }
+
       my $expected = "- $src_file";
       $self->assert($expected eq $line,
         test_msg("Expected '$expected', got '$line'"));
@@ -2962,6 +3373,10 @@ sub extlog_dele_bug3469 {
     while (my $line = <$fh>) {
       chomp($line);
 
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "$line\n";
+      }
+
       # We're only interested in the DELE log line
       unless ($line =~ /^DELE (.*)$/i) {
         next;
@@ -4668,7 +5083,7 @@ sub extlog_ftps_raw_bytes_bug3554 {
           test_msg("Expected $expected_min - $expected_max, got $bytes_in"));
 
         $expected_min = 6828;
-        $expected_max = 8140;
+        $expected_max = 9140;
         $self->assert($expected_min <= $bytes_out &&
                       $expected_max >= $bytes_out,
           test_msg("Expected $expected_min - $expected_max, got $bytes_out"));
@@ -9844,7 +10259,7 @@ EOC
   unlink($log_file);
 }
 
-sub ftps_data_xfer_cb {
+sub ftps_data_xfer_cancelled_cb {
   my $func_name = shift;
   my $data = shift;
   my $data_len = shift;
@@ -9853,7 +10268,7 @@ sub ftps_data_xfer_cb {
 
   if ($total_len > 0) {
     $user_data->close();
-    croak("FOO!");
+    die("$func_name failed due to test callback (len $total_len > 0)");
   }
 }
 
@@ -10003,7 +10418,7 @@ EOC
         die("Can't set transfer mode to binary: " . $client->last_message());
       }
 
-      $client->set_callback(\&ftps_data_xfer_cb, \&ftps_data_close_cb, $client);
+      $client->set_callback(\&ftps_data_xfer_cancelled_cb, \&ftps_data_close_cb, $client);
 
       unless ($client->get('test.txt', '/dev/null')) {
         die("Can't download 'test.txt': " .  $client->last_message());
@@ -10047,12 +10462,17 @@ EOC
       while (my $line = <$fh>) {
         chomp($line);
 
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "$line\n";
+        }
+
         if ($line =~ /^(\S+) (.*)?$/) {
           my $cmd = $1;
           my $xfer_status = $2;
 
           if ($cmd eq 'RETR') {
-            if ($xfer_status eq 'cancelled') {
+            if ($xfer_status eq 'cancelled' ||
+                $xfer_status eq 'failed') {
               $expected_xfer_status = 1;
               last;
             }
@@ -10084,7 +10504,7 @@ EOC
   unlink($log_file);
 }
 
-sub ftps_data_xfer_cb {
+sub ftps_data_xfer_failed_cb {
   my $func_name = shift;
   my $data = shift;
   my $data_len = shift;
@@ -10244,7 +10664,7 @@ EOC
         die("Can't set transfer mode to binary: " . $client->last_message());
       }
 
-      $client->set_callback(\&ftps_data_xfer_cb, \&ftps_data_close_cb, $client);
+      $client->set_callback(\&ftps_data_xfer_failed_cb, \&ftps_data_close_cb, $client);
 
       if ($client->get('test.txt', '/dev/null')) {
         die("Download of 'test.txt' succeeded unexpectedly");
@@ -11425,6 +11845,11 @@ sub extlog_micros_ts_bug3889 {
   if (open(my $fh, "< $ext_log")) {
     my $line = <$fh>;
     chomp($line);
+
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "$line\n";
+    }
+
     close($fh);
 
     if ($line =~ /^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2},\d{6}\s+(.*)?$/) {
@@ -11727,6 +12152,11 @@ sub extlog_iso8601_ts_bug3889 {
   if (open(my $fh, "< $ext_log")) {
     my $line = <$fh>;
     chomp($line);
+
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "$line\n";
+    }
+
     close($fh);
 
     if ($line =~ /^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2},\d{3}\s+(.*)?$/) {
@@ -12105,4 +12535,2618 @@ sub extlog_var_basename_bug3987 {
   unlink($log_file);
 }
 
+sub extlog_exclusion_bug4067 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $test_file = File::Spec->rel2abs($config_file);
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    LogFormat => 'custom "%m"',
+    ExtendedLog => "$ext_log ALL,!AUTH custom",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      $client->feat();
+      $client->pwd();
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  # Now, read in the ExtendedLog, and see whether the %f variable was
+  # properly written out.  Bug#3137 occurred because the session.xfer.path
+  # variable was cleared out, as part of cleaning up the data connection,
+  # too early.  The fix is to use session.notes, which also has that path
+  # information.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 1;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "$line\n";
+        }
+
+        if ($line eq 'USER' ||
+            $line eq 'PASS') {
+          $ok = 0;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok, test_msg("Unexpected ExtendedLog messages logged"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub extlog_sftp_ssh_sftp_bug4067 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$home_dir/sub.d");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    LogFormat => 'custom "%m"',
+    ExtendedLog => "$ext_log SFTP custom",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $dir = $sftp->opendir('sub.d');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory 'sub.d': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 1;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "$line\n";
+        }
+
+        if ($line eq 'USER' ||
+            $line eq 'PASS' ||
+            $line eq 'MLSD') {
+          $ok = 0;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok, test_msg("Unexpected ExtendedLog lines seen"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub extlog_sftp_ssh_sftp_exclusion_bug4067 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$home_dir/sub.d");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    LogFormat => 'custom "%m"',
+    ExtendedLog => "$ext_log DIRS,!SSH,!SFTP custom",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $dir = $sftp->opendir('sub.d');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory 'sub.d': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 1;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "$line\n";
+        }
+
+        if ($line ne 'USER' &&
+            $line ne 'PASS' &&
+            $line ne 'MLSD') {
+          $ok = 0;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Unexpected ExtendedLog lines seen"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub extlog_sftp_read_write_bug4067 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$home_dir/sub.d");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    LogFormat => 'custom "%m"',
+    ExtendedLog => "$ext_log READ,WRITE custom",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $count = 5;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 1024;
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $write_ok = 0;
+      my $read_ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "$line\n";
+        }
+
+        if ($write_ok and $read_ok) {
+          last;
+        }
+
+        if ($line eq 'WRITE') {
+          $write_ok = 1;
+          next;
+        }
+
+        if ($line eq 'READ') {
+          $read_ok = 1;
+          next;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($write_ok and $read_ok,
+        test_msg("Unexpected ExtendedLog lines seen"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub extlog_sftp_xfer_status_filtered {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    PathDenyFilter => '^.*\.csv$',
+    LogFormat => 'custom "%m %{transfer-status}"',
+    ExtendedLog => "$ext_log READ,WRITE custom",
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.csv', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      if ($fh) {
+        die("Open of test.csv succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        "Expected error $expected, got $err_name");
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $saw_failed = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "$line\n";
+        }
+
+        if ($line eq 'STOR failed') {
+          $saw_failed = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($saw_failed, "Expected ExtendedLog line not seen");
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub extlog_var_file_offset {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/extlog.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/extlog.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/extlog.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/extlog.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/extlog.group");
+
+  my $test_file = File::Spec->rel2abs($config_file);
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $offset = 1;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    LogFormat => 'custom "%f: %{file-offset}"',
+    ExtendedLog => "$ext_log READ custom",
+    AllowRetrieveRestart => 'true',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      $client->type('binary');
+
+      my ($resp_code, $resp_msg) = $client->rest($offset);
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Restarting at $offset. Send STORE or RETRIEVE to initiate transfer";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  # Now, read in the ExtendedLog, and see whether the %{note:file-offset}
+  # variable was properly written out.
+  if (open(my $fh, "< $ext_log")) {
+    my $ok = 0;
+
+    my $line = <$fh>;
+    chomp($line);
+    close($fh);
+
+    if ($line =~ /^(.*?): (\d+)$/) {
+      my $path = $1;
+      my $pos = $2;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      $self->assert($test_file eq $path,
+        test_msg("Expected '$test_file', got '$path'"));
+
+      $self->assert($pos == $offset,
+        test_msg("Expected offset $offset, got $pos"));
+
+      $ok = 1;
+    }
+
+    $self->assert($ok,
+      test_msg("ExtendedLog message '$line' did not contain expected content"));
+
+  } else {
+    die("Can't read $ext_log: $!");
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub extlog_var_file_size_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%f: %{file-size}"',
+    ExtendedLog => "$ext_log READ custom",
+    AllowRetrieveRestart => 'true',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $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);
+
+  # Now, read in the ExtendedLog, and see whether the %{file-size} variable
+  # was properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      my $line = <$fh>;
+      chomp($line);
+      close($fh);
+
+      if ($line =~ /^(.*?): (\d+)$/) {
+        my $path = $1;
+        my $size = $2;
+
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack
+          $test_file = '/private' . $test_file;
+        }
+
+        $self->assert($test_file eq $path,
+          test_msg("Expected '$test_file', got '$path'"));
+
+        my $expected = -s $setup->{config_file};
+        $self->assert($expected == $size,
+          test_msg("Expected size $expected, got $size"));
+
+        $ok = 1;
+      }
+
+      $self->assert($ok,
+        test_msg("ExtendedLog message '$line' did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_sftp_var_file_size_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 sftp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%m: %f: %{file-size}"',
+    ExtendedLog => "$ext_log READ custom",
+    AllowRetrieveRestart => 'true',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'extlog.conf';
+      my $fh = $sftp->open($path, O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $path: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+    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);
+
+  # Now, read in the ExtendedLog, and see whether the %{file-size} variable
+  # was properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($line =~ /^(\S+): (.*?): (\d+)$/) {
+          my $req = $1;
+          my $path = $2;
+          my $size = $3;
+
+          if ($ENV{TEST_VERBOSE}) {
+            print STDERR "# line: $line\n";
+          }
+
+          if ($req ne 'RETR') {
+            next;
+          }
+
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack
+            $test_file = '/private' . $test_file;
+          }
+
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
+
+          my $expected = -s $setup->{config_file};
+          $self->assert($expected == $size,
+            test_msg("Expected size $expected, got $size"));
+
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok,
+        test_msg("ExtendedLog did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_var_file_size_stor {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%f: %{file-size}"',
+    ExtendedLog => "$ext_log WRITE custom",
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->stor_raw($test_file);
+      unless ($conn) {
+        die("Failed to STOR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $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);
+
+  # Now, read in the ExtendedLog, and see whether the %{file-size} variable
+  # was properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      my $line = <$fh>;
+      chomp($line);
+      close($fh);
+
+      if ($line =~ /^(.*?): (\d+)$/) {
+        my $path = $1;
+        my $size = $2;
+
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack
+          $test_file = '/private' . $test_file;
+        }
+
+        $self->assert($test_file eq $path,
+          test_msg("Expected '$test_file', got '$path'"));
+
+        my $expected = -s $test_file;
+        $self->assert($expected == $size,
+          test_msg("Expected size $expected, got $size"));
+
+        $ok = 1;
+      }
+
+      $self->assert($ok,
+        test_msg("ExtendedLog message '$line' did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_sftp_var_file_size_stor {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 sftp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%m: %f: %{file-size}"',
+    ExtendedLog => "$ext_log WRITE custom",
+    AllowRetrieveRestart => 'true',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.txt';
+      my $fh = $sftp->open($path, O_WRONLY|O_CREAT);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $path: [$err_name] ($err_code)");
+      }
+
+      my $buf = "Hello, World!\n";
+      my $res = $fh->write($buf);
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+    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);
+
+  # Now, read in the ExtendedLog, and see whether the %{file-size} variable
+  # was properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($line =~ /^(\S+): (.*?): (\d+)$/) {
+          my $req = $1;
+          my $path = $2;
+          my $size = $3;
+
+          if ($ENV{TEST_VERBOSE}) {
+            print STDERR "# line: $line\n";
+          }
+
+          if ($req ne 'STOR') {
+            next;
+          }
+
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack
+            $test_file = '/private' . $test_file;
+          }
+
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
+
+          my $expected = -s $test_file;
+          $self->assert($expected == $size,
+            test_msg("Expected size $expected, got $size"));
+
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok,
+        test_msg("ExtendedLog did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_var_transfer_type_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%f: %{transfer-type}"',
+    ExtendedLog => "$ext_log READ custom",
+    AllowRetrieveRestart => 'true',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->type('ascii');
+
+      $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $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);
+
+  # Now, read in the ExtendedLog, and see whether the %{transfer-type} variable
+  # was properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# line: $line\n";
+        }
+
+        if ($line =~ /^(.*?): (\S+)$/) {
+          my $path = $1;
+          my $xfer_type = $2;
+
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
+
+          my $expected = 'binary';
+          if ($ok == 1) {
+            $expected = 'ASCII';
+          }
+          $self->assert($expected eq $xfer_type,
+            test_msg("Expected transfer type '$expected', got '$xfer_type'"));
+
+          $ok++;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok == 2,
+        test_msg("ExtendedLog did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_var_transfer_type_stor {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%f: %{transfer-type}"',
+    ExtendedLog => "$ext_log WRITE custom",
+
+    AllowOverwrite => 'true',
+    AllowStoreRestart => 'true',
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->stor_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->type('ascii');
+
+      $conn = $client->stor_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $conn->write($buf, length($buf), 30);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $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);
+
+  # Now, read in the ExtendedLog, and see whether the %{transfer-type} variable
+  # was properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# line: $line\n";
+        }
+
+        if ($line =~ /^(.*?): (\S+)$/) {
+          my $path = $1;
+          my $xfer_type = $2;
+
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
+
+          my $expected = 'binary';
+          if ($ok == 1) {
+            $expected = 'ASCII';
+          }
+          $self->assert($expected eq $xfer_type,
+            test_msg("Expected transfer type '$expected', got '$xfer_type'"));
+
+          $ok++;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok == 2,
+        test_msg("ExtendedLog did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_file_transfer_secs {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 8192;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    # Throttle the transfer, to aid in the timing
+    TransferRate => 'RETR 1',
+
+    LogFormat => 'custom "%f: %T"',
+    ExtendedLog => "$ext_log READ custom",
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my ($buf, $data);
+      while ($conn->read($data, 8192, 30)) {
+        $buf .= $data;
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  # Now, read in the ExtendedLog, and see whether the %T variable was properly
+  # written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      my $line = <$fh>;
+      chomp($line);
+      close($fh);
+
+      if ($line =~ /^(.*?): (\S+)$/) {
+        my $path = $1;
+        my $secs = $2;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# ExtendedLog: $line\n";
+        }
+
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack
+          $test_file = '/private' . $test_file;
+        }
+
+        $self->assert($test_file eq $path,
+          test_msg("Expected '$test_file', got '$path'"));
+
+        my $expected_min = 9;
+        $self->assert($expected_min < $secs,
+          test_msg("Expected at least transfer seconds $expected_min, got $secs"));
+
+        $ok = 1;
+      }
+
+      $self->assert($ok,
+        test_msg("ExtendedLog message '$line' did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_file_transfer_millisecs_bug4218 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 8192;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    # Throttle the transfer, to aid in the timing
+    TransferRate => 'RETR 1',
+
+    LogFormat => 'custom "%f: %{transfer-millisecs}"',
+    ExtendedLog => "$ext_log READ custom",
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my ($buf, $data);
+      while ($conn->read($data, 8192, 30)) {
+        $buf .= $data;
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  # Now, read in the ExtendedLog, and see whether the %T variable was properly
+  # written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      my $line = <$fh>;
+      chomp($line);
+      close($fh);
+
+      if ($line =~ /^(.*?): (\d+)$/) {
+        my $path = $1;
+        my $secs = $2;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# ExtendedLog: $line\n";
+        }
+
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack
+          $test_file = '/private' . $test_file;
+        }
+
+        $self->assert($test_file eq $path,
+          test_msg("Expected '$test_file', got '$path'"));
+
+        my $expected_min = 9000;
+        $self->assert($expected_min < $secs,
+          test_msg("Expected at least transfer seconds $expected_min, got $secs"));
+
+        $ok = 1;
+      }
+
+      $self->assert($ok,
+        test_msg("ExtendedLog message '$line' did not contain expected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_response_millisecs_bug4218 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 8192;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    # Throttle the transfer, to aid in the timing
+    TransferRate => 'RETR 1',
+
+    LogFormat => 'custom "%m: %R"',
+    ExtendedLog => "$ext_log ALL custom",
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my ($buf, $data);
+      while ($conn->read($data, 8192, 30)) {
+        $buf .= $data;
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  # Now, read in the ExtendedLog, and see whether the %T variable was properly
+  # written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# ExtendedLog: $line\n";
+        }
+
+        if ($line =~ /^(.*?): (\d+)$/) {
+          $ok = 1;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok, test_msg("ExtendedLog contained unexpected content"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub extlog_stor_var_f_xfer_timed_out {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'extlog');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $dst_file = File::Spec->rel2abs("$sub_dir/dst.dat");
+  my $timeout_stalled = 2;
+
+  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 => 'response:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    LogFormat => 'custom "%m %s: %f"',
+    ExtendedLog => "$ext_log ALL custom",
+    TimeoutStalled => $timeout_stalled,
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->cwd('sub.d');
+
+      my $conn = $client->stor_raw('dst.dat');
+      unless ($conn) {
+        die("Failed to STOR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Foo";
+      $conn->write($buf, length($buf));
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sleeping for $timeout_stalled secs\n";
+      }
+      sleep($timeout_stalled + 1);
+
+      # Normally we would send a QUIT, but a TimeoutStalled timer would
+      # have disconnected us.
+    };
+    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);
+
+  # Now, read in the ExtendedLog, and see whether the %f variable was
+  # properly written out.
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $expected = $dst_file;
+      my $first_ok = 0;
+      my $second_ok = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $expected = '/private' . $dst_file;
+      }
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($line =~ /^STOR 150: (\S+)$/) {
+          my $path = $1;
+
+          if ($path eq $expected) {
+            $first_ok = 1;
+          }
+        }
+
+        if ($line =~ /^STOR 426: (\S+)$/) {
+          my $path = $1;
+
+          if ($path eq $expected) {
+            $second_ok = 1;
+          }
+        }
+      }
+
+      close($fh);
+
+      $self->assert($first_ok and $second_ok,
+        test_msg("Expected ExtendedLog messages did not appear"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Logins.pm b/tests/t/lib/ProFTPD/Tests/Logins.pm
index a90e5bc..795bce1 100644
--- a/tests/t/lib/ProFTPD/Tests/Logins.pm
+++ b/tests/t/lib/ProFTPD/Tests/Logins.pm
@@ -17,11 +17,16 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
-  login_plaintext_fails => {
+  login_plaintext_fails_bad_password => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  login_plaintext_fails_empty_password_bug4139 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   login_anonymous_ok => {
     order => ++$order,
     test_class => [qw(forking rootprivs)],
@@ -37,11 +42,16 @@ my $TESTS = {
     test_class => [qw(forking rootprivs)],
   },
 
-  login_anonymous_fails => {
+  login_anonymous_fails_bad_perms => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  login_anonymous_fails_empty_password_bug4139 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   login_anonymous_symlink_dir_ok => {
     order => ++$order,
     test_class => [qw(forking rootprivs)],
@@ -67,6 +77,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  login_reauthenticate_fails_bug3736 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  login_reauthenticate_ok_same_user_bug4217 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  login_reauthenticate_fails_different_user_bug4217 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  login_reauthenticate_fails_extra_pass_bug4217 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -77,7 +107,7 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
-sub login_plaintext_fails {
+sub login_plaintext_fails_bad_password {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -158,6 +188,130 @@ sub login_plaintext_fails {
   unlink($log_file);
 }
 
+sub login_plaintext_fails_empty_password_bug4139 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/login.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/login.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/login.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/login.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/login.group");
+
+  my $user = 'proftpd';
+  my $passwd = '';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowEmptyPasswords => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd) };
+      unless ($@) {
+        die("Logged in unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 sub login_anonymous_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -547,7 +701,7 @@ sub login_anonymous_user_alias_ok {
   unlink($log_file);
 }
 
-sub login_anonymous_fails {
+sub login_anonymous_fails_bad_perms {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -644,7 +798,7 @@ sub login_anonymous_fails {
   unlink($log_file);
 }
 
-sub login_anonymous_symlink_dir_ok {
+sub login_anonymous_fails_empty_password_bug4139 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -659,39 +813,19 @@ sub login_anonymous_symlink_dir_ok {
 
   my ($config_user, $config_group) = config_get_identity();
 
-  my $anon_dir = File::Spec->rel2abs("$tmpdir/users/anonymous");
+  my $anon_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
-  my $intermed_dir = File::Spec->rel2abs("$tmpdir/users");
-  mkpath($intermed_dir);
-
-  my $symlink_dst = File::Spec->rel2abs("$tmpdir/public_ftp");
-  mkpath($symlink_dst);
-
-  my $cwd = getcwd();
-
-  unless (chdir($intermed_dir)) {
-    die("Can't chdir to $intermed_dir: $!");
-  }
-
-  unless (symlink("../public_ftp", "./anonymous")) {
-    die("Can't symlink '../public_ftp' to './anonymous': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $symlink_dst)) {
-      die("Can't set perms on $symlink_dst to 0755: $!");
+    unless (chmod(0755, $anon_dir)) {
+      die("Can't set perms on $anon_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $symlink_dst)) {
-      die("Can't set owner of $symlink_dst to $uid/$gid: $!");
+    unless (chown($uid, $gid, $anon_dir)) {
+      die("Can't set owner of $anon_dir to $uid/$gid: $!");
     }
   }
 
@@ -721,6 +855,7 @@ sub login_anonymous_symlink_dir_ok {
         Group => $config_group,
         UserAlias => "anonymous $config_user",
         RequireValidShell => 'off',
+        AllowEmptyPasswords => 'off',
       },
     },
   };
@@ -743,20 +878,23 @@ sub login_anonymous_symlink_dir_ok {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->login($config_user, '') };
+      unless ($@) {
+        die("Anonymous login succeeded unexpectedly");
+      }
 
-      # In parent process, login anonymously to server using a plaintext
-      # password which SHOULD work.
-      my ($resp_code, $resp_msg) = $client->login($config_user, 'ftp at nospam.org');
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
-      $expected = 230;
+      $expected = 501;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'Anonymous access granted, restrictions apply';
+      $expected = 'Login incorrect.';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -793,7 +931,7 @@ sub login_anonymous_symlink_dir_ok {
   unlink($log_file);
 }
 
-sub login_anonymous_allowchrootsymlinks_bug3852 {
+sub login_anonymous_symlink_dir_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -853,13 +991,11 @@ sub login_anonymous_allowchrootsymlinks_bug3852 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10',
+    Trace => 'DEFAULT:10 privs:10',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    AllowChrootSymlinks => 'off',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -894,23 +1030,20 @@ sub login_anonymous_allowchrootsymlinks_bug3852 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->login($config_user, 'ftp at nospam.org') };
-      unless ($@) {
-        die("Anonymous login succeeded unexpectedly");
-      }
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      # In parent process, login anonymously to server using a plaintext
+      # password which SHOULD work.
+      my ($resp_code, $resp_msg) = $client->login($config_user, 'ftp at nospam.org');
 
       my $expected;
 
-      $expected = 530;
+      $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected response code $expected, got $resp_code"));
+        test_msg("Expected $expected, got $resp_code"));
 
-      $expected = 'Login incorrect.';
+      $expected = 'Anonymous access granted, restrictions apply';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected response message '$expected', got '$resp_msg'"));
+        test_msg("Expected '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -947,7 +1080,7 @@ sub login_anonymous_allowchrootsymlinks_bug3852 {
   unlink($log_file);
 }
 
-sub login_multiple_attempts_per_conn {
+sub login_anonymous_allowchrootsymlinks_bug3852 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -960,36 +1093,190 @@ sub login_multiple_attempts_per_conn {
   my $auth_user_file = File::Spec->rel2abs("$tmpdir/login.passwd");
   my $auth_group_file = File::Spec->rel2abs("$tmpdir/login.group");
 
-  my $test_file = File::Spec->rel2abs($config_file);
+  my ($config_user, $config_group) = config_get_identity();
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $anon_dir = File::Spec->rel2abs("$tmpdir/users/anonymous");
   my $uid = 500;
   my $gid = 500;
 
+  my $intermed_dir = File::Spec->rel2abs("$tmpdir/users");
+  mkpath($intermed_dir);
+
+  my $symlink_dst = File::Spec->rel2abs("$tmpdir/public_ftp");
+  mkpath($symlink_dst);
+
+  my $cwd = getcwd();
+
+  unless (chdir($intermed_dir)) {
+    die("Can't chdir to $intermed_dir: $!");
+  }
+
+  unless (symlink("../public_ftp", "./anonymous")) {
+    die("Can't symlink '../public_ftp' to './anonymous': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $symlink_dst)) {
+      die("Can't set perms on $symlink_dst to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $symlink_dst)) {
+      die("Can't set owner of $symlink_dst to $uid/$gid: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  auth_user_write($auth_user_file, $config_user, 'foo', $uid, $gid,
+    '/tmp', '/bin/bash');
+  auth_group_write($auth_group_file, $config_group, $gid, $config_user);
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    AllowChrootSymlinks => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+
+    Anonymous => {
+      $anon_dir => {
+        User => $config_user,
+        Group => $config_group,
+        UserAlias => "anonymous $config_user",
+        RequireValidShell => 'off',
+      },
+    },
+  };
+
+  my ($port, $user, $group) = config_write($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->login($config_user, 'ftp at nospam.org') };
+      unless ($@) {
+        die("Anonymous login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub login_multiple_attempts_per_conn {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/login.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/login.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/login.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/login.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/login.group");
+
+  my $test_file = File::Spec->rel2abs($config_file);
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
@@ -1308,4 +1595,330 @@ sub login_fails_bad_sequence {
   unlink($log_file);
 }
 
+sub login_reauthenticate_fails_bug3736 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'login');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $other_user = 'foobar';
+      eval { $client->user($other_user) };
+      unless ($@) {
+        die("Subsequent USER command succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Reauthentication not supported";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 login_reauthenticate_ok_same_user_bug4217 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'login');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->user($setup->{user});
+      my $expected = 230;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 login_reauthenticate_fails_different_user_bug4217 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'login');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      my $bad_user = "foobar";
+      eval { $client->user($bad_user) };
+      unless ($@) {
+        die("Subsequent USER with different name succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Reauthentication not supported";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 login_reauthenticate_fails_extra_pass_bug4217 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'login');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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->login($setup->{user}, $setup->{passwd});
+
+      # We can't use login() here, since that will properly handle the 230
+      # response from the USER command.
+      $client->user($setup->{user});
+      eval { $client->pass($setup->{passwd}) };
+      unless ($@) {
+        die("Subsequent PASS succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 503;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "You are already logged in";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp.pm
new file mode 100644
index 0000000..be65e48
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp.pm
@@ -0,0 +1,1938 @@
+package ProFTPD::Tests::Modules::mod_auth_otp;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  auth_otp_hotp_host => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  # HOTP tests
+  auth_otp_hotp_login => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_consecutive_hotp_logins => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_hotp_unconfigured_user => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_hotp_authoritative => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  # TOTP tests
+  auth_otp_totp_login => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_consecutive_totp_logins => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_totp_unconfigured_user => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_totp_authoritative => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  # AuthOTPOptions tests
+  auth_otp_opt_std_response => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_opt_require_table_entry => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_opt_require_table_entry_authoritative => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sql mod_sql_sqlite)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  # Check for the required Perl modules:
+  #
+  #  Authen-OATH
+  #  MIME-Base32
+
+  my $required = [qw(
+    Authen::OATH
+    MIME::Base32
+  )];
+
+  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 build_db {
+  my $cmd = shift;
+  my $db_script = shift;
+  my $db_file = shift;
+  my $check_exit_status = shift;
+  $check_exit_status = 0 unless defined $check_exit_status;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  my $exit_status = $?;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  if ($check_exit_status) {
+    if ($? != 0) {
+      croak("'$cmd' failed");
+    }
+  }
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      croak("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  unlink($db_script);
+  return 1;
+}
+
+sub auth_otp_hotp_host {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:20 auth_otp:20 events:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultServer => 'on',
+    ServerName => '"Default Server"',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $host = 'localhost';
+
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+# This virtual host is name-based
+<VirtualHost 127.0.0.1>
+  Port $port
+  ServerAlias $host
+  ServerName "OTP Server"
+
+  AuthUserFile $setup->{auth_user_file}
+  AuthGroupFile $setup->{auth_group_file}
+
+  <IfModule mod_delay.c>
+    DelayEngine off
+  </IfModule>
+
+  <IfModule mod_auth_otp.c>
+    AuthOTPEngine on
+    AuthOTPLog $setup->{log_file}
+    AuthOTPAlgorithm hotp
+    AuthOTPTable sql:/get-user-hotp/update-user-hotp
+  </IfModule>
+
+  <IfModule mod_sql.c>
+    SQLEngine log
+    SQLBackend sqlite3
+    SQLConnectInfo $db_file
+    SQLLogFile $setup->{log_file}
+
+    SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"
+    SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp
+  </IfModule>
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      my ($resp_code, $resp_msg) = $client->host($host);
+      my $expected = 220;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      # Calculate HOTP
+      my $hotp = $oath->hotp($secret, $counter);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated HOTP $hotp for counter ", $counter, "\n";
+      }
+
+      $client->login($setup->{user}, $hotp);
+      $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 auth_otp_hotp_login {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Calculate HOTP
+      my $hotp = $oath->hotp($secret, $counter);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated HOTP $hotp for counter ", $counter, "\n";
+      }
+
+      $client->login($setup->{user}, $hotp);
+      $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 auth_otp_consecutive_hotp_logins {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $nattempts = 5;
+      my $ok = 0;
+    
+      for (my $i = 0; $i < $nattempts; $i++) { 
+        my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+        # Calculate HOTP
+        my $next_counter = $counter + $i;
+        my $hotp = $oath->hotp($secret, $next_counter);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# Generated HOTP $hotp for counter $next_counter\n";
+        }
+
+        eval { $client->login($setup->{user}, $hotp) };
+        if ($@) {
+          next;
+        }
+
+        $client->quit();
+        $ok = 1;
+        last;
+      }
+
+      $self->assert($ok, test_msg("Failed to login successfully using HOTP"));
+    };
+
+    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 auth_otp_hotp_unconfigured_user {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Calculate HOTP
+      my $hotp = $oath->hotp($secret, $counter);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated HOTP $hotp for counter ", $counter, "\n";
+      }
+
+      eval { $client->login($setup->{user}, $hotp) };
+      unless ($@) {
+        die("HOTP login for user $setup->{user} succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 530;
+      $self->assert($resp_code == $expected,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($resp_msg eq $expected,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Try again, this time using the real password.  Login should
+      # succeed, since we have not configured mod_auth_otp to be authoritative.
+      $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 auth_otp_hotp_authoritative {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', 1);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_otp.c* mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Calculate HOTP
+      my $hotp = $oath->hotp($secret, $counter);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated HOTP $hotp for counter ", $counter, "\n";
+      }
+
+      eval { $client->login($setup->{user}, $hotp) };
+      unless ($@) {
+        die("HOTP login for user $setup->{user} succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 530;
+      $self->assert($resp_code == $expected,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($resp_msg eq $expected,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Try again, this time using the real password.  Login should
+      # still fail, since we require the OTP to be valid.
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Password login for user $setup->{user} 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 auth_otp_totp_login {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', 0);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Calculate TOTP
+      my $totp = $oath->totp($secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      $client->login($setup->{user}, $totp);
+      $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 auth_otp_consecutive_totp_logins {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', 0);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $nattempts = 3; 
+      my $now = time();
+      my $ok = 0;
+
+      for (my $i = 0; $i < $nattempts; $i++) {
+        my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+        # Calculate TOTP
+        my $ts = $now;
+
+        # Try one "time step" behind and ahead.
+        if ($i == 0) {
+          $ts = $now - 30;
+
+        } elsif ($i == 2) {
+          $ts = $now + 30;
+        }
+
+        my $totp = $oath->totp($secret, $ts);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# Generated TOTP $totp for ", scalar(localtime($ts)), "\n";
+        }
+
+        eval { $client->login($setup->{user}, $totp) };
+        if ($@) {
+          next;
+        }
+
+        $client->quit();
+        $ok = 1;
+        last;
+      }
+
+      $self->assert($ok, test_msg("Failed to login successfully using TOTP"));
+    };
+
+    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 auth_otp_totp_unconfigured_user {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Calculate TOTP
+      my $totp = $oath->totp($secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      eval { $client->login($setup->{user}, $totp) };
+      unless ($@) {
+        die("TOTP login for user $setup->{user} succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 530;
+      $self->assert($resp_code == $expected,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($resp_msg eq $expected,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Try again, this time using the real password.  Login should
+      # succeed, since we have not configured mod_auth_otp to be authoritative.
+      $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 auth_otp_totp_authoritative {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret) VALUES ('$setup->{user}', '$base32_secret');
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_otp.c* mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $oath = Authen::OATH->new();
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Calculate TOTP
+      my $ts = 10;
+      my $totp = $oath->totp($secret, $ts);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for ", scalar(gmtime($ts)), "\n";
+      }
+
+      eval { $client->login($setup->{user}, $totp) };
+      unless ($@) {
+        die("TOTP login for user $setup->{user} succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 530;
+      $self->assert($resp_code == $expected,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Login incorrect.';
+      $self->assert($resp_msg eq $expected,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Try again, this time using the real password.  Login should still
+      # fail since mod_auth_otp is authoritative.
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Password login for user $setup->{user} 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 auth_otp_opt_std_response {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+        AuthOTPOptions => 'StandardResponse',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      my ($resp_code, $resp_msg) = $client->user($setup->{user});
+
+      my $expected = 331;
+      $self->assert($resp_code == $expected,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Password required for $setup->{user}";
+      $self->assert($resp_msg eq $expected,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 auth_otp_opt_require_table_entry {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+        AuthOTPOptions => 'RequireTableEntry',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Even though we have not configured an entry in the OTP table
+      # for this user, and even though we are using the RequireTableEntry
+      # option, password-based login should still succeed, because we
+      # did not make mod_auth_otp authoritative.
+      $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 auth_otp_opt_require_table_entry_authoritative {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_otp.c* mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+        AuthOTPOptions => 'RequireTableEntry',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # We have not configured an entry in the OTP table for this user, and
+      # we are using the RequireTableEntry option, AND we marked mod_auth_otp
+      # as authoriative.  Thus password-based login should fail.
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Password login 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);
+}
+
+1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp/sftp.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp/sftp.pm
new file mode 100644
index 0000000..acc0bed
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_auth_otp/sftp.pm
@@ -0,0 +1,1283 @@
+package ProFTPD::Tests::Modules::mod_auth_otp::sftp;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Data::Dumper;
+use File::Spec;
+use IO::Handle;
+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 = {
+  # HOTP tests
+  auth_otp_sftp_hotp_login_ok => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_sftp_hotp_login_failed => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  # TOTP tests
+  auth_otp_sftp_totp_login_ok => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_sftp_totp_login_failed => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_sftp_totp_login_authoritative => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  auth_otp_sftp_totp_login_authoritative_failed => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  # Other tests
+  auth_otp_sftp_password_failed => {
+    order => ++$order,
+    test_class => [qw(forking mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+
+  # Check for the required Perl modules:
+  #
+  #  Authen-OATH
+  #  MIME-Base32
+
+  my $required = [qw(
+    Authen::OATH
+    MIME::Base32
+    Net::SSH2
+  )];
+
+  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 set_up {
+  my $self = shift;
+  $self->SUPER::set_up(@_);
+
+  # Make sure that mod_sftp does not complain about permissions on the hostkey
+  # files.
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  unless (chmod(0400, $rsa_host_key, $dsa_host_key)) {
+    die("Can't set perms on $rsa_host_key, $dsa_host_key: $!");
+  }
+}
+
+# Support routines
+
+sub build_db {
+  my $cmd = shift;
+  my $db_script = shift;
+  my $db_file = shift;
+  my $check_exit_status = shift;
+  $check_exit_status = 0 unless defined $check_exit_status;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  my $exit_status = $?;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  if ($check_exit_status) {
+    if ($? != 0) {
+      croak("'$cmd' failed");
+    }
+  }
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      croak("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  unlink($db_script);
+  return 1;
+}
+
+# Tests
+
+sub auth_otp_sftp_hotp_login_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+  my $bad_secret = 'B at d1YK3pts3kr3T!';
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 sftp:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        # Configure mod_sftp to only use the keyboard-interactive method.
+        # NOTE: How to handle this when both mod_auth_otp AND mod_sftp_pam
+        # are used/loaded?
+        'SFTPAuthMethods keyboard-interactive',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate HOTP
+      my $oath = Authen::OATH->new();
+      my $hotp = $oath->hotp($secret, $counter);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated HOTP $hotp for counter ", $counter, "\n";
+      }
+
+      unless ($ssh2->auth_keyboard($setup->{user}, $hotp)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+    };
+    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 auth_otp_sftp_hotp_login_failed {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create HOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/hotp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+  my $bad_secret = 'B at d1YK3pts3kr3T!';
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'hotp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-hotp/update-user-hotp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        # Configure mod_sftp to only use the keyboard-interactive method.
+        # NOTE: How to handle this when both mod_auth_otp AND mod_sftp_pam
+        # are used/loaded?
+        'SFTPAuthMethods keyboard-interactive',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-hotp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-hotp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate HOTP
+      my $oath = Authen::OATH->new();
+      my $hotp = $oath->hotp($bad_secret, $counter);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated HOTP $hotp for counter ", $counter, "\n";
+      }
+
+      if ($ssh2->auth_keyboard($setup->{user}, $hotp)) {
+        die("Login to SSH2 server succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+      $self->assert($err_str, qr/Authentication failed \(keyboard-interactive\)/,
+        test_msg("Expected 'Authentication failed (keyboard-interactive), got '$err_str'"));
+
+      $ssh2->disconnect();
+    };
+    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 auth_otp_sftp_totp_login_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        # Configure mod_sftp to only use the keyboard-interactive method.
+        # NOTE: How to handle this when both mod_auth_otp AND mod_sftp_pam
+        # are used/loaded?
+        'SFTPAuthMethods keyboard-interactive',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate TOTP
+      my $oath = Authen::OATH->new();
+      my $totp = $oath->totp($secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      unless ($ssh2->auth_keyboard($setup->{user}, $totp)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+    };
+    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 auth_otp_sftp_totp_login_failed {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+  my $bad_secret = 'B at d1YK3pts3kr3T!';
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        # Configure mod_sftp to only use the keyboard-interactive method.
+        # NOTE: How to handle this when both mod_auth_otp AND mod_sftp_pam
+        # are used/loaded?
+        'SFTPAuthMethods keyboard-interactive',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate TOTP
+      my $oath = Authen::OATH->new();
+      my $totp = $oath->totp($bad_secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      if ($ssh2->auth_keyboard($setup->{user}, $totp)) {
+        die("Login to SSH2 server succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+      $self->assert($err_str, qr/Authentication failed \(keyboard-interactive\)/,
+        test_msg("Expected 'Authentication failed (keyboard-interactive), got '$err_str'"));
+
+      $ssh2->disconnect();
+    };
+    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 auth_otp_sftp_totp_login_authoritative {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c mod_auth_otp.c*',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        # Configure mod_sftp to only use the keyboard-interactive method.
+        # NOTE: How to handle this when both mod_auth_otp AND mod_sftp_pam
+        # are used/loaded?
+        'SFTPAuthMethods keyboard-interactive',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate TOTP
+      my $oath = Authen::OATH->new();
+      my $totp = $oath->totp($secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      unless ($ssh2->auth_keyboard($setup->{user}, $totp)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+    };
+    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 auth_otp_sftp_totp_login_authoritative_failed {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+  my $bad_secret = 'B at d1YK3pts3kr3T!';
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c mod_auth_otp.c*',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        # Configure mod_sftp to only use the keyboard-interactive method.
+        # NOTE: How to handle this when both mod_auth_otp AND mod_sftp_pam
+        # are used/loaded?
+        'SFTPAuthMethods keyboard-interactive',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate TOTP
+      my $oath = Authen::OATH->new();
+      my $totp = $oath->totp($bad_secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      if ($ssh2->auth_keyboard($setup->{user}, $totp)) {
+        die("Login to SSH2 server succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+      $self->assert($err_str, qr/Authentication failed \(keyboard-interactive\)/,
+        test_msg("Expected 'Authentication failed (keyboard-interactive), got '$err_str'"));
+
+      $ssh2->disconnect();
+    };
+    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 auth_otp_sftp_password_failed {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'auth_otp');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create TOTP tables
+  my $db_script = File::Spec->rel2abs("$tmpdir/totp.sql");
+
+  # mod_auth_otp wants this secret to be base32-encoded, for interoperability
+  # with Google Authenticator.
+  require MIME::Base32;
+  MIME::Base32->import('RFC');
+
+  my $secret = 'Sup3rS3Cr3t';
+  my $base32_secret = MIME::Base32::encode($secret);
+  my $counter = 777;
+  my $bad_secret = 'B at d1YK3pts3kr3T!';
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE auth_otp (
+  user TEXT PRIMARY KEY,
+  secret TEXT,
+  counter INTEGER
+);
+INSERT INTO auth_otp (user, secret, counter) VALUES ('$setup->{user}', '$base32_secret', $counter);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_auth_otp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:20 ssh2:20 auth_otp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c mod_auth_otp.c*',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_auth_otp.c' => {
+        AuthOTPEngine => 'on',
+        AuthOTPLog => $setup->{log_file},
+        AuthOTPAlgorithm => 'totp',
+
+        # Assumes default table names, column names
+        AuthOTPTable => 'sql:/get-user-totp/update-user-totp',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        'SFTPAuthMethods password',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+
+        'SQLNamedQuery get-user-totp SELECT "secret, counter FROM auth_otp WHERE user = \'%{0}\'"',
+        'SQLNamedQuery update-user-totp UPDATE "counter = %{1} WHERE user = \'%{0}\'" auth_otp',
+      ],
+    },
+  };
+
+  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;
+
+  require Authen::OATH;
+  require Net::SSH2;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(2);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # Calculate TOTP
+      my $oath = Authen::OATH->new();
+      my $totp = $oath->totp($bad_secret);
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Generated TOTP $totp for current time\n";
+      }
+
+      if ($ssh2->auth_password($setup->{user}, $totp)) {
+        die("Password authentication to SSH2 server succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+      $self->assert($err_str, qr/Authentication failed \(password\)/,
+        test_msg("Expected 'Authentication failed (password), got '$err_str'"));
+
+      $ssh2->disconnect();
+    };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_ban/memcache.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_ban/memcache.pm
index c150de2..50c4995 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_ban/memcache.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_ban/memcache.pm
@@ -21,6 +21,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  ban_memcache_json_max_login_attempts_bug4056 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -56,59 +61,207 @@ sub list_tests {
 sub ban_memcache_max_login_attempts {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'ban');
+
+  my $ban_tab = File::Spec->rel2abs("$tmpdir/ban.tab");
+  my $memcached_servers = $ENV{MEMCACHED_SERVERS} ? $ENV{MEMCACHED_SERVERS} : '127.0.0.1:11211';
 
-  my $config_file = "$tmpdir/ban.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/ban.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/ban.scoreboard");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ban:20 memcache:20',
 
-  my $log_file = File::Spec->rel2abs('tests.log');
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $ban_tab = File::Spec->rel2abs("$tmpdir/ban.tab");
+    MaxLoginAttempts => 1,
+
+    IfModules => {
+      'mod_ban.c' => {
+        BanEngine => 'on',
+        BanLog => $setup->{log_file},
+
+        # This says to ban a client which exceeds the MaxLoginAttempts
+        # limit once within the last 1 minute will be banned for 3 min
+        BanOnEvent => 'MaxLoginAttempts 1/00:01:00 00:03:00',
+
+        BanTable => $ban_tab,
+
+        BanCache => 'memcache',
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_memcache.c' => {
+        MemcacheEngine => 'on',
+        MemcacheLog => $setup->{log_file},
+        MemcacheServers => $memcached_servers,
+      },
+    },
+  };
+
+  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, 0, 1);
+      eval { $client->login($setup->{user}, 'foo') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, undef, 0); };
+      unless ($@) {
+        die("Connect succeeded unexpectedly");
+      }
+
+      my $conn_ex = ProFTPD::TestSuite::FTP::get_connect_exception();
+
+      my $expected = "";
+      $self->assert($expected eq $conn_ex,
+        test_msg("Expected '$expected', got '$conn_ex'"));
+    };
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/ban.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/ban.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-  
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $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) {
+    die($ex);
+  }
+
+  # Close the pipe, then re-open it for the second daemon
+  close($rfh);
+  close($wfh);
+
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Fork child
+  defined($pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Now try again with the correct info; we should be banned.  Note
+      # that we have to create a separate connection for this.
+
+      # Give the server some time to start up.
+      sleep(2);
+
+      eval { ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, undef, 0); };
+      unless ($@) {
+        die("Connect succeeded unexpectedly");
+      }
+
+      my $conn_ex = ProFTPD::TestSuite::FTP::get_connect_exception();
+
+      # If we see an exception of "Net::FTP: connect: Connection refused",
+      # it means that the daemon hadn't even started up yet, which is not
+      # the same as listening but rejecting our connection.
 
+      my $expected = '';
+      $self->assert($expected eq $conn_ex,
+        test_msg("Expected '$expected', got '$conn_ex'"));
+    };
+
+    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 ban_memcache_json_max_login_attempts_bug4056 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'ban');
+
+  my $ban_tab = File::Spec->rel2abs("$tmpdir/ban.tab");
   my $memcached_servers = $ENV{MEMCACHED_SERVERS} ? $ENV{MEMCACHED_SERVERS} : '127.0.0.1:11211';
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'memcache:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ban:20 memcache:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     MaxLoginAttempts => 1,
 
     IfModules => {
       'mod_ban.c' => {
         BanEngine => 'on',
-        BanLog => $log_file,
+        BanLog => $setup->{log_file},
 
         # This says to ban a client which exceeds the MaxLoginAttempts
         # limit once within the last 1 minute will be banned for 3 min
@@ -117,6 +270,7 @@ sub ban_memcache_max_login_attempts {
         BanTable => $ban_tab,
 
         BanCache => 'memcache',
+        BanCacheOptions => 'UseJSON',
       },
 
       'mod_delay.c' => {
@@ -125,13 +279,14 @@ sub ban_memcache_max_login_attempts {
 
       'mod_memcache.c' => {
         MemcacheEngine => 'on',
-        MemcacheLog => $log_file,
+        MemcacheLog => $setup->{log_file},
         MemcacheServers => $memcached_servers,
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -148,28 +303,24 @@ sub ban_memcache_max_login_attempts {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      eval { $client->login($user, 'foo') };
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      eval { $client->login($setup->{user}, 'foo') };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Login incorrect.";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       eval { ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, undef, 0); };
       unless ($@) {
@@ -191,7 +342,7 @@ sub ban_memcache_max_login_attempts {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -201,7 +352,7 @@ sub ban_memcache_max_login_attempts {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
@@ -224,7 +375,7 @@ sub ban_memcache_max_login_attempts {
       # Now try again with the correct info; we should be banned.  Note
       # that we have to create a separate connection for this.
 
-      # Give the server some time to start up.      
+      # Give the server some time to start up.
       sleep(2);
 
       eval { ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, undef, 0); };
@@ -251,7 +402,7 @@ sub ban_memcache_max_login_attempts {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -261,15 +412,11 @@ sub ban_memcache_max_login_attempts {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_copy.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_copy.pm
index 00d8610..778bff8 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_copy.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_copy.pm
@@ -21,7 +21,7 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  copy_file_no_login => {
+  copy_file_no_login_bug4169 => {
     order => ++$order,
     test_class => [qw(bug forking)],
   },
@@ -91,7 +91,7 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
-  copy_cpfr_cpto_no_login => {
+  copy_cpfr_cpto_no_login_bug4169 => {
     order => ++$order,
     test_class => [qw(bug forking)],
   },
@@ -111,6 +111,16 @@ my $TESTS = {
     test_class => [qw(forking rootprivs)],
   },
 
+  copy_cpto_quotatab_file_dst_enopc_bug4262 => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_quotatab_sql mod_sql_sqlite)],
+  },
+
+  copy_cpto_timeout_bug4263 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -273,7 +283,7 @@ sub copy_file {
   unlink($log_file);
 }
 
-sub copy_file_no_login {
+sub copy_file_no_login_bug4169 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2747,7 +2757,7 @@ sub copy_cpfr_cpto {
   unlink($log_file);
 }
 
-sub copy_cpfr_cpto_no_login {
+sub copy_cpfr_cpto_no_login_bug4169 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -3326,4 +3336,320 @@ EOC
   unlink($log_file);
 }
 
+sub copy_cpto_quotatab_file_dst_enopc_bug4262 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'copy');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$setup->{user}', '$setup->{passwd}', $setup->{uid}, $setup->{gid}, '$setup->{home_dir}', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('ftpd', $setup->{gid}, '$setup->{user}');
+
+CREATE TABLE quotalimits (
+  name TEXT NOT NULL,
+  quota_type TEXT NOT NULL,
+  per_session TEXT NOT NULL,
+  limit_type TEXT NOT NULL,
+  bytes_in_avail REAL NOT NULL,
+  bytes_out_avail REAL NOT NULL,
+  bytes_xfer_avail REAL NOT NULL,
+  files_in_avail INTEGER NOT NULL,
+  files_out_avail INTEGER NOT NULL,
+  files_xfer_avail INTEGER NOT NULL
+);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$setup->{user}', 'user', 'false', 'hard', 32, 0, 0, 2, 0, 0);
+
+CREATE TABLE quotatallies (
+  name TEXT NOT NULL,
+  quota_type TEXT NOT NULL,
+  bytes_in_used REAL NOT NULL,
+  bytes_out_used REAL NOT NULL,
+  bytes_xfer_used REAL NOT NULL,
+  files_in_used INTEGER NOT NULL,
+  files_out_used INTEGER NOT NULL,
+  files_xfer_used INTEGER NOT NULL
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $src_file = File::Spec->rel2abs("$setup->{home_dir}/foo.txt");
+  if (open(my $fh, "> $src_file")) {
+    # Make sure this file is larger than the allowed number of upload bytes
+    print $fh "Hello, World!\n" x 1024;
+
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$setup->{home_dir}/bar.txt");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20',
+
+    DefaultChdir => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+     'mod_quotatab_sql.c' => [
+        'SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM quotalimits WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM quotatallies WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = \'%{6}\' AND quota_type = \'%{7}\'" quotatallies',
+        'SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" quotatallies',
+
+        'QuotaEngine on',
+        "QuotaLog $setup->{log_file}",
+        'QuotaLimitTable sql:/get-quota-limit',
+        'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+      ],
+
+      'mod_sql.c' => {
+        AuthOrder => 'mod_sql.c',
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $setup->{log_file},
+        SQLMinID => '0',
+      },
+    },
+  };
+
+  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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->site('CPFR', 'foo.txt');
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->site('CPTO', 'bar.txt') };
+      unless ($@) {
+        die("SITE CPTO succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+
+      $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $client->quit();
+
+      $self->assert(!-f $dst_file,
+        test_msg("File $dst_file exists unexpectedly"));
+    };
+
+    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 copy_cpto_timeout_bug4263 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'copy');
+
+  my $src_file = File::Spec->rel2abs("$tmpdir/foo.dat");
+  if (open(my $fh, "> $src_file")) {
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "# Writing source file\n";
+    }
+
+    my $count = 20000;
+    for (my $i = 0; $i < $count; $i++) {
+      print $fh "AbCdEfGh" x 32768;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/bar.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'copy:20 timer:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => 3,
+
+    IfModules => {
+      '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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->site('CPFR', 'foo.dat');
+
+      my $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "File or directory exists, ready for destination name";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->site('CPTO', 'bar.dat');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Copy successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      unless (-f $dst_file) {
+        die("File $dst_file does not exist as expected");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_ctrls.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_ctrls.pm
index 190dffa..14c1f07 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_ctrls.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_ctrls.pm
@@ -30,6 +30,11 @@ my $TESTS = {
     test_class => [qw(bug forking os_linux)],
   },
 
+  ctrls_intvl_timeoutlogin_bug4298 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -402,4 +407,102 @@ sub ctrls_sighup_bug3756 {
   unlink($log_file);
 }
 
+sub ctrls_intvl_timeoutlogin_bug4298 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'ctrls');
+
+  my $ctrls_sock = File::Spec->rel2abs("$tmpdir/ctrls.sock");
+
+  my ($user, $group) = config_get_identity();
+  my $poll_interval = 2;
+
+  # Try to reproduce Bug#4298 by having the TimeoutLogin be a multiple of
+  # the Controls Interval.
+  my $timeout_login = ($poll_interval * 4);
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ctrls:20 event:10 timers:20',
+
+    TimeoutIdle => $timeout_idle,
+    TimeoutLogin => $timeout_login,
+
+    IfModules => {
+      'mod_ctrls.c' => {
+        ControlsEngine => 'on',
+        ControlsLog => $setup->{log_file},
+        ControlsSocket => $ctrls_sock,
+        ControlsACLs => "all allow user *",
+        ControlsSocketACL => "allow user *",
+        ControlsInterval => $poll_interval,
+      },
+
+      '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);
+
+      # Wait for one second longer than the TimeoutLogin
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Delaying for ", ($timeout_login + 1), " secs\n";
+      }
+
+      sleep($timeout_login + 1);
+
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        $client->quit();
+        die("Login succeeded unexpectedly");
+      }
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 3) };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_delay.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_delay.pm
index fa9974c..09586bb 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_delay.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_delay.pm
@@ -37,6 +37,31 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  delay_table_none_bug4020 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  delay_delayonevent_user_bug4020 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  delay_delayonevent_pass_bug4020 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  delay_delayonevent_failedlogin_bug4020 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  delay_delayonevent_user_pass_bug4020 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -380,11 +405,11 @@ sub delay_extra_user_cmd_bug3622 {
 
       $expected = 500;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'Bad sequence of commands';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -412,6 +437,9 @@ sub delay_extra_user_cmd_bug3622 {
   $self->assert_child_ok($pid);
 
   if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
     die($ex);
   }
 
@@ -462,7 +490,7 @@ sub delay_extra_pass_cmd_bug3622 {
   my $pid_file = File::Spec->rel2abs("$tmpdir/delay.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/delay.scoreboard");
 
-  my $log_file = File::Spec->rel2abs('tests.log');
+  my $log_file = test_get_logfile();
 
   my $auth_user_file = File::Spec->rel2abs("$tmpdir/delay.passwd");
   my $auth_group_file = File::Spec->rel2abs("$tmpdir/delay.group");
@@ -541,11 +569,11 @@ sub delay_extra_pass_cmd_bug3622 {
 
       $expected = 503;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'You are already logged in';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -573,6 +601,9 @@ sub delay_extra_pass_cmd_bug3622 {
   $self->assert_child_ok($pid);
 
   if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
     die($ex);
   }
 
@@ -615,4 +646,667 @@ sub delay_extra_pass_cmd_bug3622 {
   unlink($log_file);
 }
 
+sub delay_table_none_bug4020 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/delay.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/delay.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/delay.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/delay.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/delay.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'delay:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayTable => 'none',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  # Examine the TraceLog, looking for "unable to load DelayTable" messages.
+  # There shouldn't be any.
+
+  if (open(my $fh, "< $log_file")) {
+    my $ok = 1;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+      my $expected = '\[\d+\]\s+<(\S+):(\d+)>: (.*?)$';
+
+      if ($line =~ /$expected/) {
+        my $trace_channel = $1;
+        my $trace_level = $2;
+        my $trace_msg = $3;
+
+        next unless $trace_channel eq 'delay';
+
+        if ($trace_msg =~ /(unable to load|error opening) DelayTable/) {
+          $ok = 0;
+
+          if ($ENV{TEST_VERBOSE}) {
+            print STDERR " + unexpected TraceLog line: $line\n";
+          }
+
+          last;
+        }
+      }
+    }
+
+    close($fh);
+
+    $self->assert($ok, test_msg("Trace messages appeared unexpectedly"));
+
+  } else {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die("Can't open $log_file: $!");
+  }
+
+  unlink($log_file);
+}
+
+sub delay_delayonevent_user_bug4020 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/delay.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/delay.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/delay.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/delay.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/delay.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $user_delay_secs = 2;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'delay:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayTable => 'none',
+        DelayOnEvent => 'USER 2000ms',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      my $start = [gettimeofday()];        
+      $client->login($user, $passwd);
+      my $elapsed = tv_interval($start);
+
+      $client->quit();
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Elapsed login time: $elapsed secs\n";
+      }
+
+      if ($elapsed < $user_delay_secs) {
+        die("Expected at least $user_delay_secs sec delay, got $elapsed");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub delay_delayonevent_pass_bug4020 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/delay.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/delay.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/delay.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/delay.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/delay.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $pass_delay_secs = 2;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'delay:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayTable => 'none',
+        DelayOnEvent => 'PASS 2000',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      my $start = [gettimeofday()];        
+      eval { $client->login($user, 'foobar') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+      my $elapsed = tv_interval($start);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Elapsed login time: $elapsed secs\n";
+      }
+
+      if ($elapsed < $pass_delay_secs) {
+        die("Expected at least $pass_delay_secs sec delay, got $elapsed");
+      }
+
+      $start = [gettimeofday()];        
+      $client->login($user, $passwd);
+      $elapsed = tv_interval($start);
+
+      $client->quit();
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Elapsed login time: $elapsed secs\n";
+      }
+
+      if ($elapsed < $pass_delay_secs) {
+        die("Expected at least $pass_delay_secs sec delay, got $elapsed");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub delay_delayonevent_failedlogin_bug4020 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/delay.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/delay.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/delay.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/delay.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/delay.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $failed_delay_secs = 2;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'delay:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayTable => 'none',
+        DelayOnEvent => 'FailedLogin 2000',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      my $start = [gettimeofday()];        
+      eval { $client->login($user, 'foobar') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+      my $elapsed = tv_interval($start);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Elapsed login time: $elapsed secs\n";
+      }
+
+      if ($elapsed < $failed_delay_secs) {
+        die("Expected at least $failed_delay_secs sec delay, got $elapsed");
+      }
+
+      $start = [gettimeofday()];
+      $client->login($user, $passwd);
+      $elapsed = tv_interval($start);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Elapsed login time: $elapsed secs\n";
+      }
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub delay_delayonevent_user_pass_bug4020 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/delay.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/delay.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/delay.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/delay.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/delay.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $login_delay_secs = 4;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'delay:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => [
+        'DelayTable none',
+        'DelayOnEvent USER 2sec',
+        'DelayOnEvent PASS 2sec',
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      my $start = [gettimeofday()];        
+      $client->login($user, $passwd);
+      my $elapsed = tv_interval($start);
+
+      $client->quit();
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Elapsed login time: $elapsed secs\n";
+      }
+
+      if ($elapsed < $login_delay_secs) {
+        die("Expected at least $login_delay_secs sec delay, got $elapsed");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_digest.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_digest.pm
new file mode 100644
index 0000000..199dd99
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_digest.pm
@@ -0,0 +1,8679 @@
+package ProFTPD::Tests::Modules::mod_digest;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Carp;
+use Cwd;
+use File::Copy;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use IO::Socket::INET;
+use POSIX qw(:fcntl_h);
+use Time::HiRes qw(gettimeofday tv_interval usleep);
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  digest_hash_feat => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_opts => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_crc32 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_crc32_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  digest_hash_crc32_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  digest_hash_crc32_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  digest_hash_crc32_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  digest_hash_md5 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_sha1 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_sha256 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_sha512 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_failed_not_logged_in => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_failed_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_failed_not_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_failed_config_limit => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_hash_failed_blacklisted_files => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xcrc => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xcrc_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xcrc_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  digest_xcrc_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xcrc_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  digest_xcrc_2gb => {
+    order => ++$order,
+    test_class => [qw(forking slow)],
+  },
+
+  digest_md5 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_md5_failed_not_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xmd5 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xsha => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xsha1 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xsha256 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_xsha512 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_host => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_path_offset_length => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_path_with_spaces => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_path_with_spaces_offset_length => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_max_size => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_max_age_same_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_max_age_different_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_retr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_rest_retr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_stor => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_rest_stor => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_caching_appe => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_not_logged_in => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_enoent => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_not_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_blacklisted_files => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_start_pos_invalid_number => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_end_pos_invalid_number => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_end_pos_too_large => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_failed_start_pos_after_end_pos => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_algorithms => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_default_algo => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_engine => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_engine_per_user => {
+    order => ++$order,
+    test_class => [qw(forking mod_ifsession)],
+  },
+
+  digest_config_enable => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_enable_ftpaccess => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_max_size => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  digest_config_max_size_per_user => {
+    order => ++$order,
+    test_class => [qw(forking mod_ifsession)],
+  },
+
+  digest_sqllog_retr_transfer_cache => {
+    order => ++$order,
+    test_class => [qw(forking mod_sql mod_sql_sqlite)],
+  },
+
+  digest_sqllog_stor_transfer_cache => {
+    order => ++$order,
+    test_class => [qw(forking mod_sql mod_sql_sqlite)],
+  },
+
+  digest_sqllog_retr_sftp_transfer_cache => {
+    order => ++$order,
+    test_class => [qw(forking mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+  digest_sqllog_stor_sftp_transfer_cache => {
+    order => ++$order,
+    test_class => [qw(forking mod_sftp mod_sql mod_sql_sqlite)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  # Check for the required Perl modules:
+  #
+  #  Digest-CRC
+  #  Digest-MD5
+  #  Digest-SHA1
+  #  Digest-SHA256
+
+  my $required = [qw(
+    Digest::CRC
+    Digest::MD5
+    Digest::SHA1
+    Digest::SHA256
+  )];
+
+  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 set_up {
+  my $self = shift;
+  $self->SUPER::set_up(@_);
+
+  # Make sure that mod_sftp does not complain about permissions on the hostkey
+  # files.
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  unless (chmod(0400, $rsa_host_key, $dsa_host_key)) {
+    die("Can't set perms on $rsa_host_key, $dsa_host_key: $!");
+  }
+}
+
+sub digest_hash_feat {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->feat();
+
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $client->quit();
+
+      my $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH CRC32;MD5;SHA-1*;SHA-256;SHA-512;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert($found, test_msg("Did not see expected '$expected_feat'"));
+    };
+
+    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 digest_hash_opts {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my ($resp_code, $resp_msg) = $client->opts('HASH');
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "SHA-1";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $bad_algo = "CRC64";
+
+      eval { $client->opts('HASH', $bad_algo) };
+      unless ($@) {
+        die("OPTS HASH $bad_algo succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$bad_algo: Unsupported algorithm";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $good_algo = "CRC32";
+      ($resp_code, $resp_msg) = $client->opts('HASH', $good_algo);
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $good_algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->opts('HASH');
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $good_algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Make sure, via FEAT, that the '*' has changed!
+      $client->feat();
+
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $client->quit();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH CRC32*;MD5;SHA-1;SHA-256;SHA-512;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert($found, test_msg("Did not see expected '$expected_feat'"));
+    };
+
+    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 digest_hash_crc32 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_crc32_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $path = 'test.d/test.lnk';
+      ($resp_code, $resp_msg) = $client->quote('HASH', $path);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $path";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_crc32_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $path = 'test.d/test.lnk';
+      ($resp_code, $resp_msg) = $client->quote('HASH', $path);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $path";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_crc32_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $path = 'test.d/test.lnk';
+      ($resp_code, $resp_msg) = $client->quote('HASH', $path);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $path";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_crc32_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $path = 'test.d/test.lnk';
+      ($resp_code, $resp_msg) = $client->quote('HASH', $path);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $path";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_md5 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::MD5;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::MD5->new();
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'MD5';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_sha1 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA1;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::SHA1->new();
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'SHA-1';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_sha256 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA256;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    # Digest::SHA256 is a bit of an odd duck
+    my $ctx = Digest::SHA256::new(256);
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    $expected_digest =~ s/ //g;
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'SHA-256';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_sha512 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA256;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    # Digest::SHA256 is a bit of an odd duck
+    my $ctx = Digest::SHA256::new(512);
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    $expected_digest =~ s/ //g;
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'SHA-512';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_failed_not_logged_in {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Please login with USER and PASS";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_failed_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_failed_not_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', 'test.d') };
+      unless ($@) {
+        die("HASH test.d succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 553;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.d: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_failed_config_limit {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+
+    Limit => {
+      HASH => {
+        DenyAll => '',
+      },
+    },
+
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_hash_failed_blacklisted_files {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_files = [qw(
+    /dev/full
+    /dev/null
+    /dev/random
+    /dev/urandom
+    /dev/zero
+  )];
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      foreach my $test_file (@$test_files) {
+        next unless -e $test_file;
+
+        eval { $client->quote('HASH', $test_file) };
+        unless ($@) {
+          die("HASH $test_file succeeded unexpectedly");
+        }
+
+        $resp_code = $client->response_code();
+        $resp_msg = $client->response_msg();
+
+        $expected = 556;
+        $self->assert($expected == $resp_code,
+          test_msg("Expected response code $expected, got $resp_code"));
+
+        $expected = "$test_file: Operation not permitted";
+        $self->assert($expected eq $resp_msg,
+          test_msg("Expected response message '$expected', got '$resp_msg'"));
+      }
+
+      $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 digest_xcrc {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_xcrc_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', $path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 digest_xcrc_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', $path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 digest_xcrc_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', $path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 digest_xcrc_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', $path);
+      $client->quit();
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+    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 digest_xcrc_2gb {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "# Creating 2GB+ file\n";
+    }
+
+    # Create a file which is a little larger than 2GB.
+    #
+    # Seek to the 2GB limit, then fill the rest with 'A'
+    unless (seek($fh, (2 ** 31), 0)) {
+       die("Can't seek to 2GB length: $!");
+    }
+
+    print $fh "A" x 326;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest = 'DB21A206';
+
+  # WARNING: it can take a long time for Digest::CRC to grovel through
+  # a 2GB file.
+  my $compute_digest = 0;
+
+  if ($compute_digest) {
+    if (open(my $fh, "< $test_file")) {
+      my $ctx = Digest::CRC->new(type => 'crc32');
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Digesting 2GB+ file\n";
+      }
+      $ctx->addfile($fh);
+      $expected_digest = uc($ctx->hexdigest);
+      close($fh);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Computed CRC32 digest: $expected_digest\n";
+      }
+
+    } else {
+      die("Can't read $test_file: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 timer:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->quote('XCRC', 'test.dat');
+
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $nresp_msgs = scalar(@$resp_msgs);
+      my $last_resp_msg = $resp_msgs->[$nresp_msgs-1];
+      $expected = $expected_digest;
+      $self->assert($expected eq $last_resp_msg,
+        test_msg("Expected response message '$expected', got '$last_resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 3600) };
+    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 digest_md5 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::MD5;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::MD5->new();
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $file = 'test.txt';
+      my ($resp_code, $resp_msg) = $client->quote('MD5', $file);
+      $client->quit();
+
+      my $expected;
+
+      $expected = 251;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$file $expected_digest";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_md5_failed_not_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $file = 'test.d';
+      eval { $client->quote('MD5', $file) };
+      unless ($@) {
+        die("MD5 $file succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $client->quit();
+
+      my $expected;
+
+      $expected = 504;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$file: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_xmd5 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::MD5;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::MD5->new();
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XMD5', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_xsha {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA1;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::SHA1->new();
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XSHA', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_xsha1 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA1;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::SHA1->new();
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XSHA1', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_xsha256 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA256;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    # Digest::SHA256 is a bit of an odd duck...
+    my $ctx = Digest::SHA256::new(256);
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    $expected_digest =~ s/ //g;
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XSHA256', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_xsha512 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA256;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    # Digest::SHA256 is a bit of an odd duck...
+    my $ctx = Digest::SHA256::new(512);
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    $expected_digest =~ s/ //g;
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XSHA512', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_host {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA1;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::SHA1->new();
+    $ctx->addfile($fh);
+    $expected_digest = $ctx->hexdigest;
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultServer => 'on',
+    ServerName => '"Default Server"',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $host = 'localhost';
+
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+# This virtual host is name-based
+<VirtualHost 127.0.0.1>
+  Port $port
+  ServerAlias $host
+  ServerName "Digest Server"
+
+  AuthUserFile $setup->{auth_user_file}
+  AuthGroupFile $setup->{auth_group_file}
+  AuthOrder mod_auth_file.c
+
+  <IfModule mod_delay.c>
+    DelayEngine off
+  </IfModule>
+
+  <IfModule mod_digest.c>
+    DigestEngine on
+  </IfModule>
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $algo = 'SHA-1';
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      my ($resp_code, $resp_msg) = $client->host($host);
+
+      my $expected = 220;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      ($resp_code, $resp_msg) = $client->opts('HASH');
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->feat();
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH CRC32;MD5;SHA-1*;SHA-256;SHA-512;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert($found, test_msg("Did not see '$expected_feat'"));
+
+      $client->login($setup->{user}, $setup->{passwd});
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('XSHA1', 'test.txt');
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = uc($expected_digest);
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_path_offset_length {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+
+    my $buf;
+    unless (read($fh, $buf, 5)) {
+      die("Can't read $test_file: $!");
+    }
+
+    $ctx->add($buf);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt', '0', '5');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_path_with_spaces {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test file.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', '"test file.txt"');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_path_with_spaces_offset_length {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test file.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+
+    my $buf;
+    unless (read($fh, $buf, 5)) {
+      die("Can't read $test_file: $!");
+    }
+
+    $ctx->add($buf);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', '"test file.txt"',
+        '0', '5');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_caching {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # We deliberately do this multiple times, to test the in-memory
+      # caching of results.
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      $client->quit();
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_caching_max_size {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 timer:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+        DigestCache => 'size 2',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # We deliberately do this multiple times, to test the in-memory
+      # caching of results.  After each command, we "touch" the file to
+      # change its mtime, meaning a new cache entry.
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my ($atime, $mtime);
+      sleep(1);
+      $atime = $mtime = time();
+      unless (utime($atime, $mtime, $test_file)) {
+        die("Can't update timestamps on $test_file: $!");
+      }
+
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      sleep(1);
+      $atime = $mtime = time();
+      unless (utime($atime, $mtime, $test_file)) {
+        die("Can't update timestamps on $test_file: $!");
+      }
+
+      # Now we should have reach the max cache size
+      eval { $client->quote('XCRC', 'test.txt') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      sleep(1);
+      $atime = $mtime = time();
+      unless (utime($atime, $mtime, $test_file)) {
+        die("Can't update timestamps on $test_file: $!");
+      }
+
+      eval { $client->quote('XCRC', 'test.txt') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $client->quit();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Resource busy';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 300) };
+    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 digest_caching_max_age_same_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 timer:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+        DigestCache => 'size 2 maxAge 1s',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # We deliberately do this multiple times, to test the in-memory
+      # caching of results.
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      sleep(1);
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      sleep(1);
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test.txt');
+
+      $client->quit();
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 300) };
+    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 digest_caching_max_age_different_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/test1.txt");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file1: $!");
+    }
+
+  } else {
+    die("Can't open $test_file1: $!");
+  }
+
+  my $expected_digest1;
+
+  if (open(my $fh, "< $test_file1")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest1 = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "Hello, World!  It's me again.\n";
+    unless (close($fh)) {
+      die("Can't write $test_file2: $!");
+    }
+
+  } else {
+    die("Can't open $test_file2: $!");
+  }
+
+  my $expected_digest2;
+
+  if (open(my $fh, "< $test_file2")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest2 = uc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file2: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 timer:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+        DigestCache => 'size 1 maxAge 1s',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      # We deliberately do this multiple times, to test the in-memory
+      # caching of results.
+      my ($resp_code, $resp_msg) = $client->quote('XCRC', 'test1.txt');
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest1;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XCRC', 'test2.txt') };
+      unless ($@) {
+        die("XCRC test2.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test2.txt: Resource busy";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Now wait for longer than 5 secs, for the expiry timer to kick in.
+      my $delay = 6;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sleeping for $delay secs\n";
+      }
+      sleep($delay);
+
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test2.txt');
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest2;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      sleep(1);
+      ($resp_code, $resp_msg) = $client->quote('XCRC', 'test2.txt');
+
+      $client->quit();
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $expected_digest2;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 300) };
+    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 digest_caching_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    UseSendfile => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file = 'test.txt';
+
+      # Now we download the file, and see whether mod_digest can
+      # opportunistically generate and cache the digest.
+      my $conn = $client->retr_raw($file);
+      unless ($conn) {
+        die("RETR $file failed: " . $client->response_msg() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 10);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', $file);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_caching_rest_retr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    my $ctx = Digest::CRC->new(type => 'crc32');
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:20 digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    UseSendfile => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file = 'test.txt';
+
+      ($resp_code, $resp_msg) = $client->rest(0);
+
+      $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Restarting at 0. Send STORE or RETRIEVE to initiate transfer";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Now we download the file, and see whether mod_digest can
+      # opportunistically generate and cache the digest.
+      my $conn = $client->retr_raw($file);
+      unless ($conn) {
+        die("RETR $file failed: " . $client->response_msg() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 10);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', $file);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_caching_stor {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_data = "Hello, World!\n";
+
+  my $ctx = Digest::CRC->new(type => 'crc32');
+  $ctx->add($test_data);
+  my $expected_digest = lc($ctx->hexdigest);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file = 'test.txt';
+
+      # Now we upload the file, and see whether mod_digest can
+      # opportunistically generate and cache the digest.
+      my $conn = $client->stor_raw($file);
+      unless ($conn) {
+        die("STOR $file failed: " . $client->response_msg() . " " .
+          $client->response_msg());
+      }
+
+      $conn->write($test_data, length($test_data), 10);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', $file);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_caching_rest_stor {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_data = "Hello, World!\n";
+
+  my $ctx = Digest::CRC->new(type => 'crc32');
+  $ctx->add($test_data);
+  my $expected_digest = lc($ctx->hexdigest);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file = 'test.txt';
+
+      ($resp_code, $resp_msg) = $client->rest(0);
+
+      $expected = 350;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Restarting at 0. Send STORE or RETRIEVE to initiate transfer";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Now we upload the file, and see whether mod_digest can
+      # opportunistically generate and cache the digest.
+      my $conn = $client->stor_raw($file);
+      unless ($conn) {
+        die("STOR $file failed: " . $client->response_msg() . " " .
+          $client->response_msg());
+      }
+
+      $conn->write($test_data, length($test_data), 10);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', $file);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_caching_appe {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_data = "Hello, World!\n";
+
+  my $ctx = Digest::CRC->new(type => 'crc32');
+  $ctx->add($test_data);
+  my $expected_digest = lc($ctx->hexdigest);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file = 'test.txt';
+
+      # Now we upload the file, and see whether mod_digest can
+      # opportunistically generate and cache the digest.
+      my $conn = $client->appe_raw($file);
+      unless ($conn) {
+        die("APPE $file failed: " . $client->response_msg() . " " .
+          $client->response_msg());
+      }
+
+      $conn->write($test_data, length($test_data), 10);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', $file);
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest $file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_failed_not_logged_in {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      eval { $client->quote('XCRC', 'test.txt') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $client->quit();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Please login with USER and PASS";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_failed_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      eval { $client->quote('XCRC', 'test.txt') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $client->quit();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_failed_not_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      eval { $client->quote('XCRC', 'test.d') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $client->quit();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.d: Not a regular file";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    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 digest_failed_blacklisted_files {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_files = [qw(
+    /dev/full
+    /dev/null
+    /dev/random
+    /dev/urandom
+    /dev/zero
+  )];
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      foreach my $test_file (@$test_files) {
+        next unless -e $test_file;
+
+        eval { $client->quote('XCRC', $test_file) };
+        unless ($@) {
+          die("XCRC $test_file succeeded unexpectedly");
+        }
+        my $resp_code = $client->response_code();
+        my $resp_msg = $client->response_msg();
+
+        my $expected;
+
+        $expected = 550;
+        $self->assert($expected == $resp_code,
+          test_msg("Expected response code $expected, got $resp_code"));
+
+        $expected = "$test_file: Operation not permitted";
+        $self->assert($expected eq $resp_msg,
+          test_msg("Expected response message '$expected', got '$resp_msg'"));
+      }
+
+      $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 digest_failed_start_pos_invalid_number {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XCRC', $test_file, 'a', '5') };
+      unless ($@) {
+        die("XCRC $test_file succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'XCRC requires a start greater than or equal to 0';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_failed_end_pos_invalid_number {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XCRC', $test_file, '1', 'a') };
+      unless ($@) {
+        die("XCRC $test_file succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'XCRC requires an end greater than 0';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_failed_end_pos_too_large {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('XCRC', $test_file, '0', '1000') };
+      unless ($@) {
+        die("XCRC $test_file succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'XCRC: end exceeds file size';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_failed_start_pos_after_end_pos {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $start_pos = 10;
+      my $end_pos = 1;
+      eval { $client->quote('XCRC', $test_file, $start_pos, $end_pos) };
+      unless ($@) {
+        die("XCRC $test_file succeeded unexpectedly");
+      }
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "XCRC requires end ($end_pos) greater than start ($start_pos)";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $end_pos = $start_pos;
+
+      eval { $client->quote('XCRC', $test_file, $start_pos, $end_pos) };
+      unless ($@) {
+        die("XCRC $test_file succeeded unexpectedly");
+      }
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 501;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "XCRC requires end ($end_pos) greater than start ($start_pos)"
+;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_algorithms {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::SHA256;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $expected_digest;
+
+  if (open(my $fh, "< $test_file")) {
+    # Digest::SHA256 is a bit of an odd duck
+    my $ctx = Digest::SHA256::new(256);
+    $ctx->addfile($fh);
+    $expected_digest = lc($ctx->hexdigest);
+    $expected_digest =~ s/ //g;
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+        DigestAlgorithms => 'md5 sha256',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $algo = 'SHA-256';
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      my ($resp_code, $resp_msg) = $client->opts('HASH');
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->feat();
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH MD5;SHA-256*;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert($found, test_msg("Did not see expected '$expected_feat'"));
+
+      $client->login($setup->{user}, $setup->{passwd});
+      ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->quote('HASH', 'test.txt');
+
+      $expected = 213;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $filesz = -s $test_file;
+      $expected = "$algo 0-$filesz $expected_digest test.txt";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XSHA1', 'test.txt') };
+      unless ($@) {
+        die("XSHA1 test.txt succeeded unexpectedly");
+      }
+
+      ($resp_code, $resp_msg) = $client->quote('XSHA256', 'test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = uc($expected_digest);
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_default_algo {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+        DigestDefaultAlgorithm => 'md5',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $algo = 'MD5';
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      my ($resp_code, $resp_msg) = $client->opts('HASH');
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->feat();
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH CRC32;MD5*;SHA-1;SHA-256;SHA-512;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert($found, test_msg("Did not see expected '$expected_feat'"));
+      $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 digest_config_engine {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => '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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $algo = 'SHA-256';
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      eval { $client->opts('HASH') };
+      unless ($@) {
+        die("OPTS HASH succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'OPTS HASH not understood';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->feat();
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH CRC32;MD5;SHA-1*;SHA-256;SHA-512;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert(!$found, test_msg("Saw '$expected_feat' unexpectedly"));
+
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "HASH not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XSHA1', 'test.txt') };
+      unless ($@) {
+        die("XSHA1 test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "XSHA1 not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_engine_per_user {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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_digest.c>
+  <IfUser $setup->{user}>
+    DigestEngine off
+  </IfUser>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $algo = 'SHA-1';
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      # We have not logged in yet, thus the per-user setting hasn't taken
+      # effect.
+      my ($resp_code, $resp_msg) = $client->opts('HASH');
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->feat();
+      $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $expected_feat = ' HASH CRC32;MD5;SHA-1*;SHA-256;SHA-512;';
+
+      my $found = 0;
+      my $nfeat = scalar(@$resp_msgs);
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if ($resp_msgs->[$i] eq $expected_feat) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $self->assert($found, test_msg("Did not see '$expected_feat'"));
+
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "HASH not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XSHA1', 'test.txt') };
+      unless ($@) {
+        die("XSHA1 test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "XSHA1 not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_enable {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+
+    Directory => {
+      '/' => {
+        DigestEnable => '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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $file = 'test.txt';
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('HASH', $file) };
+      unless ($@) {
+        die("HASH $file succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$file: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XSHA1', $file) };
+      unless ($@) {
+        die("XSHA1 test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$file: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_enable_ftpaccess {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $ftpaccess_file = File::Spec->rel2abs("$tmpdir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOC;
+DigestEnable off
+EOC
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't write $ftpaccess_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AllowOverride => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $file = 'test.txt';
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      eval { $client->quote('HASH', $file) };
+      unless ($@) {
+        die("HASH $file succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$file: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XSHA1', $file) };
+      unless ($@) {
+        die("XSHA1 test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "$file: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_max_size {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+        DigestMaxSize => '2 B',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 556;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XCRC', 'test.txt') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 digest_config_max_size_per_user {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  require Digest::CRC;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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_digest.c>
+  DigestEngine on
+  <IfUser $setup->{user}>
+    DigestMaxSize 2
+  </IfUser>
+  <IfUser !$setup->{user}>
+    DigestMaxSize 1024
+  </IfUser>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $algo = 'CRC32';
+      my ($resp_code, $resp_msg) = $client->opts('HASH', $algo);
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = $algo;
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('HASH', 'test.txt') };
+      unless ($@) {
+        die("HASH test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 556;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->quote('XCRC', 'test.txt') };
+      unless ($@) {
+        die("XCRC test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "test.txt: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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 build_db {
+  my $cmd = shift;
+  my $db_script = shift;
+  my $check_exit_status = shift;
+  $check_exit_status = 0 unless defined $check_exit_status;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  my $exit_status = $?;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  if ($check_exit_status) {
+    if ($? != 0) {
+      croak("'$cmd' failed");
+    }
+  }
+
+  unlink($db_script);
+  return 1;
+}
+
+sub get_checksums {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, file, checksum, algo FROM ftpchecksums";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub digest_sqllog_retr_transfer_cache {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpchecksums (
+  user TEXT PRIMARY KEY,
+  file TEXT,
+  checksum TEXT,
+  algo TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    my $buf = 'AbCdEfGh' x 100;
+    my $count = 8192;
+    for (my $i = 0; $i < $count; $i++) {
+      print $fh $buf;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    UseSendfile => 'off',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $setup->{log_file},
+        SQLNamedQuery => 'log-download-checksum FREEFORM "INSERT INTO ftpchecksums (user, file, checksum, algo) VALUES (\'%u\', \'%f\', \'%{note:mod_digest.digest}\', \'%{note:mod_digest.algo}\')"',
+        SQLLog => 'RETR log-download-checksum',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->retr_raw('test.dat');
+      unless ($conn) {
+        die("RETR failed: " . $client->response_code() . ' ' .
+          $client->response_msg());
+      }
+
+      my $buf;
+      while ($conn->read($buf, 16384, 10)) {
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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);
+  }
+
+  eval {
+    my ($login, $file, $cksum, $algo) = get_checksums($db_file, "user = \'$setup->{user}\'");
+    my $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected user '$expected', got '$login'"));
+
+    $expected = File::Spec->rel2abs("$tmpdir/test.dat");
+    if ($^O eq 'darwin') {
+      # MacOSX hack
+      $expected = '/private' . $expected;
+    }
+
+    $self->assert($expected eq $file,
+      test_msg("Expected file '$expected', got '$file'"));
+
+    $expected = '57130f21b51e0a397d9fef5422b5cd5defa96e25';
+    $self->assert($expected eq $cksum,
+      test_msg("Expected checksum '$expected', got '$cksum'"));
+
+    $expected = 'SHA1';
+    $self->assert($expected eq $algo,
+      test_msg("Expected algorithm '$expected', got '$algo'"));
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub digest_sqllog_stor_transfer_cache {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpchecksums (
+  user TEXT PRIMARY KEY,
+  file TEXT,
+  checksum TEXT,
+  algo TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $setup->{log_file},
+        SQLNamedQuery => 'log-upload-checksum FREEFORM "INSERT INTO ftpchecksums (user, file, checksum, algo) VALUES (\'%u\', \'%f\', \'%{note:mod_digest.digest}\', \'%{note:mod_digest.algo}\')"',
+        SQLLog => 'STOR log-upload-checksum',
+      },
+    },
+  };
+
+  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 {
+      # Allow server to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+      $client->type('binary');
+
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . ' ' .
+          $client->response_msg());
+      }
+
+      my $buf = 'AbCdEfGh' x 100;
+      my $count = 8192;
+      for (my $i = 0; $i < $count; $i++) {
+        $conn->write($buf, length($buf), 10);
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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);
+  }
+
+  eval {
+    my ($login, $file, $cksum, $algo) = get_checksums($db_file, "user = \'$setup->{user}\'");
+    my $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected user '$expected', got '$login'"));
+
+    $expected = File::Spec->rel2abs("$tmpdir/test.dat");
+    if ($^O eq 'darwin') {
+      # MacOSX hack
+      $expected = '/private' . $expected;
+    }
+
+    $self->assert($expected eq $file,
+      test_msg("Expected file '$expected', got '$file'"));
+
+    $expected = '57130f21b51e0a397d9fef5422b5cd5defa96e25';
+    $self->assert($expected eq $cksum,
+      test_msg("Expected checksum '$expected', got '$cksum'"));
+
+    $expected = 'SHA1';
+    $self->assert($expected eq $algo,
+      test_msg("Expected algorithm '$expected', got '$algo'"));
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub digest_sqllog_retr_sftp_transfer_cache {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpchecksums (
+  user TEXT PRIMARY KEY,
+  file TEXT,
+  checksum TEXT,
+  algo TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    my $buf = 'AbCdEfGh' x 100;
+    my $count = 8192;
+    for (my $i = 0; $i < $count; $i++) {
+      print $fh $buf;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $setup->{log_file},
+        SQLNamedQuery => 'log-download-checksum FREEFORM "INSERT INTO ftpchecksums (user, file, checksum, algo) VALUES (\'%u\', \'%f\', \'%{note:mod_digest.digest}\', \'%{note:mod_digest.algo}\')"',
+        SQLLog => 'RETR log-download-checksum',
+      },
+    },
+  };
+
+  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::SSH2;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow server to start up
+      sleep(1);
+
+      my $ssh2 = Net::SSH2->new();
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.dat';
+
+      my $fh = $sftp->open($path, O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $path: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+
+      my $res = $fh->read($buf, 16384);
+      while ($res) {
+        $res = $fh->read($buf, 16384);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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);
+  }
+
+  eval {
+    my ($login, $file, $cksum, $algo) = get_checksums($db_file, "user = \'$setup->{user}\'");
+    my $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected user '$expected', got '$login'"));
+
+    $expected = File::Spec->rel2abs("$tmpdir/test.dat");
+    if ($^O eq 'darwin') {
+      # MacOSX hack
+      $expected = '/private' . $expected;
+    }
+
+    $self->assert($expected eq $file,
+      test_msg("Expected file '$expected', got '$file'"));
+
+    $expected = '57130f21b51e0a397d9fef5422b5cd5defa96e25';
+    $self->assert($expected eq $cksum,
+      test_msg("Expected checksum '$expected', got '$cksum'"));
+
+    $expected = 'SHA1';
+    $self->assert($expected eq $algo,
+      test_msg("Expected algorithm '$expected', got '$algo'"));
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub digest_sqllog_stor_sftp_transfer_cache {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'digest');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpchecksums (
+  user TEXT PRIMARY KEY,
+  file TEXT,
+  checksum TEXT,
+  algo TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'digest:20 event:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_digest.c' => {
+        DigestEngine => 'on',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $setup->{log_file},
+        SQLNamedQuery => 'log-upload-checksum FREEFORM "INSERT INTO ftpchecksums (user, file, checksum, algo) VALUES (\'%u\', \'%f\', \'%{note:mod_digest.digest}\', \'%{note:mod_digest.algo}\')"',
+        SQLLog => 'STOR log-upload-checksum',
+      },
+    },
+  };
+
+  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::SSH2;
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      # Allow server to start up
+      sleep(1);
+
+      my $ssh2 = Net::SSH2->new();
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.dat';
+
+      my $fh = $sftp->open($path, O_WRONLY|O_CREAT);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $path: [$err_name] ($err_code)");
+      }
+
+      my $buf = 'AbCdEfGh' x 100;
+      my $count = 8192;
+      for (my $i = 0; $i < $count; $i++) {
+        unless ($fh->write($buf)) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't write $path: [$err_name] ($err_code)");
+        }
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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);
+  }
+
+  eval {
+    my ($login, $file, $cksum, $algo) = get_checksums($db_file, "user = \'$setup->{user}\'");
+    my $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected user '$expected', got '$login'"));
+
+    $expected = File::Spec->rel2abs("$tmpdir/test.dat");
+    if ($^O eq 'darwin') {
+      # MacOSX hack
+      $expected = '/private' . $expected;
+    }
+
+    $self->assert($expected eq $file,
+      test_msg("Expected file '$expected', got '$file'"));
+
+    $expected = '57130f21b51e0a397d9fef5422b5cd5defa96e25';
+    $self->assert($expected eq $cksum,
+      test_msg("Expected checksum '$expected', got '$cksum'"));
+
+    $expected = 'SHA1';
+    $self->assert($expected eq $algo,
+      test_msg("Expected algorithm '$expected', got '$algo'"));
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm
index 32024e9..3d87e09 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_exec.pm
@@ -5,6 +5,7 @@ use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
 use File::Copy;
+use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
 
@@ -96,6 +97,16 @@ my $TESTS = {
     test_class => [qw(forking rootprivs)],
   },
 
+  exec_enable_per_dir_bug4076 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  exec_ifuser_on_cmd => {
+    order => ++$order,
+    test_class => [qw(forking mod_ifsession)],
+  },
+
 };
 
 sub new {
@@ -2393,4 +2404,285 @@ EOS
   unlink($log_file);
 }
 
+sub exec_enable_per_dir_bug4076 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/exec.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/exec.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/exec.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/exec.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/exec.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $cmd_file = File::Spec->rel2abs("$tmpdir/cmd.txt");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_exec.c' => {
+        ExecEngine => 'on',
+        ExecLog => $log_file,
+        ExecTimeout => 1,
+        ExecOnCommand => "LIST,NLST /bin/bash -c \"echo %a > $cmd_file\"",
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+
+<Directory ~/test.d>
+  ExecEnable off
+</Directory>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
+  # 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($user, $passwd);
+      $client->cwd('test.d');
+      $client->list();
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $cmd_file = '/private' . $cmd_file;
+  }
+
+  $self->assert(!-f $cmd_file, test_msg("Found $cmd_file unexpectedly"));
+  unlink($log_file);
+}
+
+sub exec_ifuser_on_cmd {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/exec.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/exec.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/exec.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/exec.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/exec.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+  
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $cmd_file = File::Spec->rel2abs("$tmpdir/cmd.txt");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_exec.c' => {
+        ExecEngine => 'on',
+        ExecLog => $log_file,
+        ExecTimeout => 1,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<IfUser $user>
+  ExecOnCommand LIST,NLST /bin/bash -c \"echo %a > $cmd_file\"
+</IfUser>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
+  # 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($user, $passwd);
+      $client->list();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  if (open(my $fh, "< $cmd_file")) {
+    my $line = <$fh>;
+    close($fh);
+
+    chomp($line);
+
+    my $expected = '127.0.0.1';
+
+    $self->assert($expected eq $line,
+      test_msg("Expected '$expected', got '$line'"));
+
+  } else {
+    die("Can't read $cmd_file: $!");
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip.pm
index 8d5ce67..74c73ab 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip.pm
@@ -16,6 +16,16 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
+  geoip_explicitly_allowed => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  geoip_multi_allow_bug4188 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -26,4 +36,177 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
+sub geoip_explicitly_allowed {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'geoip');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $geoip_ip_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoIP.dat');
+  my $geoip_ipv6_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoIPv6.dat');
+  my $geoip_city_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoLiteCity.dat');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'geoip:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_geoip.c' => [
+        'GeoIPEngine on',
+        "GeoIPLog $setup->{log_file}",
+        "GeoIPTable $geoip_ip_table",
+        "GeoIPTable $geoip_ipv6_table",
+        "GeoIPTable $geoip_city_table",
+
+        'GeoIPPolicy allow,deny',
+        'GeoIPAllowFilter RegionCode TX',
+        'GeoIPDenyFilter CountryCode US',
+      ],
+    },
+  };
+
+  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, 1);
+      $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 geoip_multi_allow_bug4188 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'geoip');
+
+  my $test_file = File::Spec->rel2abs($setup->{config_file});
+
+  my $geoip_ip_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoIP.dat');
+  my $geoip_ipv6_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoIPv6.dat');
+  my $geoip_city_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoLiteCity.dat');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'geoip:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_geoip.c' => [
+        'GeoIPEngine on',
+        "GeoIPLog $setup->{log_file}",
+        "GeoIPTable $geoip_ip_table",
+        "GeoIPTable $geoip_ipv6_table",
+        "GeoIPTable $geoip_city_table",
+
+        'GeoIPPolicy deny,allow',
+        'GeoIPAllowFilter RegionCode TX CountryCode US',
+      ],
+    },
+  };
+
+  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, 0, 1);
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip/sql.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip/sql.pm
new file mode 100644
index 0000000..dc9bfce
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_geoip/sql.pm
@@ -0,0 +1,199 @@
+package ProFTPD::Tests::Modules::mod_geoip::sql;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+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 = {
+  geoip_sql_allow_filter => {
+    order => ++$order,
+    test_class => [qw(forking mod_sql_sqlite)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub set_up {
+  my $self = shift;
+  $self->SUPER::set_up(@_);
+}
+
+sub build_db {
+  my $cmd = shift;
+  my $db_script = shift;
+  my $db_file = shift;
+  my $check_exit_status = shift;
+  $check_exit_status = 0 unless defined $check_exit_status;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  my $exit_status = $?;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  if ($check_exit_status) {
+    if ($? != 0) {
+      croak("'$cmd' failed");
+    }
+  }
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      croak("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  unlink($db_script);
+  return 1;
+}
+
+sub geoip_sql_allow_filter {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'geoip');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftp_user_geoip (
+  user TEXT,
+  regions TEXT,
+  countries TEXT
+);
+INSERT INTO ftp_user_geoip (user, regions, countries) VALUES ('$setup->{user}', 'TX', 'US');
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script, $db_file);
+
+  my $geoip_ip_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoIP.dat');
+  my $geoip_ipv6_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoIPv6.dat');
+  my $geoip_city_table = File::Spec->rel2abs('t/etc/modules/mod_geoip/GeoLiteCity.dat');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'geoip:20 sql:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_geoip.c' => [
+        'GeoIPEngine on',
+        "GeoIPLog $setup->{log_file}",
+        "GeoIPTable $geoip_ip_table",
+        "GeoIPTable $geoip_ipv6_table",
+        "GeoIPTable $geoip_city_table",
+
+        'GeoIPPolicy allow,deny',
+        'GeoIPAllowFilter sql:/get-user-geoip-regions',
+        'GeoIPDenyFilter sql:/get-user-geoip-countries',
+      ],
+
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $setup->{log_file}",
+        "SQLNamedQuery get-user-geoip-regions SELECT \"'RegionCode', regions FROM ftp_user_geoip WHERE user = '%U'\"",
+        "SQLNamedQuery get-user-geoip-countries SELECT \"'CountryCode', countries FROM ftp_user_geoip WHERE user = '%U'\"",
+      ],
+    },
+  };
+
+  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 {
+      # Give the server time to start up
+      sleep(1);
+
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm
index dfe37f8..a28ac09 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_ifsession.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use File::Copy;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -72,6 +73,16 @@ my $TESTS = {
     test_class => [qw(bug forking mod_sftp rootprivs)],
   },
 
+  ifclass_global_no_logging => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  ifuser_no_pass_bug4199 => {
+    order => ++$order,
+    test_class => [qw(bug mod_tls forking)],
+  },
+
 };
 
 sub new {
@@ -1800,4 +1811,371 @@ EOC
   unlink($log_file);
 }
 
+# Regression test for issues described in:
+#   https://forums.proftpd.org/smf/index.php/topic,11572.0.html
+sub ifclass_global_no_logging {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/ifsess.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/ifsess.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/ifsess.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/ifsess.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/ifsess.group");
+
+  my $ext_log = File::Spec->rel2abs("$tmpdir/ext.log");
+  my $xfer_log = File::Spec->rel2abs("$tmpdir/xfer.log");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd'; 
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'directory:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Append the mod_ifsession config to the end of the config file
+  if (open(my $fh, ">> $config_file")) {
+    my $limit_dir = $home_dir;
+    if ($^O eq 'darwin') {
+      $limit_dir = ('/private' . $home_dir);
+    }
+
+    print $fh <<EOC;
+
+ExtendedLog $ext_log ALL default
+TransferLog $xfer_log
+
+<Class local>
+  From 127.0.0.1
+</Class>
+
+<Global>
+  <IfClass local>
+    ExtendedLog $ext_log NONE
+    TransferLog none
+  </IfClass>
+</Global>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
+  # 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($user, $passwd);
+      $client->mkd('foo');
+      $client->retr($config_file);
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $ext_log")) {
+      my $ok = 1;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+        $ok = 0;
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "+ ExtendedLog: $line\n";
+        }
+      }
+      close($fh);
+
+      $self->assert($ok, test_msg("Saw ExtendedLog messages unexpectedly"));
+
+    } else {
+      die("Can't read $ext_log: $!");
+    }
+
+    if (open(my $fh, "< $xfer_log")) {
+      close($fh);
+      $self->assert(0, test_msg("Saw TransferLog unexpectedly"));
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub ifuser_no_pass_bug4199 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/ifsess.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/ifsess.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/ifsess.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/ifsess.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/ifsess.group");
+
+  my $test_file = File::Spec->rel2abs($config_file);
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd'; 
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $server_cert = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $client_cert = File::Spec->rel2abs('t/etc/modules/mod_tls/client-cert.pem');
+  my $ca_cert = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $tlslogin_file = File::Spec->rel2abs("$tmpdir/.tlslogin");
+  unless (copy($client_cert, $tlslogin_file)) {
+    die("Can't copy $client_cert to $tlslogin_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'directory:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $log_file,
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $server_cert,
+        TLSCACertificateFile => $ca_cert,
+
+        TLSOptions => 'AllowDotLogin',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # Append the mod_ifsession config to the end of the config file
+  if (open(my $fh, ">> $config_file")) {
+    my $limit_dir = $home_dir;
+    if ($^O eq 'darwin') {
+      # MacOSX hack
+      $limit_dir = ('/private' . $home_dir);
+    }
+
+    print $fh <<EOC;
+<Directory />
+  <Limit ALL>
+    DenyAll
+  </Limit>
+</Directory>
+
+<IfUser $user>
+  <Directory $limit_dir>
+    <Limit MKD>
+      AllowAll
+    </Limit>
+  </Directory>
+</IfUser>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
+  # 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);
+
+      # IO::Socket::SSL options
+      my $ssl_opts = {
+        SSL_use_cert => 1,
+        SSL_cert_file => $client_cert,
+        SSL_key_file => $client_cert,
+      };
+
+      my $client = Net::FTPSSL->new('127.0.0.1',
+        Croak => 1,
+        Encryption => 'E',
+        Port => $port,
+        SSL_Client_Certificate => $ssl_opts,
+      );
+
+      unless ($client) {
+        die("Can't connect to FTPS server: " . IO::Socket::SSL::errstr());
+      }
+
+      unless ($client->_user($user)) {
+        die("USER error: " . $client->last_message());
+      }
+
+      my $expected = "232 User $user logged in";
+      my $resp = $client->last_message();
+      $self->assert($expected eq $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
+
+      unless ($client->mkdir('foo')) {
+        die("MKD error: " . $client->last_message());
+      }
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm
index 866c1ba..9a231ed 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_lang.pm
@@ -107,6 +107,31 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  lang_opts_utf8_prefer_server_encoding_bug4125 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  lang_opts_utf8_useencoding_charsets_prefer_server_encoding_bug4125 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  lang_useencoding_ascii_utf8_require_valid_encoding_bug4125 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  lang_useencoding_latin1_utf8_per_user_bug4214 => {
+    order => ++$order,
+    test_class => [qw(forking mod_ifsession)],
+  },
+
+  lang_useencoding_utf8_latin1_per_user_bug4214 => {
+    order => ++$order,
+    test_class => [qw(forking mod_ifsession)],
+  },
+
 };
 
 sub new {
@@ -959,30 +984,27 @@ sub lang_opts_utf8_ok {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->opts('UTF8', 'off');
+      my ($resp_code, $resp_msg) = $client->opts('UTF8', 'off');
 
       my $expected;
 
       $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'UTF8 set to off';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->opts('UTF8', 'on');
 
       $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      ($resp_code, $resp_msg) = $client->opts('UTF8', 'on');
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'UTF8 set to on';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1489,6 +1511,10 @@ sub lang_opts_utf8_useencoding_on {
 
       ($resp_code, $resp_msg) = $client->opts('UTF8', 'on');
 
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
       $expected = 'UTF8 set to on';
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
@@ -2526,4 +2552,669 @@ sub lang_useencoding_utf8_latin1 {
   unlink($log_file);
 }
 
+sub lang_opts_utf8_prefer_server_encoding_bug4125 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/lang.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/lang.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/lang.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/lang.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/lang.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'encode:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_lang.c' => {
+        LangOptions => 'PreferServerEncoding',
+        UseEncoding => 'iso-8859-1 iso-8859-1',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      # Make sure the OPTS UTF8 command does not appear in the FEAT listing;
+      # see Bug#3737.
+      $client->feat(); 
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      my $expected;
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      foreach my $feat (@$resp_msgs) {
+        if ($feat =~ /^ UTF8$/) {
+          die("'$feat' feature listed unexpectedly via FEAT");
+        }
+      }
+
+      my $resp_msg;
+      ($resp_code, $resp_msg) = $client->opts('UTF8', 'off');
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'UTF8 set to off';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      eval { $client->opts('UTF8', 'on') };
+      unless ($@) {
+        die("OPTS UTF8 on succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 451;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Unable to accept OPTS UTF8';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub lang_opts_utf8_useencoding_charsets_prefer_server_encoding_bug4125 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/lang.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/lang.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/lang.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/lang.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/lang.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'encode:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_lang.c' => {
+        LangOptions => 'PreferServerEncoding',
+        UseEncoding => 'iso-8859-1 utf-8',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      # Make sure the OPTS UTF8 command does not appear in the FEAT listing;
+      # see Bug#3737.
+      $client->feat(); 
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      my $expected;
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      # Since our strict UseEncoding expects UTF8, we should see UTF8
+      # listed in the FEAT output (see Bug#3737).
+      my $have_utf8 = 0;
+      foreach my $feat (@$resp_msgs) {
+        if ($feat =~ /^ UTF8$/) {
+          $have_utf8 = 1;
+          last;
+        }
+      }
+
+      $self->assert($have_utf8,
+        test_msg("UTF8 feature not listed as expected via FEAT"));
+
+      eval { $client->opts('UTF8', 'off') };
+      unless ($@) {
+        die("OPTS UTF8 off succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $expected = 451;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Unable to accept OPTS UTF8';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->opts('UTF8', 'on');
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'UTF8 set to on';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub lang_useencoding_ascii_utf8_require_valid_encoding_bug4125 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/lang.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/lang.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/lang.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/lang.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/lang.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/üöä");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'encode:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_lang.c' => {
+        LangOptions => 'RequireValidEncoding',
+        UseEncoding => 'utf-8 us-ascii',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $name = "üöä";
+
+      my $conn = $client->stor_raw($name); 
+      if ($conn) {
+        die("STORE $name succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+ 
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Illegal character sequence';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub lang_useencoding_latin1_utf8_per_user_bug4214 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'lang');
+
+  my $test_file = File::Spec->rel2abs("$setup->{home_dir}/üöä");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'encode:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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_lang.c>
+  <IfUser $setup->{user}>
+    UseEncoding utf-8 iso-8859-1
+  </IfUser>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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});
+
+      my $name = "üöä";
+
+      my $conn = $client->stor_raw($name);
+      unless ($conn) {
+        die("STOR $name failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, world\n";
+      $conn->write($buf, length($buf), 15);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+    };
+
+    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 lang_useencoding_utf8_latin1_per_user_bug4214 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'lang');
+
+  my $test_file = File::Spec->rel2abs("$setup->{home_dir}/Grafik-Zentrumäüu.png");
+  if ($^O eq 'darwin') {
+    # MacOSX hack
+    $test_file = ('/private' . $test_file);
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'encode:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    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_lang.c>
+  <IfUser $setup->{user}>
+    UseEncoding utf-8 iso-8859-1
+  </IfUser>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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});
+
+      my $name = "Grafik-Zentrumäüu.png";
+
+      my $conn = $client->stor_raw($name);
+      unless ($conn) {
+        die("STOR $name failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, world\n";
+      $conn->write($buf, length($buf), 15);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_ldap.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_ldap.pm
index 118d870..d98c920 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_ldap.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_ldap.pm
@@ -32,70 +32,87 @@ my $TESTS = {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_users_authdenied => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_genhomedir_with_username => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_genhomedir_without_username => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_genhomedir_forcegenhdir => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_groups_authdenied => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_groups_authallowed => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_quota_on_user => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_quota_default => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_default_uid => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_default_gid => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_default_force_uid => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_default_force_gid => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_alias_dereference_off => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_alias_dereference_on => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_scope_base => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_scope_sub => {
     order => ++$order,
     test_class => [qw(forking)],
   },
+
   ldap_default_auth_scheme => {
     order => ++$order,
     test_class => [qw(forking)],
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm
index 5e11173..49c5c20 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_quotatab_sql.pm
@@ -66,6 +66,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  quotatab_stor_ok_user_default_with_group_limit => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  quotatab_stor_ok_user_default_with_no_group_limit => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   quotatab_stor_ok_group_limit => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -86,6 +96,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  quotatab_stor_ok_group_limit_with_default => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   quotatab_stor_ok_class_limit => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -211,6 +226,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  quotatab_sql_odbc => {
+    order => ++$order,
+    test_class => [qw(forking mod_sql_odbc)],
+  },
+
 };
 
 sub new {
@@ -314,7 +334,7 @@ sub quotatab_stor_ok_user_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -325,14 +345,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -346,7 +366,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -545,7 +565,7 @@ sub quotatab_appe_ok_user_limit_bytes_in_exceeded_soft_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -556,14 +576,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -577,7 +597,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 5, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -765,7 +785,7 @@ sub quotatab_appe_ok_user_limit_bytes_in_exceeded_hard_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -776,14 +796,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -797,7 +817,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'hard', 5, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -982,7 +1002,7 @@ sub quotatab_retr_ok_user_limit_bytes_out_exceeded {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -993,14 +1013,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -1014,7 +1034,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 0, 5, 0, 0, 3, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -1211,7 +1231,7 @@ sub quotatab_retr_ok_user_limit_files_out_exceeded {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -1222,14 +1242,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -1243,7 +1263,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 0, 0, 0, 0, 1, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -1422,7 +1442,7 @@ sub quotatab_stor_ok_user_limit_bytes_in_exceeded_soft_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -1433,14 +1453,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -1454,7 +1474,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 5, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -1631,7 +1651,7 @@ sub quotatab_stor_ok_user_limit_bytes_in_exceeded_hard_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -1642,14 +1662,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -1663,7 +1683,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'hard', 5, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -1837,7 +1857,7 @@ sub quotatab_stor_ok_user_default_limit_bytes_in_exceeded_soft_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -1848,14 +1868,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -1868,7 +1888,7 @@ CREATE TABLE quotalimits (
 );
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -1922,7 +1942,7 @@ EOS
         'QuotaLimitTable sql:/get-quota-limit',
         'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
 
-        'QuotaDefault user false soft 5 0 0 3 0 0',
+        'QuotaDefault group false soft 5 0 0 3 0 0',
       ],
 
       'mod_sql.c' => {
@@ -2047,7 +2067,7 @@ sub quotatab_stor_ok_user_default_limit_bytes_in_exceeded_hard_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -2058,14 +2078,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -2078,7 +2098,7 @@ CREATE TABLE quotalimits (
 );
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -2254,7 +2274,7 @@ sub quotatab_stor_ok_user_limit_files_in_exceeded {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -2265,14 +2285,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -2286,7 +2306,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 1, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -2437,7 +2457,7 @@ EOS
   unlink($log_file);
 }
 
-sub quotatab_stor_ok_group_limit {
+sub quotatab_stor_ok_user_default_with_group_limit {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2470,7 +2490,7 @@ sub quotatab_stor_ok_group_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -2482,14 +2502,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -2503,7 +2523,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -2512,6 +2532,8 @@ CREATE TABLE quotatallies (
   files_out_used INTEGER NOT NULL,
   files_xfer_used INTEGER NOT NULL
 );
+INSERT INTO quotatallies (name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used) VALUES ('$group', 'group', 32, 0, 0, 2, 0, 0);
+
 EOS
 
     unless (close($fh)) {
@@ -2556,6 +2578,7 @@ EOS
         "QuotaLog $log_file",
         'QuotaLimitTable sql:/get-quota-limit',
         'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+        'QuotaDefault user false hard 0 1 0 3 0 0',
       ],
 
       'mod_sql.c' => {
@@ -2564,6 +2587,8 @@ EOS
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
         SQLMinID => '0',
+        SQLNamedQuery => 'get-user-info SELECT "userid, passwd, uid, gid, homedir, shell FROM users WHERE userid=\'%U\'"',
+        SQLUserInfo => 'custom:/get-user-info',
       },
     },
   };
@@ -2585,47 +2610,52 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
 
       # Login as user1, and upload a file
-      $client->login($user1, $passwd);
+      $client1->login($user1, $passwd);
 
-      my $conn = $client->stor_raw('test.txt');
-      unless ($conn) {
-        die("Failed to STOR test.txt: " . $client->response_code() . " " .
-          $client->response_msg());
+      # These uploads should fail because they encounter the configured
+      # group limits/tallies in the database, despite the presence of a
+      # user QuotaDefault in the config; the group limits/tallies should
+      # take precedence over the config default.
+
+      my $conn = $client1->stor_raw('test.txt');
+      if ($conn) {
+        die("STOR test.txt succeeded unexpectedly");
       }
 
-      my $buf = "Hello, World\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
+      my $resp_code = $client1->response_code();
+      my $resp_msg = $client1->response_msg();
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-      $client->quit();
+      ($resp_code, $resp_msg) = $client1->quit();
 
-      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $expected = 221;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
 
       # Login as user2, and upload a file
-      $client->login($user2, $passwd);
+      $client2->login($user2, $passwd);
 
-      $conn = $client->stor_raw('test.txt');
-      unless ($conn) {
-        die("Failed to STOR test.txt: " . $client->response_code() . " " .
-          $client->response_msg());
+      $conn = $client2->stor_raw('test.txt');
+      if ($conn) {
+        die("STOR test.txt succeeded unexpectedly");
       }
 
-      $buf = "Hello, World\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
+      $resp_code = $client2->response_code();
+      $resp_msg = $client2->response_msg();
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $self->assert_transfer_ok($resp_code, $resp_msg);
-      $client->quit();
+      $client2->quit();
     };
 
     if ($@) {
@@ -2650,37 +2680,792 @@ EOS
 
   $self->assert_child_ok($pid);
 
-  my ($quota_type, $bytes_in_used, $bytes_out_used, $bytes_xfer_used, $files_in_used, $files_out_used, $files_xfer_used) = get_tally($db_file, "name = \'$group\'");
+  eval {
+    my ($quota_type, $bytes_in_used, $bytes_out_used, $bytes_xfer_used, $files_in_used, $files_out_used, $files_xfer_used) = get_tally($db_file, "name = \'$group\'");
 
-  my $expected;
+    my $expected = 'group';
+    $self->assert($expected eq $quota_type,
+      test_msg("Expected '$expected', got '$quota_type'"));
 
-  $expected = 'group';
-  $self->assert($expected eq $quota_type,
-    test_msg("Expected '$expected', got '$quota_type'"));
+    $expected = '^(32.0|32)$';
+    $self->assert(qr/$expected/, $bytes_in_used,
+      test_msg("Expected $expected, got $bytes_in_used"));
 
-  $expected = '^(26.0|26)$';
-  $self->assert(qr/$expected/, $bytes_in_used,
-    test_msg("Expected $expected, got $bytes_in_used"));
+    $expected = '^(0.0|0)$';
+    $self->assert(qr/$expected/, $bytes_out_used,
+      test_msg("Expected $expected, got $bytes_out_used"));
 
-  $expected = '^(0.0|0)$';
-  $self->assert(qr/$expected/, $bytes_out_used,
-    test_msg("Expected $expected, got $bytes_out_used"));
+    $expected = '^(0.0|0)$';
+    $self->assert(qr/$expected/, $bytes_xfer_used,
+      test_msg("Expected $expected, got $bytes_xfer_used"));
 
-  $expected = '^(0.0|0)$';
-  $self->assert(qr/$expected/, $bytes_xfer_used,
-    test_msg("Expected $expected, got $bytes_xfer_used"));
+    $expected = 2;
+    $self->assert($expected == $files_in_used,
+      test_msg("Expected $expected, got $files_in_used"));
+
+    $expected = 0;
+    $self->assert($expected == $files_out_used,
+      test_msg("Expected $expected, got $files_out_used"));
+
+    $expected = 0;
+    $self->assert($expected == $files_xfer_used,
+      test_msg("Expected $expected, got $files_xfer_used"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub quotatab_stor_ok_user_default_with_no_group_limit {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/quotatab.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/quotatab.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/quotatab.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user1 = 'proftpd';
+  my $group = 'ftpd';
+  my $passwd = 'test';
+  my $home_dir1 = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($home_dir1);
+
+  my $uid1 = 500;
+  my $gid = 500;
+
+  my $user2 = 'proftpd2';
+  my $home_dir2 = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($home_dir2);
+
+  my $uid2 = 1000;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT PRIMARY KEY,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '$passwd', $uid1, $gid, '$home_dir1', '/bin/bash');
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT PRIMARY KEY,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
+
+CREATE TABLE quotalimits (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  per_session TEXT NOT NULL,
+  limit_type TEXT NOT NULL,
+  bytes_in_avail REAL NOT NULL,
+  bytes_out_avail REAL NOT NULL,
+  bytes_xfer_avail REAL NOT NULL,
+  files_in_avail INTEGER NOT NULL,
+  files_out_avail INTEGER NOT NULL,
+  files_xfer_avail INTEGER NOT NULL
+);
+
+CREATE TABLE quotatallies (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  bytes_in_used REAL NOT NULL,
+  bytes_out_used REAL NOT NULL,
+  bytes_xfer_used REAL NOT NULL,
+  files_in_used INTEGER NOT NULL,
+  files_out_used INTEGER NOT NULL,
+  files_xfer_used INTEGER NOT NULL
+);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    DefaultChdir => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_quotatab_sql.c' => [
+        'SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM quotalimits WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM quotatallies WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = \'%{6}\' AND quota_type = \'%{7}\'" quotatallies',
+        'SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" quotatallies',
+
+        'QuotaEngine on',
+        "QuotaLog $log_file",
+        'QuotaLimitTable sql:/get-quota-limit',
+        'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+        'QuotaDefault user false hard 0 1 0 3 0 0',
+      ],
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLMinID => '0',
+        SQLNamedQuery => 'get-user-info SELECT "userid, passwd, uid, gid, homedir, shell FROM users WHERE userid=\'%U\'"',
+        SQLUserInfo => 'custom:/get-user-info',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      # Login as user1, and upload a file
+      $client1->login($user1, $passwd);
+
+      # These uploads should fail because they encounter the configured
+      # group limits/tallies in the database, despite the presence of a
+      # user QuotaDefault in the config; the group limits/tallies should
+      # take precedence over the config default.
+
+      my $conn = $client1->stor_raw('test.txt');
+      unless ($conn) {
+        die("STOR test.txt failed: " . $client1->response_code() . " " .
+          $client1->response_msg());
+      }
+
+      my $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client1->response_code();
+      my $resp_msg = $client1->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      ($resp_code, $resp_msg) = $client1->quit();
+
+      my $expected = 221;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+
+      # Login as user2, and upload a file
+      $client2->login($user2, $passwd);
+
+      $conn = $client2->stor_raw('test.txt');
+      unless ($conn) {
+        die("STOR test.txt failed: " . $client2->response_code() . " " .
+          $client2->response_msg());
+      }
+
+      my $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client2->response_code();
+      my $resp_msg = $client2->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client2->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    my ($quota_type, $bytes_in_used, $bytes_out_used, $bytes_xfer_used, $files_in_used, $files_out_used, $files_xfer_used) = get_tally($db_file, "name = \'$user1\'");
+
+    my $expected = 'user';
+    $self->assert($expected eq $quota_type,
+      test_msg("Expected '$expected', got '$quota_type'"));
+
+    $expected = '^(0.0|0)$';
+    $self->assert(qr/$expected/, $bytes_in_used,
+      test_msg("Expected $expected, got $bytes_in_used"));
+
+    $expected = '^(0.0|0)$';
+    $self->assert(qr/$expected/, $bytes_out_used,
+      test_msg("Expected $expected, got $bytes_out_used"));
+
+    $expected = '^(0.0|0)$';
+    $self->assert(qr/$expected/, $bytes_xfer_used,
+      test_msg("Expected $expected, got $bytes_xfer_used"));
+
+    $expected = 1;
+    $self->assert($expected == $files_in_used,
+      test_msg("Expected $expected, got $files_in_used"));
+
+    $expected = 0;
+    $self->assert($expected == $files_out_used,
+      test_msg("Expected $expected, got $files_out_used"));
+
+    $expected = 0;
+    $self->assert($expected == $files_xfer_used,
+      test_msg("Expected $expected, got $files_xfer_used"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub quotatab_stor_ok_group_limit {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/quotatab.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/quotatab.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/quotatab.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user1 = 'proftpd';
+  my $group = 'ftpd';
+  my $passwd = 'test';
+  my $home_dir1 = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($home_dir1);
+
+  my $uid1 = 500;
+  my $gid = 500;
+
+  my $user2 = 'proftpd2';
+  my $home_dir2 = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($home_dir2);
+
+  my $uid2 = 1000;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT PRIMARY KEY,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '$passwd', $uid1, $gid, '$home_dir1', '/bin/bash');
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT PRIMARY KEY,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
+
+CREATE TABLE quotalimits (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  per_session TEXT NOT NULL,
+  limit_type TEXT NOT NULL,
+  bytes_in_avail REAL NOT NULL,
+  bytes_out_avail REAL NOT NULL,
+  bytes_xfer_avail REAL NOT NULL,
+  files_in_avail INTEGER NOT NULL,
+  files_out_avail INTEGER NOT NULL,
+  files_xfer_avail INTEGER NOT NULL
+);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 32, 0, 0, 2, 0, 0);
+
+CREATE TABLE quotatallies (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  bytes_in_used REAL NOT NULL,
+  bytes_out_used REAL NOT NULL,
+  bytes_xfer_used REAL NOT NULL,
+  files_in_used INTEGER NOT NULL,
+  files_out_used INTEGER NOT NULL,
+  files_xfer_used INTEGER NOT NULL
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    DefaultChdir => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_quotatab_sql.c' => [
+        'SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM quotalimits WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM quotatallies WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = \'%{6}\' AND quota_type = \'%{7}\'" quotatallies',
+        'SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" quotatallies',
+
+        'QuotaEngine on',
+        "QuotaLog $log_file",
+        'QuotaLimitTable sql:/get-quota-limit',
+        'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+      ],
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLMinID => '0',
+        SQLNamedQuery => 'get-user-info SELECT "userid, passwd, uid, gid, homedir, shell FROM users WHERE userid=\'%U\'"',
+        SQLUserInfo => 'custom:/get-user-info',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      # Login as user1, and upload a file
+      $client->login($user1, $passwd);
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Login as user2, and upload a file
+      $client->login($user2, $passwd);
+
+      $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  my ($quota_type, $bytes_in_used, $bytes_out_used, $bytes_xfer_used, $files_in_used, $files_out_used, $files_xfer_used) = get_tally($db_file, "name = \'$group\'");
+
+  my $expected;
+
+  $expected = 'group';
+  $self->assert($expected eq $quota_type,
+    test_msg("Expected '$expected', got '$quota_type'"));
+
+  $expected = '^(26.0|26)$';
+  $self->assert(qr/$expected/, $bytes_in_used,
+    test_msg("Expected $expected, got $bytes_in_used"));
+
+  $expected = '^(0.0|0)$';
+  $self->assert(qr/$expected/, $bytes_out_used,
+    test_msg("Expected $expected, got $bytes_out_used"));
+
+  $expected = '^(0.0|0)$';
+  $self->assert(qr/$expected/, $bytes_xfer_used,
+    test_msg("Expected $expected, got $bytes_xfer_used"));
+
+  $expected = 2;
+  $self->assert($expected == $files_in_used,
+    test_msg("Expected $expected, got $files_in_used"));
+
+  $expected = 0;
+  $self->assert($expected == $files_out_used,
+    test_msg("Expected $expected, got $files_out_used"));
+
+  $expected = 0;
+  $self->assert($expected == $files_xfer_used,
+    test_msg("Expected $expected, got $files_xfer_used"));
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub quotatab_stor_ok_group_limit_bytes_in_exceeded_soft_limit  {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/quotatab.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/quotatab.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/quotatab.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user1 = 'proftpd';
+  my $group = 'ftpd';
+  my $passwd = 'test';
+  my $home_dir1 = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($home_dir1);
+
+  my $uid1 = 500;
+  my $gid = 500;
+
+  my $user2 = 'proftpd2';
+  my $home_dir2 = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($home_dir2);
+
+  my $uid2 = 1000;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT PRIMARY KEY,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '$passwd', $uid1, $gid, '$home_dir1', '/bin/bash');
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT PRIMARY KEY,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
+
+CREATE TABLE quotalimits (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  per_session TEXT NOT NULL,
+  limit_type TEXT NOT NULL,
+  bytes_in_avail REAL NOT NULL,
+  bytes_out_avail REAL NOT NULL,
+  bytes_xfer_avail REAL NOT NULL,
+  files_in_avail INTEGER NOT NULL,
+  files_out_avail INTEGER NOT NULL,
+  files_xfer_avail INTEGER NOT NULL
+);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 5, 0, 0, 3, 0, 0);
+
+CREATE TABLE quotatallies (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  bytes_in_used REAL NOT NULL,
+  bytes_out_used REAL NOT NULL,
+  bytes_xfer_used REAL NOT NULL,
+  files_in_used INTEGER NOT NULL,
+  files_out_used INTEGER NOT NULL,
+  files_xfer_used INTEGER NOT NULL
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    DefaultChdir => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_quotatab_sql.c' => [
+        'SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM quotalimits WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM quotatallies WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = \'%{6}\' AND quota_type = \'%{7}\'" quotatallies',
+        'SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" quotatallies',
+
+        'QuotaEngine on',
+        "QuotaLog $log_file",
+        'QuotaLimitTable sql:/get-quota-limit',
+        'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+      ],
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLMinID => '0',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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);
+
+      # Login as user1, and upload a file
+      $client->login($user1, $passwd);
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
+
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      # Login as user2, and upload a file
+      $client->login($user2, $passwd);
+
+      $conn = $client->stor_raw('test.txt');
+      if ($conn) {
+        die("STOR test.txt succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      my $expected = 552;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = 'STOR denied: quota exceeded: used \S+ of \S+ upload bytes';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
 
-  $expected = 2;
-  $self->assert($expected == $files_in_used,
-    test_msg("Expected $expected, got $files_in_used"));
+    exit 0;
+  }
 
-  $expected = 0;
-  $self->assert($expected == $files_out_used,
-    test_msg("Expected $expected, got $files_out_used"));
+  # Stop server
+  server_stop($pid_file);
 
-  $expected = 0;
-  $self->assert($expected == $files_xfer_used,
-    test_msg("Expected $expected, got $files_xfer_used"));
+  $self->assert_child_ok($pid);
 
   if ($ex) {
     test_append_logfile($log_file, $ex);
@@ -2692,7 +3477,7 @@ EOS
   unlink($log_file);
 }
 
-sub quotatab_stor_ok_group_limit_bytes_in_exceeded_soft_limit  {
+sub quotatab_stor_ok_group_limit_bytes_in_exceeded_hard_limit  {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2717,6 +3502,8 @@ sub quotatab_stor_ok_group_limit_bytes_in_exceeded_soft_limit  {
 
   my $uid2 = 1000;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/bar/test.txt");
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -2725,7 +3512,7 @@ sub quotatab_stor_ok_group_limit_bytes_in_exceeded_soft_limit  {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -2737,14 +3524,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -2755,10 +3542,10 @@ CREATE TABLE quotalimits (
   files_out_avail INTEGER NOT NULL,
   files_xfer_avail INTEGER NOT NULL
 );
-INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 5, 0, 0, 3, 0, 0);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'hard', 20, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -2867,10 +3654,15 @@ EOS
       $client->login($user2, $passwd);
 
       $conn = $client->stor_raw('test.txt');
-      if ($conn) {
-        die("STOR test.txt succeeded unexpectedly");
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
       }
 
+      $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
       $resp_code = $client->response_code();
       $resp_msg = $client->response_msg();
 
@@ -2878,11 +3670,15 @@ EOS
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = 'STOR denied: quota exceeded: used \S+ of \S+ upload bytes';
+      $expected = 'Transfer aborted. (Disc|Disk) quota exceeded';
       $self->assert(qr/$expected/, $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
       $client->quit();
+
+      if (-f $test_file) {
+        die("$test_file exists, should have been deleted");
+      }
     };
 
     if ($@) {
@@ -2917,7 +3713,7 @@ EOS
   unlink($log_file);
 }
 
-sub quotatab_stor_ok_group_limit_bytes_in_exceeded_hard_limit  {
+sub quotatab_stor_ok_group_limit_files_in_exceeded  {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2942,8 +3738,6 @@ sub quotatab_stor_ok_group_limit_bytes_in_exceeded_hard_limit  {
 
   my $uid2 = 1000;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/bar/test.txt");
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -2952,7 +3746,7 @@ sub quotatab_stor_ok_group_limit_bytes_in_exceeded_hard_limit  {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -2964,14 +3758,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -2982,10 +3776,10 @@ CREATE TABLE quotalimits (
   files_out_avail INTEGER NOT NULL,
   files_xfer_avail INTEGER NOT NULL
 );
-INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'hard', 20, 0, 0, 3, 0, 0);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 32, 0, 0, 1, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -3093,16 +3887,11 @@ EOS
       # Login as user2, and upload a file
       $client->login($user2, $passwd);
 
-      $conn = $client->stor_raw('test.txt');
-      unless ($conn) {
-        die("Failed to STOR test.txt: " . $client->response_code() . " " .
-          $client->response_msg());
+      $conn = $client->stor_raw('test2.txt');
+      if ($conn) {
+        die("STOR test2.txt succeeded unexpectedly");
       }
 
-      $buf = "Hello, World\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
-
       $resp_code = $client->response_code();
       $resp_msg = $client->response_msg();
 
@@ -3110,15 +3899,9 @@ EOS
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = 'Transfer aborted. (Disc|Disk) quota exceeded';
+      $expected = 'STOR: notice: quota reached: used \d+ of \d+ upload file';
       $self->assert(qr/$expected/, $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
-
-      $client->quit();
-
-      if (-f $test_file) {
-        die("$test_file exists, should have been deleted");
-      }
     };
 
     if ($@) {
@@ -3153,7 +3936,7 @@ EOS
   unlink($log_file);
 }
 
-sub quotatab_stor_ok_group_limit_files_in_exceeded  {
+sub quotatab_stor_ok_group_limit_with_default {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -3186,7 +3969,7 @@ sub quotatab_stor_ok_group_limit_files_in_exceeded  {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -3198,14 +3981,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user1,$user2');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -3216,10 +3999,10 @@ CREATE TABLE quotalimits (
   files_out_avail INTEGER NOT NULL,
   files_xfer_avail INTEGER NOT NULL
 );
-INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 32, 0, 0, 1, 0, 0);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$group', 'group', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -3272,6 +4055,7 @@ EOS
         "QuotaLog $log_file",
         'QuotaLimitTable sql:/get-quota-limit',
         'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+        'QuotaDefault group false hard 0 1 0 3 0 0',
       ],
 
       'mod_sql.c' => {
@@ -3280,6 +4064,8 @@ EOS
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
         SQLMinID => '0',
+        SQLNamedQuery => 'get-user-info SELECT "userid, passwd, uid, gid, homedir, shell FROM users WHERE userid=\'%U\'"',
+        SQLUserInfo => 'custom:/get-user-info',
       },
     },
   };
@@ -3327,21 +4113,21 @@ EOS
       # Login as user2, and upload a file
       $client->login($user2, $passwd);
 
-      $conn = $client->stor_raw('test2.txt');
-      if ($conn) {
-        die("STOR test2.txt succeeded unexpectedly");
+      $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
       }
 
+      $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
       $resp_code = $client->response_code();
       $resp_msg = $client->response_msg();
 
-      my $expected = 552;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      $expected = 'STOR: notice: quota reached: used \d+ of \d+ upload file';
-      $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
     };
 
     if ($@) {
@@ -3366,6 +4152,38 @@ EOS
 
   $self->assert_child_ok($pid);
 
+  my ($quota_type, $bytes_in_used, $bytes_out_used, $bytes_xfer_used, $files_in_used, $files_out_used, $files_xfer_used) = get_tally($db_file, "name = \'$group\'");
+
+  my $expected;
+
+  $expected = 'group';
+  $self->assert($expected eq $quota_type,
+    test_msg("Expected '$expected', got '$quota_type'"));
+
+  $expected = '^(26.0|26)$';
+  $self->assert(qr/$expected/, $bytes_in_used,
+    test_msg("Expected $expected, got $bytes_in_used"));
+
+  $expected = '^(0.0|0)$';
+  $self->assert(qr/$expected/, $bytes_out_used,
+    test_msg("Expected $expected, got $bytes_out_used"));
+
+  $expected = '^(0.0|0)$';
+  $self->assert(qr/$expected/, $bytes_xfer_used,
+    test_msg("Expected $expected, got $bytes_xfer_used"));
+
+  $expected = 2;
+  $self->assert($expected == $files_in_used,
+    test_msg("Expected $expected, got $files_in_used"));
+
+  $expected = 0;
+  $self->assert($expected == $files_out_used,
+    test_msg("Expected $expected, got $files_out_used"));
+
+  $expected = 0;
+  $self->assert($expected == $files_xfer_used,
+    test_msg("Expected $expected, got $files_xfer_used"));
+
   if ($ex) {
     test_append_logfile($log_file, $ex);
     unlink($log_file);
@@ -3411,7 +4229,7 @@ sub quotatab_stor_ok_class_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -3423,14 +4241,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -3444,7 +4262,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$class', 'class', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -3674,7 +4492,7 @@ sub quotatab_stor_ok_class_limit_bytes_in_exceeded_soft_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -3686,14 +4504,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -3707,7 +4525,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$class', 'class', 'false', 'soft', 5, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -3909,7 +4727,7 @@ sub quotatab_stor_ok_class_limit_bytes_in_exceeded_hard_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -3921,14 +4739,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -3942,7 +4760,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$class', 'class', 'false', 'hard', 20, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -4153,7 +4971,7 @@ sub quotatab_stor_ok_class_limit_files_in_exceeded {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -4165,14 +4983,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -4186,7 +5004,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$class', 'class', 'false', 'hard', 32, 0, 0, 1, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -4382,7 +5200,7 @@ sub quotatab_stor_ok_all_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -4394,14 +5212,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -4415,7 +5233,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('', 'all', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -4637,7 +5455,7 @@ sub quotatab_stor_ok_all_limit_bytes_in_exceeded_soft_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -4649,14 +5467,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -4670,7 +5488,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('', 'all', 'false', 'soft', 5, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -4864,7 +5682,7 @@ sub quotatab_stor_ok_all_limit_bytes_in_exceeded_hard_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -4876,14 +5694,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -4897,7 +5715,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('', 'all', 'false', 'hard', 20, 0, 0, 3, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -5100,7 +5918,7 @@ sub quotatab_stor_ok_all_limit_files_in_exceeded {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -5112,14 +5930,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user1', '
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user2', '$passwd', $uid2, $gid, '$home_dir2', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -5133,7 +5951,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('', 'all', 'false', 'hard', 32, 0, 0, 1, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -5315,7 +6133,7 @@ sub quotatab_stor_bug3164 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -5326,14 +6144,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -5347,7 +6165,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -5559,7 +6377,7 @@ sub quotatab_dele_ok_user_limit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -5570,14 +6388,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -5591,7 +6409,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 32, 3, 0, 3);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -5811,7 +6629,7 @@ sub quotatab_dele_user_owner_bug3161 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -5823,14 +6641,14 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$other_user', '$passwd', 777, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user,$other_user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -5845,7 +6663,7 @@ INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_ava
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$other_user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -6100,7 +6918,7 @@ sub quotatab_dele_group_owner_bug3161 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -6112,7 +6930,7 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$other_user', '$passwd', 777, 777, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
@@ -6120,7 +6938,7 @@ INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 INSERT INTO groups (groupname, gid, members) VALUES ('$other_group', 777, '$other_user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -6135,7 +6953,7 @@ INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_ava
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$other_group', 'group', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -6387,7 +7205,7 @@ sub quotatab_new_tally_lock_bug3086 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -6398,14 +7216,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -6419,7 +7237,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -6602,7 +7420,7 @@ sub quotatab_config_exclude_filter_bug3298 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -6613,14 +7431,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -6634,7 +7452,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -6839,7 +7657,7 @@ sub quotatab_config_exclude_filter_chrooted_bug3298 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -6850,14 +7668,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -6871,7 +7689,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -7062,7 +7880,7 @@ sub quotatab_config_exclude_filter_bug3878 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -7073,14 +7891,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -7094,7 +7912,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'hard', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -7289,7 +8107,7 @@ sub quotatab_config_opt_scanonlogin {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -7300,14 +8118,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -7321,7 +8139,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -7541,7 +8359,7 @@ sub quotatab_config_opt_scanonlogin_chrooted {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -7552,14 +8370,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -7573,7 +8391,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -7793,7 +8611,7 @@ sub quotatab_config_opt_scanonlogin_new_tally_bug3440 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -7804,14 +8622,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -7825,7 +8643,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -8044,7 +8862,7 @@ sub quotatab_site_bug3483 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -8055,14 +8873,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -8076,7 +8894,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -8261,7 +9079,7 @@ sub quotatab_dele_failed_bug3517 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -8272,14 +9090,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -8293,7 +9111,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -8504,7 +9322,7 @@ sub quotatab_sql_dele_bug3524 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -8515,14 +9333,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -8536,7 +9354,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 32, 3, 0, 3);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -8774,7 +9592,7 @@ sub quotatab_stor_deleteabortedstores_conn_abor_bug3621 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -8785,14 +9603,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -8806,7 +9624,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -8998,7 +9816,7 @@ sub quotatab_stor_deleteabortedstores_cmd_abor_bug3621 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -9009,14 +9827,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -9030,7 +9848,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -9234,7 +10052,7 @@ sub quotatab_sql_addl_query_columns_bug3879 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE users (
-  userid TEXT,
+  userid TEXT PRIMARY KEY,
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
@@ -9245,14 +10063,14 @@ CREATE TABLE users (
 INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', 500, 500, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
-  groupname TEXT,
+  groupname TEXT PRIMARY KEY,
   gid INTEGER,
   members TEXT
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', 500, '$user');
 
 CREATE TABLE quotalimits (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   per_session TEXT NOT NULL,
   limit_type TEXT NOT NULL,
@@ -9267,7 +10085,7 @@ CREATE TABLE quotalimits (
 INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail, ip_addr) VALUES ('$user', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0, '127.0.0.1');
 
 CREATE TABLE quotatallies (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   quota_type TEXT NOT NULL,
   bytes_in_used REAL NOT NULL,
   bytes_out_used REAL NOT NULL,
@@ -9434,4 +10252,186 @@ EOS
   unlink($log_file);
 }
 
+# See:
+#  https://forums.proftpd.org/smf/index.php/topic,11862.0.html
+#
+# Note that this is more for diagnosing ODBC issues than quotatab issues.
+sub quotatab_sql_odbc {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'quotatab');
+
+  my $odbcini_path = File::Spec->rel2abs("t/etc/modules/mod_sql_odbc/odbc.ini");
+  my $odbcinst_path = File::Spec->rel2abs("t/etc/modules/mod_sql_odbc/odbcinst.ini");
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE quotalimits (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  per_session TEXT NOT NULL,
+  limit_type TEXT NOT NULL,
+  bytes_in_avail REAL NOT NULL,
+  bytes_out_avail REAL NOT NULL,
+  bytes_xfer_avail REAL NOT NULL,
+  files_in_avail INTEGER NOT NULL,
+  files_out_avail INTEGER NOT NULL,
+  files_xfer_avail INTEGER NOT NULL
+);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$setup->{user}', 'user', 'false', 'soft', 32, 0, 0, 2, 0, 0);
+
+CREATE TABLE quotatallies (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  bytes_in_used REAL NOT NULL,
+  bytes_out_used REAL NOT NULL,
+  bytes_xfer_used REAL NOT NULL,
+  files_in_used INTEGER NOT NULL,
+  files_out_used INTEGER NOT NULL,
+  files_xfer_used INTEGER NOT NULL
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+    DefaultChdir => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_quotatab_sql.c' => [
+        'SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM quotalimits WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM quotatallies WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = \'%{6}\' AND quota_type = \'%{7}\'" quotatallies',
+        'SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" quotatallies',
+
+        'QuotaEngine on',
+        "QuotaLog $setup->{log_file}",
+        'QuotaLimitTable sql:/get-quota-limit',
+        'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+      ],
+
+      'mod_sql.c' => {
+        SQLAuthenticate => 'off',
+        SQLEngine => 'on',
+        SQLBackend => 'odbc',
+        SQLConnectInfo => "mysql proftpd developer PERCONNECTION",
+        SQLLogFile => $setup->{log_file},
+        SQLOptions => 'NoDisconnectOnError',
+      },
+
+      'mod_sql_odbc.c' => {
+        SQLODBCVersion => '2',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+# Necessary ODBC environment variables
+SetEnv ODBCINST $odbcinst_path
+SetEnv ODBCINI $odbcini_path
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("Failed to STOR test.txt: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "Hello, World\n";
+      $conn->write($buf, length($buf), 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_readme.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_readme.pm
index cf16793..b8f9c02 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_readme.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_readme.pm
@@ -59,40 +59,9 @@ sub list_tests {
 sub readme_login_path {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'readme');
 
-  my $config_file = "$tmpdir/readme.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/readme.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/readme.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/readme.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/readme.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $readme_file = File::Spec->rel2abs("$home_dir/README");
+  my $readme_file = File::Spec->rel2abs("$setup->{home_dir}/README");
   if (open(my $fh, "> $readme_file")) {
     print $fh "Hello, mod_readme users!\n";
 
@@ -105,14 +74,14 @@ sub readme_login_path {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'response:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -125,7 +94,8 @@ sub readme_login_path {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -143,7 +113,7 @@ sub readme_login_path {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg(0);
@@ -152,23 +122,23 @@ sub readme_login_path {
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "User $user logged in";
+      $expected = "User $setup->{user} logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
  
       $resp_msg = $client->response_msg(2);
 
       $expected = "Please read the file (.*?)\/README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(3);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -181,7 +151,7 @@ sub readme_login_path {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -191,64 +161,29 @@ sub readme_login_path {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub readme_login_path_nonexistent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'readme');
 
-  my $config_file = "$tmpdir/readme.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/readme.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/readme.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/readme.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/readme.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $readme_file = File::Spec->rel2abs("$home_dir/README");
+  my $readme_file = File::Spec->rel2abs("$setup->{home_dir}/README");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'response:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -261,7 +196,8 @@ sub readme_login_path_nonexistent {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -279,7 +215,7 @@ sub readme_login_path_nonexistent {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg(0);
@@ -288,11 +224,11 @@ sub readme_login_path_nonexistent {
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "User $user logged in";
+      $expected = "User $setup->{user} logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
  
       $resp_msg = $client->response_msg(2);
       $self->assert(!defined($resp_msg),
@@ -309,7 +245,7 @@ sub readme_login_path_nonexistent {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -319,54 +255,19 @@ sub readme_login_path_nonexistent {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub readme_login_pattern {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'readme');
 
-  my $config_file = "$tmpdir/readme.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/readme.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/readme.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/readme.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/readme.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $readme_file = File::Spec->rel2abs("$home_dir/README");
+  my $readme_file = File::Spec->rel2abs("$setup->{home_dir}/README");
   if (open(my $fh, "> $readme_file")) {
     print $fh "Hello, mod_readme users!\n";
 
@@ -379,14 +280,14 @@ sub readme_login_pattern {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'response:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -399,7 +300,8 @@ sub readme_login_pattern {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -417,7 +319,7 @@ sub readme_login_pattern {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg(0);
@@ -426,23 +328,23 @@ sub readme_login_pattern {
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "User $user logged in";
+      $expected = "User $setup->{user} logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
  
       $resp_msg = $client->response_msg(2);
 
       $expected = "Please read the file README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(3);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -455,7 +357,7 @@ sub readme_login_pattern {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -465,54 +367,19 @@ sub readme_login_pattern {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub readme_cwd_pattern {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'readme');
 
-  my $config_file = "$tmpdir/readme.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/readme.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/readme.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/readme.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/readme.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $readme_file = File::Spec->rel2abs("$home_dir/README");
+  my $readme_file = File::Spec->rel2abs("$setup->{home_dir}/README");
   if (open(my $fh, "> $readme_file")) {
     print $fh "Hello, mod_readme users!\n";
 
@@ -524,7 +391,7 @@ sub readme_cwd_pattern {
     die("Can't open $readme_file: $!");
   }
 
-  my $subdir = File::Spec->rel2abs("$home_dir/subdir.d");
+  my $subdir = File::Spec->rel2abs("$setup->{home_dir}/subdir.d");
   mkpath($subdir);
 
   $readme_file = File::Spec->rel2abs("$subdir/README");
@@ -540,14 +407,14 @@ sub readme_cwd_pattern {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'response:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -560,7 +427,8 @@ sub readme_cwd_pattern {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -578,7 +446,7 @@ sub readme_cwd_pattern {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg(0);
@@ -587,23 +455,23 @@ sub readme_cwd_pattern {
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "User $user logged in";
+      $expected = "User $setup->{user} logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
  
       $resp_msg = $client->response_msg(2);
 
       $expected = "Please read the file README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(3);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->cwd('subdir.d');
 
@@ -612,23 +480,23 @@ sub readme_cwd_pattern {
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(1);
 
       $expected = " Please read the file README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(2);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -641,7 +509,7 @@ sub readme_cwd_pattern {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -651,54 +519,19 @@ sub readme_cwd_pattern {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub readme_cwd_pattern_multiple_matches {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'readme');
 
-  my $config_file = "$tmpdir/readme.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/readme.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/readme.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/readme.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/readme.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $readme_file = File::Spec->rel2abs("$home_dir/README");
+  my $readme_file = File::Spec->rel2abs("$setup->{home_dir}/README");
   if (open(my $fh, "> $readme_file")) {
     print $fh "Hello, mod_readme users!\n";
 
@@ -710,7 +543,7 @@ sub readme_cwd_pattern_multiple_matches {
     die("Can't open $readme_file: $!");
   }
 
-  my $subdir = File::Spec->rel2abs("$home_dir/subdir.d");
+  my $subdir = File::Spec->rel2abs("$setup->{home_dir}/subdir.d");
   mkpath($subdir);
 
   $readme_file = File::Spec->rel2abs("$subdir/README");
@@ -738,14 +571,14 @@ sub readme_cwd_pattern_multiple_matches {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'response:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -758,7 +591,8 @@ sub readme_cwd_pattern_multiple_matches {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -776,7 +610,7 @@ sub readme_cwd_pattern_multiple_matches {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg(0);
@@ -785,23 +619,23 @@ sub readme_cwd_pattern_multiple_matches {
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "User $user logged in";
+      $expected = "User $setup->{user} logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
  
       $resp_msg = $client->response_msg(2);
 
       $expected = "Please read the file README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(3);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->cwd('subdir.d');
 
@@ -810,35 +644,35 @@ sub readme_cwd_pattern_multiple_matches {
 
       $expected = 250;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "CWD command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(1);
 
       $expected = " Please read the file README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(2);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(3);
 
       $expected = " Please read the file READMETOO";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(4);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -851,7 +685,7 @@ sub readme_cwd_pattern_multiple_matches {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -861,54 +695,19 @@ sub readme_cwd_pattern_multiple_matches {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub readme_login_path_displaylogin_bug3605 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'readme');
 
-  my $config_file = "$tmpdir/readme.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/readme.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/readme.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/readme.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/readme.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $login_file = File::Spec->rel2abs("$home_dir/.welcome");
+  my $login_file = File::Spec->rel2abs("$setup->{home_dir}/.welcome");
   if (open(my $fh, "> $login_file")) {
     print $fh "\nWelcome to my FTP server, %U.\n";
     print $fh "You are user number %N out of\n";
@@ -923,7 +722,7 @@ sub readme_login_path_displaylogin_bug3605 {
     die("Can't open $login_file: $!");
   }
 
-  my $readme_file = File::Spec->rel2abs("$home_dir/README");
+  my $readme_file = File::Spec->rel2abs("$setup->{home_dir}/README");
   if (open(my $fh, "> $readme_file")) {
     print $fh "Hello, mod_readme users!\n";
 
@@ -936,14 +735,14 @@ sub readme_login_path_displaylogin_bug3605 {
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'response:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     DisplayLogin => $login_file,
 
@@ -958,7 +757,8 @@ sub readme_login_path_displaylogin_bug3605 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -976,7 +776,7 @@ sub readme_login_path_displaylogin_bug3605 {
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg(0);
@@ -985,25 +785,25 @@ sub readme_login_path_displaylogin_bug3605 {
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $resp_msg = $client->response_msg(6);
 
-      $expected = " User $user logged in";
+      $expected = " User $setup->{user} logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
  
       $resp_msg = $client->response_msg(8);
 
       $expected = "Please read the file (.*?)\/README";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $resp_msg = $client->response_msg(9);
 
       $expected = "it was last modified on (.*?) \- 0 days ago";
       $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       $client->quit();
     };
@@ -1016,7 +816,7 @@ sub readme_login_path_displaylogin_bug3605 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1026,15 +826,11 @@ sub readme_login_path_displaylogin_bug3605 {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_rlimit.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_rlimit.pm
new file mode 100644
index 0000000..141c475
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_rlimit.pm
@@ -0,0 +1,128 @@
+package ProFTPD::Tests::Modules::mod_rlimit;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  rlimit_memory => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub rlimit_memory {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'rlimit');
+
+  my $rlimit_mem = '32K';
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'response:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_rlimit.c' => {
+        RLimitMemory => "session $rlimit_mem $rlimit_mem",
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR failed: " . $client->response_code() . ' ' .
+          $client->response_msg());
+      }
+
+      my $count = 100;
+      my $buf = 'AbCdEfGh' x 8192;
+      for (my $i = 0; $i < $count; $i++) {
+        $conn->write($buf, length($buf), 15);
+      }
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm
index 0d605b6..c919844 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp.pm
@@ -22,7 +22,27 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
-  ssh2_connect_bad_version => {
+  ssh2_connect_bad_version_bad_format => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_connect_bad_version_unsupported_proto_version => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_connect_bad_version_too_long => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_connect_bad_version_too_short => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_connect_version_with_comments => {
     order => ++$order,
     test_class => [qw(bug forking ssh2)],
   },
@@ -301,6 +321,21 @@ my $TESTS = {
     test_class => [qw(forking ssh2)],
   },
 
+  ssh2_auth_publickey_rsa2048_min_4096_bug4233 => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_auth_publickey_rsa2048_no_nl => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_auth_publickey_rsa2048_2nd_key => {
+    order => ++$order,
+    test_class => [qw(forking ssh2)],
+  },
+
   ssh2_auth_publickey_rsa4096 => {
     order => ++$order,
     test_class => [qw(forking ssh2)],
@@ -366,6 +401,11 @@ my $TESTS = {
     test_class => [qw(forking ssh2)],
   },
 
+  ssh2_auth_password_failed => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
   ssh2_auth_kbdint_failed_password_ok => {
     order => ++$order,
     test_class => [qw(bug forking mod_sftp_pam ssh2)],
@@ -376,6 +416,16 @@ my $TESTS = {
     test_class => [qw(forking ssh2)],
   },
 
+  ssh2_auth_publickey_password_chain_bug4153 => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  ssh2_auth_publickey_publickey_chain_bug4153 => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
   ssh2_interop_scanner => {
     order => ++$order,
     test_class => [qw(forking ssh2)],
@@ -436,6 +486,46 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
+  sftp_stat_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_stat_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_stat_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_stat_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_stat_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_stat_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_stat_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_stat_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_fstat => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -456,6 +546,46 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
+  sftp_setstat_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_setstat_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_setstat_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_setstat_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_setstat_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_setstat_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_setstat_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_setstat_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_fsetstat => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -536,6 +666,46 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
+  sftp_open_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_open_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_open_abs_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_open_abs_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_open_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_open_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_open_rel_symlink_enoent => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_open_rel_symlink_enoent_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_upload => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -585,7 +755,14 @@ my $TESTS = {
   # Requires libssh2-1.2.8 or later, with fixed compression
   sftp_download_with_compression => {
     order => ++$order,
-    test_class => [qw(forking inprogress sftp ssh2)],
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  # See:
+  #   https://github.com/proftpd/proftpd/issues/323
+  sftp_download_with_compression_rekeying => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
   },
 
   sftp_download_zero_len_file => {
@@ -608,11 +785,35 @@ my $TESTS = {
     test_class => [qw(bug forking sftp ssh2)],
   },
 
-  sftp_ext_download_rekey => {
+  sftp_ext_download_server_rekey => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_download_server_rekey => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
   },
 
+  # TODO: sftp_ext_download_client_rekey
+  # The issues I found with this test are:
+  #   1. Net::SSH2 does not support client-initiated rekeying
+  #   2. Attempting to use the -oRekeyLimit for publickey-based auth using
+  #     sftp(1) causes login to fail; NOT using that option allows the
+  #     login to succeed.  Thus I suspect a bug in that version of
+  #     OpenSSH sftp(1).  For posterity, this is on a Mac OSX machine:
+  #       $ uname -a
+  #       Darwin Ts-MacBook-Pro.local 11.2.0 Darwin Kernel Version 11.2.0: Tue Aug  9 20:54:00 PDT 2011; root:xnu-1699.24.8~1/RELEASE_X86_64 x86_64
+  #
+  #     with OpenSSH version:
+  #       $ ssh -version
+  #       OpenSSH_5.6p1, OpenSSL 0.9.8r 8 Feb 2011
+
+  sftp_ext_download_rekey_rsa1024_hostkey_bug4097 => {
+    order => ++$order,
+    test_class => [qw(bug forking sftp ssh2)],
+  },
+
   sftp_download_readonly_bug3787 => {
     order => ++$order,
     test_class => [qw(bug forking sftp ssh2)],
@@ -623,11 +824,31 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
-  sftp_readdir_symlink_dir => {
+  sftp_readdir_abs_symlink_dir => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_readdir_abs_symlink_dir_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_readdir_abs_symlink_dir_vroot => {
     order => ++$order,
     test_class => [qw(forking mod_vroot sftp ssh2)],
   },
 
+  sftp_readdir_rel_symlink_dir => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_readdir_rel_symlink_dir_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_readdir_wide_dir => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -643,6 +864,31 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
+  sftp_mkdir_eexist => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_mkdir_abs_symlink_eexist => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_mkdir_abs_symlink_eexist_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_mkdir_rel_symlink_eexist => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_mkdir_rel_symlink_eexist_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_mkdir_readdir_bug3481 => {
     order => ++$order,
     test_class => [qw(bug forking sftp ssh2)],
@@ -658,6 +904,26 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
+  sftp_rmdir_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_rmdir_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_rmdir_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_rmdir_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_remove => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -683,11 +949,26 @@ my $TESTS = {
     test_class => [qw(forking sftp ssh2)],
   },
 
-  sftp_readlink => {
+  sftp_readlink_abs_dst => {
+    order => ++$order,
+    test_class => [qw(forking sftp ssh2)],
+  },
+
+  sftp_readlink_abs_dst_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
+  sftp_readlink_rel_dst => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
   },
 
+  sftp_readlink_rel_dst_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs sftp ssh2)],
+  },
+
   sftp_readlink_symlink_dir_bug4140 => {
     order => ++$order,
     test_class => [qw(bug forking sftp ssh2)],
@@ -993,6 +1274,11 @@ my $TESTS = {
     test_class => [qw(bug forking sftp ssh2)],
   },
 
+  sftp_config_insecure_hostkey_perms_bug4098 => {
+    order => ++$order,
+    test_class => [qw(bug forking sftp ssh2)],
+  },
+
   sftp_multi_channels => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -1118,6 +1404,16 @@ my $TESTS = {
     test_class => [qw(bug forking sftp ssh2)],
   },
 
+  sftp_log_extlog_env_banner_bug4065 => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
+  sftp_log_extlog_userauth_full_request => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
   sftp_sighup => {
     order => ++$order,
     test_class => [qw(forking sftp ssh2)],
@@ -1153,6 +1449,26 @@ my $TESTS = {
     test_class => [qw(forking scp ssh2)],
   },
 
+  scp_upload_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking scp ssh2)],
+  },
+
+  scp_upload_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs scp ssh2)],
+  },
+
+  scp_upload_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking scp ssh2)],
+  },
+
+  scp_upload_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs scp ssh2)],
+  },
+
   scp_upload_subdir_enoent => {
     order => ++$order,
     test_class => [qw(forking scp ssh2)],
@@ -1188,6 +1504,11 @@ my $TESTS = {
     test_class => [qw(bug forking scp ssh2)],
   },
 
+  scp_ext_upload_recursive_dirs_bug4257 => {
+    order => ++$order,
+    test_class => [qw(bug forking scp ssh2)],
+  },
+
   scp_ext_upload_different_name_bug3425 => {
     order => ++$order,
     test_class => [qw(bug forking scp ssh2)],
@@ -1233,6 +1554,26 @@ my $TESTS = {
     test_class => [qw(forking inprogress scp ssh2)],
   },
 
+  scp_download_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking scp ssh2)],
+  },
+
+  scp_download_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs scp ssh2)],
+  },
+
+  scp_download_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking scp ssh2)],
+  },
+
+  scp_download_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs scp ssh2)],
+  },
+
   scp_ext_download_bug3544 => {
     order => ++$order,
     test_class => [qw(bug forking scp ssh2)],
@@ -1454,6 +1795,7 @@ sub set_up {
   # files.
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $rsa1024_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa1024_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
   my $ecdsa256_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_ecdsa256_key');
   my $ecdsa384_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_ecdsa384_key');
@@ -1461,61 +1803,166 @@ sub set_up {
   my $passphrase_rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/passphrase_host_rsa_key');
   my $passphrase_dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/passphrase_host_dsa_key');
 
-  unless (chmod(0400, $rsa_host_key, $dsa_host_key,
+  unless (chmod(0400, $rsa_host_key, $rsa1024_host_key, $dsa_host_key,
       $ecdsa256_host_key, $ecdsa384_host_key, $ecdsa521_host_key,
       $passphrase_rsa_host_key, $passphrase_dsa_host_key)) {
-    die("Can't set perms on $rsa_host_key, $dsa_host_key, $ecdsa256_host_key, $ecdsa384_host_key, $ecdsa521_host_key, $passphrase_rsa_host_key, $passphrase_dsa_host_key: $!");
+    die("Can't set perms on $rsa_host_key, $rsa1024_host_key, $dsa_host_key, $ecdsa256_host_key, $ecdsa384_host_key, $ecdsa521_host_key, $passphrase_rsa_host_key, $passphrase_dsa_host_key: $!");
   }
 }
 
-sub ssh2_connect_bad_version {
+sub concat_files {
+  my $src_path = shift;
+  my $dst_path = shift;
+
+  if (open(my $dst_fh, ">> $dst_path")) {
+    if (open(my $src_fh, "< $src_path")) {
+      while (my $line = <$src_fh>) {
+        print $dst_fh $line;
+      }
+
+      close($src_fh);
+
+      unless (close($dst_fh)) {
+        die("Can't write to $dst_path: $!");
+      }
+
+    } else {
+      my $ex = $!;
+      close($dst_fh);
+      die("Can't read $src_path: $ex");
+    }
+
+  } else {
+    die("Can't append to $dst_path: $!");
+  }
+
+  return 1;
+}
+
+sub ssh2_connect_bad_version_bad_format {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $log_file = test_get_logfile();
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20',
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        'SFTPOptions PessimisticKexinit',
+      ],
+    },
+  };
+
+  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 $proto = getprotobyname('tcp');
+
+      my $sock;
+      unless (socket($sock, PF_INET, SOCK_STREAM, $proto)) {
+        die("Can't create socket: $!");
+      }
+
+      my $in_addr = inet_aton('127.0.0.1');
+      my $addr = sockaddr_in($port, $in_addr);
+
+      unless (connect($sock, $addr)) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
+
+      $sock->autoflush(1);
+      print $sock "AAAA\r\n";
+
+      my $resp = '';
+      my $len = read($sock, $resp, 64);
+      $self->assert($len > 0, test_msg("Expected response, got none"));
+
+      chomp($resp); 
+
+      my $expected = 'Protocol mismatch.';
+      $self->assert(qr/$expected/, $resp,
+        test_msg("Expected '$expected', got '$resp'"));
+
+      close($sock); 
+    };
+
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+
+  $self->assert_child_ok($pid);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub ssh2_connect_bad_version_unsupported_proto_version {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -1524,14 +1971,16 @@ sub ssh2_connect_bad_version {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+        'SFTPOptions PessimisticKexinit',
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -1564,7 +2013,18 @@ sub ssh2_connect_bad_version {
         die("Can't connect to 127.0.0.1:$port: $!");
       }
 
-      print $sock "AAAA" x 1024;
+      $sock->autoflush(1);
+      print $sock "SSH-6.6-FOO\r\n";
+
+      my $resp = '';
+      my $len = read($sock, $resp, 64);
+      $self->assert($len > 0, test_msg("Expected response, got none"));
+
+      chomp($resp); 
+
+      my $expected = 'Protocol mismatch.';
+      $self->assert(qr/$expected/, $resp,
+        test_msg("Expected '$expected', got '$resp'"));
 
       close($sock); 
     };
@@ -1577,7 +2037,7 @@ sub ssh2_connect_bad_version {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -1587,18 +2047,331 @@ sub ssh2_connect_bad_version {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+sub ssh2_connect_bad_version_too_long {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-    die($ex);
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        'SFTPOptions PessimisticKexinit',
+      ],
+    },
+  };
+
+  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: $!");
   }
 
-  unlink($log_file);
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(1);
+
+      my $proto = getprotobyname('tcp');
+
+      my $sock;
+      unless (socket($sock, PF_INET, SOCK_STREAM, $proto)) {
+        die("Can't create socket: $!");
+      }
+
+      my $in_addr = inet_aton('127.0.0.1');
+      my $addr = sockaddr_in($port, $in_addr);
+
+      unless (connect($sock, $addr)) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
+
+      $sock->autoflush(1);
+      print $sock "SSH-2.0-" . 'AAAA' x 1024 . "\r\n";
+
+      my $resp = '';
+      my $len = read($sock, $resp, 64);
+      $self->assert($len > 0, test_msg("Expected response, got none"));
+
+      chomp($resp); 
+
+      my $expected = 'Protocol mismatch.';
+      $self->assert(qr/$expected/, $resp,
+        test_msg("Expected '$expected', got '$resp'"));
+
+      close($sock); 
+    };
+
+    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 ssh2_connect_bad_version_too_short {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        'SFTPOptions PessimisticKexinit',
+      ],
+    },
+  };
+
+  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 $proto = getprotobyname('tcp');
+
+      my $sock;
+      unless (socket($sock, PF_INET, SOCK_STREAM, $proto)) {
+        die("Can't create socket: $!");
+      }
+
+      my $in_addr = inet_aton('127.0.0.1');
+      my $addr = sockaddr_in($port, $in_addr);
+
+      unless (connect($sock, $addr)) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
+
+      $sock->autoflush(1);
+      print $sock "SSH-2.0-\r\n";
+
+      my $resp = '';
+      my $len = read($sock, $resp, 64);
+      $self->assert($len > 0, test_msg("Expected response, got none"));
+
+      chomp($resp); 
+
+      my $expected = 'Protocol mismatch.';
+      $self->assert(qr/$expected/, $resp,
+        test_msg("Expected '$expected', got '$resp'"));
+
+      close($sock); 
+    };
+
+    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 ssh2_connect_version_with_comments {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        'SFTPOptions PessimisticKexinit',
+      ],
+    },
+  };
+
+  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 $proto = getprotobyname('tcp');
+
+      my $sock;
+      unless (socket($sock, PF_INET, SOCK_STREAM, $proto)) {
+        die("Can't create socket: $!");
+      }
+
+      my $in_addr = inet_aton('127.0.0.1');
+      my $addr = sockaddr_in($port, $in_addr);
+
+      unless (connect($sock, $addr)) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
+
+      $sock->autoflush(1);
+      print $sock "SSH-2.0-ProFTPD_TestSuite internal testing here!\r\n";
+
+      my $resp = '';
+      my $len = read($sock, $resp, 64);
+      $self->assert($len > 0, test_msg("Expected response, got none"));
+
+      chomp($resp); 
+
+      my $expected = '^SSH-2.0-mod_sftp';
+      $self->assert(qr/$expected/, $resp,
+        test_msg("Expected '$expected', got '$resp'"));
+
+      close($sock); 
+    };
+
+    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 ssh2_connect_version_bug3918 {
@@ -2498,8 +3271,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -2748,8 +3521,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -2998,8 +3771,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -3780,8 +4553,8 @@ sub ssh2_hostkey_dss_bug3634 {
           $res = 0;
 
         } else {
+          $errstr = join('', <$sftp_eh>);
           if ($ENV{TEST_VERBOSE}) {
-            $errstr = join('', <$sftp_eh>);
             print STDERR "Stderr: $errstr\n";
           }
 
@@ -4154,8 +4927,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -4403,8 +5176,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -4652,8 +5425,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -8635,8 +9408,8 @@ EOC
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -9656,6 +10429,111 @@ sub ssh2_auth_publickey_rsa_with_match_bug3493 {
   unlink($log_file);
 }
 
+sub ssh2_auth_publickey_rsa2048_2nd_key {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys2');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_publickey($setup->{user}, $rsa_pub_key,
+          $rsa_priv_key)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+    };
+
+    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 ssh2_auth_publickey_rsa2048 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -9797,7 +10675,7 @@ sub ssh2_auth_publickey_rsa2048 {
   unlink($log_file);
 }
 
-sub ssh2_auth_publickey_rsa4096 {
+sub ssh2_auth_publickey_rsa2048_no_nl {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9836,9 +10714,13 @@ sub ssh2_auth_publickey_rsa4096 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa4096_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa4096_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa4096_keys');
+# Deliberately choose a key file which does NOT end in a LF.  Ultimately
+# the mod_sftp code uses fsio_gets() to read the file, line by line, and that
+# function expects an LF terminator for the line.
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys_no_nl');
 
   my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
   unless (copy($rsa_rfc4716_key, $authorized_keys)) {
@@ -9938,7 +10820,119 @@ sub ssh2_auth_publickey_rsa4096 {
   unlink($log_file);
 }
 
-sub ssh2_auth_publickey_rsa8192 {
+sub ssh2_auth_publickey_rsa2048_min_4096_bug4233 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+        "SFTPKeyLimits MinimumRSASize 4096 MinimumDSASize 384",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      if ($ssh2->auth_publickey($setup->{user}, $rsa_pub_key, $rsa_priv_key)) {
+        die("RSA publickey authentication succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+      $ssh2->disconnect();
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# error code: $err_code\n";
+        print STDERR "# error name: $err_name\n";
+        print STDERR "# error message: $err_str\n";
+      }
+
+      my $expected = 'LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+    };
+    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 ssh2_auth_publickey_rsa4096 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9977,9 +10971,9 @@ sub ssh2_auth_publickey_rsa8192 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa8192_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa8192_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa8192_keys');
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa4096_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa4096_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa4096_keys');
 
   my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
   unless (copy($rsa_rfc4716_key, $authorized_keys)) {
@@ -10079,7 +11073,7 @@ sub ssh2_auth_publickey_rsa8192 {
   unlink($log_file);
 }
 
-sub ssh2_auth_publickey_rsa16384 {
+sub ssh2_auth_publickey_rsa8192 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10118,27 +11112,24 @@ sub ssh2_auth_publickey_rsa16384 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa16384_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa16384_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa16384_keys');
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa8192_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa8192_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa8192_keys');
 
   my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
   unless (copy($rsa_rfc4716_key, $authorized_keys)) {
     die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
   }
 
-  my $timeout_idle = 15;
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutIdle => $timeout_idle,
 
     IfModules => {
       'mod_delay.c' => {
@@ -10199,7 +11190,7 @@ sub ssh2_auth_publickey_rsa16384 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout_idle + 5) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -10223,7 +11214,7 @@ sub ssh2_auth_publickey_rsa16384 {
   unlink($log_file);
 }
 
-sub ssh2_auth_publickey_dsa1024 {
+sub ssh2_auth_publickey_rsa16384 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10262,24 +11253,27 @@ sub ssh2_auth_publickey_dsa1024 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $dsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa_key');
-  my $dsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa_key.pub');
-  my $dsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_dsa_keys');
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa16384_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa16384_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa16384_keys');
 
   my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
-  unless (copy($dsa_rfc4716_key, $authorized_keys)) {
-    die("Can't copy $dsa_rfc4716_key to $authorized_keys: $!");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
   }
 
+  my $timeout_idle = 15;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    TimeoutIdle => $timeout_idle,
 
     IfModules => {
       'mod_delay.c' => {
@@ -10324,9 +11318,9 @@ sub ssh2_auth_publickey_dsa1024 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_publickey($user, $dsa_pub_key, $dsa_priv_key)) {
+      unless ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("DSA publickey authentication failed: [$err_name] ($err_code) $err_str");
+        die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str");
       }
 
       $ssh2->disconnect();
@@ -10340,7 +11334,7 @@ sub ssh2_auth_publickey_dsa1024 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout_idle + 5) };
     if ($@) {
       warn($@);
       exit 1;
@@ -10364,7 +11358,7 @@ sub ssh2_auth_publickey_dsa1024 {
   unlink($log_file);
 }
 
-sub ssh2_auth_publickey_dsa2048 {
+sub ssh2_auth_publickey_dsa1024 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10403,9 +11397,9 @@ sub ssh2_auth_publickey_dsa2048 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $dsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa2048_key');
-  my $dsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa2048_key.pub');
-  my $dsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_dsa2048_keys');
+  my $dsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa_key');
+  my $dsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa_key.pub');
+  my $dsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_dsa_keys');
 
   my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
   unless (copy($dsa_rfc4716_key, $authorized_keys)) {
@@ -10505,7 +11499,148 @@ sub ssh2_auth_publickey_dsa2048 {
   unlink($log_file);
 }
 
-sub ssh2_auth_publickey_dsa4096 {
+sub ssh2_auth_publickey_dsa2048 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $dsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa2048_key');
+  my $dsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa2048_key.pub');
+  my $dsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_dsa2048_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($dsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $dsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_publickey($user, $dsa_pub_key, $dsa_priv_key)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("DSA publickey authentication failed: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub ssh2_auth_publickey_dsa4096 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -11106,8 +12241,8 @@ sub ssh2_ext_auth_publickey_ecdsa256 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -11336,8 +12471,8 @@ sub ssh2_ext_auth_publickey_ecdsa384 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -11566,8 +12701,8 @@ sub ssh2_ext_auth_publickey_ecdsa521 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$sftp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -11767,6 +12902,102 @@ sub ssh2_auth_no_authorized_keys {
   unlink($log_file);
 }
 
+sub ssh2_auth_password_failed {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthMethods password",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $bad_passwd = 'FOOBAR';
+      for (my $i = 0; $i < 4; $i++) {
+        if ($ssh2->auth_password($setup->{user}, $bad_passwd)) {
+          die("Password authentication succeeded unexpectedly");
+        }
+      }
+
+      $ssh2->disconnect();
+    };
+
+    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 ssh2_auth_kbdint_failed_password_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -12088,7 +13319,7 @@ sub ssh2_auth_twice {
   unlink($log_file);
 }
 
-sub ssh2_interop_scanner {
+sub ssh2_auth_publickey_password_chain_bug4153 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12127,14 +13358,21 @@ sub ssh2_interop_scanner {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $banner = "SSH_Version_Mapper";
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -12149,6 +13387,8 @@ sub ssh2_interop_scanner {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+        "SFTPAuthMethods publickey+password",
       ],
     },
   };
@@ -12173,13 +13413,31 @@ sub ssh2_interop_scanner {
   if ($pid) {
     eval {
       my $ssh2 = Net::SSH2->new();
-      $ssh2->banner($banner);
 
       sleep(1);
 
-      if ($ssh2->connect('127.0.0.1', $port)) {
-        die("Connect to SSH2 server succeeded unexpectedly");
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Login succeeded unexpectedly with just publickey authentication");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
+
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -12211,37 +13469,171 @@ sub ssh2_interop_scanner {
     die($ex);
   }
 
-  if (open(my $fh, "< $log_file")) {
-    my $ok = 0;
+  unlink($log_file);
+}
 
-    while (my $line = <$fh>) {
-      chomp($line);
+sub ssh2_auth_publickey_publickey_chain_bug4153 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
 
-      if ($line =~ /SSH2 scan from '(\S+)',/) {
-          my $text = $1;
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
 
-          if ($text eq $banner) {
-            $ok = 1;
-            last;
-          }
-      }
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    close($fh);
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
 
-    unless ($ok) {
-      die("SFTPLog message about scanner unexpectedly missing");
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $dsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa_key');
+  my $dsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_dsa_key.pub');
+  my $dsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_dsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  unless (concat_files($dsa_rfc4716_key, $authorized_keys)) {
+    die("Can't append $dsa_rfc4716_key to $authorized_keys");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+        "SFTPAuthMethods publickey+publickey",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Login succeeded unexpectedly with just publickey authentication");
+      }
+
+      unless ($ssh2->auth_publickey($user, $dsa_pub_key, $dsa_priv_key)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
     }
 
+    $wfh->print("done\n");
+    $wfh->flush();
+
   } else {
-    die("Can't read $log_file: $!");
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
   }
 
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
 
   unlink($log_file);
 }
 
-sub ssh2_interop_probe {
+sub ssh2_interop_scanner {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12280,7 +13672,7 @@ sub ssh2_interop_probe {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $banner = "Probe-Me";
+  my $banner = "SSH_Version_Mapper";
 
   my $config = {
     PidFile => $pid_file,
@@ -12370,7 +13762,7 @@ sub ssh2_interop_probe {
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /SSH2 probe from '(\S+)',/) {
+      if ($line =~ /SSH2 scan from '(\S+)',/) {
           my $text = $1;
 
           if ($text eq $banner) {
@@ -12394,7 +13786,7 @@ sub ssh2_interop_probe {
   unlink($log_file);
 }
 
-sub ssh2_channel_failed_ptyreq {
+sub ssh2_interop_probe {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12433,6 +13825,8 @@ sub ssh2_channel_failed_ptyreq {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $banner = "Probe-Me";
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -12477,32 +13871,13 @@ sub ssh2_channel_failed_ptyreq {
   if ($pid) {
     eval {
       my $ssh2 = Net::SSH2->new();
+      $ssh2->banner($banner);
 
       sleep(1);
 
-      unless ($ssh2->connect('127.0.0.1', $port)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't authenticate to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $chan = $ssh2->channel('session');
-      unless ($chan) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't open 'session' channel: [$err_name] ($err_code) $err_str");
-      }
-
-      if ($chan->pty('vt100')) {
-        die("'pty-req' session channel request succeeded unexpectedly");
+      if ($ssh2->connect('127.0.0.1', $port)) {
+        die("Connect to SSH2 server succeeded unexpectedly");
       }
-
-      $chan->eof();
-      $chan->close();
-      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -12534,10 +13909,37 @@ sub ssh2_channel_failed_ptyreq {
     die($ex);
   }
 
+  if (open(my $fh, "< $log_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+      if ($line =~ /SSH2 probe from '(\S+)',/) {
+          my $text = $1;
+
+          if ($text eq $banner) {
+            $ok = 1;
+            last;
+          }
+      }
+    }
+
+    close($fh);
+
+    unless ($ok) {
+      die("SFTPLog message about scanner unexpectedly missing");
+    }
+
+  } else {
+    die("Can't read $log_file: $!");
+  }
+
+
   unlink($log_file);
 }
 
-sub ssh2_channel_failed_shell {
+sub ssh2_channel_failed_ptyreq {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12639,8 +14041,8 @@ sub ssh2_channel_failed_shell {
         die("Can't open 'session' channel: [$err_name] ($err_code) $err_str");
       }
 
-      if ($chan->shell()) {
-        die("'shell' session channel request succeeded unexpectedly");
+      if ($chan->pty('vt100')) {
+        die("'pty-req' session channel request succeeded unexpectedly");
       }
 
       $chan->eof();
@@ -12680,7 +14082,7 @@ sub ssh2_channel_failed_shell {
   unlink($log_file);
 }
 
-sub ssh2_channel_failed_exec_cmd {
+sub ssh2_channel_failed_shell {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12745,8 +14147,6 @@ sub ssh2_channel_failed_exec_cmd {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  my $have_sftp_pam = feature_have_module_compiled('mod_sftp_pam.c');
-
   # 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.
@@ -12784,8 +14184,8 @@ sub ssh2_channel_failed_exec_cmd {
         die("Can't open 'session' channel: [$err_name] ($err_code) $err_str");
       }
 
-      if ($chan->exec('date')) {
-        die("'exec' session channel request succeeded unexpectedly");
+      if ($chan->shell()) {
+        die("'shell' session channel request succeeded unexpectedly");
       }
 
       $chan->eof();
@@ -12825,7 +14225,7 @@ sub ssh2_channel_failed_exec_cmd {
   unlink($log_file);
 }
 
-sub ssh2_channel_env_default {
+sub ssh2_channel_failed_exec_cmd {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12929,27 +14329,8 @@ sub ssh2_channel_env_default {
         die("Can't open 'session' channel: [$err_name] ($err_code) $err_str");
       }
 
-      my $barred = [qw(
-        SFTP
-        SFTP_LIBRARY_VERSION
-        SFTP_CLIENT_CIPHER_ALGO
-        SFTP_CLIENT_MAC_ALGO
-        SFTP_CLIENT_COMPRESSION_ALGO
-        SFTP_KEX_ALGO
-        SFTP_SERVER_CIPHER_ALGO
-        SFTP_SERVER_MAC_ALGO
-        SFTP_SERVER_COMPRESSION_ALGO
-      )];
-
-      foreach my $key (@$barred) {
-        if ($chan->setenv($key, "1")) {
-          die("Setting environment variable '$key' via 'env' channel succeeded unexpectedly");
-        }
-      }
-
-      unless ($chan->setenv('LANG', 'FOO')) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Setting environment variable 'LANG' failed: [$err_name] ($err_code) $err_str");
+      if ($chan->exec('date')) {
+        die("'exec' session channel request succeeded unexpectedly");
       }
 
       $chan->eof();
@@ -12989,7 +14370,7 @@ sub ssh2_channel_env_default {
   unlink($log_file);
 }
 
-sub ssh2_channel_env_accept_glob_char {
+sub ssh2_channel_env_default {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13044,7 +14425,6 @@ sub ssh2_channel_env_accept_glob_char {
       },
 
       'mod_sftp.c' => [
-        'SFTPAcceptEnv L*',
         "SFTPEngine on",
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
@@ -13117,11 +14497,6 @@ sub ssh2_channel_env_accept_glob_char {
         die("Setting environment variable 'LANG' failed: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($chan->setenv('LOG', 'FOO')) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Setting environment variable 'LOG' failed: [$err_name] ($err_code) $err_str");
-      }
-
       $chan->eof();
       $chan->close();
       $ssh2->disconnect();
@@ -13159,7 +14534,7 @@ sub ssh2_channel_env_accept_glob_char {
   unlink($log_file);
 }
 
-sub ssh2_channel_env_accept_single_char {
+sub ssh2_channel_env_accept_glob_char {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13214,7 +14589,7 @@ sub ssh2_channel_env_accept_single_char {
       },
 
       'mod_sftp.c' => [
-        'SFTPAcceptEnv L?G',
+        'SFTPAcceptEnv L*',
         "SFTPEngine on",
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
@@ -13282,8 +14657,9 @@ sub ssh2_channel_env_accept_single_char {
         }
       }
 
-      if ($chan->setenv('LANG', 'FOO')) {
-        die("Setting environment variable 'LANG' via 'env' channel succeeded unexpectedly");
+      unless ($chan->setenv('LANG', 'FOO')) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Setting environment variable 'LANG' failed: [$err_name] ($err_code) $err_str");
       }
 
       unless ($chan->setenv('LOG', 'FOO')) {
@@ -13328,7 +14704,7 @@ sub ssh2_channel_env_accept_single_char {
   unlink($log_file);
 }
 
-sub ssh2_channel_max_exceeded {
+sub ssh2_channel_env_accept_single_char {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13383,11 +14759,11 @@ sub ssh2_channel_max_exceeded {
       },
 
       'mod_sftp.c' => [
+        'SFTPAcceptEnv L?G',
         "SFTPEngine on",
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "SFTPMaxChannels 1",
       ],
     },
   };
@@ -13433,9 +14809,31 @@ sub ssh2_channel_max_exceeded {
         die("Can't open 'session' channel: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      if ($sftp) {
-        die("Open 'sftp' channel succeeded unexpectedly");
+      my $barred = [qw(
+        SFTP
+        SFTP_LIBRARY_VERSION
+        SFTP_CLIENT_CIPHER_ALGO
+        SFTP_CLIENT_MAC_ALGO
+        SFTP_CLIENT_COMPRESSION_ALGO
+        SFTP_KEX_ALGO
+        SFTP_SERVER_CIPHER_ALGO
+        SFTP_SERVER_MAC_ALGO
+        SFTP_SERVER_COMPRESSION_ALGO
+      )];
+
+      foreach my $key (@$barred) {
+        if ($chan->setenv($key, "1")) {
+          die("Setting environment variable '$key' via 'env' channel succeeded unexpectedly");
+        }
+      }
+
+      if ($chan->setenv('LANG', 'FOO')) {
+        die("Setting environment variable 'LANG' via 'env' channel succeeded unexpectedly");
+      }
+
+      unless ($chan->setenv('LOG', 'FOO')) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Setting environment variable 'LOG' failed: [$err_name] ($err_code) $err_str");
       }
 
       $chan->eof();
@@ -13475,7 +14873,7 @@ sub ssh2_channel_max_exceeded {
   unlink($log_file);
 }
 
-sub ssh2_disconnect_client {
+sub ssh2_channel_max_exceeded {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13514,8 +14912,6 @@ sub ssh2_disconnect_client {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_idle = 5;
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -13526,8 +14922,6 @@ sub ssh2_disconnect_client {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    TimeoutIdle => $timeout_idle,
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -13538,6 +14932,7 @@ sub ssh2_disconnect_client {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+        "SFTPMaxChannels 1",
       ],
     },
   };
@@ -13558,8 +14953,6 @@ sub ssh2_disconnect_client {
 
   my $ex;
 
-  my $start_time = time();
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -13574,155 +14967,24 @@ sub ssh2_disconnect_client {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Don't explicitly disconnect.  But idle for 2 seconds, to see if
-      # the server is properly waiting (up to TimeoutIdle); this will be
-      # reflected in the generated logs.
-      sleep(2);
-    };
-
-    if ($@) {
-      $ex = $@;
-    }
-
-    $wfh->print("done\n");
-    $wfh->flush();
-
-  } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
-    }
-
-    exit 0;
-  }
-
-  # Stop server
-  server_stop($pid_file);
-
-  $self->assert_child_ok($pid);
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  my $elapsed = time()- $start_time;
-  if ($elapsed > $timeout_idle) {
-    die("Expected less than $timeout_idle, got $elapsed");
-  }
-
-  unlink($log_file);
-}
-
-sub sftp_without_auth {
-  my $self = shift;
-  my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
-  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
-
-  my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
-    IfModules => {
-      'mod_delay.c' => {
-        DelayEngine => 'off',
-      },
-
-      'mod_sftp.c' => [
-        "SFTPEngine on",
-        "SFTPLog $log_file",
-        "SFTPHostKey $rsa_host_key",
-        "SFTPHostKey $dsa_host_key",
-      ],
-    },
-  };
-
-  my ($port, $config_user, $config_group) = config_write($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::SSH2;
-
-  my $ex;
-
-  # Fork child
-  $self->handle_sigchld();
-  defined(my $pid = fork()) or die("Can't fork: $!");
-  if ($pid) {
-    eval {
-      my $ssh2 = Net::SSH2->new();
-
-      sleep(1);
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't authenticate to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      unless ($ssh2->connect('127.0.0.1', $port)) {
+      my $chan = $ssh2->channel('session');
+      unless ($chan) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't open 'session' channel: [$err_name] ($err_code) $err_str");
       }
 
       my $sftp = $ssh2->sftp();
       if ($sftp) {
-        die("Started SFTP channel unexpectedly");
+        die("Open 'sftp' channel succeeded unexpectedly");
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected;
-
-      # The expected error messages depend on the version of libssh2 being
-      # used.
-      $self->assert($err_name eq 'LIBSSH2_ERROR_INVAL' or
-                    $err_name eq 'LIBSSH2_ERROR_CHANNEL_FAILURE',
-        test_msg("Expected 'LIBSSH2_ERROR_INVAL' or 'LIBSSH2_ERROR_CHANNEL_FAILURE', got '$err_name'"));
-
+      $chan->eof();
+      $chan->close();
       $ssh2->disconnect();
     };
 
@@ -13758,7 +15020,7 @@ sub sftp_without_auth {
   unlink($log_file);
 }
 
-sub sftp_stat {
+sub ssh2_disconnect_client {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13797,6 +15059,8 @@ sub sftp_stat {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $timeout_idle = 5;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -13807,6 +15071,8 @@ sub sftp_stat {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TimeoutIdle => $timeout_idle,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -13823,7 +15089,7 @@ sub sftp_stat {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  my $config_size = (stat($config_file))[7];
+  my $have_sftp_pam = feature_have_module_compiled('mod_sftp_pam.c');
 
   # 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
@@ -13837,6 +15103,8 @@ sub sftp_stat {
 
   my $ex;
 
+  my $start_time = time();
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -13851,42 +15119,10 @@ sub sftp_stat {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-      
-      my $attrs = $sftp->stat('sftp.conf', 1);
-      unless ($attrs) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
-      }
-
-      my $expected;
-
-      $expected = $config_size;
-      my $file_size = $attrs->{size};
-      $self->assert($expected == $file_size,
-        test_msg("Expected '$expected', got '$file_size'"));
-
-      $expected = $<;
-      my $file_uid = $attrs->{uid};
-      $self->assert($expected == $file_uid,
-        test_msg("Expected '$expected', got '$file_uid'"));
-
-      $expected = $(;
-      my $file_gid = $attrs->{gid};
-      $self->assert($expected == $file_gid,
-        test_msg("Expected '$expected', got '$file_gid'"));
-
-      $sftp = undef;
-      $ssh2->disconnect();
+      # Don't explicitly disconnect.  But idle for 2 seconds, to see if
+      # the server is properly waiting (up to TimeoutIdle); this will be
+      # reflected in the generated logs.
+      sleep(2);
     };
 
     if ($@) {
@@ -13918,10 +15154,15 @@ sub sftp_stat {
     die($ex);
   }
 
+  my $elapsed = time()- $start_time;
+  if ($elapsed > $timeout_idle) {
+    die("Expected less than $timeout_idle, got $elapsed");
+  }
+
   unlink($log_file);
 }
 
-sub sftp_fstat {
+sub sftp_without_auth {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13986,8 +15227,6 @@ sub sftp_fstat {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  my $config_size = (stat($config_file))[7];
-
   # 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.
@@ -14014,50 +15253,21 @@ sub sftp_fstat {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
       my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-     
-      my $fh = $sftp->open('sftp.conf', O_RDONLY); 
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open sftp.conf: [$err_name] ($err_code)");
-      }
-
-      my $attrs = $fh->stat();
-      unless ($attrs) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_FSTAT sftp.conf failed: [$err_name] ($err_code)");
+      if ($sftp) {
+        die("Started SFTP channel unexpectedly");
       }
 
-      # Explicitly destroy the handle so that an FXP_CLOSE is sent.
-      $fh = undef;
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
 
       my $expected;
 
-      $expected = $config_size;
-      my $file_size = $attrs->{size};
-      $self->assert($expected == $file_size,
-        test_msg("Expected '$expected', got '$file_size'"));
-
-      $expected = $<;
-      my $file_uid = $attrs->{uid};
-      $self->assert($expected == $file_uid,
-        test_msg("Expected '$expected', got '$file_uid'"));
-
-      $expected = $(;
-      my $file_gid = $attrs->{gid};
-      $self->assert($expected == $file_gid,
-        test_msg("Expected '$expected', got '$file_gid'"));
+      # The expected error messages depend on the version of libssh2 being
+      # used.
+      $self->assert($err_name eq 'LIBSSH2_ERROR_INVAL' or
+                    $err_name eq 'LIBSSH2_ERROR_CHANNEL_FAILURE',
+        test_msg("Expected 'LIBSSH2_ERROR_INVAL' or 'LIBSSH2_ERROR_CHANNEL_FAILURE', got '$err_name'"));
 
-      $sftp = undef;
       $ssh2->disconnect();
     };
 
@@ -14093,54 +15303,23 @@ sub sftp_fstat {
   unlink($log_file);
 }
 
-sub sftp_lstat {
+sub sftp_stat {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'sftp');
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -14149,34 +15328,17 @@ sub sftp_lstat {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 1024;
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_size = (stat($test_file))[7];
-
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
-  unless (symlink($test_file, $test_symlink)) {
-    die("Can't symlink $test_symlink to $test_file: $!");
-  }
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  my $test_symlink_size = (lstat($test_symlink))[7];
+  my $config_size = (stat($setup->{config_file}))[7];
 
   # 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
@@ -14204,7 +15366,7 @@ sub sftp_lstat {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -14214,34 +15376,32 @@ sub sftp_lstat {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-     
-      my $attrs = $sftp->stat('test.lnk', 0);
+
+      my $path = 'sftp.conf';
+      my $attrs = $sftp->stat($path, 1);
       unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_LSTAT test.lnk failed: [$err_name] ($err_code)");
+        die("STAT $path failed: [$err_name] ($err_code)");
       }
 
-      my $expected;
-
-      $expected = $test_symlink_size;
+      my $expected = $config_size;
       my $file_size = $attrs->{size};
       $self->assert($expected == $file_size,
-        test_msg("Expected '$expected', got '$file_size'"));
+        test_msg("Expected file size '$expected', got '$file_size'"));
 
       $expected = $<;
       my $file_uid = $attrs->{uid};
       $self->assert($expected == $file_uid,
-        test_msg("Expected '$expected', got '$file_uid'"));
+        test_msg("Expected file UID '$expected', got '$file_uid'"));
 
       $expected = $(;
       my $file_gid = $attrs->{gid};
       $self->assert($expected == $file_gid,
-        test_msg("Expected '$expected', got '$file_gid'"));
+        test_msg("Expected file GID '$expected', got '$file_gid'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -14250,7 +15410,7 @@ sub sftp_lstat {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -14260,68 +15420,67 @@ sub sftp_lstat {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_setstat {
+sub sftp_stat_abs_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_size = (stat($test_file))[7];
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -14330,14 +15489,15 @@ sub sftp_setstat {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -14365,7 +15525,7 @@ sub sftp_setstat {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -14375,37 +15535,32 @@ sub sftp_setstat {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-     
-      my $res = $sftp->setstat('sftp.conf',
-        atime => 0,
-        mtime => 0,
-      ); 
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't setstat sftp.conf: [$err_name] ($err_code)");
-      }
 
-      my $attrs = $sftp->stat('sftp.conf');
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
       unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
+        die("STAT $path failed: [$err_name] ($err_code)");
       }
 
-      $sftp = undef;
-      $ssh2->disconnect();
+      my $expected = $test_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected file size '$expected', got '$file_size'"));
 
-      my $expected;
+      $expected = $<;
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected file UID '$expected', got '$file_uid'"));
 
-      $expected = 0;
-      my $file_atime = $attrs->{atime};
-      $self->assert($expected == $file_atime,
-        test_msg("Expected '$expected', got '$file_atime'"));
+      $expected = $(;
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID '$expected', got '$file_gid'"));
 
-      my $file_mtime = $attrs->{mtime};
-      $self->assert($expected == $file_mtime,
-        test_msg("Expected '$expected', got '$file_mtime'"));
+      $sftp = undef;
+      $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -14414,7 +15569,7 @@ sub sftp_setstat {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -14424,68 +15579,69 @@ sub sftp_setstat {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_setstat_sgid {
+sub sftp_stat_abs_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_size = (stat($test_file))[7];
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -14494,14 +15650,15 @@ sub sftp_setstat_sgid {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -14529,7 +15686,7 @@ sub sftp_setstat_sgid {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -14539,30 +15696,40 @@ sub sftp_setstat_sgid {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-     
-      my $res = $sftp->setstat('sftp.conf',
-        mode => oct(2664),
-      ); 
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't setstat sftp.conf: [$err_name] ($err_code)");
-      }
 
-      my $attrs = $sftp->stat('sftp.conf');
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
       unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
+        die("STAT $path failed: [$err_name] ($err_code)");
       }
 
-      my $expected = '2664';
-      my $file_mode = sprintf("%lo", (34228 & 07777));
-      $self->assert($expected eq $file_mode,
-        test_msg("Expected '$expected', got '$file_mode'"));
+      my $expected = $test_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected file size '$expected', got '$file_size'"));
+
+      $expected = $<;
+      if ($< == 0) {
+        $expected = $setup->{uid};
+      }
+
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected file UID '$expected', got '$file_uid'"));
+
+      $expected = $(;
+      if ($< == 0) {
+        $expected = $setup->{gid};
+      }
+
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID '$expected', got '$file_gid'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -14571,7 +15738,7 @@ sub sftp_setstat_sgid {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -14581,68 +15748,55 @@ sub sftp_setstat_sgid {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_fsetstat {
+sub sftp_stat_abs_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -14651,14 +15805,15 @@ sub sftp_fsetstat {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -14686,7 +15841,7 @@ sub sftp_fsetstat {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -14696,46 +15851,21 @@ sub sftp_fsetstat {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-    
-      my $fh = $sftp->open('sftp.conf', O_RDONLY); 
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open sftp.conf: [$err_name] ($err_code)");
-      }
 
-      my $res = $fh->setstat(
-        atime => 0,
-        mtime => 0,
-      ); 
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't fsetstat sftp.conf: [$err_name] ($err_code)");
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
+      if ($attrs) {
+        die("STAT $path succeeded unexpectedly");
       }
 
-      # Explicitly destroy the handle to issue the FXP_CLOSE
-      $fh = undef;
-
-      my $attrs = $sftp->stat('sftp.conf');
-      unless ($attrs) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
-      }
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
 
       $sftp = undef;
       $ssh2->disconnect();
-
-      my $expected;
-
-      $expected = 0;
-      my $file_atime = $attrs->{atime};
-      $self->assert($expected == $file_atime,
-        test_msg("Expected '$expected', got '$file_atime'"));
-
-      my $file_mtime = $attrs->{mtime};
-      $self->assert($expected == $file_mtime,
-        test_msg("Expected '$expected', got '$file_mtime'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -14744,7 +15874,7 @@ sub sftp_fsetstat {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -14754,68 +15884,57 @@ sub sftp_fsetstat {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_realpath {
+sub sftp_stat_abs_symlink_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -14824,14 +15943,15 @@ sub sftp_realpath {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -14859,7 +15979,7 @@ sub sftp_realpath {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -14870,22 +15990,20 @@ sub sftp_realpath {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $cwd = $sftp->realpath('.');
-      unless ($cwd) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for '.': [$err_name] ($err_code)");
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
+      if ($attrs) {
+        die("STAT $path succeeded unexpectedly");
       }
 
-      my $expected;
-
-      $expected = $home_dir;
-      $self->assert($expected eq $cwd,
-        test_msg("Expected '$expected', got '$cwd'"));
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -14894,7 +16012,7 @@ sub sftp_realpath {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -14904,60 +16022,21 @@ sub sftp_realpath {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_realpath_file {
+sub sftp_stat_rel_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
-  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     print $fh "Hello, World!\n";
     unless (close($fh)) {
@@ -14968,15 +16047,46 @@ sub sftp_realpath_file {
     die("Can't open $test_file: $!");
   }
 
-  my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+  my $test_size = (stat($test_file))[7];
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -14985,14 +16095,15 @@ sub sftp_realpath_file {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -15020,7 +16131,7 @@ sub sftp_realpath_file {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -15031,22 +16142,31 @@ sub sftp_realpath_file {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $real_path = $sftp->realpath('test.txt');
-      unless ($real_path) {
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
+      unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.txt': [$err_name] ($err_code)");
+        die("STAT $path failed: [$err_name] ($err_code)");
       }
 
-      my $expected;
+      my $expected = $test_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected file size '$expected', got '$file_size'"));
 
-      $expected = $test_file;
-      $self->assert($expected eq $real_path,
-        test_msg("Expected real path '$expected', got '$real_path'"));
+      $expected = $<;
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected file UID '$expected', got '$file_uid'"));
+
+      $expected = $(;
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID '$expected', got '$file_gid'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -15055,7 +16175,7 @@ sub sftp_realpath_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -15065,60 +16185,21 @@ sub sftp_realpath_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_realpath_symlink_file {
+sub sftp_stat_rel_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
-  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     print $fh "Hello, World!\n";
     unless (close($fh)) {
@@ -15129,28 +16210,48 @@ sub sftp_realpath_symlink_file {
     die("Can't open $test_file: $!");
   }
 
+  my $test_size = (stat($test_file))[7];
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
   my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
   }
 
-  unless (symlink('test.txt', 'test.lnk')) {
-    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
   }
 
   unless (chdir($cwd)) {
     die("Can't chdir to $cwd: $!");
   }
 
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -15159,14 +16260,15 @@ sub sftp_realpath_symlink_file {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -15194,7 +16296,7 @@ sub sftp_realpath_symlink_file {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -15205,22 +16307,39 @@ sub sftp_realpath_symlink_file {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $real_path = $sftp->realpath('test.lnk');
-      unless ($real_path) {
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
+      unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
+        die("STAT $path failed: [$err_name] ($err_code)");
       }
 
-      my $expected;
+      my $expected = $test_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected file size '$expected', got '$file_size'"));
 
-      $expected = $test_file;
-      $self->assert($expected eq $real_path,
-        test_msg("Expected real path '$expected', got '$real_path'"));
+      $expected = $<;
+      if ($< == 0) {
+        $expected = $setup->{uid};
+      }
+
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected file UID '$expected', got '$file_uid'"));
+
+      $expected = $(;
+      if ($< == 0) {
+        $expected = $setup->{gid};
+      }
+
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID '$expected', got '$file_gid'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -15229,7 +16348,7 @@ sub sftp_realpath_symlink_file {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -15239,96 +16358,60 @@ sub sftp_realpath_symlink_file {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_realpath_symlink_file_chrooted {
+sub sftp_stat_rel_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # This is what the client should see, since it is chrooted
-  my $chrooted_file = "/test.txt";
-
-  my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
-  }
-
-  unless (symlink('./test.txt', 'test.lnk')) {
-    die("Can't symlink 'test.txt' to 'test.lnk': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -15337,14 +16420,15 @@ sub sftp_realpath_symlink_file_chrooted {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -15372,7 +16456,7 @@ sub sftp_realpath_symlink_file_chrooted {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -15383,22 +16467,20 @@ sub sftp_realpath_symlink_file_chrooted {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $real_path = $sftp->realpath('test.lnk');
-      unless ($real_path) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
+      if ($attrs) {
+        die("STAT $path succeeded unexpectedly");
       }
 
-      my $expected;
-
-      $expected = $chrooted_file;
-      $self->assert($expected eq $real_path,
-        test_msg("Expected real path '$expected', got '$real_path'"));
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -15407,7 +16489,7 @@ sub sftp_realpath_symlink_file_chrooted {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -15417,71 +16499,62 @@ sub sftp_realpath_symlink_file_chrooted {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_realpath_dir {
+sub sftp_stat_rel_symlink_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($test_dir);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -15490,14 +16563,15 @@ sub sftp_realpath_dir {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -15525,7 +16599,7 @@ sub sftp_realpath_dir {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -15536,22 +16610,20 @@ sub sftp_realpath_dir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $real_path = $sftp->realpath('test.d');
-      unless ($real_path) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.d': [$err_name] ($err_code)");
+      my $path = 'test.d/test.lnk';
+      my $attrs = $sftp->stat($path, 1);
+      if ($attrs) {
+        die("STAT $path succeeded unexpectedly");
       }
 
-      my $expected;
-
-      $expected = $test_dir;
-      $self->assert($expected eq $real_path,
-        test_msg("Expected real path '$expected', got '$real_path'"));
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
 
       $sftp = undef;
       $ssh2->disconnect();
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -15560,7 +16632,7 @@ sub sftp_realpath_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -15570,21 +16642,13 @@ sub sftp_realpath_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_realpath_symlink_dir {
+sub sftp_fstat {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -15623,22 +16687,6 @@ sub sftp_realpath_symlink_dir {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($test_dir);
-
-  my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
-  }
-
-  unless (symlink('test.d', 'test.lnk')) {
-    die("Can't symlink 'test.d' to 'test.lnk': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -15665,6 +16713,8 @@ sub sftp_realpath_symlink_dir {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $config_size = (stat($config_file))[7];
+
   # 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.
@@ -15702,17 +16752,37 @@ sub sftp_realpath_symlink_dir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $real_path = $sftp->realpath('test.lnk');
-      unless ($real_path) {
+      my $fh = $sftp->open('sftp.conf', O_RDONLY);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
+        die("Can't open sftp.conf: [$err_name] ($err_code)");
+      }
+
+      my $attrs = $fh->stat();
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_FSTAT sftp.conf failed: [$err_name] ($err_code)");
       }
 
+      # Explicitly destroy the handle so that an FXP_CLOSE is sent.
+      $fh = undef;
+
       my $expected;
 
-      $expected = $test_dir;
-      $self->assert($expected eq $real_path,
-        test_msg("Expected real path '$expected', got '$real_path'"));
+      $expected = $config_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected '$expected', got '$file_size'"));
+
+      $expected = $<;
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected '$expected', got '$file_uid'"));
+
+      $expected = $(;
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected '$expected', got '$file_gid'"));
 
       $sftp = undef;
       $ssh2->disconnect();
@@ -15750,7 +16820,7 @@ sub sftp_realpath_symlink_dir {
   unlink($log_file);
 }
 
-sub sftp_realpath_symlink_dir_chrooted {
+sub sftp_lstat {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -15789,25 +16859,6 @@ sub sftp_realpath_symlink_dir_chrooted {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($test_dir);
-
-  # This is the path the client should see, since it is chrooted
-  my $chrooted_dir = "/test.d";
-
-  my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
-  }
-
-  unless (symlink('test.d', 'test.lnk')) {
-    die("Can't symlink 'test.d' to 'test.lnk': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -15817,7 +16868,6 @@ sub sftp_realpath_symlink_dir_chrooted {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -15835,6 +16885,26 @@ sub sftp_realpath_symlink_dir_chrooted {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 1024;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_size = (stat($test_file))[7];
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
+
+  my $test_symlink_size = (lstat($test_symlink))[7];
+
   # 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.
@@ -15872,17 +16942,28 @@ sub sftp_realpath_symlink_dir_chrooted {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $real_path = $sftp->realpath('test.lnk');
-      unless ($real_path) {
+      my $attrs = $sftp->stat('test.lnk', 0);
+      unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
+        die("FXP_LSTAT test.lnk failed: [$err_name] ($err_code)");
       }
 
       my $expected;
 
-      $expected = $chrooted_dir;
-      $self->assert($expected eq $real_path,
-        test_msg("Expected real path '$expected', got '$real_path'"));
+      $expected = $test_symlink_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected file size '$expected', got '$file_size'"));
+
+      $expected = $<;
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected file UID '$expected', got '$file_uid'"));
+
+      $expected = $(;
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID '$expected', got '$file_gid'"));
 
       $sftp = undef;
       $ssh2->disconnect();
@@ -15920,56 +17001,23 @@ sub sftp_realpath_symlink_dir_chrooted {
   unlink($log_file);
 }
 
-sub sftp_open_enoent_bug3345 {
+sub sftp_setstat {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'sftp');
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -15978,14 +17026,15 @@ sub sftp_open_enoent_bug3345 {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -15999,9 +17048,6 @@ sub sftp_open_enoent_bug3345 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -16016,7 +17062,7 @@ sub sftp_open_enoent_bug3345 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -16027,36 +17073,43 @@ sub sftp_open_enoent_bug3345 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      if ($fh) {
-        die("OPEN test.txt succeeded unexpectedly");
+      my $path = 'sftp.conf';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't setstat $path: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected;
-
-      $expected = 'SSH_FX_NO_SUCH_FILE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
-      $expected = 2;
-      $self->assert($expected == $err_code,
-        test_msg("Expected $expected, got $err_code"));
+      my $attrs = $sftp->stat($path);
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("STAT $path failed: [$err_name] ($err_code)");
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
-    };
 
-    if ($@) {
-      $ex = $@;
+      my $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected file atime '$expected', got '$file_atime'"));
+
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
+    };
+    if ($@) {
+      $ex = $@;
     }
 
     $wfh->print("done\n");
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -16066,21 +17119,13 @@ sub sftp_open_enoent_bug3345 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_open_trunc_bug3449 {
+sub sftp_setstat_sgid {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -16128,7 +17173,6 @@ sub sftp_open_trunc_bug3449 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -16146,19 +17190,6 @@ sub sftp_open_trunc_bug3449 {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 1024;
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_size = (stat($test_file))[7];
-
   # 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.
@@ -16196,61 +17227,24 @@ sub sftp_open_trunc_bug3449 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $attrs = $sftp->stat('test.txt', 0);
-      unless ($attrs) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("LSTAT test.txt failed: [$err_name] ($err_code)");
-      }
-
-      my $expected;
-
-      $expected = $test_size;
-      my $file_size = $attrs->{size};
-      $self->assert($expected == $file_size,
-        test_msg("Expected '$expected', got '$file_size'"));
-
-      $expected = $<;
-      my $file_uid = $attrs->{uid};
-      $self->assert($expected == $file_uid,
-        test_msg("Expected '$expected', got '$file_uid'"));
-
-      $expected = $(;
-      my $file_gid = $attrs->{gid};
-      $self->assert($expected == $file_gid,
-        test_msg("Expected '$expected', got '$file_gid'"));
-
-      my $orig_size = $attrs->{size};
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_TRUNC);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("OPEN test.txt failed: [$err_name] ($err_code)");
-      }
-
-      $attrs = $sftp->stat('test.txt', 0);
-      unless ($attrs) {
+      my $res = $sftp->setstat('sftp.conf',
+        mode => oct(2664),
+      );
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("LSTAT test.txt failed: [$err_name] ($err_code)");
+        die("Can't setstat sftp.conf: [$err_name] ($err_code)");
       }
 
-      $expected = 0;
-      $file_size = $attrs->{size};
-      $self->assert($expected == $file_size,
-        test_msg("Expected '$expected', got '$file_size'"));
-
-      # To issue a CLOSE, we need to destroy the filehandle
-      $fh = undef;
-
-      $attrs = $sftp->stat('test.txt', 0);
+      my $attrs = $sftp->stat('sftp.conf');
       unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("LSTAT test.txt failed: [$err_name] ($err_code)");
+        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
       }
 
-      $expected = 0;
-      $file_size = $attrs->{size};
-      $self->assert($expected == $file_size,
-        test_msg("Expected '$expected', got '$file_size'"));
+      my $expected = '2664';
+      my $file_mode = sprintf("%lo", (34228 & 07777));
+      $self->assert($expected eq $file_mode,
+        test_msg("Expected '$expected', got '$file_mode'"));
 
       $sftp = undef;
       $ssh2->disconnect();
@@ -16288,55 +17282,56 @@ sub sftp_open_trunc_bug3449 {
   unlink($log_file);
 }
 
-sub sftp_open_creat {
+sub sftp_setstat_abs_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -16345,16 +17340,15 @@ sub sftp_open_creat {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  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
@@ -16382,7 +17376,7 @@ sub sftp_open_creat {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -16393,23 +17387,34 @@ sub sftp_open_creat {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_CREAT, 0640);
-      unless ($fh) {
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("OPEN test.txt failed: [$err_name] ($err_code)");
+        die("Can't setstat $path: [$err_name] ($err_code)");
       }
 
-      # To issue a CLOSE, we need to destroy the filehandle
-      $fh = undef;
+      my $attrs = $sftp->stat($path);
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("STAT $path failed: [$err_name] ($err_code)");
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
-      }
-    };
+      my $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected file atime '$expected', got '$file_atime'"));
 
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -16418,7 +17423,7 @@ sub sftp_open_creat {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -16428,69 +17433,64 @@ sub sftp_open_creat {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_open_creat_excl {
+sub sftp_setstat_abs_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -16499,25 +17499,15 @@ sub sftp_open_creat_excl {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
+  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
@@ -16545,7 +17535,7 @@ sub sftp_open_creat_excl {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -16556,40 +17546,34 @@ sub sftp_open_creat_excl {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # This should fail, since the file already exists
-      my $fh = $sftp->open('test.txt', O_CREAT|O_EXCL, 0640);
-      if ($fh) {
-        die("OPEN test.txt succeeded unexpectedly");
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't setstat $path: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected;
-
-      $expected = 'SSH_FX_FAILURE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
-      # Now remove that file, and try again.
-      unlink($test_file);
-
-      $fh = $sftp->open('test.txt', O_CREAT|O_EXCL, 0640);
-      unless ($fh) {
+      my $attrs = $sftp->stat($path);
+      unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("OPEN test.txt failed: [$err_name] ($err_code)");
+        die("STAT $path failed: [$err_name] ($err_code)");
       }
 
-      # To issue a CLOSE, we need to destroy the filehandle
-      $fh = undef;
-
       $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
-      }
-    };
+      my $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected file atime '$expected', got '$file_atime'"));
 
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -16598,7 +17582,7 @@ sub sftp_open_creat_excl {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -16608,70 +17592,55 @@ sub sftp_open_creat_excl {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_open_append_bug3450 {
+sub sftp_setstat_abs_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
-    AllowStoreRestart => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -16680,27 +17649,15 @@ sub sftp_open_append_bug3450 {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_size = (stat($test_file))[7];
+  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
@@ -16728,7 +17685,7 @@ sub sftp_open_append_bug3450 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -16739,34 +17696,23 @@ sub sftp_open_append_bug3450 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_APPEND);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("OPEN test.txt failed: [$err_name] ($err_code)");
-      }
-
-      unless ($fh->write("ABCD")) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("WRITE test.txt failed: [$err_name] ($err_code)");
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      if ($res) {
+        die("SETSTAT $path succeeded unexpectedly");
       }
 
-      # To issue a CLOSE, we need to destroy the filehandle
-      $fh = undef;
-
+      my ($err_code, $err_name) = $sftp->error();
       $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
-      }
-
-      my $new_size = (stat($test_file))[7];
-      my $expected_size = $test_size + 4;
-
-      $self->assert($expected_size == $new_size,
-        test_msg("Expected $expected_size, got $new_size"));
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -16775,7 +17721,7 @@ sub sftp_open_append_bug3450 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -16785,70 +17731,57 @@ sub sftp_open_append_bug3450 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_open_rdonly {
+sub sftp_setstat_abs_symlink_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
-    AllowStoreRestart => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -16857,31 +17790,15 @@ sub sftp_open_rdonly {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Set write-only permissions
-  unless (chmod(0222, $test_file)) {
-    die("Can't set perms on $test_file to 0222: $!");
-  }
+  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
@@ -16909,7 +17826,7 @@ sub sftp_open_rdonly {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -16920,23 +17837,23 @@ sub sftp_open_rdonly {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      if ($fh) {
-        die("OPEN test.txt succeeded unexpectedly");
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      if ($res) {
+        die("SETSTAT $path succeeded unexpectedly");
       }
 
       my ($err_code, $err_name) = $sftp->error();
-
-      my $expected;
-
-      $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
       $sftp = undef;
       $ssh2->disconnect();
-    };
 
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -16945,7 +17862,7 @@ sub sftp_open_rdonly {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -16955,70 +17872,66 @@ sub sftp_open_rdonly {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_open_wronly {
+sub sftp_setstat_rel_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
-    AllowStoreRestart => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -17027,31 +17940,15 @@ sub sftp_open_wronly {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Set read-only permissions
-  unless (chmod(0444, $test_file)) {
-    die("Can't set perms on $test_file to 0444: $!");
-  }
+  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
@@ -17079,7 +17976,7 @@ sub sftp_open_wronly {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -17090,23 +17987,34 @@ sub sftp_open_wronly {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY);
-      if ($fh) {
-        die("OPEN test.txt succeeded unexpectedly");
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't setstat $path: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected;
-
-      $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      my $attrs = $sftp->stat($path);
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("STAT $path failed: [$err_name] ($err_code)");
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
-    };
 
+      my $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected file atime '$expected', got '$file_atime'"));
+
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -17115,7 +18023,7 @@ sub sftp_open_wronly {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -17125,70 +18033,68 @@ sub sftp_open_wronly {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_open_rdwr {
+sub sftp_setstat_rel_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
-    AllowStoreRestart => 'on',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -17197,31 +18103,15 @@ sub sftp_open_rdwr {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Set execute-only permissions
-  unless (chmod(0111, $test_file)) {
-    die("Can't set perms on $test_file to 0111: $!");
-  }
+  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
@@ -17249,7 +18139,7 @@ sub sftp_open_rdwr {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -17260,23 +18150,34 @@ sub sftp_open_rdwr {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDWR);
-      if ($fh) {
-        die("OPEN test.txt succeeded unexpectedly");
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't setstat $path: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected;
-
-      $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      my $attrs = $sftp->stat($path);
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("STAT $path failed: [$err_name] ($err_code)");
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
-    };
 
+      my $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected file atime '$expected', got '$file_atime'"));
+
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -17285,7 +18186,7 @@ sub sftp_open_rdwr {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -17295,68 +18196,60 @@ sub sftp_open_rdwr {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_upload {
+sub sftp_setstat_rel_symlink_enoent {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -17365,14 +18258,15 @@ sub sftp_upload {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -17386,9 +18280,6 @@ sub sftp_upload {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -17403,7 +18294,7 @@ sub sftp_upload {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -17414,24 +18305,23 @@ sub sftp_upload {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      if ($res) {
+        die("SETSTAT $path succeeded unexpectedly");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
+      my ($err_code, $err_name) = $sftp->error();
       $sftp = undef;
       $ssh2->disconnect();
-    };
 
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -17440,7 +18330,7 @@ sub sftp_upload {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -17450,68 +18340,62 @@ sub sftp_upload {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_upload_with_compression {
+sub sftp_setstat_rel_symlink_enoent_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -17520,16 +18404,15 @@ sub sftp_upload_with_compression {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPCompression on",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -17543,9 +18426,6 @@ sub sftp_upload_with_compression {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -17555,16 +18435,12 @@ sub sftp_upload_with_compression {
 
       sleep(1);
 
-      my $comp = 'zlib';
-      $ssh2->method('comp_cs', $comp);
-      $ssh2->method('comp_sc', $comp);
-
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -17575,26 +18451,23 @@ sub sftp_upload_with_compression {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
+      my $path = 'test.d/test.lnk';
+      my $res = $sftp->setstat($path,
+        atime => 0,
+        mtime => 0,
+      );
+      if ($res) {
+        die("SETSTAT $path succeeded unexpectedly");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
+      my ($err_code, $err_name) = $sftp->error();
       $sftp = undef;
-
       $ssh2->disconnect();
-    };
 
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -17603,7 +18476,7 @@ sub sftp_upload_with_compression {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -17613,21 +18486,13 @@ sub sftp_upload_with_compression {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_upload_zero_len_file {
+sub sftp_fsetstat {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -17666,8 +18531,6 @@ sub sftp_upload_zero_len_file {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -17706,9 +18569,6 @@ sub sftp_upload_zero_len_file {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -17733,25 +18593,44 @@ sub sftp_upload_zero_len_file {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+    
+      my $fh = $sftp->open('sftp.conf', O_RDONLY); 
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't open sftp.conf: [$err_name] ($err_code)");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      my $res = $fh->setstat(
+        atime => 0,
+        mtime => 0,
+      ); 
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't fsetstat sftp.conf: [$err_name] ($err_code)");
+      }
+
+      # Explicitly destroy the handle to issue the FXP_CLOSE
       $fh = undef;
 
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+      my $attrs = $sftp->stat('sftp.conf');
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
+      }
 
+      $sftp = undef;
       $ssh2->disconnect();
 
-      my $size = -s $test_file;
-      unless ($size == 0) {
-        die("$test_file has $size len unexpectedly");
-      }
+      my $expected;
+
+      $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected '$expected', got '$file_atime'"));
+
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected '$expected', got '$file_mtime'"));
     };
 
     if ($@) {
@@ -17786,7 +18665,7 @@ sub sftp_upload_zero_len_file {
   unlink($log_file);
 }
 
-sub sftp_upload_largefile {
+sub sftp_realpath {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -17825,39 +18704,6 @@ sub sftp_upload_largefile {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $fh;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open($fh, "> $test_file")) {
-    # Make a file that's larger than the maximum SSH2 packet size, forcing
-    # the scp code to loop properly entire the entire large file is sent.
-
-    print $fh "ABCDefgh" x 16384;
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Calculate the MD5 checksum of this file, for comparison with the
-  # downloaded file.
-  my $ctx = Digest::MD5->new();
-  my $expected_md5;
-
-  if (open($fh, "< $test_file")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $expected_md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $test_file: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -17896,18 +18742,10 @@ sub sftp_upload_largefile {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
-    my $test_rfh;
-    unless (open($test_rfh, "< $test_file")) {
-      die("Can't read $test_file: $!");
-    }
-
     eval {
       my $ssh2 = Net::SSH2->new();
 
@@ -17929,33 +18767,25 @@ sub sftp_upload_largefile {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $test_wfh = $sftp->open('test2.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($test_wfh) {
+      my $cwd = $sftp->realpath('.');
+      unless ($cwd) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test2.txt: [$err_name] ($err_code)");
+        die("Can't get real path for '.': [$err_name] ($err_code)");
       }
 
+      my $expected;
 
-      my $buf;
-      my $bufsz = 8192;
-
-      while (read($test_rfh, $buf, $bufsz)) {
-        print $test_wfh $buf;
+      $expected = $home_dir;
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack to deal with how it handles tmp files
+        $expected = ('/private' . $expected);
       }
 
-      close($test_rfh);
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $test_wfh = undef;
+      $self->assert($expected eq $cwd,
+        test_msg("Expected '$expected', got '$cwd'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
-
-      unless (-f $test_file2) {
-        die("$test_file2 file does not exist as expected");
-      }
     };
 
     if ($@) {
@@ -17987,28 +18817,10 @@ sub sftp_upload_largefile {
     die($ex);
   }
 
-  # Calculate the MD5 checksum of the uploaded file, for comparison with the
-  # file that was uploaded.
-  $ctx->reset();
-  my $md5;
-
-  if (open($fh, "< $test_file2")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $test_file2: $!");
-  }
-
-  $self->assert($expected_md5 eq $md5,
-    test_msg("Expected '$expected_md5', got '$md5'"));
-
   unlink($log_file);
 }
 
-sub sftp_upload_device_full {
+sub sftp_realpath_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -18047,6 +18859,17 @@ sub sftp_upload_device_full {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -18056,7 +18879,6 @@ sub sftp_upload_device_full {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -18068,7 +18890,6 @@ sub sftp_upload_device_full {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
@@ -18087,9 +18908,6 @@ sub sftp_upload_device_full {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -18115,34 +18933,24 @@ sub sftp_upload_device_full {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # XXX The /dev/full device only exists on Linux, as far as I know
-      my $fh = $sftp->open('/dev/full', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+      my $real_path = $sftp->realpath('test.txt');
+      unless ($real_path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open /dev/full: [$err_name] ($err_code)");
+        die("Can't get real path for 'test.txt': [$err_name] ($err_code)");
       }
 
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        unless (print $fh "ABCD" x 8192) {
-          die("Failed to write to /dev/full: $!");
-        }
+      my $expected;
+
+      $expected = $test_file;
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack to deal with how it handles tmp files
+        $expected = ('/private' . $expected);
       }
 
-      # XXX Hrm.  Looks like the Net::SSH2::File interface does not actually
-      # inform the user if there was a CLOSE error, or if there was an
-      # error writing (e.g. if the disk was full).  Will have to file
-      # a bug report with them about this.
-      #
-      # This means that, for now, you have to double-check the generated
-      # logs to make sure that mod_sftp is sending the correct error/response.
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      $self->assert($expected eq $real_path,
+        test_msg("Expected real path '$expected', got '$real_path'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
     };
 
@@ -18178,7 +18986,7 @@ sub sftp_upload_device_full {
   unlink($log_file);
 }
 
-sub sftp_upload_fifo_bug3312 {
+sub sftp_realpath_symlink_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -18217,9 +19025,28 @@ sub sftp_upload_fifo_bug3312 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $fifo = File::Spec->rel2abs("$tmpdir/test.fifo");
-  unless (POSIX::mkfifo($fifo, 0666)) {
-    die("Can't create fifo $fifo: $!");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('test.txt', 'test.lnk')) {
+    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
 
   my $config = {
@@ -18231,7 +19058,6 @@ sub sftp_upload_fifo_bug3312 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -18261,9 +19087,6 @@ sub sftp_upload_fifo_bug3312 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -18289,16 +19112,22 @@ sub sftp_upload_fifo_bug3312 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.fifo', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      if ($fh) {
-        die("OPEN test.fifo succeeded unexpectedly");
+      my $real_path = $sftp->realpath('test.lnk');
+      unless ($real_path) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
+      my $expected;
 
-      my $expected = 'SSH_FX_NO_SUCH_FILE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      $expected = $test_file;
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack to deal with how it handles tmp files
+        $expected = ('/private' . $expected);
+      }
+
+      $self->assert($expected eq $real_path,
+        test_msg("Expected real path '$expected', got '$real_path'"));
 
       $sftp = undef;
       $ssh2->disconnect();
@@ -18336,7 +19165,7 @@ sub sftp_upload_fifo_bug3312 {
   unlink($log_file);
 }
 
-sub sftp_upload_fifo_bug3313 {
+sub sftp_realpath_symlink_file_chrooted {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -18375,8 +19204,32 @@ sub sftp_upload_fifo_bug3313 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  # XXX This assume that a FIFO reader is running, and opened at this path
-  my $fifo = File::Spec->rel2abs("/tmp/test.fifo");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # This is what the client should see, since it is chrooted
+  my $chrooted_file = "/test.txt";
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('./test.txt', 'test.lnk')) {
+    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
@@ -18387,7 +19240,7 @@ sub sftp_upload_fifo_bug3313 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    AllowOverwrite => 'on',
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -18417,9 +19270,6 @@ sub sftp_upload_fifo_bug3313 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -18445,27 +19295,18 @@ sub sftp_upload_fifo_bug3313 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('/tmp/test.fifo', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+      my $real_path = $sftp->realpath('test.lnk');
+      unless ($real_path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open /tmp/test.fifo: [$err_name] ($err_code)");
+        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
       }
 
-      # Attempt to truncate the FIFO as well
-      my $res = $fh->setstat(
-        size => 24,
-      );
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't truncate /tmp/test.fifo: [$err_name] ($err_code)");
-      }
+      my $expected;
 
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
-      }
+      $expected = $chrooted_file;
+      $self->assert($expected eq $real_path,
+        test_msg("Expected real path '$expected', got '$real_path'"));
 
-      $fh = undef;
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -18502,7 +19343,7 @@ sub sftp_upload_fifo_bug3313 {
   unlink($log_file);
 }
 
-sub sftp_ext_upload_bug3550 {
+sub sftp_realpath_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -18541,47 +19382,8 @@ sub sftp_ext_upload_bug3550 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
-
-  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
-  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
-    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
-  }
-
-  my $src_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
-
-  # Calculate the MD5 checksum of this file, for comparison with the uploaded
-  # file.
-  my $ctx = Digest::MD5->new();
-  my $expected_md5;
-
-  if (open(my $fh, "< $src_file")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $expected_md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $src_file: $!");
-  }
-
-  my $expected_sz = (stat($src_file))[7];
- 
-  my $dst_file = File::Spec->rel2abs("$tmpdir/test.dat");
-
-  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
-  if (open(my $fh, "> $batch_file")) {
-    print $fh "put -P $src_file $dst_file\n";
-
-    unless (close($fh)) {
-      die("Can't write $batch_file: $!");
-    }
-
-  } else {
-    die("Can't open $batch_file: $!");
-  }
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
   my $config = {
     PidFile => $pid_file,
@@ -18603,14 +19405,6 @@ sub sftp_ext_upload_bug3550 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
-
-        "SFTPCiphers aes256-ctr aes192-ctr aes128-ctr",
-        "SFTPDigests hmac-sha1 hmac-ripemd160",
-
-        # Bug#3551 requires that mod_sftp support compression, and that
-        # the client use compression for the uploads.
-        "SFTPCompression delayed",
       ],
     },
   };
@@ -18629,99 +19423,50 @@ sub sftp_ext_upload_bug3550 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my @cmd = (
-        'sftp',
-        '-oBatchMode=yes',
-        '-oCheckHostIP=no',
-        '-oCompression=yes',
-        "-oPort=$port",
-        "-oIdentityFile=$rsa_priv_key",
-        '-oPubkeyAuthentication=yes',
-        '-oStrictHostKeyChecking=no',
-        '-vvv',
-        '-b',
-        "$batch_file",
-        "$user\@127.0.0.1",
-      );
-
-      my $sftp_rh = IO::Handle->new();
-      my $sftp_wh = IO::Handle->new();
-      my $sftp_eh = IO::Handle->new();
-
-      $sftp_wh->autoflush(1);
+      my $ssh2 = Net::SSH2->new();
 
       sleep(1);
 
-      local $SIG{CHLD} = 'DEFAULT';
-
-      # Make sure that the perms on the priv key are what OpenSSH wants
-      unless (chmod(0400, $rsa_priv_key)) {
-        die("Can't set perms on $rsa_priv_key to 0400: $!");
-      }
-
-      if ($ENV{TEST_VERBOSE}) {
-        print STDERR "Executing: ", join(' ', @cmd), "\n";
-      }
-
-      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
-      waitpid($sftp_pid, 0);
-      my $exit_status = $?;
-
-      # Restore the perms on the priv key
-      unless (chmod(0644, $rsa_priv_key)) {
-        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($res, $errstr);
-      if ($exit_status >> 8 == 0) {
-        $errstr = join('', <$sftp_eh>);
-        $res = 0;
-
-      } else {
-        if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
-          print STDERR "Stderr: $errstr\n";
-        }
-
-        $res = 1;
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($res == 0) {
-        die("Can't upload $src_file to server: $errstr");
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless (-f $dst_file) {
-        die("File '$dst_file' does not exist as expected");
+      my $real_path = $sftp->realpath('test.d');
+      unless ($real_path) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't get real path for 'test.d': [$err_name] ($err_code)");
       }
 
-      $ctx->reset();
-      my $md5;
-
-      if (open(my $fh, "< $dst_file")) {
-        binmode($fh);
-        $ctx->addfile($fh);
-        $md5 = $ctx->hexdigest();
-        close($fh);
+      my $expected;
 
-      } else {
-        die("Can't read $dst_file: $!");
+      $expected = $test_dir;
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack to deal with how it handles tmp files
+        $expected = ('/private' . $expected);
       }
 
-      my $sz = (stat($dst_file))[7];
-
-      $self->assert($expected_sz == $sz,
-        test_msg("Expected $expected_sz, got $sz"));
+      $self->assert($expected eq $real_path,
+        test_msg("Expected real path '$expected', got '$real_path'"));
 
-      $self->assert($expected_md5 eq $md5,
-        test_msg("Expected '$expected_md5', got '$md5'"));
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -18756,7 +19501,7 @@ sub sftp_ext_upload_bug3550 {
   unlink($log_file);
 }
 
-sub sftp_download {
+sub sftp_realpath_symlink_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -18776,23 +19521,6 @@ sub sftp_download {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    my $count = 20;
-    for (my $i = 0; $i < $count; $i++) {
-      print $fh "ABCD" x 8192;
-    }
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_sz = (stat($test_file))[7];
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -18812,6 +19540,22 @@ sub sftp_download {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('test.d', 'test.lnk')) {
+    die("Can't symlink 'test.d' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -18850,9 +19594,6 @@ sub sftp_download {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -18878,31 +19619,24 @@ sub sftp_download {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
+      my $real_path = $sftp->realpath('test.lnk');
+      unless ($real_path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
       }
 
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
+      my $expected;
 
-        $res = $fh->read($buf, 8192);
+      $expected = $test_dir;
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack to deal with how it handles tmp files
+        $expected = ('/private' . $expected);
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      $self->assert($expected eq $real_path,
+        test_msg("Expected real path '$expected', got '$real_path'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
-      $self->assert($test_sz == $size,
-        test_msg("Expected $test_sz, got $size"));
-
       $ssh2->disconnect();
     };
 
@@ -18938,7 +19672,7 @@ sub sftp_download {
   unlink($log_file);
 }
 
-sub sftp_download_with_compression {
+sub sftp_realpath_symlink_dir_chrooted {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -18958,23 +19692,6 @@ sub sftp_download_with_compression {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    my $count = 20;
-    for (my $i = 0; $i < $count; $i++) {
-      print $fh "ABCD" x 8192;
-    }
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_sz = (stat($test_file))[7];
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -18994,6 +19711,25 @@ sub sftp_download_with_compression {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  # This is the path the client should see, since it is chrooted
+  my $chrooted_dir = "/test.d";
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('test.d', 'test.lnk')) {
+    die("Can't symlink 'test.d' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -19003,6 +19739,7 @@ sub sftp_download_with_compression {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -19014,8 +19751,6 @@ sub sftp_download_with_compression {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPCompression on",
       ],
     },
   };
@@ -19034,9 +19769,6 @@ sub sftp_download_with_compression {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -19046,10 +19778,6 @@ sub sftp_download_with_compression {
 
       sleep(1);
 
-      my $comp = 'zlib';
-      $ssh2->method('comp_cs', $comp);
-      $ssh2->method('comp_sc', $comp);
-
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
@@ -19066,40 +19794,19 @@ sub sftp_download_with_compression {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      if ($res < 0) {
+      my $real_path = $sftp->realpath('test.lnk');
+      unless ($real_path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't read test.txt: [$err_name] ($err_code)");
+        die("Can't get real path for 'test.lnk': [$err_name] ($err_code)");
       }
 
-      while ($res) {
-        $size += $res;
-
-        $res = $fh->read($buf, 8192);
-        if ($res < 0) {
-          my ($err_code, $err_name) = $sftp->error();
-          die("Can't read test.txt: [$err_name] ($err_code)");
-        }
-      }
+      my $expected;
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      $expected = $chrooted_dir;
+      $self->assert($expected eq $real_path,
+        test_msg("Expected real path '$expected', got '$real_path'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
-      $self->assert($test_sz == $size,
-        test_msg("Expected $test_sz, got $size"));
-
       $ssh2->disconnect();
     };
 
@@ -19135,7 +19842,7 @@ sub sftp_download_with_compression {
   unlink($log_file);
 }
 
-sub sftp_download_zero_len_file {
+sub sftp_open_enoent_bug3345 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -19156,16 +19863,6 @@ sub sftp_download_zero_len_file {
   my $gid = 500;
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_sz = (stat($test_file))[7];
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -19253,30 +19950,23 @@ sub sftp_download_zero_len_file {
       }
 
       my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+      if ($fh) {
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
-      my $buf;
-      my $size = 0;
+      my ($err_code, $err_name) = $sftp->error();
 
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
+      my $expected;
 
-        $res = $fh->read($buf, 8192);
-      }
+      $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      $expected = 2;
+      $self->assert($expected == $err_code,
+        test_msg("Expected $expected, got $err_code"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
-      $self->assert($test_sz == $size,
-        test_msg("Expected $test_sz, got $size"));
-
       $ssh2->disconnect();
     };
 
@@ -19312,7 +20002,7 @@ sub sftp_download_zero_len_file {
   unlink($log_file);
 }
 
-sub sftp_download_largefile {
+sub sftp_open_trunc_bug3449 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -19351,39 +20041,6 @@ sub sftp_download_largefile {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $fh;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open($fh, "> $test_file")) {
-    # Make a file that's larger than the maximum SSH2 packet size, forcing
-    # the scp code to loop properly entire the entire large file is sent.
-
-    print $fh "ABCDefgh" x 16384;
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Calculate the MD5 checksum of this file, for comparison with the
-  # downloaded file.
-  my $ctx = Digest::MD5->new();
-  my $expected_md5;
-
-  if (open($fh, "< $test_file")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $expected_md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $test_file: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -19393,6 +20050,7 @@ sub sftp_download_largefile {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -19410,6 +20068,19 @@ sub sftp_download_largefile {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 1024;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_size = (stat($test_file))[7];
+
   # 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.
@@ -19422,20 +20093,10 @@ sub sftp_download_largefile {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
-    my $test_wfh;
-    unless (open($test_wfh, "> $test_file2")) {
-      die("Can't open $test_file2: $!");
-    }
-
-    binmode($test_wfh);
-
     eval {
       my $ssh2 = Net::SSH2->new();
 
@@ -19457,32 +20118,63 @@ sub sftp_download_largefile {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $test_rfh = $sftp->open('test.txt', O_RDONLY);
-      unless ($test_rfh) {
+      my $attrs = $sftp->stat('test.txt', 0);
+      unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("LSTAT test.txt failed: [$err_name] ($err_code)");
       }
 
-      my $buf;
-      my $bufsz = 8192;
+      my $expected;
 
-      my $res = $test_rfh->read($buf, $bufsz);
-      while ($res) {
-        print $test_wfh $buf;
+      $expected = $test_size;
+      my $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected '$expected', got '$file_size'"));
 
-        $res = $test_rfh->read($buf, $bufsz);
+      $expected = $<;
+      my $file_uid = $attrs->{uid};
+      $self->assert($expected == $file_uid,
+        test_msg("Expected '$expected', got '$file_uid'"));
+
+      $expected = $(;
+      my $file_gid = $attrs->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected '$expected', got '$file_gid'"));
+
+      my $orig_size = $attrs->{size};
+
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_TRUNC);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("OPEN test.txt failed: [$err_name] ($err_code)");
       }
 
-      unless (close($test_wfh)) {
-        die("Can't write $test_file2: $!");
+      $attrs = $sftp->stat('test.txt', 0);
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("LSTAT test.txt failed: [$err_name] ($err_code)");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $test_rfh = undef;
+      $expected = 0;
+      $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected '$expected', got '$file_size'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+      # To issue a CLOSE, we need to destroy the filehandle
+      $fh = undef;
 
+      $attrs = $sftp->stat('test.txt', 0);
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("LSTAT test.txt failed: [$err_name] ($err_code)");
+      }
+
+      $expected = 0;
+      $file_size = $attrs->{size};
+      $self->assert($expected == $file_size,
+        test_msg("Expected '$expected', got '$file_size'"));
+
+      $sftp = undef;
       $ssh2->disconnect();
     };
 
@@ -19515,28 +20207,10 @@ sub sftp_download_largefile {
     die($ex);
   }
 
-  # Calculate the MD5 checksum of the downloaded file, for comparison with the
-  # downloaded file.
-  $ctx->reset();
-  my $md5;
-
-  if (open($fh, "< $test_file2")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $test_file2: $!");
-  }
-
-  $self->assert($expected_md5 eq $md5,
-    test_msg("Expected '$expected_md5', got '$md5'"));
-
   unlink($log_file);
 }
 
-sub sftp_download_fifo_bug3314 {
+sub sftp_open_creat {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -19556,8 +20230,6 @@ sub sftp_download_fifo_bug3314 {
   my $uid = 500;
   my $gid = 500;
 
-  my $fifo = File::Spec->rel2abs("/tmp/test.fifo");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -19586,6 +20258,7 @@ sub sftp_download_fifo_bug3314 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -19603,6 +20276,8 @@ sub sftp_download_fifo_bug3314 {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
   # 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.
@@ -19615,9 +20290,6 @@ sub sftp_download_fifo_bug3314 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -19643,29 +20315,21 @@ sub sftp_download_fifo_bug3314 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open($fifo, O_RDONLY);
+      my $fh = $sftp->open('test.txt', O_CREAT, 0640);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open $fifo: [$err_name] ($err_code)");
-      }
-
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
-
-        $res = $fh->read($buf, 8192);
+        die("OPEN test.txt failed: [$err_name] ($err_code)");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      # To issue a CLOSE, we need to destroy the filehandle
       $fh = undef;
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
     };
 
     if ($@) {
@@ -19700,7 +20364,7 @@ sub sftp_download_fifo_bug3314 {
   unlink($log_file);
 }
 
-sub sftp_ext_download_bug3550 {
+sub sftp_open_creat_excl {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -19739,53 +20403,6 @@ sub sftp_ext_download_bug3550 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
-
-  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
-  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
-    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
-  }
-
-  my $orig_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
-
-  # Calculate the MD5 checksum of this file, for comparison with the downloaded
-  # file.
-  my $ctx = Digest::MD5->new();
-  my $expected_md5;
-
-  if (open(my $fh, "< $orig_file")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $expected_md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $orig_file: $!");
-  }
-
-  my $expected_sz = (stat($orig_file))[7];
- 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.dat");
-  unless (copy($orig_file, $src_file)) {
-    die("Can't copy $orig_file to $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/test2.dat");
-
-  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
-  if (open(my $fh, "> $batch_file")) {
-    print $fh "get -P $src_file $dst_file\n";
-
-    unless (close($fh)) {
-      die("Can't write $batch_file: $!");
-    }
-
-  } else {
-    die("Can't open $batch_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -19795,6 +20412,7 @@ sub sftp_ext_download_bug3550 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -19806,20 +20424,23 @@ sub sftp_ext_download_bug3550 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
-
-        "SFTPCiphers aes256-ctr aes192-ctr aes128-ctr",
-        "SFTPDigests hmac-sha1 hmac-ripemd160",
-
-        # Bug#3551 requires that mod_sftp support compression, and that
-        # the client use compression for the uploads.
-        "SFTPCompression delayed",
       ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # 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.
@@ -19832,99 +20453,63 @@ sub sftp_ext_download_bug3550 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my @cmd = (
-        'sftp',
-        '-oBatchMode=yes',
-        '-oCheckHostIP=no',
-        '-oCompression=yes',
-        "-oPort=$port",
-        "-oIdentityFile=$rsa_priv_key",
-        '-oPubkeyAuthentication=yes',
-        '-oStrictHostKeyChecking=no',
-        '-vvv',
-        '-b',
-        "$batch_file",
-        "$user\@127.0.0.1",
-      );
-
-      my $sftp_rh = IO::Handle->new();
-      my $sftp_wh = IO::Handle->new();
-      my $sftp_eh = IO::Handle->new();
-
-      $sftp_wh->autoflush(1);
+      my $ssh2 = Net::SSH2->new();
 
       sleep(1);
 
-      local $SIG{CHLD} = 'DEFAULT';
-
-      # Make sure that the perms on the priv key are what OpenSSH wants
-      unless (chmod(0400, $rsa_priv_key)) {
-        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ENV{TEST_VERBOSE}) {
-        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
-      waitpid($sftp_pid, 0);
-      my $exit_status = $?;
-
-      # Restore the perms on the priv key
-      unless (chmod(0644, $rsa_priv_key)) {
-        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($res, $errstr);
-      if ($exit_status >> 8 == 0) {
-        $errstr = join('', <$sftp_eh>);
-        $res = 0;
-
-      } else {
-        if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
-          print STDERR "Stderr: $errstr\n";
-        }
-
-        $res = 1;
+      # This should fail, since the file already exists
+      my $fh = $sftp->open('test.txt', O_CREAT|O_EXCL, 0640);
+      if ($fh) {
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
-      unless ($res == 0) {
-        die("Can't download $src_file from server: $errstr");
-      }
+      my ($err_code, $err_name) = $sftp->error();
 
-      unless (-f $dst_file) {
-        die("File '$dst_file' does not exist as expected");
-      }
+      my $expected;
 
-      $ctx->reset();
-      my $md5;
+      $expected = 'SSH_FX_FAILURE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      if (open(my $fh, "< $dst_file")) {
-        binmode($fh);
-        $ctx->addfile($fh);
-        $md5 = $ctx->hexdigest();
-        close($fh);
+      # Now remove that file, and try again.
+      unlink($test_file);
 
-      } else {
-        die("Can't read $dst_file: $!");
+      $fh = $sftp->open('test.txt', O_CREAT|O_EXCL, 0640);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("OPEN test.txt failed: [$err_name] ($err_code)");
       }
 
-      my $sz = (stat($dst_file))[7];
+      # To issue a CLOSE, we need to destroy the filehandle
+      $fh = undef;
 
-      $self->assert($expected_sz == $sz,
-        test_msg("Expected $expected_sz, got $sz"));
+      $sftp = undef;
+      $ssh2->disconnect();
 
-      $self->assert($expected_md5 eq $md5,
-        test_msg("Expected '$expected_md5', got '$md5'"));
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
     };
 
     if ($@) {
@@ -19959,7 +20544,7 @@ sub sftp_ext_download_bug3550 {
   unlink($log_file);
 }
 
-sub sftp_ext_download_rekey {
+sub sftp_open_append_bug3450 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -19998,55 +20583,6 @@ sub sftp_ext_download_rekey {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
-
-  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
-  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
-    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
-  }
-
-  my $orig_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
-
-  # Calculate the MD5 checksum of this file, for comparison with the downloaded
-  # file.
-  my $ctx = Digest::MD5->new();
-  my $expected_md5;
-
-  if (open(my $fh, "< $orig_file")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $expected_md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $orig_file: $!");
-  }
-
-  my $expected_sz = (stat($orig_file))[7];
- 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.dat");
-  unless (copy($orig_file, $src_file)) {
-    die("Can't copy $orig_file to $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/test2.dat");
-
-  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
-  if (open(my $fh, "> $batch_file")) {
-    print $fh "get -P $src_file $dst_file\n";
-
-    unless (close($fh)) {
-      die("Can't write $batch_file: $!");
-    }
-
-  } else {
-    die("Can't open $batch_file: $!");
-  }
-
-  my $timeout_idle = 10;
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -20056,8 +20592,8 @@ sub sftp_ext_download_rekey {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    TimeoutIdle => $timeout_idle,
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -20069,13 +20605,25 @@ sub sftp_ext_download_rekey {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
       ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_size = (stat($test_file))[7];
+
   # 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.
@@ -20088,99 +20636,57 @@ sub sftp_ext_download_rekey {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my @cmd = (
-        'sftp',
-        '-oBatchMode=yes',
-        '-oCheckHostIP=no',
-        '-oRekeyLimit=256',
-        "-oPort=$port",
-        "-oIdentityFile=$rsa_priv_key",
-        '-oPubkeyAuthentication=yes',
-        '-oStrictHostKeyChecking=no',
-        '-vvv',
-        '-b',
-        "$batch_file",
-        "$user\@127.0.0.1",
-      );
-
-      my $sftp_rh = IO::Handle->new();
-      my $sftp_wh = IO::Handle->new();
-      my $sftp_eh = IO::Handle->new();
-
-      $sftp_wh->autoflush(1);
+      my $ssh2 = Net::SSH2->new();
 
       sleep(1);
 
-      local $SIG{CHLD} = 'DEFAULT';
-
-      # Make sure that the perms on the priv key are what OpenSSH wants
-      unless (chmod(0400, $rsa_priv_key)) {
-        die("Can't set perms on $rsa_priv_key to 0400: $!");
-      }
-
-      if ($ENV{TEST_VERBOSE}) {
-        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
-      waitpid($sftp_pid, 0);
-      my $exit_status = $?;
-
-      # Restore the perms on the priv key
-      unless (chmod(0644, $rsa_priv_key)) {
-        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($res, $errstr);
-      if ($exit_status >> 8 == 0) {
-        $errstr = join('', <$sftp_eh>);
-        $res = 0;
-
-      } else {
-        if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$sftp_eh>);
-          print STDERR "Stderr: $errstr\n";
-        }
-
-        $res = 1;
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($res == 0) {
-        die("Can't download $src_file from server: $errstr");
+      my $fh = $sftp->open('test.txt', O_APPEND);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("OPEN test.txt failed: [$err_name] ($err_code)");
       }
 
-      unless (-f $dst_file) {
-        die("File '$dst_file' does not exist as expected");
+      unless ($fh->write("ABCD")) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("WRITE test.txt failed: [$err_name] ($err_code)");
       }
 
-      $ctx->reset();
-      my $md5;
+      # To issue a CLOSE, we need to destroy the filehandle
+      $fh = undef;
 
-      if (open(my $fh, "< $dst_file")) {
-        binmode($fh);
-        $ctx->addfile($fh);
-        $md5 = $ctx->hexdigest();
-        close($fh);
+      $sftp = undef;
+      $ssh2->disconnect();
 
-      } else {
-        die("Can't read $dst_file: $!");
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
       }
 
-      my $sz = (stat($dst_file))[7];
-
-      $self->assert($expected_sz == $sz,
-        test_msg("Expected $expected_sz, got $sz"));
+      my $new_size = (stat($test_file))[7];
+      my $expected_size = $test_size + 4;
 
-      $self->assert($expected_md5 eq $md5,
-        test_msg("Expected '$expected_md5', got '$md5'"));
+      $self->assert($expected_size == $new_size,
+        test_msg("Expected $expected_size, got $new_size"));
     };
 
     if ($@) {
@@ -20215,7 +20721,7 @@ sub sftp_ext_download_rekey {
   unlink($log_file);
 }
 
-sub sftp_download_readonly_bug3787 {
+sub sftp_open_rdonly {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -20235,18 +20741,6 @@ sub sftp_download_readonly_bug3787 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -20254,13 +20748,11 @@ sub sftp_download_readonly_bug3787 {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $test_file)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
 
-  my $test_mode = (stat($test_file))[2];
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -20277,6 +20769,8 @@ sub sftp_download_readonly_bug3787 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -20294,6 +20788,23 @@ sub sftp_download_readonly_bug3787 {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Set write-only permissions
+  unless (chmod(0222, $test_file)) {
+    die("Can't set perms on $test_file to 0222: $!");
+  }
+
   # 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.
@@ -20306,9 +20817,6 @@ sub sftp_download_readonly_bug3787 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -20334,29 +20842,20 @@ sub sftp_download_readonly_bug3787 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY, 0);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      if ($fh) {
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
-      my $buf;
+      my ($err_code, $err_name) = $sftp->error();
 
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $res = $fh->read($buf, 8192);
-      }
+      my $expected;
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
-      my $mode = (stat($test_file))[2];
-      $self->assert($mode == $test_mode,
-        test_msg("Expected mode $test_mode, got $mode"));
-
       $ssh2->disconnect();
     };
 
@@ -20392,7 +20891,7 @@ sub sftp_download_readonly_bug3787 {
   unlink($log_file);
 }
 
-sub sftp_readdir {
+sub sftp_open_wronly {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -20436,10 +20935,12 @@ sub sftp_readdir {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'auth:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -20457,6 +20958,23 @@ sub sftp_readdir {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Set read-only permissions
+  unless (chmod(0444, $test_file)) {
+    die("Can't set perms on $test_file to 0444: $!");
+  }
+
   # 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.
@@ -20494,68 +21012,8256 @@ sub sftp_readdir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
+      my $fh = $sftp->open('test.txt', O_WRONLY);
+      if ($fh) {
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
-      }
+      my ($err_code, $err_name) = $sftp->error();
 
-      my $expected = {
-        '.' => 1,
-        '..' => 1,
-        'sftp.conf' => 1,
-        'sftp.group' => 1,
-        'sftp.passwd' => 1,
-        'sftp.pid' => 1,
-        'sftp.scoreboard' => 1,
-        'sftp.scoreboard.lck' => 1,
-      };
+      my $expected;
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+      $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
+    };
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_open_rdwr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Set execute-only permissions
+  unless (chmod(0111, $test_file)) {
+    die("Can't set perms on $test_file to 0111: $!");
+  }
+
+  # 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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_RDWR);
+      if ($fh) {
+        die("OPEN test.txt succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+
+      my $expected;
+
+      $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_open_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_data = "Hello, World!\n";
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh $test_data;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      unless ($fh) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use open $path: [$err_name] ($err_code) $err_str");
+      }
+
+      my ($buf, $data);
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+        $data .= $buf;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      # Make sure that we followed the symlink by comparing what we wrote to
+      # what we read.
+      $self->assert($data eq $test_data,
+        test_msg("Expected '$test_data', got '$data'"));
+    };
+    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 sftp_open_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_data = "Hello, World!\n";
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh $test_data;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't use open $path: [$err_name] ($err_code)");
+      }
+
+      my ($buf, $data);
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+        $data .= $buf;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      # Make sure that we followed the symlink by comparing what we wrote to
+      # what we read.
+      $self->assert($data eq $test_data,
+        test_msg("Expected '$test_data', got '$data'"));
+    };
+    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 sftp_open_abs_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      if ($fh) {
+        die("Opening $path suceeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+    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 sftp_open_abs_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      if ($fh) {
+        die("Opening $path suceeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+    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 sftp_open_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_data = "Hello, World!\n";
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh $test_data;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      unless ($fh) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use open $path: [$err_name] ($err_code) $err_str");
+      }
+
+      my ($buf, $data);
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+        $data .= $buf;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      # Make sure that we followed the symlink by comparing what we wrote to
+      # what we read.
+      $self->assert($data eq $test_data,
+        test_msg("Expected '$test_data', got '$data'"));
+    };
+    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 sftp_open_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_data = "Hello, World!\n";
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh $test_data;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      unless ($fh) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use open $path: [$err_name] ($err_code) $err_str");
+      }
+
+      my ($buf, $data);
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+        $data .= $buf;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      # Make sure that we followed the symlink by comparing what we wrote to
+      # what we read.
+      $self->assert($data eq $test_data,
+        test_msg("Expected '$test_data', got '$data'"));
+    };
+    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 sftp_open_rel_symlink_enoent {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      if ($fh) {
+        die("Opening $path succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+    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 sftp_open_rel_symlink_enoent_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $test_file)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    AllowStoreRestart => 'on',
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $fh = $sftp->open($path, O_RDWR);
+      if ($fh) {
+        die("Opening $path succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+    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 sftp_upload {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_upload_with_compression {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        "SFTPCompression on",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      my $comp = 'zlib';
+      $ssh2->method('comp_cs', $comp);
+      $ssh2->method('comp_sc', $comp);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_upload_zero_len_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $size = -s $test_file;
+      unless ($size == 0) {
+        die("$test_file has $size len unexpectedly");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_upload_largefile {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $fh;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open($fh, "> $test_file")) {
+    # Make a file that's larger than the maximum SSH2 packet size, forcing
+    # the scp code to loop properly entire the entire large file is sent.
+
+    print $fh "ABCDefgh" x 16384;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open($fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    my $test_rfh;
+    unless (open($test_rfh, "< $test_file")) {
+      die("Can't read $test_file: $!");
+    }
+
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $test_wfh = $sftp->open('test2.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($test_wfh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test2.txt: [$err_name] ($err_code)");
+      }
+
+
+      my $buf;
+      my $bufsz = 8192;
+
+      while (read($test_rfh, $buf, $bufsz)) {
+        print $test_wfh $buf;
+      }
+
+      close($test_rfh);
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $test_wfh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      unless (-f $test_file2) {
+        die("$test_file2 file does not exist as expected");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  # Calculate the MD5 checksum of the uploaded file, for comparison with the
+  # file that was uploaded.
+  $ctx->reset();
+  my $md5;
+
+  if (open($fh, "< $test_file2")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file2: $!");
+  }
+
+  $self->assert($expected_md5 eq $md5,
+    test_msg("Expected '$expected_md5', got '$md5'"));
+
+  unlink($log_file);
+}
+
+sub sftp_upload_device_full {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPOptions IgnoreSFTPUploadPerms",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      # XXX The /dev/full device only exists on Linux, as far as I know
+      my $fh = $sftp->open('/dev/full', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open /dev/full: [$err_name] ($err_code)");
+      }
+
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        unless (print $fh "ABCD" x 8192) {
+          die("Failed to write to /dev/full: $!");
+        }
+      }
+
+      # XXX Hrm.  Looks like the Net::SSH2::File interface does not actually
+      # inform the user if there was a CLOSE error, or if there was an
+      # error writing (e.g. if the disk was full).  Will have to file
+      # a bug report with them about this.
+      #
+      # This means that, for now, you have to double-check the generated
+      # logs to make sure that mod_sftp is sending the correct error/response.
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_upload_fifo_bug3312 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $fifo = File::Spec->rel2abs("$tmpdir/test.fifo");
+  unless (POSIX::mkfifo($fifo, 0666)) {
+    die("Can't create fifo $fifo: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.fifo', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      if ($fh) {
+        die("OPEN test.fifo succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+
+      my $expected = 'SSH_FX_NO_SUCH_FILE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_upload_fifo_bug3313 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  # XXX This assume that a FIFO reader is running, and opened at this path
+  my $fifo = File::Spec->rel2abs("/tmp/test.fifo");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    AllowOverwrite => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('/tmp/test.fifo', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open /tmp/test.fifo: [$err_name] ($err_code)");
+      }
+
+      # Attempt to truncate the FIFO as well
+      my $res = $fh->setstat(
+        size => 24,
+      );
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't truncate /tmp/test.fifo: [$err_name] ($err_code)");
+      }
+
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
+      }
+
+      $fh = undef;
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_ext_upload_bug3550 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $src_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
+
+  # Calculate the MD5 checksum of this file, for comparison with the uploaded
+  # file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $src_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $src_file: $!");
+  }
+
+  my $expected_sz = (stat($src_file))[7];
+ 
+  my $dst_file = File::Spec->rel2abs("$tmpdir/test.dat");
+
+  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
+  if (open(my $fh, "> $batch_file")) {
+    print $fh "put -P $src_file $dst_file\n";
+
+    unless (close($fh)) {
+      die("Can't write $batch_file: $!");
+    }
+
+  } else {
+    die("Can't open $batch_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+
+        "SFTPCiphers aes256-ctr aes192-ctr aes128-ctr",
+        "SFTPDigests hmac-sha1 hmac-ripemd160",
+
+        # Bug#3551 requires that mod_sftp support compression, and that
+        # the client use compression for the uploads.
+        "SFTPCompression delayed",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my @cmd = (
+        'sftp',
+        '-oBatchMode=yes',
+        '-oCheckHostIP=no',
+        '-oCompression=yes',
+        "-oPort=$port",
+        "-oIdentityFile=$rsa_priv_key",
+        '-oPubkeyAuthentication=yes',
+        '-oStrictHostKeyChecking=no',
+        '-vvv',
+        '-b',
+        "$batch_file",
+        "$user\@127.0.0.1",
+      );
+
+      my $sftp_rh = IO::Handle->new();
+      my $sftp_wh = IO::Handle->new();
+      my $sftp_eh = IO::Handle->new();
+
+      $sftp_wh->autoflush(1);
+
+      sleep(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      # Make sure that the perms on the priv key are what OpenSSH wants
+      unless (chmod(0400, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
+      waitpid($sftp_pid, 0);
+      my $exit_status = $?;
+
+      # Restore the perms on the priv key
+      unless (chmod(0644, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      }
+
+      my ($res, $errstr);
+      if ($exit_status >> 8 == 0) {
+        $errstr = join('', <$sftp_eh>);
+        $res = 0;
+
+      } else {
+        $errstr = join('', <$sftp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res == 0) {
+        die("Can't upload $src_file to server: $errstr");
+      }
+
+      unless (-f $dst_file) {
+        die("File '$dst_file' does not exist as expected");
+      }
+
+      $ctx->reset();
+      my $md5;
+
+      if (open(my $fh, "< $dst_file")) {
+        binmode($fh);
+        $ctx->addfile($fh);
+        $md5 = $ctx->hexdigest();
+        close($fh);
+
+      } else {
+        die("Can't read $dst_file: $!");
+      }
+
+      my $sz = (stat($dst_file))[7];
+
+      $self->assert($expected_sz == $sz,
+        test_msg("Expected $expected_sz, got $sz"));
+
+      $self->assert($expected_md5 eq $md5,
+        test_msg("Expected '$expected_md5', got '$md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_download {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    my $count = 20;
+    for (my $i = 0; $i < $count; $i++) {
+      print $fh "ABCD" x 8192;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_download_with_compression {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    my $count = 20;
+    for (my $i = 0; $i < $count; $i++) {
+      print $fh "ABCD" x 8192;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        "SFTPCompression on",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      my $comp = 'zlib';
+      $ssh2->method('comp_cs', $comp);
+      $ssh2->method('comp_sc', $comp);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      if ($res < 0) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't read test.txt: [$err_name] ($err_code)");
+      }
+
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+        if ($res < 0) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't read test.txt: [$err_name] ($err_code)");
+        }
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_download_with_compression_rekeying {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $test_file")) {
+    my $count = 200;
+    for (my $i = 0; $i < $count; $i++) {
+      print $fh "ABCD" x 8192;
+    }
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        "SFTPCompression delayed",
+
+        # To tickle the bug, use frequent rekeying
+        'SFTPRekey required 2 1',
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      my $comp = 'zlib';
+      $ssh2->method('comp_cs', $comp);
+      $ssh2->method('comp_sc', $comp);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $file = 'test.dat';
+      my $fh = $sftp->open($file, O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $file: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      if ($res < 0) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't read $file: [$err_name] ($err_code)");
+      }
+
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+        if ($res < 0) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't read $file: [$err_name] ($err_code)");
+        }
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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 sftp_download_zero_len_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_download_largefile {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $fh;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open($fh, "> $test_file")) {
+    # Make a file that's larger than the maximum SSH2 packet size, forcing
+    # the scp code to loop properly entire the entire large file is sent.
+
+    print $fh "ABCDefgh" x 16384;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open($fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    my $test_wfh;
+    unless (open($test_wfh, "> $test_file2")) {
+      die("Can't open $test_file2: $!");
+    }
+
+    binmode($test_wfh);
+
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $test_rfh = $sftp->open('test.txt', O_RDONLY);
+      unless ($test_rfh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $bufsz = 8192;
+
+      my $res = $test_rfh->read($buf, $bufsz);
+      while ($res) {
+        print $test_wfh $buf;
+
+        $res = $test_rfh->read($buf, $bufsz);
+      }
+
+      unless (close($test_wfh)) {
+        die("Can't write $test_file2: $!");
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $test_rfh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  # Calculate the MD5 checksum of the downloaded file, for comparison with the
+  # downloaded file.
+  $ctx->reset();
+  my $md5;
+
+  if (open($fh, "< $test_file2")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $test_file2: $!");
+  }
+
+  $self->assert($expected_md5 eq $md5,
+    test_msg("Expected '$expected_md5', got '$md5'"));
+
+  unlink($log_file);
+}
+
+sub sftp_download_fifo_bug3314 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $fifo = File::Spec->rel2abs("/tmp/test.fifo");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open($fifo, O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $fifo: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_ext_download_bug3550 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $orig_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
+
+  # Calculate the MD5 checksum of this file, for comparison with the downloaded
+  # file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $orig_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $orig_file: $!");
+  }
+
+  my $expected_sz = (stat($orig_file))[7];
+ 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  unless (copy($orig_file, $src_file)) {
+    die("Can't copy $orig_file to $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/test2.dat");
+
+  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
+  if (open(my $fh, "> $batch_file")) {
+    print $fh "get -P $src_file $dst_file\n";
+
+    unless (close($fh)) {
+      die("Can't write $batch_file: $!");
+    }
+
+  } else {
+    die("Can't open $batch_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+
+        "SFTPCiphers aes256-ctr aes192-ctr aes128-ctr",
+        "SFTPDigests hmac-sha1 hmac-ripemd160",
+
+        # Bug#3551 requires that mod_sftp support compression, and that
+        # the client use compression for the uploads.
+        "SFTPCompression delayed",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my @cmd = (
+        'sftp',
+        '-oBatchMode=yes',
+        '-oCheckHostIP=no',
+        '-oCompression=yes',
+        "-oPort=$port",
+        "-oIdentityFile=$rsa_priv_key",
+        '-oPubkeyAuthentication=yes',
+        '-oStrictHostKeyChecking=no',
+        '-vvv',
+        '-b',
+        "$batch_file",
+        "$user\@127.0.0.1",
+      );
+
+      my $sftp_rh = IO::Handle->new();
+      my $sftp_wh = IO::Handle->new();
+      my $sftp_eh = IO::Handle->new();
+
+      $sftp_wh->autoflush(1);
+
+      sleep(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      # Make sure that the perms on the priv key are what OpenSSH wants
+      unless (chmod(0400, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
+      waitpid($sftp_pid, 0);
+      my $exit_status = $?;
+
+      # Restore the perms on the priv key
+      unless (chmod(0644, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      }
+
+      my ($res, $errstr);
+      if ($exit_status >> 8 == 0) {
+        $errstr = join('', <$sftp_eh>);
+        $res = 0;
+
+      } else {
+        $errstr = join('', <$sftp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res == 0) {
+        die("Can't download $src_file from server: $errstr");
+      }
+
+      unless (-f $dst_file) {
+        die("File '$dst_file' does not exist as expected");
+      }
+
+      $ctx->reset();
+      my $md5;
+
+      if (open(my $fh, "< $dst_file")) {
+        binmode($fh);
+        $ctx->addfile($fh);
+        $md5 = $ctx->hexdigest();
+        close($fh);
+
+      } else {
+        die("Can't read $dst_file: $!");
+      }
+
+      my $sz = (stat($dst_file))[7];
+
+      $self->assert($expected_sz == $sz,
+        test_msg("Expected $expected_sz, got $sz"));
+
+      $self->assert($expected_md5 eq $md5,
+        test_msg("Expected '$expected_md5', got '$md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_ext_download_server_rekey {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $orig_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
+
+  # Calculate the MD5 checksum of this file, for comparison with the downloaded
+  # file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $orig_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $orig_file: $!");
+  }
+
+  my $expected_sz = (stat($orig_file))[7];
+ 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  unless (copy($orig_file, $src_file)) {
+    die("Can't copy $orig_file to $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/test2.dat");
+
+  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
+  if (open(my $fh, "> $batch_file")) {
+    print $fh "get -P $src_file $dst_file\n";
+
+    unless (close($fh)) {
+      die("Can't write $batch_file: $!");
+    }
+
+  } else {
+    die("Can't open $batch_file: $!");
+  }
+
+  my $timeout_idle = 10;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+        "SFTPRekey required 600 256 10",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my @cmd = (
+        'sftp',
+        '-oBatchMode=yes',
+        '-oCheckHostIP=no',
+#        '-oRekeyLimit=1024',
+        "-oPort=$port",
+        "-oIdentityFile=$rsa_priv_key",
+        '-oPubkeyAuthentication=yes',
+        '-oStrictHostKeyChecking=no',
+        '-vvv',
+        '-b',
+        "$batch_file",
+        "$user\@127.0.0.1",
+      );
+
+      my $sftp_rh = IO::Handle->new();
+      my $sftp_wh = IO::Handle->new();
+      my $sftp_eh = IO::Handle->new();
+
+      $sftp_wh->autoflush(1);
+
+      sleep(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      # Make sure that the perms on the priv key are what OpenSSH wants
+      unless (chmod(0400, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
+      waitpid($sftp_pid, 0);
+      my $exit_status = $?;
+
+      # Restore the perms on the priv key
+      unless (chmod(0644, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      }
+
+      my ($res, $errstr);
+      if ($exit_status >> 8 == 0) {
+        $errstr = join('', <$sftp_eh>);
+        $res = 0;
+
+      } else {
+        $errstr = join('', <$sftp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res == 0) {
+        die("Can't download $src_file from server: $errstr");
+      }
+
+      unless (-f $dst_file) {
+        die("File '$dst_file' does not exist as expected");
+      }
+
+      $ctx->reset();
+      my $md5;
+
+      if (open(my $fh, "< $dst_file")) {
+        binmode($fh);
+        $ctx->addfile($fh);
+        $md5 = $ctx->hexdigest();
+        close($fh);
+
+      } else {
+        die("Can't read $dst_file: $!");
+      }
+
+      my $sz = (stat($dst_file))[7];
+
+      $self->assert($expected_sz == $sz,
+        test_msg("Expected $expected_sz, got $sz"));
+
+      $self->assert($expected_md5 eq $md5,
+        test_msg("Expected '$expected_md5', got '$md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_ext_download_rekey_rsa1024_hostkey_bug4097 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa1024_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  my $orig_file = File::Spec->rel2abs('t/etc/modules/mod_sftp/bug3550.php');
+
+  # Calculate the MD5 checksum of this file, for comparison with the downloaded
+  # file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  if (open(my $fh, "< $orig_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
+
+  } else {
+    die("Can't read $orig_file: $!");
+  }
+
+  my $expected_sz = (stat($orig_file))[7];
+ 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  unless (copy($orig_file, $src_file)) {
+    die("Can't copy $orig_file to $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/test2.dat");
+
+  my $batch_file = File::Spec->rel2abs("$tmpdir/sftp-batch.txt");
+  if (open(my $fh, "> $batch_file")) {
+    print $fh "get -P $src_file $dst_file\n";
+
+    unless (close($fh)) {
+      die("Can't write $batch_file: $!");
+    }
+
+  } else {
+    die("Can't open $batch_file: $!");
+  }
+
+  my $timeout_idle = 10;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 sftp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+        "SFTPRekey required 600 256 10",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    sleep(1);
+
+    eval {
+      my @cmd = (
+        'sftp',
+        '-oBatchMode=yes',
+        '-oCheckHostIP=no',
+#        '-oRekeyLimit=256',
+        "-oPort=$port",
+        "-oIdentityFile=$rsa_priv_key",
+        '-oPubkeyAuthentication=yes',
+        '-oStrictHostKeyChecking=no',
+        '-vvv',
+        '-b',
+        "$batch_file",
+        "$setup->{user}\@127.0.0.1",
+      );
+
+      my $sftp_rh = IO::Handle->new();
+      my $sftp_wh = IO::Handle->new();
+      my $sftp_eh = IO::Handle->new();
+
+      $sftp_wh->autoflush(1);
+
+      sleep(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      # Make sure that the perms on the priv key are what OpenSSH wants
+      unless (chmod(0400, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $sftp_pid = open3($sftp_wh, $sftp_rh, $sftp_eh, @cmd);
+      waitpid($sftp_pid, 0);
+      my $exit_status = $?;
+
+      # Restore the perms on the priv key
+      unless (chmod(0644, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      }
+
+      my ($res, $errstr);
+      if ($exit_status >> 8 == 0) {
+        $errstr = join('', <$sftp_eh>);
+        $res = 0;
+
+      } else {
+        if ($ENV{TEST_VERBOSE}) {
+          $errstr = join('', <$sftp_eh>);
+          print STDERR "Stderr: $errstr\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res == 0) {
+        die("Can't download $src_file from server: $errstr");
+      }
+
+      unless (-f $dst_file) {
+        die("File '$dst_file' does not exist as expected");
+      }
+
+      $ctx->reset();
+      my $md5;
+
+      if (open(my $fh, "< $dst_file")) {
+        binmode($fh);
+        $ctx->addfile($fh);
+        $md5 = $ctx->hexdigest();
+        close($fh);
+
+      } else {
+        die("Can't read $dst_file: $!");
+      }
+
+      my $sz = (stat($dst_file))[7];
+
+      $self->assert($expected_sz == $sz,
+        test_msg("Expected size $expected_sz, got $sz"));
+
+      $self->assert($expected_md5 eq $md5,
+        test_msg("Expected MD5 '$expected_md5', got '$md5'"));
+    };
+
+    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 sftp_download_server_rekey {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  # Calculate the MD5 checksum of this file, for comparison with the downloaded
+  # file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
+
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.dat");
+  if (open(my $fh, "> $src_file")) {
+    my $max = 100;
+    for (my $i = 0; $i < $max; $i++) {
+      my $chunk = 'AbCdEfGh' x 16382;
+      print $fh $chunk;
+      $ctx->add($chunk);
+    }
+
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  $expected_md5 = $ctx->hexdigest();
+  my $expected_sz = (stat($src_file))[7];
+
+  my $timeout_idle = 10;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPRekey required 5 1",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      sleep(1);
+
+      my $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.dat', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.dat: [$err_name] ($err_code)");
+      }
+
+      $ctx->reset();
+      my $buf = '';
+      my $size = 0;
+      my $md5;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+        $ctx->add($buf);
+
+        $buf = '';
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      $self->assert($expected_sz == $size,
+        test_msg("Expected $expected_sz, got $size"));
+
+      $md5 = $ctx->hexdigest();
+      $self->assert($expected_md5 eq $md5,
+        test_msg("Expected '$expected_md5', got '$md5'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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 sftp_download_readonly_bug3787 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_file)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  my $test_mode = (stat($test_file))[2];
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.txt', O_RDONLY, 0);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      my $mode = (stat($test_file))[2];
+      $self->assert($mode == $test_mode,
+        test_msg("Expected mode $test_mode, got $mode"));
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_readdir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'auth:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '.': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+        'sftp.conf' => 1,
+        'sftp.group' => 1,
+        'sftp.passwd' => 1,
+        'sftp.pid' => 1,
+        'sftp.scoreboard' => 1,
+        'sftp.scoreboard.lck' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_readdir_abs_symlink_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $sub_dir = File::Spec->rel2abs("$test_dir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $sub_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $sub_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $sub_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:10 fsio:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $dir = $sftp->opendir($path);
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '$path': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+    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 sftp_readdir_abs_symlink_dir_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $sub_dir = File::Spec->rel2abs("$test_dir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $sub_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $sub_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $sub_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:10 fsio:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $dir = $sftp->opendir($path);
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '$path': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+    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 sftp_readdir_abs_symlink_dir_vroot {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $uid = 500;
+  my $gid = 500;
+
+  # For this test, we want to create a symlink to a directory which lies
+  # outside of the user's home dir; we will be using mod_vroot + DefaultRoot
+  # to "jail" the user into their home directory.
+
+  my $home_dir = File::Spec->rel2abs("$tmpdir/subdir");
+  mkpath($home_dir);
+
+  my $other_dir = File::Spec->rel2abs("$tmpdir/otherdir");
+  mkpath($other_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$home_dir/otherdir.lnk");
+  unless (symlink($other_dir, $test_symlink)) {
+    die("Can't symlink $test_symlink to $other_dir: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'auth:10 fsio:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+
+      'mod_vroot.c' => {
+        DefaultRoot => '~',
+        VRootEngine => 'on',
+        VRootLog => $log_file,
+        VRootOptions => 'allowSymlinks',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $dir = $sftp->opendir('otherdir.lnk');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory 'otherdir.lnk': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_readdir_rel_symlink_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $sub_dir = File::Spec->rel2abs("$test_dir/sub.d");
+  mkpath($sub_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./sub.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './sub.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $sub_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $sub_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:10 fsio:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $dir = $sftp->opendir($path);
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '$path': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+    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 sftp_readdir_rel_symlink_dir_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $sub_dir = File::Spec->rel2abs("$test_dir/sub.d");
+  mkpath($sub_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./sub.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './sub.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir, $sub_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $sub_dir)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'auth:10 fsio:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d/test.lnk';
+      my $dir = $sftp->opendir($path);
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '$path': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+    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 sftp_readdir_wide_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $expected = {
+    '.' => 1,
+    '..' => 1,
+  };
+
+  my $max_nfiles = 250;
+  for (my $i = 0; $i < $max_nfiles; $i++) {
+    my $test_filename = 'SomeReallyLongAndObnoxiousTestFileNameTemplate' . $i;
+
+    # The expected hash is used later for verifying the results of the READDIR
+    $expected->{$test_filename} = 1;
+
+    my $test_path = File::Spec->rel2abs("$test_dir/$test_filename");
+
+    if (open(my $fh, "> $test_path")) {
+      close($fh);
+
+    } else {
+      die("Can't open $test_path: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'auth:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $dir = $sftp->opendir('test.d');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory 'test.d': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
       foreach my $name (keys(%$res)) {
         push(@$seen, $name);
 
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      my $nseen = scalar(@$seen);
+
+      # We add two here for '.' and '..'
+      my $expected_seen = $max_nfiles + 2;
+      $self->assert($expected_seen == $nseen,
+        test_msg("Expected '$expected_seen' files, got $nseen"));
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_readdir_with_removes {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/test1.txt");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file1");
+    }
+
+  } else {
+    die("Can't open $test_file1");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file2");
+    }
+
+  } else {
+    die("Can't open $test_file2");
+  }
+
+  my $test_file3 = File::Spec->rel2abs("$tmpdir/test3.txt");
+  if (open(my $fh, "> $test_file3")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file3");
+    }
+
+  } else {
+    die("Can't open $test_file3");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'ssh2:20 sftp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '.': [$err_name] ($err_code)");
+      }
+
+      $dir->read();
+
+      # Before we close the directory, we do the following, 3 times in a row:
+      #
+      #   OPEN/STAT/READ/CLOSE/REMOVE
+      #
+      # and then do a closedir.
+
+      my $files = [$test_file1, $test_file2, $test_file3];
+      for my $file (@$files) {
+        my $fh = $sftp->open($file, O_RDONLY);
+        unless ($fh) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't open $file: [$err_name] ($err_code)");
+        }
+
+        my $attrs = $sftp->stat($file, 1);
+        unless ($attrs) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't stat $file: [$err_name] ($err_code)");
+        }
+
+        my $buf;
+        my $size = 0;
+
+        my $res = $fh->read($buf, 8192);
+        while ($res) {
+          $size += $res;
+          $res = $fh->read($buf, 8192);
+        }
+
+        # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+        $fh = undef;
+
+        $res = $sftp->unlink($file);
+        unless ($res) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't remove $file: [$err_name] ($err_code)");
+        }
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $ok = 1;
+
+      while (my $line = <$fh>) {
+        if ($line =~ /Invalid handle/) {
+          $ok = 0;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok,
+        test_msg("Unexpectedly saw 'Invalid handle' SFTP response"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_mkdir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $res = $sftp->mkdir('testdir');
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't mkdir testdir: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      unless (-d $test_dir) {
+        die("$test_dir directory does not exist as expected");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_mkdir_eexist {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+  mkpath($test_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $test_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $res = $sftp->mkdir('testdir');
+      if ($res) {
+        die("MKDIR testdir succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      $self->assert($err_name eq 'SSH_FX_FAILURE',
+        test_msg("Expected error name 'SSH_FX_FAILURE', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      unless (-d $test_dir) {
+        die("$test_dir directory does not exist as expected");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_mkdir_abs_symlink_eexist {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($test_dir, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_dir: $!");
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.lnk';
+      my $res = $sftp->mkdir($path);
+      if ($res) {
+        die("MKDIR $path succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      $self->assert($err_name eq 'SSH_FX_FAILURE',
+        test_msg("Expected error name 'SSH_FX_FAILURE', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_mkdir_abs_symlink_eexist_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($test_dir, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_dir: $!");
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.lnk';
+      my $res = $sftp->mkdir($path);
+      if ($res) {
+        die("MKDIR $path succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      $self->assert($err_name eq 'SSH_FX_FAILURE',
+        test_msg("Expected error name 'SSH_FX_FAILURE', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_mkdir_rel_symlink_eexist {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.lnk';
+      my $res = $sftp->mkdir($path);
+      if ($res) {
+        die("MKDIR $path succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      $self->assert($err_name eq 'SSH_FX_FAILURE',
+        test_msg("Expected error name 'SSH_FX_FAILURE', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_mkdir_rel_symlink_eexist_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.lnk';
+      my $res = $sftp->mkdir($path);
+      if ($res) {
+        die("MKDIR $path succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+      $self->assert($err_name eq 'SSH_FX_FAILURE',
+        test_msg("Expected error name 'SSH_FX_FAILURE', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_mkdir_readdir_bug3481 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # See http://forums.proftpd.org/smf/index.php/topic,4759.0.html
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $res = $sftp->mkdir('testdir');
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't mkdir testdir: [$err_name] ($err_code)");
+      }
+
+      # Create 10 files in the sub directory
+      my $count = 10;
+      for (my $i = 0; $i < $count; $i++) {
+        my $test_file = 'testdir/test_' . sprintf("%03s", $i);
+
+        my $fh = $sftp->open($test_file, O_CREAT, 0644); 
+        unless ($fh) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("OPEN $test_file failed: [$err_name] ($err_code)");
+        }
+
+        $fh = undef;
+      }
+
+      # Now read the test directory twice
+      my $dir = $sftp->opendir('testdir');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("OPENDIR testdir failed: [$err_name] ($err_code)");
+      }
+
+      my $files1 = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $files1->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      $dir = undef;
+
+      $dir = $sftp->opendir('testdir');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("OPENDIR testdir failed: [$err_name] ($err_code)");
+      }
+
+      my $files2 = {};
+
+      $file = $dir->read();
+      while ($file) {
+        $files2->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      # Make sure that the same paths were returned in the directory listings
+      foreach my $file (keys(%$files1)) {
+        unless (defined($files2->{$file})) {
+          die("File $file unexpectedly missing from second READDIR");
+        }
+      }
+
+      foreach my $file (keys(%$files2)) {
+        unless (defined($files1->{$file})) {
+          die("File $file unexpectedly missing from first READDIR");
+        }
+      }
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_rmdir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'test.d';
+      my $res = $sftp->rmdir($path);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("RMDIR $path failed: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_rmdir_dir_not_empty {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $res = $sftp->rmdir('testdir');
+      if ($res) {
+        die("RMDIR testdir succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name) = $sftp->error();
+
+      my $expected = 'SSH_FX_FAILURE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      unless (-d $test_dir) {
+        die("$test_dir directory does not exist as expected");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_rmdir_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'sub.d/test.lnk';
+      my $res = $sftp->rmdir($path);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("RMDIR $path failed: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_rmdir_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$sub_dir/test.lnk");
+
+  my $dst_path = $test_dir;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 fsio:20 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
-    };
+      my $path = 'sub.d/test.lnk';
+      my $res = $sftp->rmdir($path);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("RMDIR $path failed: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
 
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -20564,7 +29270,7 @@ sub sftp_readdir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -20574,21 +29280,297 @@ sub sftp_readdir {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub sftp_rmdir_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
+  }
+
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
 
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'sub.d/test.lnk';
+      my $res = $sftp->rmdir($path);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("RMDIR $path failed: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
+sub sftp_rmdir_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d");
+  mkpath($sub_dir);
+
+  my $test_dir = File::Spec->rel2abs("$sub_dir/test.d");
+  mkpath($test_dir);
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($sub_dir)) {
+    die("Can't chdir to $sub_dir: $!");
   }
 
-  unlink($log_file);
+  unless (symlink('./test.d', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.d': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $sub_dir)) {
+      die("Can't set perms on $sub_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $sub_dir)) {
+      die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = 'sub.d/test.lnk';
+      my $res = $sftp->rmdir($path);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("RMDIR $path failed: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
+    };
+    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 sftp_readdir_symlink_dir {
+sub sftp_remove {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -20604,22 +29586,20 @@ sub sftp_readdir_symlink_dir {
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
-  # For this test, we want to create a symlink to a directory which lies
-  # outside of the user's home dir; we will be using mod_vroot + DefaultRoot
-  # to "jail" the user into their home directory.
-
-  my $home_dir = File::Spec->rel2abs("$tmpdir/subdir");
-  mkpath($home_dir);
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  my $other_dir = File::Spec->rel2abs("$tmpdir/otherdir");
-  mkpath($other_dir);
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $test_symlink = File::Spec->rel2abs("$home_dir/otherdir.lnk");
-  unless (symlink($other_dir, $test_symlink)) {
-    die("Can't symlink $test_symlink to $other_dir: $!");
+  } else {
+    die("Can't open $test_file: $!");
   }
 
   # Make sure that, if we're running as root, that the home directory has
@@ -20646,7 +29626,7 @@ sub sftp_readdir_symlink_dir {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'auth:10 fsio:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -20662,13 +29642,6 @@ sub sftp_readdir_symlink_dir {
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
-
-      'mod_vroot.c' => {
-        DefaultRoot => '~',
-        VRootEngine => 'on',
-        VRootLog => $log_file,
-        VRootOptions => 'allowSymlinks',
-      },
     },
   };
 
@@ -20711,60 +29684,184 @@ sub sftp_readdir_symlink_dir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('otherdir.lnk');
-      unless ($dir) {
+      my $res = $sftp->unlink('test.txt');
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory 'otherdir.lnk': [$err_name] ($err_code)");
+        die("Can't remove test.txt: [$err_name] ($err_code)");
       }
 
-      my $res = {};
+      $sftp = undef;
+      $ssh2->disconnect();
 
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+      if (-f $test_file) {
+        die("$test_file file exists unexpectedly");
       }
+    };
 
-      my $expected = {
-        '.' => 1,
-        '..' => 1,
-      };
+    if ($@) {
+      $ex = $@;
+    }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+    $wfh->print("done\n");
+    $wfh->flush();
 
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
 
-      $ssh2->disconnect();
+    exit 0;
+  }
 
-      my $ok = 1;
-      my $mismatch;
+  # Stop server
+  server_stop($pid_file);
 
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
+  $self->assert_child_ok($pid);
 
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_rename {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $res = $sftp->rename('test.txt', 'test2.txt');
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't rename test.txt to test2.txt: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      if (-f $test_file) {
+        die("$test_file file exists unexpectedly");
+      }
+
+      unless (-f $test_file2) {
+        die("$test_file2 file does not exist as expected");
       }
-
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -20799,79 +29896,37 @@ sub sftp_readdir_symlink_dir {
   unlink($log_file);
 }
 
-sub sftp_readdir_wide_dir {
+sub sftp_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($test_dir);
-
-  my $expected = {
-    '.' => 1,
-    '..' => 1,
-  };
-
-  my $max_nfiles = 250;
-  for (my $i = 0; $i < $max_nfiles; $i++) {
-    my $test_filename = 'SomeReallyLongAndObnoxiousTestFileNameTemplate' . $i;
-
-    # The expected hash is used later for verifying the results of the READDIR
-    $expected->{$test_filename} = 1;
-
-    my $test_path = File::Spec->rel2abs("$test_dir/$test_filename");
-
-    if (open(my $fh, "> $test_path")) {
-      close($fh);
-
-    } else {
-      die("Can't open $test_path: $!");
-    }
-  }
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'auth:10 ssh2:20 sftp:20 scp:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -20880,14 +29935,15 @@ sub sftp_readdir_wide_dir {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -20915,7 +29971,7 @@ sub sftp_readdir_wide_dir {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -20926,62 +29982,24 @@ sub sftp_readdir_wide_dir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('test.d');
-      unless ($dir) {
+      my $res = $sftp->symlink('test.txt', 'test.lnk');
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory 'test.d': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+        die("Can't symlink test.lnk to test.txt: [$err_name] ($err_code)");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
-
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
-
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
-      }
-
-      my $nseen = scalar(@$seen);
-
-      # We add two here for '.' and '..'
-      my $expected_seen = $max_nfiles + 2;
-      $self->assert($expected_seen == $nseen,
-        test_msg("Expected '$expected_seen' files, got $nseen"));
-
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
-      }
+      $self->assert(-l $test_symlink,
+        test_msg("$test_symlink symlink does not exist as expected"));
 
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
+      # Make sure that the created symlink points to the target using a
+      # RELATIVE path (Bug#4081).
+      my $target = readlink($test_symlink);
+      my $expected = 'test.txt';
+      $self->assert($expected eq $target,
+        test_msg("Expected target '$expected', got '$target'"));
     };
 
     if ($@) {
@@ -20992,7 +30010,7 @@ sub sftp_readdir_wide_dir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -21002,101 +30020,49 @@ sub sftp_readdir_wide_dir {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_readdir_with_removes {
+sub sftp_symlink_dst_already_exists {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
-  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  my $test_file1 = File::Spec->rel2abs("$tmpdir/test1.txt");
-  if (open(my $fh, "> $test_file1")) {
-    print $fh "Hello, World!\n";
     unless (close($fh)) {
-      die("Can't write $test_file1");
+      die("Can't write $test_file: $!");
     }
 
   } else {
-    die("Can't open $test_file1");
+    die("Can't open $test_file: $!");
   }
 
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-  if (open(my $fh, "> $test_file2")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file2");
-    }
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  if (open(my $fh, "> $test_symlink")) {
+    close($fh);
 
   } else {
-    die("Can't open $test_file2");
+    die("Can't open $test_symlink: $!");
   }
 
-  my $test_file3 = File::Spec->rel2abs("$tmpdir/test3.txt");
-  if (open(my $fh, "> $test_file3")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file3");
-    }
-
-  } else {
-    die("Can't open $test_file3");
-  }
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'ssh2:20 sftp:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -21105,14 +30071,15 @@ sub sftp_readdir_with_removes {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -21140,7 +30107,7 @@ sub sftp_readdir_with_removes {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -21151,60 +30118,22 @@ sub sftp_readdir_with_removes {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      $dir->read();
-
-      # Before we close the directory, we do the following, 3 times in a row:
-      #
-      #   OPEN/STAT/READ/CLOSE/REMOVE
-      #
-      # and then do a closedir.
-
-      my $files = [$test_file1, $test_file2, $test_file3];
-      for my $file (@$files) {
-        my $fh = $sftp->open($file, O_RDONLY);
-        unless ($fh) {
-          my ($err_code, $err_name) = $sftp->error();
-          die("Can't open $file: [$err_name] ($err_code)");
-        }
-
-        my $attrs = $sftp->stat($file, 1);
-        unless ($attrs) {
-          my ($err_code, $err_name) = $sftp->error();
-          die("Can't stat $file: [$err_name] ($err_code)");
-        }
-
-        my $buf;
-        my $size = 0;
-
-        my $res = $fh->read($buf, 8192);
-        while ($res) {
-          $size += $res;
-          $res = $fh->read($buf, 8192);
-        }
-
-        # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-        $fh = undef;
-
-        $res = $sftp->unlink($file);
-        unless ($res) {
-          my ($err_code, $err_name) = $sftp->error();
-          die("Can't remove $file: [$err_name] ($err_code)");
-        }
+      my $res = $sftp->symlink('test.txt', 'test.lnk');
+      if ($res) {
+        die("Symlink test.lnk to test.txt succeeded unexpectedly");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+      my ($err_code, $err_name) = $sftp->error();
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
+
+      my $expected = 'SSH_FX_FAILURE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $self->assert(!-l $test_symlink,
+        test_msg("$test_symlink symlink exists unexpectedly"));
     };
 
     if ($@) {
@@ -21215,7 +30144,7 @@ sub sftp_readdir_with_removes {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -21225,45 +30154,13 @@ sub sftp_readdir_with_removes {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
-
-  eval {
-    if (open(my $fh, "< $log_file")) {
-      my $ok = 1;
-
-      while (my $line = <$fh>) {
-        if ($line =~ /Invalid handle/) {
-          $ok = 0;
-          last;
-        }
-      }
-
-      close($fh);
-
-      $self->assert($ok,
-        test_msg("Unexpectedly saw 'Invalid handle' SFTP response"));
-
-    } else {
-      die("Can't read $log_file: $!");
-    }
-  };
-  if ($@) {
-    $ex = $@;
-  }
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_mkdir {
+sub sftp_symlink_src_does_not_exist {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -21283,7 +30180,8 @@ sub sftp_mkdir {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -21367,18 +30265,18 @@ sub sftp_mkdir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->mkdir('testdir');
+      my $res = $sftp->symlink('test.txt', 'test.lnk');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't mkdir testdir: [$err_name] ($err_code)");
+        die("Symlink test.lnk to test.txt failed: [$err_name] ($err_code)");
       }
 
+
       $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-d $test_dir) {
-        die("$test_dir directory does not exist as expected");
-      }
+      $self->assert(-l $test_symlink,
+        test_msg("$test_symlink symlink does not exist as expected"));
     };
 
     if ($@) {
@@ -21413,56 +30311,40 @@ sub sftp_mkdir {
   unlink($log_file);
 }
 
-sub sftp_mkdir_readdir_bug3481 {
+sub sftp_readlink_abs_dst {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -21471,14 +30353,15 @@ sub sftp_mkdir_readdir_bug3481 {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -21492,8 +30375,6 @@ sub sftp_mkdir_readdir_bug3481 {
 
   my $ex;
 
-  # See http://forums.proftpd.org/smf/index.php/topic,4759.0.html
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -21508,7 +30389,7 @@ sub sftp_mkdir_readdir_bug3481 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -21519,76 +30400,17 @@ sub sftp_mkdir_readdir_bug3481 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->mkdir('testdir');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't mkdir testdir: [$err_name] ($err_code)");
-      }
-
-      # Create 10 files in the sub directory
-      my $count = 10;
-      for (my $i = 0; $i < $count; $i++) {
-        my $test_file = 'testdir/test_' . sprintf("%03s", $i);
-
-        my $fh = $sftp->open($test_file, O_CREAT, 0644); 
-        unless ($fh) {
-          my ($err_code, $err_name) = $sftp->error();
-          die("OPEN $test_file failed: [$err_name] ($err_code)");
-        }
-
-        $fh = undef;
-      }
-
-      # Now read the test directory twice
-      my $dir = $sftp->opendir('testdir');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("OPENDIR testdir failed: [$err_name] ($err_code)");
-      }
-
-      my $files1 = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $files1->{$file->{name}} = $file;
-        $file = $dir->read();
-      }
-
-      $dir = undef;
-
-      $dir = $sftp->opendir('testdir');
-      unless ($dir) {
+      my $path = $sftp->readlink('test.lnk');
+      unless ($path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("OPENDIR testdir failed: [$err_name] ($err_code)");
-      }
-
-      my $files2 = {};
-
-      $file = $dir->read();
-      while ($file) {
-        $files2->{$file->{name}} = $file;
-        $file = $dir->read();
+        die("Can't readlink test.lnk: [$err_name] ($err_code)");
       }
 
-      $dir = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
-      # Make sure that the same paths were returned in the directory listings
-      foreach my $file (keys(%$files1)) {
-        unless (defined($files2->{$file})) {
-          die("File $file unexpectedly missing from second READDIR");
-        }
-      }
-
-      foreach my $file (keys(%$files2)) {
-        unless (defined($files1->{$file})) {
-          die("File $file unexpectedly missing from first READDIR");
-        }
-      }
-
       $ssh2->disconnect();
+
+      $self->assert($test_file eq $path,
+        test_msg("Expected '$test_file', got '$path'"));
     };
 
     if ($@) {
@@ -21599,7 +30421,7 @@ sub sftp_mkdir_readdir_bug3481 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -21609,71 +30431,54 @@ sub sftp_mkdir_readdir_bug3481 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_rmdir {
+sub sftp_readlink_abs_dst_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  my $test_file = File::Spec->rel2abs("$setup->{home_dir}/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
-  mkpath($test_dir);
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$setup->{home_dir}/test.lnk");
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -21682,14 +30487,15 @@ sub sftp_rmdir {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -21717,7 +30523,7 @@ sub sftp_rmdir {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -21728,18 +30534,21 @@ sub sftp_rmdir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->rmdir('testdir');
-      unless ($res) {
+      my $path = $sftp->readlink('test.lnk');
+      unless ($path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't rmdir testdir: [$err_name] ($err_code)");
+        die("Can't readlink test.lnk: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
 
-      if (-d $test_dir) {
-        die("$test_dir directory exists unexpectedly");
-      }
+      # Because we are chrooted, AND the absolute destination path is within
+      # the chroot, the retrieved path will be adjusted for the chroot
+      # (Bug#4219).
+      my $expected = '/test.txt';
+      $self->assert($expected eq $path,
+        test_msg("Expected '$expected', got '$path'"));
     };
 
     if ($@) {
@@ -21750,7 +30559,7 @@ sub sftp_rmdir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -21760,46 +30569,148 @@ sub sftp_rmdir {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_rmdir_dir_not_empty {
+sub sftp_readlink_rel_dst {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
 
-  my $log_file = test_get_logfile();
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $dst_path = './test.txt';
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
-  mkpath($test_dir);
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $path = $sftp->readlink('test.lnk');
+      unless ($path) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't readlink test.lnk: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
+
+      $self->assert($dst_path eq $path,
+        test_msg("Expected '$dst_path', got '$path'"));
+    };
+
+    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 sftp_readlink_rel_dst_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
+    print $fh "ABCD" x 8192;
+
     unless (close($fh)) {
       die("Can't write $test_file: $!");
     }
@@ -21808,34 +30719,25 @@ sub sftp_rmdir_dir_not_empty {
     die("Can't open $test_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $dst_path = './test.txt';
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -21844,14 +30746,15 @@ sub sftp_rmdir_dir_not_empty {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -21879,7 +30782,7 @@ sub sftp_rmdir_dir_not_empty {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
@@ -21890,23 +30793,17 @@ sub sftp_rmdir_dir_not_empty {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->rmdir('testdir');
-      if ($res) {
-        die("RMDIR testdir succeeded unexpectedly");
+      my $path = $sftp->readlink('test.lnk');
+      unless ($path) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't readlink test.lnk: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected = 'SSH_FX_FAILURE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
       $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-d $test_dir) {
-        die("$test_dir directory does not exist as expected");
-      }
+      $self->assert($dst_path eq $path,
+        test_msg("Expected '$dst_path', got '$path'"));
     };
 
     if ($@) {
@@ -21917,7 +30814,7 @@ sub sftp_rmdir_dir_not_empty {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -21927,21 +30824,13 @@ sub sftp_rmdir_dir_not_empty {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_remove {
+sub sftp_readlink_symlink_dir_bug4140 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -21961,18 +30850,6 @@ sub sftp_remove {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -21992,6 +30869,23 @@ sub sftp_remove {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $dir_name = 'test.d';
+  my $test_dir = File::Spec->rel2abs("$tmpdir/$dir_name");
+  mkpath($test_dir);
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('test.d', 'test.lnk')) {
+    die("Can't symlink 'test.d' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22055,18 +30949,17 @@ sub sftp_remove {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->unlink('test.txt');
-      unless ($res) {
+      my $path = $sftp->readlink('test.lnk');
+      unless ($path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't remove test.txt: [$err_name] ($err_code)");
+        die("Can't readlink test.lnk: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
 
-      if (-f $test_file) {
-        die("$test_file file exists unexpectedly");
-      }
+      $self->assert($dir_name eq $path,
+        test_msg("Expected '$dir_name', got '$path'"));
     };
 
     if ($@) {
@@ -22101,7 +30994,7 @@ sub sftp_remove {
   unlink($log_file);
 }
 
-sub sftp_rename {
+sub sftp_config_allowoverwrite {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -22121,20 +31014,6 @@ sub sftp_rename {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -22154,6 +31033,14 @@ sub sftp_rename {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22164,6 +31051,12 @@ sub sftp_rename {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    Directory => {
+      '~' => {
+        AllowOverwrite => 'off',
+      },
+    },
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -22192,6 +31085,9 @@ sub sftp_rename {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -22217,22 +31113,19 @@ sub sftp_rename {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->rename('test.txt', 'test2.txt');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't rename test.txt to test2.txt: [$err_name] ($err_code)");
+      my $fh = $sftp->open('test.txt', O_WRONLY);
+      if ($fh) {
+        $fh = undef;
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
+      my ($err_code, $err_name) = $sftp->error();
       $sftp = undef;
       $ssh2->disconnect();
 
-      if (-f $test_file) {
-        die("$test_file file exists unexpectedly");
-      }
-
-      unless (-f $test_file2) {
-        die("$test_file2 file does not exist as expected");
-      }
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
     };
 
     if ($@) {
@@ -22267,7 +31160,7 @@ sub sftp_rename {
   unlink($log_file);
 }
 
-sub sftp_symlink {
+sub sftp_config_allowstorerestart {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -22287,20 +31180,6 @@ sub sftp_symlink {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -22320,6 +31199,17 @@ sub sftp_symlink {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22330,6 +31220,12 @@ sub sftp_symlink {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    Directory => {
+      '~' => {
+        AllowOverwrite => 'on',
+      },
+    },
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -22358,6 +31254,9 @@ sub sftp_symlink {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -22383,18 +31282,19 @@ sub sftp_symlink {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->symlink('test.txt', 'test.lnk');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't symlink test.lnk to test.txt: [$err_name] ($err_code)");
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_APPEND);
+      if ($fh) {
+        $fh = undef;
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
+      my ($err_code, $err_name) = $sftp->error();
       $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-l $test_symlink) {
-        die("$test_symlink symlink does not exist as expected");
-      }
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
     };
 
     if ($@) {
@@ -22429,7 +31329,7 @@ sub sftp_symlink {
   unlink($log_file);
 }
 
-sub sftp_symlink_dst_already_exists {
+sub sftp_config_client_alive {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -22449,26 +31349,6 @@ sub sftp_symlink_dst_already_exists {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
-  if (open(my $fh, "> $test_symlink")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_symlink: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -22488,6 +31368,11 @@ sub sftp_symlink_dst_already_exists {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $timeout_idle = 30;
+
+  my $client_alive_max = 5;
+  my $client_alive_interval = 1;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22497,6 +31382,7 @@ sub sftp_symlink_dst_already_exists {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    TimeoutIdle => $timeout_idle,
 
     IfModules => {
       'mod_delay.c' => {
@@ -22508,6 +31394,8 @@ sub sftp_symlink_dst_already_exists {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPClientAlive $client_alive_max $client_alive_interval",
       ],
     },
   };
@@ -22526,6 +31414,9 @@ sub sftp_symlink_dst_already_exists {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -22551,22 +31442,19 @@ sub sftp_symlink_dst_already_exists {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->symlink('test.txt', 'test.lnk');
-      if ($res) {
-        die("Symlink test.lnk to test.txt succeeded unexpectedly");
-      }
+      # We have to tell Net::SSH2 to actually _do_ something, in order to have
+      # it process any messages it may have received from mod_sftp, like
+      # the client alive checks.
 
-      my ($err_code, $err_name) = $sftp->error();
+      for (my $i = 0; $i < 10; $i++) {
+        $sftp->realpath('.');
+
+        my $delay = $client_alive_interval + 1;
+        sleep($delay);
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
-
-      my $expected = 'SSH_FX_FAILURE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
-      $self->assert(!-l $test_symlink,
-        test_msg("$test_symlink symlink exists unexpectedly"));
     };
 
     if ($@) {
@@ -22577,7 +31465,7 @@ sub sftp_symlink_dst_already_exists {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout_idle + 2) };
     if ($@) {
       warn($@);
       exit 1;
@@ -22601,7 +31489,7 @@ sub sftp_symlink_dst_already_exists {
   unlink($log_file);
 }
 
-sub sftp_symlink_src_does_not_exist {
+sub sftp_config_client_match {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -22621,9 +31509,6 @@ sub sftp_symlink_src_does_not_exist {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -22643,6 +31528,9 @@ sub sftp_symlink_src_does_not_exist {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $banner = 'SFTP_UnitTest (Perl)';
+  my $banner_pattern = 'SFTP_UnitTest \\\\(Perl\\\\)';
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22663,6 +31551,8 @@ sub sftp_symlink_src_does_not_exist {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPClientMatch \"^$banner_pattern\$\" channelWindowSize 64MB sftpProtocolVersion 1-2",
       ],
     },
   };
@@ -22681,12 +31571,16 @@ sub sftp_symlink_src_does_not_exist {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
       my $ssh2 = Net::SSH2->new();
+      $ssh2->banner($banner);
 
       sleep(1);
 
@@ -22706,18 +31600,12 @@ sub sftp_symlink_src_does_not_exist {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->symlink('test.txt', 'test.lnk');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Symlink test.lnk to test.txt failed: [$err_name] ($err_code)");
-      }
-
+      # We can't actually check whether our configured values are applied
+      # via the Net::SSH2 methods (yet).  So we have to rely on the
+      # generated TraceLog.
 
       $sftp = undef;
       $ssh2->disconnect();
-
-      $self->assert(-l $test_symlink,
-        test_msg("$test_symlink symlink does not exist as expected"));
     };
 
     if ($@) {
@@ -22752,7 +31640,7 @@ sub sftp_symlink_src_does_not_exist {
   unlink($log_file);
 }
 
-sub sftp_readlink {
+sub sftp_config_createhome {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -22768,39 +31656,10 @@ sub sftp_readlink {
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $home_dir = File::Spec->rel2abs("$tmpdir/foo/bar");
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
-  unless (symlink($test_file, $test_symlink)) {
-    die("Can't symlink $test_symlink to $test_file: $!");
-  }
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -22808,6 +31667,8 @@ sub sftp_readlink {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $home_gid = 777;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22818,6 +31679,8 @@ sub sftp_readlink {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    CreateHome => "on 711 homegid $home_gid",
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -22846,6 +31709,9 @@ sub sftp_readlink {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -22871,17 +31737,8 @@ sub sftp_readlink {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $path = $sftp->readlink('test.lnk');
-      unless ($path) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't readlink test.lnk: [$err_name] ($err_code)");
-      }
-
       $sftp = undef;
       $ssh2->disconnect();
-
-      $self->assert($test_file eq $path,
-        test_msg("Expected '$test_file', got '$path'"));
     };
 
     if ($@) {
@@ -22906,6 +31763,21 @@ sub sftp_readlink {
 
   $self->assert_child_ok($pid);
 
+  # Check that the home directory exists, and that the parent directory
+  # of $tmpdir/foo is owned by UID/GID root.
+  $self->assert(-d $home_dir,
+    test_msg("Expected $home_dir directory to exist"));
+
+  my ($uid_owner, $gid_owner) = (stat($home_dir))[4,5];
+
+  my $expected = $uid;
+  $self->assert($expected == $uid_owner,
+    test_msg("Expected $expected, got $uid_owner"));
+
+  $expected = $home_gid;
+  $self->assert($expected == $gid_owner,
+    test_msg("Expected $expected, got $gid_owner"));
+
   if ($ex) {
     test_append_logfile($log_file, $ex);
     unlink($log_file);
@@ -22916,7 +31788,7 @@ sub sftp_readlink {
   unlink($log_file);
 }
 
-sub sftp_readlink_symlink_dir_bug4140 {
+sub sftp_config_defaultchdir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -22936,15 +31808,26 @@ sub sftp_readlink_symlink_dir_bug4140 {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$home_dir/public_sftp");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't write $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
     }
   }
 
@@ -22952,26 +31835,12 @@ sub sftp_readlink_symlink_dir_bug4140 {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
+  $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $dir_name = 'test.d';
-  my $test_dir = File::Spec->rel2abs("$tmpdir/$dir_name");
-  mkpath($test_dir);
-
-  my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
-  }
-
-  unless (symlink('test.d', 'test.lnk')) {
-    die("Can't symlink 'test.d' to 'test.lnk': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -22981,6 +31850,7 @@ sub sftp_readlink_symlink_dir_bug4140 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultChdir => '~/public_sftp',
 
     IfModules => {
       'mod_delay.c' => {
@@ -23010,6 +31880,9 @@ sub sftp_readlink_symlink_dir_bug4140 {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -23035,17 +31908,61 @@ sub sftp_readlink_symlink_dir_bug4140 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $path = $sftp->readlink('test.lnk');
-      unless ($path) {
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't readlink test.lnk: [$err_name] ($err_code)");
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+        'test.txt' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
 
-      $self->assert($dir_name eq $path,
-        test_msg("Expected '$dir_name', got '$path'"));
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -23080,7 +31997,7 @@ sub sftp_readlink_symlink_dir_bug4140 {
   unlink($log_file);
 }
 
-sub sftp_config_allowoverwrite {
+sub sftp_config_deleteabortedstores {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -23116,32 +32033,24 @@ sub sftp_config_allowoverwrite {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
-  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
-
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
 
-  } else {
-    die("Can't open $test_file: $!");
-  }
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:20 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    Directory => {
-      '~' => {
-        AllowOverwrite => 'off',
-      },
-    },
+    HiddenStores => 'on',
+    DeleteAbortedStores => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -23199,19 +32108,26 @@ sub sftp_config_allowoverwrite {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY);
-      if ($fh) {
-        $fh = undef;
-        die("OPEN test.txt succeeded unexpectedly");
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
+      # Check for the HiddenStores file
+      unless (-f $hidden_file) {
+        die("File $hidden_file does not exist as expected");
+      }
+
+      print $fh "ABCD\n" x 32;
+
+      # Explicitly close the channel before we have closed the file, to
+      # simulate an "aborted" transfer.
       $sftp = undef;
       $ssh2->disconnect();
 
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      # Give the server a little time to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
@@ -23231,11 +32147,6 @@ sub sftp_config_allowoverwrite {
     exit 0;
   }
 
-  # Stop server
-  server_stop($pid_file);
-
-  $self->assert_child_ok($pid);
-
   if ($ex) {
     test_append_logfile($log_file, $ex);
     unlink($log_file);
@@ -23243,10 +32154,24 @@ sub sftp_config_allowoverwrite {
     die($ex);
   }
 
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  # Check that the HiddenStores file is gone, and the requested
+  # file does NOT exist.
+
+  $self->assert(!-f $hidden_file,
+    test_msg("File $hidden_file exists unexpectedly"));
+
+  $self->assert(!-f $test_file,
+    test_msg("File $test_file does not exist as expected"));
+
   unlink($log_file);
 }
 
-sub sftp_config_allowstorerestart {
+sub sftp_config_dirfakemode {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -23285,17 +32210,6 @@ sub sftp_config_allowstorerestart {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -23306,11 +32220,7 @@ sub sftp_config_allowstorerestart {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    Directory => {
-      '~' => {
-        AllowOverwrite => 'on',
-      },
-    },
+    DirFakeMode => '0310',
 
     IfModules => {
       'mod_delay.c' => {
@@ -23368,19 +32278,77 @@ sub sftp_config_allowstorerestart {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_APPEND);
-      if ($fh) {
-        $fh = undef;
-        die("OPEN test.txt succeeded unexpectedly");
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = sprintf("%04o", $file->{mode} & 0777);
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        '.' => '0310',
+        '..' => '0310',
+        'sftp.conf' => '0310',
+        'sftp.group' => '0310',
+        'sftp.passwd' => '0310',
+        'sftp.pid' => '0310',
+        'sftp.scoreboard' => '0310',
+        'sftp.scoreboard.lck' => '0310',
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
 
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      my $file_ok = 1;
+      my $mode_ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $file_ok = 0;
+          last;
+        }
+
+        unless ($res->{$name} eq $expected->{$name}) {
+          $mismatch = "$name: $res->{$name}";
+          $mode_ok = 0;
+          last;
+        }
+      }
+
+      unless ($file_ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      unless ($mode_ok) {
+        die("Unexpected mode '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -23415,7 +32383,7 @@ sub sftp_config_allowstorerestart {
   unlink($log_file);
 }
 
-sub sftp_config_client_alive {
+sub sftp_config_hiddenstores {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -23451,14 +32419,12 @@ sub sftp_config_client_alive {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
+  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_idle = 30;
-
-  my $client_alive_max = 5;
-  my $client_alive_interval = 1;
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -23468,7 +32434,8 @@ sub sftp_config_client_alive {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutIdle => $timeout_idle,
+
+    HiddenStores => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -23480,8 +32447,6 @@ sub sftp_config_client_alive {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPClientAlive $client_alive_max $client_alive_interval",
       ],
     },
   };
@@ -23528,19 +32493,37 @@ sub sftp_config_client_alive {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # We have to tell Net::SSH2 to actually _do_ something, in order to have
-      # it process any messages it may have received from mod_sftp, like
-      # the client alive checks.
-
-      for (my $i = 0; $i < 10; $i++) {
-        $sftp->realpath('.');
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
 
-        my $delay = $client_alive_interval + 1;
-        sleep($delay);
+      # Check for the HiddenStores file
+      unless (-f $hidden_file) {
+        die("File $hidden_file does not exist as expected");
       }
 
+      print $fh "ABCD\n" x 32;
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      # Check that the HiddenStores file is gone, and the requested
+      # file exists.
+
+      if (-f $hidden_file) {
+        die("File $hidden_file exists unexpectedly");
+      }
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
     };
 
     if ($@) {
@@ -23551,7 +32534,7 @@ sub sftp_config_client_alive {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout_idle + 2) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -23575,7 +32558,7 @@ sub sftp_config_client_alive {
   unlink($log_file);
 }
 
-sub sftp_config_client_match {
+sub sftp_config_hidefiles_abs_path {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -23614,8 +32597,18 @@ sub sftp_config_client_match {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $banner = 'SFTP_UnitTest (Perl)';
-  my $banner_pattern = 'SFTP_UnitTest \\\\(Perl\\\\)';
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make the perms on the file such that the user can't read it
+  unless (chmod(0311, $test_file)) {
+    die("Can't chmod $test_file: $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
@@ -23627,6 +32620,12 @@ sub sftp_config_client_match {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    Directory => {
+      '/' => {
+        HideFiles => '!\.txt$',
+      },
+    },
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -23637,8 +32636,6 @@ sub sftp_config_client_match {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPClientMatch \"^$banner_pattern\$\" channelWindowSize 64MB sftpProtocolVersion 1-2",
       ],
     },
   };
@@ -23666,7 +32663,6 @@ sub sftp_config_client_match {
   if ($pid) {
     eval {
       my $ssh2 = Net::SSH2->new();
-      $ssh2->banner($banner);
 
       sleep(1);
 
@@ -23686,12 +32682,62 @@ sub sftp_config_client_match {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # We can't actually check whether our configured values are applied
-      # via the Net::SSH2 methods (yet).  So we have to rely on the
-      # generated TraceLog.
+      # Issue a READDIR.  Due to HideFiles, we should only see
+      # the 'test.txt' file in the list.
+
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '.': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        'test.txt' => 1,
+      };
 
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -23726,7 +32772,7 @@ sub sftp_config_client_match {
   unlink($log_file);
 }
 
-sub sftp_config_createhome {
+sub sftp_config_hidefiles_deferred_path_bug3470 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -23742,10 +32788,22 @@ sub sftp_config_createhome {
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs("$tmpdir/foo/bar");
+  my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -23753,19 +32811,34 @@ sub sftp_config_createhome {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $home_gid = 777;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make the perms on the file such that the user can't read it
+  unless (chmod(0311, $test_file)) {
+    die("Can't chmod $test_file: $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 directory:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    CreateHome => "on 711 homegid $home_gid",
+    Directory => {
+      '~' => {
+        HideFiles => '^(\.|sftp)',
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -23823,8 +32896,62 @@ sub sftp_config_createhome {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
+      # Issue a READDIR.  Due to HideFiles, we should only see
+      # the 'test.txt' file in the list.
+
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '.': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      my $expected = {
+        'test.txt' => 1,
+      };
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -23849,21 +32976,6 @@ sub sftp_config_createhome {
 
   $self->assert_child_ok($pid);
 
-  # Check that the home directory exists, and that the parent directory
-  # of $tmpdir/foo is owned by UID/GID root.
-  $self->assert(-d $home_dir,
-    test_msg("Expected $home_dir directory to exist"));
-
-  my ($uid_owner, $gid_owner) = (stat($home_dir))[4,5];
-
-  my $expected = $uid;
-  $self->assert($expected == $uid_owner,
-    test_msg("Expected $expected, got $uid_owner"));
-
-  $expected = $home_gid;
-  $self->assert($expected == $gid_owner,
-    test_msg("Expected $expected, got $gid_owner"));
-
   if ($ex) {
     test_append_logfile($log_file, $ex);
     unlink($log_file);
@@ -23874,7 +32986,7 @@ sub sftp_config_createhome {
   unlink($log_file);
 }
 
-sub sftp_config_defaultchdir {
+sub sftp_config_hidefiles_deferred_path_chroot_bug3470 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -23894,26 +33006,15 @@ sub sftp_config_defaultchdir {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$home_dir/public_sftp");
-  mkpath($sub_dir);
-
-  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't write $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir, $sub_dir to 0755: $!");
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir, $sub_dir to $uid/$gid: $!");
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
 
@@ -23921,22 +33022,38 @@ sub sftp_config_defaultchdir {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
-  $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make the perms on the file such that the user can't read it
+  unless (chmod(0311, $test_file)) {
+    die("Can't chmod $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 directory:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultChdir => '~/public_sftp',
+    DefaultRoot => '~',
+
+    Directory => {
+      '~' => {
+        HideFiles => '^(\.|sftp)',
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -23994,6 +33111,9 @@ sub sftp_config_defaultchdir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
+      # Issue a READDIR.  Due to HideFiles, we should only see
+      # the 'test.txt' file in the list.
+
       my $dir = $sftp->opendir('.');
       unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
@@ -24009,8 +33129,6 @@ sub sftp_config_defaultchdir {
       }
 
       my $expected = {
-        '.' => 1,
-        '..' => 1,
         'test.txt' => 1,
       };
 
@@ -24083,7 +33201,7 @@ sub sftp_config_defaultchdir {
   unlink($log_file);
 }
 
-sub sftp_config_deleteabortedstores {
+sub sftp_config_hidefiles_symlink_bug3924 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -24103,6 +33221,24 @@ sub sftp_config_deleteabortedstores {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foobar");
+  mkpath($sub_dir);
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/foobar2");
+
+  my $cwd = getcwd();
+  unless (chdir($tmpdir)) {
+    die("Can't chdir to $tmpdir: $!");
+  }
+
+  unless (symlink('/', $test_symlink)) {
+    die("Can't symlink '/' to '$test_symlink': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -24119,9 +33255,6 @@ sub sftp_config_deleteabortedstores {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
@@ -24130,13 +33263,16 @@ sub sftp_config_deleteabortedstores {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:20 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    HiddenStores => 'on',
-    DeleteAbortedStores => 'on',
+    Directory => {
+      '/' => {
+        HideFiles => 'foobar',
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -24194,26 +33330,66 @@ sub sftp_config_deleteabortedstores {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
-      # Check for the HiddenStores file
-      unless (-f $hidden_file) {
-        die("File $hidden_file does not exist as expected");
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
       }
 
-      print $fh "ABCD\n" x 32;
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+        'sftp.conf' => 1,
+        'sftp.passwd' => 1,
+        'sftp.group' => 1,
+        'sftp.pid' => 1,
+        'sftp.scoreboard' => 1,
+        'sftp.scoreboard.lck' => 1,
+      };
 
-      # Explicitly close the channel before we have closed the file, to
-      # simulate an "aborted" transfer.
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
 
-      # Give the server a little time to do its end-of-session thing.
-      sleep(1);
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -24233,31 +33409,22 @@ sub sftp_config_deleteabortedstores {
     exit 0;
   }
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
   # Stop server
   server_stop($pid_file);
 
   $self->assert_child_ok($pid);
 
-  # Check that the HiddenStores file is gone, and the requested
-  # file does NOT exist.
-
-  $self->assert(!-f $hidden_file,
-    test_msg("File $hidden_file exists unexpectedly"));
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
 
-  $self->assert(!-f $test_file,
-    test_msg("File $test_file does not exist as expected"));
+    die($ex);
+  }
 
   unlink($log_file);
 }
 
-sub sftp_config_dirfakemode {
+sub sftp_config_hidenoaccess {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -24296,6 +33463,19 @@ sub sftp_config_dirfakemode {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make the perms on the file such that the user can't read it
+  unless (chmod(0311, $test_file)) {
+    die("Can't chmod $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -24306,7 +33486,17 @@ sub sftp_config_dirfakemode {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    DirFakeMode => '0310',
+    Directory => {
+      '~' => {
+        Limit => {
+          DIRS => {
+            IgnoreHidden => 'on',
+          },
+        },
+
+        HideNoAccess => 'on',
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -24318,6 +33508,8 @@ sub sftp_config_dirfakemode {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
@@ -24364,6 +33556,9 @@ sub sftp_config_dirfakemode {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
+      # Issue a READDIR.  Due to the "HideNoAccess on", we should not see
+      # the 'test.txt' file in the list.
+
       my $dir = $sftp->opendir('.');
       unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
@@ -24374,19 +33569,19 @@ sub sftp_config_dirfakemode {
 
       my $file = $dir->read();
       while ($file) {
-        $res->{$file->{name}} = sprintf("%04o", $file->{mode} & 0777);
+        $res->{$file->{name}} = $file;
         $file = $dir->read();
       }
 
       my $expected = {
-        '.' => '0311',
-        '..' => '0311',
-        'sftp.conf' => '0310',
-        'sftp.group' => '0310',
-        'sftp.passwd' => '0310',
-        'sftp.pid' => '0310',
-        'sftp.scoreboard' => '0310',
-        'sftp.scoreboard.lck' => '0310',
+        '.' => 1,
+        '..' => 1,
+        'sftp.conf' => 1,
+        'sftp.group' => 1,
+        'sftp.passwd' => 1,
+        'sftp.pid' => 1,
+        'sftp.scoreboard' => 1,
+        'sftp.scoreboard.lck' => 1,
       };
 
       # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
@@ -24397,8 +33592,7 @@ sub sftp_config_dirfakemode {
 
       $ssh2->disconnect();
 
-      my $file_ok = 1;
-      my $mode_ok = 1;
+      my $ok = 1;
       my $mismatch;
 
       my $seen = [];
@@ -24407,25 +33601,15 @@ sub sftp_config_dirfakemode {
 
         unless (defined($expected->{$name})) {
           $mismatch = $name;
-          $file_ok = 0;
-          last;
-        }
-
-        unless ($res->{$name} eq $expected->{$name}) {
-          $mismatch = "$name: $res->{$name}";
-          $mode_ok = 0;
+          $ok = 0;
           last;
         }
       }
 
-      unless ($file_ok) {
+      unless ($ok) {
         die("Unexpected name '$mismatch' appeared in READDIR data")
       }
 
-      unless ($mode_ok) {
-        die("Unexpected mode '$mismatch' appeared in READDIR data")
-      }
-
       # Now remove from $expected all of the paths we saw; if there are
       # any entries remaining in $expected, something went wrong.
       foreach my $name (@$seen) {
@@ -24469,7 +33653,7 @@ sub sftp_config_dirfakemode {
   unlink($log_file);
 }
 
-sub sftp_config_hiddenstores {
+sub sftp_config_max_clients_per_host_bug3630 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -24505,12 +33689,13 @@ sub sftp_config_hiddenstores {
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt.");
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+
+  my $max_clients_per_host = 1;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -24520,25 +33705,40 @@ sub sftp_config_hiddenstores {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    HiddenStores => 'on',
+    MaxClientsPerHost => $max_clients_per_host,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
-
-      'mod_sftp.c' => [
-        "SFTPEngine on",
-        "SFTPLog $log_file",
-        "SFTPHostKey $rsa_host_key",
-        "SFTPHostKey $dsa_host_key",
-      ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    my $sftp_port = $port + 2;
+
+    print $fh <<EOC;
+<IfModule mod_sftp.c>
+ <VirtualHost 0.0.0.0>
+    SFTPEngine on
+    SFTPLog $log_file
+    SFTPHostKey $rsa_host_key
+    SFTPHostKey $dsa_host_key
+
+    Port $sftp_port
+  </VirtualHost>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -24547,69 +33747,32 @@ sub sftp_config_hiddenstores {
     die("Can't open pipe: $!");
   }
 
-  require Net::SSH2;
-
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $ssh2 = Net::SSH2->new();
-
-      sleep(1);
-
-      unless ($ssh2->connect('127.0.0.1', $port)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
+      # First client should be able to connect and log in...
+      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($user, $passwd);
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
+      # ...but the second client should be able to connect, but not login.
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      # Check for the HiddenStores file
-      unless (-f $hidden_file) {
-        die("File $hidden_file does not exist as expected");
+      eval { $client2->login($user, $passwd) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
       }
 
-      print $fh "ABCD\n" x 32;
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
-
-      $ssh2->disconnect();
-
-      # Check that the HiddenStores file is gone, and the requested
-      # file exists.
+      my $resp_code = $client2->response_code();
 
-      if (-f $hidden_file) {
-        die("File $hidden_file exists unexpectedly");
-      }
+      my $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
 
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
-      }
+      $client1->quit();
     };
 
     if ($@) {
@@ -24644,7 +33807,7 @@ sub sftp_config_hiddenstores {
   unlink($log_file);
 }
 
-sub sftp_config_hidefiles_abs_path {
+sub sftp_config_max_login_attempts_via_password {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -24683,18 +33846,8 @@ sub sftp_config_hidefiles_abs_path {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Make the perms on the file such that the user can't read it
-  unless (chmod(0311, $test_file)) {
-    die("Can't chmod $test_file: $!");
-  }
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
 
   my $config = {
     PidFile => $pid_file,
@@ -24705,12 +33858,7 @@ sub sftp_config_hidefiles_abs_path {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    Directory => {
-      '/' => {
-        HideFiles => '!\.txt$',
-      },
-    },
+    MaxLoginAttempts => 1,
 
     IfModules => {
       'mod_delay.c' => {
@@ -24762,68 +33910,26 @@ sub sftp_config_hidefiles_abs_path {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      # Issue a READDIR.  Due to HideFiles, we should only see
-      # the 'test.txt' file in the list.
-
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
-      }
-
-      my $expected = {
-        'test.txt' => 1,
-      };
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
+      # Now connect again, try to authenticate via 'publickey', which should
+      # fail, and then again via 'password', which should also fail, since
+      # it exceeds the MaxLoginAttempts of 1.
 
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+      $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Publickey auth succeeded unexpectedly");
       }
 
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
+      if ($ssh2->auth_password($user, $passwd)) {
+        die("Password auth succeeded unexpectedly");
       }
 
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -24858,7 +33964,7 @@ sub sftp_config_hidefiles_abs_path {
   unlink($log_file);
 }
 
-sub sftp_config_hidefiles_deferred_path_bug3470 {
+sub sftp_config_max_login_attempts_via_publickey {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -24897,17 +34003,13 @@ sub sftp_config_hidefiles_deferred_path_bug3470 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
 
-  # Make the perms on the file such that the user can't read it
-  unless (chmod(0311, $test_file)) {
-    die("Can't chmod $test_file: $!");
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
   }
 
   my $config = {
@@ -24915,16 +34017,11 @@ sub sftp_config_hidefiles_deferred_path_bug3470 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 directory:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    Directory => {
-      '~' => {
-        HideFiles => '^(\.|sftp)',
-      },
-    },
+    MaxLoginAttempts => 1,
 
     IfModules => {
       'mod_delay.c' => {
@@ -24936,6 +34033,7 @@ sub sftp_config_hidefiles_deferred_path_bug3470 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
       ],
     },
   };
@@ -24971,73 +34069,30 @@ sub sftp_config_hidefiles_deferred_path_bug3470 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      # Issue a READDIR.  Due to HideFiles, we should only see
-      # the 'test.txt' file in the list.
-
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
-      }
-
-      my $expected = {
-        'test.txt' => 1,
-      };
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
+      # Now connect again, try to authenticate via 'password', which should
+      # fail, and then again via 'publickey', which should also fail, since
+      # it exceeds the MaxLoginAttempts of 1.
 
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+      $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
+      if ($ssh2->auth_password($user, 'foobar')) {
+        die("Password auth succeeded unexpectedly");
       }
 
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Publickey auth succeeded unexpectedly");
       }
-
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -25072,7 +34127,7 @@ sub sftp_config_hidefiles_deferred_path_bug3470 {
   unlink($log_file);
 }
 
-sub sftp_config_hidefiles_deferred_path_chroot_bug3470 {
+sub sftp_config_max_login_attempts_none_bug4087 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -25111,35 +34166,19 @@ sub sftp_config_hidefiles_deferred_path_chroot_bug3470 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Make the perms on the file such that the user can't read it
-  unless (chmod(0311, $test_file)) {
-    die("Can't chmod $test_file: $!");
-  }
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 directory:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
-
-    Directory => {
-      '~' => {
-        HideFiles => '^(\.|sftp)',
-      },
-    },
+    MaxLoginAttempts => 'none',
 
     IfModules => {
       'mod_delay.c' => {
@@ -25191,68 +34230,28 @@ sub sftp_config_hidefiles_deferred_path_chroot_bug3470 {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      # Issue a READDIR.  Due to HideFiles, we should only see
-      # the 'test.txt' file in the list.
-
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
-      }
-
-      my $expected = {
-        'test.txt' => 1,
-      };
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
+      # Now connect again, try to authenticate via 'publickey', which should
+      # fail, and then again via 'password', which should succeed, since
+      # we've disabled MaxLoginAttempts using "none".
 
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
+      $ssh2 = Net::SSH2->new();
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Publickey auth succeeded unexpectedly");
       }
 
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server (2nd attempt): [$err_name] ($err_code) $err_str");
       }
 
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -25287,7 +34286,7 @@ sub sftp_config_hidefiles_deferred_path_chroot_bug3470 {
   unlink($log_file);
 }
 
-sub sftp_config_hidefiles_symlink_bug3924 {
+sub sftp_config_pathdenyfilter_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -25307,24 +34306,6 @@ sub sftp_config_hidefiles_symlink_bug3924 {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foobar");
-  mkpath($sub_dir);
-
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/foobar2");
-
-  my $cwd = getcwd();
-  unless (chdir($tmpdir)) {
-    die("Can't chdir to $tmpdir: $!");
-  }
-
-  unless (symlink('/', $test_symlink)) {
-    die("Can't symlink '/' to '$test_symlink': $!");
-  }
-
-  unless (chdir($cwd)) {
-    die("Can't chdir to $cwd: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -25344,6 +34325,8 @@ sub sftp_config_hidefiles_symlink_bug3924 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$home_dir/test.ext");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -25354,11 +34337,7 @@ sub sftp_config_hidefiles_symlink_bug3924 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    Directory => {
-      '/' => {
-        HideFiles => 'foobar',
-      },
-    },
+    PathDenyFilter => '\.ext$',
 
     IfModules => {
       'mod_delay.c' => {
@@ -25416,66 +34395,22 @@ sub sftp_config_hidefiles_symlink_bug3924 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+      my $fh = $sftp->open('test.ext', O_WRONLY|O_CREAT|O_TRUNC, 664);
+      if ($fh) {
+        die("OPEN test.ext succeeded unexpectedly");
       }
 
-      my $expected = {
-        '.' => 1,
-        '..' => 1,
-        'sftp.conf' => 1,
-        'sftp.passwd' => 1,
-        'sftp.group' => 1,
-        'sftp.pid' => 1,
-        'sftp.scoreboard' => 1,
-        'sftp.scoreboard.lck' => 1,
-      };
+      my ($err_code, $err_name) = $sftp->error();
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
-
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
-
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
-      }
-
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
-      }
-
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
+      $self->assert(!-f $test_file,
+        test_msg("File $test_file exists unexpectedly"));
     };
 
     if ($@) {
@@ -25510,7 +34445,7 @@ sub sftp_config_hidefiles_symlink_bug3924 {
   unlink($log_file);
 }
 
-sub sftp_config_hidenoaccess {
+sub sftp_config_pathdenyfilter_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -25549,18 +34484,7 @@ sub sftp_config_hidenoaccess {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Make the perms on the file such that the user can't read it
-  unless (chmod(0311, $test_file)) {
-    die("Can't chmod $test_file: $!");
-  }
+  my $test_dir = File::Spec->rel2abs("$home_dir/sub.dir");
 
   my $config = {
     PidFile => $pid_file,
@@ -25572,17 +34496,7 @@ sub sftp_config_hidenoaccess {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    Directory => {
-      '~' => {
-        Limit => {
-          DIRS => {
-            IgnoreHidden => 'on',
-          },
-        },
-
-        HideNoAccess => 'on',
-      },
-    },
+    PathDenyFilter => '\.dir$',
 
     IfModules => {
       'mod_delay.c' => {
@@ -25594,8 +34508,6 @@ sub sftp_config_hidenoaccess {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
@@ -25642,69 +34554,22 @@ sub sftp_config_hidenoaccess {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Issue a READDIR.  Due to the "HideNoAccess on", we should not see
-      # the 'test.txt' file in the list.
-
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+      my $res = $sftp->mkdir('sub.dir');
+      if ($res) {
+        die("MKDIR sub.dir succeeded unexpectedly");
       }
 
-      my $expected = {
-        '.' => 1,
-        '..' => 1,
-        'sftp.conf' => 1,
-        'sftp.group' => 1,
-        'sftp.passwd' => 1,
-        'sftp.pid' => 1,
-        'sftp.scoreboard' => 1,
-        'sftp.scoreboard.lck' => 1,
-      };
+      my ($err_code, $err_name) = $sftp->error();
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
-
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
-
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
-      }
-
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
-      }
-
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
+      $self->assert(!-d $test_dir,
+        test_msg("Directory $test_dir exists unexpectedly"));
     };
 
     if ($@) {
@@ -25739,7 +34604,7 @@ sub sftp_config_hidenoaccess {
   unlink($log_file);
 }
 
-sub sftp_config_max_clients_per_host_bug3630 {
+sub sftp_config_rekey_short_timeout_failed {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -25771,6 +34636,20 @@ sub sftp_config_max_clients_per_host_bug3630 {
     }
   }
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x (1024 * 1024);
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -25778,9 +34657,7 @@ sub sftp_config_max_clients_per_host_bug3630 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-
-  my $max_clients_per_host = 1;
+  my $timeout_idle = 20;
 
   my $config = {
     PidFile => $pid_file,
@@ -25791,40 +34668,26 @@ sub sftp_config_max_clients_per_host_bug3630 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    MaxClientsPerHost => $max_clients_per_host,
+    TimeoutIdle => $timeout_idle,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        "SFTPRekey required 3600 1 1",
+      ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    my $sftp_port = $port + 2;
-
-    print $fh <<EOC;
-<IfModule mod_sftp.c>
- <VirtualHost 0.0.0.0>
-    SFTPEngine on
-    SFTPLog $log_file
-    SFTPHostKey $rsa_host_key
-    SFTPHostKey $dsa_host_key
-
-    Port $sftp_port
-  </VirtualHost>
-</IfModule>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -25833,32 +34696,72 @@ EOC
     die("Can't open pipe: $!");
   }
 
+  require Net::SSH2;
+
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      # First client should be able to connect and log in...
-      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client1->login($user, $passwd);
+      my $ssh2 = Net::SSH2->new();
 
-      # ...but the second client should be able to connect, but not login.
-      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      sleep(1);
 
-      eval { $client2->login($user, $passwd) };
-      unless ($@) {
-        die("Login succeeded unexpectedly");
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $resp_code = $client2->response_code();
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      my $expected = 530;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      $client1->quit();
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+      my $buflen = (1024 * 250);
+
+      my $res = $fh->read($buf, $buflen);
+      while ($res) {
+        $size += $res;
+        sleep(1);
+
+        $res = $fh->read($buf, $buflen);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      # The only way we really have, given Net::SSH2's API, to detect
+      # whether the server disconnected us in mid-download is to check the
+      # number of bytes we downloaded, to see if it's the full size.
+      #
+      # With such a short rekey timeout (1 sec), it's not expected that we
+      # have all of the bytes.
+      $self->assert($test_sz != $size,
+        test_msg("Expected !$test_sz, got $size"));
     };
 
     if ($@) {
@@ -25869,7 +34772,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout_idle + 3) };
     if ($@) {
       warn($@);
       exit 1;
@@ -25893,7 +34796,7 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_config_max_login_attempts_via_password {
+sub sftp_config_rekey_long_timeout_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -25925,6 +34828,20 @@ sub sftp_config_max_login_attempts_via_password {
     }
   }
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x (1024 * 1024);
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -25932,8 +34849,7 @@ sub sftp_config_max_login_attempts_via_password {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $timeout_idle = 20;
 
   my $config = {
     PidFile => $pid_file,
@@ -25944,7 +34860,7 @@ sub sftp_config_max_login_attempts_via_password {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    MaxLoginAttempts => 1,
+    TimeoutIdle => $timeout_idle,
 
     IfModules => {
       'mod_delay.c' => {
@@ -25956,6 +34872,8 @@ sub sftp_config_max_login_attempts_via_password {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPRekey required 3600 1 15",
       ],
     },
   };
@@ -25996,26 +34914,46 @@ sub sftp_config_max_login_attempts_via_password {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      $ssh2->disconnect();
-
-      # Now connect again, try to authenticate via 'publickey', which should
-      # fail, and then again via 'password', which should also fail, since
-      # it exceeds the MaxLoginAttempts of 1.
-
-      $ssh2 = Net::SSH2->new();
-      unless ($ssh2->connect('127.0.0.1', $port)) {
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
-        die("Publickey auth succeeded unexpectedly");
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      if ($ssh2->auth_password($user, $passwd)) {
-        die("Password auth succeeded unexpectedly");
+      my $buf;
+      my $size = 0;
+      my $buflen = (1024 * 250);
+
+      my $res = $fh->read($buf, $buflen);
+      while ($res) {
+        $size += $res;
+        sleep(1);
+
+        $res = $fh->read($buf, $buflen);
       }
 
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
+
+      # The only way we really have, given Net::SSH2's API, to detect
+      # whether the server disconnected us in mid-download is to check the
+      # number of bytes we downloaded, to see if it's the full size.
+      #
+      # With such long rekey timeout (15 sec), it is expected that we have
+      # all of the bytes.
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
     };
 
     if ($@) {
@@ -26026,7 +34964,7 @@ sub sftp_config_max_login_attempts_via_password {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, $timeout_idle + 3) };
     if ($@) {
       warn($@);
       exit 1;
@@ -26050,7 +34988,7 @@ sub sftp_config_max_login_attempts_via_password {
   unlink($log_file);
 }
 
-sub sftp_config_max_login_attempts_via_publickey {
+sub sftp_config_rootlogin {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -26067,8 +35005,8 @@ sub sftp_config_max_login_attempts_via_publickey {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $uid = 0;
+  my $gid = 0;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -26089,15 +35027,6 @@ sub sftp_config_max_login_attempts_via_publickey {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
-
-  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
-  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
-    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -26107,7 +35036,8 @@ sub sftp_config_max_login_attempts_via_publickey {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    MaxLoginAttempts => 1,
+
+    RootLogin => 'off',
 
     IfModules => {
       'mod_delay.c' => {
@@ -26119,7 +35049,6 @@ sub sftp_config_max_login_attempts_via_publickey {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
       ],
     },
   };
@@ -26155,30 +35084,155 @@ sub sftp_config_max_login_attempts_via_publickey {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      if ($ssh2->auth_password($user, $passwd)) {
+        die("Unexpectedly logged in to SSH2 server");
       }
+    };
 
-      $ssh2->disconnect();
+    if ($@) {
+      $ex = $@;
+    }
 
-      # Now connect again, try to authenticate via 'password', which should
-      # fail, and then again via 'publickey', which should also fail, since
-      # it exceeds the MaxLoginAttempts of 1.
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sftp_config_protocols {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "Protocols scp",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
 
-      $ssh2 = Net::SSH2->new();
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_password($user, 'foobar')) {
-        die("Password auth succeeded unexpectedly");
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
-        die("Publickey auth succeeded unexpectedly");
+      my $sftp = $ssh2->sftp();
+      if ($sftp) {
+        die("SFTP subsystem started unexpectedly");
       }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+
+      my $expected = 'LIBSSH2_ERROR_CHANNEL_FAILURE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -26213,7 +35267,7 @@ sub sftp_config_max_login_attempts_via_publickey {
   unlink($log_file);
 }
 
-sub sftp_config_max_login_attempts_none_bug4087 {
+sub sftp_config_serverident_off {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -26252,9 +35306,6 @@ sub sftp_config_max_login_attempts_none_bug4087 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -26264,7 +35315,7 @@ sub sftp_config_max_login_attempts_none_bug4087 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    MaxLoginAttempts => 'none',
+    ServerIdent => 'off',
 
     IfModules => {
       'mod_delay.c' => {
@@ -26276,6 +35327,10 @@ sub sftp_config_max_login_attempts_none_bug4087 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        # Enable this, so that the Telnet connection does not receive the
+        # KEXINIT data upon connect.
+        "SFTPOptions PessimisticKexinit",
       ],
     },
   };
@@ -26291,6 +35346,7 @@ sub sftp_config_max_login_attempts_none_bug4087 {
   }
 
   require Net::SSH2;
+  require Net::Telnet;
 
   my $ex;
 
@@ -26304,7 +35360,7 @@ sub sftp_config_max_login_attempts_none_bug4087 {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(1);
+      sleep(2);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -26316,28 +35372,30 @@ sub sftp_config_max_login_attempts_none_bug4087 {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      $ssh2->disconnect();
-
-      # Now connect again, try to authenticate via 'publickey', which should
-      # fail, and then again via 'password', which should succeed, since
-      # we've disabled MaxLoginAttempts using "none".
-
-      $ssh2 = Net::SSH2->new();
-      unless ($ssh2->connect('127.0.0.1', $port)) {
+      unless ($ssh2->disconnect('Done with integration test')) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
-        die("Publickey auth succeeded unexpectedly");
-      }
+      $ssh2 = undef;
 
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server (2nd attempt): [$err_name] ($err_code) $err_str");
-      }
+      # Now connect again, this time with a Telnet client, to get the
+      # SSH version identification string.  The libssh2 API doesn't provide
+      # a way to get that string, and thus neither does Net::SSH2.
+      my $telnet = Net::Telnet->new(
+        Host => '127.0.0.1',
+        Port => $port,
+        Timeout => 3,
+        Errmode => 'return',
+      );
 
-      $ssh2->disconnect();
+      my $version_id = $telnet->getline();
+      chomp($version_id);
+      $telnet->close();
+
+      my $expected = 'SSH-2.0-mod_sftp';
+      $self->assert($version_id eq $expected,
+        test_msg("Expected SSH version identification '$expected', received '$version_id'"));
     };
 
     if ($@) {
@@ -26372,7 +35430,7 @@ sub sftp_config_max_login_attempts_none_bug4087 {
   unlink($log_file);
 }
 
-sub sftp_config_pathdenyfilter_file {
+sub sftp_config_serverident_on {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -26411,8 +35469,6 @@ sub sftp_config_pathdenyfilter_file {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$home_dir/test.ext");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -26422,8 +35478,7 @@ sub sftp_config_pathdenyfilter_file {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    PathDenyFilter => '\.ext$',
+    ServerIdent => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -26435,6 +35490,10 @@ sub sftp_config_pathdenyfilter_file {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        # Enable this, so that the Telnet connection does not receive the
+        # KEXINIT data upon connect.
+        "SFTPOptions PessimisticKexinit",
       ],
     },
   };
@@ -26450,6 +35509,7 @@ sub sftp_config_pathdenyfilter_file {
   }
 
   require Net::SSH2;
+  require Net::Telnet;
 
   my $ex;
 
@@ -26463,7 +35523,7 @@ sub sftp_config_pathdenyfilter_file {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(1);
+      sleep(2);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -26475,28 +35535,30 @@ sub sftp_config_pathdenyfilter_file {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
+      unless ($ssh2->disconnect('Done with integration test')) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $fh = $sftp->open('test.ext', O_WRONLY|O_CREAT|O_TRUNC, 664);
-      if ($fh) {
-        die("OPEN test.ext succeeded unexpectedly");
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
+      $ssh2 = undef;
 
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      # Now connect again, this time with a Telnet client, to get the
+      # SSH version identification string.  The libssh2 API doesn't provide
+      # a way to get that string, and thus neither does Net::SSH2.
+      my $telnet = Net::Telnet->new(
+        Host => '127.0.0.1',
+        Port => $port,
+        Timeout => 3,
+        Errmode => 'return',
+      );
 
-      $sftp = undef;
-      $ssh2->disconnect();
+      my $version_id = $telnet->getline();
+      chomp($version_id);
+      $telnet->close();
 
-      $self->assert(!-f $test_file,
-        test_msg("File $test_file exists unexpectedly"));
+      my $expected = 'SSH-2.0-mod_sftp/\d*\.\d*\.\d*';
+      $self->assert(qr/$expected/, $version_id,
+        test_msg("Expected SSH version identification '$expected', received '$version_id'"));
     };
 
     if ($@) {
@@ -26531,7 +35593,7 @@ sub sftp_config_pathdenyfilter_file {
   unlink($log_file);
 }
 
-sub sftp_config_pathdenyfilter_dir {
+sub sftp_config_serverident_on_custom {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -26570,7 +35632,7 @@ sub sftp_config_pathdenyfilter_dir {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$home_dir/sub.dir");
+  my $custom_id = "OpenSSH_5.6p1";
 
   my $config = {
     PidFile => $pid_file,
@@ -26581,8 +35643,7 @@ sub sftp_config_pathdenyfilter_dir {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    PathDenyFilter => '\.dir$',
+    ServerIdent => "on $custom_id",
 
     IfModules => {
       'mod_delay.c' => {
@@ -26594,6 +35655,10 @@ sub sftp_config_pathdenyfilter_dir {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        # Enable this, so that the Telnet connection does not receive the
+        # KEXINIT data upon connect.
+        "SFTPOptions PessimisticKexinit",
       ],
     },
   };
@@ -26609,6 +35674,7 @@ sub sftp_config_pathdenyfilter_dir {
   }
 
   require Net::SSH2;
+  require Net::Telnet;
 
   my $ex;
 
@@ -26622,7 +35688,7 @@ sub sftp_config_pathdenyfilter_dir {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(1);
+      sleep(2);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -26634,28 +35700,30 @@ sub sftp_config_pathdenyfilter_dir {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
+      unless ($ssh2->disconnect('Done with integration test')) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $res = $sftp->mkdir('sub.dir');
-      if ($res) {
-        die("MKDIR sub.dir succeeded unexpectedly");
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
+      $ssh2 = undef;
 
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      # Now connect again, this time with a Telnet client, to get the
+      # SSH version identification string.  The libssh2 API doesn't provide
+      # a way to get that string, and thus neither does Net::SSH2.
+      my $telnet = Net::Telnet->new(
+        Host => '127.0.0.1',
+        Port => $port,
+        Timeout => 3,
+        Errmode => 'return',
+      );
 
-      $sftp = undef;
-      $ssh2->disconnect();
+      my $version_id = $telnet->getline();
+      chomp($version_id);
+      $telnet->close();
 
-      $self->assert(!-d $test_dir,
-        test_msg("Directory $test_dir exists unexpectedly"));
+      my $expected = 'SSH-2.0-' . $custom_id;
+      $self->assert($version_id eq $expected,
+        test_msg("Expected SSH version identification '$expected', received '$version_id'"));
     };
 
     if ($@) {
@@ -26690,7 +35758,7 @@ sub sftp_config_pathdenyfilter_dir {
   unlink($log_file);
 }
 
-sub sftp_config_rekey_short_timeout_failed {
+sub sftp_config_timeoutidle {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -26722,20 +35790,6 @@ sub sftp_config_rekey_short_timeout_failed {
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x (1024 * 1024);
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_sz = (stat($test_file))[7];
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -26743,7 +35797,7 @@ sub sftp_config_rekey_short_timeout_failed {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_idle = 20;
+  my $timeout_idle = 5;
 
   my $config = {
     PidFile => $pid_file,
@@ -26766,8 +35820,6 @@ sub sftp_config_rekey_short_timeout_failed {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPRekey required 3600 1 1",
       ],
     },
   };
@@ -26796,7 +35848,7 @@ sub sftp_config_rekey_short_timeout_failed {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(1);
+      sleep(2);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -26808,46 +35860,54 @@ sub sftp_config_rekey_short_timeout_failed {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
+      # Wait for more than the idle period
+      sleep($timeout_idle + 2);
+
       my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      if ($sftp) {
+        die("SFTP subsystem started unexpectedly");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
 
-      my $buf;
-      my $size = 0;
-      my $buflen = (1024 * 250);
+      my $expected = 'LIBSSH2_ERROR_CHANNEL_FAILURE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      my $res = $fh->read($buf, $buflen);
-      while ($res) {
-        $size += $res;
-        sleep(1);
+      # Try again, this time hitting the TimeoutIdle in the middle of an
+      # SFTP session
 
-        $res = $fh->read($buf, $buflen);
+      $ssh2 = Net::SSH2->new();
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+      $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      $ssh2->disconnect();
+      # Wait for more than the idle period
+      sleep($timeout_idle + 2);
 
-      # The only way we really have, given Net::SSH2's API, to detect
-      # whether the server disconnected us in mid-download is to check the
-      # number of bytes we downloaded, to see if it's the full size.
-      #
-      # With such a short rekey timeout (1 sec), it's not expected that we
-      # have all of the bytes.
-      $self->assert($test_sz != $size,
-        test_msg("Expected !$test_sz, got $size"));
+      my $cwd = $sftp->realpath('.');
+      if ($cwd) {
+        die("FXP_REALPATH succeeded unexpectedly");
+      }
+
+      ($err_code, $err_name, $err_str) = $ssh2->error();
+
+      $expected = 'LIBSSH2_ERROR_SOCKET_TIMEOUT';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
     };
 
     if ($@) {
@@ -26858,7 +35918,7 @@ sub sftp_config_rekey_short_timeout_failed {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout_idle + 3) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -26882,7 +35942,7 @@ sub sftp_config_rekey_short_timeout_failed {
   unlink($log_file);
 }
 
-sub sftp_config_rekey_long_timeout_ok {
+sub sftp_config_timeoutlogin {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -26914,20 +35974,6 @@ sub sftp_config_rekey_long_timeout_ok {
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x (1024 * 1024);
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_sz = (stat($test_file))[7];
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -26935,7 +35981,7 @@ sub sftp_config_rekey_long_timeout_ok {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_idle = 20;
+  my $timeout_login = 2;
 
   my $config = {
     PidFile => $pid_file,
@@ -26946,7 +35992,7 @@ sub sftp_config_rekey_long_timeout_ok {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutIdle => $timeout_idle,
+    TimeoutLogin => $timeout_login,
 
     IfModules => {
       'mod_delay.c' => {
@@ -26958,8 +36004,6 @@ sub sftp_config_rekey_long_timeout_ok {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPRekey required 3600 1 15",
       ],
     },
   };
@@ -26995,51 +36039,18 @@ sub sftp_config_rekey_long_timeout_ok {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $buf;
-      my $size = 0;
-      my $buflen = (1024 * 250);
-
-      my $res = $fh->read($buf, $buflen);
-      while ($res) {
-        $size += $res;
-        sleep(1);
+      # Wait for more than the login period
+      sleep($timeout_login + 2);
 
-        $res = $fh->read($buf, $buflen);
+      if ($ssh2->auth_password($user, $passwd)) {
+        die("SSH2 login succeeded unexpectedly");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
-
-      $ssh2->disconnect();
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
 
-      # The only way we really have, given Net::SSH2's API, to detect
-      # whether the server disconnected us in mid-download is to check the
-      # number of bytes we downloaded, to see if it's the full size.
-      #
-      # With such long rekey timeout (15 sec), it is expected that we have
-      # all of the bytes.
-      $self->assert($test_sz == $size,
-        test_msg("Expected $test_sz, got $size"));
+      my $expected = '(LIBSSH2_ERROR_SOCKET_DISCONNECT|LIBSSH2_ERROR_TIMEOUT)';
+      $self->assert(qr/$expected/, $err_name,
+        "Expected '$expected', got '$err_name'");
     };
 
     if ($@) {
@@ -27050,7 +36061,7 @@ sub sftp_config_rekey_long_timeout_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout_idle + 3) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -27074,7 +36085,7 @@ sub sftp_config_rekey_long_timeout_ok {
   unlink($log_file);
 }
 
-sub sftp_config_rootlogin {
+sub sftp_config_timeoutnotransfer_download {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -27091,8 +36102,8 @@ sub sftp_config_rootlogin {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 0;
-  my $gid = 0;
+  my $uid = 500;
+  my $gid = 500;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -27113,6 +36124,8 @@ sub sftp_config_rootlogin {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $timeout_noxfer = 2;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -27122,8 +36135,7 @@ sub sftp_config_rootlogin {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-
-    RootLogin => 'off',
+    TimeoutNoTransfer => $timeout_noxfer,
 
     IfModules => {
       'mod_delay.c' => {
@@ -27170,9 +36182,39 @@ sub sftp_config_rootlogin {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_password($user, $passwd)) {
-        die("Unexpectedly logged in to SSH2 server");
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $cwd = $sftp->realpath('.');
+      unless ($cwd) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't get real path for '.': [$err_name] ($err_code)");
+      }
+
+      # Wait for more than the no-transfer period
+      sleep($timeout_noxfer + 2);
+
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      if ($fh) {
+        die("FXP_OPEN succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+
+      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -27183,7 +36225,7 @@ sub sftp_config_rootlogin {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -27207,7 +36249,7 @@ sub sftp_config_rootlogin {
   unlink($log_file);
 }
 
-sub sftp_config_protocols {
+sub sftp_config_timeoutnotransfer_readdir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -27246,6 +36288,8 @@ sub sftp_config_protocols {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $timeout_noxfer = 2;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -27255,6 +36299,7 @@ sub sftp_config_protocols {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    TimeoutNoTransfer => $timeout_noxfer,
 
     IfModules => {
       'mod_delay.c' => {
@@ -27266,7 +36311,6 @@ sub sftp_config_protocols {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-        "Protocols scp",
       ],
     },
   };
@@ -27304,20 +36348,36 @@ sub sftp_config_protocols {
 
       unless ($ssh2->auth_password($user, $passwd)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
       my $sftp = $ssh2->sftp();
-      if ($sftp) {
-        die("SFTP subsystem started unexpectedly");
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $cwd = $sftp->realpath('.');
+      unless ($cwd) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't get real path for '.': [$err_name] ($err_code)");
+      }
+
+      # Wait for more than the no-transfer period
+      sleep($timeout_noxfer + 2);
+
+      my $dir = $sftp->opendir('.');
+      if ($dir) {
+        die("FXP_OPENDIR succeeded unexpectedly");
       }
 
       my ($err_code, $err_name, $err_str) = $ssh2->error();
 
-      my $expected = 'LIBSSH2_ERROR_CHANNEL_FAILURE';
+      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
       $self->assert($expected eq $err_name,
         test_msg("Expected '$expected', got '$err_name'"));
 
+      $sftp = undef;
       $ssh2->disconnect();
     };
 
@@ -27329,7 +36389,7 @@ sub sftp_config_protocols {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -27353,7 +36413,7 @@ sub sftp_config_protocols {
   unlink($log_file);
 }
 
-sub sftp_config_serverident_off {
+sub sftp_config_timeoutnotransfer_upload {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -27392,6 +36452,8 @@ sub sftp_config_serverident_off {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $timeout_noxfer = 2;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -27401,7 +36463,7 @@ sub sftp_config_serverident_off {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    ServerIdent => 'off',
+    TimeoutNoTransfer => $timeout_noxfer,
 
     IfModules => {
       'mod_delay.c' => {
@@ -27413,10 +36475,6 @@ sub sftp_config_serverident_off {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        # Enable this, so that the Telnet connection does not receive the
-        # KEXINIT data upon connect.
-        "SFTPOptions PessimisticKexinit",
       ],
     },
   };
@@ -27432,7 +36490,6 @@ sub sftp_config_serverident_off {
   }
 
   require Net::SSH2;
-  require Net::Telnet;
 
   my $ex;
 
@@ -27446,7 +36503,7 @@ sub sftp_config_serverident_off {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(2);
+      sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -27458,30 +36515,34 @@ sub sftp_config_serverident_off {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->disconnect('Done with integration test')) {
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      $ssh2 = undef;
+      my $cwd = $sftp->realpath('.');
+      unless ($cwd) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't get real path for '.': [$err_name] ($err_code)");
+      }
 
-      # Now connect again, this time with a Telnet client, to get the
-      # SSH version identification string.  The libssh2 API doesn't provide
-      # a way to get that string, and thus neither does Net::SSH2.
-      my $telnet = Net::Telnet->new(
-        Host => '127.0.0.1',
-        Port => $port,
-        Timeout => 3,
-        Errmode => 'return',
-      );
+      # Wait for more than the no-transfer period
+      sleep($timeout_noxfer + 2);
 
-      my $version_id = $telnet->getline();
-      chomp($version_id);
-      $telnet->close();
+      my $fh = $sftp->open('test.txt', O_CREAT|O_WRONLY);
+      if ($fh) {
+        die("FXP_OPEN succeeded unexpectedly");
+      }
 
-      my $expected = 'SSH-2.0-mod_sftp';
-      $self->assert($version_id eq $expected,
-        test_msg("Expected SSH version identification '$expected', received '$version_id'"));
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+
+      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -27492,7 +36553,7 @@ sub sftp_config_serverident_off {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -27516,7 +36577,7 @@ sub sftp_config_serverident_off {
   unlink($log_file);
 }
 
-sub sftp_config_serverident_on {
+sub sftp_config_timeoutstalled {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -27555,6 +36616,20 @@ sub sftp_config_serverident_on {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $timeout_stalled = 2;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 32768;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -27564,7 +36639,7 @@ sub sftp_config_serverident_on {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    ServerIdent => 'on',
+    TimeoutStalled => $timeout_stalled,
 
     IfModules => {
       'mod_delay.c' => {
@@ -27576,10 +36651,6 @@ sub sftp_config_serverident_on {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        # Enable this, so that the Telnet connection does not receive the
-        # KEXINIT data upon connect.
-        "SFTPOptions PessimisticKexinit",
       ],
     },
   };
@@ -27595,7 +36666,6 @@ sub sftp_config_serverident_on {
   }
 
   require Net::SSH2;
-  require Net::Telnet;
 
   my $ex;
 
@@ -27609,7 +36679,7 @@ sub sftp_config_serverident_on {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(2);
+      sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -27621,30 +36691,39 @@ sub sftp_config_serverident_on {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->disconnect('Done with integration test')) {
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      $ssh2 = undef;
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
 
-      # Now connect again, this time with a Telnet client, to get the
-      # SSH version identification string.  The libssh2 API doesn't provide
-      # a way to get that string, and thus neither does Net::SSH2.
-      my $telnet = Net::Telnet->new(
-        Host => '127.0.0.1',
-        Port => $port,
-        Timeout => 3,
-        Errmode => 'return',
-      );
+      my $buf;
+      my $size = 0;
 
-      my $version_id = $telnet->getline();
-      chomp($version_id);
-      $telnet->close();
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
 
-      my $expected = 'SSH-2.0-mod_sftp/\d*\.\d*\.\d*';
-      $self->assert(qr/$expected/, $version_id,
-        test_msg("Expected SSH version identification '$expected', received '$version_id'"));
+        # Sleep for longer than the TimeoutStalled period
+        sleep($timeout_stalled + 2);
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+
+      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
+
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -27655,7 +36734,7 @@ sub sftp_config_serverident_on {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -27679,7 +36758,7 @@ sub sftp_config_serverident_on {
   unlink($log_file);
 }
 
-sub sftp_config_serverident_on_custom {
+sub sftp_config_ignore_upload_perms_upload {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -27718,7 +36797,7 @@ sub sftp_config_serverident_on_custom {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $custom_id = "OpenSSH_5.6p1";
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
 
   my $config = {
     PidFile => $pid_file,
@@ -27729,7 +36808,6 @@ sub sftp_config_serverident_on_custom {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    ServerIdent => "on $custom_id",
 
     IfModules => {
       'mod_delay.c' => {
@@ -27742,9 +36820,7 @@ sub sftp_config_serverident_on_custom {
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
 
-        # Enable this, so that the Telnet connection does not receive the
-        # KEXINIT data upon connect.
-        "SFTPOptions PessimisticKexinit",
+        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
@@ -27760,7 +36836,6 @@ sub sftp_config_serverident_on_custom {
   }
 
   require Net::SSH2;
-  require Net::Telnet;
 
   my $ex;
 
@@ -27774,7 +36849,7 @@ sub sftp_config_serverident_on_custom {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(2);
+      sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -27786,30 +36861,30 @@ sub sftp_config_serverident_on_custom {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->disconnect('Done with integration test')) {
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      $ssh2 = undef;
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0666);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
 
-      # Now connect again, this time with a Telnet client, to get the
-      # SSH version identification string.  The libssh2 API doesn't provide
-      # a way to get that string, and thus neither does Net::SSH2.
-      my $telnet = Net::Telnet->new(
-        Host => '127.0.0.1',
-        Port => $port,
-        Timeout => 3,
-        Errmode => 'return',
-      );
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
+      }
 
-      my $version_id = $telnet->getline();
-      chomp($version_id);
-      $telnet->close();
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
 
-      my $expected = 'SSH-2.0-' . $custom_id;
-      $self->assert($version_id eq $expected,
-        test_msg("Expected SSH version identification '$expected', received '$version_id'"));
+      # To close the SFTP channel, we have to explicitly destroy the object
+      $sftp = undef;
+
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -27841,10 +36916,16 @@ sub sftp_config_serverident_on_custom {
     die($ex);
   }
 
+  my $perms = ((stat($test_file))[2] & 07777);
+
+  my $expected = 0644;
+  $self->assert($expected == $perms,
+    test_msg("Expected '$expected', got '$perms'"));
+  
   unlink($log_file);
 }
 
-sub sftp_config_timeoutidle {
+sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -27883,7 +36964,7 @@ sub sftp_config_timeoutidle {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_idle = 5;
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
 
   my $config = {
     PidFile => $pid_file,
@@ -27894,7 +36975,6 @@ sub sftp_config_timeoutidle {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutIdle => $timeout_idle,
 
     IfModules => {
       'mod_delay.c' => {
@@ -27906,6 +36986,8 @@ sub sftp_config_timeoutidle {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
@@ -27934,7 +37016,7 @@ sub sftp_config_timeoutidle {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(2);
+      sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -27946,54 +37028,20 @@ sub sftp_config_timeoutidle {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Wait for more than the idle period
-      sleep($timeout_idle + 2);
-
       my $sftp = $ssh2->sftp();
-      if ($sftp) {
-        die("SFTP subsystem started unexpectedly");
-      }
-
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected = 'LIBSSH2_ERROR_CHANNEL_FAILURE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
-      # Try again, this time hitting the TimeoutIdle in the middle of an
-      # SFTP session
-
-      $ssh2 = Net::SSH2->new();
-
-      unless ($ssh2->connect('127.0.0.1', $port)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      $sftp = $ssh2->sftp();
       unless ($sftp) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Wait for more than the idle period
-      sleep($timeout_idle + 2);
-
-      my $cwd = $sftp->realpath('.');
-      if ($cwd) {
-        die("FXP_REALPATH succeeded unexpectedly");
+      my $res = $sftp->mkdir('testdir', 0511);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't mkdir testdir: [$err_name] ($err_code)");
       }
 
-      ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      $expected = 'LIBSSH2_ERROR_SOCKET_TIMEOUT';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -28004,7 +37052,7 @@ sub sftp_config_timeoutidle {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -28025,10 +37073,20 @@ sub sftp_config_timeoutidle {
     die($ex);
   }
 
+  unless (-d $test_dir) {
+    die("$test_dir directory does not exist as expected");
+  }
+
+  my $perms = ((stat($test_dir))[2] & 07777);
+
+  my $expected = 0755;
+  $self->assert($expected == $perms,
+    test_msg("Expected '$expected', got '$perms'"));
+  
   unlink($log_file);
 }
 
-sub sftp_config_timeoutlogin {
+sub sftp_config_ignore_set_perms_bug3599 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -28067,7 +37125,17 @@ sub sftp_config_timeoutlogin {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_login = 2;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+ 
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
@@ -28078,7 +37146,6 @@ sub sftp_config_timeoutlogin {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutLogin => $timeout_login,
 
     IfModules => {
       'mod_delay.c' => {
@@ -28090,6 +37157,8 @@ sub sftp_config_timeoutlogin {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPOptions IgnoreSFTPSetPerms",
       ],
     },
   };
@@ -28125,18 +37194,33 @@ sub sftp_config_timeoutlogin {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Wait for more than the login period
-      sleep($timeout_login + 2);
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      if ($ssh2->auth_password($user, $passwd)) {
-        die("SSH2 login succeeded unexpectedly");
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
+      my $res = $sftp->setstat('test.txt',
+        mode => 0666,
+      );
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't setstat sftp.conf: [$err_name] ($err_code)");
+      }
 
-      my $expected = 'LIBSSH2_ERROR_TIMEOUT';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      my $attrs = $sftp->stat('test.txt');
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_STAT test.txt failed: [$err_name] ($err_code)");
+      }
+
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -28147,7 +37231,7 @@ sub sftp_config_timeoutlogin {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -28168,10 +37252,16 @@ sub sftp_config_timeoutlogin {
     die($ex);
   }
 
+  my $perms = ((stat($test_file))[2] & 07777);
+
+  my $expected = 0644;
+  $self->assert($expected == $perms,
+    test_msg("Expected '$expected', got '$perms'"));
+  
   unlink($log_file);
 }
 
-sub sftp_config_timeoutnotransfer_download {
+sub sftp_config_ignore_set_times_bug3706 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -28210,7 +37300,19 @@ sub sftp_config_timeoutnotransfer_download {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_noxfer = 2;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+ 
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my ($test_atime, $test_mtime) = (stat($test_file))[8, 9];
 
   my $config = {
     PidFile => $pid_file,
@@ -28221,7 +37323,6 @@ sub sftp_config_timeoutnotransfer_download {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutNoTransfer => $timeout_noxfer,
 
     IfModules => {
       'mod_delay.c' => {
@@ -28233,6 +37334,8 @@ sub sftp_config_timeoutnotransfer_download {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPOptions IgnoreSFTPSetTimes",
       ],
     },
   };
@@ -28279,26 +37382,21 @@ sub sftp_config_timeoutnotransfer_download {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $cwd = $sftp->realpath('.');
-      unless ($cwd) {
+      my $res = $sftp->setstat('test.txt',
+        atime => 0,
+        mtime => 0,
+      );
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for '.': [$err_name] ($err_code)");
+        die("Can't setstat test.txt: [$err_name] ($err_code)");
       }
 
-      # Wait for more than the no-transfer period
-      sleep($timeout_noxfer + 2);
-
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      if ($fh) {
-        die("FXP_OPEN succeeded unexpectedly");
+      my $attrs = $sftp->stat('test.txt');
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_STAT test.txt failed: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -28311,7 +37409,7 @@ sub sftp_config_timeoutnotransfer_download {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -28332,10 +37430,20 @@ sub sftp_config_timeoutnotransfer_download {
     die($ex);
   }
 
+  my ($new_atime, $new_mtime) = (stat($test_file))[8, 9];
+
+  my $expected = $test_atime;
+  $self->assert($expected == $new_atime,
+    test_msg("Expected atime $expected, got $new_atime"));
+
+  $expected = $test_mtime;
+  $self->assert($expected == $new_mtime,
+    test_msg("Expected mtime $expected, got $new_mtime"));
+  
   unlink($log_file);
 }
 
-sub sftp_config_timeoutnotransfer_readdir {
+sub sftp_config_ignore_set_owners_bug3757 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -28374,7 +37482,19 @@ sub sftp_config_timeoutnotransfer_readdir {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_noxfer = 2;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+ 
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my ($test_uid, $test_gid) = (stat($test_file))[4, 5];
 
   my $config = {
     PidFile => $pid_file,
@@ -28385,7 +37505,6 @@ sub sftp_config_timeoutnotransfer_readdir {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutNoTransfer => $timeout_noxfer,
 
     IfModules => {
       'mod_delay.c' => {
@@ -28397,6 +37516,8 @@ sub sftp_config_timeoutnotransfer_readdir {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
+
+        "SFTPOptions IgnoreSFTPSetOwners",
       ],
     },
   };
@@ -28443,26 +37564,21 @@ sub sftp_config_timeoutnotransfer_readdir {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $cwd = $sftp->realpath('.');
-      unless ($cwd) {
+      my $res = $sftp->setstat('test.txt',
+        uid => 0,
+        gid => 0,
+      );
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for '.': [$err_name] ($err_code)");
+        die("Can't setstat test.txt: [$err_name] ($err_code)");
       }
 
-      # Wait for more than the no-transfer period
-      sleep($timeout_noxfer + 2);
-
-      my $dir = $sftp->opendir('.');
-      if ($dir) {
-        die("FXP_OPENDIR succeeded unexpectedly");
+      my $attrs = $sftp->stat('test.txt');
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_STAT test.txt failed: [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -28475,7 +37591,7 @@ sub sftp_config_timeoutnotransfer_readdir {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -28496,10 +37612,20 @@ sub sftp_config_timeoutnotransfer_readdir {
     die($ex);
   }
 
+  my ($new_uid, $new_gid) = (stat($test_file))[4, 5];
+
+  my $expected = $test_uid;
+  $self->assert($expected == $new_uid,
+    test_msg("Expected uid $expected, got $new_uid"));
+
+  $expected = $test_gid;
+  $self->assert($expected == $new_gid,
+    test_msg("Expected gid $expected, got $new_gid"));
+  
   unlink($log_file);
 }
 
-sub sftp_config_timeoutnotransfer_upload {
+sub sftp_config_userowner {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -28531,14 +37657,19 @@ sub sftp_config_timeoutnotransfer_upload {
     }
   }
 
+  my $owner = 'proftpd2';
+  my $owner_uid = 7777;
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
+  auth_user_write($auth_user_file, $owner, 'none', $owner_uid, $gid, $home_dir,
+    '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_noxfer = 2;
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
 
   my $config = {
     PidFile => $pid_file,
@@ -28549,7 +37680,13 @@ sub sftp_config_timeoutnotransfer_upload {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutNoTransfer => $timeout_noxfer,
+    RootRevoke => 'off',
+
+    Directory => {
+      '~' => {
+        UserOwner => $owner,
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -28607,28 +37744,32 @@ sub sftp_config_timeoutnotransfer_upload {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $cwd = $sftp->realpath('.');
-      unless ($cwd) {
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for '.': [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      # Wait for more than the no-transfer period
-      sleep($timeout_noxfer + 2);
-
-      my $fh = $sftp->open('test.txt', O_CREAT|O_WRONLY);
-      if ($fh) {
-        die("FXP_OPEN succeeded unexpectedly");
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
 
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+
+      my $owning_uid = (stat($test_file))[4];
+      $self->assert($owner_uid == $owning_uid,
+        test_msg("Expected $owner_uid, got $owning_uid"));
     };
 
     if ($@) {
@@ -28639,7 +37780,7 @@ sub sftp_config_timeoutnotransfer_upload {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -28663,7 +37804,7 @@ sub sftp_config_timeoutnotransfer_upload {
   unlink($log_file);
 }
 
-sub sftp_config_timeoutstalled {
+sub sftp_config_groupowner_file_nonmember {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -28695,26 +37836,18 @@ sub sftp_config_timeoutstalled {
     }
   }
 
+  my $owner = 'proftpd2';
+  my $owner_gid = 7777;
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
+  auth_group_write($auth_group_file, $owner, $owner_gid, 'foo');
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $timeout_stalled = 2;
-
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCDefgh" x 32768;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
 
   my $config = {
     PidFile => $pid_file,
@@ -28725,7 +37858,13 @@ sub sftp_config_timeoutstalled {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TimeoutStalled => $timeout_stalled,
+#    RootRevoke => 'off',
+
+    Directory => {
+      '~' => {
+        GroupOwner => $owner,
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -28783,33 +37922,32 @@ sub sftp_config_timeoutstalled {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
-
-        # Sleep for longer than the TimeoutStalled period
-        sleep($timeout_stalled + 2);
-
-        $res = $fh->read($buf, 8192);
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected = 'LIBSSH2_ERROR_SOCKET_NONE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
 
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+
+      my $owning_gid = (stat($test_file))[5];
+      $self->assert($owner_gid == $owning_gid,
+        test_msg("Expected $owner_gid, got $owning_gid"));
     };
 
     if ($@) {
@@ -28820,7 +37958,7 @@ sub sftp_config_timeoutstalled {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -28844,10 +37982,28 @@ sub sftp_config_timeoutstalled {
   unlink($log_file);
 }
 
-sub sftp_config_ignore_upload_perms_upload {
+sub sftp_config_groupowner_file_member_norootprivs {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
+  my ($config_user, $config_group) = config_get_identity();
+
+  my $members = [split(' ', (getgrnam($config_group))[3])];
+  if (scalar(@$members) < 2) {
+    print STDERR " + unable to run 'sftp_config_groupowner_member_norootprivs' test without current user belonging to multiple groups, skipping\n";
+    return;
+  }
+
+  my ($uid, $gid) = (getpwnam($members->[0]))[2,3];
+
+  my $root_login = 'off';
+  if ($uid == 0) {
+    $root_login = 'on';
+  }
+
+  my $owner = 'proftpd2';
+  my $owner_gid = (getpwnam($members->[1]))[3];
+
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
@@ -28861,8 +38017,6 @@ sub sftp_config_ignore_upload_perms_upload {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -28879,6 +38033,7 @@ sub sftp_config_ignore_upload_perms_upload {
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
+  auth_group_write($auth_group_file, $owner, $owner_gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
@@ -28894,6 +38049,13 @@ sub sftp_config_ignore_upload_perms_upload {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    RootLogin => $root_login,
+
+    Directory => {
+      '~' => {
+        GroupOwner => $owner,
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -28905,13 +38067,12 @@ sub sftp_config_ignore_upload_perms_upload {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my $port;
+  ($port, $config_user, $config_group) = config_write($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
@@ -28953,7 +38114,7 @@ sub sftp_config_ignore_upload_perms_upload {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0666);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't open test.txt: [$err_name] ($err_code)");
@@ -28971,6 +38132,14 @@ sub sftp_config_ignore_upload_perms_upload {
       $sftp = undef;
 
       $ssh2->disconnect();
+
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+
+      my $owning_gid = (stat($test_file))[5];
+      $self->assert($owner_gid == $owning_gid,
+        test_msg("Expected $owner_gid, got $owning_gid"));
     };
 
     if ($@) {
@@ -29002,19 +38171,31 @@ sub sftp_config_ignore_upload_perms_upload {
     die($ex);
   }
 
-  my $perms = ((stat($test_file))[2] & 07777);
-
-  my $expected = 0644;
-  $self->assert($expected == $perms,
-    test_msg("Expected '$expected', got '$perms'"));
-  
   unlink($log_file);
 }
 
-sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
+sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
+  my ($config_user, $config_group) = config_get_identity();
+
+  my $members = [split(' ', (getgrnam($config_group))[3])];
+  if (scalar(@$members) < 2) {
+    print STDERR " + unable to run 'sftp_config_groupowner_member_norootprivs' test without current user belonging to multiple groups, skipping\n";
+    return;
+  }
+
+  my ($uid, $gid) = (getpwnam($members->[0]))[2,3];
+
+  my $root_login = 'off';
+  if ($uid == 0) {
+    $root_login = 'on';
+  }
+
+  my $owner = 'proftpd2';
+  my $owner_gid = (getpwnam($members->[1]))[3];
+
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
@@ -29028,8 +38209,6 @@ sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -29046,6 +38225,7 @@ sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
+  auth_group_write($auth_group_file, $owner, $owner_gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
@@ -29061,6 +38241,13 @@ sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultChdir => '~',
+
+    Directory => {
+      '~' => {
+        GroupOwner => $owner,
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -29072,13 +38259,12 @@ sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPOptions IgnoreSFTPUploadPerms",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my $port;
+  ($port, $config_user, $config_group) = config_write($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
@@ -29120,14 +38306,24 @@ sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->mkdir('testdir', 0511);
+      my $res = $sftp->mkdir('testdir');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't mkdir testdir: [$err_name] ($err_code)");
       }
 
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      unless (-d $test_dir) {
+        die("Directory $test_dir does not exist as expected");
+      }
+
+      my $owning_gid = (stat($test_dir))[5];
+      $self->assert($owner_gid == $owning_gid,
+        test_msg("Expected owner GID $owner_gid, got $owning_gid"));
     };
 
     if ($@) {
@@ -29159,20 +38355,10 @@ sub sftp_config_ignore_upload_perms_mkdir_bug3680 {
     die($ex);
   }
 
-  unless (-d $test_dir) {
-    die("$test_dir directory does not exist as expected");
-  }
-
-  my $perms = ((stat($test_dir))[2] & 07777);
-
-  my $expected = 0755;
-  $self->assert($expected == $perms,
-    test_msg("Expected '$expected', got '$perms'"));
-  
   unlink($log_file);
 }
 
-sub sftp_config_ignore_set_perms_bug3599 {
+sub sftp_config_ftpaccess_bug3460 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -29192,33 +38378,99 @@ sub sftp_config_ignore_set_perms_bug3599 {
   my $uid = 500;
   my $gid = 500;
 
+  my $data_dir = File::Spec->rel2abs("$home_dir/data");
+  mkpath($data_dir);
+
+  my $ftpaccess_file = File::Spec->rel2abs("$data_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOF;
+<Limit READ WRITE>
+  IgnoreHidden on
+  DenyAll
+</Limit>
+EOF
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  my $neither_dir = File::Spec->rel2abs("$data_dir/neither");
+  mkpath($neither_dir);
+
+  my $readable_dir = File::Spec->rel2abs("$data_dir/readable");
+  mkpath($readable_dir);
+
+  $ftpaccess_file = File::Spec->rel2abs("$readable_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOF;
+<Limit READ>
+  AllowUser $user
+  AllowUser foo
+  DenyAll
+</Limit>
+EOF
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  my $writable_dir = File::Spec->rel2abs("$data_dir/writable");
+  mkpath($writable_dir);
+
+  $ftpaccess_file = File::Spec->rel2abs("$writable_dir/.ftpaccess");
+  if (open(my $fh, "> $ftpaccess_file")) {
+    print $fh <<EOF;
+<Limit WRITE READ>
+  AllowUser $user
+  AllowUser foo
+  DenyAll
+</Limit>
+EOF
+    unless (close($fh)) {
+      die("Can't write $ftpaccess_file: $!");
+    }
+
+  } else {
+    die("Can't open $ftpaccess_file: $!");
+  }
+
+  my $sub_writable_dir = File::Spec->rel2abs("$writable_dir/subwritable");
+  mkpath($sub_writable_dir);
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
+    unless (chmod(0755, $home_dir, $data_dir, $neither_dir, $readable_dir,
+        $writable_dir, $sub_writable_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
+    unless (chown($uid, $gid, $home_dir, $data_dir, $neither_dir,
+        $readable_dir, $writable_dir, $sub_writable_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $data_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_file = File::Spec->rel2abs("$sub_writable_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
+    print $fh "ABCD\n";
     unless (close($fh)) {
       die("Can't write $test_file: $!");
     }
- 
+
   } else {
     die("Can't open $test_file: $!");
   }
@@ -29228,11 +38480,21 @@ sub sftp_config_ignore_set_perms_bug3599 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 directory:20 ftpaccess:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    AllowOverride => 'on',
+    PathDenyFilter => '"\\\\.ftpaccess$"',
+
+    Directory => {
+      '/*' => {
+        AllowOverwrite => 'on',
+        HideUser => 'nobody',
+      },
+    },
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -29243,10 +38505,14 @@ sub sftp_config_ignore_set_perms_bug3599 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPOptions IgnoreSFTPSetPerms",
       ],
     },
+
+    Limit => {
+      'LOCK SYMLINK' => {
+        DenyAll => '',
+      },
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -29291,20 +38557,39 @@ sub sftp_config_ignore_set_perms_bug3599 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->setstat('test.txt',
-        mode => 0666,
-      );
-      unless ($res) {
+      my $base_path = $sftp->realpath('.');
+      unless ($base_path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't setstat sftp.conf: [$err_name] ($err_code)");
+        die("FXP_REALPATH '.' failed: [$err_name] ($err_code)");
       }
 
-      my $attrs = $sftp->stat('test.txt');
-      unless ($attrs) {
+      my $path = $sftp->realpath("$base_path/writable/subwritable");
+      unless ($path) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT test.txt failed: [$err_name] ($err_code)");
+        die("FXP_REALPATH '$base_path/writable/subwritable' failed: [$err_name] ($err_code)");
       }
 
+      my $dirh = $sftp->opendir($path);
+      unless ($dirh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_OPENDIR '$path' failed: [$err_name] ($err_code)");
+      }
+
+      $dirh = undef;
+
+      my $file_path = $sftp->realpath("$path/test.txt");
+      unless ($file_path) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_REALPATH '$path/test.txt' failed: [$err_name] ($err_code)");
+      }
+
+      my $fh = $sftp->open($file_path, O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_OPEN '$file_path' failed: [$err_name] ($err_code)");
+      }
+
+      $fh = undef;
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -29338,16 +38623,10 @@ sub sftp_config_ignore_set_perms_bug3599 {
     die($ex);
   }
 
-  my $perms = ((stat($test_file))[2] & 07777);
-
-  my $expected = 0644;
-  $self->assert($expected == $perms,
-    test_msg("Expected '$expected', got '$perms'"));
-  
   unlink($log_file);
 }
 
-sub sftp_config_ignore_set_times_bug3706 {
+sub sftp_config_limit_appe {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -29388,18 +38667,15 @@ sub sftp_config_ignore_set_times_bug3706 {
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
+    print $fh "ABCD\n";
     unless (close($fh)) {
       die("Can't write $test_file: $!");
     }
- 
+
   } else {
     die("Can't open $test_file: $!");
   }
 
-  my ($test_atime, $test_mtime) = (stat($test_file))[8, 9];
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -29410,6 +38686,13 @@ sub sftp_config_ignore_set_times_bug3706 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    Directory => {
+      '~' => {
+        AllowOverwrite => 'on',
+        AllowStoreRestart => 'on',
+      },
+    },
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -29420,10 +38703,14 @@ sub sftp_config_ignore_set_times_bug3706 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPOptions IgnoreSFTPSetTimes",
       ],
     },
+
+    Limit => {
+      APPE => {
+        DenyAll => '',
+      },
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -29468,23 +38755,19 @@ sub sftp_config_ignore_set_times_bug3706 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->setstat('test.txt',
-        atime => 0,
-        mtime => 0,
-      );
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't setstat test.txt: [$err_name] ($err_code)");
-      }
-
-      my $attrs = $sftp->stat('test.txt');
-      unless ($attrs) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT test.txt failed: [$err_name] ($err_code)");
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_APPEND);
+      if ($fh) {
+        $fh = undef;
+        die("OPEN test.txt succeeded unexpectedly");
       }
 
+      my ($err_code, $err_name) = $sftp->error();
       $sftp = undef;
       $ssh2->disconnect();
+
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
     };
 
     if ($@) {
@@ -29516,20 +38799,10 @@ sub sftp_config_ignore_set_times_bug3706 {
     die($ex);
   }
 
-  my ($new_atime, $new_mtime) = (stat($test_file))[8, 9];
-
-  my $expected = $test_atime;
-  $self->assert($expected == $new_atime,
-    test_msg("Expected atime $expected, got $new_atime"));
-
-  $expected = $test_mtime;
-  $self->assert($expected == $new_mtime,
-    test_msg("Expected mtime $expected, got $new_mtime"));
-  
   unlink($log_file);
 }
 
-sub sftp_config_ignore_set_owners_bug3757 {
+sub sftp_config_limit_chmod {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -29568,20 +38841,6 @@ sub sftp_config_ignore_set_owners_bug3757 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
- 
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my ($test_uid, $test_gid) = (stat($test_file))[4, 5];
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -29602,10 +38861,14 @@ sub sftp_config_ignore_set_owners_bug3757 {
         "SFTPLog $log_file",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
-
-        "SFTPOptions IgnoreSFTPSetOwners",
       ],
     },
+
+    Limit => {
+      'SITE_CHMOD' => {
+        DenyAll => '',
+      },
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -29622,9 +38885,6 @@ sub sftp_config_ignore_set_owners_bug3757 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -29649,24 +38909,29 @@ sub sftp_config_ignore_set_owners_bug3757 {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $res = $sftp->setstat('test.txt',
-        uid => 0,
-        gid => 0,
+     
+      my $res = $sftp->setstat('sftp.conf',
+        mode => 0777,
       );
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't setstat test.txt: [$err_name] ($err_code)");
+      if ($res) {
+        die("setstat sftp.conf succeeded unexpectly");
       }
 
-      my $attrs = $sftp->stat('test.txt');
+      my $attrs = $sftp->stat('sftp.conf');
       unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT test.txt failed: [$err_name] ($err_code)");
+        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
+
+      my $expected;
+
+      $expected = 0644;
+      my $file_mode = ($attrs->{mode} & 0777);
+      $self->assert($expected == $file_mode,
+        test_msg("Expected '$expected', got '$file_mode'"));
     };
 
     if ($@) {
@@ -29698,20 +38963,10 @@ sub sftp_config_ignore_set_owners_bug3757 {
     die($ex);
   }
 
-  my ($new_uid, $new_gid) = (stat($test_file))[4, 5];
-
-  my $expected = $test_uid;
-  $self->assert($expected == $new_uid,
-    test_msg("Expected uid $expected, got $new_uid"));
-
-  $expected = $test_gid;
-  $self->assert($expected == $new_gid,
-    test_msg("Expected gid $expected, got $new_gid"));
-  
   unlink($log_file);
 }
 
-sub sftp_config_userowner {
+sub sftp_config_limit_chgrp_bug3757 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -29743,20 +38998,13 @@ sub sftp_config_userowner {
     }
   }
 
-  my $owner = 'proftpd2';
-  my $owner_uid = 7777;
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_user_write($auth_user_file, $owner, 'none', $owner_uid, $gid, $home_dir,
-    '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -29766,13 +39014,6 @@ sub sftp_config_userowner {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    RootRevoke => 'off',
-
-    Directory => {
-      '~' => {
-        UserOwner => $owner,
-      },
-    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -29786,10 +39027,18 @@ sub sftp_config_userowner {
         "SFTPHostKey $dsa_host_key",
       ],
     },
+
+    Limit => {
+      'SITE_CHGRP' => {
+        DenyAll => '',
+      },
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  my ($test_uid, $test_gid) = (stat($config_file))[4, 5];
+
   # 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.
@@ -29802,9 +39051,6 @@ sub sftp_config_userowner {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -29829,33 +39075,23 @@ sub sftp_config_userowner {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+     
+      my $res = $sftp->setstat('sftp.conf',
+        uid => $uid,
+        gid => $gid,
+      );
+      if ($res) {
+        die("setstat sftp.conf succeeded unexpectly");
       }
 
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
+      my $attrs = $sftp->stat('sftp.conf');
+      unless ($attrs) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
-
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
-      }
-
-      my $owning_uid = (stat($test_file))[4];
-      $self->assert($owner_uid == $owning_uid,
-        test_msg("Expected $owner_uid, got $owning_uid"));
     };
 
     if ($@) {
@@ -29875,6 +39111,16 @@ sub sftp_config_userowner {
     exit 0;
   }
 
+  my ($new_uid, $new_gid) = (stat($config_file))[4, 5];
+
+  my $expected = $test_uid;
+  $self->assert($expected == $new_uid,
+    test_msg("Expected UID $expected, got $new_uid"));
+
+  $expected = $test_gid;
+  $self->assert($expected == $new_gid,
+    test_msg("Expected GID $expected, got $new_gid"));
+
   # Stop server
   server_stop($pid_file);
 
@@ -29890,7 +39136,7 @@ sub sftp_config_userowner {
   unlink($log_file);
 }
 
-sub sftp_config_groupowner_file_nonmember {
+sub sftp_config_limit_list {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -29922,19 +39168,13 @@ sub sftp_config_groupowner_file_nonmember {
     }
   }
 
-  my $owner = 'proftpd2';
-  my $owner_gid = 7777;
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
-  auth_group_write($auth_group_file, $owner, $owner_gid, 'foo');
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -29944,13 +39184,6 @@ sub sftp_config_groupowner_file_nonmember {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-#    RootRevoke => 'off',
-
-    Directory => {
-      '~' => {
-        GroupOwner => $owner,
-      },
-    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -29964,6 +39197,12 @@ sub sftp_config_groupowner_file_nonmember {
         "SFTPHostKey $dsa_host_key",
       ],
     },
+
+    Limit => {
+      LIST => {
+        DenyAll => '',
+      },
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -29980,9 +39219,6 @@ sub sftp_config_groupowner_file_nonmember {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -30007,33 +39243,57 @@ sub sftp_config_groupowner_file_nonmember {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+    
+      # Make sure that OPENDIR succeeds, but READDIR returns end-of-list.
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("FXP_OPENDIR . failed: [$err_name] ($err_code)");
       }
 
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      # To close the dirhandle, we explicitly destroy it.
+      $dir = undef;
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
+      my $expected = {};
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
       }
 
-      my $owning_gid = (stat($test_file))[5];
-      $self->assert($owner_gid == $owning_gid,
-        test_msg("Expected $owner_gid, got $owning_gid"));
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -30068,28 +39328,10 @@ sub sftp_config_groupowner_file_nonmember {
   unlink($log_file);
 }
 
-sub sftp_config_groupowner_file_member_norootprivs {
+sub sftp_config_limit_nlst {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
-  my ($config_user, $config_group) = config_get_identity();
-
-  my $members = [split(' ', (getgrnam($config_group))[3])];
-  if (scalar(@$members) < 2) {
-    print STDERR " + unable to run 'sftp_config_groupowner_member_norootprivs' test without current user belonging to multiple groups, skipping\n";
-    return;
-  }
-
-  my ($uid, $gid) = (getpwnam($members->[0]))[2,3];
-
-  my $root_login = 'off';
-  if ($uid == 0) {
-    $root_login = 'on';
-  }
-
-  my $owner = 'proftpd2';
-  my $owner_gid = (getpwnam($members->[1]))[3];
-
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
@@ -30103,6 +39345,8 @@ sub sftp_config_groupowner_file_member_norootprivs {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -30119,13 +39363,10 @@ sub sftp_config_groupowner_file_member_norootprivs {
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
-  auth_group_write($auth_group_file, $owner, $owner_gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -30135,13 +39376,6 @@ sub sftp_config_groupowner_file_member_norootprivs {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    RootLogin => $root_login,
-
-    Directory => {
-      '~' => {
-        GroupOwner => $owner,
-      },
-    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -30155,10 +39389,15 @@ sub sftp_config_groupowner_file_member_norootprivs {
         "SFTPHostKey $dsa_host_key",
       ],
     },
+
+    Limit => {
+      NLST => {
+        DenyAll => '',
+      },
+    },
   };
 
-  my $port;
-  ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my ($port, $config_user, $config_group) = config_write($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
@@ -30172,9 +39411,6 @@ sub sftp_config_groupowner_file_member_norootprivs {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -30199,33 +39435,57 @@ sub sftp_config_groupowner_file_member_norootprivs {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+    
+      # Make sure that OPENDIR succeeds, but READDIR returns end-of-list.
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("FXP_OPENDIR . failed: [$err_name] ($err_code)");
       }
 
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      # To close the dirhandle, we explicitly destroy it.
+      $dir = undef;
 
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
+      my $expected = {};
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
       }
 
-      my $owning_gid = (stat($test_file))[5];
-      $self->assert($owner_gid == $owning_gid,
-        test_msg("Expected $owner_gid, got $owning_gid"));
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -30260,28 +39520,10 @@ sub sftp_config_groupowner_file_member_norootprivs {
   unlink($log_file);
 }
 
-sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
+sub sftp_config_limit_allowfilter_stor_allowed {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
-  my ($config_user, $config_group) = config_get_identity();
-
-  my $members = [split(' ', (getgrnam($config_group))[3])];
-  if (scalar(@$members) < 2) {
-    print STDERR " + unable to run 'sftp_config_groupowner_member_norootprivs' test without current user belonging to multiple groups, skipping\n";
-    return;
-  }
-
-  my ($uid, $gid) = (getpwnam($members->[0]))[2,3];
-
-  my $root_login = 'off';
-  if ($uid == 0) {
-    $root_login = 'on';
-  }
-
-  my $owner = 'proftpd2';
-  my $owner_gid = (getpwnam($members->[1]))[3];
-
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
@@ -30295,6 +39537,8 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -30311,12 +39555,11 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
-  auth_group_write($auth_group_file, $owner, $owner_gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
 
   my $config = {
     PidFile => $pid_file,
@@ -30327,13 +39570,7 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultChdir => '~',
-
-    Directory => {
-      '~' => {
-        GroupOwner => $owner,
-      },
-    },
+    DefaultChdir => '~/test.d',
 
     IfModules => {
       'mod_delay.c' => {
@@ -30347,10 +39584,16 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
         "SFTPHostKey $dsa_host_key",
       ],
     },
+
+    Limit => {
+      STOR => {
+        AllowFilter => '\.txt$',
+        DenyAll => '',
+      },
+    },
   };
 
-  my $port;
-  ($port, $config_user, $config_group) = config_write($config_file, $config);
+  my ($port, $config_user, $config_group) = config_write($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
@@ -30364,9 +39607,6 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -30391,25 +39631,29 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $res = $sftp->mkdir('testdir');
-      unless ($res) {
+   
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't mkdir testdir: [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
+      }
 
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
       $ssh2->disconnect();
 
-      unless (-d $test_dir) {
-        die("Directory $test_dir does not exist as expected");
-      }
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
 
-      my $owning_gid = (stat($test_dir))[5];
-      $self->assert($owner_gid == $owning_gid,
-        test_msg("Expected owner GID $owner_gid, got $owning_gid"));
+      $self->assert(-s $test_file,
+        test_msg("File $test_file size is unexpectedly zero"));
     };
 
     if ($@) {
@@ -30444,7 +39688,7 @@ sub sftp_config_groupowner_dir_member_norootprivs_bug3765 {
   unlink($log_file);
 }
 
-sub sftp_config_ftpaccess_bug3460 {
+sub sftp_config_limit_allowfilter_stor_denied {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -30464,123 +39708,35 @@ sub sftp_config_ftpaccess_bug3460 {
   my $uid = 500;
   my $gid = 500;
 
-  my $data_dir = File::Spec->rel2abs("$home_dir/data");
-  mkpath($data_dir);
-
-  my $ftpaccess_file = File::Spec->rel2abs("$data_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOF;
-<Limit READ WRITE>
-  IgnoreHidden on
-  DenyAll
-</Limit>
-EOF
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
-  my $neither_dir = File::Spec->rel2abs("$data_dir/neither");
-  mkpath($neither_dir);
-
-  my $readable_dir = File::Spec->rel2abs("$data_dir/readable");
-  mkpath($readable_dir);
-
-  $ftpaccess_file = File::Spec->rel2abs("$readable_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOF;
-<Limit READ>
-  AllowUser $user
-  AllowUser foo
-  DenyAll
-</Limit>
-EOF
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
-  my $writable_dir = File::Spec->rel2abs("$data_dir/writable");
-  mkpath($writable_dir);
-
-  $ftpaccess_file = File::Spec->rel2abs("$writable_dir/.ftpaccess");
-  if (open(my $fh, "> $ftpaccess_file")) {
-    print $fh <<EOF;
-<Limit WRITE READ>
-  AllowUser $user
-  AllowUser foo
-  DenyAll
-</Limit>
-EOF
-    unless (close($fh)) {
-      die("Can't write $ftpaccess_file: $!");
-    }
-
-  } else {
-    die("Can't open $ftpaccess_file: $!");
-  }
-
-  my $sub_writable_dir = File::Spec->rel2abs("$writable_dir/subwritable");
-  mkpath($sub_writable_dir);
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $data_dir, $neither_dir, $readable_dir,
-        $writable_dir, $sub_writable_dir)) {
+    unless (chmod(0755, $home_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $data_dir, $neither_dir,
-        $readable_dir, $writable_dir, $sub_writable_dir)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $data_dir,
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$sub_writable_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 directory:20 ftpaccess:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    AllowOverride => 'on',
-    PathDenyFilter => '"\\\\.ftpaccess$"',
-
-    Directory => {
-      '/*' => {
-        AllowOverwrite => 'on',
-        HideUser => 'nobody',
-      },
-    },
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -30595,7 +39751,8 @@ EOF
     },
 
     Limit => {
-      'LOCK SYMLINK' => {
+      STOR => {
+        AllowFilter => '\.txt$',
         DenyAll => '',
       },
     },
@@ -30615,9 +39772,6 @@ EOF
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -30642,40 +39796,18 @@ EOF
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $base_path = $sftp->realpath('.');
-      unless ($base_path) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_REALPATH '.' failed: [$err_name] ($err_code)");
-      }
-
-      my $path = $sftp->realpath("$base_path/writable/subwritable");
-      unless ($path) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_REALPATH '$base_path/writable/subwritable' failed: [$err_name] ($err_code)");
-      }
-
-      my $dirh = $sftp->opendir($path);
-      unless ($dirh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_OPENDIR '$path' failed: [$err_name] ($err_code)");
+   
+      my $fh = $sftp->open('test.jpg', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      if ($fh) {
+        die("Open of test.jpg succeeded unexpectedly");
       }
 
-      $dirh = undef;
-
-      my $file_path = $sftp->realpath("$path/test.txt");
-      unless ($file_path) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_REALPATH '$path/test.txt' failed: [$err_name] ($err_code)");
-      }
+      my ($err_code, $err_name) = $sftp->error();
 
-      my $fh = $sftp->open($file_path, O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_OPEN '$file_path' failed: [$err_name] ($err_code)");
-      }
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($err_name eq $expected,
+        test_msg("Expected error name '$expected', got '$err_name'"));
 
-      $fh = undef;
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -30712,7 +39844,7 @@ EOF
   unlink($log_file);
 }
 
-sub sftp_config_limit_appe {
+sub sftp_config_limit_dirs_realpath_bug3871 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -30751,16 +39883,8 @@ sub sftp_config_limit_appe {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($sub_dir);
 
   my $config = {
     PidFile => $pid_file,
@@ -30772,13 +39896,6 @@ sub sftp_config_limit_appe {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    Directory => {
-      '~' => {
-        AllowOverwrite => 'on',
-        AllowStoreRestart => 'on',
-      },
-    },
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -30791,16 +39908,30 @@ sub sftp_config_limit_appe {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      APPE => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Directory ~>
+  <Limit ALL>
+    DenyAll
+  </Limit>
+
+  <Limit READ DIRS>
+    AllowUser $user
+  </Limit>
+</Directory>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -30841,19 +39972,25 @@ sub sftp_config_limit_appe {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_APPEND);
-      if ($fh) {
-        $fh = undef;
-        die("OPEN test.txt succeeded unexpectedly");
+      my $dir = $sftp->realpath('test.d');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't get real path for 'test.d': [$err_name] ($err_code)");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
+      my $expected;
+
+      $expected = $sub_dir;
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack to deal with how it handles tmp files
+        $expected = ('/private' . $expected);
+      }
+
+      $self->assert($expected eq $dir,
+        test_msg("Expected '$expected', got '$dir'"));
+
       $sftp = undef;
       $ssh2->disconnect();
-
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
     };
 
     if ($@) {
@@ -30888,7 +40025,7 @@ sub sftp_config_limit_appe {
   unlink($log_file);
 }
 
-sub sftp_config_limit_chmod {
+sub sftp_config_limit_readdir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -30932,7 +40069,7 @@ sub sftp_config_limit_chmod {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -30949,15 +40086,28 @@ sub sftp_config_limit_chmod {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      'SITE_CHMOD' => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit ALL>
+  DenyAll
+</Limit>
+
+# By not allowing READDIR here, we should not get any directory listing,
+# but the OPENDIR should succeed.
+<Limit OPENDIR CLOSE>
+  AllowAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
 
   # 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
@@ -30971,6 +40121,9 @@ sub sftp_config_limit_chmod {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -30995,29 +40148,33 @@ sub sftp_config_limit_chmod {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-     
-      my $res = $sftp->setstat('sftp.conf',
-        mode => 0777,
-      ); 
-      if ($res) {
-        die("setstat sftp.conf succeeded unexpectly");
-      }
 
-      my $attrs = $sftp->stat('sftp.conf');
-      unless ($attrs) {
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-      $ssh2->disconnect();
 
-      my $expected;
+      $ssh2->disconnect();
 
-      $expected = 0644;
-      my $file_mode = ($attrs->{mode} & 0777);
-      $self->assert($expected == $file_mode,
-        test_msg("Expected '$expected', got '$file_mode'"));
+      my $count = scalar(keys(%$res));
+      my $expected = 0;
+      $self->assert($count == $expected,
+        test_msg("Expected $expected directory entries, got $count"));
     };
 
     if ($@) {
@@ -31052,7 +40209,7 @@ sub sftp_config_limit_chmod {
   unlink($log_file);
 }
 
-sub sftp_config_limit_chgrp_bug3757 {
+sub sftp_config_limit_fsetstat_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -31096,7 +40253,7 @@ sub sftp_config_limit_chgrp_bug3757 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -31113,17 +40270,26 @@ sub sftp_config_limit_chgrp_bug3757 {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      'SITE_CHGRP' => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit ALL>
+  DenyAll
+</Limit>
 
-  my ($test_uid, $test_gid) = (stat($config_file))[4, 5];
+<Limit READ FSETSTAT>
+  AllowAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
 
   # 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
@@ -31161,23 +40327,45 @@ sub sftp_config_limit_chgrp_bug3757 {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-     
-      my $res = $sftp->setstat('sftp.conf',
-        uid => $uid,
-        gid => $gid,
+   
+      my $file = 'sftp.conf'; 
+      my $fh = $sftp->open($file, O_RDONLY); 
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open $file: [$err_name] ($err_code)");
+      }
+
+      my $res = $fh->setstat(
+        atime => 0,
+        mtime => 0,
       ); 
-      if ($res) {
-        die("setstat sftp.conf succeeded unexpectly");
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't fsetstat $file: [$err_name] ($err_code)");
       }
 
-      my $attrs = $sftp->stat('sftp.conf');
+      # Explicitly destroy the handle to issue the FXP_CLOSE
+      $fh = undef;
+
+      my $attrs = $sftp->stat($file);
       unless ($attrs) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_STAT sftp.conf failed: [$err_name] ($err_code)");
+        die("Can't stat $file: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
+
+      my $expected;
+
+      $expected = 0;
+      my $file_atime = $attrs->{atime};
+      $self->assert($expected == $file_atime,
+        test_msg("Expected file atime '$expected', got '$file_atime'"));
+
+      my $file_mtime = $attrs->{mtime};
+      $self->assert($expected == $file_mtime,
+        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
     };
 
     if ($@) {
@@ -31197,16 +40385,6 @@ sub sftp_config_limit_chgrp_bug3757 {
     exit 0;
   }
 
-  my ($new_uid, $new_gid) = (stat($config_file))[4, 5];
-
-  my $expected = $test_uid;
-  $self->assert($expected == $new_uid,
-    test_msg("Expected UID $expected, got $new_uid"));
-
-  $expected = $test_gid;
-  $self->assert($expected == $new_gid,
-    test_msg("Expected GID $expected, got $new_gid"));
-
   # Stop server
   server_stop($pid_file);
 
@@ -31222,7 +40400,7 @@ sub sftp_config_limit_chgrp_bug3757 {
   unlink($log_file);
 }
 
-sub sftp_config_limit_list {
+sub sftp_config_limit_mkdir_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -31242,6 +40420,8 @@ sub sftp_config_limit_list {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -31266,7 +40446,7 @@ sub sftp_config_limit_list {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -31283,15 +40463,26 @@ sub sftp_config_limit_list {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      LIST => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit ALL>
+  DenyAll
+</Limit>
+
+<Limit MKD>
+  AllowAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
 
   # 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
@@ -31329,57 +40520,19 @@ sub sftp_config_limit_list {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-    
-      # Make sure that OPENDIR succeeds, but READDIR returns end-of-list.
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("FXP_OPENDIR . failed: [$err_name] ($err_code)");
-      }
 
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+      my $res = $sftp->mkdir('testdir');
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't mkdir testdir: [$err_name] ($err_code)");
       }
 
-      # To close the dirhandle, we explicitly destroy it.
-      $dir = undef;
-
       $sftp = undef;
       $ssh2->disconnect();
 
-      my $expected = {};
-
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
-
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
-
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
-      }
-
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
+      unless (-d $test_dir) {
+        die("$test_dir directory does not exist as expected");
       }
-
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -31414,7 +40567,7 @@ sub sftp_config_limit_list {
   unlink($log_file);
 }
 
-sub sftp_config_limit_nlst {
+sub sftp_config_limit_opendir_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -31458,7 +40611,7 @@ sub sftp_config_limit_nlst {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'auth:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -31475,15 +40628,26 @@ sub sftp_config_limit_nlst {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      NLST => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit ALL>
+  DenyAll
+</Limit>
+
+<Limit MLSD>
+  AllowAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
 
   # 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
@@ -31521,57 +40685,20 @@ sub sftp_config_limit_nlst {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-    
-      # Make sure that OPENDIR succeeds, but READDIR returns end-of-list.
+
       my $dir = $sftp->opendir('.');
       unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("FXP_OPENDIR . failed: [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
-      # To close the dirhandle, we explicitly destroy it.
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
       $dir = undef;
 
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-      $ssh2->disconnect();
 
-      my $expected = {};
-
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
-
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
-
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
-      }
-
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
-      }
-
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -31606,7 +40733,7 @@ sub sftp_config_limit_nlst {
   unlink($log_file);
 }
 
-sub sftp_config_limit_allowfilter_stor_allowed {
+sub sftp_config_limit_readdir_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -31626,17 +40753,14 @@ sub sftp_config_limit_allowfilter_stor_allowed {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_dir = File::Spec->rel2abs("$home_dir/test.d");
-  mkpath($test_dir);
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $test_dir)) {
+    unless (chmod(0755, $home_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $test_dir)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -31653,11 +40777,10 @@ sub sftp_config_limit_allowfilter_stor_allowed {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultChdir => '~/test.d',
 
     IfModules => {
       'mod_delay.c' => {
@@ -31671,16 +40794,28 @@ sub sftp_config_limit_allowfilter_stor_allowed {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      STOR => {
-        AllowFilter => '\.txt$',
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit ALL>
+  DenyAll
+</Limit>
+
+# By not allowing READDIR here, we should not get any directory listing,
+# but the OPENDIR should succeed.
+<Limit OPENDIR READDIR CLOSE>
+  AllowAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
 
   # 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
@@ -31694,6 +40829,9 @@ sub sftp_config_limit_allowfilter_stor_allowed {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -31718,23 +40856,67 @@ sub sftp_config_limit_allowfilter_stor_allowed {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-   
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
-      my $count = 20;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8192;
+      my $res = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
 
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
+
       $ssh2->disconnect();
+
+      my $expected = {
+        '.' => 1,
+        '..' => 1,
+        'sftp.conf' => 1,
+        'sftp.group' => 1,
+        'sftp.passwd' => 1,
+        'sftp.pid' => 1,
+        'sftp.scoreboard' => 1,
+        'sftp.scoreboard.lck' => 1,
+      };
+
+      my $ok = 1;
+      my $mismatch;
+
+      my $seen = [];
+      foreach my $name (keys(%$res)) {
+        push(@$seen, $name);
+
+        unless (defined($expected->{$name})) {
+          $mismatch = $name;
+          $ok = 0;
+          last;
+        }
+      }
+
+      unless ($ok) {
+        die("Unexpected name '$mismatch' appeared in READDIR data")
+      }
+
+      # Now remove from $expected all of the paths we saw; if there are
+      # any entries remaining in $expected, something went wrong.
+      foreach my $name (@$seen) {
+        delete($expected->{$name});
+      }
+
+      my $remaining = scalar(keys(%$expected));
+      $self->assert(0 == $remaining,
+        test_msg("Expected 0, got $remaining"));
     };
 
     if ($@) {
@@ -31769,7 +40951,7 @@ sub sftp_config_limit_allowfilter_stor_allowed {
   unlink($log_file);
 }
 
-sub sftp_config_limit_allowfilter_stor_denied {
+sub sftp_config_limit_readlink_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -31789,6 +40971,23 @@ sub sftp_config_limit_allowfilter_stor_denied {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
+  unless (symlink($test_file, $test_symlink)) {
+    die("Can't symlink $test_symlink to $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -31813,7 +41012,7 @@ sub sftp_config_limit_allowfilter_stor_denied {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -31830,16 +41029,26 @@ sub sftp_config_limit_allowfilter_stor_denied {
         "SFTPHostKey $dsa_host_key",
       ],
     },
-
-    Limit => {
-      STOR => {
-        AllowFilter => '\.txt$',
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit ALL>
+  DenyAll
+</Limit>
+
+<Limit READLINK>
+  AllowAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
 
   # 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
@@ -31877,20 +41086,18 @@ sub sftp_config_limit_allowfilter_stor_denied {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-   
-      my $fh = $sftp->open('test.jpg', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      if ($fh) {
-        die("Open of test.jpg succeeded unexpectedly");
-      }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($err_name eq $expected,
-        test_msg("Expected error name '$expected', got '$err_name'"));
+      my $path = $sftp->readlink('test.lnk');
+      unless ($path) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't readlink test.lnk: [$err_name] ($err_code)");
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
+
+      $self->assert($test_file eq $path,
+        test_msg("Expected '$test_file', got '$path'"));
     };
 
     if ($@) {
@@ -31925,7 +41132,7 @@ sub sftp_config_limit_allowfilter_stor_denied {
   unlink($log_file);
 }
 
-sub sftp_config_limit_dirs_realpath_bug3871 {
+sub sftp_config_limit_remove_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -31945,6 +41152,18 @@ sub sftp_config_limit_dirs_realpath_bug3871 {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -31964,15 +41183,12 @@ sub sftp_config_limit_dirs_realpath_bug3871 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d");
-  mkpath($sub_dir);
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -31992,18 +41208,15 @@ sub sftp_config_limit_dirs_realpath_bug3871 {
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
   if (open(my $fh, ">> $config_file")) {
     print $fh <<EOC;
-<Directory ~>
-  <Limit ALL>
-    DenyAll
-  </Limit>
+<Limit ALL>
+  DenyAll
+</Limit>
 
-  <Limit READ DIRS>
-    AllowUser $user
-  </Limit>
-</Directory>
+<Limit DELE>
+  AllowAll
+</Limit>
 EOC
     unless (close($fh)) {
       die("Can't write $config_file: $!");
@@ -32025,9 +41238,6 @@ EOC
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -32053,20 +41263,19 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->realpath('test.d');
-      unless ($dir) {
+      my $file = 'test.txt';
+      my $res = $sftp->unlink($file);
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't get real path for 'test.d': [$err_name] ($err_code)");
+        die("Can't remove $file: [$err_name] ($err_code)");
       }
 
-      my $expected;
-
-      $expected = $sub_dir;
-      $self->assert($expected eq $dir,
-        test_msg("Expected '$expected', got '$dir'"));
-
       $sftp = undef;
       $ssh2->disconnect();
+
+      if (-f $test_file) {
+        die("$test_file file exists unexpectedly");
+      }
     };
 
     if ($@) {
@@ -32101,7 +41310,7 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_config_limit_readdir {
+sub sftp_config_limit_rename_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -32121,6 +41330,20 @@ sub sftp_config_limit_readdir {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -32171,9 +41394,7 @@ sub sftp_config_limit_readdir {
   DenyAll
 </Limit>
 
-# By not allowing READDIR here, we should not get any directory listing,
-# but the OPENDIR should succeed.
-<Limit OPENDIR CLOSE>
+<Limit RNFR RNTO>
   AllowAll
 </Limit>
 EOC
@@ -32197,9 +41418,6 @@ EOC
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -32225,32 +41443,22 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
+      my $res = $sftp->rename('test.txt', 'test2.txt');
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+        die("Can't rename test.txt to test2.txt: [$err_name] ($err_code)");
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-
       $ssh2->disconnect();
 
-      my $count = scalar(keys(%$res));
-      my $expected = 0;
-      $self->assert($count == $expected,
-        test_msg("Expected $expected directory entries, got $count"));
+      if (-f $test_file) {
+        die("$test_file file exists unexpectedly");
+      }
+
+      unless (-f $test_file2) {
+        die("$test_file2 file does not exist as expected");
+      }
     };
 
     if ($@) {
@@ -32285,7 +41493,7 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_config_limit_fsetstat_bug3753 {
+sub sftp_config_limit_rmdir_bug3753 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -32305,6 +41513,9 @@ sub sftp_config_limit_fsetstat_bug3753 {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+  mkpath($test_dir);
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -32355,7 +41566,7 @@ sub sftp_config_limit_fsetstat_bug3753 {
   DenyAll
 </Limit>
 
-<Limit READ FSETSTAT>
+<Limit RMD>
   AllowAll
 </Limit>
 EOC
@@ -32403,45 +41614,19 @@ EOC
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-   
-      my $file = 'sftp.conf'; 
-      my $fh = $sftp->open($file, O_RDONLY); 
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open $file: [$err_name] ($err_code)");
-      }
 
-      my $res = $fh->setstat(
-        atime => 0,
-        mtime => 0,
-      ); 
+      my $res = $sftp->rmdir('testdir');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't fsetstat $file: [$err_name] ($err_code)");
-      }
-
-      # Explicitly destroy the handle to issue the FXP_CLOSE
-      $fh = undef;
-
-      my $attrs = $sftp->stat($file);
-      unless ($attrs) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't stat $file: [$err_name] ($err_code)");
+        die("Can't rmdir testdir: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
 
-      my $expected;
-
-      $expected = 0;
-      my $file_atime = $attrs->{atime};
-      $self->assert($expected == $file_atime,
-        test_msg("Expected file atime '$expected', got '$file_atime'"));
-
-      my $file_mtime = $attrs->{mtime};
-      $self->assert($expected == $file_mtime,
-        test_msg("Expected file mtime '$expected', got '$file_mtime'"));
+      if (-d $test_dir) {
+        die("$test_dir directory exists unexpectedly");
+      }
     };
 
     if ($@) {
@@ -32476,7 +41661,7 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_config_limit_mkdir_bug3753 {
+sub sftp_multi_channels {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -32496,8 +41681,6 @@ sub sftp_config_limit_mkdir_bug3753 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -32541,24 +41724,7 @@ sub sftp_config_limit_mkdir_bug3753 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
-
-<Limit MKD>
-  AllowAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
+  my ($port, $config_user, $config_group) = config_write($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
@@ -32591,24 +41757,45 @@ EOC
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
+      # Open three different 'sftp' sessions, and make sure they all
+      # work as expected.
 
-      my $res = $sftp->mkdir('testdir');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't mkdir testdir: [$err_name] ($err_code)");
+      my $sftps = [];
+
+      for (my $i = 0; $i < 3; $i++) { 
+        my $sftp = $ssh2->sftp();
+        unless ($sftp) {
+          my ($err_code, $err_name, $err_str) = $ssh2->error();
+          die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+        }
+
+        push(@$sftps, $sftp);
       }
 
-      $sftp = undef;
-      $ssh2->disconnect();
+      for (my $i = 0; $i < scalar(@$sftps); $i++) {
+        my $cwd = $sftps->[$i]->realpath('.');
+        unless ($cwd) {
+          my ($err_code, $err_name) = $sftps->[$i]->error();
+          die("Can't get real path for '.': [$err_name] ($err_code)");
+        }
 
-      unless (-d $test_dir) {
-        die("$test_dir directory does not exist as expected");
+        my $expected;
+
+        $expected = $home_dir;
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack to deal with how it handles tmp files
+          $expected = ('/private' . $expected);
+        }
+
+        $self->assert($expected eq $cwd,
+          test_msg("Expected '$expected', got '$cwd'"));
+      }
+
+      for (my $i = 0; $i < scalar(@$sftps); $i++) {
+        $sftps->[$i] = undef;
       }
+ 
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -32643,7 +41830,62 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_config_limit_opendir_bug3753 {
+sub sftp_config_insecure_hostkey_perms_bug4098 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sftp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  # Deliberately set insecure perms on the hostkeys
+  unless (chmod(0444, $rsa_host_key, $dsa_host_key)) {
+    die("Can't set perms on $rsa_host_key, $dsa_host_key: $!");
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPOptions InsecureHostKeyPerms",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $ex;
+
+  # First, start the server.
+  eval { server_start($setup->{config_file}, $setup->{pid_file}) };
+  if ($@) {
+    $ex = "Server failed to start up with world-readable SFTPHostKey";
+
+  } else {
+    server_stop($setup->{pid_file});
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub sftp_multi_channel_downloads {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -32687,7 +41929,7 @@ sub sftp_config_limit_opendir_bug3753 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'auth:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -32706,25 +41948,41 @@ sub sftp_config_limit_opendir_bug3753 {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/test1.txt");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "ABCD" x 8192;
+    unless (close($fh)) {
+      die("Can't write $test_file1: $!");
+    }
 
-<Limit MLSD>
-  AllowAll
-</Limit>
-EOC
+  } else {
+    die("Can't open $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "KLMN" x 8192;
     unless (close($fh)) {
-      die("Can't write $config_file: $!");
+      die("Can't write $test_file2: $!");
     }
 
   } else {
-    die("Can't open $config_file: $!");
+    die("Can't open $test_file2: $!");
   }
 
+  my $test_file3 = File::Spec->rel2abs("$tmpdir/test3.txt");
+  if (open(my $fh, "> $test_file3")) {
+    print $fh "UVWX" x 8192;
+    unless (close($fh)) {
+      die("Can't write $test_file3: $!");
+    }
+
+  } else {
+    die("Can't open $test_file3: $!");
+  }
+
+  my ($port, $config_user, $config_group) = config_write($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.
@@ -32756,25 +42014,87 @@ EOC
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
+      # Open three different 'sftp' sessions, and make sure they all
+      # work as expected.
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
+      my $sftps = [];
+      my $fhs = [];
+      my $md5s = [];
+
+      for (my $i = 0; $i < 3; $i++) { 
+        my $sftp = $ssh2->sftp();
+        unless ($sftp) {
+          my ($err_code, $err_name, $err_str) = $ssh2->error();
+          die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+        }
+
+        push(@$sftps, $sftp);
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+      for (my $i = 0; $i < scalar(@$sftps); $i++) {
+        my $path = "test" . ($i + 1) . ".txt";
 
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+        my $test_fh = $sftps->[$i]->open($path, O_RDONLY);
+        unless ($test_fh) {
+          my ($err_code, $err_name) = $sftps->[$i]->error();
+          die("Can't open $path: [$err_name] ($err_code)");
+        }
+
+        my $ctx = Digest::MD5->new();
+
+        # my $read_len = 65535;
+        my $read_len = 32768;
+        my $buf;
+
+        my $res = $test_fh->read($buf, $read_len);
+        unless ($res) {
+          my ($err_code, $err_name) = $sftps->[$i]->error();
+          if ($err_code != 0) {
+            die("Can't read $path: [$err_name] ($err_code)");
+          } else {
+            my $err_str;
+            ($err_code, $err_name, $err_str) = $ssh2->error();
+            die("SSH2 error: [$err_name] ($err_code) $err_str");
+          }
+        }
+
+        while ($res) {
+          $ctx->add($buf);
+          unless ($test_fh->seek($res)) {
+            die("Can't seek to offset $res on $path handle: $!");
+          }
+
+          $res = $test_fh->read($buf, $read_len);
+        }
+
+        push(@$md5s, $ctx->hexdigest());
 
+        push(@$fhs, $test_fh);
+      }
+
+      for (my $i = 0; $i < scalar(@$fhs); $i++) {
+        $fhs->[$i] = undef;
+      }
+ 
+      for (my $i = 0; $i < scalar(@$sftps); $i++) {
+        $sftps->[$i] = undef;
+      }
+ 
       $ssh2->disconnect();
+
+      my $expected;
+
+      $expected = '6e816b2d373a619a29d1706ac6be1db0';
+      $self->assert($expected eq $md5s->[0],
+        test_msg("Expected '$expected', got '$md5s->[0]'"));
+
+      $expected = 'b96129f1efce8f38324adf1a9d1e889c';
+      $self->assert($expected eq $md5s->[1],
+        test_msg("Expected '$expected', got '$md5s->[1]'"));
+
+      $expected = 'b6df9d6dccce294918a51ac7deabfd96';
+      $self->assert($expected eq $md5s->[2],
+        test_msg("Expected '$expected', got '$md5s->[2]'"));
     };
 
     if ($@) {
@@ -32785,7 +42105,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($config_file, $rfh, 30) };
     if ($@) {
       warn($@);
       exit 1;
@@ -32809,13 +42129,14 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_config_limit_readdir_bug3753 {
+sub sftp_log_xferlog_download {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
 
   my $log_file = test_get_logfile();
 
@@ -32829,6 +42150,20 @@ sub sftp_config_limit_readdir_bug3753 {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 256;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_sz = (stat($test_file))[7];
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -32853,11 +42188,13 @@ sub sftp_config_limit_readdir_bug3753 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TransferLog => $xferlog_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -32873,25 +42210,6 @@ sub sftp_config_limit_readdir_bug3753 {
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
-
-# By not allowing READDIR here, we should not get any directory listing,
-# but the OPENDIR should succeed.
-<Limit OPENDIR READDIR CLOSE>
-  AllowAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
 
   # 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
@@ -32933,66 +42251,32 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my $res = {};
+      my $buf;
+      my $size = 0;
 
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
       }
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
 
       # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
 
-      $ssh2->disconnect();
-
-      my $expected = {
-        '.' => 1,
-        '..' => 1,
-        'sftp.conf' => 1,
-        'sftp.group' => 1,
-        'sftp.passwd' => 1,
-        'sftp.pid' => 1,
-        'sftp.scoreboard' => 1,
-        'sftp.scoreboard.lck' => 1,
-      };
-
-      my $ok = 1;
-      my $mismatch;
-
-      my $seen = [];
-      foreach my $name (keys(%$res)) {
-        push(@$seen, $name);
-
-        unless (defined($expected->{$name})) {
-          $mismatch = $name;
-          $ok = 0;
-          last;
-        }
-      }
-
-      unless ($ok) {
-        die("Unexpected name '$mismatch' appeared in READDIR data")
-      }
-
-      # Now remove from $expected all of the paths we saw; if there are
-      # any entries remaining in $expected, something went wrong.
-      foreach my $name (@$seen) {
-        delete($expected->{$name});
-      }
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
 
-      my $remaining = scalar(keys(%$expected));
-      $self->assert(0 == $remaining,
-        test_msg("Expected 0, got $remaining"));
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -33024,16 +42308,96 @@ EOC
     die($ex);
   }
 
+  if (open(my $fh, "< $xferlog_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
+        my $client_addr = $3;
+        my $nbytes = $4;
+        my $path = $5;
+        my $xfer_type = $6;
+        my $action_flag = $7;
+        my $xfer_direction = $8;
+        my $access_mode = $9;
+        my $user_name = $10;
+        my $service_name = $11;
+        my $completion_status = $12;
+
+        my $expected;
+
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $client_addr,
+          test_msg("Expected IP address '$expected', got '$client_addr'"));
+
+        $expected = $test_sz;
+        $self->assert($expected == $nbytes,
+          test_msg("Expected size $expected, got $nbytes"));
+
+        $expected = $test_file;
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack to deal with how it handles tmp files
+          $expected = ('/private' . $expected);
+        }
+        $self->assert($expected eq $path,
+          test_msg("Expected path '$expected', got '$path'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected transfer type '$expected', got '$xfer_type'"));
+
+        $expected = '_';
+        $self->assert($expected eq $action_flag,
+          test_msg("Expected action flag '$expected', got '$action_flag'"));
+
+        $expected = 'o';
+        $self->assert($expected eq $xfer_direction,
+          test_msg("Expected transfer direction '$expected', got '$xfer_direction'"));
+
+        $expected = 'r';
+        $self->assert($expected eq $access_mode,
+          test_msg("Expected access mode '$expected', got '$access_mode'"));
+
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected user '$expected', got '$user_name'"));
+
+        $expected = 'sftp';
+        $self->assert($expected eq $service_name,
+          test_msg("Expected service '$expected', got '$service_name'"));
+
+        $expected = 'c';
+        $self->assert($expected eq $completion_status,
+          test_msg("Expected completion status '$expected', got '$completion_status'"));
+
+        $ok = 1;
+        last;
+      }
+    }
+
+    close($fh);
+
+    unless ($ok) {
+      die("No lines found in $xferlog_file");
+    }
+
+  } else {
+    die("Can't read $xferlog_file: $!");
+  }
+
   unlink($log_file);
 }
 
-sub sftp_config_limit_readlink_bug3753 {
+sub sftp_log_xferlog_download_incomplete {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
 
   my $log_file = test_get_logfile();
 
@@ -33049,7 +42413,7 @@ sub sftp_config_limit_readlink_bug3753 {
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
+    print $fh "ABCD" x 256;
 
     unless (close($fh)) {
       die("Can't write $test_file: $!");
@@ -33059,10 +42423,7 @@ sub sftp_config_limit_readlink_bug3753 {
     die("Can't open $test_file: $!");
   }
 
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/test.lnk");
-  unless (symlink($test_file, $test_symlink)) {
-    die("Can't symlink $test_symlink to $test_file: $!");
-  }
+  my $read_sz = 32;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -33088,10 +42449,11 @@ sub sftp_config_limit_readlink_bug3753 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    TransferLog => $xferlog_file,
 
     IfModules => {
       'mod_delay.c' => {
@@ -33108,23 +42470,6 @@ sub sftp_config_limit_readlink_bug3753 {
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
-
-<Limit READLINK>
-  AllowAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
 
   # 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
@@ -33138,6 +42483,9 @@ EOC
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -33163,58 +42511,144 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $path = $sftp->readlink('test.lnk');
-      unless ($path) {
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't readlink test.lnk: [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      $sftp = undef;
+      my $buf;
+      my $res = $fh->read($buf, $read_sz);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't read test.txt: [$err_name] ($err_code)");
+      } 
+
+      sleep(1);
+
+      # Explicitly disconnect without closing the file, simulating an
+      # aborted transfer.
       $ssh2->disconnect();
 
-      $self->assert($test_file eq $path,
-        test_msg("Expected '$test_file', got '$path'"));
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
       $ex = $@;
     }
 
-    $wfh->print("done\n");
-    $wfh->flush();
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  if (open(my $fh, "< $xferlog_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
+        my $client_addr = $3;
+        my $nbytes = $4;
+        my $path = $5;
+        my $xfer_type = $6;
+        my $action_flag = $7;
+        my $xfer_direction = $8;
+        my $access_mode = $9;
+        my $user_name = $10;
+        my $service_name = $11;
+        my $completion_status = $12;
+
+        my $expected;
+
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $client_addr,
+          test_msg("Expected '$expected', got '$client_addr'"));
+
+        $expected = $read_sz;
+        $self->assert($expected == $nbytes,
+          test_msg("Expected $expected, got $nbytes"));
+
+        $expected = $test_file;
+        $self->assert($expected eq $path,
+          test_msg("Expected '$expected', got '$path'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected '$expected', got '$xfer_type'"));
+
+        $expected = '_';
+        $self->assert($expected eq $action_flag,
+          test_msg("Expected '$expected', got '$action_flag'"));
+
+        $expected = 'o';
+        $self->assert($expected eq $xfer_direction,
+          test_msg("Expected '$expected', got '$xfer_direction'"));
+
+        $expected = 'r';
+        $self->assert($expected eq $access_mode,
+          test_msg("Expected '$expected', got '$access_mode'"));
+
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected '$expected', got '$user_name'"));
 
-  } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
-    }
+        $expected = 'sftp';
+        $self->assert($expected eq $service_name,
+          test_msg("Expected '$expected', got '$service_name'"));
 
-    exit 0;
-  }
+        $expected = 'i';
+        $self->assert($expected eq $completion_status,
+          test_msg("Expected '$expected', got '$completion_status'"));
 
-  # Stop server
-  server_stop($pid_file);
+        $ok = 1;
+        last;
+      }
+    }
 
-  $self->assert_child_ok($pid);
+    close($fh);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+    unless ($ok) {
+      die("No lines found in $xferlog_file");
+    }
 
-    die($ex);
+  } else {
+    die("Can't read $xferlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_config_limit_remove_bug3753 {
+sub sftp_log_xferlog_delete {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
 
   my $log_file = test_get_logfile();
 
@@ -33230,7 +42664,7 @@ sub sftp_config_limit_remove_bug3753 {
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
+    print $fh "ABCD\n";
 
     unless (close($fh)) {
       die("Can't write $test_file: $!");
@@ -33264,11 +42698,13 @@ sub sftp_config_limit_remove_bug3753 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TransferLog => $xferlog_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -33284,23 +42720,6 @@ sub sftp_config_limit_remove_bug3753 {
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
-
-<Limit DELE>
-  AllowAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
 
   # 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
@@ -33314,6 +42733,9 @@ EOC
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -33339,19 +42761,14 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $file = 'test.txt';
-      my $res = $sftp->unlink($file);
+      my $res = $sftp->unlink('test.txt');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't remove $file: [$err_name] ($err_code)");
+        die("Can't delete test.txt: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
-
-      if (-f $test_file) {
-        die("$test_file file exists unexpectedly");
-      }
     };
 
     if ($@) {
@@ -33383,16 +42800,93 @@ EOC
     die($ex);
   }
 
+  if (open(my $fh, "< $xferlog_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
+        my $client_addr = $3;
+        my $nbytes = $4;
+        my $path = $5;
+        my $xfer_type = $6;
+        my $action_flag = $7;
+        my $xfer_direction = $8;
+        my $access_mode = $9;
+        my $user_name = $10;
+        my $service_name = $11;
+        my $completion_status = $12;
+
+        my $expected;
+
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $client_addr,
+          test_msg("Expected '$expected', got '$client_addr'"));
+
+        $expected = $test_file;
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack to deal with how it handles tmp files
+          $expected = ('/private' . $expected);
+        }
+
+        $self->assert($expected eq $path,
+          test_msg("Expected '$expected', got '$path'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected '$expected', got '$xfer_type'"));
+
+        $expected = '_';
+        $self->assert($expected eq $action_flag,
+          test_msg("Expected '$expected', got '$action_flag'"));
+
+        $expected = 'd';
+        $self->assert($expected eq $xfer_direction,
+          test_msg("Expected '$expected', got '$xfer_direction'"));
+
+        $expected = 'r';
+        $self->assert($expected eq $access_mode,
+          test_msg("Expected '$expected', got '$access_mode'"));
+
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected '$expected', got '$user_name'"));
+
+        $expected = 'sftp';
+        $self->assert($expected eq $service_name,
+          test_msg("Expected '$expected', got '$service_name'"));
+
+        $expected = 'c';
+        $self->assert($expected eq $completion_status,
+          test_msg("Expected '$expected', got '$completion_status'"));
+
+        $ok = 1;
+        last;
+      }
+    }
+
+    close($fh);
+
+    unless ($ok) {
+      die("No lines found in $xferlog_file");
+    }
+
+  } else {
+    die("Can't read $xferlog_file: $!");
+  }
+
   unlink($log_file);
 }
 
-sub sftp_config_limit_rename_bug3753 {
+sub sftp_log_xferlog_delete_chrooted {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
 
   my $log_file = test_get_logfile();
 
@@ -33408,7 +42902,7 @@ sub sftp_config_limit_rename_bug3753 {
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 8192;
+    print $fh "ABCD\n";
 
     unless (close($fh)) {
       die("Can't write $test_file: $!");
@@ -33418,8 +42912,6 @@ sub sftp_config_limit_rename_bug3753 {
     die("Can't open $test_file: $!");
   }
 
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -33444,10 +42936,13 @@ sub sftp_config_limit_rename_bug3753 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
+
+    TransferLog => $xferlog_file,
 
     IfModules => {
       'mod_delay.c' => {
@@ -33464,23 +42959,6 @@ sub sftp_config_limit_rename_bug3753 {
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
-
-<Limit RNFR RNTO>
-  AllowAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
 
   # 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
@@ -33494,6 +42972,9 @@ EOC
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -33519,22 +43000,14 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->rename('test.txt', 'test2.txt');
+      my $res = $sftp->unlink('test.txt');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't rename test.txt to test2.txt: [$err_name] ($err_code)");
+        die("Can't delete test.txt: [$err_name] ($err_code)");
       }
 
       $sftp = undef;
       $ssh2->disconnect();
-
-      if (-f $test_file) {
-        die("$test_file file exists unexpectedly");
-      }
-
-      unless (-f $test_file2) {
-        die("$test_file2 file does not exist as expected");
-      }
     };
 
     if ($@) {
@@ -33566,16 +43039,88 @@ EOC
     die($ex);
   }
 
+  if (open(my $fh, "< $xferlog_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
+        my $client_addr = $3;
+        my $nbytes = $4;
+        my $path = $5;
+        my $xfer_type = $6;
+        my $action_flag = $7;
+        my $xfer_direction = $8;
+        my $access_mode = $9;
+        my $user_name = $10;
+        my $service_name = $11;
+        my $completion_status = $12;
+
+        my $expected;
+
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $client_addr,
+          test_msg("Expected '$expected', got '$client_addr'"));
+
+        $expected = $test_file;
+        $self->assert($expected eq $path,
+          test_msg("Expected '$expected', got '$path'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected '$expected', got '$xfer_type'"));
+
+        $expected = '_';
+        $self->assert($expected eq $action_flag,
+          test_msg("Expected '$expected', got '$action_flag'"));
+
+        $expected = 'd';
+        $self->assert($expected eq $xfer_direction,
+          test_msg("Expected '$expected', got '$xfer_direction'"));
+
+        $expected = 'r';
+        $self->assert($expected eq $access_mode,
+          test_msg("Expected '$expected', got '$access_mode'"));
+
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected '$expected', got '$user_name'"));
+
+        $expected = 'sftp';
+        $self->assert($expected eq $service_name,
+          test_msg("Expected '$expected', got '$service_name'"));
+
+        $expected = 'c';
+        $self->assert($expected eq $completion_status,
+          test_msg("Expected '$expected', got '$completion_status'"));
+
+        $ok = 1;
+        last;
+      }
+    }
+
+    close($fh);
+
+    unless ($ok) {
+      die("No lines found in $xferlog_file");
+    }
+
+  } else {
+    die("Can't read $xferlog_file: $!");
+  }
+
   unlink($log_file);
 }
 
-sub sftp_config_limit_rmdir_bug3753 {
+sub sftp_log_xferlog_upload {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
 
   my $log_file = test_get_logfile();
 
@@ -33589,8 +43134,8 @@ sub sftp_config_limit_rmdir_bug3753 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
-  mkpath($test_dir);
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $write_sz = 1024;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -33616,11 +43161,13 @@ sub sftp_config_limit_rmdir_bug3753 {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    TransferLog => $xferlog_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -33636,23 +43183,6 @@ sub sftp_config_limit_rmdir_bug3753 {
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit ALL>
-  DenyAll
-</Limit>
-
-<Limit RMD>
-  AllowAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
 
   # 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
@@ -33666,6 +43196,9 @@ EOC
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -33691,18 +43224,22 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->rmdir('testdir');
-      unless ($res) {
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't rmdir testdir: [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
+      my $buf = ("ABCD" x 256);
+      print $fh $buf;
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      # To close the SFTP channel, we have to explicitly destroy the object
       $sftp = undef;
-      $ssh2->disconnect();
 
-      if (-d $test_dir) {
-        die("$test_dir directory exists unexpectedly");
-      }
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -33734,16 +43271,97 @@ EOC
     die($ex);
   }
 
+  if (open(my $fh, "< $xferlog_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
+        my $client_addr = $3;
+        my $nbytes = $4;
+        my $path = $5;
+        my $xfer_type = $6;
+        my $action_flag = $7;
+        my $xfer_direction = $8;
+        my $access_mode = $9;
+        my $user_name = $10;
+        my $service_name = $11;
+        my $completion_status = $12;
+
+        my $expected;
+
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $client_addr,
+          test_msg("Expected '$expected', got '$client_addr'"));
+
+        $expected = $write_sz;
+        $self->assert($expected == $nbytes,
+          test_msg("Expected $expected, got $nbytes"));
+
+        $expected = $test_file;
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack to deal with how it handles tmp files
+          $expected = ('/private' . $expected);
+        }
+
+        $self->assert($expected eq $path,
+          test_msg("Expected '$expected', got '$path'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected '$expected', got '$xfer_type'"));
+
+        $expected = '_';
+        $self->assert($expected eq $action_flag,
+          test_msg("Expected '$expected', got '$action_flag'"));
+
+        $expected = 'i';
+        $self->assert($expected eq $xfer_direction,
+          test_msg("Expected '$expected', got '$xfer_direction'"));
+
+        $expected = 'r';
+        $self->assert($expected eq $access_mode,
+          test_msg("Expected '$expected', got '$access_mode'"));
+
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected '$expected', got '$user_name'"));
+
+        $expected = 'sftp';
+        $self->assert($expected eq $service_name,
+          test_msg("Expected '$expected', got '$service_name'"));
+
+        $expected = 'c';
+        $self->assert($expected eq $completion_status,
+          test_msg("Expected '$expected', got '$completion_status'"));
+
+        $ok = 1;
+        last;
+      }
+    }
+
+    close($fh);
+
+    unless ($ok) {
+      die("No lines found in $xferlog_file");
+    }
+
+  } else {
+    die("Can't read $xferlog_file: $!");
+  }
+
   unlink($log_file);
 }
 
-sub sftp_multi_channels {
+sub sftp_log_xferlog_upload_incomplete {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
 
   my $log_file = test_get_logfile();
 
@@ -33757,6 +43375,9 @@ sub sftp_multi_channels {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $write_sz = 32;
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -33781,10 +43402,11 @@ sub sftp_multi_channels {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    TransferLog => $xferlog_file,
 
     IfModules => {
       'mod_delay.c' => {
@@ -33814,6 +43436,9 @@ sub sftp_multi_channels {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -33833,81 +43458,153 @@ sub sftp_multi_channels {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Open three different 'sftp' sessions, and make sure they all
-      # work as expected.
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
 
-      my $sftps = [];
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
 
-      for (my $i = 0; $i < 3; $i++) { 
-        my $sftp = $ssh2->sftp();
-        unless ($sftp) {
-          my ($err_code, $err_name, $err_str) = $ssh2->error();
-          die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-        }
+      my $buf = ("ABCD" x 8);
+      my $res = $fh->write($buf);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't write test.txt: [$err_name] ($err_code)");
+      } 
 
-        push(@$sftps, $sftp);
-      }
+      # Explicitly disconnect without closing the file, simulating an
+      # aborted transfer.
+      $ssh2->disconnect();
 
-      for (my $i = 0; $i < scalar(@$sftps); $i++) {
-        my $cwd = $sftps->[$i]->realpath('.');
-        unless ($cwd) {
-          my ($err_code, $err_name) = $sftps->[$i]->error();
-          die("Can't get real path for '.': [$err_name] ($err_code)");
-        }
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  if (open(my $fh, "< $xferlog_file")) {
+    my $ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
+        my $client_addr = $3;
+        my $nbytes = $4;
+        my $path = $5;
+        my $xfer_type = $6;
+        my $action_flag = $7;
+        my $xfer_direction = $8;
+        my $access_mode = $9;
+        my $user_name = $10;
+        my $service_name = $11;
+        my $completion_status = $12;
 
         my $expected;
 
-        $expected = $home_dir;
-        $self->assert($expected eq $cwd,
-          test_msg("Expected '$expected', got '$cwd'"));
-      }
+        $expected = '127.0.0.1';
+        $self->assert($expected eq $client_addr,
+          test_msg("Expected '$expected', got '$client_addr'"));
+
+        $expected = $write_sz;
+        $self->assert($expected == $nbytes,
+          test_msg("Expected $expected, got $nbytes"));
+
+        $expected = $test_file;
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack to deal with how it handles tmp files
+          $expected = ('/private' . $expected);
+        }
+
+        $self->assert($expected eq $path,
+          test_msg("Expected '$expected', got '$path'"));
+
+        $expected = 'b';
+        $self->assert($expected eq $xfer_type,
+          test_msg("Expected '$expected', got '$xfer_type'"));
+
+        $expected = '_';
+        $self->assert($expected eq $action_flag,
+          test_msg("Expected '$expected', got '$action_flag'"));
 
-      for (my $i = 0; $i < scalar(@$sftps); $i++) {
-        $sftps->[$i] = undef;
-      }
- 
-      $ssh2->disconnect();
-    };
+        $expected = 'i';
+        $self->assert($expected eq $xfer_direction,
+          test_msg("Expected '$expected', got '$xfer_direction'"));
 
-    if ($@) {
-      $ex = $@;
-    }
+        $expected = 'r';
+        $self->assert($expected eq $access_mode,
+          test_msg("Expected '$expected', got '$access_mode'"));
 
-    $wfh->print("done\n");
-    $wfh->flush();
+        $expected = $user;
+        $self->assert($expected eq $user_name,
+          test_msg("Expected '$expected', got '$user_name'"));
 
-  } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
-    }
+        $expected = 'sftp';
+        $self->assert($expected eq $service_name,
+          test_msg("Expected '$expected', got '$service_name'"));
 
-    exit 0;
-  }
+        $expected = 'i';
+        $self->assert($expected eq $completion_status,
+          test_msg("Expected '$expected', got '$completion_status'"));
 
-  # Stop server
-  server_stop($pid_file);
+        $ok = 1;
+        last;
+      }
+    }
 
-  $self->assert_child_ok($pid);
+    close($fh);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+    unless ($ok) {
+      die("No lines found in $xferlog_file");
+    }
 
-    die($ex);
+  } else {
+    die("Can't read $xferlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_multi_channel_downloads {
+sub sftp_log_extlog_auth_bug3845 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -33945,11 +43642,14 @@ sub sftp_multi_channel_downloads {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    LogFormat => 'login "\"%r\" %s"',
+    ExtendedLog => "$extlog_file AUTH login",
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -33964,39 +43664,6 @@ sub sftp_multi_channel_downloads {
     },
   };
 
-  my $test_file1 = File::Spec->rel2abs("$tmpdir/test1.txt");
-  if (open(my $fh, "> $test_file1")) {
-    print $fh "ABCD" x 8192;
-    unless (close($fh)) {
-      die("Can't write $test_file1: $!");
-    }
-
-  } else {
-    die("Can't open $test_file1: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-  if (open(my $fh, "> $test_file2")) {
-    print $fh "KLMN" x 8192;
-    unless (close($fh)) {
-      die("Can't write $test_file2: $!");
-    }
-
-  } else {
-    die("Can't open $test_file2: $!");
-  }
-
-  my $test_file3 = File::Spec->rel2abs("$tmpdir/test3.txt");
-  if (open(my $fh, "> $test_file3")) {
-    print $fh "UVWX" x 8192;
-    unless (close($fh)) {
-      die("Can't write $test_file3: $!");
-    }
-
-  } else {
-    die("Can't open $test_file3: $!");
-  }
-
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
   # Open pipes, for use between the parent and child processes.  Specifically,
@@ -34011,6 +43678,9 @@ sub sftp_multi_channel_downloads {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -34030,87 +43700,17 @@ sub sftp_multi_channel_downloads {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # Open three different 'sftp' sessions, and make sure they all
-      # work as expected.
-
-      my $sftps = [];
-      my $fhs = [];
-      my $md5s = [];
-
-      for (my $i = 0; $i < 3; $i++) { 
-        my $sftp = $ssh2->sftp();
-        unless ($sftp) {
-          my ($err_code, $err_name, $err_str) = $ssh2->error();
-          die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-        }
-
-        push(@$sftps, $sftp);
-      }
-
-      for (my $i = 0; $i < scalar(@$sftps); $i++) {
-        my $path = "test" . ($i + 1) . ".txt";
-
-        my $test_fh = $sftps->[$i]->open($path, O_RDONLY);
-        unless ($test_fh) {
-          my ($err_code, $err_name) = $sftps->[$i]->error();
-          die("Can't open $path: [$err_name] ($err_code)");
-        }
-
-        my $ctx = Digest::MD5->new();
-
-        # my $read_len = 65535;
-        my $read_len = 32768;
-        my $buf;
-
-        my $res = $test_fh->read($buf, $read_len);
-        unless ($res) {
-          my ($err_code, $err_name) = $sftps->[$i]->error();
-          if ($err_code != 0) {
-            die("Can't read $path: [$err_name] ($err_code)");
-          } else {
-            my $err_str;
-            ($err_code, $err_name, $err_str) = $ssh2->error();
-            die("SSH2 error: [$err_name] ($err_code) $err_str");
-          }
-        }
-
-        while ($res) {
-          $ctx->add($buf);
-          unless ($test_fh->seek($res)) {
-            die("Can't seek to offset $res on $path handle: $!");
-          }
-
-          $res = $test_fh->read($buf, $read_len);
-        }
-
-        push(@$md5s, $ctx->hexdigest());
-
-        push(@$fhs, $test_fh);
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      for (my $i = 0; $i < scalar(@$fhs); $i++) {
-        $fhs->[$i] = undef;
-      }
- 
-      for (my $i = 0; $i < scalar(@$sftps); $i++) {
-        $sftps->[$i] = undef;
-      }
- 
+      $sftp = undef;
       $ssh2->disconnect();
 
-      my $expected;
-
-      $expected = '6e816b2d373a619a29d1706ac6be1db0';
-      $self->assert($expected eq $md5s->[0],
-        test_msg("Expected '$expected', got '$md5s->[0]'"));
-
-      $expected = 'b96129f1efce8f38324adf1a9d1e889c';
-      $self->assert($expected eq $md5s->[1],
-        test_msg("Expected '$expected', got '$md5s->[1]'"));
-
-      $expected = 'b6df9d6dccce294918a51ac7deabfd96';
-      $self->assert($expected eq $md5s->[2],
-        test_msg("Expected '$expected', got '$md5s->[2]'"));
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
@@ -34121,7 +43721,7 @@ sub sftp_multi_channel_downloads {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 30) };
+    eval { server_wait($config_file, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -34142,17 +43742,53 @@ sub sftp_multi_channel_downloads {
     die($ex);
   }
 
+  if (open(my $fh, "< $extlog_file")) {
+    my $user_ok = 0;
+    my $pass_ok = 0;
+
+    while (my $line = <$fh>) {
+      chomp($line);
+
+      if ($line =~ /^"(\S+)\s+(\S+).*"\s+(\d+)$/) {
+        my $cmd = $1;
+        my $cmd_arg = $2;
+        my $resp_code = $3;
+
+        if ($cmd eq 'USER') {
+          if ($resp_code == 331) {
+            $user_ok = 1;
+          }
+
+        } elsif ($cmd eq 'PASS') {
+          if ($resp_code == 230) {
+            $pass_ok = 1;
+          }
+        }
+      }
+    }
+
+    close($fh);
+
+    $self->assert($user_ok,
+      test_msg("Did not see USER command in ExtendedLog $extlog_file as expected"));
+    $self->assert($pass_ok,
+      test_msg("Did not see PASS command in ExtendedLog $extlog_file as expected"));
+
+  } else {
+    die("Can't read $extlog_file: $!");
+  }
+
   unlink($log_file);
 }
 
-sub sftp_log_xferlog_download {
+sub sftp_log_extlog_reads {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -34167,18 +43803,7 @@ sub sftp_log_xferlog_download {
   my $gid = 500;
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 256;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $test_sz = (stat($test_file))[7];
+  my $write_sz = 32;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -34204,12 +43829,13 @@ sub sftp_log_xferlog_download {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    TransferLog => $xferlog_file,
+    LogFormat => 'transfer "%m \"%F\""',
+    ExtendedLog => "$extlog_file READ transfer",
 
     IfModules => {
       'mod_delay.c' => {
@@ -34267,32 +43893,25 @@ sub sftp_log_xferlog_download {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
-
-        $res = $fh->read($buf, 8192);
-      }
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
-
-      $self->assert($test_sz == $size,
-        test_msg("Expected $test_sz, got $size"));
+      my $buf = ("ABCD" x 8);
+      my $res = $fh->write($buf);
+      unless ($res) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't write test.txt: [$err_name] ($err_code)");
+      } 
 
+      # Explicitly disconnect without closing the file, simulating an
+      # aborted transfer.
       $ssh2->disconnect();
+
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
@@ -34324,92 +43943,40 @@ sub sftp_log_xferlog_download {
     die($ex);
   }
 
-  if (open(my $fh, "< $xferlog_file")) {
-    my $ok = 0;
+  if (open(my $fh, "< $extlog_file")) {
+    my $ok = 1;
 
     while (my $line = <$fh>) {
       chomp($line);
 
-     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
-        my $client_addr = $3;
-        my $nbytes = $4;
-        my $path = $5;
-        my $xfer_type = $6;
-        my $action_flag = $7;
-        my $xfer_direction = $8;
-        my $access_mode = $9;
-        my $user_name = $10;
-        my $service_name = $11;
-        my $completion_status = $12;
-
-        my $expected;
-
-        $expected = '127.0.0.1';
-        $self->assert($expected eq $client_addr,
-          test_msg("Expected '$expected', got '$client_addr'"));
-
-        $expected = $test_sz;
-        $self->assert($expected == $nbytes,
-          test_msg("Expected $expected, got $nbytes"));
-
-        $expected = $test_file;
-        $self->assert($expected eq $path,
-          test_msg("Expected '$expected', got '$path'"));
-
-        $expected = 'b';
-        $self->assert($expected eq $xfer_type,
-          test_msg("Expected '$expected', got '$xfer_type'"));
-
-        $expected = '_';
-        $self->assert($expected eq $action_flag,
-          test_msg("Expected '$expected', got '$action_flag'"));
-
-        $expected = 'o';
-        $self->assert($expected eq $xfer_direction,
-          test_msg("Expected '$expected', got '$xfer_direction'"));
-
-        $expected = 'r';
-        $self->assert($expected eq $access_mode,
-          test_msg("Expected '$expected', got '$access_mode'"));
-
-        $expected = $user;
-        $self->assert($expected eq $user_name,
-          test_msg("Expected '$expected', got '$user_name'"));
-
-        $expected = 'sftp';
-        $self->assert($expected eq $service_name,
-          test_msg("Expected '$expected', got '$service_name'"));
-
-        $expected = 'c';
-        $self->assert($expected eq $completion_status,
-          test_msg("Expected '$expected', got '$completion_status'"));
+      # We don't expect to see any lines in this ExtendedLog for the SSH2
+      # connection.
 
-        $ok = 1;
-        last;
-      }
+      $ok = 0;
+      last;
     }
 
     close($fh);
 
     unless ($ok) {
-      die("No lines found in $xferlog_file");
+      die("Lines found unexpectedly in $extlog_file");
     }
 
   } else {
-    die("Can't read $xferlog_file: $!");
+    die("Can't read $extlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_xferlog_download_incomplete {
+sub sftp_log_extlog_read_close {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -34424,18 +43991,7 @@ sub sftp_log_xferlog_download_incomplete {
   my $gid = 500;
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 256;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  my $read_sz = 32;
+  my $write_sz = 32;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -34461,11 +44017,13 @@ sub sftp_log_xferlog_download_incomplete {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TransferLog => $xferlog_file,
+
+    LogFormat => 'transfer "%m \"%F\""',
+    ExtendedLog => "$extlog_file READ transfer",
 
     IfModules => {
       'mod_delay.c' => {
@@ -34505,7 +44063,7 @@ sub sftp_log_xferlog_download_incomplete {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(1);
+      sleep(2);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -34523,23 +44081,22 @@ sub sftp_log_xferlog_download_incomplete {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my $buf;
-      my $res = $fh->read($buf, $read_sz);
+      my $buf = ("ABCD" x 8);
+      my $res = $fh->write($buf);
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't read test.txt: [$err_name] ($err_code)");
+        die("Can't write test.txt: [$err_name] ($err_code)");
       } 
 
-      sleep(1);
+      $fh = undef;
 
-      # Explicitly disconnect without closing the file, simulating an
-      # aborted transfer.
+      $sftp = undef;
       $ssh2->disconnect();
 
       # Give a little time for the server to do its end-of-session thing.
@@ -34568,99 +44125,55 @@ sub sftp_log_xferlog_download_incomplete {
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  if (open(my $fh, "< $xferlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
-        my $client_addr = $3;
-        my $nbytes = $4;
-        my $path = $5;
-        my $xfer_type = $6;
-        my $action_flag = $7;
-        my $xfer_direction = $8;
-        my $access_mode = $9;
-        my $user_name = $10;
-        my $service_name = $11;
-        my $completion_status = $12;
-
-        my $expected;
-
-        $expected = '127.0.0.1';
-        $self->assert($expected eq $client_addr,
-          test_msg("Expected '$expected', got '$client_addr'"));
-
-        $expected = $read_sz;
-        $self->assert($expected == $nbytes,
-          test_msg("Expected $expected, got $nbytes"));
-
-        $expected = $test_file;
-        $self->assert($expected eq $path,
-          test_msg("Expected '$expected', got '$path'"));
-
-        $expected = 'b';
-        $self->assert($expected eq $xfer_type,
-          test_msg("Expected '$expected', got '$xfer_type'"));
-
-        $expected = '_';
-        $self->assert($expected eq $action_flag,
-          test_msg("Expected '$expected', got '$action_flag'"));
+  eval {
+    if (open(my $fh, "< $extlog_file")) {
+      my $ok = 1;
 
-        $expected = 'o';
-        $self->assert($expected eq $xfer_direction,
-          test_msg("Expected '$expected', got '$xfer_direction'"));
+      while (my $line = <$fh>) {
+        chomp($line);
 
-        $expected = 'r';
-        $self->assert($expected eq $access_mode,
-          test_msg("Expected '$expected', got '$access_mode'"));
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR " - ExtendedLog: $line\n";
+        }
 
-        $expected = $user;
-        $self->assert($expected eq $user_name,
-          test_msg("Expected '$expected', got '$user_name'"));
+        # We don't expect to see any lines in this ExtendedLog for the SSH2
+        # connection.
 
-        $expected = 'sftp';
-        $self->assert($expected eq $service_name,
-          test_msg("Expected '$expected', got '$service_name'"));
+        $ok = 0;
+      }
 
-        $expected = 'i';
-        $self->assert($expected eq $completion_status,
-          test_msg("Expected '$expected', got '$completion_status'"));
+      close($fh);
 
-        $ok = 1;
-        last;
+      unless ($ok) {
+        die("Lines found unexpectedly in $extlog_file");
       }
-    }
-
-    close($fh);
 
-    unless ($ok) {
-      die("No lines found in $xferlog_file");
+    } else {
+      die("Can't read $extlog_file: $!");
     }
+  };
+  if ($@) {
+    $ex = $@;
+  }
 
-  } else {
-    die("Can't read $xferlog_file: $!");
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_xferlog_delete {
+sub sftp_log_extlog_write_close {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -34674,18 +44187,6 @@ sub sftp_log_xferlog_delete {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -34705,17 +44206,29 @@ sub sftp_log_xferlog_delete {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    TransferLog => $xferlog_file,
+    LogFormat => 'transfer "%m \"%F\""',
+    ExtendedLog => "$extlog_file WRITE transfer",
 
     IfModules => {
       'mod_delay.c' => {
@@ -34755,7 +44268,7 @@ sub sftp_log_xferlog_delete {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(1);
+      sleep(2);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -34773,14 +44286,35 @@ sub sftp_log_xferlog_delete {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->unlink('test.txt');
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $read_len = 8192;
+
+      my $res = $fh->read($buf, $read_len);
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't delete test.txt: [$err_name] ($err_code)");
+        if ($err_code != 0) {
+          die("Can't read test.txt: [$err_name] ($err_code)");
+
+        } else {
+          my $err_str;
+          ($err_code, $err_name, $err_str) = $ssh2->error();
+          die("SSH2 error: [$err_name] ($err_code) $err_str");
+        }
       }
 
+      $fh = undef;
+
       $sftp = undef;
       $ssh2->disconnect();
+
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
@@ -34805,95 +44339,55 @@ sub sftp_log_xferlog_delete {
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  if (open(my $fh, "< $xferlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
-        my $client_addr = $3;
-        my $nbytes = $4;
-        my $path = $5;
-        my $xfer_type = $6;
-        my $action_flag = $7;
-        my $xfer_direction = $8;
-        my $access_mode = $9;
-        my $user_name = $10;
-        my $service_name = $11;
-        my $completion_status = $12;
-
-        my $expected;
-
-        $expected = '127.0.0.1';
-        $self->assert($expected eq $client_addr,
-          test_msg("Expected '$expected', got '$client_addr'"));
-
-        $expected = $test_file;
-        $self->assert($expected eq $path,
-          test_msg("Expected '$expected', got '$path'"));
-
-        $expected = 'b';
-        $self->assert($expected eq $xfer_type,
-          test_msg("Expected '$expected', got '$xfer_type'"));
-
-        $expected = '_';
-        $self->assert($expected eq $action_flag,
-          test_msg("Expected '$expected', got '$action_flag'"));
+  eval {
+    if (open(my $fh, "< $extlog_file")) {
+      my $ok = 1;
 
-        $expected = 'd';
-        $self->assert($expected eq $xfer_direction,
-          test_msg("Expected '$expected', got '$xfer_direction'"));
+      while (my $line = <$fh>) {
+        chomp($line);
 
-        $expected = 'r';
-        $self->assert($expected eq $access_mode,
-          test_msg("Expected '$expected', got '$access_mode'"));
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR " - ExtendedLog: $line\n";
+        }
 
-        $expected = $user;
-        $self->assert($expected eq $user_name,
-          test_msg("Expected '$expected', got '$user_name'"));
+        # We don't expect to see any lines in this ExtendedLog for the SSH2
+        # connection.
 
-        $expected = 'sftp';
-        $self->assert($expected eq $service_name,
-          test_msg("Expected '$expected', got '$service_name'"));
+        $ok = 0;
+      }
 
-        $expected = 'c';
-        $self->assert($expected eq $completion_status,
-          test_msg("Expected '$expected', got '$completion_status'"));
+      close($fh);
 
-        $ok = 1;
-        last;
+      unless ($ok) {
+        die("Lines found unexpectedly in $extlog_file");
       }
-    }
-
-    close($fh);
 
-    unless ($ok) {
-      die("No lines found in $xferlog_file");
+    } else {
+      die("Can't read $extlog_file: $!");
     }
+  };
+  if ($@) {
+    $ex = $@;
+  }
 
-  } else {
-    die("Can't read $xferlog_file: $!");
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_xferlog_delete_chrooted {
+sub sftp_log_extlog_var_s_reads {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -34908,16 +44402,7 @@ sub sftp_log_xferlog_delete_chrooted {
   my $gid = 500;
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
+  my $write_sz = 32;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -34931,6 +44416,22 @@ sub sftp_log_xferlog_delete_chrooted {
     }
   }
 
+  # Create some files in the home directory to read
+  for (my $i = 0; $i < 5; $i++) {
+    my $path = File::Spec->rel2abs("$home_dir/$i.txt");
+
+    if (open(my $fh, "> $path")) {
+      print $fh "ABCD\n" x 64;
+
+      unless (close($fh)) {
+        die("Can't write $path: $!");
+      }
+
+    } else {
+      die("Can't open $path: $!");
+    }
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -34943,13 +44444,13 @@ sub sftp_log_xferlog_delete_chrooted {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
 
-    TransferLog => $xferlog_file,
+    LogFormat => 'response "%r %s"',
+    ExtendedLog => "$extlog_file READ response",
 
     IfModules => {
       'mod_delay.c' => {
@@ -35007,14 +44508,58 @@ sub sftp_log_xferlog_delete_chrooted {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->unlink('test.txt');
-      unless ($res) {
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't delete test.txt: [$err_name] ($err_code)");
+        die("Can't open directory '.': [$err_name] ($err_code)");
       }
 
-      $sftp = undef;
+      my $files = {};
+
+      my $file = $dir->read();
+      while ($file) {
+        $files->{$file->{name}} = $file;
+
+        $file = $dir->read();
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
+      $dir = undef;
+
+      foreach my $file (keys(%$files)) {
+        next unless $file =~ /\.txt$/;
+
+        my $fh = $sftp->open($file, O_RDONLY);
+        unless ($fh) {
+          my ($err_code, $err_name) = $sftp->error();
+          die("Can't open $file: [$err_name] ($err_code)");
+        }
+
+        my $read_len = 32768;
+        my $buf;
+
+        my $res = $fh->read($buf, $read_len);
+        unless ($res) {
+          my ($err_code, $err_name) = $sftp->error();
+          if ($err_code != 0) {
+            die("Can't read $file: [$err_name] ($err_code)");
+
+          } else {
+            my $err_str;
+            ($err_code, $err_name, $err_str) = $ssh2->error();
+            die("SSH2 error: [$err_name] ($err_code) $err_str");
+          }
+        }
+
+        $fh = undef;
+      }
+
+      # Explicitly disconnect without closing the file, simulating an
+      # aborted transfer.
       $ssh2->disconnect();
+
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
@@ -35046,88 +44591,57 @@ sub sftp_log_xferlog_delete_chrooted {
     die($ex);
   }
 
-  if (open(my $fh, "< $xferlog_file")) {
-    my $ok = 0;
-
+  if (open(my $fh, "< $extlog_file")) {
     while (my $line = <$fh>) {
       chomp($line);
 
-     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
-        my $client_addr = $3;
-        my $nbytes = $4;
-        my $path = $5;
-        my $xfer_type = $6;
-        my $action_flag = $7;
-        my $xfer_direction = $8;
-        my $access_mode = $9;
-        my $user_name = $10;
-        my $service_name = $11;
-        my $completion_status = $12;
+      if ($line =~ /(\S+)\s+\S+\s+(\S+)$/) {
+        my $cmd = $1;
+        my $code = $2;
 
         my $expected;
+ 
+        if ($cmd eq 'OPEN') {
+          $expected = '-';
 
-        $expected = '127.0.0.1';
-        $self->assert($expected eq $client_addr,
-          test_msg("Expected '$expected', got '$client_addr'"));
-
-        $expected = $test_file;
-        $self->assert($expected eq $path,
-          test_msg("Expected '$expected', got '$path'"));
-
-        $expected = 'b';
-        $self->assert($expected eq $xfer_type,
-          test_msg("Expected '$expected', got '$xfer_type'"));
-
-        $expected = '_';
-        $self->assert($expected eq $action_flag,
-          test_msg("Expected '$expected', got '$action_flag'"));
-
-        $expected = 'd';
-        $self->assert($expected eq $xfer_direction,
-          test_msg("Expected '$expected', got '$xfer_direction'"));
+        } elsif ($cmd eq 'CLOSE') {
+          $expected = '0';
 
-        $expected = 'r';
-        $self->assert($expected eq $access_mode,
-          test_msg("Expected '$expected', got '$access_mode'"));
+        } elsif ($cmd eq 'READ') {
+          $expected = '(-|1)';
 
-        $expected = $user;
-        $self->assert($expected eq $user_name,
-          test_msg("Expected '$expected', got '$user_name'"));
+        } elsif ($cmd eq 'RETR') {
+          $expected = '-';
 
-        $expected = 'sftp';
-        $self->assert($expected eq $service_name,
-          test_msg("Expected '$expected', got '$service_name'"));
+        } else {
+          die("Unexpected command '$cmd' in $extlog_file");
+        }
 
-        $expected = 'c';
-        $self->assert($expected eq $completion_status,
-          test_msg("Expected '$expected', got '$completion_status'"));
+        $self->assert(qr/$expected/, $code,
+          test_msg("Expected '$expected', got '$code'"));
 
-        $ok = 1;
-        last;
+      } else {
+        die("Unexpected line '$line' in $extlog_file");
       }
     }
 
     close($fh);
 
-    unless ($ok) {
-      die("No lines found in $xferlog_file");
-    }
-
   } else {
-    die("Can't read $xferlog_file: $!");
+    die("Can't read $extlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_xferlog_upload {
+sub sftp_log_extlog_var_s_writes {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -35142,7 +44656,7 @@ sub sftp_log_xferlog_upload {
   my $gid = 500;
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $write_sz = 1024;
+  my $write_sz = 32;
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -35156,6 +44670,22 @@ sub sftp_log_xferlog_upload {
     }
   }
 
+  # Create some files in the home directory to read
+  for (my $i = 0; $i < 5; $i++) {
+    my $path = File::Spec->rel2abs("$home_dir/$i.txt");
+
+    if (open(my $fh, "> $path")) {
+      print $fh "ABCD\n" x 64;
+
+      unless (close($fh)) {
+        die("Can't write $path: $!");
+      }
+
+    } else {
+      die("Can't open $path: $!");
+    }
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -35168,12 +44698,13 @@ sub sftp_log_xferlog_upload {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    TransferLog => $xferlog_file,
+    LogFormat => 'response "%r %s"',
+    ExtendedLog => "$extlog_file WRITE response",
 
     IfModules => {
       'mod_delay.c' => {
@@ -35191,6 +44722,20 @@ sub sftp_log_xferlog_upload {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Limit FSETSTAT SETSTAT>
+  DenyAll
+</Limit>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -35231,22 +44776,26 @@ sub sftp_log_xferlog_upload {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't open 'test.txt': [$err_name] ($err_code)");
       }
 
-      my $buf = ("ABCD" x 256);
-      print $fh $buf;
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      # To close the SFTP channel, we have to explicitly destroy the object
-      $sftp = undef;
+      # This is expected to fail because of the <Limit>
+      $fh->setstat(atime => 0, mtime => 0);
+   
+      print $fh "abcd" x 1024, "\n";
 
+      # This is expected to fail because of the <Limit>
+      $fh->setstat(atime => 0, mtime => 0);
+  
+      # Explicitly disconnect without closing the file, simulating an
+      # aborted transfer.
       $ssh2->disconnect();
+
+      # Give a little time for the server to do its end-of-session thing.
+      sleep(1);
     };
 
     if ($@) {
@@ -35278,92 +44827,60 @@ sub sftp_log_xferlog_upload {
     die($ex);
   }
 
-  if (open(my $fh, "< $xferlog_file")) {
-    my $ok = 0;
-
+  if (open(my $fh, "< $extlog_file")) {
     while (my $line = <$fh>) {
       chomp($line);
 
-     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
-        my $client_addr = $3;
-        my $nbytes = $4;
-        my $path = $5;
-        my $xfer_type = $6;
-        my $action_flag = $7;
-        my $xfer_direction = $8;
-        my $access_mode = $9;
-        my $user_name = $10;
-        my $service_name = $11;
-        my $completion_status = $12;
+      if ($line =~ /(\S+)\s+\S+\s+(\S+)/) {
+        my $cmd = $1;
+        my $code = $2;
 
         my $expected;
+ 
+        if ($cmd eq 'OPEN') {
+          $expected = '-';
 
-        $expected = '127.0.0.1';
-        $self->assert($expected eq $client_addr,
-          test_msg("Expected '$expected', got '$client_addr'"));
-
-        $expected = $write_sz;
-        $self->assert($expected == $nbytes,
-          test_msg("Expected $expected, got $nbytes"));
-
-        $expected = $test_file;
-        $self->assert($expected eq $path,
-          test_msg("Expected '$expected', got '$path'"));
-
-        $expected = 'b';
-        $self->assert($expected eq $xfer_type,
-          test_msg("Expected '$expected', got '$xfer_type'"));
-
-        $expected = '_';
-        $self->assert($expected eq $action_flag,
-          test_msg("Expected '$expected', got '$action_flag'"));
+        } elsif ($cmd eq 'CLOSE') {
+          $expected = '0';
 
-        $expected = 'i';
-        $self->assert($expected eq $xfer_direction,
-          test_msg("Expected '$expected', got '$xfer_direction'"));
+        } elsif ($cmd eq 'WRITE') {
+          $expected = '0';
 
-        $expected = 'r';
-        $self->assert($expected eq $access_mode,
-          test_msg("Expected '$expected', got '$access_mode'"));
+        } elsif ($cmd eq 'FSETSTAT') {
+          $expected = '3';
 
-        $expected = $user;
-        $self->assert($expected eq $user_name,
-          test_msg("Expected '$expected', got '$user_name'"));
+        } elsif ($cmd eq 'STOR') {
+          $expected = '-';
 
-        $expected = 'sftp';
-        $self->assert($expected eq $service_name,
-          test_msg("Expected '$expected', got '$service_name'"));
+        } else {
+          die("Unexpected command '$cmd' in $extlog_file");
+        }
 
-        $expected = 'c';
-        $self->assert($expected eq $completion_status,
-          test_msg("Expected '$expected', got '$completion_status'"));
+        $self->assert(qr/$expected/, $code,
+          test_msg("Expected '$expected', got '$code'"));
 
-        $ok = 1;
-        last;
+      } else {
+        die("Unexpected line '$line' in $extlog_file");
       }
     }
 
     close($fh);
 
-    unless ($ok) {
-      die("No lines found in $xferlog_file");
-    }
-
   } else {
-    die("Can't read $xferlog_file: $!");
+    die("Can't read $extlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_xferlog_upload_incomplete {
+sub sftp_log_extlog_file_modified_bug3457 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $xferlog_file = File::Spec->rel2abs("$tmpdir/xfer.log");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -35378,7 +44895,15 @@ sub sftp_log_xferlog_upload_incomplete {
   my $gid = 500;
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $write_sz = 32;
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -35404,11 +44929,14 @@ sub sftp_log_xferlog_upload_incomplete {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    TransferLog => $xferlog_file,
+
+    AllowOverwrite => 'on',
+    LogFormat => 'custom "%{file-modified}"',
+    ExtendedLog => "$extlog_file WRITE custom",
 
     IfModules => {
       'mod_delay.c' => {
@@ -35466,21 +44994,14 @@ sub sftp_log_xferlog_upload_incomplete {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't open 'test.txt': [$err_name] ($err_code)");
       }
 
-      my $buf = ("ABCD" x 8);
-      my $res = $fh->write($buf);
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't write test.txt: [$err_name] ($err_code)");
-      } 
-
-      # Explicitly disconnect without closing the file, simulating an
-      # aborted transfer.
+      $fh = undef;
+      $sftp = undef;
       $ssh2->disconnect();
 
       # Give a little time for the server to do its end-of-session thing.
@@ -35516,85 +45037,25 @@ sub sftp_log_xferlog_upload_incomplete {
     die($ex);
   }
 
-  if (open(my $fh, "< $xferlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-     if ($line =~ /^(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(\d+)\s+(.*?)\s+(\d+)\s+(.*?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s+(.*?)\s+.*?(\S+)$/o) {
-        my $client_addr = $3;
-        my $nbytes = $4;
-        my $path = $5;
-        my $xfer_type = $6;
-        my $action_flag = $7;
-        my $xfer_direction = $8;
-        my $access_mode = $9;
-        my $user_name = $10;
-        my $service_name = $11;
-        my $completion_status = $12;
-
-        my $expected;
-
-        $expected = '127.0.0.1';
-        $self->assert($expected eq $client_addr,
-          test_msg("Expected '$expected', got '$client_addr'"));
-
-        $expected = $write_sz;
-        $self->assert($expected == $nbytes,
-          test_msg("Expected $expected, got $nbytes"));
-
-        $expected = $test_file;
-        $self->assert($expected eq $path,
-          test_msg("Expected '$expected', got '$path'"));
-
-        $expected = 'b';
-        $self->assert($expected eq $xfer_type,
-          test_msg("Expected '$expected', got '$xfer_type'"));
-
-        $expected = '_';
-        $self->assert($expected eq $action_flag,
-          test_msg("Expected '$expected', got '$action_flag'"));
-
-        $expected = 'i';
-        $self->assert($expected eq $xfer_direction,
-          test_msg("Expected '$expected', got '$xfer_direction'"));
-
-        $expected = 'r';
-        $self->assert($expected eq $access_mode,
-          test_msg("Expected '$expected', got '$access_mode'"));
-
-        $expected = $user;
-        $self->assert($expected eq $user_name,
-          test_msg("Expected '$expected', got '$user_name'"));
-
-        $expected = 'sftp';
-        $self->assert($expected eq $service_name,
-          test_msg("Expected '$expected', got '$service_name'"));
-
-        $expected = 'i';
-        $self->assert($expected eq $completion_status,
-          test_msg("Expected '$expected', got '$completion_status'"));
-
-        $ok = 1;
-        last;
-      }
+  if (open(my $fh, "< $extlog_file")) {
+    while (my $line = <$fh>) {
+      chomp($line);
+
+      my $expected = 'true';
+      $self->assert($expected eq $line,
+        test_msg("Expected '$expected', got '$line'"));
     }
 
     close($fh);
 
-    unless ($ok) {
-      die("No lines found in $xferlog_file");
-    }
-
   } else {
-    die("Can't read $xferlog_file: $!");
+    die("Can't read $extlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_extlog_auth_bug3845 {
+sub sftp_log_extlog_retr_file_size {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -35615,6 +45076,19 @@ sub sftp_log_extlog_auth_bug3845 {
   my $uid = 500;
   my $gid = 500;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_sz = 32;
+
+  if (open(my $fh, "> $test_file")) {
+    print $fh "A" x $test_sz;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -35644,8 +45118,8 @@ sub sftp_log_extlog_auth_bug3845 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'login "\"%r\" %s"',
-    ExtendedLog => "$extlog_file AUTH login",
+    LogFormat => 'transfer "%m %b',
+    ExtendedLog => "$extlog_file READ transfer",
 
     IfModules => {
       'mod_delay.c' => {
@@ -35703,11 +45177,30 @@ sub sftp_log_extlog_auth_bug3845 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
+      my $fh = $sftp->open('test.txt', O_RDONLY);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $buf;
+      my $size = 0;
+
+      my $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
       $sftp = undef;
       $ssh2->disconnect();
 
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
+      $self->assert($test_sz == $size,
+        test_msg("Expected $test_sz, got $size"));
     };
 
     if ($@) {
@@ -35740,36 +45233,26 @@ sub sftp_log_extlog_auth_bug3845 {
   }
 
   if (open(my $fh, "< $extlog_file")) {
-    my $user_ok = 0;
-    my $pass_ok = 0;
+    my $ok = 0;
 
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /^"(\S+)\s+(\S+).*"\s+(\d+)$/) {
-        my $cmd = $1;
-        my $cmd_arg = $2;
-        my $resp_code = $3;
-
-        if ($cmd eq 'USER') {
-          if ($resp_code == 331) {
-            $user_ok = 1;
-          }
+      if ($line =~ /^RETR (\d+)/) {
+        my $xfer_sz = $1;
 
-        } elsif ($cmd eq 'PASS') {
-          if ($resp_code == 230) {
-            $pass_ok = 1;
-          }
+        if ($xfer_sz == $test_sz) {
+          $ok = 1;
+          last;
         }
       }
     }
 
     close($fh);
 
-    $self->assert($user_ok,
-      test_msg("Did not see USER command in ExtendedLog $extlog_file as expected"));
-    $self->assert($pass_ok,
-      test_msg("Did not see PASS command in ExtendedLog $extlog_file as expected"));
+    unless ($ok) {
+      die("Missing expected RETR file size ($test_sz) in $extlog_file");
+    }
 
   } else {
     die("Can't read $extlog_file: $!");
@@ -35778,7 +45261,7 @@ sub sftp_log_extlog_auth_bug3845 {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_reads {
+sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -35799,8 +45282,31 @@ sub sftp_log_extlog_reads {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $write_sz = 32;
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_sz1 = 32;
+
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "A" x $test_sz1;
+    unless (close($fh)) {
+      die("Can't write $test_file1: $!");
+    }
+
+  } else {
+    die("Can't open $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
+  my $test_sz2 = 64;
+
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "A" x $test_sz2;
+    unless (close($fh)) {
+      die("Can't write $test_file2: $!");
+    }
+
+  } else {
+    die("Can't open $test_file2: $!");
+  }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -35831,7 +45337,7 @@ sub sftp_log_extlog_reads {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'transfer "%m \"%F\""',
+    LogFormat => 'transfer "%m %b',
     ExtendedLog => "$extlog_file READ transfer",
 
     IfModules => {
@@ -35890,25 +45396,85 @@ sub sftp_log_extlog_reads {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      # PuTTy's 'mget' does:
+      #
+      # OPENDIR
+      # READDIR
+      # <check for matching names>
+      # OPEN matching name
+      # READ
+      # CLOSE
+      # READDIR
+      # OPEN matching name
+      # READ
+      # CLOSE
+      # CLOSE <dirhandle>
+
+      # OPENDIR
+      my $dir = $sftp->opendir('.');
+      unless ($dir) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open directory '.': [$err_name] ($err_code)");
+      }
+
+      my $res = {};
+
+      # READDIR
+      my $file = $dir->read();
+      while ($file) {
+        $res->{$file->{name}} = $file;
+        $file = $dir->read();
+      }
+
+      # OPEN file1
+      my $fh = $sftp->open('test.txt', O_RDONLY);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my $buf = ("ABCD" x 8);
-      my $res = $fh->write($buf);
-      unless ($res) {
+      my $buf;
+      my $size = 0;
+
+      # READ file1
+      $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # CLOSE file1
+      $fh = undef;
+
+      # OPEN file2
+      $fh = $sftp->open('test2.txt', O_RDONLY);
+      unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't write test.txt: [$err_name] ($err_code)");
-      } 
+        die("Can't open test2.txt: [$err_name] ($err_code)");
+      }
 
-      # Explicitly disconnect without closing the file, simulating an
-      # aborted transfer.
-      $ssh2->disconnect();
+      $buf = '';
+      $size = 0;
 
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
+      # READ file2
+      $res = $fh->read($buf, 8192);
+      while ($res) {
+        $size += $res;
+
+        $res = $fh->read($buf, 8192);
+      }
+
+      # CLOSE file2
+      $fh = undef;
+
+      # CLOSEDIR
+      $dir = undef;
+
+      # CHANNEL_CLOSE
+      $sftp = undef;
+
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -35941,22 +45507,35 @@ sub sftp_log_extlog_reads {
   }
 
   if (open(my $fh, "< $extlog_file")) {
-    my $ok = 1;
+    my $test_ok = 0;
+    my $test2_ok = 0;
 
     while (my $line = <$fh>) {
       chomp($line);
 
-      # We don't expect to see any lines in this ExtendedLog for the SSH2
-      # connection.
+      if ($line =~ /^RETR (\d+)/) {
+        my $xfer_sz = $1;
 
-      $ok = 0;
-      last;
+        if ($xfer_sz == $test_sz1) {
+          $test_ok = 1;
+          next;
+        }
+
+        if ($xfer_sz == $test_sz2) {
+          $test2_ok = 1;
+          last;
+        }
+      }
     }
 
     close($fh);
 
-    unless ($ok) {
-      die("Lines found unexpectedly in $extlog_file");
+    unless ($test_ok) {
+      die("Missing expected RETR file size ($test_sz1) in $extlog_file");
+    }
+
+    unless ($test2_ok) {
+      die("Missing expected RETR file size ($test_sz2) in $extlog_file");
     }
 
   } else {
@@ -35966,7 +45545,7 @@ sub sftp_log_extlog_reads {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_read_close {
+sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -35987,9 +45566,6 @@ sub sftp_log_extlog_read_close {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $write_sz = 32;
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -36009,6 +45585,8 @@ sub sftp_log_extlog_read_close {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -36018,9 +45596,10 @@ sub sftp_log_extlog_read_close {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
-    LogFormat => 'transfer "%m \"%F\""',
-    ExtendedLog => "$extlog_file READ transfer",
+    LogFormat => 'dirlog "%m %F"',
+    ExtendedLog => "$extlog_file WRITE dirlog",
 
     IfModules => {
       'mod_delay.c' => {
@@ -36060,7 +45639,7 @@ sub sftp_log_extlog_read_close {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(2);
+      sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -36078,26 +45657,24 @@ sub sftp_log_extlog_read_close {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
+      my $res = $sftp->mkdir('testdir');
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+        die("Can't mkdir testdir: [$err_name] ($err_code)");
       }
 
-      my $buf = ("ABCD" x 8);
-      my $res = $fh->write($buf);
+      $res = $sftp->rmdir('testdir');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't write test.txt: [$err_name] ($err_code)");
-      } 
-
-      $fh = undef;
+        die("Can't rmdir testdir: [$err_name] ($err_code)");
+      }
 
       $sftp = undef;
       $ssh2->disconnect();
 
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
+      if (-d $test_dir) {
+        die("$test_dir directory exists unexpectedly");
+      }
     };
 
     if ($@) {
@@ -36122,48 +45699,61 @@ sub sftp_log_extlog_read_close {
 
   $self->assert_child_ok($pid);
 
-  eval {
-    if (open(my $fh, "< $extlog_file")) {
-      my $ok = 1;
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
 
-      while (my $line = <$fh>) {
-        chomp($line);
+    die($ex);
+  }
 
-        if ($ENV{TEST_VERBOSE}) {
-          print STDERR " - ExtendedLog: $line\n";
-        }
+  if (open(my $fh, "< $extlog_file")) {
+    my $mkdir_ok = 0;
+    my $rmdir_ok = 0;
 
-        # We don't expect to see any lines in this ExtendedLog for the SSH2
-        # connection.
+    while (my $line = <$fh>) {
+      chomp($line);
 
-        $ok = 0;
-      }
+      if ($line =~ /(\S+)\s+(\S+)$/) {
+        my $cmd = $1;
+        my $dir_path = $2;
 
-      close($fh);
+        if ($cmd eq 'MKD' ||
+            $cmd eq 'RMD') {
+            my $expected = '/testdir';
 
-      unless ($ok) {
-        die("Lines found unexpectedly in $extlog_file");
-      }
+            $self->assert($expected eq $dir_path,
+              test_msg("Expected '$expected', got '$dir_path'"));
 
-    } else {
-      die("Can't read $extlog_file: $!");
+        } elsif ($cmd eq 'MKDIR') {
+          $mkdir_ok = 1;
+
+        } elsif ($cmd eq 'RMDIR') {
+          $rmdir_ok = 1;
+
+        } else {
+          die("Unexpected command '$cmd' in $extlog_file");
+        }
+
+      } else {
+        die("Unexpected line '$line' in $extlog_file");
+      }
     }
-  };
-  if ($@) {
-    $ex = $@;
-  }
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+    close($fh);
 
-    die($ex);
+    $self->assert($mkdir_ok == 1,
+      test_msg("Expected to see 'MKDIR' command but it was missing"));
+    $self->assert($rmdir_ok == 1,
+      test_msg("Expected to see 'RMDIR' command but it was missing"));
+
+  } else {
+    die("Can't read $extlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_extlog_write_close {
+sub sftp_log_extlog_var_w_rename_bug3029 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -36184,8 +45774,19 @@ sub sftp_log_extlog_write_close {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $write_sz = 32;
+  my $test_file1 = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "ABCD" x 8192;
+
+    unless (close($fh)) {
+      die("Can't write $test_file1: $!");
+    }
+
+  } else {
+    die("Can't open $test_file1: $!");
+  }
+
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -36206,17 +45807,6 @@ sub sftp_log_extlog_write_close {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -36227,8 +45817,8 @@ sub sftp_log_extlog_write_close {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'transfer "%m \"%F\""',
-    ExtendedLog => "$extlog_file WRITE transfer",
+    LogFormat => 'rename "%m: %w %f"',
+    ExtendedLog => "$extlog_file ALL rename",
 
     IfModules => {
       'mod_delay.c' => {
@@ -36268,7 +45858,7 @@ sub sftp_log_extlog_write_close {
     eval {
       my $ssh2 = Net::SSH2->new();
 
-      sleep(2);
+      sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
@@ -36286,35 +45876,22 @@ sub sftp_log_extlog_write_close {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $buf;
-      my $read_len = 8192;
-
-      my $res = $fh->read($buf, $read_len);
+      my $res = $sftp->rename('test.txt', 'test2.txt');
       unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        if ($err_code != 0) {
-          die("Can't read test.txt: [$err_name] ($err_code)");
-
-        } else {
-          my $err_str;
-          ($err_code, $err_name, $err_str) = $ssh2->error();
-          die("SSH2 error: [$err_name] ($err_code) $err_str");
-        }
+        die("Can't rename test.txt to test2.txt: [$err_name] ($err_code)");
       }
 
-      $fh = undef;
-
       $sftp = undef;
       $ssh2->disconnect();
 
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
+      if (-f $test_file1) {
+        die("$test_file1 file exists unexpectedly");
+      }
+
+      unless (-f $test_file2) {
+        die("$test_file2 file does not exist as expected");
+      }
     };
 
     if ($@) {
@@ -36339,48 +45916,91 @@ sub sftp_log_extlog_write_close {
 
   $self->assert_child_ok($pid);
 
-  eval {
-    if (open(my $fh, "< $extlog_file")) {
-      my $ok = 1;
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
 
-      while (my $line = <$fh>) {
-        chomp($line);
+    die($ex);
+  }
 
-        if ($ENV{TEST_VERBOSE}) {
-          print STDERR " - ExtendedLog: $line\n";
-        }
+  if (open(my $fh, "< $extlog_file")) {
+    my $ok = 0;
 
-        # We don't expect to see any lines in this ExtendedLog for the SSH2
-        # connection.
+    while (my $line = <$fh>) {
+      chomp($line);
 
-        $ok = 0;
-      }
+      if ($line =~ /(\S+):\s+(\S+)\s+(\S+)$/) {
+        my $cmd = $1;
+        my $whence = $2;
+        my $whither = $3;
 
-      close($fh);
+        my $expected;
 
-      unless ($ok) {
-        die("Lines found unexpectedly in $extlog_file");
-      }
+        if ($cmd eq 'RNFR') {
 
-    } else {
-      die("Can't read $extlog_file: $!");
+          $expected = '-';
+          $self->assert($expected eq $whence,
+            test_msg("Expected '$expected', got '$whence'"));
+ 
+          $expected = $test_file1;
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack to deal with how it handles tmp files
+            $expected = ('/private' . $expected);
+          }
+
+          $self->assert($expected eq $whither,
+            test_msg("Expected '$expected', got '$whither'"));
+
+        } elsif ($cmd eq 'RNTO') {
+          $expected = $test_file1;
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack to deal with how it handles tmp files
+            $expected = ('/private' . $expected);
+          }
+
+          $self->assert($expected eq $whence,
+            test_msg("Expected '$expected', got '$whence'"));
+
+          $expected = $test_file2;
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack to deal with how it handles tmp files
+            $expected = ('/private' . $expected);
+          }
+
+          $self->assert($expected eq $whither,
+            test_msg("Expected '$expected', got '$whither'"));
+
+        } elsif ($cmd eq 'RENAME') {
+          $expected = '-';
+
+          $self->assert($expected eq $whence,
+            test_msg("Expected '$expected', got '$whence'"));
+          $self->assert($expected eq $whither,
+            test_msg("Expected '$expected', got '$whither'"));
+
+          $ok = 1;
+
+        } else {
+          next;
+        }
+
+      } else {
+        die("Unexpected line '$line' in $extlog_file");
+      }
     }
-  };
-  if ($@) {
-    $ex = $@;
-  }
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+    close($fh);
 
-    die($ex);
+    $self->assert($ok, test_msg("Did not find expected ExtendedLog lines"));
+
+  } else {
+    die("Can't read $extlog_file: $!");
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_s_reads {
+sub sftp_log_extlog_var_f_remove {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -36401,7 +46021,6 @@ sub sftp_log_extlog_var_s_reads {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   my $write_sz = 32;
 
   # Make sure that, if we're running as root, that the home directory has
@@ -36416,20 +46035,16 @@ sub sftp_log_extlog_var_s_reads {
     }
   }
 
-  # Create some files in the home directory to read
-  for (my $i = 0; $i < 5; $i++) {
-    my $path = File::Spec->rel2abs("$home_dir/$i.txt");
-
-    if (open(my $fh, "> $path")) {
-      print $fh "ABCD\n" x 64;
-
-      unless (close($fh)) {
-        die("Can't write $path: $!");
-      }
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD\n" x 64;
 
-    } else {
-      die("Can't open $path: $!");
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
     }
+
+  } else {
+    die("Can't open $test_file: $!");
   }
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
@@ -36449,8 +46064,8 @@ sub sftp_log_extlog_var_s_reads {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'response "%r %s"',
-    ExtendedLog => "$extlog_file READ response",
+    LogFormat => 'delete "%m %f"',
+    ExtendedLog => "$extlog_file WRITE delete",
 
     IfModules => {
       'mod_delay.c' => {
@@ -36508,58 +46123,18 @@ sub sftp_log_extlog_var_s_reads {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
+      my $res = $sftp->unlink('test.txt');
+      unless ($res) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $files = {};
-
-      my $file = $dir->read();
-      while ($file) {
-        $files->{$file->{name}} = $file;
-
-        $file = $dir->read();
-      }
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the dirhandle
-      $dir = undef;
-
-      foreach my $file (keys(%$files)) {
-        next unless $file =~ /\.txt$/;
-
-        my $fh = $sftp->open($file, O_RDONLY);
-        unless ($fh) {
-          my ($err_code, $err_name) = $sftp->error();
-          die("Can't open $file: [$err_name] ($err_code)");
-        }
-
-        my $read_len = 32768;
-        my $buf;
-
-        my $res = $fh->read($buf, $read_len);
-        unless ($res) {
-          my ($err_code, $err_name) = $sftp->error();
-          if ($err_code != 0) {
-            die("Can't read $file: [$err_name] ($err_code)");
-
-          } else {
-            my $err_str;
-            ($err_code, $err_name, $err_str) = $ssh2->error();
-            die("SSH2 error: [$err_name] ($err_code) $err_str");
-          }
-        }
-
-        $fh = undef;
+        die("Can't remove test.txt: [$err_name] ($err_code)");
       }
 
-      # Explicitly disconnect without closing the file, simulating an
-      # aborted transfer.
+      $sftp = undef;
       $ssh2->disconnect();
 
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
+      if (-f $test_file) {
+        die("$test_file file exists unexpectedly");
+      }
     };
 
     if ($@) {
@@ -36592,41 +46167,36 @@ sub sftp_log_extlog_var_s_reads {
   }
 
   if (open(my $fh, "< $extlog_file")) {
+    my $ok = 0;
+
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /(\S+)\s+\S+\s+(\S+)$/) {
+      if ($line =~ /(\S+) (.*)$/) {
         my $cmd = $1;
-        my $code = $2;
-
-        my $expected;
- 
-        if ($cmd eq 'OPEN') {
-          $expected = '-';
-
-        } elsif ($cmd eq 'CLOSE') {
-          $expected = '0';
-
-        } elsif ($cmd eq 'READ') {
-          $expected = '(-|1)';
+        my $path = $2;
 
-        } elsif ($cmd eq 'RETR') {
-          $expected = '-';
+        next unless $cmd eq 'DELE';
 
-        } else {
-          die("Unexpected command '$cmd' in $extlog_file");
+        my $expected = $test_file;
+        if ($^O eq 'darwin') {
+          # MacOSX-specific hack to deal with how it handles tmp files
+          $expected = ('/private' . $expected);
         }
 
-        $self->assert(qr/$expected/, $code,
-          test_msg("Expected '$expected', got '$code'"));
+        $self->assert($expected eq $path,
+          test_msg("Expected file '$expected', got '$path'"));
 
-      } else {
-        die("Unexpected line '$line' in $extlog_file");
+        $ok = 1;
+        last;
       }
     }
 
     close($fh);
 
+    $self->assert($ok,
+      test_msg("Expected ExtendedLog lines did not appear as expected"));
+
   } else {
     die("Can't read $extlog_file: $!");
   }
@@ -36634,7 +46204,7 @@ sub sftp_log_extlog_var_s_reads {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_s_writes {
+sub sftp_log_extlog_var_f_write {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -36655,9 +46225,6 @@ sub sftp_log_extlog_var_s_writes {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $write_sz = 32;
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -36670,21 +46237,8 @@ sub sftp_log_extlog_var_s_writes {
     }
   }
 
-  # Create some files in the home directory to read
-  for (my $i = 0; $i < 5; $i++) {
-    my $path = File::Spec->rel2abs("$home_dir/$i.txt");
-
-    if (open(my $fh, "> $path")) {
-      print $fh "ABCD\n" x 64;
-
-      unless (close($fh)) {
-        die("Can't write $path: $!");
-      }
-
-    } else {
-      die("Can't open $path: $!");
-    }
-  }
+  my $write_sz = 32;
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
@@ -36703,8 +46257,8 @@ sub sftp_log_extlog_var_s_writes {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'response "%r %s"',
-    ExtendedLog => "$extlog_file WRITE response",
+    LogFormat => 'writes "%f: %r"',
+    ExtendedLog => "$extlog_file WRITE writes",
 
     IfModules => {
       'mod_delay.c' => {
@@ -36722,20 +46276,6 @@ sub sftp_log_extlog_var_s_writes {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Limit FSETSTAT SETSTAT>
-  DenyAll
-</Limit>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -36776,26 +46316,22 @@ EOC
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT, 0644);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open 'test.txt': [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      # This is expected to fail because of the <Limit>
-      $fh->setstat(atime => 0, mtime => 0);
-   
-      print $fh "abcd" x 1024, "\n";
+      my $count = 5;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8;
+      }
 
-      # This is expected to fail because of the <Limit>
-      $fh->setstat(atime => 0, mtime => 0);
-  
-      # Explicitly disconnect without closing the file, simulating an
-      # aborted transfer.
-      $ssh2->disconnect();
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
 
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
+      $sftp = undef;
+      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -36828,44 +46364,37 @@ EOC
   }
 
   if (open(my $fh, "< $extlog_file")) {
+    my $ok = 0;
+
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /(\S+)\s+\S+\s+(\S+)/) {
-        my $cmd = $1;
-        my $code = $2;
-
-        my $expected;
- 
-        if ($cmd eq 'OPEN') {
-          $expected = '-';
-
-        } elsif ($cmd eq 'CLOSE') {
-          $expected = '0';
-
-        } elsif ($cmd eq 'WRITE') {
-          $expected = '0';
-
-        } elsif ($cmd eq 'FSETSTAT') {
-          $expected = '3';
+      if ($line =~ /^(\S+):\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) {
+        my $path = $1;
+        my $req = $2;
+        my $handle = $3;
+        my $offset = $4;
+        my $chunklen = $5;
 
-        } elsif ($cmd eq 'STOR') {
-          $expected = '-';
+        if ($req eq 'WRITE') {
+          my $expected = $test_file;
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack to deal with how it handles tmp files
+            $expected = ('/private' . $expected);
+          }
 
-        } else {
-          die("Unexpected command '$cmd' in $extlog_file");
+          if ($path eq $expected) {
+            $ok = 1;
+          }
         }
-
-        $self->assert(qr/$expected/, $code,
-          test_msg("Expected '$expected', got '$code'"));
-
-      } else {
-        die("Unexpected line '$line' in $extlog_file");
       }
     }
 
     close($fh);
 
+    $self->assert($ok,
+      test_msg("Expected ExtendedLog lines did not appear as expected"));
+
   } else {
     die("Can't read $extlog_file: $!");
   }
@@ -36873,7 +46402,7 @@ EOC
   unlink($log_file);
 }
 
-sub sftp_log_extlog_file_modified_bug3457 {
+sub sftp_log_extlog_var_f_write_chrooted {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -36894,17 +46423,6 @@ sub sftp_log_extlog_file_modified_bug3457 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -36917,6 +46435,9 @@ sub sftp_log_extlog_file_modified_bug3457 {
     }
   }
 
+  my $write_sz = 32;
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -36933,10 +46454,10 @@ sub sftp_log_extlog_file_modified_bug3457 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
-    AllowOverwrite => 'on',
-    LogFormat => 'custom "%{file-modified}"',
-    ExtendedLog => "$extlog_file WRITE custom",
+    LogFormat => 'writes "%f: %r"',
+    ExtendedLog => "$extlog_file WRITE writes",
 
     IfModules => {
       'mod_delay.c' => {
@@ -36994,18 +46515,22 @@ sub sftp_log_extlog_file_modified_bug3457 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT, 0644);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
-        die("Can't open 'test.txt': [$err_name] ($err_code)");
+        die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
+      my $count = 5;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8;
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
       $fh = undef;
+
       $sftp = undef;
       $ssh2->disconnect();
-
-      # Give a little time for the server to do its end-of-session thing.
-      sleep(1);
     };
 
     if ($@) {
@@ -37038,16 +46563,37 @@ sub sftp_log_extlog_file_modified_bug3457 {
   }
 
   if (open(my $fh, "< $extlog_file")) {
+    my $ok = 0;
+
     while (my $line = <$fh>) {
       chomp($line);
 
-      my $expected = 'true';
-      $self->assert($expected eq $line,
-        test_msg("Expected '$expected', got '$line'"));
+      if ($line =~ /^(\S+):\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) {
+        my $path = $1;
+        my $req = $2;
+        my $handle = $3;
+        my $offset = $4;
+        my $chunklen = $5;
+
+        if ($req eq 'WRITE') {
+          my $expected = $test_file;
+          if ($^O eq 'darwin') {
+            # MacOSX-specific hack to deal with how it handles tmp files
+            $expected = ('/private' . $expected);
+          }
+
+          if ($path eq $expected) {
+            $ok = 1;
+          }
+        }
+      }
     }
 
     close($fh);
 
+    $self->assert($ok,
+      test_msg("Expected ExtendedLog lines did not appear as expected"));
+
   } else {
     die("Can't read $extlog_file: $!");
   }
@@ -37055,7 +46601,7 @@ sub sftp_log_extlog_file_modified_bug3457 {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_retr_file_size {
+sub sftp_log_extlog_var_r_write {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -37076,19 +46622,6 @@ sub sftp_log_extlog_retr_file_size {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $test_sz = 32;
-
-  if (open(my $fh, "> $test_file")) {
-    print $fh "A" x $test_sz;
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -37101,6 +46634,9 @@ sub sftp_log_extlog_retr_file_size {
     }
   }
 
+  my $write_sz = 32;
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -37118,8 +46654,8 @@ sub sftp_log_extlog_retr_file_size {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'transfer "%m %b',
-    ExtendedLog => "$extlog_file READ transfer",
+    LogFormat => 'writes "%r"',
+    ExtendedLog => "$extlog_file WRITE writes",
 
     IfModules => {
       'mod_delay.c' => {
@@ -37177,20 +46713,15 @@ sub sftp_log_extlog_retr_file_size {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_RDONLY);
+      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
         die("Can't open test.txt: [$err_name] ($err_code)");
       }
 
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
-
-        $res = $fh->read($buf, 8192);
+      my $count = 5;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8;
       }
 
       # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
@@ -37198,9 +46729,6 @@ sub sftp_log_extlog_retr_file_size {
 
       $sftp = undef;
       $ssh2->disconnect();
-
-      $self->assert($test_sz == $size,
-        test_msg("Expected $test_sz, got $size"));
     };
 
     if ($@) {
@@ -37238,21 +46766,24 @@ sub sftp_log_extlog_retr_file_size {
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /^RETR (\d+)/) {
-        my $xfer_sz = $1;
+      if ($line =~ /^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) {
+        my $req = $1;
+        my $handle = $2;
+        my $offset = $3;
+        my $chunklen = $4;
 
-        if ($xfer_sz == $test_sz) {
-          $ok = 1;
-          last;
+        if ($req eq 'WRITE') {
+          if ($chunklen == 32) {
+            $ok = 1;
+          }
         }
       }
     }
 
     close($fh);
 
-    unless ($ok) {
-      die("Missing expected RETR file size ($test_sz) in $extlog_file");
-    }
+    $self->assert($ok,
+      test_msg("Expected ExtendedLog lines did not appear as expected"));
 
   } else {
     die("Can't read $extlog_file: $!");
@@ -37261,7 +46792,7 @@ sub sftp_log_extlog_retr_file_size {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
+sub sftp_log_extlog_var_note_bug3707 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -37282,30 +46813,18 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file1 = File::Spec->rel2abs("$tmpdir/test.txt");
-  my $test_sz1 = 32;
-
-  if (open(my $fh, "> $test_file1")) {
-    print $fh "A" x $test_sz1;
-    unless (close($fh)) {
-      die("Can't write $test_file1: $!");
-    }
-
-  } else {
-    die("Can't open $test_file1: $!");
-  }
+  my $write_sz = 32;
 
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-  my $test_sz2 = 64;
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD\n" x 64;
 
-  if (open(my $fh, "> $test_file2")) {
-    print $fh "A" x $test_sz2;
     unless (close($fh)) {
-      die("Can't write $test_file2: $!");
+      die("Can't write $test_file: $!");
     }
 
   } else {
-    die("Can't open $test_file2: $!");
+    die("Can't open $test_file: $!");
   }
 
   # Make sure that, if we're running as root, that the home directory has
@@ -37315,7 +46834,7 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
+    unless (chown($uid, $gid, $home_dir, $test_file)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -37337,8 +46856,8 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'transfer "%m %b',
-    ExtendedLog => "$extlog_file READ transfer",
+    LogFormat => 'requestID "%m %f id=%{note:sftp.file-handle}"',
+    ExtendedLog => "$extlog_file READ requestID",
 
     IfModules => {
       'mod_delay.c' => {
@@ -37396,37 +46915,6 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      # PuTTy's 'mget' does:
-      #
-      # OPENDIR
-      # READDIR
-      # <check for matching names>
-      # OPEN matching name
-      # READ
-      # CLOSE
-      # READDIR
-      # OPEN matching name
-      # READ
-      # CLOSE
-      # CLOSE <dirhandle>
-
-      # OPENDIR
-      my $dir = $sftp->opendir('.');
-      unless ($dir) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open directory '.': [$err_name] ($err_code)");
-      }
-
-      my $res = {};
-
-      # READDIR
-      my $file = $dir->read();
-      while ($file) {
-        $res->{$file->{name}} = $file;
-        $file = $dir->read();
-      }
-
-      # OPEN file1
       my $fh = $sftp->open('test.txt', O_RDONLY);
       unless ($fh) {
         my ($err_code, $err_name) = $sftp->error();
@@ -37436,44 +46924,17 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
       my $buf;
       my $size = 0;
 
-      # READ file1
-      $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
-
-        $res = $fh->read($buf, 8192);
-      }
-
-      # CLOSE file1
-      $fh = undef;
-
-      # OPEN file2
-      $fh = $sftp->open('test2.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test2.txt: [$err_name] ($err_code)");
-      }
-
-      $buf = '';
-      $size = 0;
-
-      # READ file2
-      $res = $fh->read($buf, 8192);
+      my $res = $fh->read($buf, 8192);
       while ($res) {
         $size += $res;
 
         $res = $fh->read($buf, 8192);
       }
 
-      # CLOSE file2
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
       $fh = undef;
 
-      # CLOSEDIR
-      $dir = undef;
-
-      # CHANNEL_CLOSE
       $sftp = undef;
-
       $ssh2->disconnect();
     };
 
@@ -37507,22 +46968,48 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
   }
 
   if (open(my $fh, "< $extlog_file")) {
-    my $test_ok = 0;
-    my $test2_ok = 0;
+    my $ok = 0;
+
+    if ($^O eq 'darwin') {
+      # MacOSX-specific hack to deal with how it handles tmp files
+      $test_file = ('/private' . $test_file);
+    }
+
+    my $expected_id;
 
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /^RETR (\d+)/) {
-        my $xfer_sz = $1;
+      if ($line =~ /(\S+) (.*)? id=(.*)?$/) {
+        my $cmd = $1;
+        my $path = $2;
+        my $id = $3;
+
+        if ($cmd eq 'OPEN') {
+          $expected_id = $id;
+
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
 
-        if ($xfer_sz == $test_sz1) {
-          $test_ok = 1;
           next;
         }
 
-        if ($xfer_sz == $test_sz2) {
-          $test2_ok = 1;
+        if ($cmd eq 'READ') {
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
+          $self->assert($expected_id eq $id,
+            test_msg("Expected sftp.file-name '$expected_id', got '$id'"));
+
+          next;
+        }
+
+        if ($cmd eq 'CLOSE') {
+          $self->assert($test_file eq $path,
+            test_msg("Expected '$test_file', got '$path'"));
+          $self->assert($expected_id eq $id,
+            test_msg("Expected sftp.file-name '$expected_id', got '$id'"));
+
+          $ok = 1;
           last;
         }
       }
@@ -37530,13 +47017,8 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
 
     close($fh);
 
-    unless ($test_ok) {
-      die("Missing expected RETR file size ($test_sz1) in $extlog_file");
-    }
-
-    unless ($test2_ok) {
-      die("Missing expected RETR file size ($test_sz2) in $extlog_file");
-    }
+    $self->assert($ok,
+      test_msg("Expected ExtendedLog lines did not appear as expected"));
 
   } else {
     die("Can't read $extlog_file: $!");
@@ -37545,7 +47027,7 @@ sub sftp_log_extlog_putty_mget_retr_file_size_bug3560 {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
+sub sftp_log_extlog_var_s_remove_bug3873 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -37566,6 +47048,8 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
   my $uid = 500;
   my $gid = 500;
 
+  my $write_sz = 32;
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -37578,6 +47062,33 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
     }
   }
 
+  my $test_file1 = File::Spec->rel2abs("$home_dir/test1.txt");
+  if (open(my $fh, "> $test_file1")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file1: $!");
+    }
+
+  } else {
+    die("Can't open $test_file1: $!");
+  }
+
+  my $sub_dir = File::Spec->rel2abs("$home_dir/test.d");
+  mkpath($sub_dir);
+
+  my $test_file2 = File::Spec->rel2abs("$sub_dir/test2.txt");
+  if (open(my $fh, "> $test_file2")) {
+    print $fh "Hello, World!\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file2: $!");
+    }
+
+  } else {
+    die("Can't open $test_file2: $!");
+  }
+
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -37585,8 +47096,6 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/testdir");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -37596,10 +47105,9 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
 
-    LogFormat => 'dirlog "%m %F"',
-    ExtendedLog => "$extlog_file WRITE dirlog",
+    LogFormat => 'delete "%m %f %s"',
+    ExtendedLog => "$extlog_file WRITE delete",
 
     IfModules => {
       'mod_delay.c' => {
@@ -37617,6 +47125,22 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Directory $sub_dir>
+  <Limit DELE>
+    DenyAll
+  </Limit>
+</Directory>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -37657,24 +47181,33 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->mkdir('testdir');
+      my $res = $sftp->unlink('test1.txt');
       unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't mkdir testdir: [$err_name] ($err_code)");
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't unlink test1.txt: [$err_name] ($err_code) $err_str");
       }
 
-      $res = $sftp->rmdir('testdir');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't rmdir testdir: [$err_name] ($err_code)");
+      if (-f $test_file1) {
+        die("$test_file1 file exists unexpectedly");
       }
 
-      $sftp = undef;
-      $ssh2->disconnect();
+      $res = $sftp->unlink('test.d/test2.txt');
+      if ($res) {
+        die("Unlinking test.d/test2.txt succeeded unexpectedly");
+      }
 
-      if (-d $test_dir) {
-        die("$test_dir directory exists unexpectedly");
+      my ($err_code, $err_name) = $sftp->error();
+
+      my $expected = 'SSH_FX_PERMISSION_DENIED';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected error name '$expected', got '$err_name'"));
+
+      unless (-f $test_file2) {
+        die("$test_file2 file does not exist as expected");
       }
+
+      $sftp = undef;
+      $ssh2->disconnect(); 
     };
 
     if ($@) {
@@ -37707,44 +47240,39 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
   }
 
   if (open(my $fh, "< $extlog_file")) {
-    my $mkdir_ok = 0;
-    my $rmdir_ok = 0;
+    my $have_success_dele = 0;
+    my $have_failed_dele = 0;
 
     while (my $line = <$fh>) {
       chomp($line);
 
-      if ($line =~ /(\S+)\s+(\S+)$/) {
+      if ($line =~ /^(\S+) (.*) (\d+)$/) {
         my $cmd = $1;
-        my $dir_path = $2;
-
-        if ($cmd eq 'MKD' ||
-            $cmd eq 'RMD') {
-            my $expected = '/testdir';
-
-            $self->assert($expected eq $dir_path,
-              test_msg("Expected '$expected', got '$dir_path'"));
+        my $path = $2;
+        my $resp_code = $3;
 
-        } elsif ($cmd eq 'MKDIR') {
-          $mkdir_ok = 1;
+        next unless $cmd eq 'DELE';
 
-        } elsif ($cmd eq 'RMDIR') {
-          $rmdir_ok = 1;
+        if ($path eq $test_file1) {
+          if ($resp_code == 250) {
+            $have_success_dele = 1;
+          }
 
-        } else {
-          die("Unexpected command '$cmd' in $extlog_file");
+        } elsif ($path eq $test_file2) {
+          if ($resp_code == 550) {
+            $have_failed_dele = 1;
+          }
         }
-
-      } else {
-        die("Unexpected line '$line' in $extlog_file");
       }
     }
 
     close($fh);
 
-    $self->assert($mkdir_ok == 1,
-      test_msg("Expected to see 'MKDIR' command but it was missing"));
-    $self->assert($rmdir_ok == 1,
-      test_msg("Expected to see 'RMDIR' command but it was missing"));
+    $self->assert($have_success_dele,
+      test_msg("Expected ExtendedLog lines did not appear as expected for successful REMOVE"));
+    $self->assert($have_failed_dele,
+      test_msg("Expected ExtendedLog lines did not appear as expected for failed REMOVE"));
+
 
   } else {
     die("Can't read $extlog_file: $!");
@@ -37753,7 +47281,7 @@ sub sftp_log_extlog_var_F_mkdir_rmdir_bug3591 {
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_w_rename_bug3029 {
+sub sftp_log_extlog_env_banner_bug4065 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -37774,20 +47302,6 @@ sub sftp_log_extlog_var_w_rename_bug3029 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_file1 = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file1")) {
-    print $fh "ABCD" x 8192;
-
-    unless (close($fh)) {
-      die("Can't write $test_file1: $!");
-    }
-
-  } else {
-    die("Can't open $test_file1: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -37817,8 +47331,8 @@ sub sftp_log_extlog_var_w_rename_bug3029 {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'rename "%m: %w %f"',
-    ExtendedLog => "$extlog_file ALL rename",
+    LogFormat => 'banner "%m: banner=%{SFTP_CLIENT_BANNER}e"',
+    ExtendedLog => "$extlog_file ALL banner",
 
     IfModules => {
       'mod_delay.c' => {
@@ -37876,22 +47390,8 @@ sub sftp_log_extlog_var_w_rename_bug3029 {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->rename('test.txt', 'test2.txt');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't rename test.txt to test2.txt: [$err_name] ($err_code)");
-      }
-
       $sftp = undef;
-      $ssh2->disconnect();
-
-      if (-f $test_file1) {
-        die("$test_file1 file exists unexpectedly");
-      }
-
-      unless (-f $test_file2) {
-        die("$test_file2 file does not exist as expected");
-      }
+      $ssh2->disconnect(); 
     };
 
     if ($@) {
@@ -37916,75 +47416,48 @@ sub sftp_log_extlog_var_w_rename_bug3029 {
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  if (open(my $fh, "< $extlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /(\S+):\s+(\S+)\s+(\S+)$/) {
-        my $cmd = $1;
-        my $whence = $2;
-        my $whither = $3;
-
-        my $expected;
-
-        if ($cmd eq 'RNFR') {
-          $expected = '-';
-
-          $self->assert($expected eq $whence,
-            test_msg("Expected '$expected', got '$whence'"));
-          $self->assert($expected eq $whither,
-            test_msg("Expected '$expected', got '$whither'"));
-
-        } elsif ($cmd eq 'RNTO') {
-          $expected = $test_file1;
-
-          $self->assert($expected eq $whence,
-            test_msg("Expected '$expected', got '$whence'"));
+  eval {
+    if (open(my $fh, "< $extlog_file")) {
+      my $have_banner = 0;
 
-          $expected = $test_file2;
-          $self->assert($expected eq $whither,
-            test_msg("Expected '$expected', got '$whither'"));
+      while (my $line = <$fh>) {
+        chomp($line);
 
-        } elsif ($cmd eq 'RENAME') {
-          $expected = '-';
+        if ($line =~ /^(\S+): banner=(.*)$/) {
+          my $cmd = $1;
+          my $banner = $2;
 
-          $self->assert($expected eq $whence,
-            test_msg("Expected '$expected', got '$whence'"));
-          $self->assert($expected eq $whither,
-            test_msg("Expected '$expected', got '$whither'"));
+          if ($banner =~ /^libssh2/) {
+            $have_banner = 1;
+            last;
+          }
+        }
+      }
 
-          $ok = 1;
+      close($fh);
 
-        } else {
-          next;
-        }
+      $self->assert($have_banner,
+        test_msg("Expected ExtendedLog lines did not appear"));
 
-      } else {
-        die("Unexpected line '$line' in $extlog_file");
-      }
+    } else {
+      die("Can't read $extlog_file: $!");
     }
+  };
+  if ($@) {
+    $ex = $@;
+  }
 
-    close($fh);
-
-    $self->assert($ok, test_msg("Did not find expected ExtendedLog lines"));
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
 
-  } else {
-    die("Can't read $extlog_file: $!");
+    die($ex);
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_f_remove {
+sub sftp_log_extlog_userauth_full_request {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -38005,8 +47478,6 @@ sub sftp_log_extlog_var_f_remove {
   my $uid = 500;
   my $gid = 500;
 
-  my $write_sz = 32;
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -38019,18 +47490,6 @@ sub sftp_log_extlog_var_f_remove {
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n" x 64;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -38048,8 +47507,8 @@ sub sftp_log_extlog_var_f_remove {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'delete "%m %f"',
-    ExtendedLog => "$extlog_file WRITE delete",
+    LogFormat => 'userauth "%m: %r"',
+    ExtendedLog => "$extlog_file AUTH userauth",
 
     IfModules => {
       'mod_delay.c' => {
@@ -38107,18 +47566,8 @@ sub sftp_log_extlog_var_f_remove {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $sftp->unlink('test.txt');
-      unless ($res) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't remove test.txt: [$err_name] ($err_code)");
-      }
-
       $sftp = undef;
-      $ssh2->disconnect();
-
-      if (-f $test_file) {
-        die("$test_file file exists unexpectedly");
-      }
+      $ssh2->disconnect(); 
     };
 
     if ($@) {
@@ -38143,53 +47592,56 @@ sub sftp_log_extlog_var_f_remove {
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  if (open(my $fh, "< $extlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /(\S+) (.*)$/) {
-        my $cmd = $1;
-        my $path = $2;
+  eval {
+    if (open(my $fh, "< $extlog_file")) {
+      my $have_req = 0;
 
-        next unless $cmd eq 'DELE';
+      while (my $line = <$fh>) {
+        chomp($line);
 
-        $self->assert($test_file eq $path,
-          test_msg("Expected '$test_file', got '$path'"));
+        if ($line =~ /^(\S+):\s+(\S+)\s+(\S+)\s+(\S+)/) {
+          my $cmd = $1;
+          my $cmd_req = $2;
+          my $cmd_user = $3;
+          my $cmd_meth = $4;
 
-        $ok = 1;
-        last;
+          if ($cmd_meth eq 'password') {
+            $have_req = 1;
+            last;
+          }
+        }
       }
-    }
 
-    close($fh);
+      close($fh);
 
-    $self->assert($ok,
-      test_msg("Expected ExtendedLog lines did not appear as expected"));
+      $self->assert($have_req,
+        test_msg("Expected ExtendedLog lines did not appear"));
 
-  } else {
-    die("Can't read $extlog_file: $!");
+    } else {
+      die("Can't read $extlog_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
   }
 
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_f_write {
+sub sftp_sighup {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -38215,9 +47667,6 @@ sub sftp_log_extlog_var_f_write {
     }
   }
 
-  my $write_sz = 32;
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -38230,13 +47679,12 @@ sub sftp_log_extlog_var_f_write {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 event:10 regexp:10',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'writes "%f: %r"',
-    ExtendedLog => "$extlog_file WRITE writes",
+    DenyFilter => '\*/',
 
     IfModules => {
       'mod_delay.c' => {
@@ -38254,6 +47702,16 @@ sub sftp_log_extlog_var_f_write {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  # First, start the server.
+  server_start($config_file);
+
+  # Give it a second to start up, then send the SIGHUP signal
+  sleep(2);
+  server_restart($pid_file);
+
+  # Give it another second to start up again
+  sleep(2);
+
   # 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.
@@ -38266,9 +47724,6 @@ sub sftp_log_extlog_var_f_write {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -38294,20 +47749,6 @@ sub sftp_log_extlog_var_f_write {
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $count = 5;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8;
-      }
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -38320,10 +47761,13 @@ sub sftp_log_extlog_var_f_write {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
+    # Wait until we receive word from the child that it has finished its test.
+    while (my $msg = <$rfh>) {
+      chomp($msg);
+
+      if ($msg eq 'done') {
+        last;
+      }
     }
 
     exit 0;
@@ -38341,53 +47785,16 @@ sub sftp_log_extlog_var_f_write {
     die($ex);
   }
 
-  if (open(my $fh, "< $extlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /^(\S+):\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) {
-        my $path = $1;
-        my $req = $2;
-        my $handle = $3;
-        my $offset = $4;
-        my $chunklen = $5;
-
-        if ($req eq 'WRITE') {
-          my $expected = $test_file;
-          if ($^O eq 'darwin') {
-            # MacOSX-specific hack to deal with how it handles tmp files
-            $expected = ('/private' . $expected);
-          }
-
-          if ($path eq $expected) {
-            $ok = 1;
-          }
-        }
-      }
-    }
-
-    close($fh);
-
-    $self->assert($ok,
-      test_msg("Expected ExtendedLog lines did not appear as expected"));
-
-  } else {
-    die("Can't read $extlog_file: $!");
-  }
-
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_f_write_chrooted {
+sub sftp_ifsess_protocols {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
@@ -38413,9 +47820,6 @@ sub sftp_log_extlog_var_f_write_chrooted {
     }
   }
 
-  my $write_sz = 32;
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -38428,14 +47832,10 @@ sub sftp_log_extlog_var_f_write_chrooted {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    DefaultRoot => '~',
-
-    LogFormat => 'writes "%f: %r"',
-    ExtendedLog => "$extlog_file WRITE writes",
 
     IfModules => {
       'mod_delay.c' => {
@@ -38453,6 +47853,26 @@ sub sftp_log_extlog_var_f_write_chrooted {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<IfModule mod_ifsession.c>
+  <IfUser foo>
+    Protocols sftp scp
+  </IfUser>
+
+  <IfUser $user>
+    Protocols scp
+  </IfUser>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -38484,30 +47904,20 @@ sub sftp_log_extlog_var_f_write_chrooted {
 
       unless ($ssh2->auth_password($user, $passwd)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
       my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+      if ($sftp) {
+        die("SFTP subsystem started unexpectedly");
       }
 
-      my $count = 5;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8;
-      }
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
 
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
+      my $expected = 'LIBSSH2_ERROR_CHANNEL_FAILURE';
+      $self->assert($expected eq $err_name,
+        test_msg("Expected '$expected', got '$err_name'"));
 
-      $sftp = undef;
       $ssh2->disconnect();
     };
 
@@ -38540,59 +47950,47 @@ sub sftp_log_extlog_var_f_write_chrooted {
     die($ex);
   }
 
-  if (open(my $fh, "< $extlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /^(\S+):\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) {
-        my $path = $1;
-        my $req = $2;
-        my $handle = $3;
-        my $offset = $4;
-        my $chunklen = $5;
-
-        if ($req eq 'WRITE') {
-          my $expected = $test_file;
-          if ($^O eq 'darwin') {
-            # MacOSX-specific hack to deal with how it handles tmp files
-            $expected = ('/private' . $expected);
-          }
-
-          if ($path eq $expected) {
-            $ok = 1;
-          }
-        }
-      }
-    }
-
-    close($fh);
-
-    $self->assert($ok,
-      test_msg("Expected ExtendedLog lines did not appear as expected"));
-
-  } else {
-    die("Can't read $extlog_file: $!");
-  }
-
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_r_write {
+sub sftp_wrap_login_allowed_bug3352 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
   my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
   my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
 
+  my $fh;
+  my $allow_file = File::Spec->rel2abs("$tmpdir/sftp.allow");
+  if (open($fh, "> $allow_file")) {
+    print $fh "proftpd: ALL\n";
+
+    unless (close($fh)) {
+      die("Can't write $allow_file: $!");
+    }
+
+  } else {
+    die("Can't open $allow_file: $!");
+  }
+
+  my $deny_file = File::Spec->rel2abs("$tmpdir/sftp.deny");
+  if (open($fh, "> $deny_file")) {
+    print $fh "ALL: ALL\n";
+
+    unless (close($fh)) {
+      die("Can't write $deny_file: $!");
+    }
+
+  } else {
+    die("Can't open $deny_file: $!");
+  }
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -38612,9 +48010,6 @@ sub sftp_log_extlog_var_r_write {
     }
   }
 
-  my $write_sz = 32;
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
   auth_group_write($auth_group_file, $group, $gid, $user);
@@ -38622,19 +48017,18 @@ sub sftp_log_extlog_var_r_write {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'writes "%r"',
-    ExtendedLog => "$extlog_file WRITE writes",
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -38646,6 +48040,10 @@ sub sftp_log_extlog_var_r_write {
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
+
+      'mod_wrap.c' => {
+        TCPAccessFiles => "$allow_file $deny_file",
+      },
     },
   };
 
@@ -38663,9 +48061,6 @@ sub sftp_log_extlog_var_r_write {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -38680,6 +48075,10 @@ sub sftp_log_extlog_var_r_write {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Publickey auth succeeded unexpectedly");
+      }
+
       unless ($ssh2->auth_password($user, $passwd)) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
@@ -38690,21 +48089,7 @@ sub sftp_log_extlog_var_r_write {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
       }
-
-      my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT|O_TRUNC, 0644);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
-      }
-
-      my $count = 5;
-      for (my $i = 0; $i < $count; $i++) {
-        print $fh "ABCD" x 8;
-      }
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
+      
       $sftp = undef;
       $ssh2->disconnect();
     };
@@ -38738,73 +48123,50 @@ sub sftp_log_extlog_var_r_write {
     die($ex);
   }
 
-  if (open(my $fh, "< $extlog_file")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)/) {
-        my $req = $1;
-        my $handle = $2;
-        my $offset = $3;
-        my $chunklen = $4;
-
-        if ($req eq 'WRITE') {
-          if ($chunklen == 32) {
-            $ok = 1;
-          }
-        }
-      }
-    }
-
-    close($fh);
-
-    $self->assert($ok,
-      test_msg("Expected ExtendedLog lines did not appear as expected"));
-
-  } else {
-    die("Can't read $extlog_file: $!");
-  }
-
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_note_bug3707 {
+sub sftp_wrap_login_denied_bug3352 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sftp.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = test_get_logfile();
 
   my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
   my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $fh;
+  my $allow_file = File::Spec->rel2abs("$tmpdir/sftp.allow");
+  if (open($fh, "> $allow_file")) {
+    # Leave this file empty
 
-  my $write_sz = 32;
+  } else {
+    die("Can't open $allow_file: $!");
+  }
 
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD\n" x 64;
+  my $deny_file = File::Spec->rel2abs("$tmpdir/sftp.deny");
+  if (open($fh, "> $deny_file")) {
+    print $fh "ALL: ALL\n";
 
     unless (close($fh)) {
-      die("Can't write $test_file: $!");
+      die("Can't write $deny_file: $!");
     }
 
   } else {
-    die("Can't open $test_file: $!");
+    die("Can't open $deny_file: $!");
   }
 
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -38812,7 +48174,7 @@ sub sftp_log_extlog_var_note_bug3707 {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $test_file)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -38824,19 +48186,19 @@ sub sftp_log_extlog_var_note_bug3707 {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'requestID "%m %f id=%{note:sftp.file-handle}"',
-    ExtendedLog => "$extlog_file READ requestID",
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -38848,6 +48210,10 @@ sub sftp_log_extlog_var_note_bug3707 {
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
+
+      'mod_wrap.c' => {
+        TCPAccessFiles => "$allow_file $deny_file",
+      },
     },
   };
 
@@ -38865,9 +48231,6 @@ sub sftp_log_extlog_var_note_bug3707 {
 
   my $ex;
 
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -38882,38 +48245,13 @@ sub sftp_log_extlog_var_note_bug3707 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $fh = $sftp->open('test.txt', O_RDONLY);
-      unless ($fh) {
-        my ($err_code, $err_name) = $sftp->error();
-        die("Can't open test.txt: [$err_name] ($err_code)");
+      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        die("Publickey auth succeeded unexpectedly");
       }
 
-      my $buf;
-      my $size = 0;
-
-      my $res = $fh->read($buf, 8192);
-      while ($res) {
-        $size += $res;
-
-        $res = $fh->read($buf, 8192);
+      if ($ssh2->auth_password($user, $passwd)) {
+        die("Logged in to SSH2 server unexpectedly");
       }
-
-      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
-      $fh = undef;
-
-      $sftp = undef;
-      $ssh2->disconnect();
     };
 
     if ($@) {
@@ -38945,147 +48283,28 @@ sub sftp_log_extlog_var_note_bug3707 {
     die($ex);
   }
 
-  if (open(my $fh, "< $extlog_file")) {
-    my $ok = 0;
-
-    if ($^O eq 'darwin') {
-      # MacOSX-specific hack to deal with how it handles tmp files
-      $test_file = ('/private' . $test_file);
-    }
-
-    my $expected_id;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /(\S+) (.*)? id=(.*)?$/) {
-        my $cmd = $1;
-        my $path = $2;
-        my $id = $3;
-
-        if ($cmd eq 'OPEN') {
-          $expected_id = $id;
-
-          $self->assert($test_file eq $path,
-            test_msg("Expected '$test_file', got '$path'"));
-
-          next;
-        }
-
-        if ($cmd eq 'READ') {
-          $self->assert($test_file eq $path,
-            test_msg("Expected '$test_file', got '$path'"));
-          $self->assert($expected_id eq $id,
-            test_msg("Expected sftp.file-name '$expected_id', got '$id'"));
-
-          next;
-        }
-
-        if ($cmd eq 'CLOSE') {
-          $self->assert($test_file eq $path,
-            test_msg("Expected '$test_file', got '$path'"));
-          $self->assert($expected_id eq $id,
-            test_msg("Expected sftp.file-name '$expected_id', got '$id'"));
-
-          $ok = 1;
-          last;
-        }
-      }
-    }
-
-    close($fh);
-
-    $self->assert($ok,
-      test_msg("Expected ExtendedLog lines did not appear as expected"));
-
-  } else {
-    die("Can't read $extlog_file: $!");
-  }
-
   unlink($log_file);
 }
 
-sub sftp_log_extlog_var_s_remove_bug3873 {
+sub scp_upload {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $write_sz = 32;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  my $test_file1 = File::Spec->rel2abs("$home_dir/test1.txt");
-  if (open(my $fh, "> $test_file1")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file1: $!");
-    }
-
-  } else {
-    die("Can't open $test_file1: $!");
-  }
-
-  my $sub_dir = File::Spec->rel2abs("$home_dir/test.d");
-  mkpath($sub_dir);
-
-  my $test_file2 = File::Spec->rel2abs("$sub_dir/test2.txt");
-  if (open(my $fh, "> $test_file2")) {
-    print $fh "Hello, World!\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file2: $!");
-    }
-
-  } else {
-    die("Can't open $test_file2: $!");
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    LogFormat => 'delete "%m %f %s"',
-    ExtendedLog => "$extlog_file WRITE delete",
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -39094,30 +48313,15 @@ sub sftp_log_extlog_var_s_remove_bug3873 {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Directory $sub_dir>
-  <Limit DELE>
-    DenyAll
-  </Limit>
-</Directory>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
+  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
@@ -39148,46 +48352,22 @@ EOC
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $res = $sftp->unlink('test1.txt');
+      my $res = $ssh2->scp_put($setup->{config_file}, 'test.txt');
       unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't unlink test1.txt: [$err_name] ($err_code) $err_str");
-      }
-
-      if (-f $test_file1) {
-        die("$test_file1 file exists unexpectedly");
-      }
-
-      $res = $sftp->unlink('test.d/test2.txt');
-      if ($res) {
-        die("Unlinking test.d/test2.txt succeeded unexpectedly");
+        die("Can't upload $setup->{config_file} to server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($err_code, $err_name) = $sftp->error();
-
-      my $expected = 'SSH_FX_PERMISSION_DENIED';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected error name '$expected', got '$err_name'"));
-
-      unless (-f $test_file2) {
-        die("$test_file2 file does not exist as expected");
-      }
+      $ssh2->disconnect();
 
-      $sftp = undef;
-      $ssh2->disconnect(); 
+      $self->assert(-f $test_file,
+        test_msg("File $test_file does not exist as expected"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -39196,7 +48376,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -39206,60 +48386,13 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  if (open(my $fh, "< $extlog_file")) {
-    my $have_success_dele = 0;
-    my $have_failed_dele = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      if ($line =~ /^(\S+) (.*) (\d+)$/) {
-        my $cmd = $1;
-        my $path = $2;
-        my $resp_code = $3;
-
-        next unless $cmd eq 'DELE';
-
-        if ($path eq $test_file1) {
-          if ($resp_code == 250) {
-            $have_success_dele = 1;
-          }
-
-        } elsif ($path eq $test_file2) {
-          if ($resp_code == 550) {
-            $have_failed_dele = 1;
-          }
-        }
-      }
-    }
-
-    close($fh);
-
-    $self->assert($have_success_dele,
-      test_msg("Expected ExtendedLog lines did not appear as expected for successful REMOVE"));
-    $self->assert($have_failed_dele,
-      test_msg("Expected ExtendedLog lines did not appear as expected for failed REMOVE"));
-
-
-  } else {
-    die("Can't read $extlog_file: $!");
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_sighup {
+sub scp_upload_zero_len_file {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -39298,18 +48431,26 @@ sub sftp_sighup {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
+  my $empty_file = File::Spec->rel2abs("$tmpdir/empty.txt");
+  if (open(my $fh, "> $empty_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $empty_file: $!");
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20 event:10 regexp:10',
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    DenyFilter => '\*/',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -39326,16 +48467,6 @@ sub sftp_sighup {
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  # First, start the server.
-  server_start($config_file);
-
-  # Give it a second to start up, then send the SIGHUP signal
-  sleep(2);
-  server_restart($pid_file);
-
-  # Give it another second to start up again
-  sleep(2);
-
   # 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.
@@ -39348,6 +48479,9 @@ sub sftp_sighup {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -39367,14 +48501,22 @@ sub sftp_sighup {
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
+      my $res = $ssh2->scp_put($empty_file, 'test.txt');
+      unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't upload $config_file to server: [$err_name] ($err_code) $err_str");
       }
 
-      $sftp = undef;
       $ssh2->disconnect();
+
+      unless (-f $test_file) {
+        die("$test_file file does not exist as expected");
+      }
+
+      my $size = -s $test_file;
+      unless ($size == 0) {
+        die("$test_file has size $size unexpectedly");
+      }
     };
 
     if ($@) {
@@ -39385,13 +48527,10 @@ sub sftp_sighup {
     $wfh->flush();
 
   } else {
-    # Wait until we receive word from the child that it has finished its test.
-    while (my $msg = <$rfh>) {
-      chomp($msg);
-
-      if ($msg eq 'done') {
-        last;
-      }
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
 
     exit 0;
@@ -39412,54 +48551,53 @@ sub sftp_sighup {
   unlink($log_file);
 }
 
-sub sftp_ifsess_protocols {
+sub scp_upload_largefile {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    # Make a file that's larger than the maximum SSH2 packet size, forcing
+    # the scp code to loop properly entire the entire large file is sent.
+    print $fh "ABCDefgh" x 16384;
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Calculate the MD5 checksum of this file, for comparison with the
+  # downloaded file.
+  my $ctx = Digest::MD5->new();
+  my $expected_md5;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  if (open(my $fh, "< $test_file")) {
+    binmode($fh);
+    $ctx->addfile($fh);
+    $expected_md5 = $ctx->hexdigest();
+    close($fh);
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  } else {
+    die("Can't read $test_file: $!");
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -39468,34 +48606,15 @@ sub sftp_ifsess_protocols {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<IfModule mod_ifsession.c>
-  <IfUser foo>
-    Protocols sftp scp
-  </IfUser>
-
-  <IfUser $user>
-    Protocols scp
-  </IfUser>
-</IfModule>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
+  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
@@ -39526,25 +48645,22 @@ EOC
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      if ($sftp) {
-        die("SFTP subsystem started unexpectedly");
+      my $res = $ssh2->scp_put($test_file, 'test2.txt');
+      unless ($res) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't download 'test.txt' from server: [$err_name] ($err_code) $err_str");
       }
 
-      my ($err_code, $err_name, $err_str) = $ssh2->error();
-
-      my $expected = 'LIBSSH2_ERROR_CHANNEL_FAILURE';
-      $self->assert($expected eq $err_name,
-        test_msg("Expected '$expected', got '$err_name'"));
-
       $ssh2->disconnect();
-    };
 
+      $self->assert(-f $test_file2,
+        test_msg("File $test_file2 does not exist as expected"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -39553,7 +48669,7 @@ EOC
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -39563,95 +48679,93 @@ EOC
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
   if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+    test_cleanup($setup->{log_file}, $ex);
+  }
 
-    die($ex);
+  # Calculate the MD5 checksum of the uploaded file, for comparison with the
+  # file that was uploaded.
+  $ctx->reset();
+  my $md5;
+
+  eval {
+    if (open(my $fh, "< $test_file2")) {
+      binmode($fh);
+      $ctx->addfile($fh);
+      $md5 = $ctx->hexdigest();
+      close($fh);
+
+    } else {
+      die("Can't read $test_file2: $!");
+    }
+
+    $self->assert($expected_md5 eq $md5,
+      test_msg("Expected '$expected_md5', got '$md5'"));
+  };
+  if ($@) {
+    $ex = $@;
   }
 
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_wrap_login_allowed_bug3352 {
+sub scp_upload_abs_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $fh;
-  my $allow_file = File::Spec->rel2abs("$tmpdir/sftp.allow");
-  if (open($fh, "> $allow_file")) {
-    print $fh "proftpd: ALL\n";
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
     unless (close($fh)) {
-      die("Can't write $allow_file: $!");
+      die("Can't write $test_file: $!");
     }
 
   } else {
-    die("Can't open $allow_file: $!");
+    die("Can't open $test_file: $!");
   }
 
-  my $deny_file = File::Spec->rel2abs("$tmpdir/sftp.deny");
-  if (open($fh, "> $deny_file")) {
-    print $fh "ALL: ALL\n";
-
-    unless (close($fh)) {
-      die("Can't write $deny_file: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  } else {
-    die("Can't open $deny_file: $!");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -39660,18 +48774,15 @@ sub sftp_wrap_login_allowed_bug3352 {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
-
-      'mod_wrap.c' => {
-        TCPAccessFiles => "$allow_file $deny_file",
-      },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -39685,6 +48796,9 @@ sub sftp_wrap_login_allowed_bug3352 {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -39699,25 +48813,24 @@ sub sftp_wrap_login_allowed_bug3352 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
-        die("Publickey auth succeeded unexpectedly");
-      }
-
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $sftp = $ssh2->sftp();
-      unless ($sftp) {
+      my $path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_put($setup->{config_file}, $path);
+      unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+        die("Can't upload to $path on server: [$err_name] ($err_code) $err_str");
       }
-      
-      $sftp = undef;
+
       $ssh2->disconnect();
-    };
 
+      my $file_size = -s $test_file;
+      $self->assert($file_size > 0,
+        test_msg("Expected non-zero file size, got $file_size"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -39726,7 +48839,7 @@ sub sftp_wrap_login_allowed_bug3352 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -39736,92 +48849,67 @@ sub sftp_wrap_login_allowed_bug3352 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sftp_wrap_login_denied_bug3352 {
+sub scp_upload_abs_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $fh;
-  my $allow_file = File::Spec->rel2abs("$tmpdir/sftp.allow");
-  if (open($fh, "> $allow_file")) {
-    # Leave this file empty
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
   } else {
-    die("Can't open $allow_file: $!");
+    die("Can't open $test_file: $!");
   }
 
-  my $deny_file = File::Spec->rel2abs("$tmpdir/sftp.deny");
-  if (open($fh, "> $deny_file")) {
-    print $fh "ALL: ALL\n";
-
-    unless (close($fh)) {
-      die("Can't write $deny_file: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  } else {
-    die("Can't open $deny_file: $!");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
-  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -39830,18 +48918,15 @@ sub sftp_wrap_login_denied_bug3352 {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
-
-      'mod_wrap.c' => {
-        TCPAccessFiles => "$allow_file $deny_file",
-      },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -39855,6 +48940,9 @@ sub sftp_wrap_login_denied_bug3352 {
 
   my $ex;
 
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
   # Fork child
   $self->handle_sigchld();
   defined(my $pid = fork()) or die("Can't fork: $!");
@@ -39869,15 +48957,24 @@ sub sftp_wrap_login_denied_bug3352 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
-        die("Publickey auth succeeded unexpectedly");
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      if ($ssh2->auth_password($user, $passwd)) {
-        die("Logged in to SSH2 server unexpectedly");
+      my $path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_put($setup->{config_file}, $path);
+      unless ($res) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't upload to $path on server: [$err_name] ($err_code) $err_str");
       }
-    };
 
+      $ssh2->disconnect();
+
+      my $file_size = -s $test_file;
+      $self->assert($file_size > 0,
+        test_msg("Expected non-zero file size, got $file_size"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -39886,7 +48983,7 @@ sub sftp_wrap_login_denied_bug3352 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -39896,70 +48993,70 @@ sub sftp_wrap_login_denied_bug3352 {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub scp_upload {
+sub scp_upload_rel_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = test_get_logfile();
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
 
     IfModules => {
       'mod_delay.c' => {
@@ -39968,14 +49065,15 @@ sub scp_upload {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -40006,24 +49104,24 @@ sub scp_upload {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $ssh2->scp_put($config_file, 'test.txt');
+      my $path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_put($setup->{config_file}, $path);
       unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't upload $config_file to server: [$err_name] ($err_code) $err_str");
+        die("Can't upload to $path on server: [$err_name] ($err_code) $err_str");
       }
 
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("$test_file file does not exist as expected");
-      }
+      my $file_size = -s $test_file;
+      $self->assert($file_size > 0,
+        test_msg("Expected non-zero file size, got $file_size"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -40032,7 +49130,7 @@ sub scp_upload {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -40042,260 +49140,71 @@ sub scp_upload {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub scp_upload_zero_len_file {
+sub scp_upload_rel_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
     }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
-  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
-  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
-
-  my $empty_file = File::Spec->rel2abs("$tmpdir/empty.txt");
-  if (open(my $fh, "> $empty_file")) {
-    close($fh);
 
   } else {
-    die("Can't open $empty_file: $!");
+    die("Can't open $test_file: $!");
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
-  my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
-    IfModules => {
-      'mod_delay.c' => {
-        DelayEngine => 'off',
-      },
-
-      'mod_sftp.c' => [
-        "SFTPEngine on",
-        "SFTPLog $log_file",
-        "SFTPHostKey $rsa_host_key",
-        "SFTPHostKey $dsa_host_key",
-      ],
-    },
-  };
-
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
 
-  # 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 $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
   }
 
-  require Net::SSH2;
-
-  my $ex;
-
-  # Ignore SIGPIPE
-  local $SIG{PIPE} = sub { };
-
-  # Fork child
-  $self->handle_sigchld();
-  defined(my $pid = fork()) or die("Can't fork: $!");
-  if ($pid) {
-    eval {
-      my $ssh2 = Net::SSH2->new();
-
-      sleep(1);
-
-      unless ($ssh2->connect('127.0.0.1', $port)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      unless ($ssh2->auth_password($user, $passwd)) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
-      }
-
-      my $res = $ssh2->scp_put($empty_file, 'test.txt');
-      unless ($res) {
-        my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't upload $config_file to server: [$err_name] ($err_code) $err_str");
-      }
-
-      $ssh2->disconnect();
-
-      unless (-f $test_file) {
-        die("$test_file file does not exist as expected");
-      }
-
-      my $size = -s $test_file;
-      unless ($size == 0) {
-        die("$test_file has size $size unexpectedly");
-      }
-    };
-
-    if ($@) {
-      $ex = $@;
-    }
-
-    $wfh->print("done\n");
-    $wfh->flush();
-
-  } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
-    }
-
-    exit 0;
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
   }
 
-  # Stop server
-  server_stop($pid_file);
-
-  $self->assert_child_ok($pid);
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
   }
 
-  unlink($log_file);
-}
-
-sub scp_upload_largefile {
-  my $self = shift;
-  my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $fh;
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open($fh, "> $test_file")) {
-    # Make a file that's larger than the maximum SSH2 packet size, forcing
-    # the scp code to loop properly entire the entire large file is sent.
-
-    print $fh "ABCDefgh" x 16384;
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
-  # Calculate the MD5 checksum of this file, for comparison with the
-  # downloaded file.
-  my $ctx = Digest::MD5->new();
-  my $expected_md5;
-
-  if (open($fh, "< $test_file")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $expected_md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $test_file: $!");
-  }
-
-  my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt");
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    AllowOverwrite => 'on',
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
@@ -40304,14 +49213,15 @@ sub scp_upload_largefile {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -40342,24 +49252,24 @@ sub scp_upload_largefile {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $ssh2->scp_put($test_file, 'test2.txt');
+      my $path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_put($setup->{config_file}, $path);
       unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't download 'test.txt' from server: [$err_name] ($err_code) $err_str");
+        die("Can't upload to $path on server: [$err_name] ($err_code) $err_str");
       }
 
       $ssh2->disconnect();
 
-      unless (-f $test_file2) {
-        die("$test_file2 file does not exist as expected");
-      }
+      my $file_size = -s $test_file;
+      $self->assert($file_size > 0,
+        test_msg("Expected non-zero file size, got $file_size"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -40368,7 +49278,7 @@ sub scp_upload_largefile {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -40378,36 +49288,10 @@ sub scp_upload_largefile {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  # Calculate the MD5 checksum of the uploaded file, for comparison with the
-  # file that was uploaded.
-  $ctx->reset();
-  my $md5;
-
-  if (open($fh, "< $test_file2")) {
-    binmode($fh);
-    $ctx->addfile($fh);
-    $md5 = $ctx->hexdigest();
-    close($fh);
-
-  } else {
-    die("Can't read $test_file2: $!");
-  }
-
-  $self->assert($expected_md5 eq $md5,
-    test_msg("Expected '$expected_md5', got '$md5'"));
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub scp_upload_subdir_enoent {
@@ -40848,7 +49732,7 @@ sub scp_upload_fifo_bug3312 {
 
       if ($err_code) {
         chomp($err_str);
-        my $expected = 'test.fifo: (No such device or address|Device not configured)$';
+        my $expected = '(test.fifo: (No such device or address|Device not configured))|(failed to send file)$';
 
         $self->assert(qr/$expected/, $err_str,
           test_msg("Expected '$expected', got '$err_str'"));
@@ -41264,6 +50148,11 @@ sub scp_ext_upload_recursive_dir_bug3447 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -41271,6 +50160,10 @@ sub scp_ext_upload_recursive_dir_bug3447 {
         die("Can't upload $src_dir to server: $errstr");
       }
 
+      if ($^O eq 'darwin') {
+        $dst_dir = '/private' . $dst_dir;
+      }
+
       unless (-d "$dst_dir/src.d") {
         die("Directory '$dst_dir/src.d' does not exist as expected");
       }
@@ -41386,34 +50279,31 @@ sub scp_ext_upload_recursive_dir_bug3792 {
 
   my $count = 25;
   for (my $i = 0; $i < $count; $i++) {
-    my $filename = (chr(97 + $i)) . sprintf("%03s", $i);
+    my $filename = (chr(65 + $i)) . sprintf("%03s", $i);
     my $src_file = File::Spec->rel2abs("$src_dir/$filename");
 
     if (open(my $fh, "> $src_file")) {
-      print $fh "ABCDefgh" x 7891;
+      print $fh "ABCDefgh" x 6458;
 
       unless (close($fh)) {
         die("Can't write $src_file: $!");
       }
 
-      chmod(0404, $src_file);
-
     } else {
       die("Can't open $src_file: $!");
     }
-  }
-
-  for (my $i = 0; $i < $count; $i++) {
-    my $filename = (chr(65 + $i)) . sprintf("%03s", $i);
-    my $src_file = File::Spec->rel2abs("$src_dir/$filename");
 
+    $filename = (chr(97 + $i)) . sprintf("%03s", $i);
+    $src_file = File::Spec->rel2abs("$src_dir/$filename");
     if (open(my $fh, "> $src_file")) {
-      print $fh "ABCDefgh" x 6458;
+      print $fh "ABCDefgh" x 7891;
 
       unless (close($fh)) {
         die("Can't write $src_file: $!");
       }
 
+      chmod(0404, $src_file);
+
     } else {
       die("Can't open $src_file: $!");
     }
@@ -41557,6 +50447,11 @@ sub scp_ext_upload_recursive_dir_bug3792 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -41865,6 +50760,11 @@ sub scp_ext_upload_recursive_dir_bug4004 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -41964,6 +50864,228 @@ sub scp_ext_upload_recursive_dir_bug4004 {
   unlink($log_file);
 }
 
+sub scp_ext_upload_recursive_dirs_bug4257 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_rfc4716_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/authorized_rsa_keys');
+
+  my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys");
+  unless (copy($rsa_rfc4716_key, $authorized_keys)) {
+    die("Can't copy $rsa_rfc4716_key to $authorized_keys: $!");
+  }
+
+  # For this test, we need the following directory structure:
+  #
+  #  src1.d/
+  #    file1.dat
+  #  src2.d/
+  #    file2.dat
+  #  src3.d/
+  #    file3.dat
+  #  src4.d/
+  #
+
+  my $src1_dir = File::Spec->rel2abs("$tmpdir/src1.d");
+  my $src2_dir = File::Spec->rel2abs("$tmpdir/src2.d");
+  my $src3_dir = File::Spec->rel2abs("$tmpdir/src3.d");
+  my $src4_dir = File::Spec->rel2abs("$tmpdir/src4.d");
+  mkpath($src1_dir, $src2_dir, $src3_dir, $src4_dir, {
+    mode => 0755,
+  });
+
+  my $count = 3;
+  for (my $i = 1; $i <= $count; $i++) {
+    my $dirname = 'src' . $i . '.d';
+    my $filename = 'file' . $i . '.dat';
+    my $src_file = File::Spec->rel2abs("$tmpdir/$dirname/$filename");
+
+    if (open(my $fh, "> $src_file")) {
+      print $fh "ABCDefgh" x 7891;
+
+      unless (close($fh)) {
+        die("Can't write $src_file: $!");
+    }
+
+    } else {
+      die("Can't open $src_file: $!");
+    }
+  }
+
+  my $dst_dir = File::Spec->rel2abs("$tmpdir/dst.d");
+  mkpath($dst_dir);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'ssh2:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    Umask => '002',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys file:~/.authorized_keys",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my @cmd = (
+        'scp',
+        '-r',
+        '-p',
+        '-v',
+        '-oBatchMode=yes',
+        '-oCheckHostIP=no',
+        "-oPort=$port",
+        "-oIdentityFile=$rsa_priv_key",
+        '-oPubkeyAuthentication=yes',
+        '-oStrictHostKeyChecking=no',
+        "$src1_dir",
+        "$src2_dir",
+        "$src3_dir",
+        "$src4_dir",
+        "$setup->{user}\@127.0.0.1:dst.d/",
+      );
+
+      my $scp_rh = IO::Handle->new();
+      my $scp_wh = IO::Handle->new();
+      my $scp_eh = IO::Handle->new();
+
+      $scp_wh->autoflush(1);
+
+      sleep(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      # Make sure that the perms on the priv key are what OpenSSH wants
+      unless (chmod(0400, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0400: $!");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $scp_pid = open3($scp_wh, $scp_rh, $scp_eh, @cmd);
+      waitpid($scp_pid, 0);
+      my $exit_status = $?;
+
+      # Restore the perms on the priv key
+      unless (chmod(0644, $rsa_priv_key)) {
+        die("Can't set perms on $rsa_priv_key to 0644: $!");
+      }
+
+      my ($res, $errstr);
+      if ($exit_status >> 8 == 0) {
+        $errstr = join('', <$scp_eh>);
+        $res = 0;
+
+      } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res == 0) {
+        die("Can't upload dirs to server: $errstr");
+      }
+
+      my $path = "$dst_dir/src1.d";
+      $self->assert(-d $path,
+        test_msg("Directory '$path' does not exist as expected"));
+
+      $path = "$dst_dir/src1.d/file1.dat";
+      $self->assert(-f $path,
+        test_msg("File '$path' does not exist as expected"));
+
+      $path = "$dst_dir/src2.d";
+      $self->assert(-d $path,
+        test_msg("Directory '$path' does not exist as expected"));
+
+      $path = "$dst_dir/src2.d/file2.dat";
+      $self->assert(-f $path,
+        test_msg("File '$path' does not exist as expected"));
+
+      $path = "$dst_dir/src3.d";
+      $self->assert(-d $path,
+        test_msg("Directory '$path' does not exist as expected"));
+
+      $path = "$dst_dir/src3.d/file3.dat";
+      $self->assert(-f $path,
+        test_msg("File '$path' does not exist as expected"));
+
+      $path = "$dst_dir/src4.d";
+      $self->assert(-d $path,
+        test_msg("Directory '$path' does not exist as expected"));
+    };
+
+    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 scp_ext_upload_different_name_bug3425 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -42118,6 +51240,11 @@ sub scp_ext_upload_different_name_bug3425 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -42312,6 +51439,11 @@ sub scp_ext_upload_recursive_empty_dir {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -42527,6 +51659,11 @@ sub scp_ext_upload_shorter_file_bug4013 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -42737,6 +51874,11 @@ sub scp_ext_upload_file_with_timestamp_bug4026 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -42787,53 +51929,22 @@ sub scp_ext_upload_file_with_timestamp_bug4026 {
 sub scp_download {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
 
-  my $config_file = "$tmpdir/sftp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
 
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -42842,14 +51953,15 @@ sub scp_download {
 
       'mod_sftp.c' => [
         "SFTPEngine on",
-        "SFTPLog $log_file",
+        "SFTPLog $setup->{log_file}",
         "SFTPHostKey $rsa_host_key",
         "SFTPHostKey $dsa_host_key",
       ],
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -42880,24 +51992,23 @@ sub scp_download {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $ssh2->scp_get('sftp.conf', $test_file);
+      my $src_path = 'scp.conf';
+      my $res = $ssh2->scp_get($src_path, $test_file);
       unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't download sftp.conf from server: [$err_name] ($err_code) $err_str");
+        die("Can't download $src_path from server: [$err_name] ($err_code) $err_str");
       }
 
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("$test_file file does not exist as expected");
-      }
+      $self->assert(-f $test_file,
+        test_msg("File $test_file file does not exist"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -42906,7 +52017,7 @@ sub scp_download {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -42916,18 +52027,10 @@ sub scp_download {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub scp_download_enoent_bug3798 {
@@ -43522,24 +52625,320 @@ sub scp_download_fifo_bug3314 {
         die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      unless ($ssh2->auth_password($user, $passwd)) {
+      unless ($ssh2->auth_password($user, $passwd)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $res = $ssh2->scp_get($fifo, $test_file);
+      unless ($res) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't download $fifo from server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+
+      unless (-f $test_file) {
+        die("$test_file file does not exist as expected");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub scp_download_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $src_file = File::Spec->rel2abs("$test_dir/src.txt");
+  if (open(my $fh, "> $src_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $src_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $src_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $src_path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_get($src_path, $test_file);
+      unless ($res) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't download $src_path from server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file file does not exist"));
+    };
+    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 scp_download_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $src_file = File::Spec->rel2abs("$test_dir/src.txt");
+  if (open(my $fh, "> $src_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $src_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $src_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
         die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
       }
 
-      my $res = $ssh2->scp_get($fifo, $test_file);
+      my $src_path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_get($src_path, $test_file);
       unless ($res) {
         my ($err_code, $err_name, $err_str) = $ssh2->error();
-        die("Can't download $fifo from server: [$err_name] ($err_code) $err_str");
+        die("Can't download $src_path from server: [$err_name] ($err_code) $err_str");
       }
 
       $ssh2->disconnect();
 
-      unless (-f $test_file) {
-        die("$test_file file does not exist as expected");
-      }
+      $self->assert(-f $test_file,
+        test_msg("File $test_file file does not exist"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -43548,7 +52947,7 @@ sub scp_download_fifo_bug3314 {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -43558,18 +52957,306 @@ sub scp_download_fifo_bug3314 {
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub scp_download_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $src_file = File::Spec->rel2abs("$test_dir/src.txt");
+  if (open(my $fh, "> $src_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./src.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './src.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $src_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
 
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $src_path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_get($src_path, $test_file);
+      unless ($res) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't download $src_path from server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file file does not exist"));
+    };
+    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_append_logfile($log_file, $ex);
-    unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
 
-    die($ex);
+sub scp_download_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'scp');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $src_file = File::Spec->rel2abs("$test_dir/src.txt");
+  if (open(my $fh, "> $src_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $src_file: $!");
+    }
+
+  } else {
+    die("Can't open $src_file: $!");
   }
 
-  unlink($log_file);
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./src.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './src.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $src_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+    },
+  };
+
+  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::SSH2;
+
+  my $ex;
+
+  # Ignore SIGPIPE
+  local $SIG{PIPE} = sub { };
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $src_path = 'test.d/test.lnk';
+      my $res = $ssh2->scp_get($src_path, $test_file);
+      unless ($res) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't download $src_path from server: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+
+      $self->assert(-f $test_file,
+        test_msg("File $test_file file does not exist"));
+    };
+    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 scp_ext_download_bug3544 {
@@ -43718,8 +53405,8 @@ sub scp_ext_download_bug3544 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$scp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -44110,8 +53797,8 @@ sub scp_ext_download_glob_single_match_bug3904 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$scp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -44336,8 +54023,8 @@ sub scp_ext_download_glob_multiple_matches_bug3904 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
         if ($ENV{TEST_VERBOSE}) {
-          $errstr = join('', <$scp_eh>);
           print STDERR "Stderr: $errstr\n";
         }
 
@@ -44625,6 +54312,11 @@ sub scp_ext_download_recursive_dir_bug3456 {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -44846,6 +54538,11 @@ sub scp_ext_download_recursive_empty_dir {
         $res = 0;
 
       } else {
+        $errstr = join('', <$scp_eh>);
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "Stderr: $errstr\n";
+        }
+
         $res = 1;
       }
 
@@ -46544,6 +56241,11 @@ sub scp_log_extlog_var_f_upload {
       # Due to the way that Net::SSH2's scp support works, the full path is
       # sent to the server.  Note that the OpenSSH command-line scp tool
       # does not do this, so the %F value will not always be the full path.
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $upload_file = '/private' . $upload_file;
+      }
+
       my $expected = "$upload_file $upload_file";
       $self->assert($expected eq $line,
         test_msg("Expected '$expected', got '$line'"));
@@ -46925,6 +56627,11 @@ sub scp_log_xferlog_download {
           test_msg("Expected $expected, got $nbytes"));
 
         $expected = File::Spec->rel2abs($config_file);
+        if ($^O eq 'darwin') {
+          # Mac OSX hack
+          $expected = '/private' . $expected;
+        }
+
         $self->assert($expected eq $path,
           test_msg("Expected '$expected', got '$path'"));
 
@@ -47163,6 +56870,11 @@ sub scp_log_xferlog_upload {
           test_msg("Expected $expected, got $nbytes"));
 
         $expected = $upload_file;
+        if ($^O eq 'darwin') {
+          # Mac OSX hack
+          $expected = '/private' . $expected;
+        }
+
         $self->assert($expected eq $path,
           test_msg("Expected '$expected', got '$path'"));
 
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/rewrite.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/rewrite.pm
index 14f885b..0308717 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/rewrite.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/rewrite.pm
@@ -1011,6 +1011,11 @@ sub sftp_rewrite_realpath {
       my $expected;
 
       $expected = $test_file;
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $expected = '/private' . $expected;
+      }
+
       $self->assert($expected eq $resolved,
         test_msg("Expected '$expected', got '$resolved'"));
 
@@ -1182,6 +1187,11 @@ sub sftp_rewrite_realpath_backslashes_bug4017 {
       my $expected;
 
       $expected = $test_file;
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $expected = '/private' . $expected;
+      }
+
       $self->assert($expected eq $resolved,
         test_msg("Expected '$expected', got '$resolved'"));
 
@@ -3252,6 +3262,10 @@ sub sftp_rewrite_homedir {
       $ssh2->disconnect();
 
       my $expected = $home_dir;
+      if ($^O eq 'darwin') {
+        # Mac OSX hack
+        $expected = '/private' . $expected;
+      }
 
       $self->assert($expected eq $cwd,
         test_msg("Expected '$home_dir', got '$cwd'"));
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/sql.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/sql.pm
index 42e9720..342cd54 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/sql.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/sql.pm
@@ -561,6 +561,11 @@ EOS
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $test_file = '/private' . $test_file;
+  }
+
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
@@ -787,6 +792,11 @@ EOS
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $test_file = '/private' . $test_file;
+  }
+
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
@@ -1023,6 +1033,11 @@ EOS
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $test_file = '/private' . $test_file;
+  }
+
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
@@ -1268,6 +1283,11 @@ EOS
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $test_file = '/private' . $test_file;
+  }
+
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
@@ -1532,6 +1552,11 @@ EOS
   $self->assert($expected eq $xfer_failure,
     test_msg("Expected transfer failure '$expected', got '$xfer_failure'"));
 
+  if ($^O eq 'darwin') {
+    # Mac OSX hack
+    $test_file = '/private' . $test_file;
+  }
+
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/wrap2.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/wrap2.pm
index 7e7fe11..9ecd58f 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/wrap2.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/wrap2.pm
@@ -40,6 +40,11 @@ my $TESTS = {
     test_class => [qw(bug forking mod_wrap2_file sftp ssh2)],
   },
 
+  sftp_wrap2_deny_msg_var_u => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_wrap2_file sftp ssh2)],
+  },
+
 };
 
 sub new {
@@ -841,4 +846,172 @@ sub sftp_wrap2_deny_msg_on_auth_bug3670 {
   unlink($log_file);
 }
 
+sub sftp_wrap2_deny_msg_var_u {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $fh;
+  my $allow_file = File::Spec->rel2abs("$tmpdir/sftp.allow");
+  if (open($fh, "> $allow_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $allow_file: $!");
+  }
+
+  my $deny_file = File::Spec->rel2abs("$tmpdir/sftp.deny");
+  if (open($fh, "> $deny_file")) {
+    print $fh "ALL: ALL\n";
+
+    unless (close($fh)) {
+      die("Can't write $deny_file: $!");
+    }
+
+  } else {
+    die("Can't open $deny_file: $!");
+  }
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+
+  my $deny_msg = '"User %u denied by access rules"';
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+
+      'mod_wrap2.c' => {
+        WrapEngine => 'on',
+        WrapLog => $log_file,
+        WrapTables => "file:$allow_file file:$deny_file",
+        WrapDenyMsg => $deny_msg,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str (see unit test notes)");
+      }
+
+      if ($ssh2->auth_password($user, $passwd)) {
+        die("Password authentication succeeded unexpectedly");
+      }
+
+      my ($err_code, $err_name, $err_str) = $ssh2->error();
+
+      $ssh2->disconnect();
+
+      # Unfortunately, the Net::SSH2::SFTP API doesn't provide the error
+      # text as sent by the SFTP server.
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_pam.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_pam.pm
index 9a59bf4..463208b 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_pam.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_pam.pm
@@ -27,6 +27,11 @@ my $TESTS = {
     test_class => [qw(bug forking pam rootprivs ssh2)],
   },
 
+  sftp_pam_with_sql_auth => {
+    order => ++$order,
+    test_class => [qw(forking mod_sql_sqlite pam rootprivs ssh2)],
+  },
+
 };
 
 sub new {
@@ -229,4 +234,215 @@ sub sftp_pam_failed_login_attempts_bug3921 {
   unlink($log_file);
 }
 
+sub build_db {
+  my $cmd = shift;
+  my $db_script = shift;
+  my $check_exit_status = shift;
+  $check_exit_status = 0 unless defined $check_exit_status;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  my $exit_status = $?;
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  if ($check_exit_status) {
+    if ($? != 0) {
+      croak("'$cmd' failed");
+    }
+  }
+
+  unlink($db_script);
+  return 1;
+}
+
+sub sftp_pam_with_sql_auth {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+
+        "SFTPAuthMethods keyboard-interactive",
+      ],
+
+      'mod_sftp_pam.c' => {
+        AuthOrder => 'mod_sftp_pam.c* mod_sql.c',
+      }, 
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      if ($ssh2->auth_keyboard($user, $passwd)) {
+        die("Keyboard-interactive authentication succeeded unexpectedly");
+      }
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm
index 921637e..7199ee3 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sftp_sql.pm
@@ -67,6 +67,16 @@ my $TESTS = {
     test_class => [qw(bug forking ssh2)],
   },
 
+  ssh2_auth_publickey_rsa_sql_fp_env_vars => {
+    order => ++$order,
+    test_class => [qw(forking ssh2)],
+  },
+
+  ssh2_auth_publickey_rsa_sql_rfc4716_overlong_comment_bug4155 => {
+    order => ++$order,
+    test_class => [qw(bug forking ssh2)],
+  },
+
 };
 
 sub new {
@@ -145,11 +155,21 @@ sub ssh2_auth_publickey_rsa_sql {
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
 INSERT INTO sftpuserkeys (name, key) VALUES ('$user', '$rsa_data');
+
+CREATE TABLE users (
+  userid TEXT NOT NULL PRIMARY KEY,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+
 EOS
     unless (close($fh)) {
       die("Can't write $db_script: $!");
@@ -206,7 +226,7 @@ EOS
       },
 
       'mod_sql_sqlite.c' => {
-        SQLAuthenticate => 'off',
+        SQLAuthenticate => 'users usersetfast',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
         SQLNamedQuery => 'get-user-authorized-keys SELECT "key FROM sftpuserkeys WHERE name = \'%{0}\'"',
@@ -333,7 +353,8 @@ sub ssh2_auth_publickey_rsa_sql_var_U {
   my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
   my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
 
-  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
 
   my $config = {
     PidFile => $pid_file,
@@ -351,7 +372,7 @@ sub ssh2_auth_publickey_rsa_sql_var_U {
       },
 
       'mod_sql_sqlite.c' => {
-        SQLAuthenticate => 'off',
+        SQLAuthenticate => 'users usersetfast',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
         SQLNamedQuery => 'get-user-authorized-keys SELECT "key FROM sftpuserkeys WHERE name = \'%U\'"',
@@ -375,12 +396,22 @@ sub ssh2_auth_publickey_rsa_sql_var_U {
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
 INSERT INTO sftpuserkeys (name, key) VALUES ('$user', '$rsa_data');
 INSERT INTO sftpuserkeys (name, key) VALUES ('$config_user', '$rsa_data');
+
+CREATE TABLE users (
+  userid TEXT NOT NULL PRIMARY KEY,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+
 EOS
     unless (close($fh)) {
       die("Can't write $db_script: $!");
@@ -554,7 +585,7 @@ sub ssh2_auth_publickey_rsa_sql_var_u {
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
@@ -684,7 +715,7 @@ sub ssh2_auth_publickey_dsa_sql {
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
@@ -1044,7 +1075,7 @@ Byq2pv4VBo953gK7f1AQ==
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
@@ -1229,7 +1260,7 @@ Byq2pv4VBo953gK7f1AQ==
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
@@ -1417,7 +1448,7 @@ NJ/pRF0JutY1UDEUMQ==
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
@@ -1795,12 +1826,12 @@ gG0qd5fdWj6kccmG4PXw==
 
   my $rsa_rfc4716_data2 = '---- BEGIN SSH2 PUBLIC KEY ----
 Comment: "2048-bit RSA, converted from OpenSSH by tj at familiar"
-AAAAB3NzaC1yc2EAAAABIwAAAQEAsATKNn0iRFHa1+Mxb1s2z7fcNIESOZ5O+v2YsKCUaa
-3SpimJiKyemiTeiyOBfznLUXyhO8i8wYxBljr+NGknzxaF+em+U01xe5NFZt7cCKSoEc9Y
-bxydx2LzFL0Nti5BtKkkr49xcR1tYwdlOVnvKZ1EQ9kadTSidUeaeLpHw5H7mpAcTNjOsD
-AXe4w4xhGfy/YgQYGDw5j+vhwHMlqrTZ8s5xi9brT8JPWGPDYKwbiDlueQyh4Hk91xZWXb
-28EjT/vT9ukbfLcejrf9fU/YW9NYm5LFpk+mCkLqtiCKbXa3Q+XpDPmdHQYMjGIrGHBpU3
-ZKhkYKWVMtDsXqJk2BAQ==
+AAAAB3NzaC1yc2EAAAABIwAAAQEA1MDOdQ8ddQGd0hNPbO14zFAD1/c0Ontkw3egGGuVDm
+48VTnDNWGWbH5CirShUhjfLzxZkStyepdKFsYXZOeyBaHdqMfEhXhWZ+M7z9B9rUBM6R7W
+G34v7pzd8bOYDbff25PCITNYk9m/2ZGrFgAK5EChZ9jtxmaqhYWl6xKLilXYkmhId66TTq
+MgPUrM1sH9QeFV2axQmK1SVEkSzYTaY8WePds5D5cmZLmABAT3UYPQCcrOahISyKazJ9E+
+YjoMl9GoniSEiHTPK+XfyND83zIihJO16VxUVUMStR5yHBd133SVar4yKo8fv9wfgOxDcf
+GLxgWrkgXv/3qR/8zNaQ==
 ---- END SSH2 PUBLIC KEY ----
 ';
 
@@ -1810,7 +1841,7 @@ ZKhkYKWVMtDsXqJk2BAQ==
   if (open($fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE sftpuserkeys (
-  name TEXT NOT NULL,
+  name TEXT NOT NULL PRIMARY KEY,
   key BLOB NOT NULL
 );
 
@@ -1916,6 +1947,422 @@ EOS
     eval {
       my $ssh2 = Net::SSH2->new();
 
+      sleep(3);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str");
+      }
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file, $db_file);
+}
+
+sub get_sessions {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, key_fingerprint, key_fingerprint_algo FROM sftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub ssh2_auth_publickey_rsa_sql_fp_env_vars {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/sftp.db");
+
+  my $rsa_data = 'AAAAB3NzaC1yc2EAAAABIwAAAQEAzJ1CLwnVP9mUa8uyM+XBzxLxsRvGz4cS59aPTgdw7jGx1jCvC9ya400x7ej5Q4ubwlAAPblXzG5GYv2ROmYQ1DIjrhmR/61tDKUvAAZIgtvLZ00ydqqpq5lG4ubVJ4gW6sxbPfq/X12kV1gxGsFLUJCgoYInZGyIONrnvmQjFIfIx+mQXaK84uO6w0CT6KhRWgonajMrlO6P8O7qr80rFmOZsBNIMooyYrGTaMyxVsQK2SY+VKbXWFC+2HMmef62n+02ohAOBKtOsSOn8HE2wi7yMA0g8jRTd8kZcWBIkAhizPvl8pqG1F0DCmLn00rhPkByq2pv4VBo953gK7f1AQ==';
+
+  my $db_script = File::Spec->rel2abs("$tmpdir/sftp.sql");
+
+  my $fh;
+  if (open($fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE sftpuserkeys (
+  name TEXT NOT NULL PRIMARY KEY,
+  key BLOB NOT NULL
+);
+
+INSERT INTO sftpuserkeys (name, key) VALUES ('$user', '$rsa_data');
+
+CREATE TABLE sftpsessions (
+  user TEXT NOT NULL PRIMARY KEY,
+  key_fingerprint TEXT,
+  key_fingerprint_algo TEXT
+);
+EOS
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+
+  unlink($db_script);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key.pub');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql_sqlite.c' => [
+        'SQLAuthenticate off',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLNamedQuery get-user-authorized-keys SELECT "key FROM sftpuserkeys WHERE name = \'%{0}\'"',
+        'SQLNamedQuery log_user_key FREEFORM "INSERT INTO sftpsessions (user, key_fingerprint, key_fingerprint_algo) VALUES (\'%u\', \'%{env:SFTP_USER_PUBLICKEY_FINGERPRINT}\', \'%{env:SFTP_USER_PUBLICKEY_FINGERPRINT_ALGO}\')"',
+        'SQLLog INIT log_user_key',
+      ],
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys sql:/get-user-authorized-keys",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_publickey($user, $rsa_pub_key, $rsa_priv_key)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      $sftp = undef;
+
+      $ssh2->disconnect();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $key_fp, $key_fp_algo) = get_sessions($db_file,
+    "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected user '$expected', got '$login'"));
+
+  # The value here depends on the version of OpenSSL we use
+  my $expected_md5 = 'b8:ce:c2:e8:e8:9c:f7:93:11:a4:79:c2:48:64:19:45';
+  my $expected_sha256 = 'ad:13:cf:f3:07:f4:1f:20:95:44:e5:71:d9:e9:3c:9c:fa:4b:2a:d1:0d:90:fb:1f:a5:0e:77:ea:c1:91:f9:37';
+  $self->assert($expected_md5 eq $key_fp || $expected_sha256 eq $key_fp,
+    test_msg("Expected key fingerprint '$expected_md5' or '$expected_sha256', got '$key_fp'"));
+
+  $expected_md5 = 'MD5';
+  $expected_sha256 = 'SHA256';
+  $self->assert($expected_md5 eq $key_fp_algo || $expected_sha256 eq $key_fp_algo,
+    test_msg("Expected '$expected_md5' or '$expected_sha256', got '$key_fp_algo'"));
+
+  unlink($log_file, $db_file);
+}
+
+sub ssh2_auth_publickey_rsa_sql_rfc4716_overlong_comment_bug4155 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sftp.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sftp.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sftp.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sftp.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sftp.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/sftp.db");
+
+  my $rsa_rfc4716_data = '---- BEGIN SSH2 PUBLIC KEY ----
+Comment: "2048-bit RSA, converted from OpenSSH by jbaird at fc-qaftp01.corp.follett.com"
+AAAAB3NzaC1yc2EAAAABIwAAAQEA13H33uYHCPKX+any43mlzsjxrZuFpgdACmCuPa90Kh
+Xe6hIg6rx5nNLMOuKHfpMEshCQnj9zmtjSGyLZ9ufJv6Wg3SSHTIKQW2HtR9MLM8zzVXDE
+pcsWQUbwAs7mBdYKlOJxFP3J4PMVAiJe+GnQ889nXkdixB4SRU6LCfrPwg5c1Ho5FOPYys
+eAxMNjgsR1n8NUDg5COxlktnR+Tunlu/S/7TgcLi+ugvIIEB5vlhaHEZoPIpz2fl15l9FY
+ueYvzU73ESvUgdNQE16RmKpdmr6WwN9g5mG+tQCMrhWCkk4IAo5gUlx/Go1Osgp2r0ouj1
+MSJJkwubawXDDPj/RUjw==
+---- END SSH2 PUBLIC KEY ----';
+
+  my $db_script = File::Spec->rel2abs("$tmpdir/sftp.sql");
+
+  my $fh;
+  if (open($fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE sftpuserkeys (
+  name TEXT NOT NULL PRIMARY KEY,
+  key BLOB NOT NULL
+);
+
+INSERT INTO sftpuserkeys (name, key) VALUES ('$user', '$rsa_rfc4716_data');
+EOS
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+
+  unlink($db_script);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key_bug4155');
+  my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/test_rsa_key_bug4155.pub');
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 ssh2:20 sftp:20 scp:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql_sqlite.c' => {
+        SQLAuthenticate => 'off',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'get-user-authorized-keys SELECT "key FROM sftpuserkeys WHERE name = \'%{0}\'"',
+      },
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $log_file",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+        "SFTPAuthorizedUserKeys sql:/get-user-authorized-keys",
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
       sleep(1);
 
       unless ($ssh2->connect('127.0.0.1', $port)) {
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_shaper.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_shaper.pm
index 837a133..3881948 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_shaper.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_shaper.pm
@@ -31,6 +31,11 @@ my $TESTS = {
 #    test_class => [qw(bug forking)],
 #  }
 
+  shaper_sighup_shaperlog_bug4077 => {
+    order => ++$order,
+    test_class => [qw(bug forking os_linux)],
+  },
+
 };
 
 sub new {
@@ -41,6 +46,46 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
+sub get_server_pid {
+  my $pid_file = shift;
+
+  my $pid;
+  if (open(my $fh, "< $pid_file")) {
+    $pid = <$fh>;
+    chomp($pid);
+    close($fh);
+
+  } else {
+    croak("Can't read $pid_file: $!");
+  }
+
+  return $pid;
+}
+
+sub server_open_fds {
+  my $pid_file = shift;
+
+  my $pid = get_server_pid($pid_file);
+
+  my $proc_dir = "/proc/$pid/fd";
+  if (opendir(my $dirh, $proc_dir)) {
+    my $count = 0;
+
+    # Only count entries whose names are numbers
+    while (my $dent = readdir($dirh)) {
+      if ($dent =~ /^\d+$/) {
+        $count++;
+      }
+    }
+
+    closedir($dirh);
+    return $count;
+
+  } else {
+    croak("Can't open directory '$proc_dir': $!");
+  }
+}
+
 sub shaper_sighup {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -398,4 +443,103 @@ sub shaper_resumed_download_bug3928 {
   unlink($log_file);
 }
 
+sub shaper_sighup_shaperlog_bug4077 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/shaper.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/shaper.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/shaper.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $shaper_tab = File::Spec->rel2abs("$tmpdir/shaper.tab");
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/shaper.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/shaper.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+  
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_shaper.c' => {
+        ShaperEngine => 'on',
+        ShaperLog => $log_file,
+        ShaperTable => $shaper_tab,
+        ShaperAll => 'downrate 1500 uprate 1500',
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # First, start the server.
+  server_start($config_file);
+
+  # Give it a second to start up...
+  sleep(2);
+
+  my $orig_nfds = server_open_fds($pid_file);
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Found $orig_nfds open fds after server startup\n";
+  }
+
+  # Now modify the config, removing the ShaperLog...
+  delete($config->{IfModules}->{'mod_shaper.c'}->{ShaperLog});
+
+  unlink($config_file);
+  ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  # ...send SIGHUP...
+  server_restart($pid_file);
+  sleep(2);
+
+  # ...and get the open fd count again (should be 1 less).
+  my $restart_nfds = server_open_fds($pid_file);
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Found $restart_nfds open fds after server restart #1\n";
+  }
+
+  # Finally, stop the server
+  server_stop($pid_file);
+
+  my $expected_nfds = $orig_nfds-1;
+  $self->assert($expected_nfds == $restart_nfds,
+    test_msg("Expected $expected_nfds open fds, found $restart_nfds"));
+ 
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm
index bbaf589..43948ff 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_site.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -31,14 +32,64 @@ my $TESTS = {
     test_class => [qw(forking rootprivs)],
   },
 
+  site_chrgrp_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  site_chrgrp_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  site_chrgrp_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  site_chrgrp_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   # XXX Need more SITE CHGRP tests: invalid group names, paths with spaces,
   # paths that don't exist, PathAllow/DenyFilter, etc.
 
-  site_chmod_ok => {
+  site_chmod_no_login => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_chmod_numeric_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_chmod_symbolic_ok => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_chmod_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_chmod_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  site_chmod_rel_symlink => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  site_chmod_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   # XXX Need more SITE CHMOD tests: invalid modes (string and octal),
   # paths with spaces, paths that don't exist, PathAllow/DenyFilter, etc.
 };
@@ -118,6 +169,7 @@ sub site_help_ok {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      sleep(1);
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
@@ -137,7 +189,6 @@ sub site_help_ok {
       my $chmod_ok = 0;
 
       for (my $i = 0; $i < scalar(@$resp_msgs); $i++) {
-
         if ($resp_msgs->[$i] =~ / HELP/) {
           $help_ok = 1;
         }
@@ -263,6 +314,7 @@ sub site_help_chgrp_ok {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      sleep(1);
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
@@ -323,22 +375,7 @@ sub site_help_chgrp_ok {
 sub site_chgrp_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/site.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'site');
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
@@ -348,31 +385,21 @@ sub site_chgrp_ok {
     die("Can't open $test_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $test_file)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'fileperms:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -381,7 +408,8 @@ sub site_chgrp_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -398,25 +426,25 @@ sub site_chgrp_ok {
   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($user, $passwd);
+      sleep(1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      $client->site('CHGRP', "$group test.txt");
+      my $path = 'test.txt';
+      $client->site('CHGRP', "$setup->{group} $path");
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      my $expected;
-
-      $expected = 200;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = 'SITE CHGRP command successful';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -425,7 +453,7 @@ sub site_chgrp_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -435,38 +463,21 @@ sub site_chgrp_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub site_chmod_ok {
+sub site_chgrp_abs_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
 
-  my $config_file = "$tmpdir/site.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
     close($fh);
 
@@ -474,31 +485,33 @@ sub site_chmod_ok {
     die("Can't open $test_file: $!");
   }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-    unless (chown($uid, $gid, $home_dir, $test_file)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chown($setup->{uid}, 0, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/0: $!");
+    }
+  }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'fileperms:10',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -507,7 +520,8 @@ sub site_chmod_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -524,33 +538,149 @@ sub site_chmod_ok {
   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($user, $passwd);
+      sleep(1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      $client->site('CHMOD', "777 test.txt");
+      my $path = 'test.d/test.lnk';
+      $client->site('CHGRP', "$setup->{group} $path");
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
+      $client->quit();
 
-      my $expected;
-
-      $expected = 200;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 'SITE CHMOD command successful';
+      $expected = 'SITE CHGRP command successful';
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file_gid = (stat($test_file))[5];
+      $expected = $setup->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID $expected, got $file_gid"));
+    };
+    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 site_chgrp_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chown($setup->{uid}, 0, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/0: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      $client->site('CHGRP', "$setup->{group} $path");
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
       $client->quit();
 
-      my $perms = ((stat($test_file))[2] &07777);
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = 0777;
-      $self->assert($expected == $perms,
-        test_msg("Expected '$expected', got '$perms'"));
-    };
+      $expected = 'SITE CHGRP command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
+      my $file_gid = (stat($test_file))[5];
+      $expected = $setup->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID $expected, got $file_gid"));
+    };
     if ($@) {
       $ex = $@;
     }
@@ -559,7 +689,7 @@ sub site_chmod_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -569,15 +699,1103 @@ sub site_chmod_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub site_chgrp_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
   }
 
-  unlink($log_file);
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chown($setup->{uid}, 0, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/0: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      $client->site('CHGRP', "$setup->{group} $path");
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHGRP command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file_gid = (stat($test_file))[5];
+      $expected = $setup->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID $expected, got $file_gid"));
+    };
+    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 site_chgrp_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chown($setup->{uid}, 0, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/0: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      $client->site('CHGRP', "$setup->{group} $path");
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHGRP command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $file_gid = (stat($test_file))[5];
+      $expected = $setup->{gid};
+      $self->assert($expected == $file_gid,
+        test_msg("Expected file GID $expected, got $file_gid"));
+    };
+    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 site_chmod_no_login {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_file)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+      eval { $client->site('CHMOD', "777 test.txt") };
+      unless ($@) {
+        die("SITE CHMOD succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Please login with USER and PASS';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_chmod_numeric_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_file)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+
+      $client->site('CHMOD', "777 test.txt");
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = 'SITE CHMOD command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      my $perms = ((stat($test_file))[2] &07777);
+
+      $expected = 0777;
+      $self->assert($expected == $perms,
+        test_msg("Expected '$expected', got '$perms'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_chmod_symbolic_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  if ($< == 0) {
+    unless (chown($setup->{uid}, $setup->{gid}, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.txt';
+      my ($resp_code, $resp_msg) = $client->site('CHMOD', "ugo+rwx $path");
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHMOD command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $perms = ((stat($test_file))[2] &07777);
+      $expected = 0777;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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 site_chmod_abs_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('CHMOD', "ugo+rwx $path");
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHMOD command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $perms = ((stat($test_file))[2] &07777);
+      $expected = 0777;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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 site_chmod_abs_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
+
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('CHMOD', "ugo+rwx $path");
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHMOD command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $perms = ((stat($test_file))[2] &07777);
+      $expected = 0777;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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 site_chmod_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('CHMOD', "ugo+rwx $path");
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHMOD command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $perms = ((stat($test_file))[2] &07777);
+      $expected = 0777;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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 site_chmod_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_file to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fileperms:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('CHMOD', "ugo+rwx $path");
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'SITE CHMOD command successful';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my $perms = ((stat($test_file))[2] &07777);
+      $expected = 0777;
+      $self->assert($expected == $perms,
+        test_msg("Expected perms '$expected', got '$perms'"));
+    };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_site_misc.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_site_misc.pm
index 174b749..df75500 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_site_misc.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_site_misc.pm
@@ -4,6 +4,7 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Cwd;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
@@ -41,6 +42,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  site_misc_utime_abs_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_misc_utime_abs_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
+  site_misc_utime_rel_symlink => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_misc_utime_rel_symlink_chrooted_bug4219 => {
+    order => ++$order,
+    test_class => [qw(bug forking rootprivs)],
+  },
+
   site_feat_ok => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -114,6 +135,26 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  site_misc_utime_atime_mtime_ctime_bug4130 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  site_misc_utime_atime_mtime_ctime_with_spaces_bug4130 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  site_misc_extlog_rmdir_resp_code_empty_dir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  site_misc_extlog_rmdir_resp_code_nonempty_dir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
 };
 
 sub new {
@@ -514,45 +555,15 @@ sub site_misc_symlink_ok {
 sub site_misc_utime_ok {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/site.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
-
-  my $log_file = File::Spec->rel2abs('tests.log');
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  my $setup = test_setup($tmpdir, 'site');
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -561,7 +572,8 @@ sub site_misc_utime_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -578,32 +590,31 @@ sub site_misc_utime_ok {
   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($user, $passwd);
-
-      my ($resp_code, $resp_msg);
-      ($resp_code, $resp_msg) = $client->site('UTIME', '200002240826',
-        'site.conf');
+      sleep(1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my $path = 'site.conf';
+      my ($resp_code, $resp_msg) = $client->site('UTIME', '200002240826',
+        $path);
+      $client->quit();
 
-      $expected = 200;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "SITE UTIME command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      my ($atime, $mtime) = (stat($config_file))[8,9];
+      my ($atime, $mtime) = (stat($setup->{config_file}))[8,9];
 
       $expected = 951380760;
       $self->assert($expected == $atime,
-        test_msg("Expected $expected, got $atime"));
+        test_msg("Expected file atime $expected, got $atime"));
       $self->assert($expected == $mtime,
-        test_msg("Expected $expected, got $mtime"));
+        test_msg("Expected file mtime $expected, got $mtime"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -612,7 +623,7 @@ sub site_misc_utime_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -622,15 +633,10 @@ sub site_misc_utime_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub site_misc_utime_with_sec_ok {
@@ -755,50 +761,54 @@ sub site_misc_utime_with_sec_ok {
   unlink($log_file);
 }
 
-sub site_feat_ok {
+sub site_misc_utime_abs_symlink {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
 
-  my $config_file = "$tmpdir/site.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = File::Spec->rel2abs('tests.log');
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -807,7 +817,8 @@ sub site_feat_ok {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -824,38 +835,31 @@ sub site_feat_ok {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      $client->feat();
-      my $resp_code = $client->response_code();
-      my $resp_msgs = $client->response_msgs();
+      sleep(1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $expected;
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('UTIME', '200002240826',
+        $path);
+      $client->quit();
 
-      $expected = 211;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      my $nfeat = scalar(@$resp_msgs);
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      my $feats = {
-        ' SITE MKDIR' => 1,
-        ' SITE RMDIR' => 1,
-        ' SITE SYMLINK' => 1,
-        ' SITE UTIME' => 1,
-      };
+      $expected = "SITE UTIME command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      for (my $i = 0; $i < $nfeat; $i++) {
-        if (defined($feats->{$resp_msgs->[$i]})) {
-          delete($feats->{$resp_msgs->[$i]});
-        }
-      }
+      my ($atime, $mtime) = (stat($test_file))[8,9];
 
-      my $remain_misc_feats = scalar(keys(%$feats));
-      $self->assert($remain_misc_feats == 0,
-        test_msg("Unexpected 0, got $remain_misc_feats"));
+      $expected = 951380760;
+      $self->assert($expected == $atime,
+        test_msg("Expected file atime $expected, got $atime"));
+      $self->assert($expected == $mtime,
+        test_msg("Expected file mtime $expected, got $mtime"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -864,7 +868,7 @@ sub site_feat_ok {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -874,63 +878,62 @@ sub site_feat_ok {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub site_misc_symlink_ncftpd_chroot_bug {
+sub site_misc_utime_abs_symlink_chrooted_bug4219 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
 
-  my $config_file = "$tmpdir/site.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
 
-  my $log_file = File::Spec->rel2abs('tests.log');
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $test_symlink = File::Spec->rel2abs("$test_dir/test.lnk");
 
-  my $test_symlink = File::Spec->rel2abs("$home_dir/hack/.message");
+  my $dst_path = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $dst_path = '/private' . $dst_path;
+  }
+
+  unless (symlink($dst_path, $test_symlink)) {
+    die("Can't symlink $test_symlink to $dst_path: $!");
+  }
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
-
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
     DefaultRoot => '~',
-    DisplayChdir => '.message',
 
     IfModules => {
       'mod_delay.c' => {
@@ -939,7 +942,8 @@ sub site_misc_symlink_ncftpd_chroot_bug {
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -956,54 +960,974 @@ sub site_misc_symlink_ncftpd_chroot_bug {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
-      $client->login($user, $passwd);
-
-      my ($expected, $resp_code, $resp_msg);
+      sleep(1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $conn = $client->retr_raw('/etc/passwd');
-      if ($conn) {
-        die("RETR /etc/passwd succeeded unexpectedly");
-      }
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('UTIME', '200002240826',
+        $path);
+      $client->quit();
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
-       
-      $expected = 550;
+      my $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "/etc/passwd: No such file or directory";
+      $expected = "SITE UTIME command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      ($resp_code, $resp_msg) = $client->mkd('hack');
-
-      $expected = 257;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      my ($atime, $mtime) = (stat($test_file))[8,9];
 
-      $expected = "\"/hack\" - Directory successfully created";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $expected = 951380760;
+      $self->assert($expected == $atime,
+        test_msg("Expected file atime $expected, got $atime"));
+      $self->assert($expected == $mtime,
+        test_msg("Expected file mtime $expected, got $mtime"));
+    };
+    if ($@) {
+      $ex = $@;
+    }
 
-      eval { $client->site('SYMLINK', '/etc/passwd', 'hack/.message') };
-      unless ($@) {
-        die("SITE SYMLINK succeeded unexpectedly");
-      }
+    $wfh->print("done\n");
+    $wfh->flush();
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+  } 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 site_misc_utime_rel_symlink {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('UTIME', '200002240826',
+        $path);
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "SITE UTIME command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my ($atime, $mtime) = (stat($test_file))[8,9];
+
+      $expected = 951380760;
+      $self->assert($expected == $atime,
+        test_msg("Expected file atime $expected, got $atime"));
+      $self->assert($expected == $mtime,
+        test_msg("Expected file mtime $expected, got $mtime"));
+    };
+    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 site_misc_utime_rel_symlink_chrooted_bug4219 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'site');
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  my $test_file = File::Spec->rel2abs("$test_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the test directory in order to create a relative path in the
+  # symlink we need
+
+  my $cwd = getcwd();
+  unless (chdir($test_dir)) {
+    die("Can't chdir to $test_dir: $!");
+  }
+
+  unless (symlink('./test.txt', './test.lnk')) {
+    die("Can't symlink 'test.lnk' to './test.txt': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  if ($< == 0) {
+    unless (chmod(0755, $test_dir)) {
+      die("Can't set perms on $test_dir to 0755: $!");
+    }
+
+    unless (chown($setup->{uid}, $setup->{gid}, $test_dir, $test_file)) {
+      die("Can't set owner of $test_dir to $setup->{uid}/$setup->{gid}: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    DefaultRoot => '~',
+
+    IfModules => {
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $path = 'test.d/test.lnk';
+      my ($resp_code, $resp_msg) = $client->site('UTIME', '200002240826',
+        $path);
+      $client->quit();
+
+      my $expected = 200;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "SITE UTIME command successful";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      my ($atime, $mtime) = (stat($test_file))[8,9];
+
+      $expected = 951380760;
+      $self->assert($expected == $atime,
+        test_msg("Expected file atime $expected, got $atime"));
+      $self->assert($expected == $mtime,
+        test_msg("Expected file mtime $expected, got $mtime"));
+    };
+    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 site_feat_ok {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->feat();
+      my $resp_code = $client->response_code();
+      my $resp_msgs = $client->response_msgs();
+
+      my $expected;
+
+      $expected = 211;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      my $nfeat = scalar(@$resp_msgs);
+
+      my $feats = {
+        ' SITE MKDIR' => 1,
+        ' SITE RMDIR' => 1,
+        ' SITE SYMLINK' => 1,
+        ' SITE UTIME' => 1,
+      };
+
+      for (my $i = 0; $i < $nfeat; $i++) {
+        if (defined($feats->{$resp_msgs->[$i]})) {
+          delete($feats->{$resp_msgs->[$i]});
+        }
+      }
+
+      my $remain_misc_feats = scalar(keys(%$feats));
+      $self->assert($remain_misc_feats == 0,
+        test_msg("Unexpected 0, got $remain_misc_feats"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_misc_symlink_ncftpd_chroot_bug {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_symlink = File::Spec->rel2abs("$home_dir/hack/.message");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
+    DisplayChdir => '.message',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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, 1);
+      $client->login($user, $passwd);
+
+      my ($expected, $resp_code, $resp_msg);
+
+      my $conn = $client->retr_raw('/etc/passwd');
+      if ($conn) {
+        die("RETR /etc/passwd succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+       
       $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "SYMLINK /etc/passwd hack/.message: No such file or directory";
+      $expected = "/etc/passwd: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      ($resp_code, $resp_msg) = $client->mkd('hack');
+
+      $expected = 257;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "\"/hack\" - Directory successfully created";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      eval { $client->site('SYMLINK', '/etc/passwd', 'hack/.message') };
+      unless ($@) {
+        die("SITE SYMLINK succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg();
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "SYMLINK /etc/passwd hack/.message: No such file or directory";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_misc_mkdir_failed_bug3518 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_site_misc.c' => {
+        SiteMiscEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      eval { $client->site('MKDIR', 'foo/bar/baz') };
+      unless ($@) {
+        die("SITE MKDIR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "'SITE MKDIR' not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      # Make sure that the test dir is NOT present
+      if (-d $test_dir) {
+        die("Directory $test_dir exists unexpectedly");
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_misc_rmdir_failed_bug3518 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+  mkpath($sub_dir);
+
+  my $test_dirs = [
+    File::Spec->rel2abs("$tmpdir/foo"),
+    File::Spec->rel2abs("$tmpdir/foo/bar"),
+    $sub_dir,
+  ];
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar/quxx.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Quzz\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_site_misc.c' => {
+        SiteMiscEngine => 'off',
+      },
+
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      eval { $client->site('RMDIR', 'foo') };
+      unless ($@) {
+        die("SITE RMDIR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "'SITE RMDIR' not understood";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      # Make sure that the test file is NOT gone, along with all of the
+      # test dirs.
+      unless (-f $test_file) {
+        die("File $test_file does not exist as expected");
+      }
+
+      foreach my $test_dir (@$test_dirs) {
+        unless (-d $test_dir) {
+          die("Directory $test_dir does not exist as expected");
+        }
+      }
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_misc_symlink_failed_bug3518 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_symlink = File::Spec->rel2abs("$tmpdir/foo");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_site_misc.c' => {
+        SiteMiscEngine => 'off',
+      },
+
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      eval { $client->site('SYMLINK', 'site.conf', 'foo') };
+      unless ($@) {
+        die("SITE SYMLINK succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 500;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "'SITE SYMLINK' not understood";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
-      $client->quit();
+      if (-l $test_symlink) {
+        die("Symlink $test_symlink exists unexpectedly");
+      }
     };
 
     if ($@) {
@@ -1035,7 +1959,7 @@ sub site_misc_symlink_ncftpd_chroot_bug {
   unlink($log_file);
 }
 
-sub site_misc_mkdir_failed_bug3518 {
+sub site_misc_utime_failed_bug3518 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1054,8 +1978,6 @@ sub site_misc_mkdir_failed_bug3518 {
   my $uid = 500;
   my $gid = 500;
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -1088,6 +2010,7 @@ sub site_misc_mkdir_failed_bug3518 {
       'mod_site_misc.c' => {
         SiteMiscEngine => 'off',
       },
+
     },
   };
 
@@ -1111,9 +2034,9 @@ sub site_misc_mkdir_failed_bug3518 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      eval { $client->site('MKDIR', 'foo/bar/baz') };
+      eval { $client->site('UTIME', '200002240826', 'site.conf') };
       unless ($@) {
-        die("SITE MKDIR succeeded unexpectedly");
+        die("SITE UTIME succeeded unexpectedly");
       }
 
       my $resp_code = $client->response_code();
@@ -1125,11 +2048,160 @@ sub site_misc_mkdir_failed_bug3518 {
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "'SITE MKDIR' not understood";
+      $expected = "'SITE UTIME' not understood";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
+    };
 
-      # Make sure that the test dir is NOT present
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub site_misc_mkdir_failed_bug3519 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/site.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+
+  my $log_file = File::Spec->rel2abs('tests.log');
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/site.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/site.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($start_dir);
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+  if (open(my $fh, ">> $config_file")) {
+    my $limit_dir = "$tmpdir/foo";
+    if ($^O eq 'darwin') {
+      $limit_dir = '/private' . $limit_dir;
+    }
+
+    print $fh <<EOT;
+<Directory $limit_dir>
+  <Limit WRITE>
+    DenyAll
+  </Limit>
+</Directory>
+EOT
+
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
+  # 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($user, $passwd);
+
+      my $path = 'bar/../foo/bar/baz';
+
+      eval { $client->site('MKDIR', $path) };
+      unless ($@) {
+        die("SITE MKDIR succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 550;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "MKDIR $path: Operation not permitted";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Make sure the directory doesn't exist
       if (-d $test_dir) {
         die("Directory $test_dir exists unexpectedly");
       }
@@ -1164,7 +2236,7 @@ sub site_misc_mkdir_failed_bug3518 {
   unlink($log_file);
 }
 
-sub site_misc_rmdir_failed_bug3518 {
+sub site_misc_rmdir_failed_bug3519 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1183,6 +2255,9 @@ sub site_misc_rmdir_failed_bug3518 {
   my $uid = 500;
   my $gid = 500;
 
+  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($start_dir);
+
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
   mkpath($sub_dir);
 
@@ -1232,16 +2307,33 @@ sub site_misc_rmdir_failed_bug3518 {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
-
-      'mod_site_misc.c' => {
-        SiteMiscEngine => 'off',
-      },
-
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    my $limit_dir = "$tmpdir/foo";
+    if ($^O eq 'darwin') {
+      $limit_dir = '/private' . $limit_dir;
+    }
+
+    print $fh <<EOT;
+<Directory $limit_dir>
+  <Limit WRITE>
+    DenyAll
+  </Limit>
+</Directory>
+EOT
+
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -1260,7 +2352,9 @@ sub site_misc_rmdir_failed_bug3518 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      eval { $client->site('RMDIR', 'foo') };
+      my $path = 'bar/../foo';
+
+      eval { $client->site('RMDIR', $path) };
       unless ($@) {
         die("SITE RMDIR succeeded unexpectedly");
       }
@@ -1270,11 +2364,11 @@ sub site_misc_rmdir_failed_bug3518 {
 
       my $expected;
 
-      $expected = 500;
+      $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "'SITE RMDIR' not understood";
+      $expected = "RMDIR $path: Operation not permitted";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
@@ -1320,7 +2414,7 @@ sub site_misc_rmdir_failed_bug3518 {
   unlink($log_file);
 }
 
-sub site_misc_symlink_failed_bug3518 {
+sub site_misc_symlink_failed_bug3519 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1339,6 +2433,9 @@ sub site_misc_symlink_failed_bug3518 {
   my $uid = 500;
   my $gid = 500;
 
+  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($start_dir);
+
   my $test_symlink = File::Spec->rel2abs("$tmpdir/foo");
 
   # Make sure that, if we're running as root, that the home directory has
@@ -1370,15 +2467,33 @@ sub site_misc_symlink_failed_bug3518 {
         DelayEngine => 'off',
       },
 
-      'mod_site_misc.c' => {
-        SiteMiscEngine => 'off',
-      },
-
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    my $limit_dir = "$tmpdir/foo";
+    if ($^O eq 'darwin') {
+      $limit_dir = '/private' . $limit_dir;
+    }
+
+    print $fh <<EOT;
+<Directory $limit_dir>
+  <Limit WRITE>
+    DenyAll
+  </Limit>
+</Directory>
+EOT
+
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -1397,6 +2512,8 @@ sub site_misc_symlink_failed_bug3518 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
+      my $path = "bar/../foo";
+
       eval { $client->site('SYMLINK', 'site.conf', 'foo') };
       unless ($@) {
         die("SITE SYMLINK succeeded unexpectedly");
@@ -1407,11 +2524,11 @@ sub site_misc_symlink_failed_bug3518 {
 
       my $expected;
 
-      $expected = 500;
+      $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "'SITE SYMLINK' not understood";
+      $expected = "foo: Operation not permitted";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
@@ -1449,7 +2566,7 @@ sub site_misc_symlink_failed_bug3518 {
   unlink($log_file);
 }
 
-sub site_misc_utime_failed_bug3518 {
+sub site_misc_utime_failed_bug3519 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1468,14 +2585,31 @@ sub site_misc_utime_failed_bug3518 {
   my $uid = 500;
   my $gid = 500;
 
+  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
+  mkpath($start_dir);
+
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
+  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
+    unless (chmod(0755, $home_dir, $start_dir, $sub_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
+    unless (chown($uid, $gid, $home_dir, $start_dir, $sub_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -1497,15 +2631,33 @@ sub site_misc_utime_failed_bug3518 {
         DelayEngine => 'off',
       },
 
-      'mod_site_misc.c' => {
-        SiteMiscEngine => 'off',
-      },
-
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    my $limit_dir = "$tmpdir/foo";
+    if ($^O eq 'darwin') {
+      $limit_dir = '/private' . $limit_dir;
+    }
+
+    print $fh <<EOT;
+<Directory $limit_dir>
+  <Limit WRITE>
+    DenyAll
+  </Limit>
+</Directory>
+EOT
+
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -1524,7 +2676,10 @@ sub site_misc_utime_failed_bug3518 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      eval { $client->site('UTIME', '200002240826', 'site.conf') };
+      my $path = 'bar/../foo/test.txt';
+      my $timestamp = '200002240826';
+
+      eval { $client->site('UTIME', $timestamp, $path) };
       unless ($@) {
         die("SITE UTIME succeeded unexpectedly");
       }
@@ -1534,11 +2689,11 @@ sub site_misc_utime_failed_bug3518 {
 
       my $expected;
 
-      $expected = 500;
+      $expected = 550;
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "'SITE UTIME' not understood";
+      $expected = "UTIME $timestamp $path: Operation not permitted";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
     };
@@ -1572,7 +2727,7 @@ sub site_misc_utime_failed_bug3518 {
   unlink($log_file);
 }
 
-sub site_misc_mkdir_failed_bug3519 {
+sub site_misc_mkdir_failed_limit {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1591,9 +2746,6 @@ sub site_misc_mkdir_failed_bug3519 {
   my $uid = 500;
   my $gid = 500;
 
-  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
-  mkpath($start_dir);
-
   my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
 
   # Make sure that, if we're running as root, that the home directory has
@@ -1625,26 +2777,16 @@ sub site_misc_mkdir_failed_bug3519 {
         DelayEngine => 'off',
       },
     },
-  };
-
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOT;
-<Directory $tmpdir/foo>
-  <Limit WRITE>
-    DenyAll
-  </Limit>
-</Directory>
-EOT
+    Limit => {
+      SITE_MKDIR => {
+        DenyAll => '',
+      },
+    },
 
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
+  };
 
-  } else {
-    die("Can't open $config_file: $!");
-  }
+  my ($port, $config_user, $config_group) = config_write($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
@@ -1664,7 +2806,7 @@ EOT
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $path = 'bar/../foo/bar/baz';
+      my $path = 'foo/bar/baz';
 
       eval { $client->site('MKDIR', $path) };
       unless ($@) {
@@ -1684,9 +2826,7 @@ EOT
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
-      $client->quit();
-
-      # Make sure the directory doesn't exist
+      # Make sure that the test dir is NOT present
       if (-d $test_dir) {
         die("Directory $test_dir exists unexpectedly");
       }
@@ -1721,7 +2861,7 @@ EOT
   unlink($log_file);
 }
 
-sub site_misc_rmdir_failed_bug3519 {
+sub site_misc_rmdir_failed_limit {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1740,9 +2880,6 @@ sub site_misc_rmdir_failed_bug3519 {
   my $uid = 500;
   my $gid = 500;
 
-  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
-  mkpath($start_dir);
-
   my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
   mkpath($sub_dir);
 
@@ -1793,27 +2930,16 @@ sub site_misc_rmdir_failed_bug3519 {
         DelayEngine => 'off',
       },
     },
+
+    Limit => {
+      SITE_RMDIR => {
+        DenyAll => '',
+      },
+    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOT;
-<Directory $tmpdir/foo>
-  <Limit WRITE>
-    DenyAll
-  </Limit>
-</Directory>
-EOT
-
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -1832,7 +2958,7 @@ EOT
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $path = 'bar/../foo';
+      my $path = 'foo';
 
       eval { $client->site('RMDIR', $path) };
       unless ($@) {
@@ -1894,7 +3020,7 @@ EOT
   unlink($log_file);
 }
 
-sub site_misc_symlink_failed_bug3519 {
+sub site_misc_symlink_failed_limit {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -1913,9 +3039,6 @@ sub site_misc_symlink_failed_bug3519 {
   my $uid = 500;
   my $gid = 500;
 
-  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
-  mkpath($start_dir);
-
   my $test_symlink = File::Spec->rel2abs("$tmpdir/foo");
 
   # Make sure that, if we're running as root, that the home directory has
@@ -1946,29 +3069,17 @@ sub site_misc_symlink_failed_bug3519 {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
+    },
 
+    Limit => {
+      SITE_SYMLINK => {
+        DenyAll => '',
+      },
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOT;
-<Directory $tmpdir/foo>
-  <Limit WRITE>
-    DenyAll
-  </Limit>
-</Directory>
-EOT
-
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -1987,9 +3098,10 @@ EOT
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $path = "bar/../foo";
+      my $src = 'site.conf';
+      my $dst = 'foo';
 
-      eval { $client->site('SYMLINK', 'site.conf', 'foo') };
+      eval { $client->site('SYMLINK', $src, $dst) };
       unless ($@) {
         die("SITE SYMLINK succeeded unexpectedly");
       }
@@ -2003,7 +3115,7 @@ EOT
       $self->assert($expected == $resp_code,
         test_msg("Expected $expected, got $resp_code"));
 
-      $expected = "foo: Operation not permitted";
+      $expected = "$src: Operation not permitted";
       $self->assert($expected eq $resp_msg,
         test_msg("Expected '$expected', got '$resp_msg'"));
 
@@ -2041,7 +3153,7 @@ EOT
   unlink($log_file);
 }
 
-sub site_misc_utime_failed_bug3519 {
+sub site_misc_utime_failed_limit {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2060,31 +3172,14 @@ sub site_misc_utime_failed_bug3519 {
   my $uid = 500;
   my $gid = 500;
 
-  my $start_dir = File::Spec->rel2abs("$tmpdir/bar");
-  mkpath($start_dir);
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
-  my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $start_dir, $sub_dir)) {
+    unless (chmod(0755, $home_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $start_dir, $sub_dir)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -2105,28 +3200,16 @@ sub site_misc_utime_failed_bug3519 {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
-
     },
-  };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
-
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOT;
-<Directory $tmpdir/foo>
-  <Limit WRITE>
-    DenyAll
-  </Limit>
-</Directory>
-EOT
-
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
+    Limit => {
+      SITE_UTIME => {
+        DenyAll => '',
+      },
+    },
+  };
 
-  } else {
-    die("Can't open $config_file: $!");
-  }
+  my ($port, $config_user, $config_group) = config_write($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
@@ -2146,8 +3229,8 @@ EOT
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $path = 'bar/../foo/test.txt';
       my $timestamp = '200002240826';
+      my $path = 'site.conf';
 
       eval { $client->site('UTIME', $timestamp, $path) };
       unless ($@) {
@@ -2197,7 +3280,7 @@ EOT
   unlink($log_file);
 }
 
-sub site_misc_mkdir_failed_limit {
+sub site_misc_utime_atime_mtime_ctime_bug4130 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2212,12 +3295,11 @@ sub site_misc_mkdir_failed_limit {
 
   my $user = 'proftpd';
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
-  my $test_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -2232,7 +3314,7 @@ sub site_misc_mkdir_failed_limit {
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
     PidFile => $pid_file,
@@ -2247,13 +3329,6 @@ sub site_misc_mkdir_failed_limit {
         DelayEngine => 'off',
       },
     },
-
-    Limit => {
-      SITE_MKDIR => {
-        DenyAll => '',
-      },
-    },
-
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -2276,30 +3351,26 @@ sub site_misc_mkdir_failed_limit {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $path = 'foo/bar/baz';
-
-      eval { $client->site('MKDIR', $path) };
-      unless ($@) {
-        die("SITE MKDIR succeeded unexpectedly");
-      }
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my ($resp_code, $resp_msg) = $client->site('UTIME', 'site.conf',
+        '200002240826', '200002240826', '200002240826', 'UTC');
 
       my $expected;
 
-      $expected = 550;
+      $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "MKDIR $path: Operation not permitted";
+      $expected = "SITE UTIME command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      # Make sure that the test dir is NOT present
-      if (-d $test_dir) {
-        die("Directory $test_dir exists unexpectedly");
-      }
+      my ($atime, $mtime) = (stat($config_file))[8,9];
+
+      $expected = 951380760;
+      $self->assert($expected == $atime,
+        test_msg("Expected $expected, got $atime"));
+      $self->assert($expected == $mtime,
+        test_msg("Expected $expected, got $mtime"));
     };
 
     if ($@) {
@@ -2331,7 +3402,7 @@ sub site_misc_mkdir_failed_limit {
   unlink($log_file);
 }
 
-sub site_misc_rmdir_failed_limit {
+sub site_misc_utime_atime_mtime_ctime_with_spaces_bug4130 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -2346,31 +3417,11 @@ sub site_misc_rmdir_failed_limit {
 
   my $user = 'proftpd';
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
-  mkpath($sub_dir);
-
-  my $test_dirs = [
-    File::Spec->rel2abs("$tmpdir/foo"),
-    File::Spec->rel2abs("$tmpdir/foo/bar"),
-    $sub_dir,
-  ];
-
-  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar/quxx.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Quzz\n";
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -2385,7 +3436,15 @@ sub site_misc_rmdir_failed_limit {
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test file");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
   my $config = {
     PidFile => $pid_file,
@@ -2400,12 +3459,6 @@ sub site_misc_rmdir_failed_limit {
         DelayEngine => 'off',
       },
     },
-
-    Limit => {
-      SITE_RMDIR => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -2428,37 +3481,26 @@ sub site_misc_rmdir_failed_limit {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $path = 'foo';
-
-      eval { $client->site('RMDIR', $path) };
-      unless ($@) {
-        die("SITE RMDIR succeeded unexpectedly");
-      }
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my ($resp_code, $resp_msg) = $client->site('UTIME', 'test', 'file',
+        '200002240826', '200002240826', '200002240826', 'UTC');
 
       my $expected;
 
-      $expected = 550;
+      $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "RMDIR $path: Operation not permitted";
+      $expected = "SITE UTIME command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      # Make sure that the test file is NOT gone, along with all of the
-      # test dirs.
-      unless (-f $test_file) {
-        die("File $test_file does not exist as expected");
-      }
+      my ($atime, $mtime) = (stat($test_file))[8,9];
 
-      foreach my $test_dir (@$test_dirs) {
-        unless (-d $test_dir) {
-          die("Directory $test_dir does not exist as expected");
-        }
-      }
+      $expected = 951380760;
+      $self->assert($expected == $atime,
+        test_msg("Expected $expected, got $atime"));
+      $self->assert($expected == $mtime,
+        test_msg("Expected $expected, got $mtime"));
     };
 
     if ($@) {
@@ -2490,13 +3532,14 @@ sub site_misc_rmdir_failed_limit {
   unlink($log_file);
 }
 
-sub site_misc_symlink_failed_limit {
+sub site_misc_extlog_rmdir_resp_code_empty_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/site.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = File::Spec->rel2abs('tests.log');
 
@@ -2505,11 +3548,31 @@ sub site_misc_symlink_failed_limit {
 
   my $user = 'proftpd';
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
-  my $test_symlink = File::Spec->rel2abs("$tmpdir/foo");
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+  mkpath($sub_dir);
+
+  my $test_dirs = [
+    File::Spec->rel2abs("$tmpdir/foo"),
+    File::Spec->rel2abs("$tmpdir/foo/bar"),
+    $sub_dir,
+  ];
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar/quxx.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Quzz\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -2525,7 +3588,7 @@ sub site_misc_symlink_failed_limit {
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
     PidFile => $pid_file,
@@ -2535,17 +3598,14 @@ sub site_misc_symlink_failed_limit {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    LogFormat => 'custom "%r: %s"',
+    ExtendedLog => "$extlog_file MISC custom",
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
     },
-
-    Limit => {
-      SITE_SYMLINK => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -2568,29 +3628,28 @@ sub site_misc_symlink_failed_limit {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $src = 'site.conf';
-      my $dst = 'foo';
-
-      eval { $client->site('SYMLINK', $src, $dst) };
-      unless ($@) {
-        die("SITE SYMLINK succeeded unexpectedly");
-      }
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my ($resp_code, $resp_msg) = $client->site('RMDIR', 'foo');
 
       my $expected;
 
-      $expected = 550;
+      $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "$src: Operation not permitted";
+      $expected = "SITE RMDIR command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
-      if (-l $test_symlink) {
-        die("Symlink $test_symlink exists unexpectedly");
+      # Make sure that the test file is gone, along with all of the
+      # test dirs.
+      if (-f $test_file) {
+        die("File $test_file exists, should be deleted");
+      }
+
+      foreach my $test_dir (@$test_dirs) {
+        if (-d $test_dir) {
+          die("Directory $test_dir exists, should be deleted");
+        }
       }
     };
 
@@ -2616,6 +3675,34 @@ sub site_misc_symlink_failed_limit {
 
   $self->assert_child_ok($pid);
 
+  eval {
+    if (open(my $fh, "< $extlog_file")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# line: $line\n"
+        }
+
+        if ($line =~ /SITE RMDIR (\S+): 200/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($ok, "ExtendedLog did not contain expected log line");
+    } else {
+      die("Can't read $extlog_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
   if ($ex) {
     die($ex);
   }
@@ -2623,13 +3710,14 @@ sub site_misc_symlink_failed_limit {
   unlink($log_file);
 }
 
-sub site_misc_utime_failed_limit {
+sub site_misc_extlog_rmdir_resp_code_nonempty_dir {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/site.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/site.pid");
   my $scoreboard_file = File::Spec->rel2abs("$tmpdir/site.scoreboard");
+  my $extlog_file = File::Spec->rel2abs("$tmpdir/ext.log");
 
   my $log_file = File::Spec->rel2abs('tests.log');
 
@@ -2638,10 +3726,32 @@ sub site_misc_utime_failed_limit {
 
   my $user = 'proftpd';
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar/baz");
+  mkpath($sub_dir);
+
+  my $test_dirs = [
+    File::Spec->rel2abs("$tmpdir/foo"),
+    File::Spec->rel2abs("$tmpdir/foo/bar"),
+    $sub_dir,
+  ];
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/foo/bar/quxx.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Quzz\n";
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -2656,7 +3766,7 @@ sub site_misc_utime_failed_limit {
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
-  auth_group_write($auth_group_file, 'ftpd', $gid, $user);
+  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $config = {
     PidFile => $pid_file,
@@ -2666,17 +3776,14 @@ sub site_misc_utime_failed_limit {
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    LogFormat => 'custom "%r: %s"',
+    ExtendedLog => "$extlog_file ALL custom",
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
     },
-
-    Limit => {
-      SITE_UTIME => {
-        DenyAll => '',
-      },
-    },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
@@ -2699,26 +3806,29 @@ sub site_misc_utime_failed_limit {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $timestamp = '200002240826';
-      my $path = 'site.conf';
-
-      eval { $client->site('UTIME', $timestamp, $path) };
-      unless ($@) {
-        die("SITE UTIME succeeded unexpectedly");
-      }
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      my ($resp_code, $resp_msg) = $client->site('RMDIR', 'foo');
 
       my $expected;
 
-      $expected = 550;
+      $expected = 200;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
-      $expected = "UTIME $timestamp $path: Operation not permitted";
+      $expected = "SITE RMDIR command successful";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Make sure that the test file is gone, along with all of the
+      # test dirs.
+      if (-f $test_file) {
+        die("File $test_file exists, should be deleted");
+      }
+
+      foreach my $test_dir (@$test_dirs) {
+        if (-d $test_dir) {
+          die("Directory $test_dir exists, should be deleted");
+        }
+      }
     };
 
     if ($@) {
@@ -2743,6 +3853,49 @@ sub site_misc_utime_failed_limit {
 
   $self->assert_child_ok($pid);
 
+  eval {
+    if (open(my $fh, "< $extlog_file")) {
+      my $dele_ok = 0;
+      my $rmd_ok = 0;
+      my $site_ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# line: $line\n"
+        }
+
+        if ($line =~ /DELE .*?250$/) {
+          $dele_ok = 1;
+          next;
+        }
+
+        if ($line =~ /RMD .*?257$/) {
+          $rmd_ok = 1;
+          next;
+        }
+
+        if ($line =~ /SITE RMDIR (\S+): 200$/) {
+          $site_ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($dele_ok, "ExtendedLog did not contain expected log line for DELE");
+      $self->assert($rmd_ok, "ExtendedLog did not contain expected log line for RMD");
+      $self->assert($site_ok, "ExtendedLog did not contain expected log line for SITE RMDIR");
+
+    } else {
+      die("Can't read $extlog_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
   if ($ex) {
     die($ex);
   }
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_snmp.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_snmp.pm
index e17d31f..4de2130 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_snmp.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_snmp.pm
@@ -9,7 +9,7 @@ use File::Spec;
 use IO::Handle;
 
 use ProFTPD::TestSuite::FTP;
-use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+use ProFTPD::TestSuite::Utils qw(:auth :config :features :running :test :testsuite);
 
 $| = 1;
 
@@ -3821,56 +3821,27 @@ sub snmp_v1_get_next_missing_instance_id {
 sub snmp_v1_get_next_end_of_mib_view {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/snmp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/snmp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/snmp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/snmp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/snmp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'snmp');
 
   my $table_dir = File::Spec->rel2abs("$tmpdir/var/snmp");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $table_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $table_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $agent_port = ProFTPD::TestSuite::Utils::get_high_numbered_port();
   my $snmp_community = "public";
 
-  my $request_oid = '1.3.6.1.4.1.17852.2.2.4.5.0';
+  # Deliberately request an OID (within the proftpd.snmpModulen.snmp arc)
+  # which does NOT exist.
+  my $request_oid = '1.3.6.1.4.1.17852.2.2.4.6.0';
   my $timeout = 30;
 
   my $config = {
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'snmp:20 snmp.asn1:20 snmp.db:20 snmp.msg:20 snmp.pdu:20 snmp.smi:20',
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -3881,13 +3852,14 @@ sub snmp_v1_get_next_end_of_mib_view {
         SNMPAgent => "master 127.0.0.1:$agent_port",
         SNMPCommunity => $snmp_community,
         SNMPEngine => 'on',
-        SNMPLog => $log_file,
+        SNMPLog => $setup->{log_file},
         SNMPTables => $table_dir,
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -3906,6 +3878,9 @@ sub snmp_v1_get_next_end_of_mib_view {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      # Allow server to start up
+      sleep(1);
+
       my ($snmp_sess, $snmp_err) = Net::SNMP->session(
         -hostname => '127.0.0.1',
         -port => $agent_port,
@@ -3952,7 +3927,7 @@ sub snmp_v1_get_next_end_of_mib_view {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, $timeout) };
+    eval { server_wait($setup->{config_file}, $rfh, $timeout) };
     if ($@) {
       warn($@);
       exit 1;
@@ -3962,18 +3937,10 @@ sub snmp_v1_get_next_end_of_mib_view {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub snmp_v1_get_next_multi {
@@ -4960,55 +4927,37 @@ sub snmp_v2_get_missing_instance_id {
 sub snmp_v2_get_next_end_of_mib_view {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/snmp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/snmp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/snmp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/snmp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/snmp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'snmp');
 
   my $table_dir = File::Spec->rel2abs("$tmpdir/var/snmp");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $table_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $table_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $agent_port = ProFTPD::TestSuite::Utils::get_high_numbered_port();
   my $snmp_community = "public";
 
+  # The "last" OID in the entire arc depends on the modules loaded.
   my $request_oid = '1.3.6.1.4.1.17852.2.2.4.5.0';
+  if (feature_have_module_compiled('mod_tls.c')) {
+    # End of the proftpd.snmpModule.ftps arc
+    $request_oid = '1.3.6.1.4.1.17852.2.2.5.3.11.0';
+  }
+  if (feature_have_module_compiled('mod_sftp.c')) {
+    # End of the proftpd.snmpModule.scp arc
+    $request_oid = '1.3.6.1.4.1.17852.2.2.8.2.8.0';
+  }
+  if (feature_have_module_compiled('mod_ban.c')) {
+    # End of the proftpd.snmpModule.ban arc
+    $request_oid = '1.3.6.1.4.1.17852.2.2.9.2.8.0';
+  }
 
   my $config = {
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'snmp:20 snmp.asn1:20 snmp.db:20 snmp.msg:20 snmp.pdu:20 snmp.smi:20',
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -5019,13 +4968,14 @@ sub snmp_v2_get_next_end_of_mib_view {
         SNMPAgent => "master 127.0.0.1:$agent_port",
         SNMPCommunity => $snmp_community,
         SNMPEngine => 'on',
-        SNMPLog => $log_file,
+        SNMPLog => $setup->{log_file},
         SNMPTables => $table_dir,
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -5044,6 +4994,9 @@ sub snmp_v2_get_next_end_of_mib_view {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      # Allow server to start up
+      sleep(1);
+
       my ($snmp_sess, $snmp_err) = Net::SNMP->session(
         -hostname => '127.0.0.1',
         -port => $agent_port,
@@ -5100,7 +5053,7 @@ sub snmp_v2_get_next_end_of_mib_view {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -5110,18 +5063,10 @@ sub snmp_v2_get_next_end_of_mib_view {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub snmp_v2_get_bulk {
@@ -5472,44 +5417,14 @@ sub snmp_v2_get_bulk_max_repetitions_only {
 sub snmp_v2_get_bulk_end_of_mib_view {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/snmp.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/snmp.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/snmp.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/snmp.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/snmp.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'snmp');
 
   my $table_dir = File::Spec->rel2abs("$tmpdir/var/snmp");
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $table_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $table_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $agent_port = ProFTPD::TestSuite::Utils::get_high_numbered_port();
   my $snmp_community = "public";
 
+  # These OIDs to request depend on the modules loaded.
   my $request_oid = '1.3.6.1.4.1.17852.2.2.4.3.0';
 
   my $next_oids = {
@@ -5518,15 +5433,42 @@ sub snmp_v2_get_bulk_end_of_mib_view {
     '1.3.6.1.4.1.17852.2.2.4.5.0 ' => 'endOfMibView',
   };
 
+  if (feature_have_module_compiled('mod_tls.c')) {
+    $request_oid = '1.3.6.1.4.1.17852.2.2.5.3.9.0';
+    $next_oids = {
+      '1.3.6.1.4.1.17852.2.2.5.3.10.0' => '\d+',
+      '1.3.6.1.4.1.17852.2.2.5.3.11.0' => '\d+',
+      '1.3.6.1.4.1.17852.2.2.5.3.11.0 ' => 'endOfMibView',
+    };
+  }
+
+  if (feature_have_module_compiled('mod_sftp.c')) {
+    $request_oid = '1.3.6.1.4.1.17852.2.2.8.2.6.0';
+    $next_oids = {
+      '1.3.6.1.4.1.17852.2.2.8.2.7.0' => '\d+',
+      '1.3.6.1.4.1.17852.2.2.8.2.8.0' => '\d+',
+      '1.3.6.1.4.1.17852.2.2.8.2.8.0 ' => 'endOfMibView',
+    };
+  }
+
+  if (feature_have_module_compiled('mod_ban.c')) {
+    $request_oid = '1.3.6.1.4.1.17852.2.2.9.2.6.0';
+    $next_oids = {
+      '1.3.6.1.4.1.17852.2.2.9.2.7.0' => '\d+',
+      '1.3.6.1.4.1.17852.2.2.9.2.8.0' => '\d+',
+      '1.3.6.1.4.1.17852.2.2.9.2.8.0 ' => 'endOfMibView',
+    };
+  }
+
   my $config = {
-    TraceLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
     Trace => 'snmp:20 snmp.asn1:20 snmp.db:20 snmp.msg:20 snmp.pdu:20 snmp.smi:20',
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -5537,13 +5479,14 @@ sub snmp_v2_get_bulk_end_of_mib_view {
         SNMPAgent => "master 127.0.0.1:$agent_port",
         SNMPCommunity => $snmp_community,
         SNMPEngine => 'on',
-        SNMPLog => $log_file,
+        SNMPLog => $setup->{log_file},
         SNMPTables => $table_dir,
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -5562,6 +5505,9 @@ sub snmp_v2_get_bulk_end_of_mib_view {
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      # Allow the server to start up
+      sleep(1);
+
       my ($snmp_sess, $snmp_err) = Net::SNMP->session(
         -hostname => '127.0.0.1',
         -port => $agent_port,
@@ -5591,6 +5537,11 @@ sub snmp_v2_get_bulk_end_of_mib_view {
         die("No SNMP response received: " . $snmp_sess->error());
       }
 
+      if ($ENV{TEST_VERBOSE}) {
+        use Data::Dumper;
+        print STDERR "# SNMP response: ", Dumper($snmp_resp), "\n";
+      }
+
       foreach my $next_oid (keys(%$next_oids)) {
         # Do we have the next OID of the requested OID in the response?
         unless (defined($snmp_resp->{$next_oid})) {
@@ -5621,7 +5572,7 @@ sub snmp_v2_get_bulk_end_of_mib_view {
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -5631,18 +5582,10 @@ sub snmp_v2_get_bulk_end_of_mib_view {
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub snmp_v2_set_no_access {
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm
index cb636eb..ca591f5 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_passwd.pm
@@ -15,6 +15,11 @@ $| = 1;
 my $order = 0;
 
 my $TESTS = {
+  sql_passwd_host => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   sql_passwd_md5_base64 => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -165,6 +170,111 @@ my $TESTS = {
     test_class => [qw(forking bug)],
   },
 
+  sql_passwd_salt_file_with_user_salt => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  sql_passwd_user_salt_sql_base64_bug4138 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_passwd_user_salt_sql_hex_lc_bug4138 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_passwd_user_salt_sql_hex_uc_bug4138 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_passwd_salt_file_base64_bug4138 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_passwd_salt_file_hex_lc_bug4138 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_passwd_salt_file_hex_uc_bug4138 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_passwd_scrypt_base64_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_scrypt_hex_lc_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_scrypt_hex_uc_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_scrypt_base64_sensitive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_scrypt_hex_lc_sensitive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_scrypt_hex_uc_sensitive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_scrypt_hex_uc_sensitive_cost_len_64 => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_base64_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_hex_lc_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_hex_uc_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_base64_interactive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_hex_lc_sensitive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_hex_uc_sensitive_cost => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
+  sql_passwd_argon2_hex_uc_sensitive_cost_len_64 => {
+    order => ++$order,
+    test_class => [qw(feature_sodium forking)],
+  },
+
 };
 
 sub new {
@@ -175,18 +285,180 @@ sub list_tests {
   return testsuite_get_runnable_tests($TESTS);
 }
 
-sub sql_passwd_md5_base64 {
+sub sql_passwd_host {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
-  my $config_file = "$tmpdir/sqlpasswd.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+  # I used:
+  #
+  #  `/bin/echo -n "test" | openssl dgst -binary -md5 | openssl enc -base64`
+  #
+  # to generate this password.
+  my $passwd = 'CY9rzUYh03PK3k6DJie09g==';
 
-  my $log_file = test_get_logfile();
+  my $setup = test_setup($tmpdir, 'sql_passwd', undef, $passwd);
 
-  my $user = 'proftpd';
-  my $group = 'ftpd';
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$setup->{user}', '$passwd', $setup->{uid}, $setup->{gid}, '$setup->{home_dir}', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$setup->{group}', $setup->{gid}, '$setup->{user}');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'off',
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $host = 'localhost';
+
+  if (open(my $fh, ">> $setup->{config_file}")) {
+    print $fh <<EOC;
+# This virtual host is name-based
+<VirtualHost 127.0.0.1>
+  Port $port
+  ServerAlias $host
+  ServerName "SQL Passwd Server"
+
+  <IfModule mod_delay.c>
+    DelayEngine off
+  </IfModule>
+
+  <IfModule mod_sql.c>
+    SQLAuthTypes md5
+    SQLBackend sqlite3
+    SQLConnectInfo $db_file
+    SQLLogFile $setup->{log_file}
+  </IfModule>
+
+  <IfModule mod_sql_passwd.c>
+    SQLPasswordEngine on
+    SQLPasswordEncoding base64
+  </IfModule>
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $setup->{config_file}: $!");
+    }
+
+  } else {
+    die("Can't open $setup->{config_file}: $!");
+  }
+
+  # 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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->host($host);
+      $client->login($setup->{user}, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs"));
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+    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 sql_passwd_md5_base64 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
 
   # I used:
   #
@@ -195,9 +467,7 @@ sub sql_passwd_md5_base64 {
   # to generate this password.
   my $passwd = 'CY9rzUYh03PK3k6DJie09g==';
 
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+  my $setup = test_setup($tmpdir, 'sql_passwd', undef, $passwd);
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -214,14 +484,14 @@ CREATE TABLE users (
   homedir TEXT, 
   shell TEXT
 );
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$setup->{user}', '$passwd', $setup->{uid}, $setup->{gid}, '$setup->{home_dir}', '/bin/bash');
 
 CREATE TABLE groups (
   groupname TEXT,
   gid INTEGER,
   members TEXT
 );
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+INSERT INTO groups (groupname, gid, members) VALUES ('$setup->{group}', $setup->{gid}, '$setup->{user}');
 EOS
 
     unless (close($fh)) {
@@ -245,9 +515,9 @@ EOS
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -258,7 +528,7 @@ EOS
         SQLAuthTypes => 'md5',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
+        SQLLogFile => $setup->{log_file},
       },
 
       'mod_sql_passwd.c' => {
@@ -268,7 +538,8 @@ EOS
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -285,24 +556,21 @@ EOS
   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($user, "test");
+      sleep(1);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, "test");
 
       my $resp_msgs = $client->response_msgs();
       my $nmsgs = scalar(@$resp_msgs);
 
-      my $expected;
-
-      $expected = 1;
+      my $expected = 1;
       $self->assert($expected == $nmsgs,
         test_msg("Expected $expected, got $nmsgs")); 
 
-      $expected = "User proftpd logged in";
+      $expected = "User $setup->{user} logged in";
       $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
-
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
     };
-
     if ($@) {
       $ex = $@;
     }
@@ -311,7 +579,7 @@ EOS
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -321,18 +589,10 @@ EOS
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 sub sql_passwd_md5_hex_lc {
@@ -5459,4 +5719,3709 @@ EOS
   unlink($log_file);
 }
 
+sub sql_passwd_salt_file_with_user_salt {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $global_salt = '8Hkqr7bnPaZ52j81VvuoWdOEuq6EeXwpiIw5Q679xzvEqwe128';
+  my $user_salt = 'MyS00p3r$3kr3t$@lt';
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex($user_salt . (lc("password")) . $global_salt);
+  #
+  # to generate this password.
+  my $passwd = 'b939d5c8e2857d9e8d27b87939a27fb986cd41ef';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE user_salts (
+  userid TEXT,
+  salt TEXT
+);
+INSERT INTO user_salts (userid, salt) VALUES ('$user', '$user_salt');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $salt_file = File::Spec->rel2abs("$home_dir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh "$global_salt\n";
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLMinID => '100',
+        SQLNamedQuery => 'get-user-salt SELECT "salt FROM user_salts WHERE userid = \'%{0}\'"',
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltFile => "$salt_file Append",
+        SQLPasswordUserSalt => "sql:/get-user-salt Prepend",
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_user_salt_sql_base64_bug4138 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+
+  # This base64-encoded salt data has embedded NULs
+  my $salt = 'p8CCsyWyyDX/infYBQBolKobDsQlWca9LLgteqD+rSo=';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex((lc("password")) . $decoded_salt);
+  #
+  # to generate this password.
+  my $passwd = '39ff37588e1a56b243b00cb6479a716ef50fc980';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('ftpd', $gid, '$user');
+
+CREATE TABLE user_salts (
+  userid TEXT,
+  salt TEXT
+);
+INSERT INTO user_salts (userid, salt) VALUES ('$user', '$salt');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'get-user-salt SELECT "salt FROM user_salts WHERE userid = \'%{0}\'"',
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'base64',
+        SQLPasswordUserSalt => 'sql:/get-user-salt',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_user_salt_sql_hex_lc_bug4138 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'a7c082b325b2c835ff8a77d805006894aa1b0ec42559c6bd2cb82d7aa0fead2a';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex((lc("password")) . $decoded_salt);
+  #
+  # to generate this password.
+  my $passwd = '39ff37588e1a56b243b00cb6479a716ef50fc980';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('ftpd', $gid, '$user');
+
+CREATE TABLE user_salts (
+  userid TEXT,
+  salt TEXT
+);
+INSERT INTO user_salts (userid, salt) VALUES ('$user', '$salt');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'get-user-salt SELECT "salt FROM user_salts WHERE userid = \'%{0}\'"',
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordUserSalt => 'sql:/get-user-salt',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_user_salt_sql_hex_uc_bug4138 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex((lc("password")) . $decoded_salt);
+  #
+  # to generate this password.
+  my $passwd = '39ff37588e1a56b243b00cb6479a716ef50fc980';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('ftpd', $gid, '$user');
+
+CREATE TABLE user_salts (
+  userid TEXT,
+  salt TEXT
+);
+INSERT INTO user_salts (userid, salt) VALUES ('$user', '$salt');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'get-user-salt SELECT "salt FROM user_salts WHERE userid = \'%{0}\'"',
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'HEX',
+        SQLPasswordUserSalt => 'sql:/get-user-salt',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_salt_file_base64_bug4138 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  # This base64-encoded salt data has embedded NULs
+  my $salt = 'p8CCsyWyyDX/infYBQBolKobDsQlWca9LLgteqD+rSo=';
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex((lc("password")) . $decoded_salt);
+  #
+  # to generate this password.
+  my $passwd = '39ff37588e1a56b243b00cb6479a716ef50fc980';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $salt_file = File::Spec->rel2abs("$home_dir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'base64',
+        SQLPasswordSaltFile => $salt_file,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_salt_file_hex_lc_bug4138 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'a7c082b325b2c835ff8a77d805006894aa1b0ec42559c6bd2cb82d7aa0fead2a';
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex((lc("password")) . $decoded_salt);
+  #
+  # to generate this password.
+  my $passwd = '39ff37588e1a56b243b00cb6479a716ef50fc980';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $salt_file = File::Spec->rel2abs("$home_dir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_salt_file_hex_uc_bug4138 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A';
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I used:
+  #
+  #  Digest::SHA1::sha1_hex((lc("password")) . $decoded_salt);
+  #
+  # to generate this password.
+  my $passwd = '39ff37588e1a56b243b00cb6479a716ef50fc980';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  my $salt_file = File::Spec->rel2abs("$home_dir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'sha1',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'HEX',
+        SQLPasswordSaltFile => $salt_file,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "password");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_base64_interactive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = 'ZI+02v+UMvtIvFBALSHyBiNu9At1h+HgN6xzI7nhAtM=';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'base64',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Interactive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_hex_lc_interactive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = '648fb4daff9432fb48bc50402d21f206236ef40b7587e1e037ac7323b9e102d3';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Interactive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_hex_uc_interactive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = uc('648fb4daff9432fb48bc50402d21f206236ef40b7587e1e037ac7323b9e102d3');
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'HEX',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Interactive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_base64_sensitive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = 'AxoE22pqKy/Vxa30TMz3D71E0LTQ45abmtVlXOyP2hA=';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'base64',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_hex_lc_sensitive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = '031a04db6a6a2b2fd5c5adf44cccf70fbd44d0b4d0e3969b9ad5655cec8fda10';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_hex_uc_sensitive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = uc('031a04db6a6a2b2fd5c5adf44cccf70fbd44d0b4d0e3969b9ad5655cec8fda10');
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'HEX',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_scrypt_hex_uc_sensitive_cost_len_64 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  my $scrypt_hash_len = 64;
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = uc('031a04db6a6a2b2fd5c5adf44cccf70fbd44d0b4d0e3969b9ad5655cec8fda10ae28c6b433df6575876c10212a0b9c26cddef82294a223fb72d6a2fc4aa173e0');
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894AA1B0EC42559C6BD2CB82D7AA0FEAD2A');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'scrypt',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'HEX',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+        SQLPasswordScrypt => $scrypt_hash_len,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_base64_interactive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = 'CMEEQfxzMwu2y7YuG3R1r5n/qC0g24DXPRLb3ZpI6ZU=';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'A7C082B325B2C835FF8A77D805006894';
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'base64',
+        SQLPasswordSaltEncoding => 'HEX',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Interactive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_hex_lc_interactive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = '08c10441fc73330bb6cbb62e1b7475af99ffa82d20db80d73d12dbdd9a48e995';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Interactive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_hex_uc_interactive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = uc('08c10441fc73330bb6cbb62e1b7475af99ffa82d20db80d73d12dbdd9a48e995');
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'HEX',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Interactive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_base64_sensitive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = '6dZPscXkBbvrcU7FcLTuhKMqpjLlHItzRkLuic95h0k=';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'A7C082B325B2C835FF8A77D805006894';
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'base64',
+        SQLPasswordSaltEncoding => 'HEX',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_hex_lc_sensitive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = 'e9d64fb1c5e405bbeb714ec570b4ee84a32aa632e51c8b734642ee89cf798749';
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = lc('A7C082B325B2C835FF8A77D805006894');
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'hex',
+        SQLPasswordSaltEncoding => 'hex',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_hex_uc_sensitive_cost {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = uc('e9d64fb1c5e405bbeb714ec570b4ee84a32aa632e51c8b734642ee89cf798749');
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'A7C082B325B2C835FF8A77D805006894';
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'HEX',
+        SQLPasswordSaltEncoding => 'HEX',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_passwd_argon2_hex_uc_sensitive_cost_len_64 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlpasswd.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $group = 'ftpd';
+
+  my $argon2_hash_len = 64;
+
+  # I had to look at the mod_sql_passwd-generated logs to get this password;
+  # this means it's a bit incestuous and thus suspect.
+  my $passwd = uc('c7d9ca94cc8631322c30b8c39d8b9ba3350873a81acea38771e5ede737625a99bc904b33c8fd53cb314147cba409fe21fccf9f6bbb04f775f01076d2188a7dae');
+
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # This hex-encoded salt data has embedded NULs
+  my $salt = 'A7C082B325B2C835FF8A77D805006894';
+  my $salt_file = File::Spec->rel2abs("$tmpdir/sqlpasswd.salt");
+  if (open(my $fh, "> $salt_file")) {
+    binmode($fh);
+    print $fh $salt;
+
+    unless (close($fh)) {
+      die("Can't write $salt_file: $!");
+    }
+
+  } else {
+    die("Can't open $salt_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql.passwd:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLAuthTypes => 'argon2',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
+
+      'mod_sql_passwd.c' => {
+        SQLPasswordEngine => 'on',
+        SQLPasswordEncoding => 'HEX',
+        SQLPasswordSaltEncoding => 'HEX',
+        SQLPasswordSaltFile => $salt_file,
+        SQLPasswordCost => 'Sensitive',
+        SQLPasswordArgon2 => $argon2_hash_len,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, "test");
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected response message count $expected, got $nmsgs"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response message '$expected', got '$resp_msgs->[0]'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm
index 9df9279..3045398 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_sql_sqlite.pm
@@ -82,6 +82,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  sql_custom_user_info_null_ids => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   sql_userset_bug2434 => {
     order => ++$order,
     test_class => [qw(bug forking rootprivs)],
@@ -152,11 +157,26 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  sql_sqllog_var_transfer_millisecs_retr_bug4218 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_sqllog_var_R_retr_bug4218 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   sql_sqllog_exit => {
     order => ++$order,
     test_class => [qw(forking)],
   },
 
+  sql_sqllog_exit_var_remote_port_bug4296 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   sql_sqllog_var_d_bug3395 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -187,6 +207,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  sql_sqllog_pass_failed_bug4301 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   sql_sqlshowinfo_pass_bug3423 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -312,6 +337,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  sql_sqllog_var_xfer_status_filtered => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
   sql_sqllog_var_xfer_failure_none => {
     order => ++$order,
     test_class => [qw(forking)],
@@ -392,6 +422,26 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  sql_user_info_null_uid_gid_columns => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  sql_64_bit_uid_bug4164 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  sql_sqllog_var_remote_port => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  sql_userowner_issue346 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
 };
 
 sub new {
@@ -2425,6 +2475,177 @@ EOS
   unlink($log_file);
 }
 
+sub sql_custom_user_info_null_ids {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    # Note: by inserting/using NULL for the uid and gid columns, we will
+    # force mod_sql to use the SQLDefaultUID/SQLDefaultGID values (rather than
+    # e.g. segfaulting).
+
+    print $fh <<EOS;
+CREATE TABLE ftpusers (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO ftpusers (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', NULL, NULL, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => [
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLNamedQuery get-user-by-name SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers WHERE userid = \'%U\'"',
+        'SQLNamedQuery get-user-by-id SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers WHERE uid = %{0}"',
+         'SQLNamedQuery get-user-names SELECT "userid FROM ftpusers"',
+         'SQLNamedQuery get-all-users SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers"',
+        'SQLUserInfo custom:/get-user-by-name/get-user-by-id/get-user-names/get-all-users',
+      ],
+    },
+
+
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
 sub sql_userset_bug2434 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -3388,7 +3609,6 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
       $client->rnfr('test.txt');
       $client->rnto('foo.txt');
@@ -3435,6 +3655,11 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $src_file = '/private' . $src_file;
+  }
+
   $expected = $src_file;
   $self->assert($expected eq $rnfr_path,
     test_msg("Expected '$expected', got '$rnfr_path'"));
@@ -4604,6 +4829,11 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $src_file = '/private' . $src_file;
+  }
+
   $expected = $src_file;
   $self->assert($expected eq $rnfr_path,
     test_msg("Expected '$expected', got '$rnfr_path'"));
@@ -4618,38 +4848,7 @@ EOS
 sub sql_sqllog_var_T_retr {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sqlite.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'sqlite');
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -4659,7 +4858,7 @@ sub sql_sqllog_var_T_retr {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
-  user TEXT,
+  user TEXT PRIMARY KEY,
   ip_addr TEXT,
   download_time FLOAT
 );
@@ -4685,12 +4884,12 @@ EOS
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -4701,14 +4900,15 @@ EOS
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
+        SQLLogFile => $setup->{log_file},
         SQLNamedQuery => 'download FREEFORM "INSERT INTO ftpsessions (user, ip_addr, download_time) VALUES (\'%u\', \'%L\', %T)"',
         SQLLog => 'RETR download',
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -4725,9 +4925,8 @@ EOS
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
 
       my $conn = $client->retr_raw('sqlite.conf');
       unless ($conn) {
@@ -4741,17 +4940,8 @@ EOS
       $conn->close();
 
       my $resp_code = $client->response_code();
-      my $resp_mesg = $client->response_msg();
-
-      my $expected;
-
-      $expected = 226;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
-
-      $expected = 'Transfer complete';
-      $self->assert($expected eq $resp_mesg,
-        test_msg("Expected '$expected', got '$resp_mesg'"));
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
 
       $client->quit();
     };
@@ -4764,7 +4954,7 @@ EOS
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -4774,80 +4964,50 @@ EOS
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
+  eval {
+    my $query = "SELECT user, ip_addr, download_time FROM ftpsessions WHERE user = \'$setup->{user}\'";
+    $cmd = "sqlite3 $db_file \"$query\"";
 
-  my $query = "SELECT user, ip_addr, download_time FROM ftpsessions WHERE user = \'$user\'";
-  $cmd = "sqlite3 $db_file \"$query\"";
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "Executing sqlite3: $cmd\n";
+    }
 
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
+    my $res = join('', `$cmd`);
+    chomp($res);
 
-  my $res = join('', `$cmd`);
-  chomp($res);
+    my ($login, $ip_addr, $download_time) = split(/\|/, $res);
 
-  my ($login, $ip_addr, $download_time) = split(/\|/, $res);
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "# login = '$login', ip_addr = '$ip_addr', download_time = $download_time\n";
+    }
 
-  my $expected;
+    my $expected;
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+    $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected '$expected', got '$login'"));
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $self->assert($download_time > 0.0,
-    test_msg("Expected > 0.0, got $download_time"));
+    $self->assert($download_time > 0.0,
+      test_msg("Expected > 0.0, got $download_time"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
 
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sql_sqllog_exit {
+sub sql_sqllog_var_transfer_millisecs_retr_bug4218 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sqlite.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'sqlite');
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -4857,8 +5017,9 @@ sub sql_sqllog_exit {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
-  user TEXT,
-  ip_addr TEXT
+  user TEXT PRIMARY KEY,
+  ip_addr TEXT,
+  download_ms INTEGER
 );
 EOS
 
@@ -4882,12 +5043,12 @@ EOS
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -4898,14 +5059,15 @@ EOS
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr) VALUES (\'%u\', \'%L\')"',
-        SQLLog => 'EXIT logout',
+        SQLLogFile => $setup->{log_file},
+        SQLNamedQuery => 'download FREEFORM "INSERT INTO ftpsessions (user, ip_addr, download_ms) VALUES (\'%u\', \'%L\', %{transfer-millisecs})"',
+        SQLLog => 'RETR download',
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -4922,9 +5084,24 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw('sqlite.conf');
+      unless ($conn) {
+        die("RETR sqlite.conf failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      while ($conn->read($buf, 8192, 30)) {
+      }
+      $conn->close();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
 
-      $client->login($user, $passwd);
       $client->quit();
     };
 
@@ -4936,7 +5113,7 @@ EOS
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -4946,102 +5123,50 @@ EOS
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  my $query = "SELECT user, ip_addr FROM ftpsessions WHERE user = \'$user\'";
-  $cmd = "sqlite3 $db_file \"$query\"";
-
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
-
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  my ($login, $ip_addr) = split(/\|/, $res);
+  eval {
+    my $query = "SELECT user, ip_addr, download_ms FROM ftpsessions WHERE user = \'$setup->{user}\'";
+    $cmd = "sqlite3 $db_file \"$query\"";
 
-  my $expected;
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "Executing sqlite3: $cmd\n";
+    }
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+    my $res = join('', `$cmd`);
+    chomp($res);
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+    my ($login, $ip_addr, $download_ms) = split(/\|/, $res);
 
-  unlink($log_file);
-}
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "# login = '$login', ip_addr = '$ip_addr', download_ms = $download_ms\n";
+    }
 
-sub get_locations {
-  my $db_file = shift;
-  my $where = shift;
+    my $expected;
 
-  my $sql = "SELECT user, ip_addr, dir FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
-  }
+    $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected '$expected', got '$login'"));
 
-  my $cmd = "sqlite3 $db_file \"$sql\"";
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected '$expected', got '$ip_addr'"));
 
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
+    $self->assert($download_ms > 0,
+      test_msg("Expected > 0, got $download_ms"));
+  };
+  if ($@) {
+    $ex = $@;
   }
 
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
-sub sql_sqllog_var_d_bug3395 {
+sub sql_sqllog_var_R_retr_bug4218 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
-
-  my $config_file = "$tmpdir/sqlite.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $setup = test_setup($tmpdir, 'sqlite');
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -5051,9 +5176,9 @@ sub sql_sqllog_var_d_bug3395 {
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
-  user TEXT,
+  user TEXT PRIMARY KEY,
   ip_addr TEXT,
-  dir TEXT
+  response_ms INTEGER
 );
 EOS
 
@@ -5077,12 +5202,12 @@ EOS
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -5093,14 +5218,15 @@ EOS
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'location FREEFORM "INSERT INTO ftpsessions (user, ip_addr, dir) VALUES (\'%u\', \'%L\', \'%d\')"',
-        SQLLog => 'EXIT location',
+        SQLLogFile => $setup->{log_file},
+        SQLNamedQuery => 'download FREEFORM "INSERT INTO ftpsessions (user, ip_addr, response_ms) VALUES (\'%u\', \'%L\', %R)"',
+        SQLLog => 'RETR download',
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -5117,9 +5243,24 @@ EOS
   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($user, $passwd);
-      $client->cwd('foo');
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1);
+      $client->login($setup->{user}, $setup->{passwd});
+
+      my $conn = $client->retr_raw('sqlite.conf');
+      unless ($conn) {
+        die("RETR sqlite.conf failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      while ($conn->read($buf, 8192, 30)) {
+      }
+      $conn->close();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       $client->quit();
     };
 
@@ -5131,7 +5272,7 @@ EOS
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -5141,39 +5282,49 @@ EOS
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
+  eval {
+    my $query = "SELECT user, ip_addr, response_ms FROM ftpsessions WHERE user = \'$setup->{user}\'";
+    $cmd = "sqlite3 $db_file \"$query\"";
 
-  my ($login, $ip_addr, $dir) = get_locations($db_file, "user = \'$user\'");
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "Executing sqlite3: $cmd\n";
+    }
 
-  my $expected;
+    my $res = join('', `$cmd`);
+    chomp($res);
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+    my ($login, $ip_addr, $response_ms) = split(/\|/, $res);
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+    if ($ENV{TEST_VERBOSE}) {
+      print STDERR "# login = '$login', ip_addr = '$ip_addr', response_ms = $response_ms\n";
+    }
 
-  $expected = $sub_dir;
-  $self->assert($expected eq $dir,
-    test_msg("Expected '$expected', got '$dir'"));
+    my $expected;
 
-  unlink($log_file);
-}
+    $expected = $setup->{user};
+    $self->assert($expected eq $login,
+      test_msg("Expected '$expected', got '$login'"));
 
-sub sql_sqllog_var_d_chroot_bug3395 {
-  my $self = shift;
-  my $tmpdir = $self->{tmpdir};
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected '$expected', got '$ip_addr'"));
+
+    $self->assert($response_ms > 0,
+      test_msg("Expected > 0, got $response_ms"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub sql_sqllog_exit {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
 
   my $config_file = "$tmpdir/sqlite.conf";
   my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
@@ -5191,8 +5342,17 @@ sub sql_sqllog_var_d_chroot_bug3395 {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
@@ -5207,8 +5367,7 @@ sub sql_sqllog_var_d_chroot_bug3395 {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT,
-  dir TEXT
+  ip_addr TEXT
 );
 EOS
 
@@ -5223,17 +5382,9 @@ EOS
   my $cmd = "sqlite3 $db_file < $db_script";
   build_db($cmd, $db_script);
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-
     unless (chmod(0666, $db_file)) {
       die("Can't set perms on $db_file to 0666: $!");
     }
@@ -5255,10 +5406,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
+        SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'location FREEFORM "INSERT INTO ftpsessions (user, ip_addr, dir) VALUES (\'%u\', \'%L\', \'%d\')"',
-        SQLLog => 'EXIT location',
+        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr) VALUES (\'%u\', \'%L\')"',
+        SQLLog => 'EXIT logout',
       },
     },
   };
@@ -5281,8 +5432,8 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
       $client->login($user, $passwd);
-      $client->cwd('foo');
       $client->quit();
     };
 
@@ -5315,7 +5466,17 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $dir) = get_locations($db_file, "user = \'$user\'");
+  my $query = "SELECT user, ip_addr FROM ftpsessions WHERE user = \'$user\'";
+  $cmd = "sqlite3 $db_file \"$query\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  my ($login, $ip_addr) = split(/\|/, $res);
 
   my $expected;
 
@@ -5327,36 +5488,10 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = $sub_dir;
-  $self->assert($expected eq $dir,
-    test_msg("Expected '$expected', got '$dir'"));
-
   unlink($log_file);
 }
 
-sub get_ids {
-  my $db_file = shift;
-  my $where = shift;
-
-  my $sql = "SELECT user, ip_addr, uid, gid FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
-  }
-
-  my $cmd = "sqlite3 $db_file \"$sql\"";
-
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
-
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
-}
-
-sub sql_sqllog_var_uid_gid_bug3390 {
+sub sql_sqllog_exit_var_remote_port_bug4296 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -5376,17 +5511,14 @@ sub sql_sqllog_var_uid_gid_bug3390 {
   my $uid = 500;
   my $gid = 500;
 
-  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
-  mkpath($sub_dir);
-
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir, $sub_dir)) {
+    unless (chmod(0755, $home_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+    unless (chown($uid, $gid, $home_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -5405,8 +5537,7 @@ sub sql_sqllog_var_uid_gid_bug3390 {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  uid NUMBER,
-  gid NUMBER
+  port NUMBER
 );
 EOS
 
@@ -5445,10 +5576,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
+        SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'location FREEFORM "INSERT INTO ftpsessions (user, ip_addr, uid, gid) VALUES (\'%u\', \'%L\', %{uid}, %{gid})"',
-        SQLLog => 'PASS location',
+        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr, port) VALUES (\'%u\', \'%L\', %{remote-port})"',
+        SQLLog => 'EXIT logout',
       },
     },
   };
@@ -5472,7 +5603,6 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->cwd('foo');
       $client->quit();
     };
 
@@ -5505,7 +5635,18 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $sql_uid, $sql_gid) = get_ids($db_file, "user = \'$user\'");
+  my $query = "SELECT * FROM ftpsessions where user = '$user'";
+  $cmd = "sqlite3 $db_file \"$query\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $output = `$cmd`;
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  my ($login, $ip_addr, $remote_port) = split(/\|/, $res);
 
   my $expected;
 
@@ -5517,18 +5658,32 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = $uid;
-  $self->assert($expected == $sql_uid,
-    test_msg("Expected $expected, got $sql_uid"));
+  unlink($log_file);
+}
 
-  $expected = $gid;
-  $self->assert($expected == $sql_gid,
-    test_msg("Expected $expected, got $sql_gid"));
+sub get_locations {
+  my $db_file = shift;
+  my $where = shift;
 
-  unlink($log_file);
+  my $sql = "SELECT user, ip_addr, dir FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
 }
 
-sub sql_sqlite_auth_type_backend_bug3511 {
+sub sql_sqllog_var_d_bug3395 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -5538,6 +5693,9 @@ sub sql_sqlite_auth_type_backend_bug3511 {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -5545,6 +5703,25 @@ sub sql_sqlite_auth_type_backend_bug3511 {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -5552,23 +5729,11 @@ sub sql_sqlite_auth_type_backend_bug3511 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT,
-  lastdir TEXT
-);
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  dir TEXT
 );
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 EOS
 
     unless (close($fh)) {
@@ -5595,16 +5760,21 @@ EOS
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
       'mod_sql.c' => {
-        SQLAuthTypes => 'backend',
+        SQLEngine => 'log',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
+        SQLNamedQuery => 'location FREEFORM "INSERT INTO ftpsessions (user, ip_addr, dir) VALUES (\'%u\', \'%L\', \'%d\')"',
+        SQLLog => 'EXIT location',
       },
     },
   };
@@ -5627,25 +5797,9 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      eval { $client->login($user, $passwd) };
-      unless ($@) {
-        die("Login succeeded unexpectedly");
-      }
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
-      my $expected;
-
-      $expected = 530;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code")); 
-
-      $expected = "Login incorrect.";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
+      $client->login($user, $passwd);
+      $client->cwd('foo');
+      $client->quit();
     };
 
     if ($@) {
@@ -5677,10 +5831,31 @@ EOS
     die($ex);
   }
 
+  my ($login, $ip_addr, $dir) = get_locations($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $sub_dir = '/private' . $sub_dir;
+  }
+
+  $expected = $sub_dir;
+  $self->assert($expected eq $dir,
+    test_msg("Expected '$expected', got '$dir'"));
+
   unlink($log_file);
 }
 
-sub sql_sqllog_pass_ok_var_s_bug3528 {
+sub sql_sqllog_var_d_chroot_bug3395 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -5700,17 +5875,8 @@ sub sql_sqllog_pass_ok_var_s_bug3528 {
   my $uid = 500;
   my $gid = 500;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
 
   auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
     '/bin/bash');
@@ -5725,8 +5891,8 @@ sub sql_sqllog_pass_ok_var_s_bug3528 {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  resp_code TEXT,
-  resp_mesg TEXT
+  ip_addr TEXT,
+  dir TEXT
 );
 EOS
 
@@ -5741,12 +5907,20 @@ EOS
   my $cmd = "sqlite3 $db_file < $db_script";
   build_db($cmd, $db_script);
 
-  # Make sure that, if we're running as root, the database file has
-  # the permissions/privs set for use by proftpd
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0666, $db_file)) {
-      die("Can't set perms on $db_file to 0666: $!");
-    }
+    unless (chmod(0755, $home_dir, $sub_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
   }
 
   my $config = {
@@ -5765,10 +5939,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
+        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, resp_code, resp_mesg) VALUES (\'%u\', \'%s\', \'%S\')"',
-        SQLLog => 'PASS,ERR_PASS info',
+        SQLNamedQuery => 'location FREEFORM "INSERT INTO ftpsessions (user, ip_addr, dir) VALUES (\'%u\', \'%L\', \'%d\')"',
+        SQLLog => 'EXIT location',
       },
     },
   };
@@ -5792,6 +5966,7 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->cwd('foo');
       $client->quit();
     };
 
@@ -5824,23 +5999,48 @@ EOS
     die($ex);
   }
 
-  my ($login, $resp_code, $resp_mesg) = get_resp_mesgs($db_file,
-    undef, "user, resp_code, resp_mesg");
+  my ($login, $ip_addr, $dir) = get_locations($db_file, "user = \'$user\'");
 
   my $expected;
 
-  $expected = 230;
-  $self->assert($expected eq $resp_code,
-    test_msg("Expected '$expected', got '$resp_code'"));
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
 
-  $expected = "User $user logged in";
-  $self->assert($expected eq $resp_mesg,
-    test_msg("Expected '$expected', got '$resp_mesg'"));
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  $expected = $sub_dir;
+  $self->assert($expected eq $dir,
+    test_msg("Expected '$expected', got '$dir'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_pass_failed_var_s_bug3528 {
+sub get_ids {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, ip_addr, uid, gid FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub sql_sqllog_var_uid_gid_bug3390 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -5860,14 +6060,17 @@ sub sql_sqllog_pass_failed_var_s_bug3528 {
   my $uid = 500;
   my $gid = 500;
 
+  my $sub_dir = File::Spec->rel2abs("$tmpdir/foo");
+  mkpath($sub_dir);
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
+    unless (chmod(0755, $home_dir, $sub_dir)) {
       die("Can't set perms on $home_dir to 0755: $!");
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
+    unless (chown($uid, $gid, $home_dir, $sub_dir)) {
       die("Can't set owner of $home_dir to $uid/$gid: $!");
     }
   }
@@ -5885,8 +6088,9 @@ sub sql_sqllog_pass_failed_var_s_bug3528 {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  resp_code TEXT,
-  resp_mesg TEXT
+  ip_addr TEXT,
+  uid NUMBER,
+  gid NUMBER
 );
 EOS
 
@@ -5925,10 +6129,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
+        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, resp_code, resp_mesg) VALUES (\'%u\', \'%s\', \'%S\')"',
-        SQLLog => 'PASS,ERR_PASS info',
+        SQLNamedQuery => 'location FREEFORM "INSERT INTO ftpsessions (user, ip_addr, uid, gid) VALUES (\'%u\', \'%L\', %{uid}, %{gid})"',
+        SQLLog => 'PASS location',
       },
     },
   };
@@ -5951,11 +6155,8 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->login($user, 'foobar') };
-      unless ($@) {
-        die("Login succeeded unexpectedly");
-      }
-
+      $client->login($user, $passwd);
+      $client->cwd('foo');
       $client->quit();
     };
 
@@ -5988,23 +6189,30 @@ EOS
     die($ex);
   }
 
-  my ($login, $resp_code, $resp_mesg) = get_resp_mesgs($db_file,
-    undef, "user, resp_code, resp_mesg");
+  my ($login, $ip_addr, $sql_uid, $sql_gid) = get_ids($db_file, "user = \'$user\'");
 
   my $expected;
 
-  $expected = 530;
-  $self->assert($expected eq $resp_code,
-    test_msg("Expected '$expected', got '$resp_code'"));
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
 
-  $expected = "Login incorrect.";
-  $self->assert($expected eq $resp_mesg,
-    test_msg("Expected '$expected', got '$resp_mesg'"));
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  $expected = $uid;
+  $self->assert($expected == $sql_uid,
+    test_msg("Expected $expected, got $sql_uid"));
+
+  $expected = $gid;
+  $self->assert($expected == $sql_gid,
+    test_msg("Expected $expected, got $sql_gid"));
 
   unlink($log_file);
 }
 
-sub sql_sqlshowinfo_pass_bug3423 {
+sub sql_sqlite_auth_type_backend_bug3511 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -6020,7 +6228,6 @@ sub sql_sqlshowinfo_pass_bug3423 {
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
-  my $lastdir = '/path/to/lastdir';
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -6038,7 +6245,7 @@ CREATE TABLE users (
   shell TEXT,
   lastdir TEXT
 );
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell, lastdir) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', '$lastdir');
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
   groupname TEXT,
@@ -6077,27 +6284,12 @@ EOS
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'AuthOrder mod_sql.c',
-
-        'SQLAuthenticate users groups',
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 100',
-
-        'SQLNamedQuery lastdir SELECT "lastdir FROM users WHERE userid = \'%u\'"',
-
-
-        # Configure the equivalent of a multiline DisplayLogin file
-        # using the SQLShowInfo directive
-
-        'SQLShowInfo PASS 230 " "',
-        'SQLShowInfo PASS 230 "Greetings, %u"',
-        'SQLShowInfo PASS 230 "Last directory: \"%{lastdir}\""',
-        'SQLShowInfo PASS 230 "-"',
-      ],
+      'mod_sql.c' => {
+        SQLAuthTypes => 'backend',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+      },
     },
   };
 
@@ -6120,38 +6312,24 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      $client->login($user, $passwd);
+      eval { $client->login($user, $passwd) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
 
-      my $resp_msgs = $client->response_msgs();
-      my $nmsgs = scalar(@$resp_msgs);
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
-      $expected = 5;
-      $self->assert($expected == $nmsgs,
-        test_msg("Expected $expected, got $nmsgs")); 
-
-      $expected = " ";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
-
-      $expected = " Greetings, $user";
-      $self->assert($expected eq $resp_msgs->[1],
-        test_msg("Expected '$expected', got '$resp_msgs->[1]'"));
-
-      $expected = " Last directory: \"$lastdir\"";
-      $self->assert($expected eq $resp_msgs->[2],
-        test_msg("Expected '$expected', got '$resp_msgs->[2]'"));
-
-      $expected = " -";
-      $self->assert($expected eq $resp_msgs->[3],
-        test_msg("Expected '$expected', got '$resp_msgs->[3]'"));
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code")); 
 
-      $expected = "User $user logged in";
-      $self->assert($expected eq $resp_msgs->[4],
-        test_msg("Expected '$expected', got '$resp_msgs->[4]'"));
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
 
-      $client->quit();
     };
 
     if ($@) {
@@ -6186,7 +6364,7 @@ EOS
   unlink($log_file);
 }
 
-sub sql_sqlshowinfo_list_bug3423 {
+sub sql_sqllog_pass_ok_var_s_bug3528 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -6196,13 +6374,31 @@ sub sql_sqlshowinfo_list_bug3423 {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
-  my $lastdir = '/path/to/lastdir';
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -6211,23 +6407,11 @@ sub sql_sqlshowinfo_list_bug3423 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT,
-  lastdir TEXT
-);
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell, lastdir) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', '$lastdir');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
+CREATE TABLE ftpsessions (
+  user TEXT,
+  resp_code TEXT,
+  resp_mesg TEXT
 );
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 EOS
 
     unless (close($fh)) {
@@ -6254,27 +6438,22 @@ EOS
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'AuthOrder mod_sql.c',
-
-        'SQLAuthenticate users groups',
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 100',
-
-        # Configure the equivalent of a multiline DisplayLogin file
-        # using the SQLShowInfo directive
-
-        'SQLShowInfo LIST 226 "Get that directory listing OK, %u?"',
-        'SQLShowInfo LIST 226 "-"',
-      ],
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, resp_code, resp_mesg) VALUES (\'%u\', \'%s\', \'%S\')"',
+        SQLLog => 'PASS,ERR_PASS info',
+      },
     },
   };
 
@@ -6296,67 +6475,13 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
+      $client->quit();
+    };
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
-      my $expected;
-
-      $expected = 230;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code")); 
-
-      $expected = "User $user logged in";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
-
-      my $conn = $client->list_raw();
-      unless ($conn) {
-        die("LIST failed unexpectedly: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf;
-      $conn->read($buf, 8192, 25);
-      eval { $conn->close() };
-
-      $resp_code = $client->response_code();
-
-      $expected = 226;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code")); 
-
-      my $resp_msgs = $client->response_msgs();
-      my $nmsgs = scalar(@$resp_msgs);
-
-      $expected = 4;
-      $self->assert($expected == $nmsgs,
-        test_msg("Expected $expected, got $nmsgs"));
-
-      $expected = "Opening ASCII mode data connection for file list";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
-
-      $expected = "Transfer complete";
-      $self->assert($expected eq $resp_msgs->[1],
-        test_msg("Expected '$expected', got '$resp_msgs->[1]'"));
-
-      $expected = " Get that directory listing OK, $user?";
-      $self->assert($expected eq $resp_msgs->[2],
-        test_msg("Expected '$expected', got '$resp_msgs->[2]'"));
-
-      $expected = "-";
-      $self->assert($expected eq $resp_msgs->[3],
-        test_msg("Expected '$expected', got '$resp_msgs->[3]'"));
-
-      $client->quit();
-    };
-
-    if ($@) {
-      $ex = $@;
-    }
+    if ($@) {
+      $ex = $@;
+    }
 
     $wfh->print("done\n");
     $wfh->flush();
@@ -6383,10 +6508,23 @@ EOS
     die($ex);
   }
 
+  my ($login, $resp_code, $resp_mesg) = get_resp_mesgs($db_file,
+    undef, "user, resp_code, resp_mesg");
+
+  my $expected;
+
+  $expected = 230;
+  $self->assert($expected eq $resp_code,
+    test_msg("Expected '$expected', got '$resp_code'"));
+
+  $expected = "User $user logged in";
+  $self->assert($expected eq $resp_mesg,
+    test_msg("Expected '$expected', got '$resp_mesg'"));
+
   unlink($log_file);
 }
 
-sub sql_multiple_users_shared_uid_gid {
+sub sql_sqllog_pass_failed_var_s_bug3528 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -6396,6 +6534,9 @@ sub sql_multiple_users_shared_uid_gid {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -6403,54 +6544,35 @@ sub sql_multiple_users_shared_uid_gid {
   my $uid = 500;
   my $gid = 500;
 
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
   my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
 
   if (open(my $fh, "> $db_script")) {
-    print $fh <<EOU;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT
-);
-EOU
-
-  for (my $i = 1; $i <= 10; $i++) {
-    my $name = $user . $i;
-
-    print $fh "INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$name', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');\n";
-  }
-
-    print $fh <<EOG;
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  resp_code TEXT,
+  resp_mesg TEXT
 );
-EOG
-
-  my $group_members = ($user . '1');
-  for (my $i = 2; $i <= 5; $i++) {
-    my $name = $user . $i;
-    $group_members .= ",$name";
-  }
-
-  my $group_name = ($group . '1');
-  print $fh "INSERT INTO groups (groupname, gid, members) VALUES ('$group_name', $gid, '$group_members');\n";
-
-  $group_members = ($user . '6');
-  for (my $i = 7; $i <= 10; $i++) {
-    my $name = $user . $i;
-    $group_members .= ",$name";
-  }
-
-  $group_name = ($group . '2');
-  print $fh "INSERT INTO groups (groupname, gid, members) VALUES ('$group_name', $gid, '$group_members');\n";
+EOS
 
     unless (close($fh)) {
       die("Can't write $db_script: $!");
@@ -6476,21 +6598,22 @@ EOG
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'AuthOrder mod_sql.c',
-
-        'SQLAuthenticate users groups',
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 100',
-      ],
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, resp_code, resp_mesg) VALUES (\'%u\', \'%s\', \'%S\')"',
+        SQLLog => 'PASS,ERR_PASS info',
+      },
     },
   };
 
@@ -6512,9 +6635,11 @@ EOG
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->login($user, 'foobar') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
 
-      my $name = $user . '10';
-      $client->login($name, $passwd);
       $client->quit();
     };
 
@@ -6547,46 +6672,23 @@ EOG
     die($ex);
   }
 
-  unlink($log_file);
-}
-
-sub get_cmds {
-  my $db_file = shift;
-  my $where = shift;
-
-  my $sql = "SELECT user, ip_addr, command, request FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
-  }
-
-  my $cmd = "sqlite3 $db_file \"$sql\"";
-
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
-
-  my $rows = [`$cmd`];
-
-  if ($ENV{TEST_VERBOSE}) {
-    use Data::Dumper;
-    print STDERR "Results: ", Dumper($rows), "\n";
-  }
+  my ($login, $resp_code, $resp_mesg) = get_resp_mesgs($db_file,
+    undef, "user, resp_code, resp_mesg");
 
-  my $res;
+  my $expected;
 
-  # Return the last row found, for now
-  foreach my $row (@$rows) {
-    chomp($row);
+  $expected = 530;
+  $self->assert($expected eq $resp_code,
+    test_msg("Expected '$expected', got '$resp_code'"));
 
-    # The default sqlite3 delimiter is '|'
-    $res = [split(/\|/, $row)];
-  }
+  $expected = "Login incorrect.";
+  $self->assert($expected eq $resp_mesg,
+    test_msg("Expected '$expected', got '$resp_mesg'"));
 
-  return unless $res;
-  return @$res;
+  unlink($log_file);
 }
 
-sub sql_resolve_tag_bug3536 {
+sub sql_sqllog_pass_failed_bug4301 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -6596,6 +6698,9 @@ sub sql_resolve_tag_bug3536 {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -6615,6 +6720,10 @@ sub sql_resolve_tag_bug3536 {
     }
   }
 
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -6622,28 +6731,10 @@ sub sql_resolve_tag_bug3536 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT
-);
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
-);
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
-
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT,
-  command TEXT,
-  request TEXT
+  resp_code TEXT,
+  resp_mesg TEXT
 );
 EOS
 
@@ -6670,8 +6761,9 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
@@ -6679,12 +6771,12 @@ EOS
       },
 
       'mod_sql.c' => {
-        SQLAuthTypes => 'plaintext',
+        SQLEngine => 'log',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'command FREEFORM "INSERT INTO ftpsessions (user, ip_addr, command, request) VALUES (\'%u\', \'%L\', \'%m\', \'%r\')"',
-        SQLLog => '* command',
+        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, resp_code, resp_mesg) VALUES (\'%u\', \'%s\', \'%S\')"',
+        SQLLog => 'PASS info',
       },
     },
   };
@@ -6707,12 +6799,13 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->user($user);
+      eval { $client->pass('') };
+      unless ($@) {
+        die("PASS succeeded unexpectedly");
+      }
 
-      # Ignore errors for these commands
-      eval {
-        my $name = "AAAAAAAAAA%m%m%mA%m%m%mA%m%mAA%m%m%m%m%mA%m%Z%m%mA%m%mAA%mA%ZAA%m%m%m%m%m%mA%m%ZAAA%m%m%m%m%m%m%m%m%m%m%m%m%m%m%mAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A%%%m%r%m%Z";
-        $client->user($name);
-      };
+      $client->quit();
     };
 
     if ($@) {
@@ -6734,7 +6827,6 @@ EOS
 
   # Stop server
   server_stop($pid_file);
-
   $self->assert_child_ok($pid);
 
   if ($ex) {
@@ -6744,38 +6836,29 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $req);
-  ($login, $ip_addr, $cmd, $req) = get_cmds($db_file, "user = \'$config_user\'");
-
-  $self->assert(!defined($login), test_msg("Expected undef, got '$login'"));
-  $self->assert(!defined($ip_addr), test_msg("Expected undef, got '$ip_addr'"));
-
-  unlink($log_file);
-}
-
-sub get_session_io {
-  my $db_file = shift;
-  my $where = shift;
-
-  my $sql = "SELECT user, ip_addr, bytes_in, bytes_out FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
+  # We want to ensure/assert that a failed PASS command does NOT cause
+  # the SQLLog directive to be triggered (Bug#4301).
+  eval {
+    my ($login, $resp_code, $resp_mesg) = get_resp_mesgs($db_file,
+      undef, "user, resp_code, resp_mesg");
+    $self->assert(!defined($resp_code), "Expected undef, got $resp_code");
+    $self->assert(!defined($resp_mesg), "Expected undef, got '$resp_mesg'");
+  };
+  if ($@) {
+    $ex = $@;
   }
 
-  my $cmd = "sqlite3 $db_file \"$sql\"";
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
 
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
+    die($ex);
   }
 
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
+  unlink($log_file);
 }
 
-sub sql_sqllog_vars_I_O_bug3554 {
+sub sql_sqlshowinfo_pass_bug3423 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -6785,31 +6868,13 @@ sub sql_sqllog_vars_I_O_bug3554 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $lastdir = '/path/to/lastdir';
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -6818,12 +6883,23 @@ sub sql_sqllog_vars_I_O_bug3554 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
-  ip_addr TEXT,
-  bytes_in NUMBER,
-  bytes_out NUMBER
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell, lastdir) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', '$lastdir');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
 );
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 EOS
 
     unless (close($fh)) {
@@ -6849,25 +6925,33 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_io FREEFORM "INSERT INTO ftpsessions (user, ip_addr, bytes_in, bytes_out) VALUES (\'%u\', \'%L\', %I, %O)"',
-        SQLLog => 'EXIT session_io',
-      },
+      'mod_sql.c' => [
+        'AuthOrder mod_sql.c',
+
+        'SQLAuthenticate users groups',
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 100',
+
+        'SQLNamedQuery lastdir SELECT "lastdir FROM users WHERE userid = \'%u\'"',
+
+
+        # Configure the equivalent of a multiline DisplayLogin file
+        # using the SQLShowInfo directive
+
+        'SQLShowInfo PASS 230 " "',
+        'SQLShowInfo PASS 230 "Greetings, %u"',
+        'SQLShowInfo PASS 230 "Last directory: \"%{lastdir}\""',
+        'SQLShowInfo PASS 230 "-"',
+      ],
     },
   };
 
@@ -6889,31 +6973,39 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
       $client->login($user, $passwd);
-      $client->type('ascii');
 
-      my $conn = $client->stor_raw('test.txt');
-      unless ($conn) {
-        die("STOR test.txt failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
 
-      my $buf = "ABCD\n" x 8;
-      $conn->write($buf, length($buf), 30);
-      $conn->close();
+      my $expected;
 
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
+      $expected = 5;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
 
-      $client->quit();
+      $expected = " ";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
 
-      my $expected = 226;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      $expected = " Greetings, $user";
+      $self->assert($expected eq $resp_msgs->[1],
+        test_msg("Expected '$expected', got '$resp_msgs->[1]'"));
 
-      $expected = 'Transfer complete';
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $expected = " Last directory: \"$lastdir\"";
+      $self->assert($expected eq $resp_msgs->[2],
+        test_msg("Expected '$expected', got '$resp_msgs->[2]'"));
+
+      $expected = " -";
+      $self->assert($expected eq $resp_msgs->[3],
+        test_msg("Expected '$expected', got '$resp_msgs->[3]'"));
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msgs->[4],
+        test_msg("Expected '$expected', got '$resp_msgs->[4]'"));
+
+      $client->quit();
     };
 
     if ($@) {
@@ -6945,59 +7037,10 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $bytes_in, $bytes_out) = get_session_io($db_file,
-    "user = \'$user\'");
-
-  my $expected;
-
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
-
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
-
-  $expected = 108;
-  $self->assert($expected == $bytes_in,
-    test_msg("Expected $expected, got $bytes_in"));
-
-  # Why would this number vary so widely?  It's because of the notation
-  # used to express the port number in a PASV response.  That port
-  # number is ephemeral, chosen by the kernel.
-
-  my $expected_min = 232;
-  my $expected_max = 236;
-  $self->assert($expected_min <= $bytes_out ||
-                $expected_max >= $bytes_out,
-    test_msg("Expected $expected_min - $expected_max, got $bytes_out"));
-
   unlink($log_file);
 }
 
-sub get_session_id {
-  my $db_file = shift;
-  my $where = shift;
-
-  my $sql = "SELECT user, ip_addr, session_id FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
-  }
-
-  my $cmd = "sqlite3 $db_file \"$sql\"";
-
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
-
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
-}
-
-sub sql_sqllog_note_var_unique_id_bug3572 {
+sub sql_sqlshowinfo_list_bug3423 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -7007,31 +7050,13 @@ sub sql_sqllog_note_var_unique_id_bug3572 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $lastdir = '/path/to/lastdir';
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
@@ -7040,11 +7065,23 @@ sub sql_sqllog_note_var_unique_id_bug3572 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
-  ip_addr TEXT,
-  session_id TEXT
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell, lastdir) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', '$lastdir');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
 );
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 EOS
 
     unless (close($fh)) {
@@ -7070,25 +7107,28 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_id FREEFORM "INSERT INTO ftpsessions (user, ip_addr, session_id) VALUES (\'%u\', \'%L\', \'%{note:UNIQUE_ID}\')"',
-        SQLLog => 'PASS session_id',
-      },
+      'mod_sql.c' => [
+        'AuthOrder mod_sql.c',
+
+        'SQLAuthenticate users groups',
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 100',
+
+        # Configure the equivalent of a multiline DisplayLogin file
+        # using the SQLShowInfo directive
+
+        'SQLShowInfo LIST 226 "Get that directory listing OK, %u?"',
+        'SQLShowInfo LIST 226 "-"',
+      ],
     },
   };
 
@@ -7110,7 +7150,61 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
       $client->login($user, $passwd);
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code")); 
+
+      $expected = "User $user logged in";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+
+      my $conn = $client->list_raw();
+      unless ($conn) {
+        die("LIST failed unexpectedly: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      $resp_code = $client->response_code();
+
+      $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code")); 
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      $expected = 4;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs"));
+
+      $expected = "Opening ASCII mode data connection for file list";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
+      $expected = "Transfer complete";
+      $self->assert($expected eq $resp_msgs->[1],
+        test_msg("Expected '$expected', got '$resp_msgs->[1]'"));
+
+      $expected = " Get that directory listing OK, $user?";
+      $self->assert($expected eq $resp_msgs->[2],
+        test_msg("Expected '$expected', got '$resp_msgs->[2]'"));
+
+      $expected = "-";
+      $self->assert($expected eq $resp_msgs->[3],
+        test_msg("Expected '$expected', got '$resp_msgs->[3]'"));
+
       $client->quit();
     };
 
@@ -7143,26 +7237,10 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $session_id) = get_session_id($db_file,
-    "user = \'$user\'");
-
-  my $expected;
-
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
-
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
-
-  $self->assert($session_id ne '',
-    test_msg("Expected session ID ($session_id), got ''"));
-
   unlink($log_file);
 }
 
-sub sql_sqllog_note_var_rewrite_bug3572 {
+sub sql_multiple_users_shared_uid_gid {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -7172,9 +7250,6 @@ sub sql_sqllog_note_var_rewrite_bug3572 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -7182,37 +7257,54 @@ sub sql_sqllog_note_var_rewrite_bug3572 {
   my $uid = 500;
   my $gid = 500;
 
-  my $domain = 'proftpd.org';
-
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
   my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
 
   if (open(my $fh, "> $db_script")) {
-    print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
-  ip_addr TEXT,
-  session_id TEXT
+    print $fh <<EOU;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
 );
-EOS
+EOU
+
+  for (my $i = 1; $i <= 10; $i++) {
+    my $name = $user . $i;
+
+    print $fh "INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$name', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');\n";
+  }
+
+    print $fh <<EOG;
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+EOG
+
+  my $group_members = ($user . '1');
+  for (my $i = 2; $i <= 5; $i++) {
+    my $name = $user . $i;
+    $group_members .= ",$name";
+  }
+
+  my $group_name = ($group . '1');
+  print $fh "INSERT INTO groups (groupname, gid, members) VALUES ('$group_name', $gid, '$group_members');\n";
+
+  $group_members = ($user . '6');
+  for (my $i = 7; $i <= 10; $i++) {
+    my $name = $user . $i;
+    $group_members .= ",$name";
+  }
+
+  $group_name = ($group . '2');
+  print $fh "INSERT INTO groups (groupname, gid, members) VALUES ('$group_name', $gid, '$group_members');\n";
 
     unless (close($fh)) {
       die("Can't write $db_script: $!");
@@ -7237,33 +7329,22 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'rewrite:10 sql:20 table:20',
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_rewrite.c' => [
-        'RewriteEngine on',
-        "RewriteLog $log_file",
+      'mod_sql.c' => [
+        'AuthOrder mod_sql.c',
 
-        'RewriteCondition %m USER',
-        'RewriteRule ^(.*#)?([-_.0-9A-Za-z]+)(@)?(.*)? $2',
+        'SQLAuthenticate users groups',
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 100',
       ],
-
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_id FREEFORM "INSERT INTO ftpsessions (user, ip_addr, session_id) VALUES (\'%U\', \'%L\', \'%{note:mod_rewrite.$4}\')"',
-        SQLLog => 'USER session_id',
-      },
     },
   };
 
@@ -7286,8 +7367,8 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my $login = $user . '@' . $domain;
-      $client->login($login, $passwd);
+      my $name = $user . '10';
+      $client->login($name, $passwd);
       $client->quit();
     };
 
@@ -7320,31 +7401,14 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $session_id) = get_session_id($db_file,
-    "user = \'$user\'");
-
-  my $expected;
-
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
-
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
-
-  $expected = $domain;
-  $self->assert($expected eq $session_id,
-    test_msg("Expected '$expected', got '$session_id'"));
-
   unlink($log_file);
 }
 
-sub get_sql_notes {
+sub get_cmds {
   my $db_file = shift;
   my $where = shift;
 
-  my $sql = "SELECT user, primary_group, home, shell FROM ftpnotes";
+  my $sql = "SELECT user, ip_addr, command, request FROM ftpsessions";
   if ($where) {
     $sql .= " WHERE $where";
   }
@@ -7355,14 +7419,28 @@ sub get_sql_notes {
     print STDERR "Executing sqlite3: $cmd\n";
   }
 
-  my $res = join('', `$cmd`);
-  chomp($res);
+  my $rows = [`$cmd`];
 
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
+  if ($ENV{TEST_VERBOSE}) {
+    use Data::Dumper;
+    print STDERR "Results: ", Dumper($rows), "\n";
+  }
+
+  my $res;
+
+  # Return the last row found, for now
+  foreach my $row (@$rows) {
+    chomp($row);
+
+    # The default sqlite3 delimiter is '|'
+    $res = [split(/\|/, $row)];
+  }
+
+  return unless $res;
+  return @$res;
 }
 
-sub sql_sqllog_note_sql_user_info {
+sub sql_resolve_tag_bug3536 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -7373,12 +7451,11 @@ sub sql_sqllog_note_sql_user_info {
   my $log_file = test_get_logfile();
 
   my $user = 'proftpd';
-  my $group = 'ftpd',
   my $passwd = 'test';
+  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
-  my $shell = '/bin/bash';
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -7404,11 +7481,10 @@ CREATE TABLE users (
   passwd TEXT,
   uid INTEGER,
   gid INTEGER,
-  homedir TEXT,
-  shell TEXT,
-  lastdir TEXT
+  homedir TEXT, 
+  shell TEXT
 );
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '$shell');
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
 
 CREATE TABLE groups (
   groupname TEXT,
@@ -7417,11 +7493,11 @@ CREATE TABLE groups (
 );
 INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 
-CREATE TABLE ftpnotes (
+CREATE TABLE ftpsessions (
   user TEXT,
-  primary_group TEXT,
-  home TEXT,
-  shell TEXT
+  ip_addr TEXT,
+  command TEXT,
+  request TEXT
 );
 EOS
 
@@ -7449,7 +7525,7 @@ EOS
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'DEFAULT:10 sql:20',
+    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
@@ -7460,10 +7536,9 @@ EOS
         SQLAuthTypes => 'plaintext',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
-        SQLMinID => 100,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'sql_notes FREEFORM "INSERT INTO ftpnotes (user, primary_group, home, shell) VALUES (\'%u\', \'%{note:primary-group}\', \'%{note:home}\', \'%{note:shell}\')"',
-        SQLLog => 'PASS sql_notes',
+        SQLNamedQuery => 'command FREEFORM "INSERT INTO ftpsessions (user, ip_addr, command, request) VALUES (\'%u\', \'%L\', \'%m\', \'%r\')"',
+        SQLLog => '* command',
       },
     },
   };
@@ -7486,8 +7561,12 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
-      $client->quit();
+
+      # Ignore errors for these commands
+      eval {
+        my $name = "AAAAAAAAAA%m%m%mA%m%m%mA%m%mAA%m%m%m%m%mA%m%Z%m%mA%m%mAA%mA%ZAA%m%m%m%m%m%mA%m%ZAAA%m%m%m%m%m%m%m%m%m%m%m%m%m%m%mAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A%%%m%r%m%Z";
+        $client->user($name);
+      };
     };
 
     if ($@) {
@@ -7519,31 +7598,38 @@ EOS
     die($ex);
   }
 
-  my ($login, $sql_group, $sql_home, $sql_shell) = get_sql_notes($db_file,
-    "user = \'$user\'");
+  my ($login, $ip_addr, $req);
+  ($login, $ip_addr, $cmd, $req) = get_cmds($db_file, "user = \'$config_user\'");
 
-  my $expected;
+  $self->assert(!defined($login), test_msg("Expected undef, got '$login'"));
+  $self->assert(!defined($ip_addr), test_msg("Expected undef, got '$ip_addr'"));
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+  unlink($log_file);
+}
 
-  $expected = $group;
-  $self->assert($expected eq $sql_group,
-    test_msg("Expected '$expected', got '$sql_group'"));
+sub get_session_io {
+  my $db_file = shift;
+  my $where = shift;
 
-  $expected = $home_dir;
-  $self->assert($expected eq $sql_home,
-    test_msg("Expected '$expected', got '$sql_home'"));
+  my $sql = "SELECT user, ip_addr, bytes_in, bytes_out FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
 
-  $expected = $shell;
-  $self->assert($expected eq $sql_shell,
-    test_msg("Expected '$expected', got '$sql_shell'"));
+  my $cmd = "sqlite3 $db_file \"$sql\"";
 
-  unlink($log_file);
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
 }
 
-sub sql_sqllog_var_E {
+sub sql_sqllog_vars_I_O_bug3554 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -7589,7 +7675,8 @@ sub sql_sqllog_var_E {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  resp_mesg TEXT
+  bytes_in NUMBER,
+  bytes_out NUMBER
 );
 EOS
 
@@ -7616,6 +7703,8 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -7630,8 +7719,8 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, ip_addr, resp_mesg) VALUES (\'%u\', \'%L\', \'%E\')"',
-        SQLLog => 'EXIT info',
+        SQLNamedQuery => 'session_io FREEFORM "INSERT INTO ftpsessions (user, ip_addr, bytes_in, bytes_out) VALUES (\'%u\', \'%L\', %I, %O)"',
+        SQLLog => 'EXIT session_io',
       },
     },
   };
@@ -7655,8 +7744,30 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->list();
+      $client->type('ascii');
+
+      my $conn = $client->stor_raw('test.txt');
+      unless ($conn) {
+        die("STOR test.txt failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf = "ABCD\n" x 8;
+      $conn->write($buf, length($buf), 30);
+      $conn->close();
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       $client->quit();
+
+      my $expected = 226;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = 'Transfer complete';
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -7688,8 +7799,8 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $resp_mesg) = get_resp_mesgs($db_file,
-    "user = \'$user\'", "user, ip_addr, resp_mesg");
+  my ($login, $ip_addr, $bytes_in, $bytes_out) = get_session_io($db_file,
+    "user = \'$user\'");
 
   my $expected;
 
@@ -7701,14 +7812,46 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = 'Quit';
-  $self->assert($expected eq $resp_mesg,
-    test_msg("Expected '$expected', got '$resp_mesg'"));
+  $expected = 108;
+  $self->assert($expected == $bytes_in,
+    test_msg("Expected $expected, got $bytes_in"));
+
+  # Why would this number vary so widely?  It's because of the notation
+  # used to express the port number in a PASV response.  That port
+  # number is ephemeral, chosen by the kernel.
+
+  my $expected_min = 232;
+  my $expected_max = 236;
+  $self->assert($expected_min <= $bytes_out ||
+                $expected_max >= $bytes_out,
+    test_msg("Expected $expected_min - $expected_max, got $bytes_out"));
 
   unlink($log_file);
 }
 
-sub sql_named_conn_bug3262 {
+sub get_session_id {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, ip_addr, session_id FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub sql_sqllog_note_var_unique_id_bug3572 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -7718,6 +7861,9 @@ sub sql_named_conn_bug3262 {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -7737,63 +7883,22 @@ sub sql_named_conn_bug3262 {
     }
   }
 
-  my $userdb_file = File::Spec->rel2abs("$tmpdir/proftpd-users.db");
-
-  # Build up sqlite3 command to create users, groups tables and populate them
-  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd-users.sql");
-
-  if (open(my $fh, "> $db_script")) {
-    print $fh <<EOS;
-CREATE TABLE ftpusers (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT,
-  lastdir TEXT
-);
-INSERT INTO ftpusers (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
-);
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
-
-CREATE TABLE ftpsessions (
-  user TEXT,
-  ip_addr TEXT,
-  timestamp TEXT
-);
-
-EOS
-
-    unless (close($fh)) {
-      die("Can't write $db_script: $!");
-    }
-
-  } else {
-    die("Can't open $db_script: $!");
-  }
-
-  my $cmd = "sqlite3 $userdb_file < $db_script";
-  build_db($cmd, $db_script);
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $logdb_file = File::Spec->rel2abs("$tmpdir/proftpd-log.db");
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
-  $db_script = File::Spec->rel2abs("$tmpdir/proftpd-log.sql");
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  timestamp TEXT
+  session_id TEXT
 );
-
 EOS
 
     unless (close($fh)) {
@@ -7804,14 +7909,14 @@ EOS
     die("Can't open $db_script: $!");
   }
 
-  $cmd = "sqlite3 $logdb_file < $db_script";
+  my $cmd = "sqlite3 $db_file < $db_script";
   build_db($cmd, $db_script);
 
   # Make sure that, if we're running as root, the database file has
   # the permissions/privs set for use by proftpd
   if ($< == 0) {
-    unless (chmod(0666, $userdb_file, $logdb_file)) {
-      die("Can't set perms on $userdb_file, $logdb_file to 0666: $!");
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
     }
   }
 
@@ -7819,26 +7924,25 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $userdb_file",
-        "SQLNamedConnectInfo logdb sqlite3 $logdb_file foo bar PERSESSION",
-        "SQLLogFile $log_file",
-        'SQLNamedQuery get-user-by-name SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers WHERE userid = \'%U\'"',
-        'SQLNamedQuery get-user-by-id SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers WHERE uid = %{0}"',
-        'SQLNamedQuery get-user-names SELECT "userid FROM ftpusers"',
-        'SQLNamedQuery get-all-users SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers"',
-        'SQLUserInfo custom:/get-user-by-name/get-user-by-id/get-user-names/get-all-users',
-        'SQLNamedQuery session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, timestamp) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')" logdb',
-        'SQLLog PASS session_start',
-      ],
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'session_id FREEFORM "INSERT INTO ftpsessions (user, ip_addr, session_id) VALUES (\'%u\', \'%L\', \'%{note:UNIQUE_ID}\')"',
+        SQLLog => 'PASS session_id',
+      },
     },
   };
 
@@ -7860,22 +7964,8 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       $client->login($user, $passwd);
-
-      my $resp_msgs = $client->response_msgs();
-      my $nmsgs = scalar(@$resp_msgs);
-
-      my $expected;
-
-      $expected = 1;
-      $self->assert($expected == $nmsgs,
-        test_msg("Expected $expected, got $nmsgs")); 
-
-      $expected = "User proftpd logged in";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
-
+      $client->quit();
     };
 
     if ($@) {
@@ -7907,19 +7997,7 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $timestamp) = get_sessions($userdb_file,
-    "user = \'$user\'");
-
-  $self->assert(!defined($login),
-    test_msg("Expected undef, got '$login'"));
-
-  $self->assert(!defined($ip_addr),
-    test_msg("Expected undef, got '$ip_addr'"));
-
-  $self->assert(!defined($timestamp),
-    test_msg("Expected undef, got '$timestamp'"));
-
-  ($login, $ip_addr, $timestamp) = get_sessions($logdb_file,
+  my ($login, $ip_addr, $session_id) = get_session_id($db_file,
     "user = \'$user\'");
 
   my $expected;
@@ -7932,14 +8010,13 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}';
-  $self->assert(qr/$expected/, $timestamp,
-    test_msg("Expected '$expected', got '$timestamp'"));
+  $self->assert($session_id ne '',
+    test_msg("Expected session ID ($session_id), got ''"));
 
   unlink($log_file);
 }
 
-sub sql_named_conn_sqllog_exit_bug3645 {
+sub sql_sqllog_note_var_rewrite_bug3572 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -7949,13 +8026,18 @@ sub sql_named_conn_sqllog_exit_bug3645 {
 
   my $log_file = test_get_logfile();
 
-  my $user = 'proftpd';
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
 
+  my $domain = 'proftpd.org';
+
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
   if ($< == 0) {
@@ -7968,55 +8050,22 @@ sub sql_named_conn_sqllog_exit_bug3645 {
     }
   }
 
-  my $userdb_file = File::Spec->rel2abs("$tmpdir/proftpd-users.db");
-
-  # Build up sqlite3 command to create users, groups tables and populate them
-  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd-users.sql");
-
-  if (open(my $fh, "> $db_script")) {
-    print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT
-);
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
-);
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
-EOS
-
-    unless (close($fh)) {
-      die("Can't write $db_script: $!");
-    }
-
-  } else {
-    die("Can't open $db_script: $!");
-  }
-
-  my $cmd = "sqlite3 $userdb_file < $db_script";
-  build_db($cmd, $db_script);
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
 
-  my $logdb_file = File::Spec->rel2abs("$tmpdir/proftpd-log.db");
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
-  $db_script = File::Spec->rel2abs("$tmpdir/proftpd-log.sql");
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  timestamp TEXT
+  session_id TEXT
 );
-
 EOS
 
     unless (close($fh)) {
@@ -8027,14 +8076,14 @@ EOS
     die("Can't open $db_script: $!");
   }
 
-  $cmd = "sqlite3 $logdb_file < $db_script";
+  my $cmd = "sqlite3 $db_file < $db_script";
   build_db($cmd, $db_script);
 
   # Make sure that, if we're running as root, the database file has
   # the permissions/privs set for use by proftpd
   if ($< == 0) {
-    unless (chmod(0666, $userdb_file, $logdb_file)) {
-      die("Can't set perms on $userdb_file, $logdb_file to 0666: $!");
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
     }
   }
 
@@ -8042,23 +8091,33 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'rewrite:10 sql:20 table:20',
 
-    DefaultRoot => '~',
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $userdb_file",
-        "SQLNamedConnectInfo logdb sqlite3 $logdb_file",
-        "SQLLogFile $log_file",
-        'SQLNamedQuery session_end FREEFORM "INSERT INTO ftpsessions (user, ip_addr, timestamp) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')" logdb',
-        'SQLLog EXIT session_end',
+      'mod_rewrite.c' => [
+        'RewriteEngine on',
+        "RewriteLog $log_file",
+
+        'RewriteCondition %m USER',
+        'RewriteRule ^(.*#)?([-_.0-9A-Za-z]+)(@)?(.*)? $2',
       ],
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'session_id FREEFORM "INSERT INTO ftpsessions (user, ip_addr, session_id) VALUES (\'%U\', \'%L\', \'%{note:mod_rewrite.$4}\')"',
+        SQLLog => 'USER session_id',
+      },
     },
   };
 
@@ -8081,21 +8140,9 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      $client->login($user, $passwd);
-
-      my $resp_msgs = $client->response_msgs();
-      my $nmsgs = scalar(@$resp_msgs);
-
-      my $expected;
-
-      $expected = 1;
-      $self->assert($expected == $nmsgs,
-        test_msg("Expected $expected, got $nmsgs")); 
-
-      $expected = "User proftpd logged in";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
-
+      my $login = $user . '@' . $domain;
+      $client->login($login, $passwd);
+      $client->quit();
     };
 
     if ($@) {
@@ -8127,19 +8174,7 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $timestamp) = get_sessions($userdb_file,
-    "user = \'$user\'");
-
-  $self->assert(!defined($login),
-    test_msg("Expected undef, got '$login'"));
-
-  $self->assert(!defined($ip_addr),
-    test_msg("Expected undef, got '$ip_addr'"));
-
-  $self->assert(!defined($timestamp),
-    test_msg("Expected undef, got '$timestamp'"));
-
-  ($login, $ip_addr, $timestamp) = get_sessions($logdb_file,
+  my ($login, $ip_addr, $session_id) = get_session_id($db_file,
     "user = \'$user\'");
 
   my $expected;
@@ -8152,14 +8187,36 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}';
-  $self->assert(qr/$expected/, $timestamp,
-    test_msg("Expected '$expected', got '$timestamp'"));
+  $expected = $domain;
+  $self->assert($expected eq $session_id,
+    test_msg("Expected '$expected', got '$session_id'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_vars_H_L_matching_server_bug3620 {
+sub get_sql_notes {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, primary_group, home, shell FROM ftpnotes";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub sql_sqllog_note_sql_user_info {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -8169,15 +8226,13 @@ sub sql_sqllog_vars_H_L_matching_server_bug3620 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
+  my $group = 'ftpd',
   my $passwd = 'test';
-  my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
   my $uid = 500;
   my $gid = 500;
+  my $shell = '/bin/bash';
 
   # Make sure that, if we're running as root, that the home directory has
   # permissions/privs set for the account we create
@@ -8191,10 +8246,6 @@ sub sql_sqllog_vars_H_L_matching_server_bug3620 {
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -8202,10 +8253,29 @@ sub sql_sqllog_vars_H_L_matching_server_bug3620 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '$shell');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE ftpnotes (
   user TEXT,
-  ip_addr TEXT,
-  rename_from TEXT
+  primary_group TEXT,
+  home TEXT,
+  shell TEXT
 );
 EOS
 
@@ -8228,23 +8298,12 @@ EOS
     }
   }
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    TraceLog => $log_file,
+    Trace => 'DEFAULT:10 sql:20',
 
     IfModules => {
       'mod_delay.c' => {
@@ -8252,12 +8311,13 @@ EOS
       },
 
       'mod_sql.c' => {
-        SQLEngine => 'log',
+        SQLAuthTypes => 'plaintext',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
+        SQLMinID => 100,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, rename_from) VALUES (\'%u\', \'%L\', \'%H\')"',
-        SQLLog => 'PASS session_start',
+        SQLNamedQuery => 'sql_notes FREEFORM "INSERT INTO ftpnotes (user, primary_group, home, shell) VALUES (\'%u\', \'%{note:primary-group}\', \'%{note:home}\', \'%{note:shell}\')"',
+        SQLLog => 'PASS sql_notes',
       },
     },
   };
@@ -8313,7 +8373,8 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $vhost_addr) = get_renames($db_file, "user = \'$user\'");
+  my ($login, $sql_group, $sql_home, $sql_shell) = get_sql_notes($db_file,
+    "user = \'$user\'");
 
   my $expected;
 
@@ -8321,17 +8382,22 @@ EOS
   $self->assert($expected eq $login,
     test_msg("Expected '$expected', got '$login'"));
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+  $expected = $group;
+  $self->assert($expected eq $sql_group,
+    test_msg("Expected '$expected', got '$sql_group'"));
 
-  $self->assert($expected eq $vhost_addr,
-    test_msg("Expected '$expected', got '$vhost_addr'"));
+  $expected = $home_dir;
+  $self->assert($expected eq $sql_home,
+    test_msg("Expected '$expected', got '$sql_home'"));
+
+  $expected = $shell;
+  $self->assert($expected eq $sql_shell,
+    test_msg("Expected '$expected', got '$sql_shell'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_vars_H_L_default_server_bug3620 {
+sub sql_sqllog_var_E {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -8377,7 +8443,7 @@ sub sql_sqllog_vars_H_L_default_server_bug3620 {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  rename_from TEXT
+  resp_mesg TEXT
 );
 EOS
 
@@ -8400,16 +8466,6 @@ EOS
     }
   }
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -8418,10 +8474,6 @@ EOS
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    DefaultServer => 'off',
-    SocketBindTight => 'off',
-    Port => '0',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -8432,52 +8484,14 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, rename_from) VALUES (\'%u\', \'%L\', \'%H\')"',
-        SQLLog => 'PASS session_start',
+        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, ip_addr, resp_mesg) VALUES (\'%u\', \'%L\', \'%E\')"',
+        SQLLog => 'EXIT info',
       },
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  # NOTE: this real_addr value may need to be tweaked as necessary.  Or
-  # maybe find a Perl module which can list the interfaces currently configured
-  # for the machine; all we really want is a non-127.0.0.1 address.
-  my $real_addr = '192.168.0.101';
-  my $real_port = ProFTPD::TestSuite::Utils::get_high_numbered_port();
-  my $vhost_addr = '0.0.0.0';
-
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-
-<VirtualHost $vhost_addr>
-  ServerName "DefaultServer VHost"
-  Port $real_port
-  DefaultServer on
-
-  AuthUserFile $auth_user_file
-  AuthGroupFile $auth_group_file
-  RequireValidShell off
-  WtmpLog off
-
-  <IfModule mod_sql.c>
-    SQLEngine log
-    SQLBackend sqlite3
-    SQLConnectInfo $db_file
-    SQLLogFile $log_file
-    SQLNamedQuery session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, rename_from) VALUES (\'%u\', \'%L\', \'%H\')"
-    SQLLog PASS session_start
-  </IfModule>
-</VirtualHost>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -8493,8 +8507,9 @@ EOC
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new($real_addr, $real_port);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->list();
       $client->quit();
     };
 
@@ -8527,7 +8542,8 @@ EOC
     die($ex);
   }
 
-  my ($login, $ip_addr, $sess_addr) = get_renames($db_file, "user = \'$user\'");
+  my ($login, $ip_addr, $resp_mesg) = get_resp_mesgs($db_file,
+    "user = \'$user\'", "user, ip_addr, resp_mesg");
 
   my $expected;
 
@@ -8535,18 +8551,18 @@ EOC
   $self->assert($expected eq $login,
     test_msg("Expected '$expected', got '$login'"));
 
-  $expected = $real_addr;
+  $expected = '127.0.0.1';
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = $vhost_addr;
-  $self->assert($expected eq $sess_addr,
-    test_msg("Expected '$expected', got '$sess_addr'"));
+  $expected = 'Quit';
+  $self->assert($expected eq $resp_mesg,
+    test_msg("Expected '$expected', got '$resp_mesg'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_exit_ifuser {
+sub sql_named_conn_bug3262 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -8556,9 +8572,6 @@ sub sql_sqllog_exit_ifuser {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -8578,21 +8591,63 @@ sub sql_sqllog_exit_ifuser {
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $userdb_file = File::Spec->rel2abs("$tmpdir/proftpd-users.db");
 
-  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd-users.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpusers (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  lastdir TEXT
+);
+INSERT INTO ftpusers (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  timestamp TEXT
+);
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $userdb_file < $db_script";
+  build_db($cmd, $db_script);
+
+  my $logdb_file = File::Spec->rel2abs("$tmpdir/proftpd-log.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
-  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+  $db_script = File::Spec->rel2abs("$tmpdir/proftpd-log.sql");
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT
+  ip_addr TEXT,
+  timestamp TEXT
 );
+
 EOS
 
     unless (close($fh)) {
@@ -8603,14 +8658,14 @@ EOS
     die("Can't open $db_script: $!");
   }
 
-  my $cmd = "sqlite3 $db_file < $db_script";
+  $cmd = "sqlite3 $logdb_file < $db_script";
   build_db($cmd, $db_script);
 
   # Make sure that, if we're running as root, the database file has
   # the permissions/privs set for use by proftpd
   if ($< == 0) {
-    unless (chmod(0666, $db_file)) {
-      die("Can't set perms on $db_file to 0666: $!");
+    unless (chmod(0666, $userdb_file, $logdb_file)) {
+      die("Can't set perms on $userdb_file, $logdb_file to 0666: $!");
     }
   }
 
@@ -8619,42 +8674,30 @@ EOS
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr) VALUES (\'%u\', \'%L\')"',
-      },
+      'mod_sql.c' => [
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $userdb_file",
+        "SQLNamedConnectInfo logdb sqlite3 $logdb_file foo bar PERSESSION",
+        "SQLLogFile $log_file",
+        'SQLNamedQuery get-user-by-name SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers WHERE userid = \'%U\'"',
+        'SQLNamedQuery get-user-by-id SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers WHERE uid = %{0}"',
+        'SQLNamedQuery get-user-names SELECT "userid FROM ftpusers"',
+        'SQLNamedQuery get-all-users SELECT "userid, passwd, uid, gid, homedir, shell FROM ftpusers"',
+        'SQLUserInfo custom:/get-user-by-name/get-user-by-id/get-user-names/get-all-users',
+        'SQLNamedQuery session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, timestamp) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')" logdb',
+        'SQLLog PASS session_start',
+      ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<IfModule mod_ifsession.c>
-  <IfUser regex .*>
-    SQLLog EXIT logout
-  </IfUser>
-</IfModule>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -8670,15 +8713,23 @@ EOC
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-
-      # First, just connect and quit, without logging in
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->quit();
 
-      # Then connect, login, and quit
-      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->quit();
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
     };
 
     if ($@) {
@@ -8710,18 +8761,20 @@ EOC
     die($ex);
   }
 
-  my $query = "SELECT user, ip_addr FROM ftpsessions";
-  $cmd = "sqlite3 $db_file \"$query\"";
+  my ($login, $ip_addr, $timestamp) = get_sessions($userdb_file,
+    "user = \'$user\'");
 
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
+  $self->assert(!defined($login),
+    test_msg("Expected undef, got '$login'"));
 
-  my @res = `$cmd`;
-  my $res = join('', @res);
-  chomp($res);
+  $self->assert(!defined($ip_addr),
+    test_msg("Expected undef, got '$ip_addr'"));
 
-  my ($login, $ip_addr) = split(/\|/, $res);
+  $self->assert(!defined($timestamp),
+    test_msg("Expected undef, got '$timestamp'"));
+
+  ($login, $ip_addr, $timestamp) = get_sessions($logdb_file,
+    "user = \'$user\'");
 
   my $expected;
 
@@ -8733,10 +8786,14 @@ EOC
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
+  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}';
+  $self->assert(qr/$expected/, $timestamp,
+    test_msg("Expected '$expected', got '$timestamp'"));
+
   unlink($log_file);
 }
 
-sub sql_sqllog_exit_ifgroup {
+sub sql_named_conn_sqllog_exit_bug3645 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -8746,9 +8803,6 @@ sub sql_sqllog_exit_ifgroup {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -8768,21 +8822,55 @@ sub sql_sqllog_exit_ifgroup {
     }
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  my $userdb_file = File::Spec->rel2abs("$tmpdir/proftpd-users.db");
 
-  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd-users.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $userdb_file < $db_script";
+  build_db($cmd, $db_script);
+
+  my $logdb_file = File::Spec->rel2abs("$tmpdir/proftpd-log.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
-  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+  $db_script = File::Spec->rel2abs("$tmpdir/proftpd-log.sql");
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT
+  ip_addr TEXT,
+  timestamp TEXT
 );
+
 EOS
 
     unless (close($fh)) {
@@ -8793,14 +8881,14 @@ EOS
     die("Can't open $db_script: $!");
   }
 
-  my $cmd = "sqlite3 $db_file < $db_script";
+  $cmd = "sqlite3 $logdb_file < $db_script";
   build_db($cmd, $db_script);
 
   # Make sure that, if we're running as root, the database file has
   # the permissions/privs set for use by proftpd
   if ($< == 0) {
-    unless (chmod(0666, $db_file)) {
-      die("Can't set perms on $db_file to 0666: $!");
+    unless (chmod(0666, $userdb_file, $logdb_file)) {
+      die("Can't set perms on $userdb_file, $logdb_file to 0666: $!");
     }
   }
 
@@ -8809,42 +8897,27 @@ EOS
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr) VALUES (\'%u\', \'%L\')"',
-      },
+      'mod_sql.c' => [
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $userdb_file",
+        "SQLNamedConnectInfo logdb sqlite3 $logdb_file",
+        "SQLLogFile $log_file",
+        'SQLNamedQuery session_end FREEFORM "INSERT INTO ftpsessions (user, ip_addr, timestamp) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')" logdb',
+        'SQLLog EXIT session_end',
+      ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<IfModule mod_ifsession.c>
-  <IfGroup regex .*>
-    SQLLog EXIT logout
-  </IfGroup>
-</IfModule>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -8860,15 +8933,23 @@ EOC
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-
-      # First, just connect and quit, without logging in
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->quit();
 
-      # Then connect, login, and quit
-      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->quit();
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
     };
 
     if ($@) {
@@ -8900,18 +8981,20 @@ EOC
     die($ex);
   }
 
-  my $query = "SELECT user, ip_addr FROM ftpsessions";
-  $cmd = "sqlite3 $db_file \"$query\"";
+  my ($login, $ip_addr, $timestamp) = get_sessions($userdb_file,
+    "user = \'$user\'");
 
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
+  $self->assert(!defined($login),
+    test_msg("Expected undef, got '$login'"));
 
-  my @res = `$cmd`;
-  my $res = join('', @res);
-  chomp($res);
+  $self->assert(!defined($ip_addr),
+    test_msg("Expected undef, got '$ip_addr'"));
 
-  my ($login, $ip_addr) = split(/\|/, $res);
+  $self->assert(!defined($timestamp),
+    test_msg("Expected undef, got '$timestamp'"));
+
+  ($login, $ip_addr, $timestamp) = get_sessions($logdb_file,
+    "user = \'$user\'");
 
   my $expected;
 
@@ -8923,10 +9006,14 @@ EOC
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
+  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}';
+  $self->assert(qr/$expected/, $timestamp,
+    test_msg("Expected '$expected', got '$timestamp'"));
+
   unlink($log_file);
 }
 
-sub sql_sqllog_multi_pass_ifclass_bug4025 {
+sub sql_sqllog_vars_H_L_matching_server_bug3620 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -8971,7 +9058,8 @@ sub sql_sqllog_multi_pass_ifclass_bug4025 {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT
+  ip_addr TEXT,
+  rename_from TEXT
 );
 EOS
 
@@ -8994,56 +9082,42 @@ EOS
     }
   }
 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 ifsession:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
-    AuthOrder => 'mod_auth_file.c',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'SQLEngine log',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLNamedQuery login_ip FREEFORM "INSERT INTO ftpsessions (ip_addr) VALUES (\'%L\')"',
-        'SQLNamedQuery login_user FREEFORM "INSERT INTO ftpsessions (user) VALUES (\'%u\')"',
-      ],
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, rename_from) VALUES (\'%u\', \'%L\', \'%H\')"',
+        SQLLog => 'PASS session_start',
+      },
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
-  if (open(my $fh, ">> $config_file")) {
-    print $fh <<EOC;
-<Class test>
-  From 127.0.0.1
-</Class>
-
-<IfModule mod_ifsession.c>
-  <IfClass test>
-    SQLLog PASS login_ip
-    SQLLog PASS login_user
-  </IfClass>
-</IfModule>
-EOC
-    unless (close($fh)) {
-      die("Can't write $config_file: $!");
-    }
-
-  } else {
-    die("Can't open $config_file: $!");
-  }
-
   # 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.
@@ -9093,19 +9167,7 @@ EOC
     die($ex);
   }
 
-  my $query = "SELECT user, ip_addr FROM ftpsessions";
-  $cmd = "sqlite3 $db_file \"$query\"";
-
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
-
-  my @res = `$cmd`;
-  my $res = join('', @res);
-  $res =~ s/\n//g;
-  chomp($res);
-
-  my ($login, $ip_addr) = split(/\|+/, $res);
+  my ($login, $ip_addr, $vhost_addr) = get_renames($db_file, "user = \'$user\'");
 
   my $expected;
 
@@ -9117,10 +9179,13 @@ EOC
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
+  $self->assert($expected eq $vhost_addr,
+    test_msg("Expected '$expected', got '$vhost_addr'"));
+
   unlink($log_file);
 }
 
-sub sql_opt_no_disconnect_on_error_with_extlog_bug3633 {
+sub sql_sqllog_vars_H_L_default_server_bug3620 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9166,7 +9231,7 @@ sub sql_opt_no_disconnect_on_error_with_extlog_bug3633 {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  timestamp TEXT
+  rename_from TEXT
 );
 EOS
 
@@ -9189,31 +9254,27 @@ EOS
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "Hello, World!\n";
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
 
   } else {
-    die("Can't open $test_file: $!");
+    die("Can't open $src_file: $!");
   }
 
-  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
 
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'DEFAULT:10 sql:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    LogFormat => 'custom "%m %f"',
-    ExtendedLog => "$ext_log ALL custom",
+    DefaultServer => 'off',
+    SocketBindTight => 'off',
+    Port => '0',
 
     IfModules => {
       'mod_delay.c' => {
@@ -9225,16 +9286,52 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'log_dele FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
-        SQLLog => 'DELE log_dele',
-
-        SQLOptions => 'noDisconnectOnError',
+        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, rename_from) VALUES (\'%u\', \'%L\', \'%H\')"',
+        SQLLog => 'PASS session_start',
       },
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  # NOTE: this real_addr value may need to be tweaked as necessary.  Or
+  # maybe find a Perl module which can list the interfaces currently configured
+  # for the machine; all we really want is a non-127.0.0.1 address.
+  my $real_addr = '192.168.0.101';
+  my $real_port = ProFTPD::TestSuite::Utils::get_high_numbered_port();
+  my $vhost_addr = '0.0.0.0';
+
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+
+<VirtualHost $vhost_addr>
+  ServerName "DefaultServer VHost"
+  Port $real_port
+  DefaultServer on
+
+  AuthUserFile $auth_user_file
+  AuthGroupFile $auth_group_file
+  RequireValidShell off
+  WtmpLog off
+
+  <IfModule mod_sql.c>
+    SQLEngine log
+    SQLBackend sqlite3
+    SQLConnectInfo $db_file
+    SQLLogFile $log_file
+    SQLNamedQuery session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, rename_from) VALUES (\'%u\', \'%L\', \'%H\')"
+    SQLLog PASS session_start
+  </IfModule>
+</VirtualHost>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -9250,10 +9347,8 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
+      my $client = ProFTPD::TestSuite::FTP->new($real_addr, $real_port);
       $client->login($user, $passwd);
-      $client->dele('test.txt');
       $client->quit();
     };
 
@@ -9286,39 +9381,26 @@ EOS
     die($ex);
   }
 
-  # Now, read in the ExtendedLog, and see whether the %f variable was
-  # properly written out.
-  if (open(my $fh, "< $ext_log")) {
-    my $ok = 0;
-
-    while (my $line = <$fh>) {
-      chomp($line);
-
-      # We're only interested in the DELE log line
-      unless ($line =~ /^DELE (.*)$/i) {
-        next;
-      }
-
-      my $name = $1;
-      my $expected = $test_file;
-      $self->assert($expected eq $name,
-        test_msg("Expected '$expected', got '$name'"));
+  my ($login, $ip_addr, $sess_addr) = get_renames($db_file, "user = \'$user\'");
 
-      $ok = 1;
-    }
+  my $expected;
 
-    close($fh);
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
 
-    $self->assert($ok, test_msg("Expected ExtendedLog messages not found"));
+  $expected = $real_addr;
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
 
-  } else {
-    die("Can't read $ext_log: $!");
-  }
+  $expected = $vhost_addr;
+  $self->assert($expected eq $sess_addr,
+    test_msg("Expected '$expected', got '$sess_addr'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_ignore_errors_bad_table_bug3692 {
+sub sql_sqllog_exit_ifuser {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9363,8 +9445,7 @@ sub sql_sqllog_ignore_errors_bad_table_bug3692 {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT,
-  timestamp TEXT
+  ip_addr TEXT
 );
 EOS
 
@@ -9405,14 +9486,29 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
-        SQLLog => 'PASS session_start IGNORE_ERRORS',
+        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr) VALUES (\'%u\', \'%L\')"',
       },
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<IfModule mod_ifsession.c>
+  <IfUser regex .*>
+    SQLLog EXIT logout
+  </IfUser>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -9428,9 +9524,15 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+
+      # First, just connect and quit, without logging in
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->quit();
 
+      # Then connect, login, and quit
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->quit();
     };
 
     if ($@) {
@@ -9462,10 +9564,33 @@ EOS
     die($ex);
   }
 
+  my $query = "SELECT user, ip_addr FROM ftpsessions";
+  $cmd = "sqlite3 $db_file \"$query\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @res = `$cmd`;
+  my $res = join('', @res);
+  chomp($res);
+
+  my ($login, $ip_addr) = split(/\|/, $res);
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
   unlink($log_file);
 }
 
-sub sql_sqllog_ignore_errors_bad_db_bug3692 {
+sub sql_sqllog_exit_ifgroup {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9510,8 +9635,7 @@ sub sql_sqllog_ignore_errors_bad_db_bug3692 {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT,
-  timestamp TEXT
+  ip_addr TEXT
 );
 EOS
 
@@ -9550,16 +9674,31 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => "/foobar/$db_file",
+        SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
-        SQLLog => 'PASS session_start IGNORE_ERRORS',
+        SQLNamedQuery => 'logout FREEFORM "INSERT INTO ftpsessions (user, ip_addr) VALUES (\'%u\', \'%L\')"',
       },
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<IfModule mod_ifsession.c>
+  <IfGroup regex .*>
+    SQLLog EXIT logout
+  </IfGroup>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -9575,9 +9714,15 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+
+      # First, just connect and quit, without logging in
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->quit();
 
+      # Then connect, login, and quit
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->quit();
     };
 
     if ($@) {
@@ -9609,33 +9754,33 @@ EOS
     die($ex);
   }
 
-  unlink($log_file);
-}
-
-sub get_xfer_status {
-  my $db_file = shift;
-  my $where = shift;
-
-  my $sql = "SELECT user, ip_addr, xfer_status, xfer_path FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
-  }
-  $sql .= " LIMIT 1";
-
-  my $cmd = "sqlite3 $db_file \"$sql\"";
+  my $query = "SELECT user, ip_addr FROM ftpsessions";
+  $cmd = "sqlite3 $db_file \"$query\"";
 
   if ($ENV{TEST_VERBOSE}) {
     print STDERR "Executing sqlite3: $cmd\n";
   }
 
-  my $res = join('', `$cmd`);
+  my @res = `$cmd`;
+  my $res = join('', @res);
   chomp($res);
 
-  # The default sqlite3 delimiter is '|'
-  return map { chomp($_); $_; } split(/\|/, $res);
+  my ($login, $ip_addr) = split(/\|/, $res);
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  unlink($log_file);
 }
 
-sub sql_sqllog_var_xfer_status_nonxfer {
+sub sql_sqllog_multi_pass_ifclass_bug4025 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9680,9 +9825,7 @@ sub sql_sqllog_var_xfer_status_nonxfer {
     print $fh <<EOS;
 CREATE TABLE ftpsessions (
   user TEXT,
-  ip_addr TEXT,
-  xfer_status TEXT,
-  xfer_path TEXT
+  ip_addr TEXT
 );
 EOS
 
@@ -9705,42 +9848,56 @@ EOS
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'sql:20 command:20 netio:20',
+    Trace => 'DEFAULT:10 ifsession:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
+    AuthOrder => 'mod_auth_file.c',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
-        SQLLog => 'PWD xfer_status',
-      },
+      'mod_sql.c' => [
+        'SQLEngine log',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLNamedQuery login_ip FREEFORM "INSERT INTO ftpsessions (ip_addr) VALUES (\'%L\')"',
+        'SQLNamedQuery login_user FREEFORM "INSERT INTO ftpsessions (user) VALUES (\'%u\')"',
+      ],
     },
   };
 
   my ($port, $config_user, $config_group) = config_write($config_file, $config);
 
+  if (open(my $fh, ">> $config_file")) {
+    print $fh <<EOC;
+<Class test>
+  From 127.0.0.1
+</Class>
+
+<IfModule mod_ifsession.c>
+  <IfClass test>
+    SQLLog PASS login_ip
+    SQLLog PASS login_user
+  </IfClass>
+</IfModule>
+EOC
+    unless (close($fh)) {
+      die("Can't write $config_file: $!");
+    }
+
+  } else {
+    die("Can't open $config_file: $!");
+  }
+
   # 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.
@@ -9758,7 +9915,6 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->pwd();
       $client->quit();
     };
 
@@ -9791,30 +9947,34 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
+  my $query = "SELECT user, ip_addr FROM ftpsessions";
+  $cmd = "sqlite3 $db_file \"$query\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @res = `$cmd`;
+  my $res = join('', @res);
+  $res =~ s/\n//g;
+  chomp($res);
+
+  my ($login, $ip_addr) = split(/\|+/, $res);
 
   my $expected;
 
   $expected = $user;
   $self->assert($expected eq $login,
-    test_msg("Expected user '$expected', got '$login'"));
+    test_msg("Expected '$expected', got '$login'"));
 
   $expected = '127.0.0.1';
   $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
-
-  $expected = '-';
-  $self->assert($expected eq $xfer_status,
-    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
-
-  $expected = $home_dir;
-  $self->assert($expected eq $xfer_path,
-    test_msg("Expected file path '$expected', got '$xfer_path'"));
+    test_msg("Expected '$expected', got '$ip_addr'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_var_xfer_status_success_download {
+sub sql_opt_no_disconnect_on_error_with_extlog_bug3633 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -9860,8 +10020,7 @@ sub sql_sqllog_var_xfer_status_success_download {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  xfer_status TEXT,
-  xfer_path TEXT
+  timestamp TEXT
 );
 EOS
 
@@ -9884,24 +10043,32 @@ EOS
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    close($fh);
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
   } else {
     die("Can't open $test_file: $!");
   }
 
+  my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'sql:20 command:20 netio:20',
+    Trace => 'DEFAULT:10 sql:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
+    LogFormat => 'custom "%m %f"',
+    ExtendedLog => "$ext_log ALL custom",
+
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -9912,8 +10079,10 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
-        SQLLog => 'ERR_RETR,RETR xfer_status',
+        SQLNamedQuery => 'log_dele FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
+        SQLLog => 'DELE log_dele',
+
+        SQLOptions => 'noDisconnectOnError',
       },
     },
   };
@@ -9936,23 +10105,9 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
-
-      my $conn = $client->retr_raw('test.txt');
-      unless ($conn) {
-        die("RETR test.txt failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf;
-      $conn->read($buf, 8192, 25);
-      eval { $conn->close() };
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
-      $self->assert_transfer_ok($resp_code, $resp_msg);
 
+      $client->login($user, $passwd);
+      $client->dele('test.txt');
       $client->quit();
     };
 
@@ -9985,30 +10140,45 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
+  # Now, read in the ExtendedLog, and see whether the %f variable was
+  # properly written out.
+  if (open(my $fh, "< $ext_log")) {
+    my $ok = 0;
 
-  my $expected;
+    while (my $line = <$fh>) {
+      chomp($line);
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected user '$expected', got '$login'"));
+      # We're only interested in the DELE log line
+      unless ($line =~ /^DELE (.*)$/i) {
+        next;
+      }
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+      my $name = $1;
 
-  $expected = 'success';
-  $self->assert($expected eq $xfer_status,
-    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
 
-  $expected = $test_file;
-  $self->assert($expected eq $xfer_path,
-    test_msg("Expected file path '$expected', got '$xfer_path'"));
+      my $expected = $test_file;
+      $self->assert($expected eq $name,
+        test_msg("Expected '$expected', got '$name'"));
+
+      $ok = 1;
+    }
+
+    close($fh);
+
+    $self->assert($ok, test_msg("Expected ExtendedLog messages not found"));
+
+  } else {
+    die("Can't read $ext_log: $!");
+  }
 
   unlink($log_file);
 }
 
-sub sql_sqllog_var_xfer_status_success_upload {
+sub sql_sqllog_ignore_errors_bad_table_bug3692 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10054,8 +10224,7 @@ sub sql_sqllog_var_xfer_status_success_upload {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  xfer_status TEXT,
-  xfer_path TEXT
+  timestamp TEXT
 );
 EOS
 
@@ -10078,14 +10247,10 @@ EOS
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20 command:20 netio:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -10100,8 +10265,8 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
-        SQLLog => 'ERR_STOR,STOR xfer_status',
+        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
+        SQLLog => 'PASS session_start IGNORE_ERRORS',
       },
     },
   };
@@ -10124,24 +10289,8 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
-
-      my $conn = $client->stor_raw('test.txt');
-      unless ($conn) {
-        die("STOR test.txt failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf = "Hello, World!\n";
-      $conn->write($buf, length($buf), 25);
-      eval { $conn->close() };
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
-      $self->assert_transfer_ok($resp_code, $resp_msg);
 
-      $client->quit();
+      $client->login($user, $passwd);
     };
 
     if ($@) {
@@ -10173,30 +10322,10 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
-
-  my $expected;
-
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected user '$expected', got '$login'"));
-
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
-
-  $expected = 'success';
-  $self->assert($expected eq $xfer_status,
-    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
-
-  $expected = $test_file;
-  $self->assert($expected eq $xfer_path,
-    test_msg("Expected file path '$expected', got '$xfer_path'"));
-
   unlink($log_file);
 }
 
-sub sql_sqllog_var_xfer_status_cancelled {
+sub sql_sqllog_ignore_errors_bad_db_bug3692 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10242,8 +10371,7 @@ sub sql_sqllog_var_xfer_status_cancelled {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  xfer_status TEXT,
-  xfer_path TEXT
+  timestamp TEXT
 );
 EOS
 
@@ -10266,24 +10394,10 @@ EOS
     }
   }
 
-  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    print $fh "ABCDefgh" x 32768;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20 command:20 netio:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -10296,10 +10410,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
+        SQLConnectInfo => "/foobar/$db_file",
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
-        SQLLog => 'ERR_RETR,RETR xfer_status',
+        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
+        SQLLog => 'PASS session_start IGNORE_ERRORS',
       },
     },
   };
@@ -10322,24 +10436,8 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client->login($user, $passwd);
 
-      my $conn = $client->retr_raw('test.txt');
-      unless ($conn) {
-        die("RETR test.txt failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf;
-      $conn->read($buf, 4, 25);
-      eval { $conn->abort() };
-
-      my $resp_code = $client->response_code();
-      my $resp_msg = $client->response_msg();
-
-      $self->assert_transfer_ok($resp_code, $resp_msg, 1);
-
-      $client->quit();
+      $client->login($user, $passwd);
     };
 
     if ($@) {
@@ -10371,30 +10469,33 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
+  unlink($log_file);
+}
 
-  my $expected;
+sub get_xfer_status {
+  my $db_file = shift;
+  my $where = shift;
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected user '$expected', got '$login'"));
+  my $sql = "SELECT user, ip_addr, xfer_status, xfer_path FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+  $sql .= " LIMIT 1";
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+  my $cmd = "sqlite3 $db_file \"$sql\"";
 
-  $expected = 'cancelled';
-  $self->assert($expected eq $xfer_status,
-    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
 
-  $expected = $test_file;
-  $self->assert($expected eq $xfer_path,
-    test_msg("Expected file path '$expected', got '$xfer_path'"));
+  my $res = join('', `$cmd`);
+  chomp($res);
 
-  unlink($log_file);
+  # The default sqlite3 delimiter is '|'
+  return map { chomp($_); $_; } split(/\|/, $res);
 }
 
-sub sql_sqllog_var_xfer_status_failed {
+sub sql_sqllog_var_xfer_status_nonxfer {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10466,11 +10567,7 @@ EOS
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "ABCDefgh" x 32768;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
+    close($fh);
 
   } else {
     die("Can't open $test_file: $!");
@@ -10486,9 +10583,6 @@ EOS
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    # This is used to tickle the "failed" transfer status
-    MaxRetrieveFileSize => '12 B',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -10500,7 +10594,7 @@ EOS
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
         SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
-        SQLLog => 'ERR_RETR,RETR xfer_status',
+        SQLLog => 'PWD xfer_status',
       },
     },
   };
@@ -10524,18 +10618,7 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-
-      my $conn = $client->retr_raw('test.txt');
-      unless ($conn) {
-        die("RETR test.txt failed: " . $client->response_code() . " " .
-          $client->response_msg());
-      }
-
-      my $buf;
-      $conn->read($buf, 4, 25);
-      eval { $conn->close() };
-
-      # Make the control connection go away uncleanly
+      $client->pwd();
       $client->quit();
     };
 
@@ -10580,18 +10663,23 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = 'failed';
+  $expected = '-';
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
-  $expected = $test_file;
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $home_dir = '/private' . $home_dir;
+  }
+
+  $expected = $home_dir;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_var_xfer_status_timeout {
+sub sql_sqllog_var_xfer_status_success_download {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10663,18 +10751,12 @@ EOS
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "ABCDefgh" x 32768;
-
-    unless (close($fh)) {
-      die("Can't write $test_file: $!");
-    }
+    close($fh);
 
   } else {
     die("Can't open $test_file: $!");
   }
 
-  my $timeout_stalled = 1;
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -10685,9 +10767,6 @@ EOS
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    # This is used to tickle the "timeout" transfer status
-    TimeoutStalled => $timeout_stalled,
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -10699,7 +10778,7 @@ EOS
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
         SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
-        SQLLog => 'ERR_RETR,RETR,EXIT xfer_status',
+        SQLLog => 'ERR_RETR,RETR xfer_status',
       },
     },
   };
@@ -10723,40 +10802,23 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->type('ascii');
 
       my $conn = $client->retr_raw('test.txt');
       unless ($conn) {
-        die("RETR test.xt failed: " . $client->response_code() . " " .
+        die("RETR test.txt failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
-      sleep($timeout_stalled + 1);
-
       my $buf;
-      $conn->read($buf, 32, 25);
-      sleep($timeout_stalled);
+      $conn->read($buf, 8192, 25);
       eval { $conn->close() };
 
-      eval { $client->noop() };
-      unless ($@) {
-        die("NOOP succeeded unexpectedly");
-      }
-
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
 
-      my $expected;
-
-      # Perl's Net::Cmd module uses a very non-standard 599 code to
-      # indicate that the connection is closed
-      $expected = 599;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+      $self->assert_transfer_ok($resp_code, $resp_msg);
 
-      $expected = "Connection closed";
-      $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+      $client->quit();
     };
 
     if ($@) {
@@ -10800,10 +10862,15 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = 'timeout';
+  $expected = 'success';
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $test_file = '/private' . $test_file;
+  }
+
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
     test_msg("Expected file path '$expected', got '$xfer_path'"));
@@ -10811,30 +10878,7 @@ EOS
   unlink($log_file);
 }
 
-sub get_xfer_failure {
-  my $db_file = shift;
-  my $where = shift;
-
-  my $sql = "SELECT user, ip_addr, xfer_status, xfer_failure, xfer_path FROM ftpsessions";
-  if ($where) {
-    $sql .= " WHERE $where";
-  }
-  $sql .= " LIMIT 1";
-
-  my $cmd = "sqlite3 $db_file \"$sql\"";
-
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
-  }
-
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
-}
-
-sub sql_sqllog_var_xfer_failure_none {
+sub sql_sqllog_var_xfer_status_success_upload {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -10881,7 +10925,6 @@ CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
   xfer_status TEXT,
-  xfer_failure TEXT,
   xfer_path TEXT
 );
 EOS
@@ -10906,12 +10949,6 @@ EOS
   }
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $test_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $test_file: $!");
-  }
 
   my $config = {
     PidFile => $pid_file,
@@ -10933,9 +10970,9 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'xfer_reason FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_failure, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%{transfer-failure}\', \'%f\')"',
-        SQLLog => 'ERR_RETR,RETR xfer_reason',
-      },
+        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
+        SQLLog => 'ERR_STOR,STOR xfer_status',
+      },
     },
   };
 
@@ -10959,14 +10996,14 @@ EOS
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
 
-      my $conn = $client->retr_raw('test.txt');
+      my $conn = $client->stor_raw('test.txt');
       unless ($conn) {
-        die("RETR test.txt failed: " . $client->response_code() . " " .
+        die("STOR test.txt failed: " . $client->response_code() . " " .
           $client->response_msg());
       }
 
-      my $buf;
-      $conn->read($buf, 8192, 25);
+      my $buf = "Hello, World!\n";
+      $conn->write($buf, length($buf), 25);
       eval { $conn->close() };
 
       my $resp_code = $client->response_code();
@@ -11006,7 +11043,7 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $xfer_status, $xfer_failure, $xfer_path) = get_xfer_failure($db_file, "user = \'$user\'");
+  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
 
   my $expected;
 
@@ -11022,9 +11059,10 @@ EOS
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
-  $expected = '-';
-  $self->assert($expected eq $xfer_failure,
-    test_msg("Expected transfer failure '$expected', got '$xfer_failure'"));
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $test_file = '/private' . $test_file;
+  }
 
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
@@ -11033,7 +11071,7 @@ EOS
   unlink($log_file);
 }
 
-sub sql_sqllog_var_xfer_failure_reason {
+sub sql_sqllog_var_xfer_status_cancelled {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -11080,7 +11118,6 @@ CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
   xfer_status TEXT,
-  xfer_failure TEXT,
   xfer_path TEXT
 );
 EOS
@@ -11106,8 +11143,11 @@ EOS
 
   my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
   if (open(my $fh, "> $test_file")) {
-    print $fh "ABCD" x 1024;
-    close($fh);
+    print $fh "ABCDefgh" x 32768;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
 
   } else {
     die("Can't open $test_file: $!");
@@ -11123,9 +11163,6 @@ EOS
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
 
-    # This is used to tickle the "failed" transfer status
-    MaxRetrieveFileSize => '12 B',
-
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
@@ -11136,8 +11173,8 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'xfer_reason FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_failure, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%{transfer-failure}\', \'%f\')"',
-        SQLLog => 'ERR_RETR,RETR xfer_reason',
+        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
+        SQLLog => 'ERR_RETR,RETR xfer_status',
       },
     },
   };
@@ -11169,19 +11206,13 @@ EOS
       }
 
       my $buf;
-      $conn->read($buf, 8192, 25);
-      eval { $conn->close() };
+      $conn->read($buf, 4, 25);
+      eval { $conn->abort() };
 
       my $resp_code = $client->response_code();
       my $resp_msg = $client->response_msg();
 
-      my $expected = 426;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected response code $expected, got $resp_code"));
-
-      $expected = 'Transfer aborted';
-      $self->assert(qr/$expected/, $resp_msg,
-        test_msg("Expected response message '$expected', got '$resp_msg'"));
+      $self->assert_transfer_ok($resp_code, $resp_msg, 1);
 
       $client->quit();
     };
@@ -11215,7 +11246,7 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $xfer_status, $xfer_failure, $xfer_path) = get_xfer_failure($db_file, "user = \'$user\'");
+  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
 
   my $expected;
 
@@ -11227,13 +11258,14 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = 'failed';
+  $expected = 'cancelled';
   $self->assert($expected eq $xfer_status,
     test_msg("Expected transfer status '$expected', got '$xfer_status'"));
 
-  $expected = 'Operation not permitted';
-  $self->assert($expected eq $xfer_failure,
-    test_msg("Expected transfer failure '$expected', got '$xfer_failure'"));
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $test_file = '/private' . $test_file;
+  }
 
   $expected = $test_file;
   $self->assert($expected eq $xfer_path,
@@ -11242,7 +11274,7 @@ EOS
   unlink($log_file);
 }
 
-sub sql_sqlshowinfo_pass_failed_bug3782 {
+sub sql_sqllog_var_xfer_status_failed {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -11252,6 +11284,9 @@ sub sql_sqlshowinfo_pass_failed_bug3782 {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -11259,6 +11294,22 @@ sub sql_sqlshowinfo_pass_failed_bug3782 {
   my $uid = 500;
   my $gid = 500;
 
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -11266,31 +11317,1841 @@ sub sql_sqlshowinfo_pass_failed_bug3782 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT,
-  secure_only INTEGER
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  xfer_status TEXT,
+  xfer_path TEXT
 );
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell, secure_only) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', 1);
+EOS
 
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
-);
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
 
-CREATE TABLE login_responses (
-  secure_only INTEGER,
-  response TEXT
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 32768;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20 command:20 netio:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    # This is used to tickle the "failed" transfer status
+    MaxRetrieveFileSize => '12 B',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
+        SQLLog => 'ERR_RETR,RETR xfer_status',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->retr_raw('test.txt');
+      unless ($conn) {
+        die("RETR test.txt failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 4, 25);
+      eval { $conn->close() };
+
+      # Make the control connection go away uncleanly
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected user '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+
+  $expected = 'failed';
+  $self->assert($expected eq $xfer_status,
+    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $test_file = '/private' . $test_file;
+  }
+
+  $expected = $test_file;
+  $self->assert($expected eq $xfer_path,
+    test_msg("Expected file path '$expected', got '$xfer_path'"));
+
+  unlink($log_file);
+}
+
+sub sql_sqllog_var_xfer_status_timeout {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  xfer_status TEXT,
+  xfer_path TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 32768;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $timeout_stalled = 1;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20 command:20 netio:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    # This is used to tickle the "timeout" transfer status
+    TimeoutStalled => $timeout_stalled,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
+        SQLLog => 'ERR_RETR,RETR,EXIT xfer_status',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      $client->type('ascii');
+
+      my $conn = $client->retr_raw('test.txt');
+      unless ($conn) {
+        die("RETR test.xt failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      sleep($timeout_stalled + 1);
+
+      my $buf;
+      $conn->read($buf, 32, 25);
+      sleep($timeout_stalled);
+      eval { $conn->close() };
+
+      eval { $client->noop() };
+      unless ($@) {
+        die("NOOP succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      # Perl's Net::Cmd module uses a very non-standard 599 code to
+      # indicate that the connection is closed
+      $expected = 599;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected $expected, got $resp_code"));
+
+      $expected = "Connection closed";
+      $self->assert($expected eq $resp_msg,
+        test_msg("Expected '$expected', got '$resp_msg'"));
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected user '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+
+  $expected = '^(timeout|failed)$';
+  $self->assert(qr/$expected/, $xfer_status,
+    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+
+  if ($xfer_status eq 'timeout') {
+    if ($^O eq 'darwin') {
+      # MacOSX-specific hack
+      $test_file = '/private' . $test_file;
+    }
+
+    $expected = $test_file;
+    $self->assert($expected eq $xfer_path,
+      test_msg("Expected file path '$expected', got '$xfer_path'"));
+  }
+
+  unlink($log_file);
+}
+
+sub sql_sqllog_var_xfer_status_filtered {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  xfer_status TEXT,
+  xfer_path TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCDefgh" x 32768;
+
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20 command:20 netio:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    PathDenyFilter => '^.*\.csv$',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'xfer_status FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%f\')"',
+        SQLLog => 'ERR_STOR,STOR,EXIT xfer_status',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      $client->type('ascii');
+
+      my $conn = $client->stor_raw('test.csv');
+      if ($conn) {
+        die("RETR test.csv succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $client->quit();
+
+      my $expected = 550;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "test.csv: Forbidden filename";
+      $self->assert($expected eq $resp_msg,
+        "Expected '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $xfer_status, $xfer_path) = get_xfer_status($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected user '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+
+  $expected = '^(timeout|failed)$';
+  $self->assert(qr/$expected/, $xfer_status,
+    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+
+  if ($xfer_status eq 'timeout') {
+    if ($^O eq 'darwin') {
+      # MacOSX-specific hack
+      $test_file = '/private' . $test_file;
+    }
+
+    $expected = $test_file;
+    $self->assert($expected eq $xfer_path,
+      test_msg("Expected file path '$expected', got '$xfer_path'"));
+  }
+
+  unlink($log_file);
+}
+
+sub get_xfer_failure {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, ip_addr, xfer_status, xfer_failure, xfer_path FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+  $sql .= " LIMIT 1";
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub sql_sqllog_var_xfer_failure_none {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  xfer_status TEXT,
+  xfer_failure TEXT,
+  xfer_path TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20 command:20 netio:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'xfer_reason FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_failure, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%{transfer-failure}\', \'%f\')"',
+        SQLLog => 'ERR_RETR,RETR xfer_reason',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->retr_raw('test.txt');
+      unless ($conn) {
+        die("RETR test.txt failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $xfer_status, $xfer_failure, $xfer_path) = get_xfer_failure($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected user '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+
+  $expected = 'success';
+  $self->assert($expected eq $xfer_status,
+    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+
+  $expected = '-';
+  $self->assert($expected eq $xfer_failure,
+    test_msg("Expected transfer failure '$expected', got '$xfer_failure'"));
+
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $test_file = '/private' . $test_file;
+  }
+
+  $expected = $test_file;
+  $self->assert($expected eq $xfer_path,
+    test_msg("Expected file path '$expected', got '$xfer_path'"));
+
+  unlink($log_file);
+}
+
+sub sql_sqllog_var_xfer_failure_reason {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  xfer_status TEXT,
+  xfer_failure TEXT,
+  xfer_path TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "ABCD" x 1024;
+    close($fh);
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20 command:20 netio:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    # This is used to tickle the "failed" transfer status
+    MaxRetrieveFileSize => '12 B',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'xfer_reason FREEFORM "INSERT INTO ftpsessions (user, ip_addr, xfer_status, xfer_failure, xfer_path) VALUES (\'%u\', \'%L\', \'%{transfer-status}\', \'%{transfer-failure}\', \'%f\')"',
+        SQLLog => 'ERR_RETR,RETR xfer_reason',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+
+      my $conn = $client->retr_raw('test.txt');
+      unless ($conn) {
+        die("RETR test.txt failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 25);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected = 426;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'Transfer aborted';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $xfer_status, $xfer_failure, $xfer_path) = get_xfer_failure($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected user '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+
+  $expected = 'failed';
+  $self->assert($expected eq $xfer_status,
+    test_msg("Expected transfer status '$expected', got '$xfer_status'"));
+
+  $expected = 'Operation not permitted';
+  $self->assert($expected eq $xfer_failure,
+    test_msg("Expected transfer failure '$expected', got '$xfer_failure'"));
+
+  if ($^O eq 'darwin') {
+    # MacOSX-specific hack
+    $test_file = '/private' . $test_file;
+  }
+
+  $expected = $test_file;
+  $self->assert($expected eq $xfer_path,
+    test_msg("Expected file path '$expected', got '$xfer_path'"));
+
+  unlink($log_file);
+}
+
+sub sql_sqlshowinfo_pass_failed_bug3782 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  secure_only INTEGER
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell, secure_only) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', 1);
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE login_responses (
+  secure_only INTEGER,
+  response TEXT
+);
+
+INSERT INTO login_responses (secure_only, response) VALUES (0, "Login failed.");
+INSERT INTO login_responses (secure_only, response) VALUES (1, "You may not log into this account via FTP. Please use SFTP instead.");
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'response:20 sql:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => [
+        'AuthOrder mod_sql.c',
+
+        'SQLAuthenticate users groups',
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 100',
+
+        'SQLUserWhereClause "secure_only != 1"',
+        'SQLNamedQuery login_failure SELECT "response FROM users u, login_responses lr WHERE u.userid = \'%U\' AND u.secure_only = lr.secure_only"',
+
+
+        # Configure the equivalent of a multiline DisplayLogin file
+        # using the SQLShowInfo directive
+
+        'SQLShowInfo ERR_PASS 530 "Sorry, %U"',
+        'SQLShowInfo ERR_PASS 530 "%{login_failure}"',
+        'SQLShowInfo ERR_PASS 530 " "',
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 4;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response '$expected', got '$resp_msgs->[0]'"));
+
+      $expected = " Sorry, $user";
+      $self->assert($expected eq $resp_msgs->[1],
+        test_msg("Expected response '$expected', got '$resp_msgs->[1]'"));
+
+      $expected = " You may not log into this account via FTP. Please use SFTP instead.";
+      $self->assert($expected eq $resp_msgs->[2],
+        test_msg("Expected response '$expected', got '$resp_msgs->[2]'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub sql_sqllog_preauth_var_U_bug3822 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  resp_mesg TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, ip_addr, resp_mesg) VALUES (\'%U\', \'%L\', \'%S\')"',
+        SQLLog => 'ERR_PWD info',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->pwd() };
+      unless ($@) {
+        die("PWD succeeded unexpectedly");
+      }
+
+      $client->login($user, $passwd);
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $resp_mesg) = get_resp_mesgs($db_file,
+    "user = '-'", "user, ip_addr, resp_mesg");
+
+  my $expected;
+
+  $expected = '-';
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  $expected = 'Please login with USER and PASS';
+  $self->assert($expected eq $resp_mesg,
+    test_msg("Expected '$expected', got '$resp_mesg'"));
+
+  unlink($log_file);
+}
+
+sub sql_sqllog_preauth_var_u_bug3822 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  resp_mesg TEXT
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, ip_addr, resp_mesg) VALUES (\'%u\', \'%L\', \'%S\')"',
+        SQLLog => 'ERR_PWD info',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->pwd() };
+      unless ($@) {
+        die("PWD succeeded unexpectedly");
+      }
+
+      $client->login($user, $passwd);
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  my ($login, $ip_addr, $resp_mesg) = get_resp_mesgs($db_file,
+    "user = '-'", "user, ip_addr, resp_mesg");
+
+  my $expected;
+
+  $expected = '-';
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  $expected = 'Please login with USER and PASS';
+  $self->assert($expected eq $resp_mesg,
+    test_msg("Expected '$expected', got '$resp_mesg'"));
+
+  unlink($log_file);
+}
+
+sub sql_sqlite_maxhostsperuser {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT
+);
+CREATE INDEX i_users_userid ON users.userid;
+CREATE INDEX i_users_uid ON users.uid;
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+CREATE INDEX i_groups_gid ON groups.gid;
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE user_hosts (
+  session_id TEXT,
+  userid TEXT,
+  host TEXT
+);
+CREATE INDEX i_user_hosts_userid ON user_hosts.userid;
+INSERT INTO user_hosts (session_id, userid, host) VALUES ('abc', '$user', '127.0.0.1');
+
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+  build_db($cmd, $db_script);
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'response:20 sql:20',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => [
+        'AuthOrder mod_sql.c',
+
+        'SQLAuthenticate users groups',
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 100',
+
+        # Here, the MaxHostsPerUser limit is 3; it has to be hardcoded into
+        # the SQL query, unfortunately.  (Or maybe use a config file variable?)
+        #
+        # The LEFT JOIN is necessary for dealing with the case where the
+        # user_hosts table has no matching rows for the user logging in.
+        'SQLNamedQuery get-user-by-name SELECT "u.userid, u.passwd, u.uid, u.gid, u.homedir, u.shell, COUNT(uh.session_id) AS count FROM users u JOIN user_hosts uh ON u.userid = \"%U\" GROUP BY u.userid HAVING (count >= 0 AND count < 4)"',
+        'SQLNamedQuery get-user-by-id SELECT "userid, passwd, uid, gid, homedir, shell FROM users WHERE uid = %{0}"',
+        'SQLUserInfo custom:/get-user-by-name/get-user-by-id',
+
+        'SQLNamedQuery add-user-host FREEFORM "INSERT INTO user_hosts (session_id, userid, host) VALUES (\'%{env:UNIQUE_ID}\', \'%u\', \'%a\')"',
+        'SQLLog PASS add-user-host',
+
+        'SQLNamedQuery remove-user-host FREEFORM "DELETE FROM user_hosts WHERE session_id = \"%{env:UNIQUE_ID}\"',
+        'SQLLog EXIT remove-user-host',
+
+        'SQLShowInfo ERR_PASS 530 "Sorry, the maximum number of hosts for this user are already connected."',
+      ],
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client1->login($user, $passwd);
+
+      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client2->login($user, $passwd);
+
+      my $client3 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client3->login($user, $passwd);
+
+      my $client4 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client4->login($user, $passwd) };
+      unless ($@) {
+        die("Fourth login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client4->response_code();
+      my $resp_msgs = $client4->response_msgs();
+
+      my $expected = 530;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected response '$expected', got '$resp_msgs->[0]'"));
+
+      $expected = "Sorry, the maximum number of hosts for this user are already connected.";
+      $self->assert($expected eq $resp_msgs->[1],
+        test_msg("Expected response '$expected', got '$resp_msgs->[1]'"));
+
+      $client4->quit();
+
+      $client3->quit();
+
+      $client4 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client4->login($user, $passwd);
+      $client4->quit();
+
+      $client2->quit();
+      $client1->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub get_logins {
+  my $db_file = shift;
+  my $where = shift;
+
+  my $sql = "SELECT user, ip_addr, ts FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
+  }
+
+  my $cmd = "sqlite3 $db_file \"$sql\"";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
+}
+
+sub sql_sqllog_var_micros_ts_bug3889 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/sqlite.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  # Build up sqlite3 command to create users, groups tables and populate them
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  ts TEXT
 );
-
-INSERT INTO login_responses (secure_only, response) VALUES (0, "Login failed.");
-INSERT INTO login_responses (secure_only, response) VALUES (1, "You may not log into this account via FTP. Please use SFTP instead.");
 EOS
 
     unless (close($fh)) {
@@ -11312,39 +13173,37 @@ EOS
     }
   }
 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'response:20 sql:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'AuthOrder mod_sql.c',
-
-        'SQLAuthenticate users groups',
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 100',
-
-        'SQLUserWhereClause "secure_only != 1"',
-        'SQLNamedQuery login_failure SELECT "response FROM users u, login_responses lr WHERE u.userid = \'%U\' AND u.secure_only = lr.secure_only"',
-
-
-        # Configure the equivalent of a multiline DisplayLogin file
-        # using the SQLShowInfo directive
-
-        'SQLShowInfo ERR_PASS 530 "Sorry, %U"',
-        'SQLShowInfo ERR_PASS 530 "%{login_failure}"',
-        'SQLShowInfo ERR_PASS 530 " "',
-      ],
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'login FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{microsecs}\')"',
+        SQLLog => 'PASS login',
+      },
     },
   };
 
@@ -11366,33 +13225,7 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      eval { $client->login($user, $passwd) };
-      unless ($@) {
-        die("Login succeeded unexpectedly");
-      }
-
-      my $resp_msgs = $client->response_msgs();
-      my $nmsgs = scalar(@$resp_msgs);
-
-      my $expected;
-
-      $expected = 4;
-      $self->assert($expected == $nmsgs,
-        test_msg("Expected $expected, got $nmsgs")); 
-
-      $expected = "Login incorrect.";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected response '$expected', got '$resp_msgs->[0]'"));
-
-      $expected = " Sorry, $user";
-      $self->assert($expected eq $resp_msgs->[1],
-        test_msg("Expected response '$expected', got '$resp_msgs->[1]'"));
-
-      $expected = " You may not log into this account via FTP. Please use SFTP instead.";
-      $self->assert($expected eq $resp_msgs->[2],
-        test_msg("Expected response '$expected', got '$resp_msgs->[2]'"));
-
+      $client->login($user, $passwd);
       $client->quit();
     };
 
@@ -11425,10 +13258,26 @@ EOS
     die($ex);
   }
 
+  my ($login, $ip_addr, $ts) = get_logins($db_file, "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected login user '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+
+  $expected = '\d{6}';
+  $self->assert(qr/$expected/, $ts,
+    test_msg("Expected timestamp '$expected', got '$ts'"));
+
   unlink($log_file);
 }
 
-sub sql_sqllog_preauth_var_U_bug3822 {
+sub sql_sqllog_var_millis_ts_bug3889 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -11474,7 +13323,7 @@ sub sql_sqllog_preauth_var_U_bug3822 {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  resp_mesg TEXT
+  ts TEXT
 );
 EOS
 
@@ -11497,6 +13346,16 @@ EOS
     }
   }
 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -11513,10 +13372,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
+        SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, ip_addr, resp_mesg) VALUES (\'%U\', \'%L\', \'%S\')"',
-        SQLLog => 'ERR_PWD info',
+        SQLNamedQuery => 'login FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{millisecs}\')"',
+        SQLLog => 'PASS login',
       },
     },
   };
@@ -11539,11 +13398,6 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->pwd() };
-      unless ($@) {
-        die("PWD succeeded unexpectedly");
-      }
-
       $client->login($user, $passwd);
       $client->quit();
     };
@@ -11577,27 +13431,26 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $resp_mesg) = get_resp_mesgs($db_file,
-    "user = '-'", "user, ip_addr, resp_mesg");
+  my ($login, $ip_addr, $ts) = get_logins($db_file, "user = \'$user\'");
 
   my $expected;
 
-  $expected = '-';
+  $expected = $user;
   $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+    test_msg("Expected login user '$expected', got '$login'"));
 
   $expected = '127.0.0.1';
   $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = 'Please login with USER and PASS';
-  $self->assert($expected eq $resp_mesg,
-    test_msg("Expected '$expected', got '$resp_mesg'"));
+  $expected = '\d{3}';
+  $self->assert(qr/$expected/, $ts,
+    test_msg("Expected timestamp '$expected', got '$ts'"));
 
   unlink($log_file);
 }
 
-sub sql_sqllog_preauth_var_u_bug3822 {
+sub sql_sqllog_var_iso8601_ts_bug3889 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -11643,7 +13496,7 @@ sub sql_sqllog_preauth_var_u_bug3822 {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  resp_mesg TEXT
+  ts TEXT
 );
 EOS
 
@@ -11666,6 +13519,16 @@ EOS
     }
   }
 
+  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
+  if (open(my $fh, "> $src_file")) {
+    close($fh);
+
+  } else {
+    die("Can't open $src_file: $!");
+  }
+
+  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
@@ -11682,10 +13545,10 @@ EOS
       'mod_sql.c' => {
         SQLEngine => 'log',
         SQLBackend => 'sqlite3',
-        SQLConnectInfo => "$db_file foo bar PERCONNECTION",
+        SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'info FREEFORM "INSERT INTO ftpsessions (user, ip_addr, resp_mesg) VALUES (\'%u\', \'%L\', \'%S\')"',
-        SQLLog => 'ERR_PWD info',
+        SQLNamedQuery => 'login FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{iso8601}\')"',
+        SQLLog => 'PASS login',
       },
     },
   };
@@ -11708,11 +13571,6 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->pwd() };
-      unless ($@) {
-        die("PWD succeeded unexpectedly");
-      }
-
       $client->login($user, $passwd);
       $client->quit();
     };
@@ -11746,27 +13604,26 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $resp_mesg) = get_resp_mesgs($db_file,
-    "user = '-'", "user, ip_addr, resp_mesg");
+  my ($login, $ip_addr, $ts) = get_logins($db_file, "user = \'$user\'");
 
   my $expected;
 
-  $expected = '-';
+  $expected = $user;
   $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+    test_msg("Expected login user '$expected', got '$login'"));
 
   $expected = '127.0.0.1';
   $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+    test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = 'Please login with USER and PASS';
-  $self->assert($expected eq $resp_mesg,
-    test_msg("Expected '$expected', got '$resp_mesg'"));
+  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2},\d{3}';
+  $self->assert(qr/$expected/, $ts,
+    test_msg("Expected timestamp '$expected', got '$ts'"));
 
   unlink($log_file);
 }
 
-sub sql_sqlite_maxhostsperuser {
+sub sql_sqllogonevent_bug3893 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -11776,6 +13633,9 @@ sub sql_sqlite_maxhostsperuser {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -11783,6 +13643,22 @@ sub sql_sqlite_maxhostsperuser {
   my $uid = 500;
   my $gid = 500;
 
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -11790,34 +13666,11 @@ sub sql_sqlite_maxhostsperuser {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT,
-  shell TEXT
-);
-CREATE INDEX i_users_userid ON users.userid;
-CREATE INDEX i_users_uid ON users.uid;
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
-);
-CREATE INDEX i_groups_gid ON groups.gid;
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
-
-CREATE TABLE user_hosts (
-  session_id TEXT,
-  userid TEXT,
-  host TEXT
+CREATE TABLE ftpsessions (
+  user TEXT,
+  ip_addr TEXT,
+  timestamp TEXT
 );
-CREATE INDEX i_user_hosts_userid ON user_hosts.userid;
-INSERT INTO user_hosts (session_id, userid, host) VALUES ('abc', '$user', '127.0.0.1');
-
 EOS
 
     unless (close($fh)) {
@@ -11839,45 +13692,33 @@ EOS
     }
   }
 
+  my $max_login_attempts = 2;
+
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'response:20 sql:20',
-
-    IfModules => {
-      'mod_delay.c' => {
-        DelayEngine => 'off',
-      },
-
-      'mod_sql.c' => [
-        'AuthOrder mod_sql.c',
-
-        'SQLAuthenticate users groups',
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 100',
-
-        # Here, the MaxHostsPerUser limit is 3; it has to be hardcoded into
-        # the SQL query, unfortunately.  (Or maybe use a config file variable?)
-        #
-        # The LEFT JOIN is necessary for dealing with the case where the
-        # user_hosts table has no matching rows for the user logging in.
-        'SQLNamedQuery get-user-by-name SELECT "u.userid, u.passwd, u.uid, u.gid, u.homedir, u.shell, COUNT(uh.session_id) AS count FROM users u JOIN user_hosts uh ON u.userid = \"%U\" GROUP BY u.userid HAVING (count >= 0 AND count < 4)"',
-        'SQLNamedQuery get-user-by-id SELECT "userid, passwd, uid, gid, homedir, shell FROM users WHERE uid = %{0}"',
-        'SQLUserInfo custom:/get-user-by-name/get-user-by-id',
+    Trace => 'event:10 sql:20',
 
-        'SQLNamedQuery add-user-host FREEFORM "INSERT INTO user_hosts (session_id, userid, host) VALUES (\'%{env:UNIQUE_ID}\', \'%u\', \'%a\')"',
-        'SQLLog PASS add-user-host',
+    AuthOrder => 'mod_auth_file.c',
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    MaxLoginAttempts => $max_login_attempts,
 
-        'SQLNamedQuery remove-user-host FREEFORM "DELETE FROM user_hosts WHERE session_id = \"%{env:UNIQUE_ID}\"',
-        'SQLLog EXIT remove-user-host',
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
 
-        'SQLShowInfo ERR_PASS 530 "Sorry, the maximum number of hosts for this user are already connected."',
-      ],
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLNamedQuery => 'max_logins_exceeded FREEFORM "INSERT INTO ftpsessions (user, ip_addr, timestamp) VALUES (\'%U\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
+        SQLLogOnEvent => 'MaxLoginAttempts max_logins_exceeded',
+      },
     },
   };
 
@@ -11898,46 +13739,16 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
-      my $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client1->login($user, $passwd);
-
-      my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client2->login($user, $passwd);
-
-      my $client3 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client3->login($user, $passwd);
-
-      my $client4 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client4->login($user, $passwd) };
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->login($user, 'badpassword') };
       unless ($@) {
-        die("Fourth login succeeded unexpectedly");
+        die("Login succeeded unexpectedly");
       }
 
-      my $resp_code = $client4->response_code();
-      my $resp_msgs = $client4->response_msgs();
-
-      my $expected = 530;
-      $self->assert($expected == $resp_code,
-        test_msg("Expected response code $expected, got $resp_code"));
-
-      $expected = "Login incorrect.";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected response '$expected', got '$resp_msgs->[0]'"));
-
-      $expected = "Sorry, the maximum number of hosts for this user are already connected.";
-      $self->assert($expected eq $resp_msgs->[1],
-        test_msg("Expected response '$expected', got '$resp_msgs->[1]'"));
-
-      $client4->quit();
-
-      $client3->quit();
-
-      $client4 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      $client4->login($user, $passwd);
-      $client4->quit();
-
-      $client2->quit();
-      $client1->quit();
+      eval { $client->login($user, 'badpassword') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
     };
 
     if ($@) {
@@ -11969,14 +13780,31 @@ EOS
     die($ex);
   }
 
+  my ($login, $ip_addr, $timestamp) = get_sessions($db_file,
+    "user = \'$user\'");
+
+  my $expected;
+
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
+
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
+
+  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}';
+  $self->assert(qr/$expected/, $timestamp,
+    test_msg("Expected '$expected', got '$timestamp'"));
+
   unlink($log_file);
 }
 
-sub get_logins {
+sub get_session_with_primary_key {
   my $db_file = shift;
   my $where = shift;
 
-  my $sql = "SELECT user, ip_addr, ts FROM ftpsessions";
+  my $sql = "SELECT name, ip_addr, primary_key FROM sessions";
   if ($where) {
     $sql .= " WHERE $where";
   }
@@ -11994,7 +13822,7 @@ sub get_logins {
   return split(/\|/, $res);
 }
 
-sub sql_sqllog_var_micros_ts_bug3889 {
+sub sql_userprimarykey_bug3864 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12004,9 +13832,6 @@ sub sql_sqllog_var_micros_ts_bug3889 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -12014,22 +13839,6 @@ sub sql_sqllog_var_micros_ts_bug3889 {
   my $uid = 500;
   my $gid = 500;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -12037,10 +13846,27 @@ sub sql_sqllog_var_micros_ts_bug3889 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE sessions (
+  name TEXT,
   ip_addr TEXT,
-  ts TEXT
+  primary_key INTEGER
 );
 EOS
 
@@ -12063,23 +13889,12 @@ EOS
     }
   }
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
@@ -12087,12 +13902,16 @@ EOS
       },
 
       'mod_sql.c' => {
-        SQLEngine => 'log',
+        SQLAuthTypes => 'plaintext',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'login FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{microsecs}\')"',
-        SQLLog => 'PASS login',
+        SQLMinID => 200,
+
+        SQLUserPrimaryKey => 'uid',
+
+        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%u\', \'%L\', %{note:sql.user-primary-key})"',
+        SQLLog => 'PASS session_start',
       },
     },
   };
@@ -12148,26 +13967,39 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $ts) = get_logins($db_file, "user = \'$user\'");
+  eval {
+    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
+      "name = \'$user\'");
 
-  my $expected;
+    my $expected;
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected login user '$expected', got '$login'"));
+    $expected = $user;
+    $self->assert($expected eq $name,
+      test_msg("Expected name '$expected', got '$name'"));
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = '\d{6}';
-  $self->assert(qr/$expected/, $ts,
-    test_msg("Expected timestamp '$expected', got '$ts'"));
+    $expected = $uid;
+    $self->assert($expected == $primary_key,
+      test_msg("Expected primary key $expected, got $primary_key"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
 
   unlink($log_file);
 }
 
-sub sql_sqllog_var_millis_ts_bug3889 {
+sub sql_userprimarykey_custom_bug3864 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12177,9 +14009,6 @@ sub sql_sqllog_var_millis_ts_bug3889 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -12187,22 +14016,6 @@ sub sql_sqllog_var_millis_ts_bug3889 {
   my $uid = 500;
   my $gid = 500;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -12210,10 +14023,28 @@ sub sql_sqllog_var_millis_ts_bug3889 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT,
+  primary_key INTEGER
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell, primary_key) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', $uid);
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE sessions (
+  name TEXT,
   ip_addr TEXT,
-  ts TEXT
+  primary_key INTEGER
 );
 EOS
 
@@ -12236,37 +14067,31 @@ EOS
     }
   }
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_sql.c' => [
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 200',
 
-    IfModules => {
-      'mod_delay.c' => {
-        DelayEngine => 'off',
-      },
+        'SQLNamedQuery get-user-primary-key SELECT "primary_key FROM users WHERE userid = \'%{0}\'"',
+        'SQLUserPrimaryKey custom:/get-user-primary-key',
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'login FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{millisecs}\')"',
-        SQLLog => 'PASS login',
-      },
+        'SQLNamedQuery session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%u\', \'%L\', %{note:sql.user-primary-key})"',
+        'SQLLog PASS session_start',
+      ],
     },
   };
 
@@ -12321,26 +14146,39 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $ts) = get_logins($db_file, "user = \'$user\'");
+  eval {
+    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
+      "name = \'$user\'");
 
-  my $expected;
+    my $expected;
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected login user '$expected', got '$login'"));
+    $expected = $user;
+    $self->assert($expected eq $name,
+      test_msg("Expected name '$expected', got '$name'"));
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = '\d{3}';
-  $self->assert(qr/$expected/, $ts,
-    test_msg("Expected timestamp '$expected', got '$ts'"));
+    $expected = $uid;
+    $self->assert($expected == $primary_key,
+      test_msg("Expected primary key $expected, got $primary_key"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
 
   unlink($log_file);
 }
 
-sub sql_sqllog_var_iso8601_ts_bug3889 {
+sub sql_groupprimarykey_bug3864 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12350,9 +14188,6 @@ sub sql_sqllog_var_iso8601_ts_bug3889 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -12360,22 +14195,6 @@ sub sql_sqllog_var_iso8601_ts_bug3889 {
   my $uid = 500;
   my $gid = 500;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -12383,10 +14202,27 @@ sub sql_sqllog_var_iso8601_ts_bug3889 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+
+CREATE TABLE sessions (
+  name TEXT,
   ip_addr TEXT,
-  ts TEXT
+  primary_key INTEGER
 );
 EOS
 
@@ -12409,23 +14245,12 @@ EOS
     }
   }
 
-  my $src_file = File::Spec->rel2abs("$tmpdir/test.txt");
-  if (open(my $fh, "> $src_file")) {
-    close($fh);
-
-  } else {
-    die("Can't open $src_file: $!");
-  }
-
-  my $dst_file = File::Spec->rel2abs("$tmpdir/foo.txt");
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    TraceLog => $log_file,
+    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
@@ -12433,12 +14258,16 @@ EOS
       },
 
       'mod_sql.c' => {
-        SQLEngine => 'log',
+        SQLAuthTypes => 'plaintext',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'login FREEFORM "INSERT INTO ftpsessions (user, ip_addr, ts) VALUES (\'%u\', \'%L\', \'%{iso8601}\')"',
-        SQLLog => 'PASS login',
+        SQLMinID => 200,
+
+        SQLGroupPrimaryKey => 'gid',
+
+        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%g\', \'%L\', %{note:sql.group-primary-key})"',
+        SQLLog => 'PASS session_start',
       },
     },
   };
@@ -12494,26 +14323,39 @@ EOS
     die($ex);
   }
 
-  my ($login, $ip_addr, $ts) = get_logins($db_file, "user = \'$user\'");
+  eval {
+    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
+      "name = \'$group\'");
 
-  my $expected;
+    my $expected;
 
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected login user '$expected', got '$login'"));
+    $expected = $group;
+    $self->assert($expected eq $name,
+      test_msg("Expected name '$expected', got '$name'"));
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected IP address '$expected', got '$ip_addr'"));
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2},\d{3}';
-  $self->assert(qr/$expected/, $ts,
-    test_msg("Expected timestamp '$expected', got '$ts'"));
+    $expected = $uid;
+    $self->assert($expected == $primary_key,
+      test_msg("Expected primary key $expected, got $primary_key"));
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
 
   unlink($log_file);
 }
 
-sub sql_sqllogonevent_bug3893 {
+sub sql_groupprimarykey_custom_bug3864 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12523,9 +14365,6 @@ sub sql_sqllogonevent_bug3893 {
 
   my $log_file = test_get_logfile();
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
-
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -12533,22 +14372,6 @@ sub sql_sqllogonevent_bug3893 {
   my $uid = 500;
   my $gid = 500;
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
-    }
-
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
-    }
-  }
-
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
-
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -12556,10 +14379,28 @@ sub sql_sqllogonevent_bug3893 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE ftpsessions (
-  user TEXT,
+CREATE TABLE users (
+  userid TEXT,
+  passwd TEXT,
+  uid INTEGER,
+  gid INTEGER,
+  homedir TEXT, 
+  shell TEXT
+);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT,
+  primary_key INTEGER
+);
+INSERT INTO groups (groupname, gid, members, primary_key) VALUES ('$group', $gid, '$user', $gid);
+
+CREATE TABLE sessions (
+  name TEXT,
   ip_addr TEXT,
-  timestamp TEXT
+  primary_key INTEGER
 );
 EOS
 
@@ -12582,33 +14423,31 @@ EOS
     }
   }
 
-  my $max_login_attempts = 2;
-
   my $config = {
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'event:10 sql:20',
-
-    AuthOrder => 'mod_auth_file.c',
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
-    MaxLoginAttempts => $max_login_attempts,
+    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => {
-        SQLEngine => 'log',
-        SQLBackend => 'sqlite3',
-        SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLNamedQuery => 'max_logins_exceeded FREEFORM "INSERT INTO ftpsessions (user, ip_addr, timestamp) VALUES (\'%U\', \'%L\', \'%{time:%Y-%m-%d %H:%M:%S}\')"',
-        SQLLogOnEvent => 'MaxLoginAttempts max_logins_exceeded',
-      },
+      'mod_sql.c' => [
+        'SQLAuthTypes plaintext',
+        'SQLBackend sqlite3',
+        "SQLConnectInfo $db_file",
+        "SQLLogFile $log_file",
+        'SQLMinID 200',
+
+        'SQLNamedQuery get-group-primary-key SELECT "primary_key from groups WHERE groupname = \'%{0}\'"',
+        'SQLGroupPrimaryKey custom:/get-group-primary-key',
+
+        'SQLNamedQuery session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%g\', \'%L\', %{note:sql.group-primary-key})"',
+        'SQLLog PASS session_start',
+      ],
     },
   };
 
@@ -12630,15 +14469,8 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-      eval { $client->login($user, 'badpassword') };
-      unless ($@) {
-        die("Login succeeded unexpectedly");
-      }
-
-      eval { $client->login($user, 'badpassword') };
-      unless ($@) {
-        die("Login succeeded unexpectedly");
-      }
+      $client->login($user, $passwd);
+      $client->quit();
     };
 
     if ($@) {
@@ -12667,52 +14499,42 @@ EOS
     test_append_logfile($log_file, $ex);
     unlink($log_file);
 
-    die($ex);
-  }
-
-  my ($login, $ip_addr, $timestamp) = get_sessions($db_file,
-    "user = \'$user\'");
-
-  my $expected;
-
-  $expected = $user;
-  $self->assert($expected eq $login,
-    test_msg("Expected '$expected', got '$login'"));
+    die($ex);
+  }
 
-  $expected = '127.0.0.1';
-  $self->assert($expected eq $ip_addr,
-    test_msg("Expected '$expected', got '$ip_addr'"));
+  eval {
+    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
+      "name = \'$group\'");
 
-  $expected = '\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}';
-  $self->assert(qr/$expected/, $timestamp,
-    test_msg("Expected '$expected', got '$timestamp'"));
+    my $expected;
 
-  unlink($log_file);
-}
+    $expected = $group;
+    $self->assert($expected eq $name,
+      test_msg("Expected name '$expected', got '$name'"));
 
-sub get_session_with_primary_key {
-  my $db_file = shift;
-  my $where = shift;
+    $expected = '127.0.0.1';
+    $self->assert($expected eq $ip_addr,
+      test_msg("Expected IP address '$expected', got '$ip_addr'"));
 
-  my $sql = "SELECT name, ip_addr, primary_key FROM sessions";
-  if ($where) {
-    $sql .= " WHERE $where";
+    $expected = $uid;
+    $self->assert($expected == $primary_key,
+      test_msg("Expected primary key $expected, got $primary_key"));
+  };
+  if ($@) {
+    $ex = $@;
   }
 
-  my $cmd = "sqlite3 $db_file \"$sql\"";
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
 
-  if ($ENV{TEST_VERBOSE}) {
-    print STDERR "Executing sqlite3: $cmd\n";
+    die($ex);
   }
 
-  my $res = join('', `$cmd`);
-  chomp($res);
-
-  # The default sqlite3 delimiter is '|'
-  return split(/\|/, $res);
+  unlink($log_file);
 }
 
-sub sql_userprimarykey_bug3864 {
+sub sql_sqllog_var_basename_bug3987 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12722,6 +14544,9 @@ sub sql_userprimarykey_bug3864 {
 
   my $log_file = test_get_logfile();
 
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/sqlite.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/sqlite.group");
+
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
@@ -12729,6 +14554,24 @@ sub sql_userprimarykey_bug3864 {
   my $uid = 500;
   my $gid = 500;
 
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs($config_file);
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -12736,27 +14579,10 @@ sub sql_userprimarykey_bug3864 {
 
   if (open(my $fh, "> $db_script")) {
     print $fh <<EOS;
-CREATE TABLE users (
-  userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT
-);
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
-);
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
-
-CREATE TABLE sessions (
-  name TEXT,
+CREATE TABLE ftpsessions (
+  user TEXT,
   ip_addr TEXT,
-  primary_key INTEGER
+  dir TEXT
 );
 EOS
 
@@ -12783,8 +14609,9 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
 
     IfModules => {
       'mod_delay.c' => {
@@ -12792,16 +14619,12 @@ EOS
       },
 
       'mod_sql.c' => {
-        SQLAuthTypes => 'plaintext',
+        SQLEngine => 'log',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLMinID => 200,
-
-        SQLUserPrimaryKey => 'uid',
-
-        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%u\', \'%L\', %{note:sql.user-primary-key})"',
-        SQLLog => 'PASS session_start',
+        SQLNamedQuery => 'filename FREEFORM "INSERT INTO ftpsessions (user, ip_addr, dir) VALUES (\'%u\', \'%L\', \'%{basename}\')"',
+        SQLLog => 'RETR filename',
       },
     },
   };
@@ -12825,6 +14648,21 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+
+      my $conn = $client->retr_raw($test_file);
+      unless ($conn) {
+        die("Failed to RETR: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
+
+      my $buf;
+      $conn->read($buf, 8192, 30);
+      eval { $conn->close() };
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+
       $client->quit();
     };
 
@@ -12857,39 +14695,28 @@ EOS
     die($ex);
   }
 
-  eval {
-    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
-      "name = \'$user\'");
-
-    my $expected;
-
-    $expected = $user;
-    $self->assert($expected eq $name,
-      test_msg("Expected name '$expected', got '$name'"));
+  # Note: We are simply re-using the existing get_locations() function here
+  # for convenience/expedience.
+  my ($login, $ip_addr, $name) = get_locations($db_file, "user = \'$user\'");
 
-    $expected = '127.0.0.1';
-    $self->assert($expected eq $ip_addr,
-      test_msg("Expected IP address '$expected', got '$ip_addr'"));
+  my $expected;
 
-    $expected = $uid;
-    $self->assert($expected == $primary_key,
-      test_msg("Expected primary key $expected, got $primary_key"));
-  };
-  if ($@) {
-    $ex = $@;
-  }
+  $expected = $user;
+  $self->assert($expected eq $login,
+    test_msg("Expected '$expected', got '$login'"));
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  $expected = '127.0.0.1';
+  $self->assert($expected eq $ip_addr,
+    test_msg("Expected '$expected', got '$ip_addr'"));
 
-    die($ex);
-  }
+  $expected = 'sqlite.conf';
+  $self->assert($expected eq $name,
+    test_msg("Expected '$expected', got '$name'"));
 
   unlink($log_file);
 }
 
-sub sql_userprimarykey_custom_bug3864 {
+sub sql_user_info_defaulthomedir_bug4083 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -12902,10 +14729,13 @@ sub sql_userprimarykey_custom_bug3864 {
   my $user = 'proftpd';
   my $passwd = 'test';
   my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $home_dir = File::Spec->rel2abs("$tmpdir/$user");
+  mkpath($home_dir);
   my $uid = 500;
   my $gid = 500;
 
+  my $default_home = File::Spec->rel2abs($tmpdir) . '/%u';
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -12917,25 +14747,10 @@ CREATE TABLE users (
   userid TEXT,
   passwd TEXT,
   uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT,
-  primary_key INTEGER
-);
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell, primary_key) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash', $uid);
-
-CREATE TABLE groups (
-  groupname TEXT,
-  gid INTEGER,
-  members TEXT
+  gid INTEGER
 );
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+INSERT INTO users (userid, passwd, uid, gid) VALUES ('$user', '$passwd', $uid, $gid);
 
-CREATE TABLE sessions (
-  name TEXT,
-  ip_addr TEXT,
-  primary_key INTEGER
-);
 EOS
 
     unless (close($fh)) {
@@ -12961,27 +14776,21 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 200',
-
-        'SQLNamedQuery get-user-primary-key SELECT "primary_key FROM users WHERE userid = \'%{0}\'"',
-        'SQLUserPrimaryKey custom:/get-user-primary-key',
-
-        'SQLNamedQuery session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%u\', \'%L\', %{note:sql.user-primary-key})"',
-        'SQLLog PASS session_start',
-      ],
+      'mod_sql.c' => {
+        SQLAuthenticate => 'users',
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLUserInfo => 'users userid passwd uid gid Null Null',
+        SQLDefaultHomedir => $default_home,
+      },
     },
   };
 
@@ -13004,7 +14813,19 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->quit();
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
     };
 
     if ($@) {
@@ -13015,49 +14836,20 @@ EOS
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
-    if ($@) {
-      warn($@);
-      exit 1;
-    }
-
-    exit 0;
-  }
-
-  # Stop server
-  server_stop($pid_file);
-
-  $self->assert_child_ok($pid);
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  eval {
-    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
-      "name = \'$user\'");
-
-    my $expected;
-
-    $expected = $user;
-    $self->assert($expected eq $name,
-      test_msg("Expected name '$expected', got '$name'"));
-
-    $expected = '127.0.0.1';
-    $self->assert($expected eq $ip_addr,
-      test_msg("Expected IP address '$expected', got '$ip_addr'"));
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
 
-    $expected = $uid;
-    $self->assert($expected == $primary_key,
-      test_msg("Expected primary key $expected, got $primary_key"));
-  };
-  if ($@) {
-    $ex = $@;
+    exit 0;
   }
 
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
   if ($ex) {
     test_append_logfile($log_file, $ex);
     unlink($log_file);
@@ -13068,7 +14860,7 @@ EOS
   unlink($log_file);
 }
 
-sub sql_groupprimarykey_bug3864 {
+sub sql_user_info_null_uid_gid_columns {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13085,6 +14877,10 @@ sub sql_groupprimarykey_bug3864 {
   my $uid = 500;
   my $gid = 500;
 
+  my $default_home = '/tmp';
+  my $default_uid = 14;
+  my $default_gid = 28;
+
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -13094,26 +14890,17 @@ sub sql_groupprimarykey_bug3864 {
     print $fh <<EOS;
 CREATE TABLE users (
   userid TEXT,
-  passwd TEXT,
-  uid INTEGER,
-  gid INTEGER,
-  homedir TEXT, 
-  shell TEXT
+  passwd TEXT
 );
-INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$passwd', $uid, $gid, '$home_dir', '/bin/bash');
+INSERT INTO users (userid, passwd) VALUES ('$user', '$passwd');
 
 CREATE TABLE groups (
   groupname TEXT,
   gid INTEGER,
   members TEXT
 );
-INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $default_gid, '$user');
 
-CREATE TABLE sessions (
-  name TEXT,
-  ip_addr TEXT,
-  primary_key INTEGER
-);
 EOS
 
     unless (close($fh)) {
@@ -13139,8 +14926,6 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
@@ -13148,16 +14933,15 @@ EOS
       },
 
       'mod_sql.c' => {
+        SQLAuthenticate => 'users groups',
         SQLAuthTypes => 'plaintext',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLMinID => 200,
-
-        SQLGroupPrimaryKey => 'gid',
-
-        SQLNamedQuery => 'session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%g\', \'%L\', %{note:sql.group-primary-key})"',
-        SQLLog => 'PASS session_start',
+        SQLUserInfo => 'users userid passwd Null Null Null Null',
+        SQLDefaultUID => $default_uid,
+        SQLDefaultGID => $default_gid,
+        SQLDefaultHomedir => $default_home,
       },
     },
   };
@@ -13179,9 +14963,23 @@ EOS
   defined(my $pid = fork()) or die("Can't fork: $!");
   if ($pid) {
     eval {
+      sleep(2);
+
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
-      $client->quit();
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
     };
 
     if ($@) {
@@ -13213,39 +15011,10 @@ EOS
     die($ex);
   }
 
-  eval {
-    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
-      "name = \'$group\'");
-
-    my $expected;
-
-    $expected = $group;
-    $self->assert($expected eq $name,
-      test_msg("Expected name '$expected', got '$name'"));
-
-    $expected = '127.0.0.1';
-    $self->assert($expected eq $ip_addr,
-      test_msg("Expected IP address '$expected', got '$ip_addr'"));
-
-    $expected = $uid;
-    $self->assert($expected == $primary_key,
-      test_msg("Expected primary key $expected, got $primary_key"));
-  };
-  if ($@) {
-    $ex = $@;
-  }
-
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
   unlink($log_file);
 }
 
-sub sql_groupprimarykey_custom_bug3864 {
+sub sql_64_bit_uid_bug4164 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13259,7 +15028,9 @@ sub sql_groupprimarykey_custom_bug3864 {
   my $passwd = 'test';
   my $group = 'ftpd';
   my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
+
+  # See: https://github.com/proftpd/proftpd/issues/74
+  my $uid = 25440237859;
   my $gid = 500;
 
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
@@ -13282,16 +15053,9 @@ INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$user', '$
 CREATE TABLE groups (
   groupname TEXT,
   gid INTEGER,
-  members TEXT,
-  primary_key INTEGER
-);
-INSERT INTO groups (groupname, gid, members, primary_key) VALUES ('$group', $gid, '$user', $gid);
-
-CREATE TABLE sessions (
-  name TEXT,
-  ip_addr TEXT,
-  primary_key INTEGER
+  members TEXT
 );
+INSERT INTO groups (groupname, gid, members) VALUES ('$group', $gid, '$user');
 EOS
 
     unless (close($fh)) {
@@ -13317,27 +15081,19 @@ EOS
     PidFile => $pid_file,
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'sql:20',
 
     IfModules => {
       'mod_delay.c' => {
         DelayEngine => 'off',
       },
 
-      'mod_sql.c' => [
-        'SQLAuthTypes plaintext',
-        'SQLBackend sqlite3',
-        "SQLConnectInfo $db_file",
-        "SQLLogFile $log_file",
-        'SQLMinID 200',
-
-        'SQLNamedQuery get-group-primary-key SELECT "primary_key from groups WHERE groupname = \'%{0}\'"',
-        'SQLGroupPrimaryKey custom:/get-group-primary-key',
-
-        'SQLNamedQuery session_start FREEFORM "INSERT INTO sessions (name, ip_addr, primary_key) VALUES (\'%g\', \'%L\', %{note:sql.group-primary-key})"',
-        'SQLLog PASS session_start',
-      ],
+      'mod_sql.c' => {
+        SQLAuthTypes => 'plaintext',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $log_file,
+        SQLMinID => 100,
+      },
     },
   };
 
@@ -13360,6 +15116,20 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+
+      my $resp_msgs = $client->response_msgs();
+      my $nmsgs = scalar(@$resp_msgs);
+
+      my $expected;
+
+      $expected = 1;
+      $self->assert($expected == $nmsgs,
+        test_msg("Expected $expected, got $nmsgs")); 
+
+      $expected = "User proftpd logged in";
+      $self->assert($expected eq $resp_msgs->[0],
+        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+
       $client->quit();
     };
 
@@ -13392,39 +15162,32 @@ EOS
     die($ex);
   }
 
-  eval {
-    my ($name, $ip_addr, $primary_key) = get_session_with_primary_key($db_file,
-      "name = \'$group\'");
-
-    my $expected;
-
-    $expected = $group;
-    $self->assert($expected eq $name,
-      test_msg("Expected name '$expected', got '$name'"));
+  unlink($log_file);
+}
 
-    $expected = '127.0.0.1';
-    $self->assert($expected eq $ip_addr,
-      test_msg("Expected IP address '$expected', got '$ip_addr'"));
+sub get_port {
+  my $db_file = shift;
+  my $where = shift;
 
-    $expected = $uid;
-    $self->assert($expected == $primary_key,
-      test_msg("Expected primary key $expected, got $primary_key"));
-  };
-  if ($@) {
-    $ex = $@;
+  my $sql = "SELECT user, ip_addr, port FROM ftpsessions";
+  if ($where) {
+    $sql .= " WHERE $where";
   }
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
+  my $cmd = "sqlite3 $db_file \"$sql\"";
 
-    die($ex);
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
   }
 
-  unlink($log_file);
+  my $res = join('', `$cmd`);
+  chomp($res);
+
+  # The default sqlite3 delimiter is '|'
+  return split(/\|/, $res);
 }
 
-sub sql_sqllog_var_basename_bug3987 {
+sub sql_sqllog_var_remote_port {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
 
@@ -13472,7 +15235,7 @@ sub sql_sqllog_var_basename_bug3987 {
 CREATE TABLE ftpsessions (
   user TEXT,
   ip_addr TEXT,
-  dir TEXT
+  port NUMBER
 );
 EOS
 
@@ -13513,7 +15276,7 @@ EOS
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
         SQLLogFile => $log_file,
-        SQLNamedQuery => 'filename FREEFORM "INSERT INTO ftpsessions (user, ip_addr, dir) VALUES (\'%u\', \'%L\', \'%{basename}\')"',
+        SQLNamedQuery => 'filename FREEFORM "INSERT INTO ftpsessions (user, ip_addr, port) VALUES (\'%u\', \'%L\', %{remote-port})"',
         SQLLog => 'RETR filename',
       },
     },
@@ -13575,7 +15338,6 @@ EOS
 
   # Stop server
   server_stop($pid_file);
-
   $self->assert_child_ok($pid);
 
   if ($ex) {
@@ -13585,13 +15347,9 @@ EOS
     die($ex);
   }
 
-  # Note: We are simply re-using the existing get_locations() function here
-  # for convenience/expedience.
-  my ($login, $ip_addr, $name) = get_locations($db_file, "user = \'$user\'");
-
-  my $expected;
+  my ($login, $ip_addr, $port) = get_port($db_file, "user = \'$user\'");
 
-  $expected = $user;
+  my $expected = $user;
   $self->assert($expected eq $login,
     test_msg("Expected '$expected', got '$login'"));
 
@@ -13599,33 +15357,22 @@ EOS
   $self->assert($expected eq $ip_addr,
     test_msg("Expected '$expected', got '$ip_addr'"));
 
-  $expected = 'sqlite.conf';
-  $self->assert($expected eq $name,
-    test_msg("Expected '$expected', got '$name'"));
+  $expected = '^\d+$';
+  $self->assert(qr/$expected/, $port,
+    test_msg("Expected '$expected', got '$port'"));
 
   unlink($log_file);
 }
 
-sub sql_user_info_defaulthomedir_bug4083 {
+sub sql_userowner_issue346 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'sqlite');
 
-  my $config_file = "$tmpdir/sqlite.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/sqlite.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/sqlite.scoreboard");
-
-  my $log_file = test_get_logfile();
-
-  my $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs("$tmpdir/$user");
-  mkpath($home_dir);
-  my $uid = 500;
-  my $gid = 500;
-
-  my $default_home = File::Spec->rel2abs($tmpdir) . '/%u';
+  my $uid = 0;
+  my $gid = 0;
 
+  my $test_file = File::Spec->rel2abs("$tmpdir/test.dat");
   my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
 
   # Build up sqlite3 command to create users, groups tables and populate them
@@ -13637,10 +15384,19 @@ CREATE TABLE users (
   userid TEXT,
   passwd TEXT,
   uid INTEGER,
-  gid INTEGER
+  gid INTEGER,
+  homedir TEXT,
+  shell TEXT,
+  lastdir TEXT
 );
-INSERT INTO users (userid, passwd, uid, gid) VALUES ('$user', '$passwd', $uid, $gid);
+INSERT INTO users (userid, passwd, uid, gid, homedir, shell) VALUES ('$setup->{user}', '$setup->{passwd}', $uid, $gid, '$setup->{home_dir}', '/bin/bash');
 
+CREATE TABLE groups (
+  groupname TEXT,
+  gid INTEGER,
+  members TEXT
+);
+INSERT INTO groups (groupname, gid, members) VALUES ('$setup->{group}', $gid, '$setup->{user}');
 EOS
 
     unless (close($fh)) {
@@ -13663,9 +15419,19 @@ EOS
   }
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    RootLogin => 'on',
+    RootRevoke => 'off',
+
+    Directory => {
+      '/' => {
+        UserOwner => 'www',
+        GroupOwner => 'www',
+      },
+    },
 
     IfModules => {
       'mod_delay.c' => {
@@ -13673,18 +15439,19 @@ EOS
       },
 
       'mod_sql.c' => {
-        SQLAuthenticate => 'users',
         SQLAuthTypes => 'plaintext',
         SQLBackend => 'sqlite3',
         SQLConnectInfo => $db_file,
-        SQLLogFile => $log_file,
-        SQLUserInfo => 'users userid passwd uid gid Null Null',
-        SQLDefaultHomedir => $default_home,
+        SQLLogFile => $setup->{log_file},
+        SQLMinUserUID => 0,
+        SQLMinUserGID => 0,
+        SQLMinID => 0,
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -13701,21 +15468,32 @@ EOS
   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($user, $passwd);
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0);
+      $client->login($setup->{user}, $setup->{passwd});
 
-      my $resp_msgs = $client->response_msgs();
-      my $nmsgs = scalar(@$resp_msgs);
+      my $conn = $client->stor_raw('test.dat');
+      unless ($conn) {
+        die("STOR test.dat failed: " . $client->response_code() . " " .
+          $client->response_msg());
+      }
 
-      my $expected;
+      my $text = "Hello, World!\n";
+      $conn->write($text, length($text), 10);
+      eval { $conn->close() };
 
-      $expected = 1;
-      $self->assert($expected == $nmsgs,
-        test_msg("Expected $expected, got $nmsgs")); 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+      $self->assert_transfer_ok($resp_code, $resp_msg);
+      $client->quit();
 
-      $expected = "User proftpd logged in";
-      $self->assert($expected eq $resp_msgs->[0],
-        test_msg("Expected '$expected', got '$resp_msgs->[0]'"));
+      $self->assert(-f $test_file,
+        "File $test_file does not exist as expected");
+
+      my $file_uid = (stat($test_file))[4];
+      my $file_gid = (stat($test_file))[5];
+
+      $self->assert($file_uid != 0, "Expected UID non-0, got $file_uid");
+      $self->assert($file_gid != 0, "Expected GID non-0, got $file_gid");
     };
 
     if ($@) {
@@ -13726,7 +15504,7 @@ EOS
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh) };
+    eval { server_wait($setup->{config_file}, $rfh) };
     if ($@) {
       warn($@);
       exit 1;
@@ -13736,18 +15514,10 @@ EOS
   }
 
   # Stop server
-  server_stop($pid_file);
-
+  server_stop($setup->{pid_file});
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    test_append_logfile($log_file, $ex);
-    unlink($log_file);
-
-    die($ex);
-  }
-
-  unlink($log_file);
+  test_cleanup($setup->{log_file}, $ex);
 }
 
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_statcache.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_statcache.pm
new file mode 100644
index 0000000..e541bd6
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_statcache.pm
@@ -0,0 +1,2569 @@
+package ProFTPD::Tests::Modules::mod_statcache;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Cwd;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  statcache_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  statcache_file_chrooted => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  statcache_file_tilde => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  statcache_dir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  statcache_dir_chrooted => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  statcache_rel_symlink_file => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  statcache_rel_symlink_file_chrooted => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  statcache_rel_symlink_dir => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  statcache_rel_symlink_dir_chrooted => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  statcache_config_max_age => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  statcache_config_capacity => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub statcache_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'statcache');
+
+  my $test_file = File::Spec->rel2abs("$setup->{home_dir}/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      '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 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+      my ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      my $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($setup->{user}, $setup->{passwd});
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $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);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# line: $line\n";
+        }
+
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_file'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_file'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub statcache_file_chrooted {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      # Since we're chrooted, the expected path is now different.
+      $test_file = '/test.txt';
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_file'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_file'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_file_tilde {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/~test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('~test.txt');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/~test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('~test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/~test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('~test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/~test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_file'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_file'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$home_dir/test.d");
+  mkpath($test_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $test_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.d');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.d$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.d');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.d$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.d');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.d$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_dir = '/private' . $test_dir;
+      }
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type dir/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_dir'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_dir'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_dir_chrooted {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$home_dir/test.d");
+  mkpath($test_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $test_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.d');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.d$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.d');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.d$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.d');
+    
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.d$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      # Since we're chrooted, the expected path is now different.
+      $test_dir = '/test.d';
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type dir/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_dir'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_dir'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_rel_symlink_file {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the directory in order to create a relative path in the
+  # symlink we need.
+
+  my $cwd = getcwd();
+  unless (chdir("$home_dir")) {
+    die("Can't chdir to $home_dir: $!");
+  }
+
+  unless (symlink('test.txt', 'test.lnk')) {
+    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$home_dir/test.lnk");
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+    
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_symlink = '/private' . $test_symlink;
+      }
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+
+          if ($line =~ /adding entry.*?type symlink/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_symlink'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_symlink'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_rel_symlink_file_chrooted {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  # Change to the directory in order to create a relative path in the
+  # symlink we need.
+
+  my $cwd = getcwd();
+  unless (chdir("$home_dir")) {
+    die("Can't chdir to $home_dir: $!");
+  }
+
+  unless (symlink('test.txt', 'test.lnk')) {
+    die("Can't symlink 'test.txt' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$home_dir/test.lnk");
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+    
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      # Since we're chrooted, the expected path is now different.
+      $test_symlink = '/test.lnk';
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+
+          if ($line =~ /adding entry.*?type symlink/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_symlink'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_symlink'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_rel_symlink_dir {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $test_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  # Change to the directory in order to create a relative path in the
+  # symlink we need.
+
+  my $cwd = getcwd();
+  unless (chdir("$home_dir")) {
+    die("Can't chdir to $home_dir: $!");
+  }
+
+  unless (symlink('test.d', 'test.lnk')) {
+    die("Can't symlink 'test.d' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$home_dir/test.lnk");
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+    
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_symlink = '/private' . $test_symlink;
+      }
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type dir/) {
+            $adding_entry++;
+            next;
+          }
+
+          if ($line =~ /adding entry.*?type symlink/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_symlink'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_symlink'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_rel_symlink_dir_chrooted {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  my $test_dir = File::Spec->rel2abs("$tmpdir/test.d");
+  mkpath($test_dir);
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir, $test_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir, $test_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  # Change to the directory in order to create a relative path in the
+  # symlink we need.
+
+  my $cwd = getcwd();
+  unless (chdir("$home_dir")) {
+    die("Can't chdir to $home_dir: $!");
+  }
+
+  unless (symlink('test.d', 'test.lnk')) {
+    die("Can't symlink 'test.d' to 'test.lnk': $!");
+  }
+
+  unless (chdir($cwd)) {
+    die("Can't chdir to $cwd: $!");
+  }
+
+  my $test_symlink = File::Spec->rel2abs("$home_dir/test.lnk");
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+    DefaultRoot => '~',
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.lnk');
+    
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/test\.lnk$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      # Since we're chrooted, the expected path is now different.
+      $test_symlink = '/test.lnk';
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type dir/) {
+            $adding_entry++;
+            next;
+          }
+
+          if ($line =~ /adding entry.*?type symlink/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_symlink'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_symlink'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_config_max_age {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+  my $max_age = 3;
+  my $timeout = 30;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+        StatCacheMaxAge => $max_age,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.  But give enough time for the entry to expire.
+      sleep($max_age + 1);
+
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh, $timeout) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $expired_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_file'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_file'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:17>/) {
+          if ($line =~ /expired cache entry.*?path '$test_file'/) {
+            $expired_entry++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $expired_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $expired_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+sub statcache_config_capacity {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+
+  my $config_file = "$tmpdir/statcache.conf";
+  my $pid_file = File::Spec->rel2abs("$tmpdir/statcache.pid");
+  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/statcache.scoreboard");
+
+  my $log_file = test_get_logfile();
+
+  my $auth_user_file = File::Spec->rel2abs("$tmpdir/statcache.passwd");
+  my $auth_group_file = File::Spec->rel2abs("$tmpdir/statcache.group");
+
+  my $user = 'proftpd';
+  my $passwd = 'test';
+  my $group = 'ftpd';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+  my $uid = 500;
+  my $gid = 500;
+
+  # Make sure that, if we're running as root, that the home directory has
+  # permissions/privs set for the account we create
+  if ($< == 0) {
+    unless (chmod(0755, $home_dir)) {
+      die("Can't set perms on $home_dir to 0755: $!");
+    }
+
+    unless (chown($uid, $gid, $home_dir)) {
+      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    }
+  }
+
+  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+    '/bin/bash');
+  auth_group_write($auth_group_file, $group, $gid, $user);
+
+  my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+  if (open(my $fh, "> $test_file")) {
+    print $fh "Hello, World!\n";
+    unless (close($fh)) {
+      die("Can't write $test_file: $!");
+    }
+
+  } else {
+    die("Can't open $test_file: $!");
+  }
+
+  my $statcache_tab = File::Spec->rel2abs("$tmpdir/statcache.tab");
+  my $capacity = 10000;
+  my $max_age = 5;
+  my $timeout = 300;
+
+  my $config = {
+    PidFile => $pid_file,
+    ScoreboardFile => $scoreboard_file,
+    SystemLog => $log_file,
+    TraceLog => $log_file,
+    Trace => 'fsio:10 statcache:20',
+
+    AuthUserFile => $auth_user_file,
+    AuthGroupFile => $auth_group_file,
+
+    IfModules => {
+      'mod_statcache.c' => {
+        StatCacheEngine => 'on',
+        StatCacheTable => $statcache_tab,
+        StatCacheCapacity => $capacity,
+      },
+
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($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->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      my $expected;
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      # Do the MLST again; we'll check the logs to see if mod_statcache
+      # did its job.
+      $resp_code = $resp_msg = undef;
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+
+      # Now connect again, do another MLST, and see if we're still using
+      # the cached entry.  But give enough time for the entry to expire.
+      sleep($max_age + 1);
+
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      $client->login($user, $passwd);
+      ($resp_code, $resp_msg) = $client->mlst('test.txt');
+
+      $expected = 250;
+      $self->assert($expected == $resp_code,
+        test_msg("Expected response code $expected, got $resp_code"));
+
+      $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/.*\/test\.txt$';
+      $self->assert(qr/$expected/, $resp_msg,
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+      $client->quit();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($config_file, $rfh, $timeout) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($pid_file);
+
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $log_file")) {
+      my $adding_entry = 0;
+      my $expired_entry = 0;
+      my $cached_stat = 0;
+      my $cached_lstat = 0;
+
+      if ($^O eq 'darwin') {
+        # MacOSX-specific hack
+        $test_file = '/private' . $test_file;
+      }
+
+      while (my $line = <$fh>) {
+        if ($line =~ /<statcache:9>/) {
+          if ($line =~ /adding entry.*?type file/) {
+            $adding_entry++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:11>/) {
+          if ($cached_stat == 0 &&
+              $line =~ /using cached stat.*?path '$test_file'/) {
+            $cached_stat++;
+            next;
+          }
+
+          if ($cached_lstat == 0 &&
+              $line =~ /using cached lstat.*?path '$test_file'/) {
+            $cached_lstat++;
+            next;
+          }
+        }
+
+        if ($line =~ /<statcache:17>/) {
+          if ($line =~ /expired cache entry.*?path '$test_file'/) {
+            $expired_entry++;
+            next;
+          }
+        }
+
+        if ($adding_entry >= 2 &&
+            $expired_entry >= 2 &&
+            $cached_stat == 1 &&
+            $cached_lstat == 1) {
+          last;
+        }
+      }
+
+      close($fh);
+
+      $self->assert($adding_entry >= 2 &&
+                    $expired_entry >= 2 &&
+                    $cached_stat == 1 &&
+                    $cached_lstat == 1,
+        test_msg("Did not see expected 'statcache' TraceLog messages"));
+
+    } else {
+      die("Can't read $log_file: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  if ($ex) {
+    test_append_logfile($log_file, $ex);
+    unlink($log_file);
+
+    die($ex);
+  }
+
+  unlink($log_file);
+}
+
+1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm
index a42d12e..f7cd171 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls.pm
@@ -4,10 +4,12 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Carp;
 use File::Copy;
 use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
+use Socket;
 
 use ProFTPD::TestSuite::FTP;
 use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
@@ -179,6 +181,11 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  tls_ccc_before_login => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
   tls_opts_commonname_required_bug3512 => {
     order => ++$order,
     test_class => [qw(bug forking)],
@@ -332,6 +339,26 @@ my $TESTS = {
     test_class => [qw(bug forking)],
   },
 
+  tls_config_missing_certs => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  tls_stapling_on_bug4175 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  tls_session_tickets_on_bug4176 => {
+    order => ++$order,
+    test_class => [qw(bug forking inprogress)],
+  },
+
+  tls_restart_protected_certs_bug4260 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -985,6 +1012,7 @@ sub tls_dh_ciphersuite {
       my $ssl_opts = {
         # Results in set_tmp_dh_callback() keylength of 1024 BITS
         SSL_cipher_list => 'DHE-RSA-AES256-SHA',
+        SSL_ca_file => $ca_file,
       };
 
       my $client = Net::FTPSSL->new('127.0.0.1',
@@ -1136,6 +1164,7 @@ sub tls_crl_file_ok {
           SSL_use_cert => 1,
           SSL_cert_file => $client_cert,
           SSL_key_file => $client_cert,
+          SSL_ca_file => $ca_cert,
         };
 
         $client = Net::FTPSSL->new('127.0.0.1',
@@ -1575,6 +1604,7 @@ sub tls_list_fails_tls_required_by_dir_bug2178 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       $client = Net::FTPSSL->new('127.0.0.1',
@@ -1735,6 +1765,7 @@ sub tls_list_ok_tls_required_by_dir_bug2178 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       $client = Net::FTPSSL->new('127.0.0.1',
@@ -1905,6 +1936,7 @@ sub tls_list_fails_tls_required_by_ftpaccess_bug2178 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       $client = Net::FTPSSL->new('127.0.0.1',
@@ -2075,6 +2107,7 @@ sub tls_list_ok_tls_required_by_ftpaccess_bug2178 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       $client = Net::FTPSSL->new('127.0.0.1',
@@ -2770,6 +2803,7 @@ sub tls_opts_std_env_vars_client_vars {
         TLSRSACertificateFile => $cert_file,
         TLSCACertificateFile => $ca_file,
         TLSOptions => 'StdEnvVars',
+        TLSVerifyClient => 'optional',
       },
     },
   };
@@ -2801,6 +2835,7 @@ sub tls_opts_std_env_vars_client_vars {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_file,
       };
 
       my $client = Net::FTPSSL->new('127.0.0.1',
@@ -2977,6 +3012,7 @@ sub tls_opts_ipaddr_required_ipv4 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_file,
       };
 
       my $client;
@@ -3133,6 +3169,7 @@ sub tls_opts_ipaddr_required_ipv6 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_file,
       };
 
       my $client;
@@ -5327,6 +5364,180 @@ sub tls_ccc_list_bug3465 {
   unlink($log_file);
 }
 
+sub tls_ccc_before_login {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'DEFAULT:10 tls:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'TLSv1',
+        TLSRequired => 'off',
+        TLSOptions => 'NoSessionReuseRequired',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+      },
+    },
+  };
+
+  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 $client_opts = {
+        PeerHost => '127.0.0.1',
+        PeerPort => $port,
+        Proto => 'tcp',
+        Type => SOCK_STREAM,
+        Timeout => 10
+      };
+
+      my $ssl_opts = {
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+      };
+
+      my $client = IO::Socket::INET->new(%$client_opts);
+      unless ($client) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
+
+      # Read the banner
+      my $banner = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Received banner: $banner";
+      }
+
+      # Send the AUTH command
+      my $cmd = "AUTH TLS\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Sending command: $cmd";
+      }
+      $client->print($cmd);
+      $client->flush();
+
+      # Read the AUTH response
+      my $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Received response: $resp";
+      }
+
+      my $expected = "234 AUTH TLS successful\r\n";
+      unless ($expected eq $resp) {
+        die("Expected response '$expected', got '$resp'");
+      }
+
+      if ($ENV{TEST_VERBOSE}) {
+        $IO::Socket::SSL::DEBUG = 3;
+      }
+
+      my $res = IO::Socket::SSL->start_SSL($client, $ssl_opts);
+      unless ($res) {
+        croak("Failed SSL handshake: " . IO::Socket::SSL::errstr());
+      }
+
+      $cmd = "CCC\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Sending command: $cmd";
+      }
+      $client->print($cmd);
+      $client->flush();
+
+      $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Received response: $resp";
+      }
+
+      $expected = "530 Please login with USER and PASS\r\n";
+      unless ($expected eq $resp) {
+        die("Expected response '$expected', got '$resp'");
+      }
+
+      $res = $client->stop_SSL();
+      unless ($res) {
+        croak("Failed SSL shutdown: " . IO::Socket::SSL::errstr());
+      }
+
+      $cmd = "QUIT\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Sending command: $cmd";
+      }
+      $client->print($cmd);
+      $client->flush();
+
+      $resp = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDOUT "# Received response: $resp";
+      }
+
+      if ($resp) {
+        die("Received response unexpectedly");
+      }
+
+      $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 tls_opts_commonname_required_bug3512 {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
@@ -5423,6 +5634,7 @@ sub tls_opts_commonname_required_bug3512 {
         SSL_use_cert => 1,
         SSL_cert_file => $bad_client_cert,
         SSL_key_file => $bad_client_cert,
+        SSL_ca_file => $ca_file,
       };
 
       my $client;
@@ -5589,6 +5801,7 @@ sub tls_opts_dns_name_required {
         SSL_use_cert => 1,
         SSL_cert_file => $bad_client_cert,
         SSL_key_file => $bad_client_cert,
+        SSL_ca_file => $ca_file,
       };
 
       my $client;
@@ -5755,6 +5968,7 @@ sub tls_opts_ip_addr_dns_name_cn_required {
         SSL_use_cert => 1,
         SSL_cert_file => $bad_client_cert,
         SSL_key_file => $bad_client_cert,
+        SSL_ca_file => $ca_file,
       };
 
       my $client;
@@ -7223,6 +7437,7 @@ sub tls_verify_order_crl_bug3658 {
           SSL_use_cert => 1,
           SSL_cert_file => $client_cert,
           SSL_key_file => $client_cert,
+          SSL_ca_file => $ca_cert,
         };
 
         $client = Net::FTPSSL->new('127.0.0.1',
@@ -7390,6 +7605,7 @@ sub tls_verify_order_ocsp {
           SSL_use_cert => 1,
           SSL_cert_file => $client_cert,
           SSL_key_file => $client_cert,
+          SSL_ca_file => $ca_cert,
         };
 
         $client = Net::FTPSSL->new('127.0.0.1',
@@ -7560,6 +7776,7 @@ sub tls_verify_order_ocsp_https {
           SSL_use_cert => 1,
           SSL_cert_file => $client_cert,
           SSL_key_file => $client_cert,
+          SSL_ca_file => $ca_cert,
         };
 
         $client = Net::FTPSSL->new('127.0.0.1',
@@ -7718,6 +7935,7 @@ sub tls_client_cert_verify_failed_selfsigned_cert_only_bug3742 {
           SSL_use_cert => 1,
           SSL_cert_file => $client_cert,
           SSL_key_file => $client_key,
+          SSL_ca_file => $ca_cert,
         };
 
         eval {
@@ -8233,7 +8451,7 @@ sub tls_opts_allow_dot_login {
         TLSRequired => 'on',
         TLSRSACertificateFile => $server_cert,
         TLSCACertificateFile => $ca_cert,
-
+        TLSVerifyClient => 'optional',
         TLSOptions => 'AllowDotLogin',
       },
     },
@@ -8266,6 +8484,7 @@ sub tls_opts_allow_dot_login {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       my $client = Net::FTPSSL->new('127.0.0.1',
@@ -8287,6 +8506,15 @@ sub tls_opts_allow_dot_login {
       my $resp = $client->last_message();
       $self->assert($expected eq $resp,
         test_msg("Expected response '$expected', got '$resp'"));
+
+      if ($client->_passwd()) {
+        die("PASS succeeded unexpectedly");
+      }
+
+      my $expected = "503 You are already logged in";
+      my $resp = $client->last_message();
+      $self->assert($expected eq $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
     };
 
     if ($@) {
@@ -8703,6 +8931,7 @@ sub tls_config_tlsdhparamfile_bug3868 {
       my $ssl_opts = {
         # Results in set_tmp_dh_callback() keylength of 1024 BITS
         SSL_cipher_list => 'DHE-RSA-AES256-SHA',
+        SSL_ca_file => $ca_file,
       };
 
       my $client = Net::FTPSSL->new('127.0.0.1',
@@ -9014,6 +9243,7 @@ EOC
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       my $debug = 0;
@@ -9048,7 +9278,7 @@ EOC
         die("PROT failed unexpectedly: " . $client->last_message());
       }
 
-      my $res = $client->list();
+      $res = $client->list();
       unless ($res) {
         die("LIST failed unexpectedly: " . $client->last_message());
       }
@@ -9155,6 +9385,7 @@ sub tls_config_tlsusername_bug3899 {
         TLSRequired => 'on',
         TLSRSACertificateFile => $server_cert,
         TLSCACertificateFile => $ca_cert,
+        TLSVerifyClient => 'optional',
 
         TLSUserName => 'CommonName',
       },
@@ -9188,6 +9419,7 @@ sub tls_config_tlsusername_bug3899 {
         SSL_use_cert => 1,
         SSL_cert_file => $client_cert,
         SSL_key_file => $client_cert,
+        SSL_ca_file => $ca_cert,
       };
 
       my $client = Net::FTPSSL->new('127.0.0.1',
@@ -10037,4 +10269,422 @@ sub tls_config_limit_sscn_bug3955 {
   unlink($log_file);
 }
 
+sub tls_config_missing_certs {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        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 $client = Net::FTPSSL->new('127.0.0.1',
+        Encryption => 'E',
+        Port => $port,
+      );
+
+      if ($client) {
+        die("Connected via AUTH TLS unexpectedly");
+      }
+
+      my $errstr = IO::Socket::SSL::errstr();
+      unless ($errstr) {
+        $errstr = $Net::FTPSSL::ERRSTR;
+      }
+
+      $self->assert(qr/^431/, $errstr, "Expected 431, 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 starttls_ftp {
+  my $port = shift;
+  my $ssl_opts = shift;
+
+  my $client = IO::Socket::INET->new(
+    PeerHost => '127.0.0.1',
+    PeerPort => $port,
+    Proto => 'tcp',
+    Type => SOCK_STREAM,
+    Timeout => 10
+  );
+  unless ($client) {
+    croak("Can't connect to 127.0.0.1:$port: $!");
+  }
+
+  # Read the banner
+  my $banner = <$client>;
+
+  # Send the AUTH command
+  my $cmd = "AUTH TLS\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+  $client->print($cmd);
+  $client->flush();
+
+  # Read the AUTH response
+  my $resp = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received response: $resp";
+  }
+
+  my $expected = "234 AUTH TLS successful\r\n";
+  unless ($expected eq $resp) {
+    croak(test_msg("Expected response '$expected', got '$resp'"));
+  }
+
+  # Now perform the SSL handshake
+  if ($ENV{TEST_VERBOSE}) {
+    $IO::Socket::SSL::DEBUG = 3;
+  }
+
+  my $res = IO::Socket::SSL->start_SSL($client, $ssl_opts);
+  unless ($res) {
+    croak("Failed SSL handshake: " . IO::Socket::SSL::errstr());
+  }
+
+  $cmd = "QUIT\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  $client->print($cmd);
+  $client->flush();
+
+  $resp = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received response: $resp";
+  }
+
+  $client->close();
+}
+
+sub tls_stapling_on_bug4175 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+        TLSStapling => 'on',
+      },
+    },
+  };
+
+  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 IO::Socket::INET;
+  require IO::Socket::SSL;
+
+  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);
+
+      # Manually simulate the STARTTLS protocol
+
+      my $ssl_opts = {
+        SSL_ocsp_mode => IO::Socket::SSL::SSL_OCSP_TRY_STAPLE(),
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+      };
+
+      starttls_ftp($port, $ssl_opts);
+    };
+
+    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 tls_session_tickets_on_bug4176 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+        TLSSessionTickets => 'on',
+        TLSSessionTicketKeys => 'age 3sec count 2',
+      },
+    },
+  };
+
+  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 IO::Socket::INET;
+  require IO::Socket::SSL;
+
+  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 = {
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+      };
+
+      # Manually simulate the STARTTLS protocol
+
+      starttls_ftp($port, $ssl_opts);
+
+      my $delay = 7;
+      if ($delay > 0) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDOUT "# Delaying for $delay secs\n";
+        }
+
+        sleep($delay);
+      }
+
+      starttls_ftp($port, $ssl_opts);
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 30) };
+    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 tls_restart_protected_certs_bug4260 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert-passwd.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+  my $passphrase_provider = File::Spec->rel2abs('t/etc/modules/mod_tls/tls-get-passphrase-once.pl');
+
+  # Note: This lock file path MUST be kept in sync with the path used by
+  # the TLSPassPhraseProvider script used in this test.
+  my $lock_file = "/tmp/tls-passphrase.lock";
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+
+        TLSPassPhraseProvider => $passphrase_provider,
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  my $ex;
+
+  # Start the server
+  server_start($setup->{config_file});
+  sleep(4);
+
+  # Restart the server
+  server_restart($setup->{pid_file});
+  sleep(4);
+
+  # Stop server
+  unless (server_stop($setup->{pid_file})) {
+    $ex = "Error stopping server";
+  }
+
+  unlink($lock_file);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
 1;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_fscache.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_fscache.pm
new file mode 100644
index 0000000..44a4913
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_fscache.pm
@@ -0,0 +1,235 @@
+package ProFTPD::Tests::Modules::mod_tls_fscache;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Carp;
+use File::Copy;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use Socket;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  tls_stapling_on_fscache_bug4175 => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_tls_fscache)],
+  },
+
+};
+
+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 starttls_ftp {
+  my $port = shift;
+  my $ssl_opts = shift;
+
+  my $client = IO::Socket::INET->new(
+    PeerHost => '127.0.0.1',
+    PeerPort => $port,
+    Proto => 'tcp',
+    Type => SOCK_STREAM,
+    Timeout => 10
+  );
+  unless ($client) {
+    croak("Can't connect to 127.0.0.1:$port: $!");
+  }
+
+  # Read the banner
+  my $banner = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received banner: $banner\n";
+  }
+
+  # Send the AUTH command
+  my $cmd = "AUTH TLS\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  $client->print($cmd);
+  $client->flush();
+
+  # Read the AUTH response
+  my $resp = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received response: $resp\n";
+  }
+
+  my $expected = "234 AUTH TLS successful\r\n";
+  unless ($expected eq $resp) {
+    croak("Expected response '$expected', got '$resp'");
+  }
+
+  # Now perform the SSL handshake
+  if ($ENV{TEST_VERBOSE}) {
+    $IO::Socket::SSL::DEBUG = 3;
+  }
+
+  my $res = IO::Socket::SSL->start_SSL($client, $ssl_opts);
+  unless ($res) {
+    croak("Failed SSL handshake: " . IO::Socket::SSL::errstr());
+  }
+
+  $cmd = "QUIT\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  print $client $cmd;
+  $client->flush();
+  $client->close();
+}
+
+sub tls_stapling_on_fscache_bug4175 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls_fscache');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $cache_tab = File::Spec->rel2abs("$tmpdir/var/tls/cache/ocsp");
+  mkpath($cache_tab);
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20 tls.fscache:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+        TLSStapling => 'on',
+        TLSStaplingCache => "fs:/path=$cache_tab",
+      },
+    },
+  };
+
+  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 IO::Socket::INET;
+  require IO::Socket::SSL;
+
+  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);
+
+      # Manually simulate the STARTTLS protocol
+
+      my $ssl_opts = {
+        SSL_ocsp_mode => IO::Socket::SSL::SSL_OCSP_TRY_STAPLE(),
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+        SSL_alpn_protocols => [qw(ftp)],
+      };
+
+      starttls_ftp($port, $ssl_opts);
+
+      my $delay = 5;
+      if ($delay > 0) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDOUT "# Sleeping for $delay seconds\n";
+        }
+
+        sleep($delay);
+      }
+
+      # Do it again, see if we actually read our our cached OCSP response
+      starttls_ftp($port, $ssl_opts);
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm
index 2cf4150..ee13a12 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_memcache.pm
@@ -5,9 +5,11 @@ use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
 use Cache::Memcached;
+use Carp;
 use File::Spec;
 use IO::Handle;
 use IPC::Open3;
+use Socket;
 
 use ProFTPD::TestSuite::FTP;
 use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
@@ -22,6 +24,16 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  tls_sess_cache_memcache_json_bug4057 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  tls_stapling_on_memcache_bug4175 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -83,38 +95,243 @@ sub list_tests {
 sub tls_sess_cache_memcache {
   my $self = shift;
   my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls_memcache');
+
+  my $memcached_servers = $ENV{MEMCACHED_SERVERS} ? $ENV{MEMCACHED_SERVERS} : '127.0.0.1:11211';
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $sessid_file = File::Spec->rel2abs("$tmpdir/sessid.pem");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20 memcache:30 tls.memcache:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_memcache.c' => {
+        MemcacheEngine => 'on',
+        MemcacheLog => $setup->{log_file},
+        MemcacheServers => $memcached_servers,
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSVerifyClient => 'off',
+        TLSOptions => 'EnableDiags',
+      },
+
+      'mod_tls_memcache.c' => {
+        TLSSessionCache => 'memcache:',
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
 
-  my $config_file = "$tmpdir/tls.conf";
-  my $pid_file = File::Spec->rel2abs("$tmpdir/tls.pid");
-  my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tls.scoreboard");
+  # 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 $log_file = File::Spec->rel2abs('tests.log');
+  my $ex;
 
-  my $auth_user_file = File::Spec->rel2abs("$tmpdir/tls.passwd");
-  my $auth_group_file = File::Spec->rel2abs("$tmpdir/tls.group");
+  # 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 $user = 'proftpd';
-  my $passwd = 'test';
-  my $group = 'ftpd';
-  my $home_dir = File::Spec->rel2abs($tmpdir);
-  my $uid = 500;
-  my $gid = 500;
+      # To test SSL session resumption, we use the command-line
+      # openssl s_client tool, rather than any Perl module.
 
-  # Make sure that, if we're running as root, that the home directory has
-  # permissions/privs set for the account we create
-  if ($< == 0) {
-    unless (chmod(0755, $home_dir)) {
-      die("Can't set perms on $home_dir to 0755: $!");
+      # XXX Some OpenSSL versions' of s_client do not support the 'ftp'
+      # parameter for -starttls; in this case, point the openssl binary
+      # to be used to a version which does support this.
+#      my $openssl = 'openssl';
+my $openssl = '/Users/tj/local/openssl-1.0.2d/bin/openssl';
+
+      my @cmd = (
+        $openssl,
+        's_client',
+        '-connect',
+        "127.0.0.1:$port",
+        '-starttls',
+        'ftp',
+        '-sess_out',
+        $sessid_file,
+      );
+
+      my $tls_rh = IO::Handle->new();
+      my $tls_wh = IO::Handle->new();
+      my $tls_eh = IO::Handle->new();
+
+      $tls_wh->autoflush(1);
+
+      local $SIG{CHLD} = 'DEFAULT';
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      my $tls_pid = open3($tls_wh, $tls_rh, $tls_eh, @cmd);
+      print $tls_wh "QUIT\r\n";
+      waitpid($tls_pid, 0);
+
+      my ($res, $cipher_str, $err_str, $out_str);
+      if ($? >> 8) {
+        $err_str = join('', <$tls_eh>);
+        $res = 0;
+
+      } else {
+        my $output = [<$tls_rh>];
+
+        # Specifically look for the line containing 'Cipher is'
+        foreach my $line (@$output) {
+          if ($line =~ /Cipher is/) {
+            $cipher_str = $line;
+            chomp($cipher_str);
+          }
+        }
+
+        if ($ENV{TEST_VERBOSE}) {
+          $out_str = join('', @$output);
+          print STDERR "Stdout: $out_str\n";
+        }
+
+        if ($ENV{TEST_VERBOSE}) {
+          $err_str = join('', <$tls_eh>);
+          print STDERR "Stderr: $err_str\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res) {
+        die("Can't talk to server: $err_str");
+      }
+
+      my $expected = '^New';
+      $self->assert(qr/$expected/, $cipher_str,
+        test_msg("Expected '$expected', got '$cipher_str'"));
+
+      @cmd = (
+        $openssl,
+        's_client',
+        '-connect',
+        "127.0.0.1:$port",
+        '-starttls',
+        'ftp',
+        '-sess_in',
+        $sessid_file,
+      );
+
+      $tls_rh = IO::Handle->new();
+      $tls_wh = IO::Handle->new();
+      $tls_eh = IO::Handle->new();
+
+      $tls_wh->autoflush(1);
+
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "Executing: ", join(' ', @cmd), "\n";
+      }
+
+      $tls_pid = open3($tls_wh, $tls_rh, $tls_eh, @cmd);
+      print $tls_wh "QUIT\r\n";
+      waitpid($tls_pid, 0);
+
+      $res = 0;
+      $cipher_str = undef;
+      $err_str = undef;
+      $out_str = undef;
+
+      if ($? >> 8) {
+        $err_str = join('', <$tls_eh>);
+        $res = 0;
+
+      } else {
+        my $output = [<$tls_rh>];
+
+        # Specifically look for the line containing 'Cipher is'
+        foreach my $line (@$output) {
+          if ($line =~ /Cipher is/) {
+            $cipher_str = $line;
+            chomp($cipher_str);
+          }
+        }
+
+        if ($ENV{TEST_VERBOSE}) {
+          $out_str = join('', @$output);
+          print STDERR "Stdout: $out_str\n";
+        }
+
+        if ($ENV{TEST_VERBOSE}) {
+          $err_str = join('', <$tls_eh>);
+          print STDERR "Stderr: $err_str\n";
+        }
+
+        $res = 1;
+      }
+
+      unless ($res) {
+        die("Can't talk to server: $err_str");
+      }
+
+      $expected = '^Reused';
+      $self->assert(qr/$expected/, $cipher_str,
+        test_msg("Expected '$expected', got '$cipher_str'"));
+    };
+
+    if ($@) {
+      $ex = $@;
     }
 
-    unless (chown($uid, $gid, $home_dir)) {
-      die("Can't set owner of $home_dir to $uid/$gid: $!");
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, 45) };
+    if ($@) {
+      warn($@);
+      exit 1;
     }
+
+    exit 0;
   }
 
-  auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
-    '/bin/bash');
-  auth_group_write($auth_group_file, $group, $gid, $user);
+  # Stop server
+  server_stop($setup->{pid_file});
+
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub tls_sess_cache_memcache_json_bug4057 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls_memcache');
 
   my $memcached_servers = $ENV{MEMCACHED_SERVERS} ? $ENV{MEMCACHED_SERVERS} : '127.0.0.1:11211';
 
@@ -124,14 +341,14 @@ sub tls_sess_cache_memcache {
   my $sessid_file = File::Spec->rel2abs("$tmpdir/sessid.pem");
 
   my $config = {
-    PidFile => $pid_file,
-    ScoreboardFile => $scoreboard_file,
-    SystemLog => $log_file,
-    TraceLog => $log_file,
-    Trace => 'memcache:30 tls_memcache:20',
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20 memcache:30 tls.memcache:20',
 
-    AuthUserFile => $auth_user_file,
-    AuthGroupFile => $auth_group_file,
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
 
     IfModules => {
       'mod_delay.c' => {
@@ -140,27 +357,29 @@ sub tls_sess_cache_memcache {
 
       'mod_memcache.c' => {
         MemcacheEngine => 'on',
-        MemcacheLog => $log_file,
+        MemcacheLog => $setup->{log_file},
         MemcacheServers => $memcached_servers,
       },
 
       'mod_tls.c' => {
         TLSEngine => 'on',
-        TLSLog => $log_file,
+        TLSLog => $setup->{log_file},
         TLSProtocol => 'SSLv3 TLSv1',
         TLSRequired => 'on',
         TLSRSACertificateFile => $cert_file,
         TLSCACertificateFile => $ca_file,
         TLSVerifyClient => 'off',
+        TLSOptions => 'EnableDiags',
       },
 
       'mod_tls_memcache.c' => {
-        TLSSessionCache => "memcache:",
+        TLSSessionCache => 'memcache:/json',
       },
     },
   };
 
-  my ($port, $config_user, $config_group) = config_write($config_file, $config);
+  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
@@ -187,7 +406,7 @@ sub tls_sess_cache_memcache {
       # parameter for -starttls; in this case, point the openssl binary
       # to be used to a version which does support this.
 #      my $openssl = 'openssl';
-my $openssl = '/Users/tjsaunders/local/openssl-0.9.8j/bin/openssl';
+my $openssl = '/Users/tj/local/openssl-1.0.2d/bin/openssl';
 
       my @cmd = (
         $openssl,
@@ -213,7 +432,7 @@ my $openssl = '/Users/tjsaunders/local/openssl-0.9.8j/bin/openssl';
       }
 
       my $tls_pid = open3($tls_wh, $tls_rh, $tls_eh, @cmd);
-      print $tls_wh "quit\n";
+      print $tls_wh "QUIT\r\n";
       waitpid($tls_pid, 0);
 
       my ($res, $cipher_str, $err_str, $out_str);
@@ -275,7 +494,7 @@ my $openssl = '/Users/tjsaunders/local/openssl-0.9.8j/bin/openssl';
       }
 
       $tls_pid = open3($tls_wh, $tls_rh, $tls_eh, @cmd);
-      print $tls_wh "quit\n";
+      print $tls_wh "QUIT\r\n";
       waitpid($tls_pid, 0);
 
       $res = 0;
@@ -328,7 +547,7 @@ my $openssl = '/Users/tjsaunders/local/openssl-0.9.8j/bin/openssl';
     $wfh->flush();
 
   } else {
-    eval { server_wait($config_file, $rfh, 45) };
+    eval { server_wait($setup->{config_file}, $rfh, 45) };
     if ($@) {
       warn($@);
       exit 1;
@@ -338,15 +557,189 @@ my $openssl = '/Users/tjsaunders/local/openssl-0.9.8j/bin/openssl';
   }
 
   # Stop server
-  server_stop($pid_file);
+  server_stop($setup->{pid_file});
 
   $self->assert_child_ok($pid);
 
-  if ($ex) {
-    die($ex);
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub starttls_ftp {
+  my $port = shift;
+  my $ssl_opts = shift;
+
+  my $client = IO::Socket::INET->new(
+    PeerHost => '127.0.0.1',
+    PeerPort => $port,
+    Proto => 'tcp',
+    Type => SOCK_STREAM,
+    Timeout => 10
+  );
+  unless ($client) {
+    croak("Can't connect to 127.0.0.1:$port: $!");
+  }
+
+  # Read the banner
+  my $banner = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received banner: $banner\n";
+  }
+
+  # Send the AUTH command
+  my $cmd = "AUTH TLS\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  $client->print($cmd);
+  $client->flush();
+
+  # Read the AUTH response
+  my $resp = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received response: $resp\n";
+  }
+
+  my $expected = "234 AUTH TLS successful\r\n";
+  unless ($expected eq $resp) {
+    croak("Expected response '$expected', got '$resp'");
+  }
+
+  # Now perform the SSL handshake
+  if ($ENV{TEST_VERBOSE}) {
+    $IO::Socket::SSL::DEBUG = 3;
+  }
+
+  my $res = IO::Socket::SSL->start_SSL($client, $ssl_opts);
+  unless ($res) {
+    croak("Failed SSL handshake: " . IO::Socket::SSL::errstr());
+  }
+
+  $cmd = "QUIT\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  print $client $cmd;
+  $client->flush();
+  $client->close();
+}
+
+sub tls_stapling_on_memcache_bug4175 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls_memcache');
+
+  my $memcached_servers = $ENV{MEMCACHED_SERVERS} ? $ENV{MEMCACHED_SERVERS} : '127.0.0.1:11211';
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20 tls.memcache:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_memcache.c' => {
+        MemcacheEngine => 'on',
+        MemcacheLog => $setup->{log_file},
+        MemcacheServers => $memcached_servers,
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+        TLSStapling => 'on',
+        TLSStaplingCache => "memcache:/",
+      },
+    },
+  };
+
+  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: $!");
   }
 
-  unlink($log_file);
+  require IO::Socket::INET;
+  require IO::Socket::SSL;
+
+  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);
+
+      # Manually simulate the STARTTLS protocol
+
+      my $ssl_opts = {
+        SSL_ocsp_mode => IO::Socket::SSL::SSL_OCSP_TRY_STAPLE(),
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+        SSL_alpn_protocols => [qw(ftp)],
+      };
+
+      starttls_ftp($port, $ssl_opts);
+
+      my $delay = 5;
+      if ($delay > 0) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDOUT "# Sleeping for $delay seconds\n";
+        }
+
+        sleep($delay);
+      }
+
+      # Do it again, see if we actually read our our cached OCSP response
+      starttls_ftp($port, $ssl_opts);
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm
index 0935b1f..c4931f1 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_tls_shmcache.pm
@@ -4,9 +4,12 @@ use lib qw(t/lib);
 use base qw(ProFTPD::TestSuite::Child);
 use strict;
 
+use Carp;
+use File::Path qw(mkpath);
 use File::Spec;
 use IO::Handle;
 use IPC::Open3;
+use Socket;
 
 use ProFTPD::TestSuite::FTP;
 use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
@@ -21,6 +24,11 @@ my $TESTS = {
     test_class => [qw(forking)],
   },
 
+  tls_stapling_on_shmcache_bug4175 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
 };
 
 sub new {
@@ -103,7 +111,7 @@ sub tls_sess_cache_shm {
     ScoreboardFile => $scoreboard_file,
     SystemLog => $log_file,
     TraceLog => $log_file,
-    Trace => 'tls_shmcache:20',
+    Trace => 'tls.shmcache:20',
 
     AuthUserFile => $auth_user_file,
     AuthGroupFile => $auth_group_file,
@@ -328,4 +336,178 @@ sub tls_sess_cache_shm {
   unlink($log_file);
 }
 
+sub starttls_ftp {
+  my $port = shift;
+  my $ssl_opts = shift;
+
+  my $client = IO::Socket::INET->new(
+    PeerHost => '127.0.0.1',
+    PeerPort => $port,
+    Proto => 'tcp',
+    Type => SOCK_STREAM,
+    Timeout => 10
+  );
+  unless ($client) {
+    croak("Can't connect to 127.0.0.1:$port: $!");
+  }
+
+  # Read the banner
+  my $banner = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received banner: $banner\n";
+  }
+
+  # Send the AUTH command
+  my $cmd = "AUTH TLS\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  $client->print($cmd);
+  $client->flush();
+
+  # Read the AUTH response
+  my $resp = <$client>;
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Received response: $resp\n";
+  }
+
+  my $expected = "234 AUTH TLS successful\r\n";
+  unless ($expected eq $resp) {
+    croak("Expected response '$expected', got '$resp'");
+  }
+
+  # Now perform the SSL handshake
+  if ($ENV{TEST_VERBOSE}) {
+    $IO::Socket::SSL::DEBUG = 3;
+  }
+
+  my $res = IO::Socket::SSL->start_SSL($client, $ssl_opts);
+  unless ($res) {
+    croak("Failed SSL handshake: " . IO::Socket::SSL::errstr());
+  }
+
+  $cmd = "QUIT\r\n";
+  if ($ENV{TEST_VERBOSE}) {
+    print STDOUT "# Sending command: $cmd";
+  }
+
+  print $client $cmd;
+  $client->flush();
+  $client->close();
+}
+
+sub tls_stapling_on_shmcache_bug4175 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'tls_shmcache');
+
+  my $cert_file = File::Spec->rel2abs('t/etc/modules/mod_tls/server-cert.pem');
+  my $ca_file = File::Spec->rel2abs('t/etc/modules/mod_tls/ca-cert.pem');
+
+  my $cache_dir = File::Spec->rel2abs("$tmpdir/var/tls/cache");
+  mkpath($cache_dir);
+  my $cache_tab = File::Spec->rel2abs("$cache_dir/ocsp.dat");
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'tls:20 tls.shmcache:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_tls.c' => {
+        TLSEngine => 'on',
+        TLSLog => $setup->{log_file},
+        TLSProtocol => 'SSLv3 TLSv1',
+        TLSRequired => 'on',
+        TLSRSACertificateFile => $cert_file,
+        TLSCACertificateFile => $ca_file,
+        TLSOptions => 'EnableDiags',
+        TLSStapling => 'on',
+        TLSStaplingCache => "shm:/file=$cache_tab",
+      },
+    },
+  };
+
+  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 IO::Socket::INET;
+  require IO::Socket::SSL;
+
+  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);
+
+      # Manually simulate the STARTTLS protocol
+
+      my $ssl_opts = {
+        SSL_ocsp_mode => IO::Socket::SSL::SSL_OCSP_TRY_STAPLE(),
+        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
+        SSL_alpn_protocols => [qw(ftp)],
+      };
+
+      starttls_ftp($port, $ssl_opts);
+
+      my $delay = 5;
+      if ($delay > 0) {
+        if ($ENV{TEST_VERBOSE}) {
+          print STDOUT "# Sleeping for $delay seconds\n";
+        }
+
+        sleep($delay);
+      }
+
+      # Do it again, see if we actually read our our cached OCSP response
+      starttls_ftp($port, $ssl_opts);
+    };
+
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_redis.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_redis.pm
new file mode 100644
index 0000000..29eb1bb
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_redis.pm
@@ -0,0 +1,2927 @@
+package ProFTPD::Tests::Modules::mod_wrap2_redis;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+use IO::Socket::INET6;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :features :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  wrap2_allow_msg => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_deny_msg => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_engine => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_allow_list => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_allow_set => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_allow_list_multi_rows_multi_entries => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  wrap2_redis_allow_list_all => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  wrap2_redis_deny_list_ip_addr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_deny_set_ip_addr => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_deny_list_ipv4_netmask => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_deny_list_ipv4mappedv6_netmask => {
+    order => ++$order,
+    test_class => [qw(bug feature_ipv6 forking)],
+  },
+
+  wrap2_redis_deny_list_ipv6_netmask_bug3606 => {
+    order => ++$order,
+    test_class => [qw(bug feature_ipv6 forking)],
+  },
+
+  wrap2_redis_deny_list_dns_name => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  # Note: requires local modification to /etc/hosts, to add
+  # "familiar.castaglia.org" for 127.0.0.1.
+  wrap2_redis_deny_list_dns_domain_bug3558 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  wrap2_redis_user_lists => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_redis_group_lists => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_bug3341 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  wrap2_redis_opt_check_on_connect_bug3508 => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+  wrap2_allow_msg_bug3538 => {
+    order => ++$order,
+    test_class => [qw(forking)],
+  },
+
+  wrap2_allow_msg_anon_bug3538 => {
+    order => ++$order,
+    test_class => [qw(forking rootprivs)],
+  },
+
+  wrap2_redis_deny_event_exec_bug3209 => {
+    order => ++$order,
+    test_class => [qw(forking mod_exec)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  # Check for the required Perl modules:
+  #
+  #  Redis
+
+  my $required = [qw(
+    Redis
+  )];
+
+  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 provision_redis {
+  my $allowed_key = shift;
+  my $allowed = shift;
+  my $denied_key = shift;
+  my $denied = shift;
+  my $use_set = shift;
+  $use_set = 0 unless defined($use_set);
+
+  require Redis;
+  my $redis = Redis->new(
+    reconnect => 5,
+    every => 250_000
+  );
+
+  $redis->del($allowed_key, $denied_key);
+
+  if ($use_set) {
+    if (scalar(@$allowed) > 0) {
+      $redis->sadd($allowed_key, @$allowed);
+    }
+
+    if (scalar(@$denied) > 0) {
+      $redis->sadd($denied_key, @$denied);
+    }
+
+  } else {
+    if (scalar(@$allowed) > 0) {
+      $redis->lpush($allowed_key, @$allowed);
+    }
+
+    if (scalar(@$denied) > 0) {
+      $redis->lpush($denied_key, @$denied);
+    }
+  }
+
+  $redis->quit();
+  return 1;
+}
+
+sub wrap2_allow_msg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # XXX Create allow, deny lists in Redis, and populate them
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapAllowMsg => '"User %u allowed by access rules"',
+        WrapTables => "redis:/get-allowed-clients redis:/get-denied-clients",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd});
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg(0);
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} allowed by access rules";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_deny_msg {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapDenyMsg => '"User %u rejected by access rules"',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} rejected by access rules";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_engine {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'off',
+        WrapAllowMsg => '"User %u allowed by access rules"',
+        WrapDenyMsg => '"User %u rejected by access rules"',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_redis_allow_list {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, []);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+  }
+
+  provision_redis($allowed_key, [qw(127.0.0.1)], $denied_key, [qw(ALL)]);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    my ($resp_code, $resp_msg) = $client->login($setup->{user},
+      $setup->{passwd});
+    $client->quit();
+
+    my $expected;
+
+    $expected = 230;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "User $setup->{user} logged in";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_allow_set {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [], 1);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'redis:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/set:$allowed_key redis:/set:$denied_key",
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [qw(127.0.0.1)], $denied_key, [qw(ALL)], 1);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    my ($resp_code, $resp_msg) = $client->login($setup->{user},
+      $setup->{passwd});
+    $client->quit();
+
+    my $expected;
+
+    $expected = 230;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "User $setup->{user} logged in";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_allow_list_multi_rows_multi_entries {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+
+  my $allowed = [
+    '192.168.127.5, 192.168.127.6',
+    '192.168.127.1 192.168.127.2 127.0.0.1',
+    '192.168.127.3,192.168.127.4 127.0.0.1'
+  ];
+  provision_redis($allowed_key, $allowed, $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapLog => $setup->{log_file},
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_redis_allow_list_all {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [qw(ALL)], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'dns:10',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_redis_deny_list_ip_addr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, [qw(127.0.0.1)]);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    eval { $client->login($setup->{user}, $setup->{passwd}) };
+    unless ($@) {
+      die("Login succeeded unexpectedly");
+    }
+
+    my $resp_code = $client->response_code();
+    my $resp_msg = $client->response_msg();
+
+    my $expected;
+
+    $expected = 530;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "Access denied";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_deny_set_ip_addr {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)], 1);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/set:$allowed_key redis:/set:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, [qw(127.0.0.1)], 1);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    eval { $client->login($setup->{user}, $setup->{passwd}) };
+    unless ($@) {
+      die("Login succeeded unexpectedly");
+    }
+
+    my $resp_code = $client->response_code();
+    my $resp_msg = $client->response_msg();
+
+    my $expected;
+
+    $expected = 530;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "Access denied";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_deny_list_ipv4_netmask {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, [qw(127.0.0.0/255.255.255.0)]);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    eval { $client->login($setup->{user}, $setup->{passwd}) };
+    unless ($@) {
+      die("Login succeeded unexpectedly");
+    }
+
+    my $resp_code = $client->response_code();
+    my $resp_msg = $client->response_msg();
+
+    my $expected;
+
+    $expected = 530;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "Access denied";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+   };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_deny_list_ipv4mappedv6_netmask {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    UseIPv6 => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  # Note: this does NOT actually test the handling of IPv4-mapped IPv6 ACLs;
+  # the Net::FTP Perl module does not handle connecting to IPv6 addresses.
+  provision_redis($allowed_key, [], $denied_key, ['ALL, [::ffff:127.0.0.1]/32']);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+
+    eval { $client->login($setup->{user}, $setup->{passwd}) };
+    unless ($@) {
+      die("Login succeeded unexpectedly");
+    }
+
+    my $resp_code = $client->response_code();
+    my $resp_msg = $client->response_msg();
+
+    my $expected;
+
+    $expected = 530;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "Access denied";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_deny_list_ipv6_netmask_bug3606 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    DefaultAddress => '::1',
+    UseIPv6 => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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(2);
+
+      my $client = IO::Socket::INET6->new(
+        PeerAddr => '::1',
+        PeerPort => $port,
+        Proto => 'tcp',
+        Timeout => 5,
+      );
+      unless ($client) {
+        die("Can't connect to ::1: $!");
+      } 
+
+      # Read the banner
+      my $banner = <$client>;
+
+      # Send the USER command
+      my $cmd = "USER $setup->{user}\r\n";
+      $client->print($cmd);
+      $client->flush();
+
+      # Read USER response
+      my $resp = <$client>;
+
+      my $expected = "331 Password required for $setup->{user}\r\n";
+      $self->assert($expected eq $resp, "Expected '$expected', got '$resp'");
+ 
+      # Send the PASS command
+      $cmd = "PASS $setup->{passwd}\r\n";
+      $client->print($cmd);
+      $client->flush();
+
+      # Read PASS response
+      $resp = <$client>;
+
+      $expected = "530 Access denied\r\n";
+      $self->assert($expected eq $resp, "Expected '$expected', got '$resp'");
+ 
+      $client->close();
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, ['[::1]/32']);
+
+  eval {
+    sleep(2);
+
+    my $client = IO::Socket::INET6->new(
+      PeerAddr => '::1',
+      PeerPort => $port,
+      Proto => 'tcp',
+      Timeout => 5,
+    );
+    unless ($client) {
+      die("Can't connect to ::1: $!");
+    } 
+
+    # Read the banner
+    my $banner = <$client>;
+
+    # Send the USER command
+    my $cmd = "USER $setup->{user}\r\n";
+    $client->print($cmd);
+    $client->flush();
+
+    # Read USER response
+    my $resp = <$client>;
+
+    my $expected = "331 Password required for $setup->{user}\r\n";
+    $self->assert($expected eq $resp, "Expected '$expected', got '$resp'");
+ 
+    # Send the PASS command
+    $cmd = "PASS $setup->{passwd}\r\n";
+    $client->print($cmd);
+    $client->flush();
+
+    # Read PASS response
+    $resp = <$client>;
+
+    $expected = "530 Access denied\r\n";
+    $self->assert($expected eq $resp, "Expected '$expected', got '$resp'");
+ 
+    $client->close();
+  };
+
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_deny_list_dns_name {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+    UseReverseDNS => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, [qw(localhost)]);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    eval { $client->login($setup->{user}, $setup->{passwd}) };
+    unless ($@) {
+      die("Login succeeded unexpectedly");
+    }
+
+    my $resp_code = $client->response_code();
+    my $resp_msg = $client->response_msg();
+
+    my $expected;
+
+    $expected = 530;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "Access denied";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_deny_list_dns_domain_bug3558 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'dns:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    UseReverseDNS => 'on',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+        WrapOptions => 'CheckAllNames',
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, [qw(.castaglia.org)]);
+
+  eval {
+    my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+    eval { $client->login($setup->{user}, $setup->{passwd}) };
+    unless ($@) {
+      die("Login succeeded unexpectedly");
+    }
+
+    my $resp_code = $client->response_code();
+    my $resp_msg = $client->response_msg();
+
+    my $expected;
+
+    $expected = 530;
+    $self->assert($expected == $resp_code,
+      "Expected response code $expected, got $resp_code");
+
+    $expected = "Access denied";
+    $self->assert($expected eq $resp_msg,
+      "Expected response message '$expected', got '$resp_msg'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_redis_user_lists {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = "ftpdeny.$setup->{user}";
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+  $denied_key = 'ftpdeny.%{name}';
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'redis:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapUserTables => "!$setup->{user} redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  # Wait a little for the daemon to shutdown.
+  sleep(1);
+
+  if ($ex) {
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  # Modify the config a little
+  $config->{IfModules}->{'mod_wrap2_redis.c'}->{WrapUserTables} = "$setup->{user} redis:/list:$allowed_key redis:/list:$denied_key";
+
+  unlink($setup->{config_file});
+  ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Fork child
+  $self->handle_sigchld();
+  defined($pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_redis_group_lists {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = "ftpdeny.$setup->{group}";
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+  $denied_key = 'ftpdeny.%{name}';
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'redis:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapGroupTables => "foo redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+      $client->quit();
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  # Give the server time to shut down.
+  sleep(1);
+
+  if ($ex) {
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  # Modify the config a little
+  $config->{IfModules}->{'mod_wrap2_redis.c'}->{WrapGroupTables} = "$setup->{group} redis:/list:$allowed_key redis:/list:$denied_key";
+
+  unlink($setup->{config_file});
+  ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Fork child
+  $self->handle_sigchld();
+  defined($pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+      eval { $client->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_bug3341 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+
+  my $allowed = ['192.168.0.1,192.168.0.2 192.168.0.3, 192.168.0.4 127.0.0.1'];
+  provision_redis($allowed_key, $allowed, $denied_key, [qw(ALL)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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);
+
+      # As per Bug#3341, we need to send a bad password twice.  The second
+      # attempt triggered a segfault in mod_wrap2.
+      eval { $client->login($setup->{user}, 'foo') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg(); 
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+
+      # Now try to login again
+      eval { $client->login($setup->{user}, 'foo') };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      $resp_code = $client->response_code();
+      $resp_msg = $client->response_msg(); 
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Login incorrect.";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+
+      ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_redis_opt_check_on_connect_bug3508 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(127.0.0.2)]);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+        WrapOptions => 'CheckOnConnect',
+      },
+    },
+  };
+
+  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);
+      my ($resp_code, $resp_msg) = $client->login($setup->{user},
+        $setup->{passwd});
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} logged in";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  if ($ex) {
+    # Stop server
+    server_stop($setup->{pid_file});
+    $self->assert_child_ok($pid);
+
+    test_cleanup($setup->{log_file}, $ex);
+    return;
+  }
+
+  provision_redis($allowed_key, [], $denied_key, [qw(127.0.0.1)]);
+
+  eval {
+    my $client;
+
+    eval {
+      $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, undef, 2);
+    };
+    unless ($@) {
+      die("Connect succeeded unexpectedly");
+    }
+
+    my $ex = ProFTPD::TestSuite::FTP::get_connect_exception();
+
+    my $expected = "Access denied";
+    $self->assert($expected eq $ex, "Expected '$expected', got '$ex'");
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+sub wrap2_allow_msg_bug3538 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, []);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AccessGrantMsg => '"User %u logged in."',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapAllowMsg => '"User %u allowed by access rules"',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd});
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg(0);
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User $setup->{user} allowed by access rules";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+
+      $resp_msg = $client->response_msg(1);
+
+      $expected = "User $setup->{user} logged in.";
+      $self->assert($expected eq $resp_msg,
+        "Expected response meessage '$expected', got '$resp_msg'");
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_allow_msg_anon_bug3538 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, []);
+
+  my ($test_user, $test_group) = config_get_identity();
+  my $passwd = 'test';
+  my $home_dir = File::Spec->rel2abs($tmpdir);
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AccessGrantMsg => '"User %u logged in."',
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    Anonymous => {
+      $setup->{home_dir} => {
+        User => $test_user,
+        Group => $test_group,
+        UserAlias => "anonymous $test_user", 
+        RequireValidShell => 'off',
+      },
+    },
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapAllowMsg => '"User %u allowed by access rules"',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login('anonymous', 'ftp at nospam.org');
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg(0);
+
+      my $expected;
+
+      $expected = 230;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "User anonymous allowed by access rules";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+
+      $resp_msg = $client->response_msg(1);
+
+      $expected = "User anonymous logged in.";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+
+      $client->quit();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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 wrap2_redis_deny_event_exec_bug3209 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'wrap2');
+
+  # Create allow, deny lists in Redis, and populate them
+  my $allowed_key = 'ftpallow';
+  my $denied_key = 'ftpdeny';
+  provision_redis($allowed_key, [], $denied_key, [qw(ALL)]);
+
+  my $event_file = File::Spec->rel2abs("$tmpdir/denied-client.txt");
+  my $spawn_script = File::Spec->rel2abs("$tmpdir/spawn.sh");
+  if (open(my $fh, "> $spawn_script")) {
+    print $fh <<EOS;
+#!/bin/bash
+echo \$\@ > $event_file
+exit
+EOS
+    unless (close($fh)) {
+      die("Can't write $spawn_script: $!");
+    }
+
+    unless (chmod(0777, $spawn_script)) {
+      die("Can't set perms on $spawn_script to 0777: $!");
+    }
+
+  } else {
+    die("Can't open $spawn_script: $!");
+  }
+
+  my $timeout_idle = 30;
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'event:20',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    TimeoutIdle => $timeout_idle,
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_exec.c' => {
+        ExecEngine => 'on',
+        ExecLog => $setup->{log_file},
+        ExecTimeout => 1,
+        ExecOnEvent => "mod_wrap.connection-denied $spawn_script %a",
+      },
+
+      'mod_redis.c' => {
+        RedisEngine => 'on',
+        RedisServer => '127.0.0.1:6379',
+        RedisLog => $setup->{log_file},
+      },
+
+      'mod_wrap2_redis.c' => {
+        WrapEngine => 'on',
+        WrapTables => "redis:/list:$allowed_key redis:/list:$denied_key",
+        WrapLog => $setup->{log_file},
+      },
+    },
+  };
+
+  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->login($setup->{user}, $setup->{passwd}) };
+      unless ($@) {
+        die("Login succeeded unexpectedly");
+      }
+
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
+      my $expected;
+
+      $expected = 530;
+      $self->assert($expected == $resp_code,
+        "Expected response code $expected, got $resp_code");
+
+      $expected = "Access denied";
+      $self->assert($expected eq $resp_msg,
+        "Expected response message '$expected', got '$resp_msg'");
+
+      if (open(my $fh, "< $event_file")) {
+        my $line = <$fh>;
+        chomp($line);
+        close($fh);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDOUT "# line: $line\n";
+        }
+
+        my $expected = '127.0.0.1';
+        $self->assert($expected eq $line,
+          "Expected line '$expected', got '$line'");
+
+      } else {
+        die("Can't read $event_file: $!");
+      }
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh, $timeout_idle + 2) };
+    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;
diff --git a/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_sql.pm b/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_sql.pm
index 9bcbbfd..8fbc42b 100644
--- a/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_sql.pm
+++ b/tests/t/lib/ProFTPD/Tests/Modules/mod_wrap2_sql.pm
@@ -441,11 +441,11 @@ EOS
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user rejected by access rules";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -606,20 +606,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -777,20 +774,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -849,20 +843,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1025,20 +1016,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1200,20 +1188,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1373,27 +1358,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1453,27 +1434,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1633,7 +1610,6 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
@@ -1646,11 +1622,11 @@ EOS
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1710,7 +1686,6 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
@@ -1723,11 +1698,11 @@ EOS
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1889,7 +1864,6 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
@@ -1902,11 +1876,11 @@ EOS
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -1979,11 +1953,11 @@ EOS
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -2443,27 +2417,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -2523,27 +2493,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -2708,26 +2674,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -2787,26 +2750,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg();
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
 
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -2966,20 +2926,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -3019,27 +2976,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -3199,20 +3152,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -3252,27 +3202,23 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
       eval { $client->login($user, $passwd) };
       unless ($@) {
         die("Login succeeded unexpectedly");
-
-      } else {
-        $resp_code = $client->response_code();
-        $resp_msg = $client->response_msg();
       }
 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg();
+
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Access denied";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -3432,20 +3378,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -3606,8 +3549,6 @@ EOS
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
-      my ($resp_code, $resp_msg);
-
       # As per Bug#3341, we need to send a bad password twice.  The second
       # attempt triggered a segfault in mod_wrap2.
       eval { $client->login($user, 'foo') };
@@ -3615,18 +3556,18 @@ EOS
         die("Login succeeded unexpectedly");
       }
 
-      $resp_code = $client->response_code();
-      $resp_msg = $client->response_msg(); 
+      my $resp_code = $client->response_code();
+      my $resp_msg = $client->response_msg(); 
 
       my $expected;
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Login incorrect.";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       # Now try to login again
       eval { $client->login($user, 'foo') };
@@ -3639,21 +3580,21 @@ EOS
 
       $expected = 530;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "Login incorrect.";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
 
       ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
@@ -3851,20 +3792,17 @@ EOS
   if ($pid) {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
-
-      my ($resp_code, $resp_msg);
-
-      ($resp_code, $resp_msg) = $client->login($user, $passwd);
+      my ($resp_code, $resp_msg) = $client->login($user, $passwd);
 
       my $expected;
 
       $expected = 230;
       $self->assert($expected == $resp_code,
-        test_msg("Expected $expected, got $resp_code"));
+        test_msg("Expected response code $expected, got $resp_code"));
 
       $expected = "User $user logged in";
       $self->assert($expected eq $resp_msg,
-        test_msg("Expected '$expected', got '$resp_msg'"));
+        test_msg("Expected response message '$expected', got '$resp_msg'"));
     };
 
     if ($@) {
diff --git a/tests/t/lib/ProFTPD/Tests/SMTP.pm b/tests/t/lib/ProFTPD/Tests/SMTP.pm
index 732aef9..c2bf377 100644
--- a/tests/t/lib/ProFTPD/Tests/SMTP.pm
+++ b/tests/t/lib/ProFTPD/Tests/SMTP.pm
@@ -123,7 +123,7 @@ sub smtp_session {
     eval {
       sleep(1);
 
-      # To reproduce Bug#XXXX, we only need to connect to the server,
+      # To reproduce Bug#4143, we only need to connect to the server,
       # then issue an SMTP command.
 
       my $client = Net::SMTP->new('127.0.0.1',
diff --git a/tests/t/lib/ProFTPD/Tests/SSH2.pm b/tests/t/lib/ProFTPD/Tests/SSH2.pm
new file mode 100644
index 0000000..62a9eaa
--- /dev/null
+++ b/tests/t/lib/ProFTPD/Tests/SSH2.pm
@@ -0,0 +1,135 @@
+package ProFTPD::Tests::SSH2;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Spec;
+use IO::Handle;
+use Socket;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  ssh2_session => {
+    order => ++$order,
+    test_class => [qw(bug forking)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub ssh2_session {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'ssh2');
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+
+    IfModules => {
+      '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_opts = {
+        PeerHost => '127.0.0.1',
+        PeerPort => $port,
+        Proto => 'tcp',
+        Type => SOCK_STREAM,
+        Timeout => 10
+      };
+
+      my $client = IO::Socket::INET->new(%$client_opts);
+      unless ($client) {
+        die("Can't connect to 127.0.0.1:$port: $!");
+      }
+
+      my $banner = <$client>;
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Received banner: $banner";
+      }
+
+      my $req = "SSH-2.0-OpenSSH_5.6p1\r\n";
+      if ($ENV{TEST_VERBOSE}) {
+        print STDERR "# Sending request: $req";
+      }
+      $client->print($req);
+      $client->flush(); 
+
+      my $resp = <$client>;
+      if ($ENV{TEST_VEROBSE}) {
+        print STDERR "# Received response: $resp";
+      }
+      $client->close();
+
+      chomp($resp);
+      my $expected = '';
+      $self->assert(qr/$expected/, $resp,
+        test_msg("Expected response '$expected', got '$resp'"));
+    };
+
+    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;
diff --git a/tests/t/modules/mod_auth_otp.t b/tests/t/modules/mod_auth_otp.t
new file mode 100644
index 0000000..18219dc
--- /dev/null
+++ b/tests/t/modules/mod_auth_otp.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_auth_otp");
diff --git a/tests/t/modules/mod_auth_otp/sftp.t b/tests/t/modules/mod_auth_otp/sftp.t
new file mode 100644
index 0000000..91ac35e
--- /dev/null
+++ b/tests/t/modules/mod_auth_otp/sftp.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_auth_otp::sftp");
diff --git a/tests/t/modules/mod_digest.t b/tests/t/modules/mod_digest.t
new file mode 100644
index 0000000..2ab6f35
--- /dev/null
+++ b/tests/t/modules/mod_digest.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_digest");
diff --git a/tests/t/modules/mod_dynmasq.t b/tests/t/modules/mod_dynmasq.t
new file mode 100644
index 0000000..3ae2b33
--- /dev/null
+++ b/tests/t/modules/mod_dynmasq.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_dynmasq");
diff --git a/tests/t/modules/mod_geoip/sql.t b/tests/t/modules/mod_geoip/sql.t
new file mode 100644
index 0000000..e4e7b51
--- /dev/null
+++ b/tests/t/modules/mod_geoip/sql.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_geoip::sql");
diff --git a/tests/t/modules/mod_rlimit.t b/tests/t/modules/mod_rlimit.t
new file mode 100644
index 0000000..a5354e0
--- /dev/null
+++ b/tests/t/modules/mod_rlimit.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_rlimit");
diff --git a/tests/t/modules/mod_statcache.t b/tests/t/modules/mod_statcache.t
new file mode 100644
index 0000000..b542f65
--- /dev/null
+++ b/tests/t/modules/mod_statcache.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_statcache");
diff --git a/tests/t/modules/mod_tls_fscache.t b/tests/t/modules/mod_tls_fscache.t
new file mode 100644
index 0000000..3796eb4
--- /dev/null
+++ b/tests/t/modules/mod_tls_fscache.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_tls_fscache");
diff --git a/tests/t/modules/mod_wrap2_redis.t b/tests/t/modules/mod_wrap2_redis.t
new file mode 100644
index 0000000..d116aeb
--- /dev/null
+++ b/tests/t/modules/mod_wrap2_redis.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_wrap2_redis");
diff --git a/tests/t/ssh2.t b/tests/t/ssh2.t
new file mode 100644
index 0000000..877bdb3
--- /dev/null
+++ b/tests/t/ssh2.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::SSH2");
diff --git a/tests/tests.pl b/tests/tests.pl
index 4fe18dc..451d645 100644
--- a/tests/tests.pl
+++ b/tests/tests.pl
@@ -54,6 +54,9 @@ if (scalar(@ARGV) > 0) {
 
 } else {
   $test_files = [qw(
+    t/http.t
+    t/smtp.t
+    t/ssh2.t
     t/logins.t
     t/commands/user.t
     t/commands/pass.t
@@ -95,6 +98,7 @@ if (scalar(@ARGV) > 0) {
     t/commands/mfmt.t
     t/commands/opts.t
     t/commands/host.t
+    t/commands/clnt.t
     t/commands/site/chgrp.t
     t/commands/site/chmod.t
     t/config/accessdenymsg.t
@@ -136,6 +140,7 @@ if (scalar(@ARGV) > 0) {
     t/config/ifdefine.t
     t/config/include.t
     t/config/listoptions.t
+    t/config/loginpasswordprompt.t
     t/config/masqueradeaddress.t
     t/config/maxclients.t
     t/config/maxclientsperclass.t
@@ -145,8 +150,11 @@ if (scalar(@ARGV) > 0) {
     t/config/maxconnectionsperhost.t
     t/config/maxinstances.t
     t/config/maxloginattempts.t
+    t/config/maxpasswordsize.t
     t/config/maxretrievefilesize.t
     t/config/maxstorefilesize.t
+    t/config/maxtransfersperhost.t
+    t/config/maxtransfersperuser.t
     t/config/multilinerfc2228.t
     t/config/order.t
     t/config/passiveports.t
@@ -181,6 +189,7 @@ if (scalar(@ARGV) > 0) {
     t/config/userowner.t
     t/config/userpassword.t
     t/config/usesendfile.t
+    t/config/virtualhost.t
     t/config/directory/limits.t
     t/config/directory/umask.t
     t/config/ftpaccess/dele.t
@@ -218,6 +227,16 @@ if (scalar(@ARGV) > 0) {
       test_class => [qw(mod_auth_file)],
     },
 
+    't/modules/mod_auth_otp.t' => {
+      order => ++$order,
+      test_class => [qw(mod_auth_otp mod_sql mod_sql_sqlite)],
+    },
+
+    't/modules/mod_auth_otp/sftp.t' => {
+      order => ++$order,
+      test_class => [qw(mod_auth_otp mod_sftp mod_sql mod_sql_sqlite)],
+    },
+
     't/modules/mod_ban.t' => {
       order => ++$order,
       test_class => [qw(mod_ban)],
@@ -253,6 +272,16 @@ if (scalar(@ARGV) > 0) {
       test_class => [qw(mod_delay)],
     },
 
+    't/modules/mod_digest.t' => {
+      order => ++$order,
+      test_class => [qw(mod_digest)],
+    },
+
+    't/modules/mod_dynmasq.t' => {
+      order => ++$order,
+      test_class => [qw(mod_dynmasq)],
+    },
+
     't/modules/mod_exec.t' => {
       order => ++$order,
       test_class => [qw(mod_exec)],
@@ -263,6 +292,11 @@ if (scalar(@ARGV) > 0) {
       test_class => [qw(mod_geoip)],
     },
 
+    't/modules/mod_geoip/sql.t' => {
+      order => ++$order,
+      test_class => [qw(mod_geoip mod_sql mod_sql_sqlite)],
+    },
+
     't/modules/mod_ifversion.t' => {
       order => ++$order,
       test_class => [qw(mod_ifversion)],
@@ -319,6 +353,11 @@ if (scalar(@ARGV) > 0) {
       test_class => [qw(mod_rewrite)],
     },
 
+    't/modules/mod_rlimit.t' => {
+      order => ++$order,
+      test_class => [qw(mod_rlimit)],
+    },
+
     't/modules/mod_sftp.t' => {
       order => ++$order,
       test_class => [qw(mod_sftp)],
@@ -409,11 +448,21 @@ if (scalar(@ARGV) > 0) {
       test_class => [qw(mod_sql_sqlite)],
     },
 
+    't/modules/mod_statcache.t' => {
+      order => ++$order,
+      test_class => [qw(mod_statcache)],
+    },
+
     't/modules/mod_tls.t' => {
       order => ++$order,
       test_class => [qw(mod_tls)],
     },
 
+    't/modules/mod_tls_fscache.t' => {
+      order => ++$order,
+      test_class => [qw(mod_tls_fscache)],
+    },
+
     't/modules/mod_tls_memcache.t' => {
       order => ++$order,
       test_class => [qw(mod_tls_memcache)],
@@ -439,6 +488,11 @@ if (scalar(@ARGV) > 0) {
       test_class => [qw(mod_wrap2_file)],
     },
 
+    't/modules/mod_wrap2_redis.t' => {
+      order => ++$order,
+      test_class => [qw(mod_redis mod_wrap2_redis)],
+    },
+
     't/modules/mod_wrap2_sql.t' => {
       order => ++$order,
       test_class => [qw(mod_sql_sqlite mod_wrap2_sql)],
diff --git a/utils/Makefile.in b/utils/Makefile.in
index c3d705e..6925d35 100644
--- a/utils/Makefile.in
+++ b/utils/Makefile.in
@@ -7,7 +7,6 @@ VPATH=@srcdir@
 
 include ../Make.rules
 
-
 all:
 	@echo "Running make from top level directory."
 	cd ../; $(MAKE) all
@@ -22,8 +21,11 @@ Makefile: Makefile.in ../config.status
 utils: $(FTPCOUNT_OBJS) $(FTPSCRUB_OBJS) $(FTPSHUT_OBJS) $(FTPTOP_OBJS) $(FTPWHO_OBJS)
 
 clean:
-	rm -f *.o
+	$(RM) -f *.o
 
 depend:
 	$(MAKEDEPEND) $(CPPFLAGS) *.c 2>/dev/null
 	$(MAKEDEPEND) $(CPPFLAGS) -fMakefile.in *.c 2>/dev/null
+
+distclean: clean
+	-$(RM) *.gcda *.gcno
diff --git a/utils/ftpcount.c b/utils/ftpcount.c
index 12eb9dd..366f1b6 100644
--- a/utils/ftpcount.c
+++ b/utils/ftpcount.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,10 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Shows a count of "who" is online via proftpd.  Uses the scoreboard file.
- *
- * $Id: ftpcount.c,v 1.21 2011-05-23 20:46:20 castaglia Exp $
- */
+/* Shows a count of "who" is online via proftpd.  Uses the scoreboard file. */
 
 #include "utils.h"
 
@@ -78,10 +75,12 @@ static void show_usage(const char *progname, int exit_code) {
 #else /* HAVE_GETOPT_LONG */
     printf("  %s\n", h->short_opt);
 #endif
-    if (!h->desc)
+    if (h->desc == NULL) {
       printf("    display %s usage\n", progname);
-    else
+
+    } else {
       printf("    %s\n", h->desc);
+    }
   }
 
   exit(exit_code);
@@ -115,6 +114,7 @@ int main(int argc, char **argv) {
     switch (c) {
       case 'h':
         show_usage(progname, 0);
+        break;
 
       case 'f':
         util_set_scoreboard(optarg);
@@ -131,6 +131,7 @@ int main(int argc, char **argv) {
       case '?':
         fprintf(stderr, "unknown option: %c\n", (char) optopt);
         show_usage(progname, 1);
+        break;
     }
   }
 
@@ -181,13 +182,15 @@ int main(int argc, char **argv) {
   errno = 0;
   while ((score = util_scoreboard_entry_read()) != NULL) {
 
-    if (errno)
+    if (errno) {
       break;
+    }
 
     if (!count++ ||
         oldpid != mpid) {
-      if (total)
+      if (total) {
         printf("   -  %d user%s\n\n", total, total > 1 ? "s" : "");
+      }
 
       if (!mpid) {
         printf("inetd FTP connections:\n");
@@ -196,8 +199,9 @@ int main(int argc, char **argv) {
         printf("Master proftpd process %u:\n", (unsigned int) mpid);
       }
 
-      if (server_name)
+      if (server_name) {
         printf("ProFTPD Server '%s'\n", server_name);
+      }
 
       oldpid = mpid;
       total = 0;
@@ -229,11 +233,16 @@ int main(int argc, char **argv) {
   /* Print out the total. */
   if (total) {
     for (i = 0; i != MAX_CLASSES; i++) {
-      if (classes[i].score_class == 0)
-         break;
+      if (classes[i].score_class == NULL) {
+        break;
+      }
 
       printf("Service class %-20s - %3lu %s\n", classes[i].score_class,
-         classes[i].score_count, classes[i].score_count > 1 ? "users" : "user");
+        classes[i].score_count, classes[i].score_count > 1 ? "users" : "user");
+
+      /* Free up the memory, now that we're done with it. */
+      free(classes[i].score_class);
+      classes[i].score_class = NULL;
     }
 
   } else {
diff --git a/utils/ftpscrub.c b/utils/ftpscrub.c
index 0ffac6f..f13ab3f 100644
--- a/utils/ftpscrub.c
+++ b/utils/ftpscrub.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * the source distribution.
  */
 
-/* "Scrubs" the scoreboard file, clearing it of old/stale entries.
- * $Id: ftpscrub.c,v 1.3 2011-05-23 20:46:20 castaglia Exp $
- */
+/* "Scrubs" the scoreboard file, clearing it of old/stale entries. */
 
 #include "utils.h"
 
@@ -33,8 +31,9 @@ static const char *config_filename = PR_CONFIG_FILE_PATH;
 static int check_scoreboard_file(void) {
   struct stat st;
 
-  if (stat(util_get_scoreboard(), &st) < 0)
+  if (stat(util_get_scoreboard(), &st) < 0) {
     return -1;
+  }
 
   return 0;
 }
@@ -69,10 +68,12 @@ static void show_usage(const char *progname, int exit_code) {
 #else /* HAVE_GETOPT_LONG */
     printf("  %s\n", h->short_opt);
 #endif
-    if (!h->desc)
+    if (h->desc == NULL) {
       printf("    display %s usage\n", progname);
-    else
+
+    } else {
       printf("    %s\n", h->desc);
+    }
   }
 
   exit(exit_code);
@@ -99,6 +100,7 @@ int main(int argc, char **argv) {
     switch (c) {
       case 'h':
         show_usage(progname, 0);
+        break;
 
       case 'f':
         util_set_scoreboard(optarg);
@@ -115,6 +117,7 @@ int main(int argc, char **argv) {
       case '?':
         fprintf(stderr, "unknown option: %c\n", (char) optopt);
         show_usage(progname, 1);
+        break;
     }
   }
 
diff --git a/utils/ftpshut.8.in b/utils/ftpshut.8.in
index 0342ab3..a4ac71b 100644
--- a/utils/ftpshut.8.in
+++ b/utils/ftpshut.8.in
@@ -49,12 +49,12 @@ is used, this will be the only operation performed by
 .TP
 .B -l min
 Specifies the number of minutes before shutdown that new ftp access will
-be disabled.  If the -l option is not specified, a default of 10 minutes
+be disabled.  If the \-l option is not specified, a default of 10 minutes
 (or immediately, if less than 10 minutes remains until shutdown) is used.
 .TP
 .B -d min
 Specifies the number of minutes before shutdown that existing ftp
-connections will be disconnected.  If the -d option is not specified,
+connections will be disconnected.  If the \-d option is not specified,
 a default of 5 minutes (or immediately, if less than 5 minutes remains
 until shutdown) is used.
 .TP
diff --git a/utils/ftpshut.c b/utils/ftpshut.c
index 546448e..609f241 100644
--- a/utils/ftpshut.c
+++ b/utils/ftpshut.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,8 +25,6 @@
 
 /* Simple utility to create the proftpd shutdown message file, allowing
  * an admin to configure the shutdown, deny, disconnect times and messages.
- *
- * $Id: ftpshut.c,v 1.10 2013-02-15 22:39:01 castaglia Exp $
  */
 
 #include "conf.h"
@@ -43,8 +41,10 @@ static int isnumeric(char *str) {
     str++;
   }
 
-  if (!str || !*str)
+  if (str == NULL ||
+      !*str) {
     return 0;
+  }
 
   for (; str && *str; str++) {
     if (!PR_ISDIGIT(*str)) {
@@ -88,16 +88,19 @@ int main(int argc, char *argv[]) {
 	  show_usage(progname);
         }
 
-        if (c == 'd')
+        if (c == 'd') {
 	  disc = atoi(optarg);
 
-        else if (c == 'l')
+        } else if (c == 'l') {
 	  deny = atoi(optarg);
+        }
 
         break;
 
       case '?':
         fprintf(stderr, "%s: unknown option '%c'\n", progname, (char)optopt);
+        show_usage(progname);
+        break;
 
       case 'h':
       default:
@@ -106,28 +109,31 @@ int main(int argc, char *argv[]) {
   }
 
   /* Everything left on the command line is the message */
-  if (optind >= argc)
+  if (optind >= argc) {
     show_usage(progname);
+  }
 
   shut = argv[optind++];
 
-  if(optind < argc)
+  if (optind < argc) {
     msg = argv[optind];
-  else
+
+  } else {
     msg = "going down at %s";
+  }
 
   time(&now);
   tm = localtime(&now);
 
   /* shut must be either 'now', '+number' or 'HHMM' */
-  if(strcasecmp(shut,"now") != 0) {
-    if(*shut == '+') {
+  if (strcasecmp(shut,"now") != 0) {
+    if (*shut == '+') {
       shut++;
       while (shut && *shut && PR_ISSPACE(*shut)) shut++;
 
       if (!isnumeric(shut)) {
-	fprintf(stderr, "%s: Invalid time interval specified.\n", progname);
-	show_usage(progname);
+        fprintf(stderr, "%s: Invalid time interval specified.\n", progname);
+        show_usage(progname);
       }
 
       now += (60 * atoi(shut));
@@ -135,31 +141,29 @@ int main(int argc, char *argv[]) {
 
     } else {
       if ((strlen(shut) != 4 && strlen(shut) != 2) || !isnumeric(shut)) {
-	fprintf(stderr, "%s: Invalid time interval specified.\n", progname);
-	show_usage(progname);
+        fprintf(stderr, "%s: Invalid time interval specified.\n", progname);
+        show_usage(progname);
       }
 
-      if(strlen(shut) > 2) {
+      if (strlen(shut) > 2) {
         mn = atoi((shut + strlen(shut) - 2));
-
-	if(mn > 59) {
-	  fprintf(stderr, "%s: Invalid time interval specified.\n",
-		  progname);
-	  show_usage(progname);
-	}
+        if (mn > 59) {
+          fprintf(stderr, "%s: Invalid time interval specified.\n", progname);
+          show_usage(progname);
+        }
 
         *(shut + strlen(shut) - 2) = '\0';
       }
 
       hr = atoi(shut);
-
-      if(hr > 23) {
-	fprintf(stderr, "%s: Invalid time interval specified.\n",
-		progname);
-	show_usage(progname);
+      if (hr > 23) {
+        fprintf(stderr, "%s: Invalid time interval specified.\n", progname);
+        show_usage(progname);
       }
 
-      if(hr < tm->tm_hour || (hr == tm->tm_hour && mn <= tm->tm_min)) {
+      if (hr < tm->tm_hour ||
+          (hr == tm->tm_hour &&
+           mn <= tm->tm_min)) {
         now += 86400;		/* one day forward */
         tm = localtime(&now);
       }
@@ -169,19 +173,21 @@ int main(int argc, char *argv[]) {
   }
 
   umask(022);
-  if ((outf = fopen(PR_SHUTMSG_PATH, "w")) == NULL) {
+
+  outf = fopen(PR_SHUTMSG_PATH, "w");
+  if (outf == NULL) {
     fprintf(stderr,"%s: error opening '" PR_SHUTMSG_PATH "': %s\n", progname,
       strerror(errno));
     exit(1);
   }
 
-  fprintf(outf,"%d %d %d %d %d %d",
+  fprintf(outf, "%d %d %d %d %d %d",
           tm->tm_year+1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
           tm->tm_min, tm->tm_sec);
-  fprintf(outf," %02d%02d %02d%02d\n",
-          (deny / 60),(deny % 60),
-          (disc / 60),(disc % 60));
-  fprintf(outf,"%s\n",msg);
+  fprintf(outf, " %02d%02d %02d%02d\n",
+          (deny / 60), (deny % 60),
+          (disc / 60), (disc % 60));
+  fprintf(outf, "%s\n", msg);
   fclose(outf);
   return 0;
 }
diff --git a/utils/ftptop.c b/utils/ftptop.c
index b8f773a..61c6069 100644
--- a/utils/ftptop.c
+++ b/utils/ftptop.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - ftptop: a utility for monitoring proftpd sessions
  * Copyright (c) 2000-2002 TJ Saunders <tj at castaglia.org>
- * Copyright (c) 2003-2013 The ProFTPD Project team
+ * Copyright (c) 2003-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,11 +25,9 @@
 
 /* Shows who is online via proftpd, in a manner similar to top.  Uses the
  * scoreboard files.
- *
- * $Id: ftptop.c,v 1.45 2013-03-08 16:25:27 castaglia Exp $
  */
 
-#define FTPTOP_VERSION "ftptop/0.9"
+#define FTPTOP_VERSION 		"ftptop/0.9"
 
 #include "utils.h"
 
@@ -181,7 +179,7 @@ ascii:
   }
 
   while (str[i] &&
-         nchars < max_chars) {
+         (size_t) nchars < max_chars) {
     size_t len;
 
     if (str[i] > 0) {
@@ -310,6 +308,11 @@ static void process_opts(int argc, char *argv[]) {
         break;
 
       case 'S':
+        if (server_name != NULL) {
+          free(server_name);
+          server_name = NULL;
+        }
+
         server_name = strdup(optarg);
         break;
 
@@ -411,9 +414,10 @@ static void read_scoreboard(void) {
     char *status = "A";
 
     /* If a ServerName was given, skip unless the scoreboard entry matches. */
-    if (server_name &&
-        strcmp(server_name, score->sce_server_label) != 0)
+    if (server_name != NULL &&
+        strcmp(server_name, score->sce_server_label) != 0) {
       continue;
+    }
 
     /* Clear the buffer for this run. */
     memset(buf, '\0', sizeof(buf));
diff --git a/utils/ftpwho.1.in b/utils/ftpwho.1.in
index 45f05b1..7a7caa0 100644
--- a/utils/ftpwho.1.in
+++ b/utils/ftpwho.1.in
@@ -30,10 +30,9 @@ must either be recompiled, or this option must be used in order to find
 proftpd's scoreboard.
 .TP
 .BI \-o,\--outform " format"
-Specify an output format.  Currently, two "formats" are supported: \fBcompat\fP
-and \fBoneline\fP.  The \fBcompat\fP format is used for backward compatibility.
-The \fBoneline\fP format causes all of the fields for a session to be displayed
-on a single line, for ease of line-based searches (e.g. grep).
+Specify an output format.  Currently, three formats are supported: \fBcompat\fP,\fBoneline\fP and \fBjson\fP.  The \fBcompat\fP format is used for backward
+compatibility.  The \fBoneline\fP format causes all of the fields for a session
+to be displayed on a single line, for ease of line-based searches (e.g. grep).  The \fBjson\fP format emits all of the information as a JSON object.
 .TP
 .B \-v,\--verbose
 Reports additional information for each connection, such as the remote
diff --git a/utils/ftpwho.c b/utils/ftpwho.c
index abb164c..4b3553f 100644
--- a/utils/ftpwho.c
+++ b/utils/ftpwho.c
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,12 +24,10 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Shows a count of "who" is online via proftpd.  Uses the scoreboard file.
- *
- * $Id: ftpwho.c,v 1.31 2011-11-16 22:30:09 castaglia Exp $
- */
+/* Shows a count of "who" is online via proftpd.  Uses the scoreboard file. */
 
 #include "utils.h"
+#include "ccan-json.h"
 
 #define MAX_CLASSES 100
 struct scoreboard_class {
@@ -39,6 +37,7 @@ struct scoreboard_class {
 
 #define OF_COMPAT		0x001
 #define OF_ONELINE		0x002
+#define OF_JSON			0x004
 
 static const char *config_filename = PR_CONFIG_FILE_PATH;
 
@@ -65,31 +64,212 @@ static char *percent_complete(off_t size, off_t done) {
   return sbuf;
 }
 
-static const char *show_time(time_t *i) {
+static JsonNode *get_server_json(void) {
+  JsonNode *server;
+  pid_t daemon_pid;
+  time_t daemon_uptime;
+
+  server = json_mkobject();
+
+  daemon_pid = util_scoreboard_get_daemon_pid();
+  if (daemon_pid != 0) {
+    json_append_member(server, "server_type", json_mkstring("standalone"));
+    json_append_member(server, "pid", json_mknumber((double) daemon_pid));
+
+  } else {
+    json_append_member(server, "server_type", json_mkstring("inetd"));
+  }
+
+  daemon_uptime = util_scoreboard_get_daemon_uptime();
+  json_append_member(server, "started_ms",
+    json_mknumber(((double) (daemon_uptime * 1000L))));
+
+  return server;
+}
+
+static JsonNode *get_conns_json(void) {
+  JsonNode *conns;
+  pr_scoreboard_entry_t *score = NULL;
+
+  conns = json_mkarray();
+
+  while ((score = util_scoreboard_entry_read()) != NULL) {
+    JsonNode *conn;
+    int authenticating = FALSE, downloading = FALSE, uploading = FALSE;
+
+    conn = json_mkobject();
+
+    json_append_member(conn, "pid", json_mknumber((double) score->sce_pid));
+    json_append_member(conn, "connected_since_ms",
+      json_mknumber(((double) (score->sce_begin_session * 1000L))));
+    json_append_member(conn, "remote_name",
+      json_mkstring(score->sce_client_name));
+    json_append_member(conn, "remote_address",
+      json_mkstring(score->sce_client_addr));
+
+    if (score->sce_server_addr[0]) {
+      char *ptr, server_addr[80];
+
+      /* Trim off the port portion of the server_addr field; we report that
+       * separately.
+       */
+      memset(server_addr, '\0', sizeof(server_addr));
+
+      ptr = strrchr(score->sce_server_addr, ':');
+      if (ptr != NULL) {
+        memcpy(server_addr, score->sce_server_addr,
+          (ptr - score->sce_server_addr));
+      } else {
+        memcpy(server_addr, score->sce_server_addr, sizeof(server_addr)-1);
+      }
+
+      json_append_member(conn, "local_address", json_mkstring(server_addr));
+    }
+
+    json_append_member(conn, "local_port",
+      json_mknumber((double) score->sce_server_port));
+
+    if (strcmp(score->sce_user, "(none)") == 0) {
+      authenticating = TRUE;
+    }
+
+    if (authenticating) {
+      json_append_member(conn, "authenticating", json_mkbool(TRUE));
+
+    } else {
+      json_append_member(conn, "user", json_mkstring(score->sce_user));
+    }
+
+    if (score->sce_class[0]) {
+      json_append_member(conn, "class", json_mkstring(score->sce_class));
+    }
+
+    if (score->sce_protocol[0]) {
+      json_append_member(conn, "protocol", json_mkstring(score->sce_protocol));
+    }
+
+    if (score->sce_cwd[0]) {
+      json_append_member(conn, "location", json_mkstring(score->sce_cwd));
+    }
+
+    if (score->sce_cmd[0]) {
+      if (strcmp(score->sce_cmd, "idle") == 0) {
+        json_append_member(conn, "idling", json_mkbool(TRUE));
+
+        if (!authenticating) {
+          json_append_member(conn, "idle_since_ms",
+            json_mknumber(((double) (score->sce_begin_idle * 1000L))));
+        }
+
+      } else {
+        json_append_member(conn, "command", json_mkstring(score->sce_cmd));
+
+        if (score->sce_cmd_arg[0]) {
+          json_append_member(conn, "command_args",
+            json_mkstring(score->sce_cmd_arg));
+        }
+      }
+
+    } else {
+      json_append_member(conn, "idling", json_mkbool(TRUE));
+
+      if (!authenticating) {
+        json_append_member(conn, "idle_since_ms",
+          json_mknumber(((double) (score->sce_begin_idle * 1000L))));
+      }
+    }
+
+    if (strncmp(score->sce_cmd, "RETR", 5) == 0 ||
+        strncmp(score->sce_cmd, "READ", 5) == 0 ||
+        strcmp(score->sce_cmd, "scp download") == 0) {
+      downloading = TRUE;
+
+    } else {
+      if (strncmp(score->sce_cmd, "STOR", 5) == 0 ||
+          strncmp(score->sce_cmd, "STOU", 5) == 0 ||
+          strncmp(score->sce_cmd, "APPE", 5) == 0 ||
+          strncmp(score->sce_cmd, "WRITE", 6) == 0 ||
+          strcmp(score->sce_cmd, "scp upload") == 0) {
+        uploading = TRUE;
+      }
+    }
+
+    if (downloading) {
+      json_append_member(conn, "downloading", json_mkbool(TRUE));
+      json_append_member(conn, "transfer_completed",
+        json_mkstring(percent_complete(score->sce_xfer_size,
+          score->sce_xfer_done)));
+    }
+
+    if (uploading) {
+      json_append_member(conn, "uploading", json_mkbool(TRUE));
+    }
+
+    if (score->sce_xfer_done > 0) {
+      json_append_member(conn, "transfer_bytes",
+        json_mknumber((double) score->sce_xfer_done));
+    }
+ 
+    if (score->sce_xfer_elapsed > 0) {
+      json_append_member(conn, "transfer_duration_ms",
+        json_mknumber(((double) (score->sce_xfer_elapsed * 1000L))));
+    }
+
+    json_append_element(conns, conn);
+  }
+
+  return conns;
+}
+
+static JsonNode *get_json(void) {
+  JsonNode *json, *server = NULL, *conns = NULL;
+
+  server = get_server_json();
+  conns = get_conns_json();
+  json = json_mkobject();
+
+  if (server != NULL) {
+    json_append_member(json, "server", server);
+  }
+
+  if (conns != NULL) {
+    json_append_member(json, "connections", conns);
+  }
+
+  return json;
+}
+
+static const char *strtime(time_t *then) {
   time_t now = time(NULL);
-  unsigned long l;
-  static char sbuf[7];
+  unsigned long since;
+  static char time_str[32];
 
-  if (!i || !*i)
+  if (then == NULL ||
+      *then == 0) {
     return "-";
+  }
 
-  memset(sbuf, '\0', sizeof(sbuf));
-  l = now - *i;
+  memset(time_str, '\0', sizeof(time_str));
+  since = now - *then;
 
-  if (l < 3600)
-    snprintf(sbuf, sizeof(sbuf), "%lum%lus",(l / 60),(l % 60));
-  else
-    snprintf(sbuf, sizeof(sbuf), "%luh%lum",(l / 3600),
-    ((l - (l / 3600) * 3600) / 60));
+  if (since < 3600) {
+    snprintf(time_str, sizeof(time_str)-1, "%lum%lus", (since / 60),
+      (since % 60));
 
-  return sbuf;
+  } else {
+    snprintf(time_str, sizeof(time_str)-1, "%luh%lum", (since / 3600),
+      ((since - (since / 3600) * 3600) / 60));
+  }
+
+  return time_str;
 }
 
 static int check_scoreboard_file(void) {
   struct stat st;
 
-  if (stat(util_get_scoreboard(), &st) < 0)
+  if (stat(util_get_scoreboard(), &st) < 0) {
     return -1;
+  }
 
   return 0;
 }
@@ -103,9 +283,9 @@ static const char *show_uptime(time_t uptime_since) {
   memset(buf, '\0', sizeof(buf));
 
   updays = (int) uptime_secs / (60 * 60 * 24);
-
-  if (updays)
+  if (updays > 0) {
     pos += sprintf(buf + pos, "%d day%s, ", updays, (updays != 1) ? "s" : "");
+  }
 
   upminutes = (int) uptime_secs / 60;
 
@@ -159,7 +339,7 @@ static void show_usage(const char *progname, int exit_code) {
 #else /* HAVE_GETOPT_LONG */
     printf("  %s\n", h->short_opt);
 #endif
-    if (!h->desc) {
+    if (h->desc == NULL) {
       printf("    display %s usage\n", progname);
 
     } else {
@@ -200,6 +380,7 @@ int main(int argc, char **argv) {
     switch (c) {
       case 'h':
         show_usage(progname, 0);
+        break;
 
       case 'v':
         verbose = TRUE;
@@ -215,13 +396,17 @@ int main(int argc, char **argv) {
 
       case 'o':
         /* Check the given outform parameter. */
-        if (strcmp(optarg, "compat") == 0) {
+        if (strcasecmp(optarg, "compat") == 0) {
           outform |= OF_COMPAT;
           break;
 
-        } else if (strcmp(optarg, "oneline") == 0) {
+        } else if (strcasecmp(optarg, "oneline") == 0) {
           outform |= OF_ONELINE;
           break;
+
+        } else if (strcasecmp(optarg, "json") == 0) {
+          outform = OF_JSON;
+          break;
         }
 
         fprintf(stderr, "unknown outform value: '%s'\n", optarg);
@@ -234,6 +419,7 @@ int main(int argc, char **argv) {
       case '?':
         fprintf(stderr, "unknown option: %c\n", (char) optopt);
         show_usage(progname, 1);
+        break;
     }
   }
 
@@ -244,7 +430,7 @@ int main(int argc, char **argv) {
     char *path;
 
     path = util_scan_config(config_filename, "ScoreboardFile");
-    if (path) {
+    if (path != NULL) {
       util_set_scoreboard(path);
       free(path);
     }
@@ -252,13 +438,13 @@ int main(int argc, char **argv) {
     if (check_scoreboard_file() < 0) {
       fprintf(stderr, "%s: %s\n", util_get_scoreboard(), strerror(errno));
       fprintf(stderr, "(Perhaps you need to specify the ScoreboardFile with -f, or change\n");
-      fprintf(stderr," the compile-time default directory?)\n");
+      fprintf(stderr, " the compile-time default directory?)\n");
       exit(1);
     }
   }
 
-  count = 0;
-  if ((res = util_open_scoreboard(O_RDONLY)) < 0) {
+  res = util_open_scoreboard(O_RDONLY);
+  if (res < 0) {
     switch (res) {
       case -1:
         fprintf(stderr, "unable to open scoreboard: %s\n", strerror(errno));
@@ -278,8 +464,30 @@ int main(int argc, char **argv) {
     }
   }
 
+  if (outform == OF_JSON) {
+    JsonNode *json;
+
+    json = get_json();
+    if (json != NULL) {
+      char *json_str;
+
+      json_str = json_stringify(json, "  ");
+      fprintf(stdout, "%s\n", json_str);
+      free(json_str);
+      json_delete(json);
+    }
+
+    if (server_name) {
+      free(server_name);
+      server_name = NULL;
+    }
+
+    return 0;
+  }
+
   mpid = util_scoreboard_get_daemon_pid();
   uptime = util_scoreboard_get_daemon_uptime();
+  count = 0;
 
   if (!mpid) {
     printf("inetd FTP daemon:\n");
@@ -289,22 +497,24 @@ int main(int argc, char **argv) {
       show_uptime(uptime));
   }
 
-  if (server_name)
+  if (server_name) {
     printf("ProFTPD Server '%s'\n", server_name);
+  }
 
   while ((score = util_scoreboard_entry_read()) != NULL) {
     int downloading = FALSE, uploading = FALSE;
     register unsigned int i = 0;
 
     /* If a ServerName was given, skip unless the scoreboard entry matches. */
-    if (server_name &&
-        strcmp(server_name, score->sce_server_label) != 0)
+    if (server_name != NULL &&
+        strcmp(server_name, score->sce_server_label) != 0) {
       continue;
+    }
 
     if (!count++) {
-      if (total)
+      if (total) {
         printf("   -  %d user%s\n\n", total, total > 1 ? "s" : "");
-
+      }
       total = 0;
     }
 
@@ -324,16 +534,16 @@ int main(int argc, char **argv) {
 
     total++;
 
-    if (strcmp(score->sce_cmd, "RETR") == 0 ||
-        strcmp(score->sce_cmd, "READ") == 0 ||
+    if (strncmp(score->sce_cmd, "RETR", 5) == 0 ||
+        strncmp(score->sce_cmd, "READ", 5) == 0 ||
         strcmp(score->sce_cmd, "scp download") == 0) {
       downloading = TRUE;
 
     } else {
-      if (strcmp(score->sce_cmd, "STOR") == 0 ||
-          strcmp(score->sce_cmd, "STOU") == 0 ||
-          strcmp(score->sce_cmd, "APPE") == 0 ||
-          strcmp(score->sce_cmd, "WRITE") == 0 ||
+      if (strncmp(score->sce_cmd, "STOR", 5) == 0 ||
+          strncmp(score->sce_cmd, "STOU", 5) == 0 ||
+          strncmp(score->sce_cmd, "APPE", 5) == 0 ||
+          strncmp(score->sce_cmd, "WRITE", 6) == 0 ||
           strcmp(score->sce_cmd, "scp upload") == 0) {
         uploading = TRUE;
       }
@@ -344,19 +554,19 @@ int main(int argc, char **argv) {
           score->sce_xfer_size > 0) {
         if (downloading) {
           printf("%5d %-6s (%s%%) %s %s\n", (int) score->sce_pid,
-            show_time(&score->sce_begin_idle),
+            strtime(&score->sce_begin_idle),
             percent_complete(score->sce_xfer_size, score->sce_xfer_done),
             score->sce_cmd, score->sce_cmd_arg);
 
         } else {
           printf("%5d %-6s (n/a) %s %s\n", (int) score->sce_pid,
-            show_time(&score->sce_begin_idle), score->sce_cmd,
+            strtime(&score->sce_begin_idle), score->sce_cmd,
             score->sce_cmd_arg);
         }
 
       } else {
         printf("%5d %-6s %s %s\n", (int) score->sce_pid,
-          show_time(&score->sce_begin_idle), score->sce_cmd,
+          strtime(&score->sce_begin_idle), score->sce_cmd,
           score->sce_cmd_arg);
       }
 
@@ -386,29 +596,31 @@ int main(int argc, char **argv) {
     if (strcmp(score->sce_user, "(none)")) {
 
       /* Is the client idle? */
-      if (strcmp(score->sce_cmd, "idle") == 0) {
+      if (strncmp(score->sce_cmd, "idle", 5) == 0) {
 
-        /* These printf() calls needs to be split up, as show_time() returns
+        /* These printf() calls needs to be split up, as strtime() returns
          * a pointer to a static buffer, and pushing two invocations onto
          * the stack means that the times thus formatted will be incorrect.
          */
         printf("%5d %-8s [%6s] ", (int) score->sce_pid,
-          score->sce_user, show_time(&score->sce_begin_session));
-        printf("%6s %s", show_time(&score->sce_begin_idle), score->sce_cmd);
+          score->sce_user, strtime(&score->sce_begin_session));
+        printf("%6s %s", strtime(&score->sce_begin_idle), score->sce_cmd);
 
-        if (verbose && !(outform & OF_ONELINE))
+        if (verbose &&
+            !(outform & OF_ONELINE)) {
           printf("\n");
+        }
 
       } else {
         if (downloading) {
           printf("%5d %-8s [%6s] (%3s%%) %s %s", (int) score->sce_pid,
-            score->sce_user, show_time(&score->sce_begin_session),
+            score->sce_user, strtime(&score->sce_begin_session),
             percent_complete(score->sce_xfer_size, score->sce_xfer_done),
             score->sce_cmd, score->sce_cmd_arg);
 
         } else {
           printf("%5d %-8s [%6s] (n/a) %s %s", (int) score->sce_pid,
-            score->sce_user, show_time(&score->sce_begin_session),
+            score->sce_user, strtime(&score->sce_begin_session),
             score->sce_cmd, score->sce_cmd_arg);
         }
 
@@ -464,9 +676,8 @@ int main(int argc, char **argv) {
       }
 
     } else {
-
       printf("%5d %-8s [%6s] (authenticating)", (int) score->sce_pid,
-        score->sce_user, show_time(&score->sce_begin_session));
+        score->sce_user, strtime(&score->sce_begin_session));
 
       /* Display additional information, if requested. */
       if (verbose) {
@@ -507,11 +718,12 @@ int main(int argc, char **argv) {
     register unsigned int i = 0;
 
     for (i = 0; i != MAX_CLASSES; i++) {
-      if (classes[i].score_class == 0)
-         break;
+      if (classes[i].score_class == 0) {
+        break;
+      }
 
-       printf("Service class %-20s - %3lu user%s\n", classes[i].score_class,
-         classes[i].score_count, classes[i].score_count > 1 ? "s" : "");
+      printf("Service class %-20s - %3lu user%s\n", classes[i].score_class,
+        classes[i].score_count, classes[i].score_count > 1 ? "s" : "");
     }
 
   } else {
diff --git a/utils/misc.c b/utils/misc.c
index ae2d538..880138d 100644
--- a/utils/misc.c
+++ b/utils/misc.c
@@ -1,7 +1,7 @@
 /*
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@
 
 /* Utility module linked to utilities to provide functions normally
  * present in full src tree.
- * $Id: misc.c,v 1.11 2013-02-15 22:39:01 castaglia Exp $
  */
 
 #include "utils.h"
diff --git a/utils/scoreboard.c b/utils/scoreboard.c
index eb38350..53fae0d 100644
--- a/utils/scoreboard.c
+++ b/utils/scoreboard.c
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - FTP server daemon
- * Copyright (c) 2001-2011 The ProFTPD Project team
+ * Copyright (c) 2001-2016 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,7 @@
  * OpenSSL in the source distribution.
  */
 
-/* ProFTPD scoreboard support (modified for use by external utilities).
- * $Id: scoreboard.c,v 1.17 2011-05-23 20:46:20 castaglia Exp $
- */
+/* ProFTPD scoreboard support (modified for use by external utilities). */
 
 #include "utils.h"
 
@@ -123,10 +121,11 @@ int util_close_scoreboard(void) {
   if (util_scoreboard_fd == -1)
     return 0;
 
-  if (util_scoreboard_read_locked)
+  if (util_scoreboard_read_locked) {
     unlock_scoreboard();
+  }
 
-  close(util_scoreboard_fd);
+  (void) close(util_scoreboard_fd);
   util_scoreboard_fd = -1;
 
   return 0;
@@ -264,7 +263,7 @@ pr_scoreboard_entry_t *util_scoreboard_entry_read(void) {
 }
 
 int util_scoreboard_scrub(int verbose) {
-  int fd = -1;
+  int fd = -1, res = 0;
   off_t curr_offset = 0;
   struct flock lock;
   pr_scoreboard_entry_t sce;
@@ -278,6 +277,10 @@ int util_scoreboard_scrub(int verbose) {
    */
   fd = open(util_get_scoreboard(), O_RDWR);
   if (fd < 0) {
+    if (verbose) {
+      fprintf(stdout, "error opening scoreboard: %s", strerror(errno));
+    }
+
     return -1;
   }
 
@@ -290,15 +293,40 @@ int util_scoreboard_scrub(int verbose) {
 
   /* We can afford to block/wait until we obtain our lock on the file. */
   while (fcntl(fd, F_SETLKW, &lock) < 0) {
-    if (errno == EINTR) {
+    int xerrno = errno;
+
+    if (xerrno == EINTR) {
       continue;
     }
 
+    (void) close(fd);
+    errno = xerrno;
     return -1;
   }
 
   /* Skip past the scoreboard header. */
   curr_offset = lseek(fd, (off_t) sizeof(pr_scoreboard_header_t), SEEK_SET);
+  if (curr_offset < 0) {
+    int xerrno = errno;
+
+    /* Release the scoreboard. */
+    lock.l_type = F_UNLCK;
+    lock.l_whence = SEEK_SET;
+    lock.l_start = 0;
+    lock.l_len = 0;
+
+    while (fcntl(fd, F_SETLKW, &lock) < 0) {
+      if (errno == EINTR) {
+        continue;
+      }
+    }
+
+    /* Don't need the descriptor anymore. */
+    (void) close(fd);
+
+    errno = xerrno;
+    return -1;
+  }
 
   memset(&sce, 0, sizeof(sce));
 
@@ -340,6 +368,10 @@ int util_scoreboard_scrub(int verbose) {
 
     /* Mark the current offset. */
     curr_offset = lseek(fd, (off_t) 0, SEEK_CUR);
+    if (curr_offset < 0) {
+      res = -1;
+      break;
+    }
   }
 
   /* Release the scoreboard. */
@@ -357,5 +389,5 @@ int util_scoreboard_scrub(int verbose) {
   /* Don't need the descriptor anymore. */
   (void) close(fd);
 
-  return 0;
+  return res;
 }
diff --git a/utils/utils.h b/utils/utils.h
index ef8dab8..f6073d0 100644
--- a/utils/utils.h
+++ b/utils/utils.h
@@ -2,7 +2,7 @@
  * ProFTPD - FTP server daemon
  * Copyright (c) 1997, 1998 Public Flood Software
  * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver at tos.net>
- * Copyright (c) 2001-2013 The ProFTPD Project team
+ * Copyright (c) 2001-2015 The ProFTPD Project team
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,10 +24,7 @@
  * the source code for OpenSSL in the source distribution.
  */
 
-/* Scoreboard routines.
- *
- * $Id: utils.h,v 1.29 2013-02-15 22:33:23 castaglia Exp $
- */
+/* Utility scoreboard routines. */
 
 #ifndef UTILS_UTILS_H
 #define UTILS_UTILS_H
@@ -74,6 +71,7 @@
 # include <sys/stat.h>
 #endif
 
+#include "pool.h"
 #include "ascii.h"
 #include "default_paths.h"
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-proftpd/proftpd-dfsg.git



More information about the Pkg-proftpd-maintainers mailing list